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