feat: add expense date and CSV export to budget

- New expense_date column on budget items (DB migration #42)
- Date column in budget table with custom date picker
- CSV export button with BOM, semicolon separator, localized dates,
  currency in header, per-person/day calculations
- CustomDatePicker compact/borderless modes for inline table use
- i18n keys for all 12 languages
This commit is contained in:
Maurice
2026-04-01 12:16:11 +02:00
parent 60906cf1d1
commit 4ebf9c5f11
17 changed files with 117 additions and 28 deletions

View File

@@ -436,6 +436,9 @@ function runMigrations(db: Database.Database): void {
() => {
try { db.exec('ALTER TABLE trips ADD COLUMN reminder_days INTEGER DEFAULT 3'); } catch {}
},
() => {
try { db.exec('ALTER TABLE budget_items ADD COLUMN expense_date TEXT DEFAULT NULL'); } catch {}
},
];
if (currentVersion < migrations.length) {

View File

@@ -79,7 +79,7 @@ router.get('/summary/per-person', authenticate, (req: Request, res: Response) =>
router.post('/', authenticate, (req: Request, res: Response) => {
const authReq = req as AuthRequest;
const { tripId } = req.params;
const { category, name, total_price, persons, days, note } = req.body;
const { category, name, total_price, persons, days, note, expense_date } = req.body;
const trip = verifyTripOwnership(tripId, authReq.user.id);
if (!trip) return res.status(404).json({ error: 'Trip not found' });
@@ -93,7 +93,7 @@ router.post('/', authenticate, (req: Request, res: Response) => {
const sortOrder = (maxOrder.max !== null ? maxOrder.max : -1) + 1;
const result = db.prepare(
'INSERT INTO budget_items (trip_id, category, name, total_price, persons, days, note, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
'INSERT INTO budget_items (trip_id, category, name, total_price, persons, days, note, sort_order, expense_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
).run(
tripId,
category || 'Other',
@@ -102,7 +102,8 @@ router.post('/', authenticate, (req: Request, res: Response) => {
persons != null ? persons : null,
days !== undefined && days !== null ? days : null,
note || null,
sortOrder
sortOrder,
expense_date || null
);
const item = db.prepare('SELECT * FROM budget_items WHERE id = ?').get(result.lastInsertRowid) as BudgetItem & { members?: BudgetItemMember[] };
@@ -114,7 +115,7 @@ router.post('/', authenticate, (req: Request, res: Response) => {
router.put('/:id', authenticate, (req: Request, res: Response) => {
const authReq = req as AuthRequest;
const { tripId, id } = req.params;
const { category, name, total_price, persons, days, note, sort_order } = req.body;
const { category, name, total_price, persons, days, note, sort_order, expense_date } = req.body;
const trip = verifyTripOwnership(tripId, authReq.user.id);
if (!trip) return res.status(404).json({ error: 'Trip not found' });
@@ -133,7 +134,8 @@ router.put('/:id', authenticate, (req: Request, res: Response) => {
persons = CASE WHEN ? IS NOT NULL THEN ? ELSE persons END,
days = CASE WHEN ? THEN ? ELSE days END,
note = CASE WHEN ? THEN ? ELSE note END,
sort_order = CASE WHEN ? IS NOT NULL THEN ? ELSE sort_order END
sort_order = CASE WHEN ? IS NOT NULL THEN ? ELSE sort_order END,
expense_date = CASE WHEN ? THEN ? ELSE expense_date END
WHERE id = ?
`).run(
category || null,
@@ -143,6 +145,7 @@ router.put('/:id', authenticate, (req: Request, res: Response) => {
days !== undefined ? 1 : 0, days !== undefined ? days : null,
note !== undefined ? 1 : 0, note !== undefined ? note : null,
sort_order !== undefined ? 1 : null, sort_order !== undefined ? sort_order : 0,
expense_date !== undefined ? 1 : 0, expense_date !== undefined ? (expense_date || null) : null,
id
);