Add bills CRUD API and management UI
Full REST API for bill definitions with validation. Bills page supports add, edit, toggle active, and delete. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ const cors = require('cors');
|
||||
const path = require('path');
|
||||
const healthRouter = require('./routes/health');
|
||||
const configRouter = require('./routes/config');
|
||||
const billsRouter = require('./routes/bills');
|
||||
const db = require('./db');
|
||||
|
||||
const app = express();
|
||||
@@ -15,6 +16,7 @@ app.use(express.json());
|
||||
// API routes
|
||||
app.use('/api', healthRouter);
|
||||
app.use('/api', configRouter);
|
||||
app.use('/api', billsRouter);
|
||||
|
||||
// Serve static client files in production
|
||||
const clientDist = path.join(__dirname, '../../client/dist');
|
||||
|
||||
174
server/src/routes/bills.js
Normal file
174
server/src/routes/bills.js
Normal file
@@ -0,0 +1,174 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { pool } = require('../db');
|
||||
|
||||
function validateBillFields(body) {
|
||||
const { name, amount, due_day, assigned_paycheck } = body;
|
||||
if (!name || name.toString().trim() === '') {
|
||||
return 'name is required';
|
||||
}
|
||||
if (amount === undefined || amount === null || amount === '') {
|
||||
return 'amount is required';
|
||||
}
|
||||
if (isNaN(Number(amount))) {
|
||||
return 'amount must be a number';
|
||||
}
|
||||
if (due_day === undefined || due_day === null || due_day === '') {
|
||||
return 'due_day is required';
|
||||
}
|
||||
const dueDayInt = parseInt(due_day, 10);
|
||||
if (isNaN(dueDayInt) || dueDayInt < 1 || dueDayInt > 31) {
|
||||
return 'due_day must be an integer between 1 and 31';
|
||||
}
|
||||
if (assigned_paycheck === undefined || assigned_paycheck === null || assigned_paycheck === '') {
|
||||
return 'assigned_paycheck is required';
|
||||
}
|
||||
const pc = parseInt(assigned_paycheck, 10);
|
||||
if (pc !== 1 && pc !== 2) {
|
||||
return 'assigned_paycheck must be 1 or 2';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// GET /api/bills — list all bills
|
||||
router.get('/bills', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM bills ORDER BY assigned_paycheck, name'
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error('GET /api/bills error:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch bills' });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/bills — create a bill
|
||||
router.post('/bills', async (req, res) => {
|
||||
const validationError = validateBillFields(req.body);
|
||||
if (validationError) {
|
||||
return res.status(400).json({ error: validationError });
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
amount,
|
||||
due_day,
|
||||
assigned_paycheck,
|
||||
category = 'General',
|
||||
active = true,
|
||||
} = 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)
|
||||
RETURNING *`,
|
||||
[
|
||||
name.toString().trim(),
|
||||
Number(amount),
|
||||
parseInt(due_day, 10),
|
||||
parseInt(assigned_paycheck, 10),
|
||||
category || 'General',
|
||||
active !== undefined ? active : true,
|
||||
]
|
||||
);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('POST /api/bills error:', err);
|
||||
res.status(500).json({ error: 'Failed to create bill' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/bills/:id — get single bill
|
||||
router.get('/bills/:id', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT * FROM bills WHERE id = $1', [req.params.id]);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Bill not found' });
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('GET /api/bills/:id error:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch bill' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/bills/:id — update bill
|
||||
router.put('/bills/:id', async (req, res) => {
|
||||
const validationError = validateBillFields(req.body);
|
||||
if (validationError) {
|
||||
return res.status(400).json({ error: validationError });
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
amount,
|
||||
due_day,
|
||||
assigned_paycheck,
|
||||
category = 'General',
|
||||
active = true,
|
||||
} = 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
|
||||
RETURNING *`,
|
||||
[
|
||||
name.toString().trim(),
|
||||
Number(amount),
|
||||
parseInt(due_day, 10),
|
||||
parseInt(assigned_paycheck, 10),
|
||||
category || 'General',
|
||||
active !== undefined ? active : true,
|
||||
req.params.id,
|
||||
]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Bill not found' });
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('PUT /api/bills/:id error:', err);
|
||||
res.status(500).json({ error: 'Failed to update bill' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/bills/:id — hard delete
|
||||
router.delete('/bills/:id', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'DELETE FROM bills WHERE id = $1 RETURNING id',
|
||||
[req.params.id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Bill not found' });
|
||||
}
|
||||
res.json({ deleted: true, id: result.rows[0].id });
|
||||
} catch (err) {
|
||||
console.error('DELETE /api/bills/:id error:', err);
|
||||
res.status(500).json({ error: 'Failed to delete bill' });
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /api/bills/:id/toggle — toggle active field
|
||||
router.patch('/bills/:id/toggle', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'UPDATE bills SET active = NOT active WHERE id = $1 RETURNING *',
|
||||
[req.params.id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Bill not found' });
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('PATCH /api/bills/:id/toggle error:', err);
|
||||
res.status(500).json({ error: 'Failed to toggle bill' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user