ceo-web/src/app/dashboard/opportunities/page.tsx

132 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '../../../lib/api';
import type { Opportunity } from '../../../lib/api';
import { useSession } from '../../../components/session-provider';
function probBadge(p: string | null): string {
const n = Number(p ?? 0);
if (n >= 0.7) return 'bg-signal-ok text-white';
if (n >= 0.4) return 'bg-signal-warn text-white';
return 'bg-paper-sunken text-ink-faint';
}
function fmtCurrency(minor: string | null): string {
if (!minor) return '—';
return new Intl.NumberFormat('ro-RO', { maximumFractionDigits: 0 }).format(Number(minor) / 100);
}
export default function OpportunitiesPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data: opportunities = [], isLoading } = useQuery({
queryKey: ['opportunities', tenantId],
queryFn: () => apiFetch<Opportunity[]>('/v1/opportunities', { tenantId }),
enabled: Boolean(tenantId),
staleTime: 30_000,
});
const now = new Date();
const expiringSoon = opportunities.filter(
(o) => o.expiresAt && new Date(o.expiresAt) < new Date(now.getTime() + 7 * 86400_000),
);
const highProb = opportunities.filter((o) => Number(o.probability ?? 0) >= 0.7);
return (
<div className="max-w-5xl space-y-6">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Opportunity Inbox</h1>
<p className="mt-1 text-sm text-ink-faint">
Oportunități detectate {' '}
{isLoading ? '…' : `${opportunities.length} active`}
</p>
</div>
{isLoading ? (
<p className="text-sm text-ink-faint">Se încarcă</p>
) : (
<>
{/* Quick stats */}
<div className="grid grid-cols-3 gap-4">
<div className="card p-4">
<p className="text-xs text-ink-faint">Total</p>
<p className="mt-1 font-display text-2xl font-semibold text-ink">{opportunities.length}</p>
</div>
<div className="card p-4">
<p className="text-xs text-ink-faint">Probabilitate ridicată</p>
<p className="mt-1 font-display text-2xl font-semibold text-signal-ok">{highProb.length}</p>
</div>
<div className={`card p-4 ${expiringSoon.length > 0 ? 'border-l-2 border-l-signal-warn' : ''}`}>
<p className="text-xs text-ink-faint">Expiră în 7 zile</p>
<p className={`mt-1 font-display text-2xl font-semibold ${expiringSoon.length > 0 ? 'text-signal-warn' : 'text-ink'}`}>
{expiringSoon.length}
</p>
</div>
</div>
{opportunities.length === 0 ? (
<div className="card p-10 text-center">
<p className="text-sm font-medium text-ink">Nicio oportunitate detectată</p>
<p className="mt-1 text-xs text-ink-faint">
Oportunitățile sunt detectate automat din activitate și research.
</p>
</div>
) : (
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-paper-sunken text-left">
<th className="px-4 py-3 text-xs font-medium text-ink-faint">Sursă</th>
<th className="px-4 py-3 text-xs font-medium text-ink-faint">Entitate</th>
<th className="px-4 py-3 text-xs font-medium text-ink-faint">Probabilitate</th>
<th className="px-4 py-3 text-right text-xs font-medium text-ink-faint">Valoare estimată</th>
<th className="px-4 py-3 text-xs font-medium text-ink-faint">Expirare</th>
<th className="px-4 py-3 text-xs font-medium text-ink-faint">Acțiune</th>
</tr>
</thead>
<tbody>
{opportunities
.sort((a, b) => Number(b.probability ?? 0) - Number(a.probability ?? 0))
.map((o) => {
const prob = Number(o.probability ?? 0);
const isExpiring = o.expiresAt && new Date(o.expiresAt) < new Date(now.getTime() + 7 * 86400_000);
return (
<tr key={o.id} className={`border-b border-paper-sunken last:border-0 hover:bg-paper-sunken/40 ${isExpiring ? 'bg-signal-warn/5' : ''}`}>
<td className="px-4 py-2.5 text-sm text-ink capitalize">{o.source.replace(/_/g, ' ')}</td>
<td className="px-4 py-2.5 text-sm text-ink-faint">
{o.entityType ? (
<span className="rounded bg-paper-sunken px-1.5 py-0.5 font-mono text-[10px]">{o.entityType}</span>
) : '—'}
</td>
<td className="px-4 py-2.5">
<span className={`rounded-full px-2 py-0.5 text-[10px] font-semibold ${probBadge(o.probability)}`}>
{prob > 0 ? `${Math.round(prob * 100)}%` : '—'}
</span>
</td>
<td className="px-4 py-2.5 text-right font-mono text-xs text-ink">
{o.valueRangeMin || o.valueRangeMax ? (
`${fmtCurrency(o.valueRangeMin)} ${fmtCurrency(o.valueRangeMax)}`
) : '—'}
</td>
<td className={`px-4 py-2.5 text-xs ${isExpiring ? 'font-semibold text-signal-warn' : 'text-ink-faint'}`}>
{o.expiresAt ? new Date(o.expiresAt).toLocaleDateString('ro-RO') : '—'}
</td>
<td className="px-4 py-2.5 text-xs text-ink">
{o.nextAction ?? '—'}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
</>
)}
</div>
);
}