/** 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}`} />