Files
pn-new-crm/src/components/documents/entity-folder-view.tsx
Matt a4c49f5e5a fix(documents): surface signedFromDocumentId + hub cleanup
Three follow-ups from Task 15 code review:

1. (Important) The aggregated files API now LEFT JOINs against
   documents to surface signedFromDocumentId per file row. The
   "view signing details" button on EntityFolderView's Files
   section now passes the workflow id to SigningDetailsDialog
   instead of the file id. Previously the button always 404'd
   and the dialog hung in the loading state. Drops the v1
   filename-prefix heuristic.

2. (Minor) Drop dead initialTab prop + DocumentsHubTab import —
   leftover from the pre-refactor tab strip.

3. (Minor) FlatFolderListing remounts on folder switch via a key
   prop, restoring the pre-refactor typeFilter reset behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:44:48 +02:00

105 lines
3.6 KiB
TypeScript

'use client';
import { useState } from 'react';
import Link from 'next/link';
import { ClipboardSignature, FileText, Eye } from 'lucide-react';
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 {
AggregatedWorkflow,
AggregatedFile,
AggregatedGroup,
} 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
title="Signing in progress"
icon={<ClipboardSignature className="h-4 w-4 text-muted-foreground" />}
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
title="Files"
icon={<FileText className="h-4 w-4 text-muted-foreground" />}
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
type="button"
className="flex items-center gap-1 text-brand hover:underline"
onClick={() => setDetailsId(signedFromDocumentId)}
>
<Eye className="h-3 w-3" />
view signing details
</button>
) : null}
</div>
</div>
);
}}
/>
<SigningDetailsDialog
documentId={detailsId}
open={Boolean(detailsId)}
onOpenChange={(open) => !open && setDetailsId(null)}
/>
</div>
);
}