- Cash Floats: ledger card (opening balance, top-ups, slips), manual entries, slip photos - Office access level; clock app Slips tab (photograph a slip, popup for supplier/cost code/comments/amount); only Office sees it; per-role route table in lib/clock-access - Job cards: Machine Service PDF (with Oil block), JC numbers, manual Close job, Delete - Hide Expenses > Vehicle Costs and Finance (Bastion) from the menu Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
26 lines
1.2 KiB
TypeScript
26 lines
1.2 KiB
TypeScript
/** The cash-float card is a ledger: balance = opening + top-ups − slips. */
|
||
// Loose on purpose: PocketBase rows arrive as RecordModel, so only `kind` and `amount` are read.
|
||
export type CashEntry = { [k: string]: any };
|
||
|
||
export const sign = (kind: unknown) => (kind === "expense" ? -1 : 1);
|
||
|
||
export function cashBalance(entries: CashEntry[]): number {
|
||
const cents = entries.reduce((s, e) => s + sign(e.kind) * Math.round(Number(e.amount || 0) * 100), 0);
|
||
return cents / 100;
|
||
}
|
||
|
||
export const hasOpening = (entries: CashEntry[]) => entries.some((e) => e.kind === "opening");
|
||
|
||
export const MAX_AMOUNT = 1_000_000;
|
||
|
||
/** Parse a rand amount typed by a person ("1 250,50" / "1250.5"). null if it is not a positive number. */
|
||
export function parseAmount(raw: unknown, allowZero = false): number | null {
|
||
const text = String(raw ?? "").replace(/\s/g, "").replace(",", ".");
|
||
if (text === "") return null;
|
||
const n = Number(text);
|
||
if (!Number.isFinite(n) || n < 0 || (n === 0 && !allowZero) || n > MAX_AMOUNT) return null;
|
||
return Math.round(n * 100) / 100;
|
||
}
|
||
|
||
export const rand = (n: number) =>
|
||
`${n < 0 ? "-" : ""}R ${Math.abs(n).toLocaleString("en-ZA", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|