feat(CC-078): add Shared Documents page (docs tagged shared/partajat with GDPR warning for C3/C4)

This commit is contained in:
admin-valentin 2026-08-02 12:53:51 +00:00
parent 6a7adb6f5d
commit fffc37baab

View file

@ -0,0 +1,146 @@
'use client';
import { useMemo, useState } from 'react';
import { useQuery } 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; type: string | null; classification: string | null;
tags: string[]; status: string | null; createdAt: string; updatedAt: string;
}
const SHARED_TAGS = ['shared', 'partajat', 'public', 'distribuit', 'trimis'];
function isShared(doc: Document) {
const tags = doc.tags.map((t) => t.toLowerCase());
const status = (doc.status ?? '').toLowerCase();
return tags.some((t) => SHARED_TAGS.includes(t)) || status === 'shared' || status === 'sent';
}
const CLS_CLS: Record<string, string> = {
C0: 'bg-signal-ok/10 text-signal-ok',
C1: 'bg-primary/10 text-primary',
C2: 'bg-warn/10 text-warn',
C3: 'bg-signal-danger/10 text-signal-danger',
C4: 'bg-signal-danger/20 text-signal-danger',
};
export default function SharedDocsPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const [search, setSearch] = useState('');
const [clsFilter, setClsFilter] = useState('all');
const { data: allDocs = [], isLoading } = useQuery({
queryKey: ['docs-shared', tenantId],
queryFn: () => apiFetch<Document[]>('/v1/documents?limit=500', { tenantId }),
enabled: Boolean(tenantId), staleTime: 60_000,
});
const shared = useMemo(() => allDocs.filter(isShared), [allDocs]);
const filtered = useMemo(() => {
return shared.filter((d) => {
if (clsFilter !== 'all' && d.classification !== clsFilter) return false;
if (search && !d.title.toLowerCase().includes(search.toLowerCase())) return false;
return true;
});
}, [shared, clsFilter, search]);
const classGroups = useMemo(() => {
const counts: Record<string, number> = {};
for (const d of shared) {
const cls = d.classification ?? 'C1';
counts[cls] = (counts[cls] ?? 0) + 1;
}
return counts;
}, [shared]);
return (
<div className="max-w-4xl space-y-6 p-6">
<div className="flex items-start justify-between gap-3">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Documente Partajate</h1>
<p className="text-sm text-ink-faint mt-1">
{isLoading ? 'Se încarcă…' : `${shared.length} documente partajate (tag: shared / partajat / public)`}
</p>
</div>
<Link href="/dashboard/documents" className="text-sm text-primary hover:underline">Toate documentele </Link>
</div>
{shared.length === 0 && !isLoading && (
<div className="card p-4 bg-primary/5 border-primary/20">
<p className="text-xs text-ink">
Pentru a marca un document ca partajat, adaugă tagul <code className="bg-muted px-1 rounded">shared</code> sau <code className="bg-muted px-1 rounded">partajat</code> documentului.
</p>
</div>
)}
{/* Classification breakdown */}
{shared.length > 0 && (
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{Object.entries(classGroups).sort().map(([cls, count]) => (
<div key={cls} className={`card p-3 text-center ${CLS_CLS[cls] ?? 'bg-muted text-ink-faint'}`}>
<p className="text-xl font-bold">{count}</p>
<p className="text-[10px] font-semibold">{cls}</p>
</div>
))}
</div>
)}
{shared.length > 0 && (
<div className="card p-3 bg-warn/5 border-warn/30">
<p className="text-xs text-warn">
Documentele C3/C4 partajate extern necesită revizuire GDPR. Verifică dacă destinatarii au dreptul de acces.
</p>
</div>
)}
<div className="flex flex-wrap gap-2">
<input placeholder="Caută…" value={search} onChange={(e) => setSearch(e.target.value)}
className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
<div className="flex gap-1">
{['all', 'C0', 'C1', 'C2', 'C3', 'C4'].map((cls) => (
<button key={cls} onClick={() => setClsFilter(cls)}
className={`rounded-full border px-2 py-0.5 text-[10px] ${clsFilter === cls ? 'bg-primary/10 border-primary text-ink' : 'bg-card border-border text-ink-faint'}`}>
{cls === 'all' ? 'Toate' : cls}
</button>
))}
</div>
</div>
{isLoading ? (
<div className="text-center text-sm text-ink-faint py-8">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">Niciun document partajat.</p>
</div>
) : (
<div className="card divide-y divide-border/50">
{filtered.map((doc) => (
<div key={doc.id} className="flex items-center gap-4 p-4">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-ink">{doc.title}</p>
<div className="flex flex-wrap gap-2 mt-0.5 text-[10px] text-ink-faint">
{doc.type && <span>{doc.type}</span>}
<span>{new Date(doc.updatedAt).toLocaleDateString('ro-RO')}</span>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
{doc.tags.filter((t) => SHARED_TAGS.includes(t.toLowerCase())).map((t) => (
<span key={t} className="rounded-full bg-signal-ok/10 text-signal-ok px-2 py-0.5 text-[9px] font-semibold">{t}</span>
))}
<span className={`rounded-full px-2 py-0.5 text-[9px] font-bold ${CLS_CLS[doc.classification ?? 'C1'] ?? 'bg-muted text-ink-faint'}`}>
{doc.classification ?? 'C1'}
</span>
</div>
</div>
))}
</div>
)}
</div>
);
}