feat: support unassigned purchases and allocation status
Continuous Integration / lint-and-test (push) Successful in 13s
Continuous Integration / docker-build-and-push (push) Successful in 35s

Allow month-only purchases to start finance plans. Keep unbudgeted posts distinct from explicit zero rules in overspend reporting.
This commit is contained in:
Lucas Winther
2026-09-30 13:10:10 +02:00
parent c1446db39f
commit b0e8145d2e
31 changed files with 469 additions and 80 deletions
+18 -2
View File
@@ -49,6 +49,7 @@ export function PurchaseDialog({ monthId, purchase, trigger }: Props) {
.sort((a, b) => a.order - b.order);
const [open, setOpen] = useState(false);
const [detachBill, setDetachBill] = useState(false);
const [draft, setDraft] = useState<PurchaseDraft>(() =>
purchase ? fromPurchase(purchase) : emptyDraft(monthId, activePosts[0]?.id ?? ""),
);
@@ -68,6 +69,7 @@ export function PurchaseDialog({ monthId, purchase, trigger }: Props) {
const errors = bulkMode ? validateBulk(bulk) : validatePurchase(draft);
function reset() {
setDetachBill(false);
setDraft(
purchase ? fromPurchase(purchase) : emptyDraft(monthId, activePosts[0]?.id ?? ""),
);
@@ -86,7 +88,10 @@ export function PurchaseDialog({ monthId, purchase, trigger }: Props) {
const created = bulkMode ? toPurchases(bulk) : null;
void mutate((data) => {
if (created) for (const p of created) addPurchase(data, p);
else if (purchase) updatePurchase(data, purchase.id, toPurchase(draft));
else if (purchase) updatePurchase(data, purchase.id, {
...toPurchase(draft),
...(detachBill ? { source: undefined } : {}),
});
else addPurchase(data, toPurchase(draft));
}).then((saved) => {
if (!saved) return;
@@ -166,7 +171,7 @@ export function PurchaseDialog({ monthId, purchase, trigger }: Props) {
setBulk((b) => ({ ...b, postId }));
}}
>
<option value="">Choose a post…</option>
<option value="">No post (unassigned)</option>
{activePosts.map((post) => (
<option key={post.id} value={post.id}>
{post.name}
@@ -301,6 +306,17 @@ export function PurchaseDialog({ monthId, purchase, trigger }: Props) {
<SplitEditor draft={draft} posts={activePosts} onChange={setDraft} />
{purchase?.source && (
<label className="flex items-start gap-2 text-sm">
<input
type="checkbox"
checked={detachBill}
onChange={(event) => setDetachBill(event.target.checked)}
/>
<span>Unlink from bill. The occurrence will show as expected again.</span>
</label>
)}
<PlanEditor draft={draft} onChange={setDraft} />
</>
)}