From 06a3c375df02e81379db552d0ded222651a8360c Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 12:50:22 +0000 Subject: [PATCH] feat(CC-077): add Organizations (relationship view) with contact + contract linking --- .../relationships/organizations/page.tsx | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 src/app/dashboard/relationships/organizations/page.tsx diff --git a/src/app/dashboard/relationships/organizations/page.tsx b/src/app/dashboard/relationships/organizations/page.tsx new file mode 100644 index 0000000..2d54635 --- /dev/null +++ b/src/app/dashboard/relationships/organizations/page.tsx @@ -0,0 +1,194 @@ +'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 Organization { + id: string; name: string; industry: string | null; country: string | null; + employeeCount: number | null; tags: string[]; website: string | null; + notes: string | null; createdAt: string; +} +interface Contact { + id: string; fullName: string; role: string | null; tags: string[]; +} +interface Contract { + id: string; title: string; status: string; counterpartyName: string | null; + expiresAt: string | null; +} + +export default function OrganizationsRelPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const [search, setSearch] = useState(''); + const [industry, setIndustry] = useState('all'); + const [selected, setSelected] = useState(null); + + const { data: orgs = [], isLoading } = useQuery({ + queryKey: ['orgs-rel', tenantId], + queryFn: () => apiFetch('/v1/organizations?limit=500', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + const { data: contacts = [] } = useQuery({ + queryKey: ['contacts-rel2', tenantId], + queryFn: () => apiFetch('/v1/contacts?limit=500', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + const { data: contracts = [] } = useQuery({ + queryKey: ['contracts-rel', tenantId], + queryFn: () => apiFetch('/v1/contracts?limit=200', { tenantId }), + enabled: Boolean(tenantId), staleTime: 120_000, + }); + + const industries = useMemo(() => { + const set = new Set(orgs.map((o) => o.industry).filter(Boolean) as string[]); + return ['all', ...Array.from(set).sort()]; + }, [orgs]); + + const filtered = orgs.filter((o) => { + if (industry !== 'all' && o.industry !== industry) return false; + if (search && !o.name.toLowerCase().includes(search.toLowerCase())) return false; + return true; + }); + + // Contacts linked to selected org by tag (org name match) + const orgContacts = useMemo(() => { + if (!selected) return []; + const orgNameLower = selected.name.toLowerCase(); + return contacts.filter((c) => + c.tags.some((t) => t.toLowerCase() === orgNameLower || t.toLowerCase().includes(orgNameLower.split(' ')[0])) + ); + }, [selected, contacts]); + + const orgContracts = useMemo(() => { + if (!selected) return []; + return contracts.filter((c) => + (c.counterpartyName ?? '').toLowerCase().includes(selected.name.toLowerCase().split(' ')[0]) + ); + }, [selected, contracts]); + + return ( +
+
+
+

Organizații

+

+ {isLoading ? 'Se încarcă…' : `${orgs.length} organizații · vizualizare relații`} +

+
+ CRM complet → +
+ +
+ {/* List */} +
+ setSearch(e.target.value)} + className="w-full rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + +
+ {industries.slice(0, 6).map((ind) => ( + + ))} +
+ + {isLoading ? ( +
Se încarcă…
+ ) : ( +
+ {filtered.map((o) => ( + + ))} + {filtered.length === 0 && ( +

Nicio organizație.

+ )} +
+ )} +
+ + {/* Detail */} +
+ {!selected ? ( +
+

🏢

+

Selectează o organizație pentru a vedea relațiile.

+
+ ) : ( + <> +
+

{selected.name}

+
+ {selected.industry && 🏭 {selected.industry}} + {selected.country && 📍 {selected.country}} + {selected.employeeCount && 👥 {selected.employeeCount} angajați} + {selected.website && 🌐 Website} +
+ {selected.tags.length > 0 && ( +
+ {selected.tags.map((t) => ( + {t} + ))} +
+ )} + {selected.notes &&

{selected.notes}

} +
+ + {/* Linked contacts */} +
+

+ Contacte legate ({orgContacts.length}) +

+ {orgContacts.length === 0 ? ( +

Nicio legătură prin taguri. Tag-ează contacte cu numele organizației.

+ ) : orgContacts.map((c) => ( +
+
+ {c.fullName.split(' ').map(n => n[0]).slice(0,2).join('')} +
+
+

{c.fullName}

+ {c.role &&

{c.role}

} +
+
+ ))} +
+ + {/* Linked contracts */} +
+

+ Contracte ({orgContracts.length}) +

+ {orgContracts.length === 0 ? ( +

Niciun contract direct. Lookup după counterpartyName.

+ ) : orgContracts.map((c) => ( +
+

{c.title}

+
+ + {c.status} + + {c.expiresAt &&

{new Date(c.expiresAt).toLocaleDateString('ro-RO', { year: 'numeric', month: 'short' })}

} +
+
+ ))} +
+ + )} +
+
+
+ ); +}