ceo-web/src/app/dashboard/relationships/organizations/page.tsx

194 lines
8.9 KiB
TypeScript

'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<Organization | null>(null);
const { data: orgs = [], isLoading } = useQuery({
queryKey: ['orgs-rel', tenantId],
queryFn: () => apiFetch<Organization[]>('/v1/organizations?limit=500', { tenantId }),
enabled: Boolean(tenantId), staleTime: 60_000,
});
const { data: contacts = [] } = useQuery({
queryKey: ['contacts-rel2', tenantId],
queryFn: () => apiFetch<Contact[]>('/v1/contacts?limit=500', { tenantId }),
enabled: Boolean(tenantId), staleTime: 60_000,
});
const { data: contracts = [] } = useQuery({
queryKey: ['contracts-rel', tenantId],
queryFn: () => apiFetch<Contract[]>('/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 (
<div className="max-w-5xl p-6">
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Organizații</h1>
<p className="text-sm text-ink-faint mt-1">
{isLoading ? 'Se încarcă…' : `${orgs.length} organizații · vizualizare relații`}
</p>
</div>
<Link href="/dashboard/crm" className="text-sm text-primary hover:underline">CRM complet </Link>
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-5">
{/* List */}
<div className="lg:col-span-2 space-y-3">
<input placeholder="Caută organizație…" value={search}
onChange={(e) => 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" />
<div className="flex flex-wrap gap-1">
{industries.slice(0, 6).map((ind) => (
<button key={ind} onClick={() => setIndustry(ind)}
className={`rounded-full px-2 py-0.5 text-[10px] border ${industry === ind ? 'bg-primary/10 border-primary text-ink' : 'bg-card border-border text-ink-faint'}`}>
{ind === 'all' ? 'Toate' : ind}
</button>
))}
</div>
{isLoading ? (
<div className="text-center text-sm text-ink-faint py-6">Se încarcă</div>
) : (
<div className="card divide-y divide-border/50 max-h-[500px] overflow-y-auto">
{filtered.map((o) => (
<button key={o.id} onClick={() => setSelected(o)}
className={`w-full text-left p-3 transition-colors hover:bg-muted/50 ${selected?.id === o.id ? 'bg-primary/5' : ''}`}>
<p className="text-sm font-medium text-ink">{o.name}</p>
<div className="flex gap-2 mt-0.5">
{o.industry && <span className="text-[10px] text-ink-faint">{o.industry}</span>}
{o.country && <span className="text-[10px] text-ink-faint">📍 {o.country}</span>}
{o.employeeCount && <span className="text-[10px] text-ink-faint">👥 {o.employeeCount}</span>}
</div>
</button>
))}
{filtered.length === 0 && (
<p className="p-6 text-center text-sm text-ink-faint">Nicio organizație.</p>
)}
</div>
)}
</div>
{/* Detail */}
<div className="lg:col-span-3 space-y-4">
{!selected ? (
<div className="card p-8 text-center space-y-2">
<p className="text-2xl">🏢</p>
<p className="text-sm text-ink-faint">Selectează o organizație pentru a vedea relațiile.</p>
</div>
) : (
<>
<div className="card p-5 space-y-2">
<h2 className="text-base font-semibold text-ink">{selected.name}</h2>
<div className="flex flex-wrap gap-3 text-xs text-ink-faint">
{selected.industry && <span>🏭 {selected.industry}</span>}
{selected.country && <span>📍 {selected.country}</span>}
{selected.employeeCount && <span>👥 {selected.employeeCount} angajați</span>}
{selected.website && <a href={selected.website} target="_blank" rel="noopener" className="text-primary hover:underline">🌐 Website</a>}
</div>
{selected.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{selected.tags.map((t) => (
<span key={t} className="rounded-full bg-primary/10 px-2 py-0.5 text-[9px] text-ink">{t}</span>
))}
</div>
)}
{selected.notes && <p className="text-xs text-ink-faint border-t border-border/50 pt-2">{selected.notes}</p>}
</div>
{/* Linked contacts */}
<div className="card p-4 space-y-2">
<p className="text-[10px] font-semibold uppercase tracking-wider text-ink-faint">
Contacte legate ({orgContacts.length})
</p>
{orgContacts.length === 0 ? (
<p className="text-xs text-ink-faint">Nicio legătură prin taguri. Tag-ează contacte cu numele organizației.</p>
) : orgContacts.map((c) => (
<div key={c.id} className="flex items-center gap-2 py-1 border-t border-border/30">
<div className="w-7 h-7 rounded-full bg-primary/10 flex items-center justify-center text-xs font-bold text-primary">
{c.fullName.split(' ').map(n => n[0]).slice(0,2).join('')}
</div>
<div>
<p className="text-xs font-medium text-ink">{c.fullName}</p>
{c.role && <p className="text-[10px] text-ink-faint">{c.role}</p>}
</div>
</div>
))}
</div>
{/* Linked contracts */}
<div className="card p-4 space-y-2">
<p className="text-[10px] font-semibold uppercase tracking-wider text-ink-faint">
Contracte ({orgContracts.length})
</p>
{orgContracts.length === 0 ? (
<p className="text-xs text-ink-faint">Niciun contract direct. Lookup după counterpartyName.</p>
) : orgContracts.map((c) => (
<div key={c.id} className="flex items-center justify-between py-1 border-t border-border/30">
<p className="text-xs text-ink">{c.title}</p>
<div className="text-right">
<span className={`text-[9px] rounded-full px-1.5 py-0.5 ${c.status === 'active' ? 'bg-signal-ok/10 text-signal-ok' : 'bg-muted text-ink-faint'}`}>
{c.status}
</span>
{c.expiresAt && <p className="text-[9px] text-ink-faint">{new Date(c.expiresAt).toLocaleDateString('ro-RO', { year: 'numeric', month: 'short' })}</p>}
</div>
</div>
))}
</div>
</>
)}
</div>
</div>
</div>
);
}