Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | 2x 68x 268x 68x 268x 67x 68x 245x 68x 245x 245x 126x 245x 61x 59x 59x 59x 295x | import type { KanbanData } from "../../lib/api/pro/crm";
import { abbreviatePaise } from "../../lib/currency-utils";
import { overdueLeads } from "./lead-overdue";
import { isTerminalStage } from "./lead-status";
/**
* The figures, printed before the board.
*
* **Figures and rows lead** — so the Leads page opens on five numbers, not on
* a wall of cards. `.statstrip` in `.context/mockups/_base.css`: one ruled
* strip, each figure a `.label` and a tabular `.num` on one baseline, ₹
* glyphed, no cards and no boxes.
*
* EVERY FIGURE IS DERIVED FROM THE KANBAN PAYLOAD THE PAGE ALREADY HAS.
* No second request, and — more importantly — no second definition. "Overdue"
* is `overdueLeads()` from `lead-overdue.ts`, the same function the band below
* and `/` (Today) call, so the figure and the rows under it can never
* disagree. "Open" is "not in a terminal stage", which is the same
* `isTerminalStage()` the columns use.
*
* ONE SUBSTITUTION FROM THE MOCKUP, STATED
* The mockup's fifth figure is "Won this month". The kanban payload carries no
* stage-entry date, only `dateUpdated`, and a won lead whose phone number was
* corrected last Tuesday would be counted as won last Tuesday. Rather than
* print a number that is quietly wrong, the figure is "Won" — every won lead's
* value, which IS exact. `/insights/pipeline` already owns period-scoped
* figures (`useCrmAnalytics`, `wonValueInPeriod`) and is where a month-scoped
* version belongs.
*/
/** Seven days, the window "added this week" means. */
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
export interface LeadsFigures {
open: number;
overdue: number;
addedThisWeek: number;
/** Paise. Quote value across every open lead. */
pipelinePaise: number;
/** Paise. Order value (falling back to quote) across every won lead. */
wonPaise: number;
}
/**
* The five figures. Exported for its own test — the arithmetic is the whole
* substance of this component and asserting it through the DOM would only
* prove that `abbreviatePaise` was called.
*/
export function deriveLeadsFigures(
data: KanbanData,
now: number = Date.now(),
): LeadsFigures {
const terminalStageIds = new Set(
data.stages.filter((s) => isTerminalStage(s.stageType)).map((s) => s.id),
);
const wonStageIds = new Set(
data.stages
.filter((s) => s.stageType === "system_terminal_won")
.map((s) => s.id),
);
const openLeads = data.leads.filter(
(lead) => !terminalStageIds.has(lead.currentStageId),
);
return {
open: openLeads.length,
overdue: overdueLeads(data.leads, now).length,
addedThisWeek: data.leads.filter((lead) => {
const created = new Date(lead.dateCreated).getTime();
return !Number.isNaN(created) && now - created <= WEEK_MS;
}).length,
pipelinePaise: openLeads.reduce(
(sum, lead) => sum + (lead.quoteValuePaise ?? 0),
0,
),
wonPaise: data.leads
.filter((lead) => wonStageIds.has(lead.currentStageId))
.reduce(
(sum, lead) =>
sum + (lead.orderValuePaise ?? lead.quoteValuePaise ?? 0),
0,
),
};
}
// ─── Component ───────────────────────────────────────────────────────────────
type LeadsStatStripProps = {
data: KanbanData;
/** One reading of the clock for the whole page. */
now: number;
};
export function LeadsStatStrip({ data, now }: LeadsStatStripProps) {
const figures = deriveLeadsFigures(data, now);
const stats: { label: string; value: string }[] = [
{ label: "Open", value: String(figures.open) },
{ label: "Overdue", value: String(figures.overdue) },
{ label: "Added this week", value: String(figures.addedThisWeek) },
{ label: "Pipeline", value: `₹ ${abbreviatePaise(figures.pipelinePaise)}` },
{ label: "Won", value: `₹ ${abbreviatePaise(figures.wonPaise)}` },
];
return (
<dl className="flex flex-wrap items-baseline gap-x-6 gap-y-1.5 border-y border-border-default py-2">
{stats.map((stat) => (
<div key={stat.label} className="inline-flex items-baseline gap-1.5">
<dt className="text-xs font-semibold uppercase tracking-wider text-foreground-muted">
{stat.label}
</dt>
<dd className="text-base font-semibold tabular-nums text-foreground-default">
{stat.value}
</dd>
</div>
))}
</dl>
);
}
|