295 lines
11 KiB
TypeScript
295 lines
11 KiB
TypeScript
import {
|
|
AlertTriangle,
|
|
FileStack,
|
|
FolderKanban,
|
|
ShieldAlert,
|
|
ShieldCheck,
|
|
Shirt,
|
|
} from "lucide-react"
|
|
import { useSearchParams } from "react-router-dom"
|
|
|
|
import { MetricCard } from "@/components/shared/MetricCard"
|
|
import { Page, PageIntro, SectionCard } from "@/components/shared/Page"
|
|
import { StatusBadge } from "@/components/shared/StatusBadge"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table"
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
import { COMPLIANCE_RISKS, STAFF } from "@/data/mockData"
|
|
import { complianceFlagCount, staffWithComplianceIssues } from "@/data/selectors"
|
|
import { useWorkforceRevision } from "@/data/store"
|
|
|
|
const CATALOG = [
|
|
{ name: "Right to Work", required: "All staff", holders: 44, of: 46, renewal: "On hire" },
|
|
{ name: "Manual Handling", required: "Warehouse & Logistics", holders: 15, of: 16, renewal: "2 years" },
|
|
{ name: "Forklift Licence", required: "Forklift Drivers", holders: 3, of: 4, renewal: "3 years" },
|
|
{ name: "Food Hygiene L2", required: "Event & Hospitality", holders: 17, of: 18, renewal: "3 years" },
|
|
{ name: "Personal Licence", required: "Bar Supervisors", holders: 2, of: 2, renewal: "10 years" },
|
|
{ name: "COSHH Awareness", required: "Facilities & Cleaning", holders: 11, of: 12, renewal: "1 year" },
|
|
]
|
|
|
|
const DOCUMENTS = [
|
|
{ staff: "Rosa Delgado", doc: "Right to Work", status: "Expired", updated: "Pending upload" },
|
|
{ staff: "Aisha Patel", doc: "Policy Acknowledgement", status: "Warning", updated: "Awaiting signature" },
|
|
{ staff: "Samuel Okoro", doc: "Manual Handling Certificate", status: "Expired", updated: "Expired 02 Sep 2025" },
|
|
{ staff: "Tomas Ferreira", doc: "Forklift Licence", status: "Warning", updated: "Expires 17 Sep 2025" },
|
|
{ staff: "Leila Haddad", doc: "Right to Work", status: "Verified", updated: "Verified 12 Jul 2025" },
|
|
{ staff: "Marcus Vance", doc: "Supervisor Training", status: "Verified", updated: "Verified 03 Aug 2025" },
|
|
]
|
|
|
|
const ATTIRE = [
|
|
{ staff: "Priya Raman", item: "Event uniform · size exchange", due: "05 Oct 2025", status: "Review" },
|
|
{ staff: "Liam O'Connor", item: "Bar tunic · replacement", due: "05 Oct 2025", status: "Review" },
|
|
{ staff: "Rosa Delgado", item: "Hi-vis & safety boots", due: "05 Oct 2025", status: "Review" },
|
|
{ staff: "Owen Bradley", item: "Hi-vis · annual check", due: "18 Nov 2025", status: "Verified" },
|
|
]
|
|
|
|
export function CompliancePage() {
|
|
useWorkforceRevision()
|
|
const [params, setParams] = useSearchParams()
|
|
const tab = params.get("tab") ?? "certificates"
|
|
|
|
const high = COMPLIANCE_RISKS.filter((r) => r.severity === "High")
|
|
const expiringSoon = STAFF.filter((s) => s.compliance === "Warning")
|
|
|
|
return (
|
|
<Page>
|
|
<PageIntro
|
|
title="Compliance"
|
|
description="Certificates, documents, policies and attire checks across the workforce."
|
|
actions={<Button variant="outline">Export register</Button>}
|
|
/>
|
|
|
|
<div className="grid gap-2.5 grid-cols-2 lg:grid-cols-4">
|
|
<MetricCard
|
|
label="Open Flags"
|
|
value={String(complianceFlagCount())}
|
|
sub={`Across ${COMPLIANCE_RISKS.length} risk categories`}
|
|
icon={ShieldAlert}
|
|
badge="Review"
|
|
badgeTone="warn"
|
|
tone="warn"
|
|
/>
|
|
<MetricCard
|
|
label="High Severity"
|
|
value={String(high.reduce((s, r) => s + r.staffCount, 0))}
|
|
sub="Block assignment until cleared"
|
|
icon={AlertTriangle}
|
|
badge="Urgent"
|
|
badgeTone="bad"
|
|
tone="bad"
|
|
/>
|
|
<MetricCard
|
|
label="Staff Affected"
|
|
value={String(staffWithComplianceIssues().length)}
|
|
sub={`of ${STAFF.length} tracked staff`}
|
|
icon={ShieldCheck}
|
|
tone="primary"
|
|
/>
|
|
<MetricCard
|
|
label="Expiring Soon"
|
|
value={String(expiringSoon.length)}
|
|
sub="Within the next 14 days"
|
|
icon={FileStack}
|
|
badge="Watch"
|
|
badgeTone="warn"
|
|
tone="primary"
|
|
/>
|
|
</div>
|
|
|
|
<SectionCard title="Compliance Risk Overview" icon={ShieldAlert} bodyClassName="p-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="hover:bg-surface-soft">
|
|
<TableHead>Risk</TableHead>
|
|
<TableHead>Category</TableHead>
|
|
<TableHead>Affected staff</TableHead>
|
|
<TableHead>Severity</TableHead>
|
|
<TableHead>Expires</TableHead>
|
|
<TableHead className="text-right">Action</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{COMPLIANCE_RISKS.map((r) => (
|
|
<TableRow key={r.id}>
|
|
<TableCell className="font-medium text-ink">{r.risk}</TableCell>
|
|
<TableCell>
|
|
<Badge tone="neutral" size="sm">
|
|
{r.category}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="w-[168px]" title={r.staffNames.join(", ")}>
|
|
<span className="font-semibold text-ink">{r.staffCount}</span>
|
|
<div className="truncate text-[12px] text-ink-3">
|
|
{r.staffNames.join(", ")}
|
|
</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<StatusBadge value={r.severity} size="sm" />
|
|
</TableCell>
|
|
<TableCell className="whitespace-nowrap">{r.expires.replace(" 2025", "")}</TableCell>
|
|
<TableCell className="text-right">
|
|
<Button size="sm" variant="outline">
|
|
{r.action}
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</SectionCard>
|
|
|
|
<Tabs value={tab} onValueChange={(v) => setParams({ tab: v })}>
|
|
<TabsList>
|
|
<TabsTrigger value="certificates">
|
|
<ShieldCheck className="size-3.5" />
|
|
Staff Certificates
|
|
</TabsTrigger>
|
|
<TabsTrigger value="documents">
|
|
<FolderKanban className="size-3.5" />
|
|
Employee Documents
|
|
</TabsTrigger>
|
|
<TabsTrigger value="attire">
|
|
<Shirt className="size-3.5" />
|
|
Attire Review
|
|
</TabsTrigger>
|
|
<TabsTrigger value="catalog">
|
|
<FileStack className="size-3.5" />
|
|
Compliance Catalog
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="certificates">
|
|
<SectionCard title="Staff Certificates" icon={ShieldCheck} bodyClassName="p-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="hover:bg-surface-soft">
|
|
<TableHead>Staff</TableHead>
|
|
<TableHead>Role</TableHead>
|
|
<TableHead>Standing</TableHead>
|
|
<TableHead>Note</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{STAFF.map((s) => (
|
|
<TableRow key={s.id}>
|
|
<TableCell className="font-medium text-ink">{s.name}</TableCell>
|
|
<TableCell>{s.role}</TableCell>
|
|
<TableCell>
|
|
<StatusBadge value={s.compliance} size="sm" />
|
|
</TableCell>
|
|
<TableCell className="text-ink-3">
|
|
{s.complianceNote ?? "All certificates current"}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</SectionCard>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="documents">
|
|
<SectionCard title="Employee Documents" icon={FolderKanban} bodyClassName="p-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="hover:bg-surface-soft">
|
|
<TableHead>Staff</TableHead>
|
|
<TableHead>Document</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Last update</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{DOCUMENTS.map((d) => (
|
|
<TableRow key={`${d.staff}-${d.doc}`}>
|
|
<TableCell className="font-medium text-ink">{d.staff}</TableCell>
|
|
<TableCell>{d.doc}</TableCell>
|
|
<TableCell>
|
|
<StatusBadge value={d.status} size="sm" />
|
|
</TableCell>
|
|
<TableCell className="text-ink-3">{d.updated}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</SectionCard>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="attire">
|
|
<SectionCard title="Attire Review" icon={Shirt} bodyClassName="p-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="hover:bg-surface-soft">
|
|
<TableHead>Staff</TableHead>
|
|
<TableHead>Item</TableHead>
|
|
<TableHead>Due</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{ATTIRE.map((a) => (
|
|
<TableRow key={`${a.staff}-${a.item}`}>
|
|
<TableCell className="font-medium text-ink">{a.staff}</TableCell>
|
|
<TableCell>{a.item}</TableCell>
|
|
<TableCell className="whitespace-nowrap">{a.due}</TableCell>
|
|
<TableCell>
|
|
<StatusBadge value={a.status} size="sm" />
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</SectionCard>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="catalog">
|
|
<SectionCard title="Compliance Catalog" icon={FileStack} bodyClassName="p-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="hover:bg-surface-soft">
|
|
<TableHead>Requirement</TableHead>
|
|
<TableHead>Applies to</TableHead>
|
|
<TableHead>Coverage</TableHead>
|
|
<TableHead>Renewal</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{CATALOG.map((c) => {
|
|
const complete = c.holders === c.of
|
|
return (
|
|
<TableRow key={c.name}>
|
|
<TableCell className="font-medium text-ink">{c.name}</TableCell>
|
|
<TableCell>{c.required}</TableCell>
|
|
<TableCell>
|
|
<span className="flex items-center gap-2">
|
|
<span className="font-semibold text-ink">
|
|
{c.holders}/{c.of}
|
|
</span>
|
|
<StatusBadge
|
|
value={complete ? "Verified" : "Warning"}
|
|
size="sm"
|
|
/>
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="text-ink-3">{c.renewal}</TableCell>
|
|
</TableRow>
|
|
)
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</SectionCard>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</Page>
|
|
)
|
|
}
|