From 365b17d71b7a0e32918dc8a8497435f54f2daeaa Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sat, 1 Aug 2026 21:20:56 +0000 Subject: [PATCH] feat(CC-063): add CRM & Contacts page (list/create, consent status, org link) --- src/app/dashboard/crm/page.tsx | 188 +++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 src/app/dashboard/crm/page.tsx diff --git a/src/app/dashboard/crm/page.tsx b/src/app/dashboard/crm/page.tsx new file mode 100644 index 0000000..1ac57de --- /dev/null +++ b/src/app/dashboard/crm/page.tsx @@ -0,0 +1,188 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +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; organizationId: string | null; source: string | null; + consentStatus: string; tags: string[]; notes: string | null; + createdAt: string; +} +interface Organization { id: string; name: string; } + +const CONSENT_META: Record = { + granted: { label: 'Consimțit', cls: 'bg-signal-ok/10 text-signal-ok' }, + revoked: { label: 'Revocat', cls: 'bg-signal-danger/10 text-signal-danger' }, + unknown: { label: 'Necunoscut', cls: 'bg-muted text-ink-faint' }, +}; + +export default function CrmPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [search, setSearch] = useState(''); + const [showCreate, setShowCreate] = useState(false); + const [form, setForm] = useState({ fullName: '', email: '', phone: '', role: '', notes: '' }); + + const { data: contacts = [], isLoading } = useQuery({ + queryKey: ['contacts', tenantId], + queryFn: () => apiFetch('/v1/contacts', { tenantId }), + enabled: Boolean(tenantId), + staleTime: 60_000, + }); + const { data: orgs = [] } = useQuery({ + queryKey: ['orgs', tenantId], + queryFn: () => apiFetch('/v1/organizations', { tenantId }), + enabled: Boolean(tenantId), + staleTime: 300_000, + }); + const orgMap = Object.fromEntries(orgs.map((o) => [o.id, o.name])); + + const { mutate: createContact, isPending } = useMutation({ + mutationFn: () => apiFetch('/v1/contacts', { method: 'POST', body: form, tenantId }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['contacts', tenantId] }); + setShowCreate(false); + setForm({ fullName: '', email: '', phone: '', role: '', notes: '' }); + }, + }); + + const filtered = contacts.filter((c) => + !search || + c.fullName.toLowerCase().includes(search.toLowerCase()) || + (c.email ?? '').toLowerCase().includes(search.toLowerCase()) || + (c.role ?? '').toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+
+
+

CRM & Contacte

+

{contacts.length} contacte înregistrate

+
+ +
+ + {/* Create form */} + {showCreate && ( +
+

Contact nou

+
+ {[ + { key: 'fullName', label: 'Nume complet *', placeholder: 'Ion Popescu' }, + { key: 'email', label: 'Email', placeholder: 'ion@example.com' }, + { key: 'phone', label: 'Telefon', placeholder: '+40 7xx xxx xxx' }, + { key: 'role', label: 'Rol / Funcție', placeholder: 'Director General' }, + ].map(({ key, label, placeholder }) => ( +
+ + )[key]} + onChange={(e) => setForm((f) => ({ ...f, [key]: e.target.value }))} + placeholder={placeholder} + className="w-full rounded-lg border bg-card px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" + /> +
+ ))} +
+
+ +