feat(Makefile): patch Layout.jsx queryKey for local development feat(frontend-web): mock base44 client for local development with role switching feat(frontend-web): add event assignment modal with conflict detection and bulk assign feat(frontend-web): add client dashboard with key metrics and quick actions feat(frontend-web): add layout component with role-based navigation feat(frontend-web): update various pages to use "@/components" alias feat(frontend-web): update create event page with ai assistant toggle feat(frontend-web): update dashboard page with new components feat(frontend-web): update events page with quick assign popover feat(frontend-web): update invite vendor page with hover card feat(frontend-web): update messages page with conversation list and message thread feat(frontend-web): update operator dashboard page with new components feat(frontend-web): update partner management page with new components feat(frontend-web): update permissions page with new components feat(frontend-web): update procurement dashboard page with new components feat(frontend-web): update smart vendor onboarding page with new components feat(frontend-web): update staff directory page with new components feat(frontend-web): update teams page with new components feat(frontend-web): update user management page with new components feat(frontend-web): update vendor compliance page with new components feat(frontend-web): update main.jsx to include react query provider feat: add vendor marketplace page feat: add global import fix to prepare-export script feat: add patch-layout-query-key script to fix query key feat: update patch-base44-client script to use a more robust method
307 lines
14 KiB
JavaScript
307 lines
14 KiB
JavaScript
import React, { useState } from "react";
|
|
import { base44 } from "@/api/base44Client";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { Link } from "react-router-dom";
|
|
import { createPageUrl } from "@/utils";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { UserPlus, Users, LayoutGrid, List as ListIcon, Phone, MapPin, Calendar, Star } from "lucide-react";
|
|
import FilterBar from "@/components/staff/FilterBar";
|
|
import StaffCard from "@/components/staff/StaffCard";
|
|
import EmployeeCard from "@/components/staff/EmployeeCard";
|
|
import PageHeader from "@/components/common/PageHeader";
|
|
|
|
export default function StaffDirectory() {
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [departmentFilter, setDepartmentFilter] = useState("all");
|
|
const [locationFilter, setLocationFilter] = useState("all");
|
|
const [viewMode, setViewMode] = useState("grid"); // "grid" or "list"
|
|
|
|
const { data: user } = useQuery({
|
|
queryKey: ['current-user'],
|
|
queryFn: () => base44.auth.me(),
|
|
});
|
|
|
|
const { data: staff, isLoading } = useQuery({
|
|
queryKey: ['staff'],
|
|
queryFn: () => base44.entities.Staff.list('-created_date'),
|
|
initialData: [],
|
|
});
|
|
|
|
const { data: events } = useQuery({
|
|
queryKey: ['events-for-staff-filter'],
|
|
queryFn: () => base44.entities.Event.list(),
|
|
initialData: [],
|
|
enabled: !!user
|
|
});
|
|
|
|
const visibleStaff = React.useMemo(() => {
|
|
const userRole = user?.user_role || user?.role;
|
|
|
|
if (['admin', 'procurement'].includes(userRole)) {
|
|
return staff;
|
|
}
|
|
|
|
if (['operator', 'sector'].includes(userRole)) {
|
|
return staff;
|
|
}
|
|
|
|
if (userRole === 'vendor') {
|
|
return staff.filter(s =>
|
|
s.vendor_id === user?.id ||
|
|
s.vendor_name === user?.company_name ||
|
|
s.created_by === user?.email
|
|
);
|
|
}
|
|
|
|
if (userRole === 'client') {
|
|
const clientEvents = events.filter(e =>
|
|
e.client_email === user?.email ||
|
|
e.business_name === user?.company_name ||
|
|
e.created_by === user?.email
|
|
);
|
|
|
|
const assignedStaffIds = new Set();
|
|
clientEvents.forEach(event => {
|
|
if (event.assigned_staff) {
|
|
event.assigned_staff.forEach(assignment => {
|
|
if (assignment.staff_id) {
|
|
assignedStaffIds.add(assignment.staff_id);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return staff.filter(s => assignedStaffIds.has(s.id));
|
|
}
|
|
|
|
if (userRole === 'workforce') {
|
|
return staff;
|
|
}
|
|
|
|
return staff;
|
|
}, [staff, user, events]);
|
|
|
|
const uniqueDepartments = [...new Set(visibleStaff.map(s => s.department).filter(Boolean))];
|
|
const uniqueLocations = [...new Set(visibleStaff.map(s => s.hub_location).filter(Boolean))];
|
|
|
|
const filteredStaff = visibleStaff.filter(member => {
|
|
const matchesSearch = !searchTerm ||
|
|
member.employee_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
member.position?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
member.manager?.toLowerCase().includes(searchTerm.toLowerCase());
|
|
|
|
const matchesDepartment = departmentFilter === "all" || member.department === departmentFilter;
|
|
const matchesLocation = locationFilter === "all" || member.hub_location === locationFilter;
|
|
|
|
return matchesSearch && matchesDepartment && matchesLocation;
|
|
});
|
|
|
|
const canAddStaff = ['admin', 'procurement', 'operator', 'sector', 'vendor'].includes(user?.user_role || user?.role);
|
|
|
|
const getPageTitle = () => {
|
|
const userRole = user?.user_role || user?.role;
|
|
if (userRole === 'vendor') return "My Staff Directory";
|
|
if (userRole === 'client') return "Event Staff Directory";
|
|
if (userRole === 'workforce') return "Team Directory";
|
|
return "Staff Directory";
|
|
};
|
|
|
|
const getPageSubtitle = () => {
|
|
const userRole = user?.user_role || user?.role;
|
|
if (userRole === 'vendor') return `${filteredStaff.length} of your staff members`;
|
|
if (userRole === 'client') return `${filteredStaff.length} staff assigned to your events`;
|
|
if (userRole === 'workforce') return `${filteredStaff.length} team members`;
|
|
return `${filteredStaff.length} ${filteredStaff.length === 1 ? 'member' : 'members'} found`;
|
|
};
|
|
|
|
const getCoverageColor = (percentage) => {
|
|
if (!percentage) return "bg-red-100 text-red-700";
|
|
if (percentage >= 90) return "bg-green-100 text-green-700";
|
|
if (percentage >= 50) return "bg-yellow-100 text-yellow-700";
|
|
return "bg-red-100 text-red-700";
|
|
};
|
|
|
|
return (
|
|
<div className="p-4 md:p-8">
|
|
<div className="max-w-7xl mx-auto">
|
|
<PageHeader
|
|
title={getPageTitle()}
|
|
subtitle={getPageSubtitle()}
|
|
actions={
|
|
canAddStaff ? (
|
|
<Link to={createPageUrl("AddStaff")}>
|
|
<Button className="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white shadow-lg">
|
|
<UserPlus className="w-5 h-5 mr-2" />
|
|
Add New Staff
|
|
</Button>
|
|
</Link>
|
|
) : null
|
|
}
|
|
/>
|
|
|
|
<div className="mb-6">
|
|
<Card className="border-slate-200">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-4 mb-4">
|
|
<div className="flex-1">
|
|
<FilterBar
|
|
searchTerm={searchTerm}
|
|
setSearchTerm={setSearchTerm}
|
|
departmentFilter={departmentFilter}
|
|
setDepartmentFilter={setDepartmentFilter}
|
|
locationFilter={locationFilter}
|
|
setLocationFilter={setLocationFilter}
|
|
departments={uniqueDepartments}
|
|
locations={uniqueLocations}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1 bg-slate-100 p-1 rounded-lg">
|
|
<Button
|
|
size="sm"
|
|
variant={viewMode === "grid" ? "default" : "ghost"}
|
|
onClick={() => setViewMode("grid")}
|
|
className={viewMode === "grid" ? "bg-[#0A39DF] hover:bg-[#0A39DF]/90" : "hover:bg-white"}
|
|
>
|
|
<LayoutGrid className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant={viewMode === "list" ? "default" : "ghost"}
|
|
onClick={() => setViewMode("list")}
|
|
className={viewMode === "list" ? "bg-[#0A39DF] hover:bg-[#0A39DF]/90" : "hover:bg-white"}
|
|
>
|
|
<ListIcon className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{[...Array(9)].map((_, i) => (
|
|
<div key={i} className="h-64 bg-slate-100 animate-pulse rounded-xl" />
|
|
))}
|
|
</div>
|
|
) : filteredStaff.length > 0 ? (
|
|
<>
|
|
{viewMode === "grid" && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{filteredStaff.map((member) => (
|
|
<EmployeeCard key={member.id} staff={member} />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{viewMode === "list" && (
|
|
<Card className="border-slate-200 shadow-lg">
|
|
<CardContent className="p-0">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead className="bg-slate-50 border-b-2 border-slate-200">
|
|
<tr>
|
|
<th className="text-left py-4 px-4 font-semibold text-sm text-slate-700">Employee</th>
|
|
<th className="text-left py-4 px-4 font-semibold text-sm text-slate-700">Primary Skill</th>
|
|
<th className="text-left py-4 px-4 font-semibold text-sm text-slate-700">Secondary Skill</th>
|
|
<th className="text-center py-4 px-4 font-semibold text-sm text-slate-700">Rating</th>
|
|
<th className="text-center py-4 px-4 font-semibold text-sm text-slate-700">Reliability</th>
|
|
<th className="text-center py-4 px-4 font-semibold text-sm text-slate-700">Coverage</th>
|
|
<th className="text-center py-4 px-4 font-semibold text-sm text-slate-700">Cancellations</th>
|
|
<th className="text-left py-4 px-4 font-semibold text-sm text-slate-700">Manager</th>
|
|
<th className="text-left py-4 px-4 font-semibold text-sm text-slate-700">Location</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filteredStaff.map((member) => (
|
|
<tr key={member.id} className="border-b border-slate-100 hover:bg-slate-50 transition-colors">
|
|
<td className="py-4 px-4">
|
|
<Link to={createPageUrl(`EditStaff?id=${member.id}`)} className="flex items-center gap-3 hover:text-[#0A39DF]">
|
|
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-700 rounded-full flex items-center justify-center text-white font-bold text-sm">
|
|
{member.employee_name?.charAt(0) || '?'}
|
|
</div>
|
|
<div>
|
|
<p className="font-semibold text-[#1C323E]">{member.employee_name}</p>
|
|
{member.contact_number && (
|
|
<p className="text-xs text-slate-500 flex items-center gap-1">
|
|
<Phone className="w-3 h-3" />
|
|
{member.contact_number}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</Link>
|
|
</td>
|
|
<td className="py-4 px-4 text-sm text-slate-700">{member.position || '—'}</td>
|
|
<td className="py-4 px-4 text-sm text-slate-500">{member.position_2 || '—'}</td>
|
|
<td className="py-4 px-4 text-center">
|
|
<div className="flex items-center justify-center gap-1">
|
|
<Star className="w-4 h-4 fill-yellow-400 text-yellow-400" />
|
|
<span className="font-semibold text-sm">
|
|
{member.rating ? member.rating.toFixed(1) : '0.0'}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td className="py-4 px-4 text-center">
|
|
<Badge className={`${
|
|
(member.reliability_score || 0) >= 90 ? 'bg-green-100 text-green-700' :
|
|
(member.reliability_score || 0) >= 70 ? 'bg-yellow-100 text-yellow-700' :
|
|
(member.reliability_score || 0) >= 50 ? 'bg-orange-100 text-orange-700' :
|
|
'bg-red-100 text-red-700'
|
|
} font-semibold`}>
|
|
{member.reliability_score || 0}%
|
|
</Badge>
|
|
</td>
|
|
<td className="py-4 px-4 text-center">
|
|
<Badge className={`${getCoverageColor(member.shift_coverage_percentage)} font-semibold`}>
|
|
{member.shift_coverage_percentage || 0}%
|
|
</Badge>
|
|
</td>
|
|
<td className="py-4 px-4 text-center">
|
|
<Badge variant="outline" className={member.cancellation_count > 0 ? "text-red-600 border-red-300" : "text-slate-600"}>
|
|
{member.cancellation_count || 0}
|
|
</Badge>
|
|
</td>
|
|
<td className="py-4 px-4 text-sm text-slate-700">{member.manager || '—'}</td>
|
|
<td className="py-4 px-4">
|
|
{member.hub_location && (
|
|
<div className="flex items-center gap-1 text-sm text-slate-600">
|
|
<MapPin className="w-3 h-3" />
|
|
{member.hub_location}
|
|
</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className="text-center py-16 bg-white rounded-xl border border-slate-200">
|
|
<Users className="w-16 h-16 mx-auto text-slate-300 mb-4" />
|
|
<h3 className="text-xl font-semibold text-slate-900 mb-2">No Staff Members Found</h3>
|
|
<p className="text-slate-600 mb-6">
|
|
{visibleStaff.length === 0
|
|
? "No staff members available to display"
|
|
: "Try adjusting your filters"}
|
|
</p>
|
|
{canAddStaff && (
|
|
<Link to={createPageUrl("AddStaff")}>
|
|
<Button className="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white">
|
|
<UserPlus className="w-4 h-4 mr-2" />
|
|
Add Staff Member
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
} |