Vacay drag-to-paint, "Everyone" button, live exchange rates

- Vacay: click-and-drag to paint/erase vacation days across calendar
- Vacay: "Everyone" button sets days for all persons (2+ only)
- Budget: live currency conversion via frankfurter.app (cached 1h)
- Budget: conversion widget in total card with selectable target currency
- Day planner: remove transport mode buttons from day view
This commit is contained in:
Maurice
2026-03-23 21:11:20 +01:00
parent 88dca41ef7
commit faa8c84655
10 changed files with 178 additions and 25 deletions

View File

@@ -107,6 +107,23 @@ app.use('/api/weather', weatherRoutes);
app.use('/api/settings', settingsRoutes);
app.use('/api/backup', backupRoutes);
// Exchange rates (cached 1h, authenticated)
const { authenticate: rateAuth } = require('./middleware/auth');
let _rateCache = { data: null, ts: 0 };
app.get('/api/exchange-rates', rateAuth, async (req, res) => {
const now = Date.now();
if (_rateCache.data && now - _rateCache.ts < 3600000) return res.json(_rateCache.data);
try {
const r = await fetch('https://api.frankfurter.app/latest?from=EUR');
if (!r.ok) return res.status(502).json({ error: 'Failed to fetch rates' });
const data = await r.json();
_rateCache = { data, ts: now };
res.json(data);
} catch {
res.status(502).json({ error: 'Failed to fetch rates' });
}
});
// Serve static files in production
if (process.env.NODE_ENV === 'production') {
const publicPath = path.join(__dirname, '../public');

View File

@@ -453,10 +453,28 @@ router.post('/entries/toggle', (req, res) => {
const { date, target_user_id } = req.body;
if (!date) return res.status(400).json({ error: 'date required' });
const planId = getActivePlanId(req.user.id);
const planUsers = getPlanUsers(planId);
// Toggle for all users in plan
if (target_user_id === 'all') {
const actions = [];
for (const u of planUsers) {
const existing = db.prepare('SELECT id FROM vacay_entries WHERE user_id = ? AND date = ? AND plan_id = ?').get(u.id, date, planId);
if (existing) {
db.prepare('DELETE FROM vacay_entries WHERE id = ?').run(existing.id);
actions.push({ user_id: u.id, action: 'removed' });
} else {
db.prepare('INSERT INTO vacay_entries (plan_id, user_id, date, note) VALUES (?, ?, ?, ?)').run(planId, u.id, date, '');
actions.push({ user_id: u.id, action: 'added' });
}
}
notifyPlanUsers(planId, req.user.id);
return res.json({ action: 'toggled_all', actions });
}
// Allow toggling for another user if they are in the same plan
let userId = req.user.id;
if (target_user_id && parseInt(target_user_id) !== req.user.id) {
const planUsers = getPlanUsers(planId);
const tid = parseInt(target_user_id);
if (!planUsers.find(u => u.id === tid)) {
return res.status(403).json({ error: 'User not in plan' });