Add one-time expenses per paycheck
API for adding, removing, and marking one-time expenses paid. Paycheck view supports inline add form and paid/delete actions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@ const healthRouter = require('./routes/health');
|
||||
const configRouter = require('./routes/config');
|
||||
const billsRouter = require('./routes/bills');
|
||||
const paychecksRouter = require('./routes/paychecks');
|
||||
const actualsRouter = require('./routes/actuals');
|
||||
const oneTimeExpensesRouter = require('./routes/one-time-expenses');
|
||||
const db = require('./db');
|
||||
|
||||
const app = express();
|
||||
@@ -19,6 +21,8 @@ app.use('/api', healthRouter);
|
||||
app.use('/api', configRouter);
|
||||
app.use('/api', billsRouter);
|
||||
app.use('/api', paychecksRouter);
|
||||
app.use('/api', actualsRouter);
|
||||
app.use('/api', oneTimeExpensesRouter);
|
||||
|
||||
// Serve static client files in production
|
||||
const clientDist = path.join(__dirname, '../../client/dist');
|
||||
|
||||
88
server/src/routes/one-time-expenses.js
Normal file
88
server/src/routes/one-time-expenses.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { pool } = require('../db');
|
||||
|
||||
// POST /api/one-time-expenses — create a new one-time expense
|
||||
// Body: { paycheck_id, name, amount }
|
||||
router.post('/one-time-expenses', async (req, res) => {
|
||||
const { paycheck_id, name, amount } = req.body;
|
||||
|
||||
if (!paycheck_id) {
|
||||
return res.status(400).json({ message: 'paycheck_id is required' });
|
||||
}
|
||||
if (!name || typeof name !== 'string' || !name.trim()) {
|
||||
return res.status(400).json({ message: 'name is required' });
|
||||
}
|
||||
if (amount === undefined || amount === null || isNaN(parseFloat(amount))) {
|
||||
return res.status(400).json({ message: 'amount is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO one_time_expenses (paycheck_id, name, amount)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, paycheck_id, name, amount, paid, paid_at`,
|
||||
[paycheck_id, name.trim(), parseFloat(amount)]
|
||||
);
|
||||
res.status(201).json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('POST /api/one-time-expenses error:', err);
|
||||
res.status(500).json({ message: 'Failed to create one-time expense' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/one-time-expenses/:id — remove a one-time expense
|
||||
router.delete('/one-time-expenses/:id', async (req, res) => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({ message: 'Invalid id' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'DELETE FROM one_time_expenses WHERE id = $1 RETURNING id',
|
||||
[id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ message: 'One-time expense not found' });
|
||||
}
|
||||
res.json({ id: result.rows[0].id });
|
||||
} catch (err) {
|
||||
console.error('DELETE /api/one-time-expenses/:id error:', err);
|
||||
res.status(500).json({ message: 'Failed to delete one-time expense' });
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /api/one-time-expenses/:id/paid — toggle paid status
|
||||
// Body: { paid: true|false }
|
||||
router.patch('/one-time-expenses/:id/paid', async (req, res) => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({ message: 'Invalid id' });
|
||||
}
|
||||
|
||||
const { paid } = req.body;
|
||||
if (typeof paid !== 'boolean') {
|
||||
return res.status(400).json({ message: 'paid must be a boolean' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`UPDATE one_time_expenses
|
||||
SET paid = $1,
|
||||
paid_at = CASE WHEN $1 THEN NOW() ELSE NULL END
|
||||
WHERE id = $2
|
||||
RETURNING id, paycheck_id, name, amount, paid, paid_at`,
|
||||
[paid, id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ message: 'One-time expense not found' });
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error('PATCH /api/one-time-expenses/:id/paid error:', err);
|
||||
res.status(500).json({ message: 'Failed to update paid status' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user