diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..62eb288 --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,33 @@ +const express = require('express'); + +const router = express.Router(); + +router.get('/login', (req, res) => { + if (req.session.authenticated) return res.redirect('/'); + res.render('login', { error: null, title: 'Anmelden' }); +}); + +router.post('/login', (req, res) => { + const { username, password } = req.body; + const validUser = process.env.APP_USERNAME || 'admin'; + const validPass = process.env.APP_PASSWORD || 'changeme'; + + if (username === validUser && password === validPass) { + req.session.authenticated = true; + req.session.username = username; + return res.redirect('/'); + } + + res.render('login', { + error: 'Benutzername oder Passwort ist falsch.', + title: 'Anmelden', + }); +}); + +router.post('/logout', (req, res) => { + req.session.destroy(() => { + res.redirect('/login'); + }); +}); + +module.exports = router; diff --git a/src/routes/dashboard.js b/src/routes/dashboard.js new file mode 100644 index 0000000..dea7205 --- /dev/null +++ b/src/routes/dashboard.js @@ -0,0 +1,101 @@ +const express = require('express'); + +const router = express.Router(); +const { requireAuth } = require('../middleware/auth'); +const TimeEntry = require('../models/TimeEntry'); +const Settings = require('../models/Settings'); +const { + computeWorkedMinutes, + computeBalanceMinutes, + formatBalance, + minutesToHM, + formatDateDisplay, + formatMonthDisplay, +} = require('../utils/time'); + +router.get('/', requireAuth, async (req, res) => { + const settings = await Settings.getSingleton(); + const entries = await TimeEntry.find().sort({ date: -1 }).limit(90).lean(); + + const totalAgg = await TimeEntry.aggregate([ + { $group: { _id: null, total: { $sum: '$balanceMinutes' } } }, + ]); + const totalBalanceMinutes = settings.startingBalanceMinutes + (totalAgg[0]?.total || 0); + + const monthlyAgg = await TimeEntry.aggregate([ + { + $group: { + _id: { $substrCP: ['$date', 0, 7] }, + balance: { $sum: '$balanceMinutes' }, + worked: { $sum: '$workedMinutes' }, + days: { $sum: 1 }, + }, + }, + { $sort: { _id: -1 } }, + { $limit: 12 }, + ]); + + let editEntry = null; + if (req.query.edit) { + editEntry = await TimeEntry.findOne({ date: req.query.edit }).lean(); + } + + res.render('dashboard', { + title: 'Übersicht', + entries, + settings, + totalBalanceMinutes, + monthlyAgg, + editEntry, + formatBalance, + minutesToHM, + formatDateDisplay, + formatMonthDisplay, + error: req.query.error || null, + }); +}); + +router.post('/entries', requireAuth, async (req, res) => { + try { + const { date, startTime, endTime, breakMinutes, note } = req.body; + + if (!date || !startTime || !endTime) { + return res.redirect('/?error=Bitte+Datum%2C+Start-+und+Endzeit+angeben.'); + } + + const workedMinutes = computeWorkedMinutes(startTime, endTime, breakMinutes); + + if (workedMinutes < 0) { + return res.redirect('/?error=Die+Endzeit+muss+nach+der+Startzeit+liegen.'); + } + + const settings = await Settings.getSingleton(); + const balanceMinutes = computeBalanceMinutes(workedMinutes, date, settings); + + await TimeEntry.findOneAndUpdate( + { date }, + { + date, + startTime, + endTime, + breakMinutes: Number(breakMinutes || 0), + note: note || '', + workedMinutes, + balanceMinutes, + }, + { upsert: true, new: true } + ); + + res.redirect('/'); + } catch (err) { + console.error(err); + res.redirect('/?error=Beim+Speichern+ist+ein+Fehler+aufgetreten.'); + } +}); + +router.post('/entries/:id/delete', requireAuth, async (req, res) => { + await TimeEntry.findByIdAndDelete(req.params.id); + res.redirect('/'); +}); + +module.exports = router; diff --git a/src/routes/settings.js b/src/routes/settings.js new file mode 100644 index 0000000..7fa3a52 --- /dev/null +++ b/src/routes/settings.js @@ -0,0 +1,36 @@ +const express = require('express'); + +const router = express.Router(); +const { requireAuth } = require('../middleware/auth'); +const Settings = require('../models/Settings'); +const { WEEKDAY_NAMES } = require('../utils/time'); + +router.get('/settings', requireAuth, async (req, res) => { + const settings = await Settings.getSingleton(); + res.render('settings', { + title: 'Einstellungen', + settings, + weekdayNames: WEEKDAY_NAMES, + saved: req.query.saved === '1', + }); +}); + +router.post('/settings', requireAuth, async (req, res) => { + const { targetHoursPerDay, startingBalanceHours, workDays } = req.body; + + const days = Array.isArray(workDays) + ? workDays.map(Number) + : workDays + ? [Number(workDays)] + : []; + + const settings = await Settings.getSingleton(); + settings.targetHoursPerDay = Number(targetHoursPerDay) || 0; + settings.workDays = days; + settings.startingBalanceMinutes = Math.round(Number(startingBalanceHours || 0) * 60); + await settings.save(); + + res.redirect('/settings?saved=1'); +}); + +module.exports = router;