ceo-web/src/app/dashboard/expirations/page.tsx

152 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 <span className="text-[10px] font-bold text-signal-danger">EXPIRAT {Math.abs(days)}z</span>;
if (days <= 7) return <span className="text-[10px] font-bold text-signal-danger">{days}z</span>;
if (days <= 30) return <span className="text-[10px] font-semibold text-signal-warn">{days}z</span>;
return <span className="text-[10px] text-ink-faint">{days}z</span>;
}
export default function ExpirationsPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data: docs = [], isLoading: docsLoading } = useQuery({
queryKey: ['docs-exp', tenantId],
queryFn: () => apiFetch<Document[]>('/v1/documents?limit=200', { tenantId }),
enabled: Boolean(tenantId),
staleTime: 120_000,
});
const { data: contracts = [], isLoading: cLoading } = useQuery({
queryKey: ['contracts-exp', tenantId],
queryFn: () => apiFetch<Contract[]>('/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 (
<div className="max-w-4xl space-y-6 p-6">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Expirări</h1>
<p className="text-sm text-ink-faint mt-1">
{isLoading ? 'Se încarcă…' : `${totalExpiring} expirări în 90 zile${critical > 0 ? ` · ${critical} critice` : ''}`}
</p>
</div>
{!isLoading && totalExpiring === 0 && (
<div className="card p-12 text-center space-y-2">
<p className="text-3xl"></p>
<p className="text-sm text-ink-faint">Nicio expirare în următoarele 90 zile.</p>
</div>
)}
{!isLoading && totalExpiring > 0 && (
<>
{/* Summary chips */}
<div className="flex gap-3 flex-wrap">
{[
{ label: 'Azi7 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: '830 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: '3190 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 ? (
<div key={label} className={`rounded-lg border px-3 py-2 text-center min-w-20 ${cls}`}>
<p className="font-display text-xl font-bold">{count}</p>
<p className="text-[10px]">{label}</p>
</div>
) : null)}
</div>
{/* Expiring contracts */}
{expiringContracts.length > 0 && (
<div className="card p-5 space-y-3">
<div className="flex items-center justify-between">
<h2 className="text-sm font-semibold text-ink">Contracte</h2>
<Link href={`${B}/contracts`} className="text-xs text-bronze-deep hover:underline">Administrează </Link>
</div>
<div className="divide-y divide-border/50">
{expiringContracts.map((c) => {
const days = daysUntil(c.endDate!);
return (
<div key={c.id} className="flex items-center justify-between py-2.5 gap-4">
<div className="flex-1 min-w-0">
<p className="text-sm text-ink font-medium truncate">{c.title}</p>
<p className="text-[10px] text-ink-faint capitalize">{c.contractType}</p>
</div>
<div className="text-right shrink-0">
<ExpiryBadge days={days} />
<p className="text-[10px] text-ink-faint mt-0.5">{c.endDate}</p>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Expiring documents */}
{expiringDocs.length > 0 && (
<div className="card p-5 space-y-3">
<div className="flex items-center justify-between">
<h2 className="text-sm font-semibold text-ink">Documente</h2>
<Link href={`${B}/documents`} className="text-xs text-bronze-deep hover:underline">Bibliotecă </Link>
</div>
<div className="divide-y divide-border/50">
{expiringDocs.map((d) => {
const days = daysUntil(d.expiresAt!);
return (
<div key={d.id} className="flex items-center justify-between py-2.5 gap-4">
<div className="flex-1 min-w-0">
<p className="text-sm text-ink font-medium truncate">{d.filename}</p>
<p className="text-[10px] text-ink-faint uppercase">{d.classification}</p>
</div>
<div className="text-right shrink-0">
<ExpiryBadge days={days} />
<p className="text-[10px] text-ink-faint mt-0.5">{d.expiresAt?.slice(0, 10)}</p>
</div>
</div>
);
})}
</div>
</div>
)}
</>
)}
</div>
);
}