File size: 15,948 Bytes
46463e1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 | /**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import {
Users,
UserCheck,
Lock,
Shield,
Trash2,
UserPlus,
Fingerprint,
CheckCircle2,
XCircle,
ToggleLeft,
KeyRound,
Sliders,
Settings
} from 'lucide-react';
import { StaffUser, StaffRole } from '../types';
interface StaffManagerProps {
staffList: StaffUser[];
currentStaff: StaffUser;
onSetCurrentStaff: (staff: StaffUser) => void;
onAddStaffMember: (user: StaffUser) => void;
onToggleStaffStatus: (staffId: string) => void;
onUpdateStaffRole: (staffId: string, role: StaffRole) => void;
}
export default function StaffManager({
staffList,
currentStaff,
onSetCurrentStaff,
onAddStaffMember,
onToggleStaffStatus,
onUpdateStaffRole
}: StaffManagerProps) {
const [showAddStaff, setShowAddStaff] = useState(false);
// Create staff states
const [newName, setNewName] = useState('');
const [newEmail, setNewEmail] = useState('');
const [newRole, setNewRole] = useState<StaffRole>('cashier');
const isOwner = currentStaff.role === 'owner';
const isManagerOrOwner = currentStaff.role === 'owner' || currentStaff.role === 'manager';
const handleCreateStaff = (e: React.FormEvent) => {
e.preventDefault();
if (!newName.trim() || !newEmail.trim()) {
alert("Name and email references are mandatory.");
return;
}
if (staffList.some(s => s.email.toLowerCase() === newEmail.toLowerCase())) {
alert("An account is already configured under this email register.");
return;
}
const newUser: StaffUser = {
id: 'staff_' + Math.random().toString(36).substr(2, 9),
name: newName,
email: newEmail,
role: newRole,
active: true
};
onAddStaffMember(newUser);
setNewName('');
setNewEmail('');
setNewRole('cashier');
setShowAddStaff(false);
};
const handleSwitchOperator = (user: StaffUser) => {
if (!user.active) {
alert("Cannot switch to inactive staff profile. Owner must restore authorization status first.");
return;
}
onSetCurrentStaff(user);
};
// Role details mapping for easy visual display
const getRoleBadgeColor = (role: StaffRole) => {
switch(role) {
case 'owner': return 'bg-slate-900 text-white border-transparent';
case 'manager': return 'bg-slate-100 text-slate-800 border-slate-200';
case 'cashier': return 'bg-slate-50 text-slate-650 border-slate-200/60';
}
};
return (
<div className="space-y-6" id="role-vault-cockpit">
{/* Visual Identity Widget - Active privileges indicator */}
<div className="bg-white border border-slate-200 rounded-xl p-5 flex flex-col md:flex-row items-start md:items-center justify-between gap-6 shadow-sm">
<div className="flex items-center gap-4">
<div className="p-3.5 bg-slate-900 text-white rounded-lg shadow-sm">
<Fingerprint className="w-6 h-6 text-slate-100" />
</div>
<div className="space-y-1">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold block">Authorized Terminal Cashier</span>
<div className="flex items-center gap-2">
<h3 className="text-base font-bold text-slate-900 leading-none">{currentStaff.name}</h3>
<span className={`text-[9px] font-bold px-2 py-0.5 border rounded-md capitalize ${getRoleBadgeColor(currentStaff.role)}`}>
{currentStaff.role} MODE
</span>
</div>
<p className="text-xs text-slate-400">Security ledger email: {currentStaff.email}</p>
</div>
</div>
<div className="bg-slate-50 border border-slate-200/80 p-3.5 rounded-lg space-y-1 md:max-w-xs text-xs font-sans">
<span className="font-bold text-slate-800 flex items-center gap-1.5 uppercase tracking-wider text-[10px]">
<Shield className="w-3.5 h-3.5 text-slate-755" />
Enforced Role Clearances
</span>
<p className="text-slate-550 leading-relaxed text-[11px]">
{currentStaff.role === 'owner' ? "Owner clearance active. Unlimited access of warehouse multipliers, revenue parameters, and full backups." :
currentStaff.role === 'manager' ? "Manager clearance active. Full access to manual stock adjusters, inventory updates, and cashier rosters." :
"Cashier clearance active. Invoice generation, payments checkout, and outreach promotions. Stock manual modifications locked."}
</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* LEFT COLUMN: Switch Active Terminal Operator Block */}
<div className="lg:col-span-4 bg-white border border-slate-200 rounded-xl shadow-sm p-5 space-y-4">
<div className="space-y-1 border-b border-slate-100 pb-3">
<h3 className="text-xs font-bold text-slate-950 uppercase flex items-center gap-1.5">
<KeyRound className="w-4 h-4 text-slate-705" />
Switch Terminal Operator
</h3>
<p className="text-xs text-slate-400">Review security flows by logging into alternative staff portfolios</p>
</div>
<div className="space-y-2.5">
{staffList.map((user) => {
const isActiveLocal = user.id === currentStaff.id;
return (
<div
key={user.id}
onClick={() => handleSwitchOperator(user)}
className={`border p-3 rounded-lg flex items-center justify-between transition cursor-pointer ${
!user.active ? 'bg-slate-50 border-slate-100 opacity-55 cursor-not-allowed' :
isActiveLocal
? 'bg-slate-900 text-white border-transparent shadow-sm font-semibold'
: 'bg-white hover:border-slate-350 hover:bg-slate-50 border-slate-200'
}`}
>
<div className="space-y-0.5">
<p className={`text-xs ${isActiveLocal ? 'text-white' : 'text-slate-900'} font-bold`}>{user.name}</p>
<span className={`text-[10px] capitalize block ${isActiveLocal ? 'text-slate-300 font-medium' : 'text-slate-400'}`}>
{user.role} • <span>{user.active ? 'Authorized' : 'Suspended'}</span>
</span>
</div>
{isActiveLocal ? (
<div className="bg-white/10 p-1 rounded-md text-white">
<UserCheck className="w-4 h-4" />
</div>
) : (
<span className="text-[10px] text-slate-800 font-semibold underline hover:no-underline">Log in</span>
)}
</div>
);
})}
</div>
</div>
{/* RIGHT COLUMN: Full Staff registers with Bounded Controls */}
<div className="lg:col-span-8 bg-white border border-slate-200 rounded-xl shadow-sm p-5 space-y-4">
<div className="flex items-center justify-between border-b border-slate-100 pb-3">
<div className="space-y-1">
<h3 className="text-xs font-bold text-slate-950 uppercase flex items-center gap-1.5">
<Users className="w-4 h-4 text-slate-705" />
Staff Directory & RBAC Registers
</h3>
<p className="text-xs text-slate-400">Edit employee details, adjust authorization levels, and configure status settings</p>
</div>
{isManagerOrOwner && (
<button
type="button"
onClick={() => setShowAddStaff(true)}
className="bg-slate-900 hover:bg-slate-800 text-white font-medium text-xs px-3 py-2 rounded-lg transition shadow-sm flex items-center gap-1.5 cursor-pointer"
>
<UserPlus className="w-4 h-4" />
Register Staff
</button>
)}
</div>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse text-xs font-sans">
<thead className="bg-slate-50 font-bold uppercase text-[9px] tracking-widest text-slate-500 leading-none">
<tr className="border-b border-slate-150">
<th className="py-3 px-4">Employee Card</th>
<th className="py-3 px-4">System Access Tier</th>
<th className="py-3 px-4 text-center">Duty Status</th>
<th className="py-3 px-4 text-right">Administrative overrides</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 align-middle">
{staffList.map((user) => (
<tr key={user.id} className={!user.active ? 'opacity-60 bg-slate-50/50' : ''}>
<td className="py-3.5 px-4">
<div className="space-y-0.5">
<p className="font-bold text-slate-900">{user.name}</p>
<p className="text-[10px] text-slate-500">{user.email}</p>
</div>
</td>
<td className="py-3.5 px-4 font-sans font-bold">
{isOwner && user.id !== currentStaff.id ? (
<select
value={user.role}
onChange={(e) => onUpdateStaffRole(user.id, e.target.value as StaffRole)}
className="border border-slate-200 bg-white p-1 rounded font-sans text-xs outline-none"
>
<option value="owner">Owner</option>
<option value="manager">Manager</option>
<option value="cashier">Cashier</option>
</select>
) : (
<span className={`text-[9px] font-bold px-2 py-0.5 border rounded-full capitalize ${getRoleBadgeColor(user.role)}`}>
{user.role}
</span>
)}
</td>
<td className="py-3.5 px-4 text-center">
<div className="flex items-center justify-center gap-1">
{user.active ? (
<span className="bg-emerald-50 text-emerald-800 border border-emerald-100 text-[10px] px-2 py-0.5 rounded-full font-semibold flex items-center gap-1">
<CheckCircle2 className="w-3 h-3 text-emerald-600" />
Active
</span>
) : (
<span className="bg-red-50 text-red-800 border border-red-100 text-[10px] px-2 py-0.5 rounded-full font-semibold flex items-center gap-1">
<XCircle className="w-3 h-3 text-red-500 animate-pulse" />
Suspended
</span>
)}
</div>
</td>
<td className="py-3.5 px-4 text-right">
{user.id === currentStaff.id ? (
<span className="text-[10px] text-slate-400 italic">Operating account</span>
) : isManagerOrOwner ? (
<button
type="button"
onClick={() => onToggleStaffStatus(user.id)}
className={`text-xs font-semibold px-3 py-1 rounded-lg transition border cursor-pointer ${
user.active
? 'text-red-600 bg-red-50 hover:bg-red-100 border-red-150'
: 'text-emerald-700 bg-emerald-50 hover:bg-emerald-100 border-emerald-150'
}`}
>
{user.active ? 'Deauthorize' : 'Authorize Duty'}
</button>
) : (
<span className="text-[10px] text-slate-400 italic flex items-center gap-1 justify-end">
<Lock className="w-3.5 h-3.5" /> Locked
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* CORE MODAL: REGISTER STAFF MEMBER */}
{showAddStaff && (
<div className="fixed inset-0 bg-slate-900/40 backdrop-blur-[2px] z-50 flex items-center justify-center p-4">
<div className="bg-white rounded-xl border border-slate-200 max-w-md w-full p-6 space-y-5 relative shadow-xl">
<button
onClick={() => setShowAddStaff(false)}
className="absolute top-4 right-4 bg-slate-50 hover:bg-slate-150 text-slate-400 hover:text-slate-800 font-bold px-2.5 py-1 rounded text-xs cursor-pointer"
>
✕
</button>
<div className="pb-2 border-b border-slate-100">
<h3 className="text-sm font-bold text-slate-900 uppercase">Register New Terminal Account</h3>
<p className="text-xs text-slate-400">Provide credentials and role permission clearings</p>
</div>
<form onSubmit={handleCreateStaff} className="space-y-4 text-xs font-sans">
<div className="space-y-1">
<label className="text-[10px] text-slate-500 font-bold uppercase">Name / Identification</label>
<input
type="text"
required
placeholder="e.g. Zain Brother"
value={newName}
onChange={(e) => setNewName(e.target.value)}
className="w-full border border-slate-200 bg-white px-3 py-2 rounded-lg outline-none focus:border-slate-800 transition"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] text-slate-500 font-bold uppercase">Secure Email register</label>
<input
type="email"
required
placeholder="e.g. zain@traders.com"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
className="w-full border border-slate-200 bg-white px-3 py-2 rounded-lg outline-none focus:border-slate-800 transition"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] text-slate-500 font-bold uppercase">Assigned Security Role Clearance</label>
<select
value={newRole}
onChange={(e) => setNewRole(e.target.value as StaffRole)}
className="w-full border border-slate-200 bg-white px-3 py-2 rounded-lg outline-none focus:border-slate-800 transition text-xs"
>
<option value="cashier">Cashier clearance level (Draft bills & Payments)</option>
<option value="manager">Manager clearance level (Adjustments & Inventory config)</option>
<option value="owner">Owner credentials clearance (Total configuration overrides)</option>
</select>
</div>
<button
type="submit"
className="w-full bg-slate-900 hover:bg-slate-800 text-white font-medium py-2.5 rounded-lg shadow-sm transition cursor-pointer text-center"
>
Register Employee Account
</button>
</form>
</div>
</div>
)}
</div>
);
}
|