/** The split — one amount spread across targets until the remainder is zero. */
import { useState } from "react";
import { Text } from "@lotics/ui/text";
import { colors } from "@lotics/ui/colors";
import { DetailRow, DetailTable, type DetailTableGaps } from "@lotics/ui/detail_row";
import { Status } from "@lotics/ui/status";
import { Box } from "@lotics/ui/box";
import { Button } from "@lotics/ui/button";
import { CardFooter } from "@lotics/ui/card";
import { Divider } from "@lotics/ui/divider";
import { NumberInput } from "@lotics/ui/number_input";
import { Progress } from "@lotics/ui/progress";
import { ScrollArea } from "@lotics/ui/scroll_area";
import { Stack } from "@lotics/ui/stack";
import { formatMoney } from "@lotics/ui/format_money";
// ─────────────────────────────────────────────────────────────────────────────
// Template, Allocation / split — apply ONE source across MANY targets until the
// remainder is zero. Here: cash application — a received payment spread across a
// customer's open invoices (the same shape serves stock-to-orders, landed-cost,
// budget distribution). The inverse of the batch builder: batch SUMS parts up to
// a total; allocation SPLITS a fixed total DOWN, with a remainder that must hit
// zero. Progress reading="remaining" is the spine — under (left on account),
// exact (apply), over (blocked). Oldest-first auto-allocates; each row can be
// filled or typed.
// ─────────────────────────────────────────────────────────────────────────────
interface Invoice {
id: string;
issued: string;
ageDays: number;
/** Outstanding amount — the cap this invoice can absorb. */
due: number;
}
const PAYMENT = { amount: 24_500_000, from: "ATLAS COMPONENTS", ref: "TT-88412", date: "11/06" };
const INVOICES: Invoice[] = [
{ id: "INV-2026-0301", issued: "24/05", ageDays: 18, due: 8_200_000 },
{ id: "INV-2026-0305", issued: "31/05", ageDays: 11, due: 6_400_000 },
{ id: "INV-2026-0312", issued: "07/06", ageDays: 4, due: 11_250_000 },
{ id: "INV-2026-0318", issued: "09/06", ageDays: 2, due: 3_150_000 },
];
/**
* ONE ALLOCATION TARGET, as a `DetailRow`: the identity is the label and its
* qualifier, and everything the reader ACTS on is the value — the cap it can
* absorb, the bounded input, and the fill/empty shortcut.
*
* Not a kit entry, because "allocation row" names a USE and the kit names what
* a thing IS. The two money columns are FIXED so a run of these shares one
* right edge: an allocation is read by scanning the caps against the amounts,
* and a column that resizes per row cannot be scanned. The age mark rides the
* VALUE rather than the label — a fact about the amount, beside the amount.
*
* The narrow answer costs nothing here: the `DetailTable` around the run
* measures its own container and stacks every row's identity above its money
* once the columns would crush the value.
*/
function AllocationLine(props: {
invoice: Invoice;
value: number;
onValueChange: (n: number) => void;
}) {
const { invoice, value, onValueChange } = props;
const full = value >= invoice.due;
return (
14 ? "red" : "zinc"} />
{`${formatMoney(invoice.due)} due`}
onValueChange(Math.max(0, Math.min(invoice.due, n ?? 0)))}
min={0}
max={invoice.due}
accessibilityLabel={`Allocate to ${invoice.id}`}
/>
);
}
const GAPS: DetailTableGaps = {
"--lotics-detail-table-gap": "0px",
"--lotics-detail-table-stacked-gap": "0px",
};
export function TplAllocate() {
const [alloc, setAlloc] = useState>({});
const allocated = INVOICES.reduce((s, inv) => s + (alloc[inv.id] ?? 0), 0);
const remainder = PAYMENT.amount - allocated;
const over = remainder < 0;
// Oldest-first — fill the oldest invoices in full until the payment runs out.
const autoOldest = () => {
let left = PAYMENT.amount;
const next: Record = {};
[...INVOICES].sort((a, b) => b.ageDays - a.ageDays).forEach((inv) => {
const take = Math.max(0, Math.min(inv.due, left));
next[inv.id] = take;
left -= take;
});
setAlloc(next);
};
const status = over
? `Reduce by ${formatMoney(-remainder)} — you can't apply more than was received`
: remainder > 0
? `${formatMoney(remainder)} will be left on the customer's account`
: "Payment fully applied across the selected invoices";
return (
{/* header */}
Apply paymentSplit one received payment across the customer's open invoices — oldest first, or line by line
{/* the source — the payment, and the remainder as you place it */}
Payment received{formatMoney(PAYMENT.amount)}{`${PAYMENT.from}, ref ${PAYMENT.ref}, ${PAYMENT.date}`}
{/* the targets — the open invoices, each absorbing part of the payment */}
{`Open invoices (${INVOICES.length})`}
{/* A register's rhythm, not a record page's: the rows are separated by
a rule, so the stack's own gutter drops to the tightest rung. */}
{INVOICES.map((inv, i) => (
{i > 0 ? : null}
setAlloc((prev) => ({ ...prev, [inv.id]: n }))}
/>
))}
{status}
);
}