feat(CC-085): add Trust References page (contacts with reference/recomandare tags)
This commit is contained in:
parent
7d782ddf0e
commit
14416eb20e
1 changed files with 126 additions and 0 deletions
126
src/app/dashboard/trust/references/page.tsx
Normal file
126
src/app/dashboard/trust/references/page.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQuery, useMutation, 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; firstName: string; lastName: string | null; role: string | null; organization: string | null; tags: string[]; email: string | null; phone: string | null; linkedinUrl: string | null; createdAt: string; }
|
||||
|
||||
const REF_TAGS = ['reference', 'referinta', 'referința', 'testimonial', 'recomandare', 'recommendation'];
|
||||
|
||||
export default function TrustReferencesPage() {
|
||||
const { activeTenant } = useSession();
|
||||
const tenantId = activeTenant?.tenantId ?? '';
|
||||
const qc = useQueryClient();
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
|
||||
const { data: contacts = [], isLoading } = useQuery({
|
||||
queryKey: ['refs', tenantId],
|
||||
queryFn: () => apiFetch<Contact[]>('/v1/contacts?limit=500', { tenantId }),
|
||||
enabled: Boolean(tenantId), staleTime: 60_000,
|
||||
});
|
||||
|
||||
const references = useMemo(() =>
|
||||
contacts.filter((c) => c.tags.some((t) => REF_TAGS.includes(t.toLowerCase())))
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
|
||||
[contacts]);
|
||||
|
||||
const [form, setForm] = useState({ firstName: '', lastName: '', role: '', organization: '', email: '', linkedinUrl: '', note: '' });
|
||||
|
||||
const addMut = useMutation({
|
||||
mutationFn: () => apiFetch('/v1/contacts', { tenantId, method: 'POST', body: {
|
||||
firstName: form.firstName, lastName: form.lastName || undefined,
|
||||
role: form.role || undefined, organization: form.organization || undefined,
|
||||
email: form.email || undefined, linkedinUrl: form.linkedinUrl || undefined,
|
||||
tags: ['reference', 'recomandare'],
|
||||
notes: form.note || undefined,
|
||||
}}),
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ['refs', tenantId] }); setShowAdd(false); setForm({ firstName: '', lastName: '', role: '', organization: '', email: '', linkedinUrl: '', note: '' }); },
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl space-y-6 p-6">
|
||||
<div className="flex items-start justify-between flex-wrap gap-3">
|
||||
<div>
|
||||
<nav className="text-xs text-ink-faint mb-1">
|
||||
<Link href="/dashboard/trust" className="hover:underline">Trust Dashboard</Link> / Referințe
|
||||
</nav>
|
||||
<h1 className="font-display text-2xl font-semibold text-ink">Referințe & Recomandări</h1>
|
||||
<p className="text-sm text-ink-faint mt-1">
|
||||
{isLoading ? 'Se încarcă…' : `${references.length} contacte care pot oferi referințe`}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => setShowAdd(true)}
|
||||
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary/90">
|
||||
+ Adaugă referință
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="card p-3 bg-primary/5 border-primary/20">
|
||||
<p className="text-[10px] text-ink-faint">
|
||||
Contactele cu tag-ul <code className="bg-muted px-1 rounded">reference</code> sau <code className="bg-muted px-1 rounded">recomandare</code> apar automat.
|
||||
Poți adăuga tag-ul și din pagina de <Link href="/dashboard/relationships/contacts" className="text-primary hover:underline">Contacte</Link>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{showAdd && (
|
||||
<div className="card p-5 space-y-3">
|
||||
<p className="text-sm font-semibold text-ink">Referință nouă</p>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<input placeholder="Prenume *" value={form.firstName} onChange={(e) => setForm((p) => ({ ...p, firstName: e.target.value }))}
|
||||
className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
|
||||
<input placeholder="Nume" value={form.lastName} onChange={(e) => setForm((p) => ({ ...p, lastName: e.target.value }))}
|
||||
className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
|
||||
<input placeholder="Rol / Titlu" value={form.role} onChange={(e) => setForm((p) => ({ ...p, role: e.target.value }))}
|
||||
className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
|
||||
<input placeholder="Companie / Organizație" value={form.organization} onChange={(e) => setForm((p) => ({ ...p, organization: e.target.value }))}
|
||||
className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
|
||||
<input placeholder="Email" type="email" value={form.email} onChange={(e) => setForm((p) => ({ ...p, email: e.target.value }))}
|
||||
className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
|
||||
<input placeholder="LinkedIn URL" value={form.linkedinUrl} onChange={(e) => setForm((p) => ({ ...p, linkedinUrl: e.target.value }))}
|
||||
className="rounded-lg border bg-background px-3 py-2 text-sm text-ink font-mono text-xs focus:outline-none focus:ring-2 focus:ring-ring" />
|
||||
<input placeholder="Notă context (în ce context te recomandă)" value={form.note} onChange={(e) => setForm((p) => ({ ...p, note: e.target.value }))}
|
||||
className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring sm:col-span-2" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => addMut.mutate()} disabled={!form.firstName || addMut.isPending}
|
||||
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white disabled:opacity-50">
|
||||
{addMut.isPending ? 'Se salvează…' : 'Salvează'}
|
||||
</button>
|
||||
<button onClick={() => setShowAdd(false)} className="rounded-lg border px-4 py-2 text-sm text-ink">Anulează</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center text-sm text-ink-faint py-8">Se încarcă…</div>
|
||||
) : references.length === 0 ? (
|
||||
<div className="card p-8 text-center space-y-2">
|
||||
<p className="text-2xl">📋</p>
|
||||
<p className="text-sm text-ink-faint">Nicio referință. Adaugă contacte care pot confirma experiența ta.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{references.map((ref) => (
|
||||
<div key={ref.id} className="card p-4 space-y-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-ink">{ref.firstName} {ref.lastName ?? ''}</p>
|
||||
{ref.role && <p className="text-[10px] text-ink-faint">{ref.role}</p>}
|
||||
{ref.organization && <p className="text-[10px] text-ink-faint">{ref.organization}</p>}
|
||||
</div>
|
||||
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-[9px] font-bold text-primary">REF</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 text-[10px] text-ink-faint">
|
||||
{ref.email && <a href={`mailto:${ref.email}`} className="text-primary hover:underline">{ref.email}</a>}
|
||||
{ref.linkedinUrl && <a href={ref.linkedinUrl} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">LinkedIn →</a>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue