Merge branch 'master' into docs/release-tag-warning
All checks were successful
PR checks / checks (pull_request) Successful in 33s
All checks were successful
PR checks / checks (pull_request) Successful in 33s
This commit is contained in:
commit
8ee077e548
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 { StatCard } from "@/components/dashboard/stat-card";
|
||||||
import { SpendCharts } from "@/components/dashboard/spend-charts";
|
import { SpendCharts } from "@/components/dashboard/spend-charts";
|
||||||
import { PoStatusBadge } from "@/components/po/po-status-badge";
|
import { PoStatusBadge } from "@/components/po/po-status-badge";
|
||||||
import { formatCurrency, formatDate, POST_APPROVAL_STATUSES } from "@/lib/utils";
|
import { formatCurrency, formatCompactINR, formatDate, POST_APPROVAL_STATUSES } from "@/lib/utils";
|
||||||
import { FileText, Clock, CheckCircle, DollarSign } from "lucide-react";
|
import { FileText, Clock, CheckCircle, DollarSign, IndianRupee } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
|
@ -182,7 +182,7 @@ async function ManagerDashboard() {
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
<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="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="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>
|
</div>
|
||||||
|
|
||||||
{/* Recent approved POs */}
|
{/* 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 {
|
export function formatDate(date: Date | string): string {
|
||||||
return new Intl.DateTimeFormat("en-US", {
|
return new Intl.DateTimeFormat("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import {
|
import {
|
||||||
formatCurrency, formatDate, formatDateTime,
|
formatCurrency, formatCompactINR, formatDate, formatDateTime,
|
||||||
generatePoNumber, PO_STATUS_LABELS, PO_STATUS_VARIANTS,
|
generatePoNumber, PO_STATUS_LABELS, PO_STATUS_VARIANTS,
|
||||||
} from "@/lib/utils";
|
} 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", () => {
|
describe("formatDate", () => {
|
||||||
it("returns a readable date string", () => {
|
it("returns a readable date string", () => {
|
||||||
const result = formatDate(new Date("2026-04-29"));
|
const result = formatDate(new Date("2026-04-29"));
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue