40227-vm/frontend/src/components/director-dashboard/DirectorAcknowledgmentTrackingPanel.tsx
2026-06-19 16:43:41 +02:00

180 lines
7.7 KiB
TypeScript

import { useMemo, useState } from 'react';
import { ChevronDown, ClipboardCheck } from 'lucide-react';
import type { DirectorAcknowledgmentDocumentRow } from '@/business/director-dashboard/types';
import { cn } from '@/lib/utils';
interface DirectorAcknowledgmentTrackingPanelProps {
readonly documents: readonly DirectorAcknowledgmentDocumentRow[];
}
export function DirectorAcknowledgmentTrackingPanel({
documents,
}: DirectorAcknowledgmentTrackingPanelProps) {
const [isPanelExpanded, setIsPanelExpanded] = useState(false);
const [expandedDocumentIds, setExpandedDocumentIds] = useState<ReadonlySet<string>>(
() => new Set(),
);
const groupedDocuments = useMemo(() => groupDocumentsByCategory(documents), [documents]);
const missingAcknowledgmentCount = documents.reduce(
(total, document) => total + document.missingCount,
0,
);
function toggleDocument(documentId: string) {
setExpandedDocumentIds((current) => {
const next = new Set(current);
if (next.has(documentId)) {
next.delete(documentId);
} else {
next.add(documentId);
}
return next;
});
}
return (
<section className="rounded-2xl border border-violet-100 bg-white p-5 text-gray-900 shadow-sm">
<button
type="button"
onClick={() => setIsPanelExpanded((current) => !current)}
className="mb-4 flex w-full items-center justify-between gap-4 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-400 focus-visible:ring-offset-2"
aria-expanded={isPanelExpanded}
aria-controls="director-acknowledgment-results-panel"
>
<span>
<span className="flex items-center gap-2 font-semibold text-gray-800">
<ClipboardCheck size={18} className="text-emerald-600" />
Document Acknowledgments
</span>
<span className="mt-1 block text-sm text-gray-500">
{documents.length} documents · {missingAcknowledgmentCount} missing acknowledgments
</span>
</span>
<ChevronDown
size={20}
className={cn('shrink-0 text-gray-400 transition-transform', isPanelExpanded && 'rotate-180')}
aria-hidden="true"
/>
</button>
<div id="director-acknowledgment-results-panel">
{isPanelExpanded && groupedDocuments.length === 0 ? (
<p className="rounded-xl bg-gray-50 px-4 py-3 text-sm text-gray-500">
No active documents are assigned to this scope yet.
</p>
) : isPanelExpanded ? (
<div className="space-y-5">
{groupedDocuments.map((group) => (
<div key={group.category} className="space-y-2">
<p className="text-xs font-bold uppercase text-gray-500">
{group.label}
</p>
<div className="overflow-hidden rounded-xl border border-gray-100">
{group.documents.map((document, index) => {
const isExpanded = expandedDocumentIds.has(document.id);
return (
<div
key={document.id}
className={index > 0 ? 'border-t border-gray-100' : undefined}
>
<button
type="button"
onClick={() => toggleDocument(document.id)}
className="flex w-full items-center justify-between gap-4 bg-white px-4 py-3 text-left transition-transform hover:scale-[1.005] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-400 focus-visible:ring-offset-2"
aria-expanded={isExpanded}
aria-controls={`acknowledgment-${document.id}`}
>
<span className="min-w-0">
<span className="block truncate text-sm font-semibold text-gray-800">
{document.title}
</span>
<span className="mt-1 block text-xs text-gray-500">
Version {document.version}
</span>
</span>
<span className="flex shrink-0 items-center gap-3">
<span className={getStatusClassName(document.missingCount)}>
{document.acknowledgedCount}/{document.totalStaff}
</span>
<ChevronDown
size={18}
className={cn('text-gray-400 transition-transform', isExpanded && 'rotate-180')}
aria-hidden="true"
/>
</span>
</button>
{isExpanded && (
<div
id={`acknowledgment-${document.id}`}
className="bg-gray-50 px-4 py-3"
>
{document.missingStaff.length === 0 ? (
<p className="text-sm text-emerald-700">
Every staff member in scope has acknowledged this version.
</p>
) : (
<div className="space-y-2">
<p className="text-xs font-semibold uppercase text-gray-500">
Not acknowledged
</p>
{document.missingStaff.map((staff) => (
<div
key={`${document.id}-${staff.userId}`}
className="flex flex-wrap items-center justify-between gap-2 rounded-lg bg-white px-3 py-2 text-sm"
>
<span className="font-medium text-gray-800">{staff.name}</span>
<span className="text-xs capitalize text-gray-500">
{staff.role}
</span>
</div>
))}
</div>
)}
</div>
)}
</div>
);
})}
</div>
</div>
))}
</div>
) : (
<p className="rounded-xl bg-gray-50 px-4 py-3 text-sm text-gray-500">
Expand to review document acknowledgment status by category.
</p>
)}
</div>
</section>
);
}
function groupDocumentsByCategory(
documents: readonly DirectorAcknowledgmentDocumentRow[],
): readonly {
readonly category: string;
readonly label: string;
readonly documents: readonly DirectorAcknowledgmentDocumentRow[];
}[] {
const groups = new Map<string, DirectorAcknowledgmentDocumentRow[]>();
const labels = new Map<string, string>();
for (const document of documents) {
const rows = groups.get(document.category) ?? [];
rows.push(document);
groups.set(document.category, rows);
labels.set(document.category, document.categoryLabel);
}
return [...groups.entries()].map(([category, rows]) => ({
category,
label: labels.get(category) ?? category,
documents: rows,
}));
}
function getStatusClassName(missingCount: number): string {
return missingCount > 0
? 'rounded-lg bg-amber-100 px-2.5 py-1 text-xs font-semibold text-amber-700'
: 'rounded-lg bg-emerald-100 px-2.5 py-1 text-xs font-semibold text-emerald-700';
}