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:
@@ -11,6 +11,7 @@ const EMPTY_FORM = {
|
|||||||
due_day: '',
|
due_day: '',
|
||||||
assigned_paycheck: '1',
|
assigned_paycheck: '1',
|
||||||
category: 'General',
|
category: 'General',
|
||||||
|
variable_amount: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatCurrency(value) {
|
function formatCurrency(value) {
|
||||||
@@ -68,6 +69,7 @@ function Bills() {
|
|||||||
due_day: bill.due_day,
|
due_day: bill.due_day,
|
||||||
assigned_paycheck: String(bill.assigned_paycheck),
|
assigned_paycheck: String(bill.assigned_paycheck),
|
||||||
category: bill.category || 'General',
|
category: bill.category || 'General',
|
||||||
|
variable_amount: !!bill.variable_amount,
|
||||||
});
|
});
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
@@ -81,8 +83,8 @@ function Bills() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleChange(e) {
|
function handleChange(e) {
|
||||||
const { name, value } = e.target;
|
const { name, value, type, checked } = e.target;
|
||||||
setForm(prev => ({ ...prev, [name]: value }));
|
setForm(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave(e) {
|
async function handleSave(e) {
|
||||||
@@ -92,10 +94,11 @@ function Bills() {
|
|||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: form.name,
|
name: form.name,
|
||||||
amount: form.amount,
|
amount: form.variable_amount ? (form.amount || 0) : form.amount,
|
||||||
due_day: form.due_day,
|
due_day: form.due_day,
|
||||||
assigned_paycheck: form.assigned_paycheck,
|
assigned_paycheck: form.assigned_paycheck,
|
||||||
category: form.category,
|
category: form.category,
|
||||||
|
variable_amount: form.variable_amount,
|
||||||
};
|
};
|
||||||
const url = editingId ? `/api/bills/${editingId}` : '/api/bills';
|
const url = editingId ? `/api/bills/${editingId}` : '/api/bills';
|
||||||
const method = editingId ? 'PUT' : 'POST';
|
const method = editingId ? 'PUT' : 'POST';
|
||||||
@@ -157,11 +160,21 @@ function Bills() {
|
|||||||
required placeholder="e.g. Rent" className="form-input" />
|
required placeholder="e.g. Rent" className="form-input" />
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label" htmlFor="amount">Amount ($)</label>
|
<label className="form-label" htmlFor="amount">
|
||||||
|
{form.variable_amount ? 'Typical amount (optional)' : 'Amount ($)'}
|
||||||
|
</label>
|
||||||
<input id="amount" name="amount" type="number" min="0" step="0.01"
|
<input id="amount" name="amount" type="number" min="0" step="0.01"
|
||||||
value={form.amount} onChange={handleChange} required placeholder="0.00" className="form-input" />
|
value={form.amount} onChange={handleChange}
|
||||||
|
required={!form.variable_amount} placeholder="0.00" className="form-input" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ marginBottom: '0.75rem' }}>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.875rem' }}>
|
||||||
|
<input type="checkbox" name="variable_amount" checked={form.variable_amount} onChange={handleChange}
|
||||||
|
style={{ accentColor: 'var(--accent)', width: '15px', height: '15px' }} />
|
||||||
|
<span>Variable amount <span className="text-muted">(amount changes each month, e.g. utility bills)</span></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 2fr', gap: '0.75rem', marginBottom: '1rem' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 2fr', gap: '0.75rem', marginBottom: '1rem' }}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="form-label" htmlFor="due_day">Due Day</label>
|
<label className="form-label" htmlFor="due_day">Due Day</label>
|
||||||
@@ -214,6 +227,7 @@ function Bills() {
|
|||||||
<th>Due Day</th>
|
<th>Due Day</th>
|
||||||
<th>Paycheck</th>
|
<th>Paycheck</th>
|
||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
|
<th>Variable</th>
|
||||||
<th>Active</th>
|
<th>Active</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -222,12 +236,17 @@ function Bills() {
|
|||||||
{bills.map((bill) => (
|
{bills.map((bill) => (
|
||||||
<tr key={bill.id} className={bill.active ? '' : 'row-muted'}>
|
<tr key={bill.id} className={bill.active ? '' : 'row-muted'}>
|
||||||
<td>{bill.name}</td>
|
<td>{bill.name}</td>
|
||||||
<td className="font-tabular">{formatCurrency(bill.amount)}</td>
|
<td className="font-tabular">
|
||||||
|
{bill.variable_amount
|
||||||
|
? <span className="text-muted" style={{ fontSize: '0.8rem' }}>varies{bill.amount > 0 ? ` (~${formatCurrency(bill.amount)})` : ''}</span>
|
||||||
|
: formatCurrency(bill.amount)}
|
||||||
|
</td>
|
||||||
<td>{ordinal(bill.due_day)}</td>
|
<td>{ordinal(bill.due_day)}</td>
|
||||||
<td>#{bill.assigned_paycheck}</td>
|
<td>#{bill.assigned_paycheck}</td>
|
||||||
<td>
|
<td>
|
||||||
{bill.category && <span className="badge badge-category">{bill.category}</span>}
|
{bill.category && <span className="badge badge-category">{bill.category}</span>}
|
||||||
</td>
|
</td>
|
||||||
|
<td>{bill.variable_amount ? '〜' : ''}</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<span
|
||||||
style={{ cursor: 'pointer', userSelect: 'none', fontSize: '1.1rem' }}
|
style={{ cursor: 'pointer', userSelect: 'none', fontSize: '1.1rem' }}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function todayISO() {
|
|||||||
|
|
||||||
// ─── PaycheckColumn ───────────────────────────────────────────────────────────
|
// ─── PaycheckColumn ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggle, onOteDelete, onOteAdd, onGenerate, onAmountSave }) {
|
function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggle, onOteDelete, onOteAdd, onGenerate, onAmountSave, onBillAmountSave }) {
|
||||||
const [newOteName, setNewOteName] = useState('');
|
const [newOteName, setNewOteName] = useState('');
|
||||||
const [newOteAmount, setNewOteAmount] = useState('');
|
const [newOteAmount, setNewOteAmount] = useState('');
|
||||||
const [actuals, setActuals] = useState([]);
|
const [actuals, setActuals] = useState([]);
|
||||||
@@ -41,6 +41,23 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
|||||||
const [formSubmitting, setFormSubmitting] = useState(false);
|
const [formSubmitting, setFormSubmitting] = useState(false);
|
||||||
const [formError, setFormError] = useState(null);
|
const [formError, setFormError] = useState(null);
|
||||||
|
|
||||||
|
// Inline bill amount editing (for variable bills)
|
||||||
|
const [editingBillAmount, setEditingBillAmount] = useState(null); // bill_id
|
||||||
|
const [billAmountDraft, setBillAmountDraft] = useState('');
|
||||||
|
const [billAmountSaving, setBillAmountSaving] = useState(false);
|
||||||
|
|
||||||
|
async function saveBillAmount(bill) {
|
||||||
|
setBillAmountSaving(true);
|
||||||
|
try {
|
||||||
|
await onBillAmountSave(bill.paycheck_bill_id, bill.bill_id, paycheck.paycheck_number, parseFloat(billAmountDraft) || 0);
|
||||||
|
setEditingBillAmount(null);
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Failed to save amount: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setBillAmountSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Inline gross/net editing
|
// Inline gross/net editing
|
||||||
const [editingAmounts, setEditingAmounts] = useState(false);
|
const [editingAmounts, setEditingAmounts] = useState(false);
|
||||||
const [editGross, setEditGross] = useState('');
|
const [editGross, setEditGross] = useState('');
|
||||||
@@ -242,10 +259,47 @@ function PaycheckColumn({ paycheck, onBillPaidToggle, categories, onOtePaidToggl
|
|||||||
<div className="bill-row__info">
|
<div className="bill-row__info">
|
||||||
<div className={`bill-row__name${bill.paid ? ' paid' : ''}`}>
|
<div className={`bill-row__name${bill.paid ? ' paid' : ''}`}>
|
||||||
<span>{bill.name}</span>
|
<span>{bill.name}</span>
|
||||||
<span className="bill-row__amount">{formatCurrency(bill.effective_amount)}</span>
|
{bill.variable_amount && editingBillAmount === bill.bill_id ? (
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
value={billAmountDraft}
|
||||||
|
onChange={e => setBillAmountDraft(e.target.value)}
|
||||||
|
className="form-input"
|
||||||
|
style={{ width: '90px', padding: '0.2rem 0.35rem', fontSize: '0.8rem' }}
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter') saveBillAmount(bill);
|
||||||
|
if (e.key === 'Escape') setEditingBillAmount(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-sm btn-primary" disabled={billAmountSaving}
|
||||||
|
onClick={() => saveBillAmount(bill)} style={{ padding: '0.2rem 0.4rem', fontSize: '0.75rem' }}>
|
||||||
|
✓
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-sm" onClick={() => setEditingBillAmount(null)}
|
||||||
|
style={{ padding: '0.2rem 0.4rem', fontSize: '0.75rem' }}>✕</button>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
||||||
|
<span className="bill-row__amount">
|
||||||
|
{bill.variable_amount && !bill.amount_override
|
||||||
|
? <span className="text-faint" style={{ fontSize: '0.8rem' }}>enter amount</span>
|
||||||
|
: formatCurrency(bill.effective_amount)}
|
||||||
|
</span>
|
||||||
|
{bill.variable_amount && !bill.paid && (
|
||||||
|
<button className="btn-icon" style={{ fontSize: '0.8rem', color: 'var(--text-faint)' }}
|
||||||
|
onClick={() => { setBillAmountDraft(bill.amount_override ?? bill.amount ?? ''); setEditingBillAmount(bill.bill_id); }}
|
||||||
|
title="Set this month's amount">✎</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="bill-row__meta">
|
<div className="bill-row__meta">
|
||||||
<span>due {ordinal(bill.due_day)}</span>
|
<span>due {ordinal(bill.due_day)}</span>
|
||||||
|
{bill.variable_amount && <span className="badge badge-category">variable</span>}
|
||||||
{bill.category && <span className="badge badge-category">{bill.category}</span>}
|
{bill.category && <span className="badge badge-category">{bill.category}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -445,6 +499,35 @@ function PaycheckView() {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleBillAmountSave(paycheckBillId, billId, paycheckNumber, amount) {
|
||||||
|
let realPaycheckBillId = paycheckBillId;
|
||||||
|
|
||||||
|
if (!realPaycheckBillId) {
|
||||||
|
const generated = await generateMonth();
|
||||||
|
const pc = generated.find(p => p.paycheck_number === paycheckNumber);
|
||||||
|
realPaycheckBillId = pc.bills.find(b => b.bill_id === billId).paycheck_bill_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(`/api/paycheck-bills/${realPaycheckBillId}/amount`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ amount }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json();
|
||||||
|
throw new Error(body.error || `Server error: ${res.status}`);
|
||||||
|
}
|
||||||
|
// Reflect the new override in local state
|
||||||
|
setPaychecks(prev => prev.map(pc => ({
|
||||||
|
...pc,
|
||||||
|
bills: pc.bills.map(b =>
|
||||||
|
b.paycheck_bill_id === realPaycheckBillId
|
||||||
|
? { ...b, amount_override: amount, effective_amount: amount }
|
||||||
|
: b
|
||||||
|
),
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
async function handleBillPaidToggle(paycheckBillId, paid, billId, paycheckNumber) {
|
async function handleBillPaidToggle(paycheckBillId, paid, billId, paycheckNumber) {
|
||||||
let realPaycheckBillId = paycheckBillId;
|
let realPaycheckBillId = paycheckBillId;
|
||||||
|
|
||||||
@@ -589,6 +672,7 @@ function PaycheckView() {
|
|||||||
categories={categories}
|
categories={categories}
|
||||||
onGenerate={generateMonth}
|
onGenerate={generateMonth}
|
||||||
onAmountSave={handleAmountSave}
|
onAmountSave={handleAmountSave}
|
||||||
|
onBillAmountSave={handleBillAmountSave}
|
||||||
/>
|
/>
|
||||||
<PaycheckColumn
|
<PaycheckColumn
|
||||||
paycheck={pc2}
|
paycheck={pc2}
|
||||||
@@ -599,6 +683,7 @@ function PaycheckView() {
|
|||||||
categories={categories}
|
categories={categories}
|
||||||
onGenerate={generateMonth}
|
onGenerate={generateMonth}
|
||||||
onAmountSave={handleAmountSave}
|
onAmountSave={handleAmountSave}
|
||||||
|
onBillAmountSave={handleBillAmountSave}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
4
db/migrations/002_variable_amount_bills.sql
Normal file
4
db/migrations/002_variable_amount_bills.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- Add variable_amount flag to bills.
|
||||||
|
-- When true, the amount is expected to change each month and must be
|
||||||
|
-- entered per-paycheck via amount_override on paycheck_bills.
|
||||||
|
ALTER TABLE bills ADD COLUMN IF NOT EXISTS variable_amount BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
@@ -3,14 +3,15 @@ const router = express.Router();
|
|||||||
const { pool } = require('../db');
|
const { pool } = require('../db');
|
||||||
|
|
||||||
function validateBillFields(body) {
|
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() === '') {
|
if (!name || name.toString().trim() === '') {
|
||||||
return 'name is required';
|
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';
|
return 'amount is required';
|
||||||
}
|
}
|
||||||
if (isNaN(Number(amount))) {
|
if (amount !== undefined && amount !== null && amount !== '' && isNaN(Number(amount))) {
|
||||||
return 'amount must be a number';
|
return 'amount must be a number';
|
||||||
}
|
}
|
||||||
if (due_day === undefined || due_day === null || due_day === '') {
|
if (due_day === undefined || due_day === null || due_day === '') {
|
||||||
@@ -57,20 +58,22 @@ router.post('/bills', async (req, res) => {
|
|||||||
assigned_paycheck,
|
assigned_paycheck,
|
||||||
category = 'General',
|
category = 'General',
|
||||||
active = true,
|
active = true,
|
||||||
|
variable_amount = false,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`INSERT INTO bills (name, amount, due_day, assigned_paycheck, category, active)
|
`INSERT INTO bills (name, amount, due_day, assigned_paycheck, category, active, variable_amount)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
name.toString().trim(),
|
name.toString().trim(),
|
||||||
Number(amount),
|
Number(amount) || 0,
|
||||||
parseInt(due_day, 10),
|
parseInt(due_day, 10),
|
||||||
parseInt(assigned_paycheck, 10),
|
parseInt(assigned_paycheck, 10),
|
||||||
category || 'General',
|
category || 'General',
|
||||||
active !== undefined ? active : true,
|
active !== undefined ? active : true,
|
||||||
|
Boolean(variable_amount),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
res.status(201).json(result.rows[0]);
|
res.status(201).json(result.rows[0]);
|
||||||
@@ -108,22 +111,24 @@ router.put('/bills/:id', async (req, res) => {
|
|||||||
assigned_paycheck,
|
assigned_paycheck,
|
||||||
category = 'General',
|
category = 'General',
|
||||||
active = true,
|
active = true,
|
||||||
|
variable_amount = false,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`UPDATE bills
|
`UPDATE bills
|
||||||
SET name = $1, amount = $2, due_day = $3, assigned_paycheck = $4,
|
SET name = $1, amount = $2, due_day = $3, assigned_paycheck = $4,
|
||||||
category = $5, active = $6
|
category = $5, active = $6, variable_amount = $7
|
||||||
WHERE id = $7
|
WHERE id = $8
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
name.toString().trim(),
|
name.toString().trim(),
|
||||||
Number(amount),
|
Number(amount) || 0,
|
||||||
parseInt(due_day, 10),
|
parseInt(due_day, 10),
|
||||||
parseInt(assigned_paycheck, 10),
|
parseInt(assigned_paycheck, 10),
|
||||||
category || 'General',
|
category || 'General',
|
||||||
active !== undefined ? active : true,
|
active !== undefined ? active : true,
|
||||||
|
Boolean(variable_amount),
|
||||||
req.params.id,
|
req.params.id,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ async function buildVirtualPaychecks(year, month) {
|
|||||||
const payDate = `${year}-${pad2(month)}-${pad2(day ?? 1)}`;
|
const payDate = `${year}-${pad2(month)}-${pad2(day ?? 1)}`;
|
||||||
|
|
||||||
const billsResult = await pool.query(
|
const billsResult = await pool.query(
|
||||||
`SELECT id, name, amount, due_day, category
|
`SELECT id, name, amount, due_day, category, variable_amount
|
||||||
FROM bills WHERE active = TRUE AND assigned_paycheck = $1
|
FROM bills WHERE active = TRUE AND assigned_paycheck = $1
|
||||||
ORDER BY due_day, name`,
|
ORDER BY due_day, name`,
|
||||||
[num]
|
[num]
|
||||||
@@ -79,6 +79,7 @@ async function buildVirtualPaychecks(year, month) {
|
|||||||
effective_amount: b.amount,
|
effective_amount: b.amount,
|
||||||
due_day: b.due_day,
|
due_day: b.due_day,
|
||||||
category: b.category,
|
category: b.category,
|
||||||
|
variable_amount: b.variable_amount,
|
||||||
paid: false,
|
paid: false,
|
||||||
paid_at: null,
|
paid_at: null,
|
||||||
})),
|
})),
|
||||||
@@ -162,6 +163,7 @@ async function fetchPaychecksForMonth(year, month) {
|
|||||||
pb.bill_id,
|
pb.bill_id,
|
||||||
b.name,
|
b.name,
|
||||||
b.amount,
|
b.amount,
|
||||||
|
b.variable_amount,
|
||||||
pb.amount_override,
|
pb.amount_override,
|
||||||
CASE WHEN pb.amount_override IS NOT NULL THEN pb.amount_override ELSE b.amount END AS effective_amount,
|
CASE WHEN pb.amount_override IS NOT NULL THEN pb.amount_override ELSE b.amount END AS effective_amount,
|
||||||
b.due_day,
|
b.due_day,
|
||||||
@@ -196,6 +198,7 @@ async function fetchPaychecksForMonth(year, month) {
|
|||||||
bill_id: b.bill_id,
|
bill_id: b.bill_id,
|
||||||
name: b.name,
|
name: b.name,
|
||||||
amount: b.amount,
|
amount: b.amount,
|
||||||
|
variable_amount: b.variable_amount,
|
||||||
amount_override: b.amount_override,
|
amount_override: b.amount_override,
|
||||||
effective_amount: b.effective_amount,
|
effective_amount: b.effective_amount,
|
||||||
due_day: b.due_day,
|
due_day: b.due_day,
|
||||||
@@ -303,6 +306,36 @@ router.patch('/paychecks/:id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// PATCH /api/paycheck-bills/:id/amount — set amount_override for a variable bill
|
||||||
|
router.patch('/paycheck-bills/:id/amount', async (req, res) => {
|
||||||
|
const id = parseInt(req.params.id, 10);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid id' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { amount } = req.body;
|
||||||
|
if (amount == null || isNaN(parseFloat(amount))) {
|
||||||
|
return res.status(400).json({ error: 'amount is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
`UPDATE paycheck_bills SET amount_override = $1 WHERE id = $2
|
||||||
|
RETURNING id, amount_override`,
|
||||||
|
[parseFloat(amount), id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'paycheck_bill not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('PATCH /api/paycheck-bills/:id/amount error:', err);
|
||||||
|
res.status(500).json({ error: 'Failed to update amount' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// PATCH /api/paycheck-bills/:id/paid
|
// PATCH /api/paycheck-bills/:id/paid
|
||||||
router.patch('/paycheck-bills/:id/paid', async (req, res) => {
|
router.patch('/paycheck-bills/:id/paid', async (req, res) => {
|
||||||
const id = parseInt(req.params.id, 10);
|
const id = parseInt(req.params.id, 10);
|
||||||
|
|||||||
Reference in New Issue
Block a user