feat(CC-078): add Shared Documents page (docs tagged shared/partajat with GDPR warning for C3/C4)
This commit is contained in:
parent
6a7adb6f5d
commit
fffc37baab
1 changed files with 146 additions and 0 deletions
146
src/app/dashboard/documents/shared/page.tsx
Normal file
146
src/app/dashboard/documents/shared/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue