Files
pn-new-crm/src/components/interests/urgency.ts
Matt Ciaccio 8699f81879
Some checks failed
Build & Push Docker Images / lint (push) Failing after 1m18s
Build & Push Docker Images / build-and-push (push) Has been skipped
chore(style): codebase em-dash sweep + minor layout polish
Replaces every em-dash and en-dash with regular ASCII hyphens
across comments, JSX strings, and dev-facing logs. Mostly cosmetic
but stops the inconsistent mix that crept in over the last few
months (some files used em-dashes in comments, others didn't,
some used both).

Bundles two small dashboard-layout tweaks that touch a couple of
already-modified files:
- (dashboard)/layout.tsx main padding goes from p-6 to pt-3 px-6
  pb-6 so page content sits closer to the topbar.
- Sidebar now receives the ports list it needs for the footer
  port switcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:57:01 +02:00

92 lines
2.9 KiB
TypeScript

/**
* 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;
}