Budget Workbook: editable unit count for per-unit assessment rate

The per-unit assessment (annual expenses / 12 / units) used the live association
unit count only. Add an override so the rate can use weighted/excluded units,
persisted on accounting.budget_workbooks.unit_override (null = live count). New
"# Units" control with a reset link; summary card and CSV use the effective count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 22:43:45 -04:00
parent d18296c3a6
commit 6fe1e3943c
2 changed files with 23 additions and 3 deletions
+18 -3
View File
@@ -54,6 +54,7 @@ export default function BudgetWorkbookPage() {
const [ytdOv, setYtdOv] = useState<Record<string, string>>({});
const [infl, setInfl] = useState<Record<string, string>>({});
const [projOv, setProjOv] = useState<Record<string, string>>({});
const [unitOv, setUnitOv] = useState(""); // "" = use the live unit count
const [saving, setSaving] = useState(false);
const [pushing, setPushing] = useState(false);
@@ -114,6 +115,7 @@ export default function BudgetWorkbookPage() {
}
setYtdOv(y); setInfl(i); setProjOv(p);
if ((workbook.head as any)?.through_month) setThrough((workbook.head as any).through_month);
setUnitOv((workbook.head as any)?.unit_override != null ? String((workbook.head as any).unit_override) : "");
}, [workbook]);
// Computed YTD actual per account from the GL
@@ -146,7 +148,10 @@ export default function BudgetWorkbookPage() {
const expenseAnnual = sum(sections.exp);
const incomeAnnual = sum(sections.inc);
const monthly = expenseAnnual / 12;
const perUnit = unitCount > 0 ? monthly / unitCount : 0;
// Per-unit assessment rate = annual expenses / 12 / units. Units default to the
// live association count but can be overridden (weighted/excluded units).
const effUnits = unitOv.trim() ? Number(unitOv) : unitCount;
const perUnit = effUnits > 0 ? monthly / effUnits : 0;
const resetRow = (id: string) => {
setYtdOv((m) => ({ ...m, [id]: "" }));
@@ -159,7 +164,7 @@ export default function BudgetWorkbookPage() {
setSaving(true);
try {
const { data: head, error: hErr } = await accounting.from("budget_workbooks")
.upsert({ company_id: cid, fiscal_year: fy, through_month: through, updated_at: new Date().toISOString() },
.upsert({ company_id: cid, fiscal_year: fy, through_month: through, unit_override: unitOv.trim() ? Math.round(Number(unitOv)) : null, updated_at: new Date().toISOString() },
{ onConflict: "company_id,fiscal_year" })
.select("id").single();
if (hErr || !head) throw new Error(hErr?.message || "save failed");
@@ -333,6 +338,16 @@ export default function BudgetWorkbookPage() {
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs"># Units (assessment)</Label>
<Input type="number" step="1" min="0" className="h-9 w-[120px]"
value={unitOv} placeholder={String(unitCount)}
onChange={(e) => setUnitOv(e.target.value)} />
{unitOv.trim() && Number(unitOv) !== unitCount && (
<button type="button" className="text-[11px] text-muted-foreground hover:underline mt-0.5"
onClick={() => setUnitOv("")}>Reset to {unitCount}</button>
)}
</div>
<div className="ml-auto flex gap-2">
<Button variant="outline" size="sm" onClick={() => qc.invalidateQueries({ queryKey: ["wb-gl", cid, from, to] })} disabled={glFetching}>
<RefreshCw className={`h-4 w-4 mr-1 ${glFetching ? "animate-spin" : ""}`} /> Refresh
@@ -348,7 +363,7 @@ export default function BudgetWorkbookPage() {
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<Card><CardContent className="p-4"><div className="text-xs text-muted-foreground uppercase tracking-wide">Annual Budget (Expenses)</div><div className="text-xl font-semibold mt-1 tabular-nums">{money(expenseAnnual)}</div></CardContent></Card>
<Card><CardContent className="p-4"><div className="text-xs text-muted-foreground uppercase tracking-wide">Monthly (÷12)</div><div className="text-xl font-semibold mt-1 tabular-nums">{money(monthly)}</div></CardContent></Card>
<Card><CardContent className="p-4"><div className="text-xs text-muted-foreground uppercase tracking-wide">Per Unit / Month ({unitCount} units)</div><div className="text-xl font-semibold mt-1 tabular-nums">{unitCount > 0 ? money(perUnit) : "—"}</div></CardContent></Card>
<Card><CardContent className="p-4"><div className="text-xs text-muted-foreground uppercase tracking-wide">Per Unit / Month ({effUnits} units{unitOv.trim() && Number(unitOv) !== unitCount ? " · override" : ""})</div><div className="text-xl font-semibold mt-1 tabular-nums">{effUnits > 0 ? money(perUnit) : "—"}</div></CardContent></Card>
<Card><CardContent className="p-4"><div className="text-xs text-muted-foreground uppercase tracking-wide">Projected Surplus / (Deficit)</div><div className={`text-xl font-semibold mt-1 tabular-nums ${incomeAnnual - expenseAnnual < 0 ? "text-destructive" : "text-emerald-700"}`}>{money(incomeAnnual - expenseAnnual)}</div></CardContent></Card>
</div>
@@ -0,0 +1,5 @@
-- Budget Workbook: let the per-unit assessment rate use an overridden unit count
-- (e.g. weighted units, excluding developer-owned). Null = use the live count of
-- units in the association.
alter table accounting.budget_workbooks
add column if not exists unit_override integer;