feat(documents): rebuild hub — root view + entity-folder view
Three rendering modes for the main panel:
- HubRootView (no folder selected): port-wide Signing + recent Files.
- EntityFolderView (system-managed entity subfolder selected):
AggregatedSection × 2 with owner-grouped subsections + per-row
"view signing details" link on signed files (heuristic: filename
starts with "signed-"; follow-up: surface signedFromDocumentId
from the aggregated API).
- FlatFolderListing (any other folder): existing search + chips + list.
Drops the signing-status tab strip (in_progress / awaiting_them / etc.)
— folders are the primary navigation now. hub-counts query removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
102
src/components/documents/entity-folder-view.tsx
Normal file
102
src/components/documents/entity-folder-view.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
'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>) => {
|
||||
// Heuristic v1: auto-deposit handler (Task 7) names signed files with "signed-" prefix.
|
||||
// Follow-up: surface signedFromDocumentId from the aggregated API for a principled check.
|
||||
const isSigned = f.filename?.startsWith('signed-');
|
||||
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>
|
||||
{isSigned ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-1 text-brand hover:underline"
|
||||
onClick={() => setDetailsId(f.id)}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user