Add payments with TDS, invoice detail view and filterable History (Phase E2)
- Migration M8 adds payments (cash, TDS, mode, reference). Payment status is derived, never stored: unpaid, partially paid, paid, or overdue. Overpayment is rejected, TDS counts toward settling, and only issued invoices accept payments. list_invoices and get_invoice carry the paid, TDS and balance summary. - New invoice detail view: archived-PDF preview, payments list, Record payment modal, Export, Duplicate (as a draft), Re-render and Cancel. An invoice with payments cannot be cancelled until they are deleted. - History gets search, status, financial-year, month and client filters, sortable columns, pagination, footer totals and an FY summary. Cancelled invoices are excluded from totals. The per-row archive-status calls are gone; archiving is on the detail page. - Rust tests for payments and the list summary; vitest for fiscal-year, money, filter and duplicate helpers. PDF output and fingerprint goldens are unchanged. The new UI has not been run in a webview yet.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import type { InvoiceStatus, PaymentStatus } from "./types";
|
||||
|
||||
export type PaymentMode = "bank_transfer" | "upi" | "cash" | "cheque" | "other";
|
||||
|
||||
export const PAYMENT_MODES: { id: PaymentMode; label: string }[] = [
|
||||
{ id: "bank_transfer", label: "Bank transfer" },
|
||||
{ id: "upi", label: "UPI" },
|
||||
{ id: "cash", label: "Cash" },
|
||||
{ id: "cheque", label: "Cheque" },
|
||||
{ id: "other", label: "Other" },
|
||||
];
|
||||
|
||||
export const paymentModeLabel = (mode: string): string => PAYMENT_MODES.find((m) => m.id === mode)?.label ?? mode;
|
||||
|
||||
/** A payment as stored: amounts are integer paise. `amountPaise` is cash; `tdsPaise` is tax the client deducted. */
|
||||
export interface Payment {
|
||||
id: number;
|
||||
invoiceId: number;
|
||||
paidOn: string;
|
||||
amountPaise: number;
|
||||
tdsPaise: number;
|
||||
mode: PaymentMode;
|
||||
reference: string;
|
||||
notes: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type PaymentInput = Omit<Payment, "id" | "createdAt">;
|
||||
|
||||
type TagType = "red" | "green" | "blue" | "gray" | "warm-gray" | "cool-gray" | "purple";
|
||||
|
||||
/** What to show in the status column and badges: the cancelled state wins over payment state. */
|
||||
export function statusBadge(status: InvoiceStatus, payment: PaymentStatus | undefined): { label: string; type: TagType } {
|
||||
if (status === "cancelled") return { label: "Cancelled", type: "gray" };
|
||||
if (status === "draft") return { label: "Draft", type: "warm-gray" };
|
||||
switch (payment) {
|
||||
case "paid":
|
||||
return { label: "Paid", type: "green" };
|
||||
case "partially_paid":
|
||||
return { label: "Partly paid", type: "blue" };
|
||||
case "overdue":
|
||||
return { label: "Overdue", type: "red" };
|
||||
default:
|
||||
return { label: "Unpaid", type: "cool-gray" };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user