From 006a4f0562acd2bb0ea945236fe6a4643651ac81 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sat, 1 Aug 2026 10:09:29 +0000 Subject: [PATCH] feat(cc-051): Opportunity Inbox page --- src/app/dashboard/opportunities/page.tsx | 132 +++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/app/dashboard/opportunities/page.tsx diff --git a/src/app/dashboard/opportunities/page.tsx b/src/app/dashboard/opportunities/page.tsx new file mode 100644 index 0000000..37181b3 --- /dev/null +++ b/src/app/dashboard/opportunities/page.tsx @@ -0,0 +1,132 @@ +'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('/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 ( +
+
+

Opportunity Inbox

+

+ Oportunități detectate —{' '} + {isLoading ? '…' : `${opportunities.length} active`} +

+
+ + {isLoading ? ( +

Se încarcă…

+ ) : ( + <> + {/* Quick stats */} +
+
+

Total

+

{opportunities.length}

+
+
+

Probabilitate ridicată

+

{highProb.length}

+
+
0 ? 'border-l-2 border-l-signal-warn' : ''}`}> +

Expiră în 7 zile

+

0 ? 'text-signal-warn' : 'text-ink'}`}> + {expiringSoon.length} +

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

Nicio oportunitate detectată

+

+ Oportunitățile sunt detectate automat din activitate și research. +

+
+ ) : ( +
+
+ + + + + + + + + + + + + {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 ( + + + + + + + + + ); + })} + +
SursăEntitateProbabilitateValoare estimatăExpirareAcțiune
{o.source.replace(/_/g, ' ')} + {o.entityType ? ( + {o.entityType} + ) : '—'} + + + {prob > 0 ? `${Math.round(prob * 100)}%` : '—'} + + + {o.valueRangeMin || o.valueRangeMax ? ( + `${fmtCurrency(o.valueRangeMin)} – ${fmtCurrency(o.valueRangeMax)}` + ) : '—'} + + {o.expiresAt ? new Date(o.expiresAt).toLocaleDateString('ro-RO') : '—'} + + {o.nextAction ?? '—'} +
+
+
+ )} + + )} +
+ ); +}