Files
pn-new-crm/src/components/documents/hub-root-view.tsx

193 lines
7.0 KiB
TypeScript

'use client';
import { useState } from 'react';
import Link from 'next/link';
import { FileText, ClipboardSignature, Folder } from 'lucide-react';
import { usePaginatedQuery } from '@/hooks/use-paginated-query';
import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill';
import { FilePreviewDialog } from '@/components/files/file-preview-dialog';
interface HubRootDoc {
id: string;
title: string;
documentType: string;
status: string;
createdAt: string;
}
interface HubRootFile {
id: string;
filename: string;
mimeType: string | null;
createdAt: string;
folderId: string | null;
folderName: string | null;
clientId: string | null;
clientName: string | null;
yachtId: string | null;
yachtName: string | null;
companyId: string | null;
companyName: string | null;
interestId: string | null;
interestSummary: { stage: string; clientName: string | null } | null;
}
interface Props {
portSlug: string;
}
const STATUS_PILL_MAP: Record<string, StatusPillStatus> = {
draft: 'draft',
sent: 'sent',
partially_signed: 'partial',
completed: 'completed',
signed: 'signed',
expired: 'expired',
cancelled: 'cancelled',
rejected: 'rejected',
};
export function HubRootView({ portSlug }: Props) {
const [previewFile, setPreviewFile] = useState<{
id: string;
name: string;
mimeType: string | null;
} | null>(null);
const { data: workflows, isLoading: workflowsLoading } = usePaginatedQuery<HubRootDoc>({
queryKey: ['documents', 'hub-root', 'workflows'],
endpoint: '/api/v1/documents?tab=in_progress',
filterDefinitions: [],
});
const { data: filesData, isLoading: filesLoading } = usePaginatedQuery<HubRootFile>({
queryKey: ['files', 'hub-root'],
endpoint: '/api/v1/files?sort=createdAt&order=desc&limit=20',
filterDefinitions: [],
});
return (
<div className="space-y-4">
<section className="rounded-md border bg-white">
<h3 className="flex items-center gap-2 border-b px-3 py-2 text-sm font-semibold">
<ClipboardSignature className="h-4 w-4 text-muted-foreground" aria-hidden />
Signing in progress
</h3>
{workflowsLoading ? (
<div className="p-3 text-sm text-muted-foreground">Loading...</div>
) : workflows.length === 0 ? (
<div className="p-3 text-sm text-muted-foreground">No workflows in flight.</div>
) : (
<ul className="divide-y">
{workflows.map((w) => (
<li key={w.id} className="flex items-center justify-between gap-2 px-3 py-2 text-sm">
<Link href={`/${portSlug}/documents/${w.id}`} className="truncate hover:underline">
{w.title}
</Link>
<StatusPill status={STATUS_PILL_MAP[w.status] ?? 'pending'}>
{w.status.replace(/_/g, ' ')}
</StatusPill>
</li>
))}
</ul>
)}
</section>
<section className="rounded-md border bg-white">
<h3 className="flex items-center gap-2 border-b px-3 py-2 text-sm font-semibold">
<FileText className="h-4 w-4 text-muted-foreground" aria-hidden />
Recent files
</h3>
{filesLoading ? (
<div className="p-3 text-sm text-muted-foreground">Loading...</div>
) : filesData.length === 0 ? (
<div className="p-3 text-sm text-muted-foreground">No files yet.</div>
) : (
<ul className="divide-y">
{filesData.map((f) => {
const entityBadge = (() => {
if (f.interestId)
return {
label: f.interestSummary?.clientName
? `Interest: ${f.interestSummary.clientName}`
: 'Interest',
href: `/${portSlug}/interests/${f.interestId}`,
};
if (f.clientId)
return {
label: f.clientName ?? 'Client',
href: `/${portSlug}/clients/${f.clientId}`,
};
if (f.yachtId)
return {
label: f.yachtName ?? 'Yacht',
href: `/${portSlug}/yachts/${f.yachtId}`,
};
if (f.companyId)
return {
label: f.companyName ?? 'Company',
href: `/${portSlug}/companies/${f.companyId}`,
};
return null;
})();
return (
<li
key={f.id}
className="flex items-center justify-between gap-3 px-3 py-2 text-sm"
>
<div className="min-w-0 flex-1">
<button
type="button"
className="block truncate text-left hover:text-brand hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 rounded-sm"
onClick={() =>
setPreviewFile({ id: f.id, name: f.filename, mimeType: f.mimeType })
}
aria-label={`Preview ${f.filename}`}
>
{f.filename}
</button>
<div className="mt-0.5 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
{f.folderId && f.folderName ? (
<Link
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
href={`/${portSlug}/documents?folderId=${f.folderId}` as any}
className="inline-flex items-center gap-1 hover:underline"
>
<Folder className="h-3 w-3" aria-hidden />
{f.folderName}
</Link>
) : null}
{entityBadge ? (
<Link
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
href={entityBadge.href as any}
className="inline-flex items-center rounded-full border bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground hover:bg-muted/70"
>
{entityBadge.label}
</Link>
) : null}
</div>
</div>
<span className="text-xs text-muted-foreground tabular-nums shrink-0">
{new Date(f.createdAt).toLocaleDateString(undefined)}
</span>
</li>
);
})}
</ul>
)}
</section>
<FilePreviewDialog
open={Boolean(previewFile)}
onOpenChange={(open) => {
if (!open) setPreviewFile(null);
}}
fileId={previewFile?.id}
fileName={previewFile?.name}
mimeType={previewFile?.mimeType ?? undefined}
/>
</div>
);
}