From 03738bfa9ae1d615e7d3b6b95c98fda8b4b248a5 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 11 May 2026 12:26:57 +0200 Subject: [PATCH] feat(documents): AggregatedSection + useAggregatedListing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two TanStack Query hooks fetch the entity-aggregated payload for files and workflows; AggregatedSection renders one labelled subsection per owner-source group with a Show all (N) button wired via the onShowAll callback. Dumb component — parent owns the row rendering + drill- through navigation (Task 15 composes this into EntityFolderView). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../documents/aggregated-section.tsx | 112 ++++++++++++++++++ src/hooks/use-aggregated-listing.ts | 63 ++++++++++ 2 files changed, 175 insertions(+) create mode 100644 src/components/documents/aggregated-section.tsx create mode 100644 src/hooks/use-aggregated-listing.ts diff --git a/src/components/documents/aggregated-section.tsx b/src/components/documents/aggregated-section.tsx new file mode 100644 index 00000000..17dbf735 --- /dev/null +++ b/src/components/documents/aggregated-section.tsx @@ -0,0 +1,112 @@ +'use client'; + +import { Loader2 } from 'lucide-react'; + +import type { AggregatedGroup } from '@/hooks/use-aggregated-listing'; + +interface AggregatedSectionProps { + title: string; + icon?: React.ReactNode; + groups: AggregatedGroup[]; + renderRow: (item: T, group: AggregatedGroup) => React.ReactNode; + emptyMessage?: string; + loading?: boolean; + onShowAll?: (group: AggregatedGroup) => void; +} + +/** + * Renders a Signing or Files section with one labelled subsection per + * owner-source group. Each group shows up to 20 rows; a `Show all (N)` + * link drills into the source-scoped flat list. Hidden when groups is + * empty. + */ +export function AggregatedSection({ + title, + icon, + groups, + renderRow, + emptyMessage = 'Nothing here yet.', + loading, + onShowAll, +}: AggregatedSectionProps) { + const total = groups.reduce((sum, g) => sum + g.total, 0); + + if (loading) { + return ( +
+

+ {icon} + {title} + +

+
+ ); + } + + if (groups.length === 0) { + return ( +
+

+ {icon} + {title} + · 0 +

+

{emptyMessage}

+
+ ); + } + + return ( +
+

+ {icon} + {title} + · {total} +

+
+ {groups.map((g) => ( + + ))} +
+
+ ); +} + +function GroupBlock({ + group, + renderRow, + onShowAll, +}: { + group: AggregatedGroup; + renderRow: (item: T, group: AggregatedGroup) => React.ReactNode; + onShowAll?: (group: AggregatedGroup) => void; +}) { + const items = (group.files ?? group.workflows ?? []) as T[]; + return ( +
+
+ {group.label} + · {group.total} +
+
    + {items.map((item) => ( +
  • {renderRow(item, group)}
  • + ))} +
+ {group.total > items.length ? ( + + ) : null} +
+ ); +} diff --git a/src/hooks/use-aggregated-listing.ts b/src/hooks/use-aggregated-listing.ts new file mode 100644 index 00000000..35dfca49 --- /dev/null +++ b/src/hooks/use-aggregated-listing.ts @@ -0,0 +1,63 @@ +'use client'; + +import { useQuery } from '@tanstack/react-query'; + +import { apiFetch } from '@/lib/api/client'; + +export interface AggregatedFile { + id: string; + filename: string; + mimeType: string | null; + createdAt: string; + folderId: string | null; + clientId: string | null; + companyId: string | null; + yachtId: string | null; +} + +export interface AggregatedWorkflow { + id: string; + title: string; + status: string; + documentType: string; + updatedAt: string; + createdAt: string; +} + +export interface AggregatedGroup { + label: string; + source: 'direct' | 'client' | 'company' | 'yacht'; + files?: T[]; + workflows?: T[]; + total: number; +} + +export function useAggregatedFiles( + entityType: 'client' | 'company' | 'yacht' | undefined, + entityId: string | undefined, +) { + return useQuery[]>({ + queryKey: ['files', 'aggregated', entityType, entityId], + queryFn: () => + apiFetch<{ data: { groups: AggregatedGroup[] } }>( + `/api/v1/files?entityType=${entityType}&entityId=${entityId}`, + ).then((r) => r.data.groups), + enabled: Boolean(entityType && entityId), + staleTime: 10_000, + }); +} + +export function useAggregatedWorkflows( + entityType: 'client' | 'company' | 'yacht' | undefined, + entityId: string | undefined, +) { + return useQuery[]>({ + queryKey: ['documents', 'aggregated', entityType, entityId], + queryFn: () => + apiFetch<{ data: { groups: AggregatedGroup[] } }>( + `/api/v1/documents?entityType=${entityType}&entityId=${entityId}`, + ).then((r) => r.data.groups), + enabled: Boolean(entityType && entityId), + staleTime: 10_000, + }); +}