/** * Sales-triage urgency badges for interest list rows + cards. * * Derived purely from the dates we already return on the row, so this is a * pure function - no DB hits, no extra fetch. Mirrors the logic the * server-side alert-rules engine uses, but for at-a-glance rendering on * the list itself. */ const SILENT_DAYS_THRESHOLD = 7; const EOI_AWAITING_DAYS_THRESHOLD = 14; const DEPOSIT_PENDING_DAYS_THRESHOLD = 21; const ACTIVE_MID_FUNNEL_STAGES = new Set(['details_sent', 'in_communication']); export interface InterestUrgencyInput { pipelineStage: string; outcome?: string | null; archivedAt?: string | null; dateLastContact?: string | null; updatedAt?: string; dateEoiSent?: string | null; eoiStatus?: string | null; dateDepositReceived?: string | null; } export interface UrgencyBadge { /** Stable id for keying. */ id: 'silent' | 'eoi_awaiting' | 'deposit_pending'; label: string; /** Long form for tooltip / aria-label. */ detail: string; /** Tailwind classes for the pill. */ className: string; } function daysSince(iso: string | null | undefined): number | null { if (!iso) return null; const t = new Date(iso).getTime(); if (Number.isNaN(t)) return null; return Math.floor((Date.now() - t) / 86_400_000); } export function computeUrgencyBadges(row: InterestUrgencyInput): UrgencyBadge[] { // Closed / archived interests don't need triage signals. if (row.archivedAt || row.outcome) return []; const badges: UrgencyBadge[] = []; // Silent in mid-funnel stages - most actionable. if (ACTIVE_MID_FUNNEL_STAGES.has(row.pipelineStage)) { const lastTouchIso = row.dateLastContact ?? row.updatedAt ?? null; const days = daysSince(lastTouchIso); if (days !== null && days >= SILENT_DAYS_THRESHOLD) { badges.push({ id: 'silent', label: `Silent ${days}d`, detail: `No contact in ${days} days`, className: 'bg-amber-100 text-amber-800 border border-amber-200', }); } } // EOI sent but not signed for too long. if (row.eoiStatus === 'waiting_for_signatures') { const days = daysSince(row.dateEoiSent); if (days !== null && days >= EOI_AWAITING_DAYS_THRESHOLD) { badges.push({ id: 'eoi_awaiting', label: `EOI ${days}d`, detail: `EOI awaiting signature for ${days} days`, className: 'bg-orange-100 text-orange-800 border border-orange-200', }); } } // EOI signed but deposit not received. if (row.pipelineStage === 'eoi_signed' && !row.dateDepositReceived && row.dateEoiSent) { const days = daysSince(row.dateEoiSent); if (days !== null && days >= DEPOSIT_PENDING_DAYS_THRESHOLD) { badges.push({ id: 'deposit_pending', label: `Deposit ${days}d`, detail: `Awaiting deposit for ${days} days since EOI sent`, className: 'bg-rose-100 text-rose-800 border border-rose-200', }); } } return badges; }