Files
pn-new-crm/src/components/documents/entity-folder-view.tsx

107 lines
3.7 KiB
TypeScript
Raw Normal View History

'use client';
import { useState } from 'react';
import Link from 'next/link';
import { ClipboardSignature, FileText, Eye } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { AggregatedSection } from './aggregated-section';
import { SigningDetailsDialog } from './signing-details-dialog';
import { useAggregatedFiles, useAggregatedWorkflows } from '@/hooks/use-aggregated-listing';
import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill';
import type {
AggregatedFile,
AggregatedGroup,
AggregatedWorkflow,
} from '@/hooks/use-aggregated-listing';
interface Props {
portSlug: string;
entityType: 'client' | 'company' | 'yacht';
entityId: string;
}
function mapWorkflowStatus(status: string): StatusPillStatus {
const known: Record<string, StatusPillStatus> = {
draft: 'draft',
sent: 'sent',
partially_signed: 'partial',
completed: 'completed',
expired: 'expired',
cancelled: 'cancelled',
};
return known[status] ?? 'pending';
}
export function EntityFolderView({ portSlug, entityType, entityId }: Props) {
const [detailsId, setDetailsId] = useState<string | null>(null);
// Hook data is the bare AggregatedGroup<T>[] array (hooks unwrap the API envelope).
const { data: workflowGroups = [], isLoading: workflowsLoading } = useAggregatedWorkflows(
entityType,
entityId,
);
const { data: fileGroups = [], isLoading: filesLoading } = useAggregatedFiles(
entityType,
entityId,
);
return (
<div className="space-y-4">
<AggregatedSection<'workflows'>
title="Signing in progress"
icon={<ClipboardSignature className="h-4 w-4 text-muted-foreground" aria-hidden />}
groups={workflowGroups}
loading={workflowsLoading}
emptyMessage="No workflows in flight for this entity."
renderRow={(w: AggregatedWorkflow, _group: AggregatedGroup<AggregatedWorkflow>) => (
<div className="flex items-center justify-between gap-2 text-sm">
<Link href={`/${portSlug}/documents/${w.id}`} className="truncate hover:underline">
{w.title}
</Link>
<StatusPill status={mapWorkflowStatus(w.status)}>
{w.status.replace(/_/g, ' ')}
</StatusPill>
</div>
)}
/>
<AggregatedSection<'files'>
title="Files"
icon={<FileText className="h-4 w-4 text-muted-foreground" aria-hidden />}
groups={fileGroups}
loading={filesLoading}
emptyMessage="No files for this entity yet."
renderRow={(f: AggregatedFile, _group: AggregatedGroup<AggregatedFile>) => {
const signedFromDocumentId = f.signedFromDocumentId;
return (
<div className="flex items-center justify-between gap-2 text-sm">
<span className="truncate">{f.filename}</span>
<div className="flex items-center gap-2 text-xs text-muted-foreground tabular-nums">
<span>{new Date(f.createdAt).toLocaleDateString('en-GB')}</span>
{signedFromDocumentId ? (
<Button
variant="ghost"
size="sm"
className="min-h-[44px] gap-1 px-2 text-xs text-brand"
onClick={() => setDetailsId(signedFromDocumentId)}
>
<Eye className="h-3 w-3" aria-hidden />
View signing details
</Button>
) : null}
</div>
</div>
);
}}
/>
<SigningDetailsDialog
documentId={detailsId}
open={Boolean(detailsId)}
onOpenChange={(open) => !open && setDetailsId(null)}
/>
</div>
);
}