All files / src/components/crm OverdueFollowUps.tsx

100% Statements 7/7
100% Branches 6/6
100% Functions 3/3
100% Lines 6/6

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 122 123                                                                                      69x 69x   66x                                           75x                   75x                                                               4x                        
import { Link } from "@tanstack/react-router";
import type { KanbanLead } from "../../lib/api/pro/crm";
import { abbreviatePaise } from "../../lib/currency-utils";
import { cn } from "../../lib/utils";
import { Button } from "../ui/button";
import { StatusBadge } from "../ui/status-badge";
import { DENSE_BUTTON } from "./lead-controls";
import { formatDueLabel, overdueLeads } from "./lead-overdue";
import { LEAD_STATUS_OVERDUE } from "./lead-status";
import { scopeLabel } from "./lead-vocabulary";
 
/**
 * Overdue follow-ups, surfaced above the board.
 *
 * `/` (Today) reads exactly this data, so the two surfaces have to agree on
 * what "overdue" means — and they do, because both call `overdueLeads()` from
 * `lead-overdue.ts`. That module is the only definition; see its header.
 *
 * THIS IS THE PAGE'S ONE INDEX-RED MARK. The Index Red Rule gives a page a
 * single 3px inset bar for the thing it is currently about, and on the Leads
 * board that is the work waiting on the pro — not a stage, not a selection.
 * Every other band on the page (the five column heads, the table head) is
 * `.band-quiet` with a `--border-strong` marker for exactly that reason.
 *
 * Red here is the OVERDUE status code's ink (`--error`), which is a different
 * token from `--index` and a different job: the code says "past due", the bar
 * says "this is the section you are reading". They are allowed to share a page
 * because they never mean the same thing.
 */
 
type OverdueFollowUpsProps = {
	leads: readonly KanbanLead[];
	/** One reading of the clock for the whole page. */
	now: number;
	/** Opens the note dialog — logging the call is how a follow-up is cleared. */
	onLogContact: (leadId: number) => void;
};
 
export function OverdueFollowUps({
	leads,
	now,
	onLogContact,
}: OverdueFollowUpsProps) {
	const overdue = overdueLeads(leads, now);
	if (overdue.length === 0) return null;
 
	return (
		<section aria-labelledby="overdue-follow-ups">
			{/* The band, with the page's one index-red mark. */}
			<div className="flex items-center gap-2 border-b border-border-strong pb-1.5">
				<span
					aria-hidden="true"
					className="h-[1.05em] w-0.75 shrink-0 rounded-[1px] bg-index"
				/>
				<h2
					id="overdue-follow-ups"
					className="flex-1 text-xs font-semibold uppercase tracking-wider text-foreground-default"
				>
					Overdue follow-ups
				</h2>
				<span className="text-xs font-semibold tabular-nums text-foreground-muted">
					{overdue.length}
				</span>
			</div>
 
			<div className="mt-2.5 overflow-hidden rounded-lg bg-background-elevated shadow-card">
				<ul className="flex flex-col">
					{overdue.map((lead) => {
						const meta = [
							lead.location,
							scopeLabel(lead.projectType),
							lead.quoteValuePaise != null
								? `₹ ${abbreviatePaise(lead.quoteValuePaise)}`
								: null,
						]
							.filter(Boolean)
							.join(" · ");
 
						return (
							<li
								key={lead.id}
								className="flex items-start gap-3 border-t border-border-default px-3.5 py-2.5 transition-colors first:border-t-0 hover:bg-foreground-default/7"
							>
								<StatusBadge
									status={LEAD_STATUS_OVERDUE.code}
									variant={LEAD_STATUS_OVERDUE.tone}
									className="mt-0.5"
								/>
								<span className="min-w-0 flex-1">
									<Link
										to="/leads/$leadId"
										params={{ leadId: String(lead.id) }}
										className="block truncate font-medium text-foreground-default underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
									>
										Call {lead.customerName}
									</Link>
									{meta && (
										<span className="block truncate text-sm text-foreground-muted">
											{meta}
										</span>
									)}
								</span>
								<span className="shrink-0 whitespace-nowrap text-xs font-semibold uppercase tracking-wider tabular-nums text-foreground-muted">
									{/* Non-null: overdueLeads() filtered on it. */}
									{formatDueLabel(lead.nextReminderDueAt as string, now)}
								</span>
								<Button
									variant="outline"
									size="sm"
									className={cn("shrink-0", DENSE_BUTTON)}
									onClick={() => onLogContact(lead.id)}
								>
									Log a call
								</Button>
							</li>
						);
					})}
				</ul>
			</div>
		</section>
	);
}