feat(CC-074): add Document Archive page (docs > 1yr or tagged "archived")

This commit is contained in:
admin-valentin 2026-08-02 12:38:19 +00:00
parent 5b6b012e53
commit 3a3c8dae8c

View file

@ -0,0 +1,137 @@
'use client';
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import Link from 'next/link';
import { apiFetch } from '../../../../lib/api';
import { useSession } from '../../../../components/session-provider';
interface Document {
id: string; title: string; fileType: string | null; classification: string | null;
expiresAt: string | null; ocrStatus: string | null; status?: string;
createdAt: string; updatedAt: string; tags: string[];
}
const CLS_CLS: Record<string, string> = {
C0: 'bg-muted text-ink-faint', C1: 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400',
C2: 'bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400',
C3: 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400',
C4: 'bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400',
};
export default function DocumentArchivePage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const qc = useQueryClient();
const [search, setSearch] = useState('');
const [clsFilter, setClsFilter] = useState('all');
const { data: docs = [], isLoading } = useQuery({
queryKey: ['docs-archive', tenantId],
queryFn: () => apiFetch<Document[]>('/v1/documents?limit=500', { tenantId }),
enabled: Boolean(tenantId), staleTime: 60_000,
});
// Archive = docs older than 1 year or explicitly tagged/classified
const archived = docs.filter((d) => {
const createdLong = (Date.now() - new Date(d.createdAt).getTime()) > 365 * 86400_000;
const isTagged = d.tags?.includes('archived') || d.tags?.includes('archive');
return createdLong || isTagged;
});
const filtered = archived.filter((d) => {
if (clsFilter !== 'all' && d.classification !== clsFilter) return false;
if (search && !d.title.toLowerCase().includes(search.toLowerCase())) return false;
return true;
});
const classBreakdown: Record<string, number> = {};
for (const d of archived) {
const cls = d.classification ?? 'none';
classBreakdown[cls] = (classBreakdown[cls] ?? 0) + 1;
}
const unarchiveMut = useMutation({
mutationFn: (id: string) =>
apiFetch<Document>(`/v1/documents/${id}`, { tenantId, method: 'PATCH', body: { tags: [] } }),
onSuccess: () => qc.invalidateQueries({ queryKey: ['docs-archive', tenantId] }),
});
return (
<div className="max-w-4xl space-y-6 p-6">
<div className="flex items-center justify-between flex-wrap gap-3">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Arhivă Documente</h1>
<p className="text-sm text-ink-faint mt-1">
{archived.length} documente arhivate din {docs.length} total.
</p>
</div>
<Link href="/dashboard/documents" className="text-sm text-primary hover:underline">
Toate documentele
</Link>
</div>
{/* Classification breakdown */}
<div className="flex flex-wrap gap-2">
<button onClick={() => setClsFilter('all')}
className={`rounded-full px-3 py-1 text-xs border transition-colors ${clsFilter === 'all' ? 'bg-primary/10 border-primary text-ink font-medium' : 'bg-card border-border text-ink-faint'}`}>
Toate ({archived.length})
</button>
{Object.entries(classBreakdown).map(([cls, count]) => (
<button key={cls} onClick={() => setClsFilter(cls)}
className={`rounded-full px-3 py-1 text-xs border transition-colors ${clsFilter === cls ? 'bg-primary/10 border-primary text-ink font-medium' : 'bg-card border-border text-ink-faint'}`}>
{cls.toUpperCase()} ({count})
</button>
))}
</div>
{/* Search */}
<input placeholder="Caută în arhivă…" value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full rounded-lg border bg-card px-4 py-2.5 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
{/* Documents list */}
{isLoading ? (
<div className="text-center text-sm text-ink-faint py-6">Se încarcă</div>
) : filtered.length === 0 ? (
<div className="card p-8 text-center space-y-2">
<p className="text-2xl">📦</p>
<p className="text-sm text-ink-faint">
{archived.length === 0 ? 'Nicio arhivă. Documentele mai vechi de 1 an apar automat.' : 'Niciun rezultat.'}
</p>
</div>
) : (
<div className="card divide-y divide-border/50">
{filtered.map((d) => (
<div key={d.id} className="flex items-center gap-3 p-4">
<span className="text-xl">
{d.fileType?.includes('pdf') ? '📄' : d.fileType?.includes('image') ? '🖼️' : '📁'}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-ink truncate">{d.title}</p>
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
{d.classification && (
<span className={`rounded-full px-1.5 py-0.5 text-[9px] font-bold ${CLS_CLS[d.classification] ?? 'bg-muted text-ink-faint'}`}>
{d.classification}
</span>
)}
{d.fileType && <span className="text-[10px] text-ink-faint uppercase">{d.fileType}</span>}
{d.expiresAt && (
<span className="text-[10px] text-ink-faint">
exp: {new Date(d.expiresAt).toLocaleDateString('ro-RO', { year: 'numeric', month: 'short' })}
</span>
)}
</div>
</div>
<div className="text-right space-y-1 shrink-0">
<p className="text-[10px] text-ink-faint">
{new Date(d.createdAt).toLocaleDateString('ro-RO', { year: 'numeric', month: 'short' })}
</p>
</div>
</div>
))}
</div>
)}
</div>
);
}