diff --git a/src/app/dashboard/people/page.tsx b/src/app/dashboard/people/page.tsx new file mode 100644 index 0000000..4eabdee --- /dev/null +++ b/src/app/dashboard/people/page.tsx @@ -0,0 +1,210 @@ +'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 Contact { + id: string; fullName: string; email: string | null; phone: string | null; + role: string | null; tags: string[]; consentStatus: string; + notes: string | null; createdAt: string; updatedAt: string; +} + +const CONSENT_CONFIG: Record = { + granted: { label: 'GDPR granted', icon: '✅', cls: 'bg-signal-ok/10 text-signal-ok border-signal-ok/30' }, + revoked: { label: 'GDPR revoked', icon: '❌', cls: 'bg-signal-danger/10 text-signal-danger border-signal-danger/30' }, + unknown: { label: 'Necunoscut', icon: '❓', cls: 'bg-muted text-ink-faint border-border' }, + pending: { label: 'În așteptare', icon: '⏳', cls: 'bg-warn/10 text-warn border-warn/30' }, +}; + +export default function PeoplePage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + + const [search, setSearch] = useState(''); + const [consent, setConsent] = useState('all'); + const [showCreate, setShowCreate] = useState(false); + const [form, setForm] = useState({ fullName: '', email: '', phone: '', role: '', tags: '', notes: '', consentStatus: 'unknown' }); + const [selected, setSelected] = useState(null); + + const { data: contacts = [], isLoading } = useQuery({ + queryKey: ['people', tenantId], + queryFn: () => apiFetch('/v1/contacts?limit=500', { tenantId }), + enabled: Boolean(tenantId), staleTime: 30_000, + }); + + const createMut = useMutation({ + mutationFn: (body: Record) => + apiFetch('/v1/contacts', { tenantId, method: 'POST', body }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['people', tenantId] }); + setShowCreate(false); + setForm({ fullName: '', email: '', phone: '', role: '', tags: '', notes: '', consentStatus: 'unknown' }); + }, + }); + + const updateConsentMut = useMutation({ + mutationFn: ({ id, consentStatus }: { id: string; consentStatus: string }) => + apiFetch(`/v1/contacts/${id}`, { tenantId, method: 'PATCH', body: { consentStatus } }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['people', tenantId] }); + setSelected(null); + }, + }); + + const filtered = contacts.filter((c) => { + if (consent !== 'all' && c.consentStatus !== consent) return false; + if (search && !c.fullName.toLowerCase().includes(search.toLowerCase()) + && !(c.email ?? '').toLowerCase().includes(search.toLowerCase()) + && !(c.role ?? '').toLowerCase().includes(search.toLowerCase())) return false; + return true; + }); + + const counts: Record = { all: contacts.length }; + for (const c of contacts) counts[c.consentStatus] = (counts[c.consentStatus] ?? 0) + 1; + + return ( +
+
+
+

Persoane

+

+ {isLoading ? 'Se încarcă…' : `${contacts.length} persoane · ${counts['granted'] ?? 0} cu consimțământ GDPR`} +

+
+ +
+ + {/* GDPR warning for revoked contacts */} + {(counts['revoked'] ?? 0) > 0 && ( +
+ ⚠️ +
+

+ {counts['revoked']} persoane cu GDPR revocat +

+

Datele acestor persoane ar trebui șterse conform politicii de retenție.

+
+ + Gestionează → + +
+ )} + + {/* Consent filter */} +
+ + {Object.entries(CONSENT_CONFIG).map(([key, cfg]) => ( + + ))} +
+ + {/* Search */} + 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" /> + + {/* Create form */} + {showCreate && ( +
+

Persoană nouă

+
+ setForm({ ...form, fullName: e.target.value })} + className="col-span-2 rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm({ ...form, email: 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" /> + setForm({ ...form, phone: 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" /> + setForm({ ...form, role: 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" /> + + setForm({ ...form, tags: 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" /> + setForm({ ...form, notes: e.target.value })} + className="col-span-2 rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+
+ + +
+
+ )} + + {/* People list */} + {isLoading ? ( +
Se încarcă…
+ ) : filtered.length === 0 ? ( +
+

👥

+

Nicio persoană{search ? ' pentru această căutare' : ''}.

+
+ ) : ( +
+ {filtered.map((c) => { + const cfg = CONSENT_CONFIG[c.consentStatus] ?? CONSENT_CONFIG.unknown; + return ( +
+ {/* Avatar */} +
+ {c.fullName.split(' ').map(n => n[0]).slice(0, 2).join('').toUpperCase()} +
+
+

{c.fullName}

+
+ {c.role && {c.role}} + {c.email && {c.email}} + {c.tags.slice(0, 3).map((t) => ( + {t} + ))} +
+
+
+ + {cfg.icon} {c.consentStatus} + + {/* Quick consent toggle */} + {c.consentStatus !== 'granted' && ( + + )} +
+ + Profil → + +
+ ); + })} +
+ )} +
+ ); +}