| |
| |
| |
| |
|
|
| 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); |
| |
| |
| 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); |
| }; |
|
|
| |
| 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> |
| ); |
| } |
|
|