'use client'; import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { apiFetch } from '../../../../lib/api'; import { useSession } from '../../../../components/session-provider'; interface Opportunity { id: string; source: string; entityType: string | null; entityId: string | null; valueRangeMin: number | null; valueRangeMax: number | null; probability: number | null; nextAction: string | null; expiresAt: string | null; createdAt: string; } type ProbBucket = { label: string; range: [number, number]; cls: string; bgCls: string; }; const BUCKETS: ProbBucket[] = [ { label: 'Explorare', range: [0, 0.25], cls: 'text-ink-faint', bgCls: 'bg-muted/50 border-border/30' }, { label: 'Dezvoltare', range: [0.26, 0.5], cls: 'text-sky-500', bgCls: 'bg-sky-500/5 border-sky-500/20' }, { label: 'Calificare', range: [0.51, 0.75],cls: 'text-signal-warn', bgCls: 'bg-signal-warn/5 border-signal-warn/20' }, { label: 'Angajament', range: [0.76, 1], cls: 'text-signal-ok', bgCls: 'bg-signal-ok/5 border-signal-ok/20' }, ]; function fmtRange(min: number | null, max: number | null, currency = 'RON') { if (!min && !max) return null; const fmt = (v: number) => new Intl.NumberFormat('ro-RO', { style: 'currency', currency, maximumFractionDigits: 0 }).format(v / 100); if (min && max) return `${fmt(min)} – ${fmt(max)}`; if (min) return `>${fmt(min)}`; return `<${fmt(max!)}`; } function isExpired(expiresAt: string | null) { return expiresAt ? new Date(expiresAt) < new Date() : false; } export default function OpportunitiesPipelinePage() { const { activeTenant } = useSession(); const tenantId = activeTenant?.tenantId ?? ''; const qc = useQueryClient(); const [showCreate, setShowCreate] = useState(false); const [form, setForm] = useState({ source: '', entityType: '', valueMin: '', valueMax: '', probability: '0.5', nextAction: '', expiresAt: '' }); const { data: opportunities = [], isLoading } = useQuery({ queryKey: ['opportunities', tenantId], queryFn: () => apiFetch('/v1/opportunities', { tenantId }), enabled: Boolean(tenantId), staleTime: 30_000, }); const createMut = useMutation({ mutationFn: (body: Record) => apiFetch('/v1/opportunities', { tenantId, method: 'POST', body }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['opportunities', tenantId] }); setShowCreate(false); setForm({ source: '', entityType: '', valueMin: '', valueMax: '', probability: '0.5', nextAction: '', expiresAt: '' }); }, }); const patchMut = useMutation({ mutationFn: ({ id, ...body }: { id: string; probability?: number; nextAction?: string }) => apiFetch(`/v1/opportunities/${id}`, { tenantId, method: 'PATCH', body }), onSuccess: () => qc.invalidateQueries({ queryKey: ['opportunities', tenantId] }), }); const active = opportunities.filter((o) => !isExpired(o.expiresAt)); const expired = opportunities.filter((o) => isExpired(o.expiresAt)); const totalWeightedValue = active.reduce((sum, o) => { const mid = ((o.valueRangeMin ?? 0) + (o.valueRangeMax ?? o.valueRangeMin ?? 0)) / 2; return sum + mid * (o.probability ?? 0.5); }, 0); return (

Pipeline Oportunități

{isLoading ? 'Se încarcă…' : `${active.length} active`} {totalWeightedValue > 0 && ( · Valoare ponderată: {new Intl.NumberFormat('ro-RO', { style: 'currency', currency: 'RON', maximumFractionDigits: 0 }).format(totalWeightedValue / 100)} )}

{/* Create form */} {showCreate && (

Oportunitate nouă

setForm({ ...form, source: 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" />
setForm({ ...form, entityType: e.target.value })} className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
setForm({ ...form, probability: e.target.value })} className="flex-1" /> {Math.round(parseFloat(form.probability) * 100)}%
setForm({ ...form, valueMin: e.target.value })} className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> setForm({ ...form, valueMax: e.target.value })} className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
setForm({ ...form, nextAction: 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" />
setForm({ ...form, expiresAt: e.target.value })} className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" />
)} {/* Pipeline buckets */} {!isLoading && (
{BUCKETS.map((bucket) => { const items = active.filter((o) => { const p = o.probability ?? 0; return p >= bucket.range[0] && p <= bucket.range[1]; }); const bucketValue = items.reduce((s, o) => { const mid = ((o.valueRangeMin ?? 0) + (o.valueRangeMax ?? o.valueRangeMin ?? 0)) / 2; return s + mid; }, 0); return (

{bucket.label}

{Math.round(bucket.range[0]*100)}–{Math.round(bucket.range[1]*100)}%
{items.length} {bucketValue > 0 && ( {(bucketValue/100/1000).toFixed(0)}k )}
{items.length > 0 && (
{items.map((o) => (

{o.source}

{fmtRange(o.valueRangeMin, o.valueRangeMax) && (

{fmtRange(o.valueRangeMin, o.valueRangeMax)}

)} {o.nextAction && (

→ {o.nextAction}

)}
patchMut.mutate({ id: o.id, probability: parseFloat((e.target as HTMLInputElement).value) })} className="flex-1 h-1" /> {Math.round((o.probability ?? 0) * 100)}%
))}
)}
); })}
)} {/* Expired */} {expired.length > 0 && (

Expirate ({expired.length})

{expired.map((o) => (
{o.source} {o.expiresAt?.slice(0, 10)}
))}
)}
); }