180 lines
7.7 KiB
TypeScript
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';
|
|
}
|