Merge pull request 'fix: On manager dashboard, overhaul approved spend card' (#51) from claude/issue-50 into master
All checks were successful
Refresh staging / refresh (push) Successful in 7s
All checks were successful
Refresh staging / refresh (push) Successful in 7s
Reviewed-on: #51
This commit is contained in:
commit
9d08ca1990
3 changed files with 77 additions and 4 deletions
|
|
@ -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() {
|
|||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<StatCard label="Awaiting Approval" value={awaitingCount} icon={Clock} color="orange" href="/approvals" />
|
||||
<StatCard label="Approved This Month" value={approvedThisMonth} icon={CheckCircle} color="green" href={`/history?approvedFrom=${startOfMonthParam}`} />
|
||||
<StatCard label="Total Approved Spend" value={formatCurrency(totalSpend)} icon={DollarSign} color="blue" />
|
||||
<StatCard label="Total Approved Spend" value={formatCompactINR(totalSpend)} icon={IndianRupee} color="blue" />
|
||||
</div>
|
||||
|
||||
{/* Recent approved POs */}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue