From 5121cb4cef36f231f9467cfc3376426067a000e1 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sat, 1 Aug 2026 21:29:52 +0000 Subject: [PATCH] feat(CC-065): add Expirations page (documents + contracts expiring in 90 days) --- src/app/dashboard/expirations/page.tsx | 152 +++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/app/dashboard/expirations/page.tsx diff --git a/src/app/dashboard/expirations/page.tsx b/src/app/dashboard/expirations/page.tsx new file mode 100644 index 0000000..689bdf7 --- /dev/null +++ b/src/app/dashboard/expirations/page.tsx @@ -0,0 +1,152 @@ +'use client'; + +import Link from 'next/link'; +import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Document { + id: string; filename: string; classification: string; + expiresAt: string | null; ocrStatus: string | null; createdAt: string; +} +interface Contract { + id: string; title: string; contractType: string; status: string; + endDate: string | null; +} + +const B = '/dashboard'; + +function daysUntil(dateStr: string): number { + return Math.ceil((new Date(dateStr).getTime() - Date.now()) / 86400_000); +} + +function ExpiryBadge({ days }: { days: number }) { + if (days < 0) return EXPIRAT {Math.abs(days)}z; + if (days <= 7) return {days}z; + if (days <= 30) return {days}z; + return {days}z; +} + +export default function ExpirationsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + + const { data: docs = [], isLoading: docsLoading } = useQuery({ + queryKey: ['docs-exp', tenantId], + queryFn: () => apiFetch('/v1/documents?limit=200', { tenantId }), + enabled: Boolean(tenantId), + staleTime: 120_000, + }); + const { data: contracts = [], isLoading: cLoading } = useQuery({ + queryKey: ['contracts-exp', tenantId], + queryFn: () => apiFetch('/v1/contracts?status=active', { tenantId }), + enabled: Boolean(tenantId), + staleTime: 120_000, + }); + + const now = new Date(); + const in90 = new Date(now.getTime() + 90 * 86400_000); + + const expiringDocs = docs + .filter((d) => d.expiresAt && new Date(d.expiresAt) <= in90) + .sort((a, b) => new Date(a.expiresAt!).getTime() - new Date(b.expiresAt!).getTime()); + + const expiringContracts = contracts + .filter((c) => c.endDate && new Date(c.endDate) <= in90) + .sort((a, b) => new Date(a.endDate!).getTime() - new Date(b.endDate!).getTime()); + + const totalExpiring = expiringDocs.length + expiringContracts.length; + const critical = expiringDocs.filter((d) => daysUntil(d.expiresAt!) <= 7).length + + expiringContracts.filter((c) => daysUntil(c.endDate!) <= 7).length; + const isLoading = docsLoading || cLoading; + + return ( +
+
+

Expirări

+

+ {isLoading ? 'Se încarcă…' : `${totalExpiring} expirări în 90 zile${critical > 0 ? ` · ${critical} critice` : ''}`} +

+
+ + {!isLoading && totalExpiring === 0 && ( +
+

+

Nicio expirare în următoarele 90 zile.

+
+ )} + + {!isLoading && totalExpiring > 0 && ( + <> + {/* Summary chips */} +
+ {[ + { label: 'Azi–7 zile', count: expiringDocs.filter((d) => daysUntil(d.expiresAt!) <= 7).length + expiringContracts.filter((c) => daysUntil(c.endDate!) <= 7).length, cls: 'text-signal-danger bg-signal-danger/5 border-signal-danger/20' }, + { label: '8–30 zile', count: expiringDocs.filter((d) => { const n = daysUntil(d.expiresAt!); return n > 7 && n <= 30; }).length + expiringContracts.filter((c) => { const n = daysUntil(c.endDate!); return n > 7 && n <= 30; }).length, cls: 'text-signal-warn bg-signal-warn/5 border-signal-warn/20' }, + { label: '31–90 zile', count: expiringDocs.filter((d) => daysUntil(d.expiresAt!) > 30).length + expiringContracts.filter((c) => daysUntil(c.endDate!) > 30).length, cls: 'text-ink-faint bg-muted/30 border-border/50' }, + ].map(({ label, count, cls }) => count > 0 ? ( +
+

{count}

+

{label}

+
+ ) : null)} +
+ + {/* Expiring contracts */} + {expiringContracts.length > 0 && ( +
+
+

Contracte

+ Administrează → +
+
+ {expiringContracts.map((c) => { + const days = daysUntil(c.endDate!); + return ( +
+
+

{c.title}

+

{c.contractType}

+
+
+ +

{c.endDate}

+
+
+ ); + })} +
+
+ )} + + {/* Expiring documents */} + {expiringDocs.length > 0 && ( +
+
+

Documente

+ Bibliotecă → +
+
+ {expiringDocs.map((d) => { + const days = daysUntil(d.expiresAt!); + return ( +
+
+

{d.filename}

+

{d.classification}

+
+
+ +

{d.expiresAt?.slice(0, 10)}

+
+
+ ); + })} +
+
+ )} + + )} +
+ ); +}