import express from 'express'; import { pool, withTransaction } from '../db.js'; import { hasRole } from '../middleware/auth.js'; import { visibleUserIds } from '../lib/access.js'; import { getSettings } from '../lib/settings.js'; import { loadEntries } from '../lib/entries.js'; import { parseDate } from '../lib/dates.js'; import { BookingError, isBillable, loadProjects, resolvePot } from '../lib/projects.js'; const router = express.Router(); function publicEntry(entry, showAmounts) { return { id: entry.id, user_id: entry.user_id, user_name: entry.user_name, project_id: entry.project_id, project_name: entry.project_name, project_code: entry.project_code, project_display: entry.project_display, project_type: entry.project_type, customer_id: entry.customer_id, customer_name: entry.customer_name, customer_color: entry.customer_color, task_id: entry.task_id, task_name: entry.task_name, pot_id: entry.pot_id, pot_label: entry.pot_label, entry_date: entry.entry_date, minutes: entry.minutes, note: entry.note, billable: entry.billable, amount: showAmounts ? entry.amount : null, created_at: entry.created_at, updated_at: entry.updated_at, }; } router.get('/', async (req, res, next) => { try { const { from, to } = req.query; if (!parseDate(from) || !parseDate(to)) return res.status(400).json({ error: 'Zeitraum von/bis fehlt.' }); let userIds = [req.user.id]; if (req.query.user_id && Number(req.query.user_id) !== req.user.id) { const visible = await visibleUserIds(req.user); const wanted = Number(req.query.user_id); if (visible !== null && !visible.includes(wanted)) return res.status(403).json({ error: 'Keine Einsicht in diese Zeiten.' }); userIds = [wanted]; } const settings = await getSettings(); // Optionaler Kundenfilter, damit die Listenansicht im Report den dort // gesetzten Filter uebernehmen kann. const customerId = req.query.customer_id ? Number(req.query.customer_id) : undefined; const entries = await loadEntries(settings, { from, to, userIds, customerId }); res.json(entries.map((e) => publicEntry(e, hasRole(req.user, 'lead')))); } catch (err) { next(err); } }); async function validate(conn, user, body, existing) { const projectId = Number(body.project_id ?? existing?.project_id); const date = body.entry_date ?? existing?.entry_date; const minutes = Number(body.minutes ?? existing?.minutes); const taskId = body.task_id !== undefined ? (body.task_id ? Number(body.task_id) : null) : existing?.task_id ?? null; const note = body.note !== undefined ? String(body.note || '').trim().slice(0, 1000) || null : existing?.note ?? null; if (user.role === 'admin' && !existing) throw new BookingError('Administratoren buchen keine Zeiten.'); if (!parseDate(date)) throw new BookingError('Datum fehlt.'); if (!Number.isInteger(minutes) || minutes < 1) throw new BookingError('Bitte eine Zeit größer 0:00 eingeben.'); if (minutes > 24 * 60) throw new BookingError('Mehr als 24 Stunden an einem Tag gehen nicht.'); const [projects] = await conn.query('SELECT * FROM pulse_projects WHERE id = ? LIMIT 1', [projectId]); const project = projects[0]; if (!project) throw new BookingError('Projekt nicht gefunden.'); if (!project.is_active) throw new BookingError('Das Projekt ist archiviert und nicht mehr buchbar.'); // Gebucht wird nur auf zugewiesene Projekte - fuer jede Rolle. const [member] = await conn.query('SELECT 1 FROM pulse_project_members WHERE project_id = ? AND user_id = ?', [ project.id, existing?.user_id ?? user.id, ]); if (!member.length) throw new BookingError('Dieses Projekt ist dir nicht zugewiesen.'); let task = null; if (taskId) { const [tasks] = await conn.query('SELECT * FROM pulse_tasks WHERE id = ? AND project_id = ?', [taskId, project.id]); task = tasks[0]; if (!task) throw new BookingError('Der Task gehört nicht zu diesem Projekt.'); if (!task.is_active && task.id !== existing?.task_id) throw new BookingError('Der Task ist nicht mehr aktiv.'); } const [[day]] = await conn.query( 'SELECT COALESCE(SUM(minutes), 0) AS total FROM pulse_time_entries WHERE user_id = ? AND entry_date = ? AND id <> ?', [existing?.user_id ?? user.id, date, existing?.id || 0] ); if (Number(day.total) + minutes > 24 * 60) throw new BookingError('An diesem Tag wären es mehr als 24 Stunden.'); const billable = isBillable(project, task); const pot = await resolvePot(conn, project, date, minutes, existing?.id, billable); return { project, task, pot, date, minutes, note, billable: billable ? 1 : 0 }; } async function respond(req, res, id, status) { const settings = await getSettings(); const [[row]] = await pool.query('SELECT entry_date, user_id, project_id, pot_id FROM pulse_time_entries WHERE id = ?', [id]); const entries = await loadEntries(settings, { from: row.entry_date, to: row.entry_date, userIds: [row.user_id] }); const entry = entries.find((e) => e.id === id); // Hinweis zum Topf gleich mitliefern, damit der Dialog warnen kann. let warning = null; if (row.pot_id) { const [project] = await loadProjects(settings, { where: 'p.id = ?', params: [row.project_id] }); warning = project?.pots.find((p) => p.id === row.pot_id)?.warning ?? null; } res.status(status).json({ entry: publicEntry(entry, hasRole(req.user, 'lead')), warning }); } router.post('/', async (req, res, next) => { try { const id = await withTransaction(async (conn) => { const v = await validate(conn, req.user, req.body || {}, null); const [result] = await conn.query( `INSERT INTO pulse_time_entries (user_id, project_id, task_id, pot_id, entry_date, minutes, note, billable) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [req.user.id, v.project.id, v.task?.id ?? null, v.pot?.id ?? null, v.date, v.minutes, v.note, v.billable] ); return result.insertId; }); await respond(req, res, id, 201); } catch (err) { if (err instanceof BookingError) return res.status(422).json({ error: err.message }); next(err); } }); async function loadOwned(req) { const [rows] = await pool.query('SELECT * FROM pulse_time_entries WHERE id = ? LIMIT 1', [Number(req.params.id)]); const entry = rows[0]; if (!entry) return { error: [404, 'Buchung nicht gefunden.'] }; // Eigene Buchungen; Administratoren duerfen korrigieren. if (entry.user_id !== req.user.id && req.user.role !== 'admin') { return { error: [403, 'Nur eigene Buchungen können geändert werden.'] }; } return { entry }; } router.put('/:id', async (req, res, next) => { try { const { entry, error } = await loadOwned(req); if (error) return res.status(error[0]).json({ error: error[1] }); await withTransaction(async (conn) => { const v = await validate(conn, req.user, req.body || {}, entry); await conn.query( `UPDATE pulse_time_entries SET project_id = ?, task_id = ?, pot_id = ?, entry_date = ?, minutes = ?, note = ?, billable = ? WHERE id = ?`, [v.project.id, v.task?.id ?? null, v.pot?.id ?? null, v.date, v.minutes, v.note, v.billable, entry.id] ); }); await respond(req, res, entry.id, 200); } catch (err) { if (err instanceof BookingError) return res.status(422).json({ error: err.message }); next(err); } }); router.delete('/:id', async (req, res, next) => { try { const { entry, error } = await loadOwned(req); if (error) return res.status(error[0]).json({ error: error[1] }); await pool.query('DELETE FROM pulse_time_entries WHERE id = ?', [entry.id]); res.json({ ok: true }); } catch (err) { next(err); } }); export default router;