Add variable amount bills

- Migration 002: variable_amount boolean column on bills (default false)
- Bills form: 'Variable amount' checkbox; amount field becomes optional
  'Typical amount' when checked; table shows 'varies (~$X)' and a 〜 badge
- Paycheck view: variable bills show a pencil edit button to enter the
  month's actual amount, stored as amount_override on paycheck_bills
- New PATCH /api/paycheck-bills/:id/amount endpoint
- Lazy generation still works: setting an amount on a virtual paycheck
  generates it first then saves the override

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 20:14:08 -04:00
parent 3bac852a40
commit 45383b80cf
5 changed files with 164 additions and 18 deletions

View File

@@ -3,14 +3,15 @@ const router = express.Router();
const { pool } = require('../db');
function validateBillFields(body) {
const { name, amount, due_day, assigned_paycheck } = body;
const { name, amount, due_day, assigned_paycheck, variable_amount } = body;
if (!name || name.toString().trim() === '') {
return 'name is required';
}
if (amount === undefined || amount === null || amount === '') {
// Amount is optional for variable bills (defaults to 0)
if (!variable_amount && (amount === undefined || amount === null || amount === '')) {
return 'amount is required';
}
if (isNaN(Number(amount))) {
if (amount !== undefined && amount !== null && amount !== '' && isNaN(Number(amount))) {
return 'amount must be a number';
}
if (due_day === undefined || due_day === null || due_day === '') {
@@ -57,20 +58,22 @@ router.post('/bills', async (req, res) => {
assigned_paycheck,
category = 'General',
active = true,
variable_amount = false,
} = req.body;
try {
const result = await pool.query(
`INSERT INTO bills (name, amount, due_day, assigned_paycheck, category, active)
VALUES ($1, $2, $3, $4, $5, $6)
`INSERT INTO bills (name, amount, due_day, assigned_paycheck, category, active, variable_amount)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *`,
[
name.toString().trim(),
Number(amount),
Number(amount) || 0,
parseInt(due_day, 10),
parseInt(assigned_paycheck, 10),
category || 'General',
active !== undefined ? active : true,
Boolean(variable_amount),
]
);
res.status(201).json(result.rows[0]);
@@ -108,22 +111,24 @@ router.put('/bills/:id', async (req, res) => {
assigned_paycheck,
category = 'General',
active = true,
variable_amount = false,
} = req.body;
try {
const result = await pool.query(
`UPDATE bills
SET name = $1, amount = $2, due_day = $3, assigned_paycheck = $4,
category = $5, active = $6
WHERE id = $7
category = $5, active = $6, variable_amount = $7
WHERE id = $8
RETURNING *`,
[
name.toString().trim(),
Number(amount),
Number(amount) || 0,
parseInt(due_day, 10),
parseInt(assigned_paycheck, 10),
category || 'General',
active !== undefined ? active : true,
Boolean(variable_amount),
req.params.id,
]
);