pelagia-portal/App/lib/utils.ts
Claude (auto-fix) fdc3ebdac9 fix(dashboard): count all POs approved this month, not just current MGR_APPROVED
The manager dashboard "Approved This Month" card only counted POs whose
current status is MGR_APPROVED, so approvals that had already moved on to
payment, delivery, or closure dropped out of the count. Managers could not
see what happened to the POs they approved this month.

- Count every PO whose `approvedAt` falls in the current month across all
  post-approval statuses (MGR_APPROVED → ... → CLOSED). `approvedAt` is set
  once at approval and persists, so it is the correct anchor.
- Introduce a shared `POST_APPROVAL_STATUSES` constant (includes the
  previously-omitted PARTIALLY_CLOSED). This also fixes Total Approved Spend
  and the vessel/monthly breakdowns, which were silently dropping
  partially-received POs.
- Make the card a link into /history with an approval-date filter applied
  (?approvedFrom=<startOfMonth>) so a click shows the full set with each PO's
  current status, as requested.
- Add `approvedFrom`/`approvedTo` filtering to the history page, its filter
  UI, and the reports export route so the deep-link and exports stay in sync.

Scope note: the count remains org-wide, consistent with every other card on
the manager dashboard.

Adds an integration test covering the moved-on case and the date window.

Fixes #32
2026-06-19 12:07:53 +05:30

89 lines
2.4 KiB
TypeScript

import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import type { POStatus } from "@prisma/client";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatCurrency(amount: number | string, currency = "INR"): string {
return new Intl.NumberFormat("en-IN", { style: "currency", currency }).format(
Number(amount)
);
}
export function formatDate(date: Date | string): string {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
}).format(new Date(date));
}
export function formatDateTime(date: Date | string): string {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(new Date(date));
}
export function generatePoNumber(): string {
const year = new Date().getFullYear();
const seq = Math.floor(Math.random() * 100000)
.toString()
.padStart(5, "0");
return `PO-${year}-${seq}`;
}
export const PO_STATUS_LABELS: Record<POStatus, string> = {
DRAFT: "Draft",
SUBMITTED: "Submitted",
MGR_REVIEW: "Under Review",
VENDOR_ID_PENDING: "Vendor ID Pending",
EDITS_REQUESTED: "Edits Requested",
REJECTED: "Rejected",
MGR_APPROVED: "Approved",
SENT_FOR_PAYMENT: "Sent for Payment",
PARTIALLY_PAID: "Partially Paid",
PAID_DELIVERED: "Paid",
PARTIALLY_CLOSED: "Partially Received",
CLOSED: "Closed",
};
// Statuses a PO can be in once it has received manager approval. A PO keeps its
// `approvedAt` timestamp as it moves through these states, so "approved this month"
// aggregations must match against all of them — not just MGR_APPROVED.
export const POST_APPROVAL_STATUSES = [
"MGR_APPROVED",
"SENT_FOR_PAYMENT",
"PARTIALLY_PAID",
"PAID_DELIVERED",
"PARTIALLY_CLOSED",
"CLOSED",
] as const satisfies readonly POStatus[];
export type BadgeVariant =
| "default"
| "secondary"
| "success"
| "warning"
| "danger"
| "outline";
export const PO_STATUS_VARIANTS: Record<POStatus, BadgeVariant> = {
DRAFT: "outline",
SUBMITTED: "secondary",
MGR_REVIEW: "secondary",
VENDOR_ID_PENDING: "warning",
EDITS_REQUESTED: "warning",
REJECTED: "danger",
MGR_APPROVED: "success",
SENT_FOR_PAYMENT: "default",
PARTIALLY_PAID: "warning",
PAID_DELIVERED: "success",
PARTIALLY_CLOSED: "warning",
CLOSED: "secondary",
};