Files
eventify_command_center/src/features/settings/components/tabs/StaffDirectory.tsx

204 lines
13 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Department, Squad, StaffMember, SCOPE_DEFINITIONS } from '@/lib/types/staff';
import { getStaff, getDepartments, getSquads, updateStaffRole, moveStaff, deactivateStaff } from '@/lib/actions/staff-management';
import { getEffectiveScopes } from '@/lib/auth/permissions';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { InviteStaffDialog } from '../dialogs/InviteStaffDialog';
import { toast } from 'sonner';
import { Loader2, Search, MoreHorizontal, UserCog, ArrowRightLeft, UserX, Crown, Shield, ShieldCheck, Eye } from 'lucide-react';
export function StaffDirectory() {
const [staff, setStaff] = useState<StaffMember[]>([]);
const [departments, setDepartments] = useState<Department[]>([]);
const [squads, setSquads] = useState<Squad[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [filterDept, setFilterDept] = useState('all');
const [filterRole, setFilterRole] = useState('all');
const loadData = async () => {
setLoading(true);
try {
const [staffRes, deptRes, sqRes] = await Promise.all([getStaff(), getDepartments(), getSquads()]);
if (staffRes.success) setStaff(staffRes.data);
if (deptRes.success) setDepartments(deptRes.data);
if (sqRes.success) setSquads(sqRes.data);
} finally { setLoading(false); }
};
useEffect(() => { loadData(); }, []);
const getDeptName = (id: string | null) => departments.find(d => d.id === id)?.name || '—';
const getDeptColor = (id: string | null) => departments.find(d => d.id === id)?.color || '#666';
const getSquadName = (id: string | null) => squads.find(s => s.id === id)?.name || '—';
const handleRoleChange = async (staffId: string, newRole: 'MANAGER' | 'MEMBER') => {
const res = await updateStaffRole(staffId, newRole);
if (res.success) { toast.success(res.message); loadData(); }
else toast.error(res.message);
};
const handleMove = async (staffId: string, newSquadId: string) => {
const res = await moveStaff(staffId, newSquadId);
if (res.success) { toast.success(res.message); loadData(); }
else toast.error(res.message);
};
const handleDeactivate = async (staffId: string) => {
const res = await deactivateStaff(staffId);
if (res.success) { toast.success('Staff deactivated'); loadData(); }
else toast.error(res.message);
};
const filtered = staff.filter(s => {
if (search && !s.name.toLowerCase().includes(search.toLowerCase()) && !s.email.toLowerCase().includes(search.toLowerCase())) return false;
if (filterDept !== 'all' && s.departmentId !== filterDept) return false;
if (filterRole !== 'all' && s.role !== filterRole) return false;
return true;
});
if (loading) {
return (
<div className="flex items-center justify-center py-20 text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin mr-2" /> Loading staff directory...
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold">Staff Directory</h2>
<p className="text-sm text-muted-foreground">{staff.filter(s => s.status === 'active').length} active, {staff.filter(s => s.status === 'invited').length} invited</p>
</div>
<InviteStaffDialog departments={departments} squads={squads} onInvited={loadData} />
</div>
{/* Filters */}
<div className="flex items-center gap-3">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
<Input placeholder="Search by name or email..." value={search} onChange={e => setSearch(e.target.value)} className="pl-9" />
</div>
<Select value={filterDept} onValueChange={setFilterDept}>
<SelectTrigger className="w-[180px]"><SelectValue placeholder="Department" /></SelectTrigger>
<SelectContent>
<SelectItem value="all">All Departments</SelectItem>
{departments.map(d => <SelectItem key={d.id} value={d.id}>{d.name}</SelectItem>)}
</SelectContent>
</Select>
<Select value={filterRole} onValueChange={setFilterRole}>
<SelectTrigger className="w-[140px]"><SelectValue placeholder="Role" /></SelectTrigger>
<SelectContent>
<SelectItem value="all">All Roles</SelectItem>
<SelectItem value="SUPER_ADMIN">Super Admin</SelectItem>
<SelectItem value="MANAGER">Manager</SelectItem>
<SelectItem value="MEMBER">Member</SelectItem>
</SelectContent>
</Select>
</div>
{/* Table */}
<Card>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b text-left text-xs uppercase tracking-wider text-muted-foreground">
<th className="px-4 py-3">Name</th>
<th className="px-4 py-3">Email</th>
<th className="px-4 py-3">Department</th>
<th className="px-4 py-3">Squad</th>
<th className="px-4 py-3">Role</th>
<th className="px-4 py-3">Status</th>
<th className="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y">
{filtered.map(member => (
<tr key={member.id} className="hover:bg-muted/30 transition-colors">
<td className="px-4 py-3">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-primary/30 to-primary/5 flex items-center justify-center text-xs font-bold text-primary">
{member.name.split(' ').map(n => n[0]).join('')}
</div>
<span className="font-medium text-sm">{member.name}</span>
</div>
</td>
<td className="px-4 py-3 text-sm text-muted-foreground">{member.email}</td>
<td className="px-4 py-3">
{member.departmentId ? (
<Badge variant="outline" className="text-[11px] gap-1" style={{ borderColor: getDeptColor(member.departmentId) + '60', color: getDeptColor(member.departmentId) }}>
{getDeptName(member.departmentId)}
</Badge>
) : <span className="text-sm text-muted-foreground"></span>}
</td>
<td className="px-4 py-3 text-sm">{getSquadName(member.squadId)}</td>
<td className="px-4 py-3">
{member.role === 'SUPER_ADMIN' && <Badge className="bg-red-500/10 text-red-600 border-red-200 text-[10px]"><ShieldCheck className="h-3 w-3 mr-1" />Super Admin</Badge>}
{member.role === 'MANAGER' && <Badge className="bg-amber-500/10 text-amber-600 border-amber-200 text-[10px]"><Crown className="h-3 w-3 mr-1" />Manager</Badge>}
{member.role === 'MEMBER' && <Badge variant="outline" className="text-[10px]">Member</Badge>}
</td>
<td className="px-4 py-3">
{member.status === 'active' && <Badge variant="outline" className="text-[10px] border-emerald-300 text-emerald-600">Active</Badge>}
{member.status === 'invited' && <Badge variant="outline" className="text-[10px] border-sky-300 text-sky-600">Invited</Badge>}
{member.status === 'deactivated' && <Badge variant="outline" className="text-[10px] border-red-300 text-red-500">Deactivated</Badge>}
</td>
<td className="px-4 py-3 text-right">
{member.role !== 'SUPER_ADMIN' && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Manage</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleRoleChange(member.id, member.role === 'MANAGER' ? 'MEMBER' : 'MANAGER')}>
<UserCog className="h-4 w-4 mr-2" />
{member.role === 'MANAGER' ? 'Demote to Member' : 'Promote to Manager'}
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<ArrowRightLeft className="h-4 w-4 mr-2" /> Move to Squad
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{squads.filter(s => s.id !== member.squadId).map(sq => (
<DropdownMenuItem key={sq.id} onClick={() => handleMove(member.id, sq.id)}>
{sq.name}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
{member.status !== 'deactivated' && (
<DropdownMenuItem className="text-destructive" onClick={() => handleDeactivate(member.id)}>
<UserX className="h-4 w-4 mr-2" /> Deactivate
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</td>
</tr>
))}
{filtered.length === 0 && (
<tr><td colSpan={7} className="text-center py-8 text-muted-foreground">No staff found.</td></tr>
)}
</tbody>
</table>
</div>
</Card>
</div>
);
}