const express = require('express'); const cors = require('cors'); const db = require('./db'); const app = express(); app.use(cors()); app.use(express.json()); const PORT = process.env.PORT || 8080; // ---------- helpers ---------- function isoWeekKey(date) { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = (d.getUTCDay() + 6) % 7; // Monday = 0 d.setUTCDate(d.getUTCDate() - dayNum + 3); const firstThursday = new Date(Date.UTC(d.getUTCFullYear(), 0, 4)); const week = 1 + Math.round(((d - firstThursday) / 86400000 - 3 + ((firstThursday.getUTCDay() + 6) % 7)) / 7); return `${d.getUTCFullYear()}-W${String(week).padStart(2, '0')}`; } function mondayOf(date) { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = (d.getUTCDay() + 6) % 7; d.setUTCDate(d.getUTCDate() - dayNum); return d; } function addDays(date, days) { const d = new Date(date); d.setUTCDate(d.getUTCDate() + days); return d; } function parseDate(s) { const [y, m, d] = s.split('-').map(Number); return new Date(Date.UTC(y, m - 1, d)); } // ---------- employees ---------- app.get('/api/employees', (req, res) => { res.json(db.prepare('SELECT * FROM employees ORDER BY name').all()); }); app.post('/api/employees', (req, res) => { const { name, email, role, weekly_capacity_hours } = req.body; if (!name) return res.status(400).json({ error: 'name ist erforderlich' }); const result = db .prepare( 'INSERT INTO employees (name, email, role, weekly_capacity_hours) VALUES (?, ?, ?, ?)' ) .run(name, email || null, role || null, weekly_capacity_hours ?? 40); res.status(201).json(db.prepare('SELECT * FROM employees WHERE id = ?').get(result.lastInsertRowid)); }); app.put('/api/employees/:id', (req, res) => { const { name, email, role, weekly_capacity_hours } = req.body; const existing = db.prepare('SELECT * FROM employees WHERE id = ?').get(req.params.id); if (!existing) return res.status(404).json({ error: 'nicht gefunden' }); db.prepare( 'UPDATE employees SET name = ?, email = ?, role = ?, weekly_capacity_hours = ? WHERE id = ?' ).run( name ?? existing.name, email ?? existing.email, role ?? existing.role, weekly_capacity_hours ?? existing.weekly_capacity_hours, req.params.id ); res.json(db.prepare('SELECT * FROM employees WHERE id = ?').get(req.params.id)); }); app.delete('/api/employees/:id', (req, res) => { db.prepare('DELETE FROM employees WHERE id = ?').run(req.params.id); res.status(204).end(); }); // ---------- teams ---------- app.get('/api/teams', (req, res) => { const teams = db.prepare('SELECT * FROM teams ORDER BY name').all(); const members = db .prepare( `SELECT tm.team_id, e.* FROM team_members tm JOIN employees e ON e.id = tm.employee_id` ) .all(); res.json( teams.map((t) => ({ ...t, members: members.filter((m) => m.team_id === t.id).map(({ team_id, ...rest }) => rest), })) ); }); app.post('/api/teams', (req, res) => { const { name, description } = req.body; if (!name) return res.status(400).json({ error: 'name ist erforderlich' }); const result = db .prepare('INSERT INTO teams (name, description) VALUES (?, ?)') .run(name, description || null); res.status(201).json({ ...db.prepare('SELECT * FROM teams WHERE id = ?').get(result.lastInsertRowid), members: [] }); }); app.put('/api/teams/:id', (req, res) => { const { name, description } = req.body; const existing = db.prepare('SELECT * FROM teams WHERE id = ?').get(req.params.id); if (!existing) return res.status(404).json({ error: 'nicht gefunden' }); db.prepare('UPDATE teams SET name = ?, description = ? WHERE id = ?').run( name ?? existing.name, description ?? existing.description, req.params.id ); res.json(db.prepare('SELECT * FROM teams WHERE id = ?').get(req.params.id)); }); app.delete('/api/teams/:id', (req, res) => { db.prepare('DELETE FROM teams WHERE id = ?').run(req.params.id); res.status(204).end(); }); app.post('/api/teams/:id/members', (req, res) => { const { employee_id } = req.body; db.prepare('INSERT OR IGNORE INTO team_members (team_id, employee_id) VALUES (?, ?)').run( req.params.id, employee_id ); res.status(201).end(); }); app.delete('/api/teams/:id/members/:employeeId', (req, res) => { db.prepare('DELETE FROM team_members WHERE team_id = ? AND employee_id = ?').run( req.params.id, req.params.employeeId ); res.status(204).end(); }); // ---------- projects & tasks ---------- app.get('/api/projects', (req, res) => { const projects = db.prepare('SELECT * FROM projects ORDER BY start_date').all(); const tasks = db.prepare('SELECT * FROM tasks ORDER BY start_date').all(); const assignments = db .prepare( `SELECT a.*, e.name AS employee_name FROM assignments a JOIN employees e ON e.id = a.employee_id` ) .all(); const deps = db.prepare('SELECT * FROM task_dependencies').all(); const tasksByProject = projects.map((p) => ({ ...p, tasks: tasks .filter((t) => t.project_id === p.id) .map((t) => ({ ...t, assignments: assignments.filter((a) => a.task_id === t.id), depends_on: deps.filter((d) => d.task_id === t.id).map((d) => d.depends_on_task_id), })), })); res.json(tasksByProject); }); app.post('/api/projects', (req, res) => { const { name, description, start_date, end_date, color } = req.body; if (!name) return res.status(400).json({ error: 'name ist erforderlich' }); const result = db .prepare( 'INSERT INTO projects (name, description, start_date, end_date, color) VALUES (?, ?, ?, ?, ?)' ) .run(name, description || null, start_date || null, end_date || null, color || '#4f46e5'); res.status(201).json(db.prepare('SELECT * FROM projects WHERE id = ?').get(result.lastInsertRowid)); }); app.put('/api/projects/:id', (req, res) => { const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(req.params.id); if (!existing) return res.status(404).json({ error: 'nicht gefunden' }); const { name, description, start_date, end_date, color } = req.body; db.prepare( 'UPDATE projects SET name = ?, description = ?, start_date = ?, end_date = ?, color = ? WHERE id = ?' ).run( name ?? existing.name, description ?? existing.description, start_date ?? existing.start_date, end_date ?? existing.end_date, color ?? existing.color, req.params.id ); res.json(db.prepare('SELECT * FROM projects WHERE id = ?').get(req.params.id)); }); app.delete('/api/projects/:id', (req, res) => { db.prepare('DELETE FROM projects WHERE id = ?').run(req.params.id); res.status(204).end(); }); app.post('/api/tasks', (req, res) => { const { project_id, name, start_date, end_date, estimated_hours, status } = req.body; if (!project_id || !name || !start_date || !end_date) { return res.status(400).json({ error: 'project_id, name, start_date, end_date sind erforderlich' }); } const result = db .prepare( 'INSERT INTO tasks (project_id, name, start_date, end_date, estimated_hours, status) VALUES (?, ?, ?, ?, ?, ?)' ) .run(project_id, name, start_date, end_date, estimated_hours || 0, status || 'geplant'); res.status(201).json(db.prepare('SELECT * FROM tasks WHERE id = ?').get(result.lastInsertRowid)); }); app.put('/api/tasks/:id', (req, res) => { const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id); if (!existing) return res.status(404).json({ error: 'nicht gefunden' }); const { name, start_date, end_date, estimated_hours, status } = req.body; db.prepare( 'UPDATE tasks SET name = ?, start_date = ?, end_date = ?, estimated_hours = ?, status = ? WHERE id = ?' ).run( name ?? existing.name, start_date ?? existing.start_date, end_date ?? existing.end_date, estimated_hours ?? existing.estimated_hours, status ?? existing.status, req.params.id ); res.json(db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id)); }); app.delete('/api/tasks/:id', (req, res) => { db.prepare('DELETE FROM tasks WHERE id = ?').run(req.params.id); res.status(204).end(); }); app.post('/api/tasks/:id/dependencies', (req, res) => { const { depends_on_task_id } = req.body; db.prepare( 'INSERT OR IGNORE INTO task_dependencies (task_id, depends_on_task_id) VALUES (?, ?)' ).run(req.params.id, depends_on_task_id); res.status(201).end(); }); app.delete('/api/tasks/:id/dependencies/:dependsOnId', (req, res) => { db.prepare( 'DELETE FROM task_dependencies WHERE task_id = ? AND depends_on_task_id = ?' ).run(req.params.id, req.params.dependsOnId); res.status(204).end(); }); // ---------- assignments ---------- app.post('/api/assignments', (req, res) => { const { task_id, employee_id, allocated_hours_per_week } = req.body; if (!task_id || !employee_id) { return res.status(400).json({ error: 'task_id und employee_id sind erforderlich' }); } const result = db .prepare( `INSERT INTO assignments (task_id, employee_id, allocated_hours_per_week) VALUES (?, ?, ?) ON CONFLICT (task_id, employee_id) DO UPDATE SET allocated_hours_per_week = excluded.allocated_hours_per_week` ) .run(task_id, employee_id, allocated_hours_per_week || 0); const id = result.lastInsertRowid || db.prepare( 'SELECT id FROM assignments WHERE task_id = ? AND employee_id = ?' ).get(task_id, employee_id).id; res.status(201).json(db.prepare('SELECT * FROM assignments WHERE id = ?').get(id)); }); app.put('/api/assignments/:id', (req, res) => { const { allocated_hours_per_week } = req.body; db.prepare('UPDATE assignments SET allocated_hours_per_week = ? WHERE id = ?').run( allocated_hours_per_week, req.params.id ); res.json(db.prepare('SELECT * FROM assignments WHERE id = ?').get(req.params.id)); }); app.delete('/api/assignments/:id', (req, res) => { db.prepare('DELETE FROM assignments WHERE id = ?').run(req.params.id); res.status(204).end(); }); // ---------- utilization heatmap ---------- app.get('/api/utilization', (req, res) => { const from = req.query.from ? parseDate(req.query.from) : mondayOf(new Date()); const weekCount = Math.min(Number(req.query.weeks) || 12, 52); const weeks = []; for (let i = 0; i < weekCount; i++) { const weekStart = addDays(mondayOf(from), i * 7); weeks.push({ key: isoWeekKey(weekStart), start: weekStart }); } const employees = db.prepare('SELECT * FROM employees ORDER BY name').all(); const rows = db .prepare( `SELECT a.employee_id, a.allocated_hours_per_week, t.start_date, t.end_date FROM assignments a JOIN tasks t ON t.id = a.task_id` ) .all(); const data = {}; for (const e of employees) data[e.id] = {}; for (const row of rows) { const taskStart = parseDate(row.start_date); const taskEnd = parseDate(row.end_date); for (const week of weeks) { const weekEnd = addDays(week.start, 6); const overlapStart = taskStart > week.start ? taskStart : week.start; const overlapEnd = taskEnd < weekEnd ? taskEnd : weekEnd; const overlapDays = Math.max(0, Math.round((overlapEnd - overlapStart) / 86400000) + 1); if (overlapDays <= 0) continue; const hours = row.allocated_hours_per_week * (overlapDays / 7); data[row.employee_id][week.key] = (data[row.employee_id][week.key] || 0) + hours; } } res.json({ weeks: weeks.map((w) => w.key), employees: employees.map((e) => ({ id: e.id, name: e.name, weekly_capacity_hours: e.weekly_capacity_hours, hours: weeks.map((w) => Math.round((data[e.id][w.key] || 0) * 10) / 10), })), }); }); app.get('/health', (req, res) => res.json({ ok: true })); // serve built frontend in production const path = require('path'); const fs = require('fs'); const clientDist = path.join(__dirname, '..', 'client', 'dist'); if (fs.existsSync(clientDist)) { app.use(express.static(clientDist)); app.get(/^(?!\/api|\/health).*/, (req, res) => { res.sendFile(path.join(clientDist, 'index.html')); }); } app.listen(PORT, () => { console.log(`Ressourcenplanung-API läuft auf http://localhost:${PORT}`); });