feat(CC-074): add Document Archive page (docs > 1yr or tagged "archived")
This commit is contained in:
parent
5b6b012e53
commit
3a3c8dae8c
1 changed files with 137 additions and 0 deletions
137
src/app/dashboard/documents/archive/page.tsx
Normal file
137
src/app/dashboard/documents/archive/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue