Migrates Tailwind v3 -> v4 (CSS-first, theme tokens for brand, workflow states and charts) and adds 23 shadcn/ui components on Radix. - lib/workflow.ts is now the single source of truth for state name, colour, icon and available actions; "Financial Review" is the canonical served name, with "Finance Review" kept as an alias so those rows stop falling through to the unstyled fallback and dropping out of every count - record view: TanStack Table with server-side sort/paginate, company column with initials avatar, page totals, density and a URL-synced query state - dashboard: one cached analytics fetch shared by tiles and charts, donut + aging + vendor spend, click-to-filter - detail view: rebuilt against the live detailview payload — restores the PO number and payment terms that the old key heuristics silently dropped, parses match_summary into a 3-way-match checklist, and separates the department's and reviewer's own words from AI-generated content - form modal on Radix Dialog (focus trap, scroll lock, escape handling)
60 lines
2.3 KiB
TypeScript
60 lines
2.3 KiB
TypeScript
// Value formatting shared by the record view and the detail view. Previously
|
|
// each had its own near-identical fmtVal/renderCell pair, which drifted.
|
|
|
|
const MONEY_HINTS = ["amount", "tax", "price", "cost", "total", "payment", "value"];
|
|
|
|
export const isMoneyField = (key: string) => {
|
|
const k = key.toLowerCase();
|
|
return MONEY_HINTS.some((h) => k.includes(h));
|
|
};
|
|
|
|
export function formatCurrency(n: number, compact = false): string {
|
|
return n.toLocaleString("en-IN", {
|
|
style: "currency",
|
|
currency: "INR",
|
|
maximumFractionDigits: 0,
|
|
...(compact ? { notation: "compact", compactDisplay: "short" } : {}),
|
|
});
|
|
}
|
|
|
|
export function formatDate(value: string): string {
|
|
const d = new Date(value);
|
|
if (Number.isNaN(d.getTime())) return value;
|
|
return d.toLocaleDateString("en-IN", { day: "2-digit", month: "short", year: "numeric" });
|
|
}
|
|
|
|
/** Clock time, or null when the value carries no time component. */
|
|
export function formatTime(value: string): string | null {
|
|
const d = new Date(value);
|
|
if (Number.isNaN(d.getTime())) return null;
|
|
if (!/[T\s]\d{2}:\d{2}/.test(value)) return null;
|
|
return d.toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit" });
|
|
}
|
|
|
|
/** "3 days ago" / "in 2 days" — for aging and timeline captions. */
|
|
export function formatRelative(value: string): string | null {
|
|
const d = new Date(value);
|
|
if (Number.isNaN(d.getTime())) return null;
|
|
const diffDays = Math.round((d.getTime() - Date.now()) / 86_400_000);
|
|
if (diffDays === 0) return "today";
|
|
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
|
if (Math.abs(diffDays) < 31) return rtf.format(diffDays, "day");
|
|
return rtf.format(Math.round(diffDays / 30), "month");
|
|
}
|
|
|
|
export function formatLabel(label: string): string {
|
|
return label.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
}
|
|
|
|
/** Plain-text formatting of a view value, honouring its declared data_type. */
|
|
export function formatValue(value: unknown, dataType: string, fieldKey: string): string {
|
|
if (value == null || value === "") return "—";
|
|
if (dataType === "number") {
|
|
const n = Number(value);
|
|
if (!Number.isFinite(n)) return String(value);
|
|
return isMoneyField(fieldKey) ? formatCurrency(n) : n.toLocaleString("en-IN");
|
|
}
|
|
if (dataType === "date" || dataType === "datetime") return formatDate(String(value));
|
|
return String(value);
|
|
}
|