From defd6e7a189bfd7129b643550bd7cef8f4310272 Mon Sep 17 00:00:00 2001 From: "Claude (auto-fix)" Date: Sun, 21 Jun 2026 02:02:41 +0530 Subject: [PATCH] feat(dashboard): compact INR formatting for Total Approved Spend card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overhaul the manager dashboard "Total Approved Spend" stat card per the reporter's request: - Swap the DollarSign lucide icon for IndianRupee (rupee symbol). - Render the amount in the Indian short scale (lakh/crore) via a new `formatCompactINR` helper, e.g. ₹2 Cr, ₹49 L, ₹75 K, instead of the full ₹49,00,000.00. `formatCompactINR` rounds to at most 2 decimals, trims trailing zeros, keeps the ₹ prefix and sign. The DollarSign icon is retained for the Accounts "Payment Queue Value" card; the precise `formatCurrency` is kept for tables. Adds unit tests covering crore/lakh/thousand/sub-thousand, boundaries, zero, string input, negatives and non-finite input. Fixes #50 Co-Authored-By: Claude Opus 4.8 (1M context) --- App/app/(portal)/dashboard/page.tsx | 6 ++-- App/lib/utils.ts | 24 ++++++++++++++ App/tests/unit/utils.test.ts | 51 ++++++++++++++++++++++++++++- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/App/app/(portal)/dashboard/page.tsx b/App/app/(portal)/dashboard/page.tsx index b4ca973..7ae3e26 100644 --- a/App/app/(portal)/dashboard/page.tsx +++ b/App/app/(portal)/dashboard/page.tsx @@ -3,8 +3,8 @@ import { db } from "@/lib/db"; import { StatCard } from "@/components/dashboard/stat-card"; import { SpendCharts } from "@/components/dashboard/spend-charts"; import { PoStatusBadge } from "@/components/po/po-status-badge"; -import { formatCurrency, formatDate, POST_APPROVAL_STATUSES } from "@/lib/utils"; -import { FileText, Clock, CheckCircle, DollarSign } from "lucide-react"; +import { formatCurrency, formatCompactINR, formatDate, POST_APPROVAL_STATUSES } from "@/lib/utils"; +import { FileText, Clock, CheckCircle, DollarSign, IndianRupee } from "lucide-react"; import Link from "next/link"; import type { Metadata } from "next"; @@ -182,7 +182,7 @@ async function ManagerDashboard() {
- +
{/* Recent approved POs */} diff --git a/App/lib/utils.ts b/App/lib/utils.ts index e4d767d..2158edb 100644 --- a/App/lib/utils.ts +++ b/App/lib/utils.ts @@ -12,6 +12,30 @@ export function formatCurrency(amount: number | string, currency = "INR"): strin ); } +// Compact INR formatter using the Indian short scale (lakh = 1e5, crore = 1e7). +// Produces readable abbreviations for dashboard stat cards, e.g. ₹2 Cr, ₹49 L, +// ₹75 K, ₹500. Values are rounded to at most 2 decimals with trailing zeros +// trimmed (₹2.5 Cr, not ₹2.50 Cr). Negative amounts keep their sign. +export function formatCompactINR(amount: number | string): string { + const n = Number(amount); + if (!Number.isFinite(n)) return "₹0"; + + const sign = n < 0 ? "-" : ""; + const abs = Math.abs(n); + + const format = (value: number, suffix: string) => { + const rounded = Math.round(value * 100) / 100; + // Trim trailing zeros: 2 -> "2", 2.5 -> "2.5", 2.05 -> "2.05". + const text = rounded.toFixed(2).replace(/\.?0+$/, ""); + return `${sign}₹${text}${suffix}`; + }; + + if (abs >= 1e7) return format(abs / 1e7, " Cr"); + if (abs >= 1e5) return format(abs / 1e5, " L"); + if (abs >= 1e3) return format(abs / 1e3, " K"); + return format(abs, ""); +} + export function formatDate(date: Date | string): string { return new Intl.DateTimeFormat("en-US", { year: "numeric", diff --git a/App/tests/unit/utils.test.ts b/App/tests/unit/utils.test.ts index 5ba3680..13dc12e 100644 --- a/App/tests/unit/utils.test.ts +++ b/App/tests/unit/utils.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import { - formatCurrency, formatDate, formatDateTime, + formatCurrency, formatCompactINR, formatDate, formatDateTime, generatePoNumber, PO_STATUS_LABELS, PO_STATUS_VARIANTS, } from "@/lib/utils"; @@ -32,6 +32,55 @@ describe("formatCurrency", () => { }); }); +describe("formatCompactINR", () => { + it("abbreviates crore amounts with Cr", () => { + expect(formatCompactINR(20000000)).toBe("₹2 Cr"); + }); + + it("abbreviates lakh amounts with L", () => { + expect(formatCompactINR(4900000)).toBe("₹49 L"); + }); + + it("abbreviates thousand amounts with K", () => { + expect(formatCompactINR(75000)).toBe("₹75 K"); + }); + + it("renders sub-thousand amounts without a suffix", () => { + expect(formatCompactINR(500)).toBe("₹500"); + }); + + it("formats zero as ₹0", () => { + expect(formatCompactINR(0)).toBe("₹0"); + }); + + it("trims trailing zeros but keeps significant decimals", () => { + expect(formatCompactINR(25000000)).toBe("₹2.5 Cr"); + expect(formatCompactINR(4950000)).toBe("₹49.5 L"); + }); + + it("rounds to at most two decimals", () => { + expect(formatCompactINR(12345678)).toBe("₹1.23 Cr"); + }); + + it("uses the right unit at boundaries", () => { + expect(formatCompactINR(100000)).toBe("₹1 L"); + expect(formatCompactINR(10000000)).toBe("₹1 Cr"); + expect(formatCompactINR(1000)).toBe("₹1 K"); + }); + + it("accepts string input", () => { + expect(formatCompactINR("4900000")).toBe("₹49 L"); + }); + + it("preserves the sign for negative amounts", () => { + expect(formatCompactINR(-4900000)).toBe("-₹49 L"); + }); + + it("handles non-finite input gracefully", () => { + expect(formatCompactINR(NaN)).toBe("₹0"); + }); +}); + describe("formatDate", () => { it("returns a readable date string", () => { const result = formatDate(new Date("2026-04-29"));