first commit
This commit is contained in:
294
src/pages/CompliancePage.tsx
Normal file
294
src/pages/CompliancePage.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user