1596 lines
59 KiB
JavaScript
1596 lines
59 KiB
JavaScript
const express = require('express');
|
|
const session = require('express-session');
|
|
const bodyParser = require('body-parser');
|
|
const bcrypt = require('bcryptjs');
|
|
const path = require('path');
|
|
const { db, initDatabase } = require('./database');
|
|
const LDAPService = require('./ldap-service');
|
|
const cron = require('node-cron');
|
|
|
|
const app = express();
|
|
const PORT = 3000;
|
|
|
|
// Middleware
|
|
app.use(bodyParser.urlencoded({ extended: true }));
|
|
app.use(bodyParser.json());
|
|
app.use(express.static('public'));
|
|
app.set('view engine', 'ejs');
|
|
app.set('views', path.join(__dirname, 'views'));
|
|
|
|
// Session-Konfiguration
|
|
app.use(session({
|
|
secret: 'stundenerfassung-geheim-2024',
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 Stunden
|
|
}));
|
|
|
|
// Datenbank initialisieren
|
|
initDatabase();
|
|
|
|
// Helper: Prüft ob User eine bestimmte Rolle hat
|
|
function hasRole(req, role) {
|
|
if (!req.session.roles || !Array.isArray(req.session.roles)) {
|
|
return false;
|
|
}
|
|
return req.session.roles.includes(role);
|
|
}
|
|
|
|
// Helper: Bestimmt die Standard-Rolle (höchste Priorität: admin > verwaltung > mitarbeiter)
|
|
function getDefaultRole(roles) {
|
|
if (!Array.isArray(roles) || roles.length === 0) {
|
|
return 'mitarbeiter';
|
|
}
|
|
if (roles.includes('admin')) return 'admin';
|
|
if (roles.includes('verwaltung')) return 'verwaltung';
|
|
return roles[0]; // Fallback auf erste Rolle
|
|
}
|
|
|
|
// Helper: Gibt aktuelles Datum als YYYY-MM-DD zurück
|
|
function getCurrentDate() {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
const day = String(now.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
// Helper: Gibt aktuelle Zeit als HH:MM zurück
|
|
function getCurrentTime() {
|
|
const now = new Date();
|
|
const hours = String(now.getHours()).padStart(2, '0');
|
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
return `${hours}:${minutes}`;
|
|
}
|
|
|
|
// Helper: Berechnet Pausenzeit in Minuten zwischen zwei Zeiten
|
|
function calculateBreakMinutes(pauseStart, pauseEnd) {
|
|
if (!pauseStart || !pauseEnd) return 0;
|
|
|
|
const [startHours, startMinutes] = pauseStart.split(':').map(Number);
|
|
const [endHours, endMinutes] = pauseEnd.split(':').map(Number);
|
|
|
|
const startTotalMinutes = startHours * 60 + startMinutes;
|
|
const endTotalMinutes = endHours * 60 + endMinutes;
|
|
|
|
return endTotalMinutes - startTotalMinutes;
|
|
}
|
|
|
|
// Helper: Berechnet total_hours basierend auf start_time, end_time und break_minutes
|
|
function updateTotalHours(startTime, endTime, breakMinutes) {
|
|
if (!startTime || !endTime) return 0;
|
|
|
|
const [startHours, startMinutes] = startTime.split(':').map(Number);
|
|
const [endHours, endMinutes] = endTime.split(':').map(Number);
|
|
|
|
const startTotalMinutes = startHours * 60 + startMinutes;
|
|
const endTotalMinutes = endHours * 60 + endMinutes;
|
|
|
|
const totalMinutes = endTotalMinutes - startTotalMinutes - (breakMinutes || 0);
|
|
return totalMinutes / 60; // Konvertiere zu Stunden
|
|
}
|
|
|
|
// Middleware: Authentifizierung prüfen
|
|
function requireAuth(req, res, next) {
|
|
if (req.session.userId) {
|
|
next();
|
|
} else {
|
|
res.redirect('/login');
|
|
}
|
|
}
|
|
|
|
// Middleware: Prüft ob User eine bestimmte Rolle hat
|
|
function requireRole(role) {
|
|
return (req, res, next) => {
|
|
if (req.session.userId && hasRole(req, role)) {
|
|
next();
|
|
} else {
|
|
res.status(403).send('Zugriff verweigert');
|
|
}
|
|
};
|
|
}
|
|
|
|
// Middleware: Admin-Rolle prüfen
|
|
function requireAdmin(req, res, next) {
|
|
if (req.session.userId && hasRole(req, 'admin')) {
|
|
next();
|
|
} else {
|
|
res.status(403).send('Zugriff verweigert');
|
|
}
|
|
}
|
|
|
|
// Middleware: Verwaltung-Rolle prüfen (Verwaltung oder Admin)
|
|
function requireVerwaltung(req, res, next) {
|
|
if (req.session.userId && (hasRole(req, 'verwaltung') || hasRole(req, 'admin'))) {
|
|
next();
|
|
} else {
|
|
res.status(403).send('Zugriff verweigert');
|
|
}
|
|
}
|
|
|
|
// ROUTEN
|
|
|
|
// Login-Seite
|
|
app.get('/login', (req, res) => {
|
|
res.render('login', { error: null });
|
|
});
|
|
|
|
// Login-Verarbeitung
|
|
app.post('/login', (req, res) => {
|
|
const { username, password } = req.body;
|
|
|
|
db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => {
|
|
if (err || !user) {
|
|
return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' });
|
|
}
|
|
|
|
if (bcrypt.compareSync(password, user.password)) {
|
|
// Rollen als JSON-Array parsen
|
|
let roles = [];
|
|
try {
|
|
roles = JSON.parse(user.role);
|
|
if (!Array.isArray(roles)) {
|
|
// Fallback: Falls kein Array, erstelle Array mit vorhandener Rolle
|
|
roles = [user.role];
|
|
}
|
|
} catch (e) {
|
|
// Fallback: Falls kein JSON, erstelle Array mit vorhandener Rolle
|
|
roles = [user.role || 'mitarbeiter'];
|
|
}
|
|
|
|
// Standard-Rolle bestimmen: Immer "mitarbeiter" wenn vorhanden, sonst höchste Priorität
|
|
let defaultRole;
|
|
if (roles.includes('mitarbeiter')) {
|
|
defaultRole = 'mitarbeiter';
|
|
} else {
|
|
defaultRole = getDefaultRole(roles);
|
|
}
|
|
|
|
req.session.userId = user.id;
|
|
req.session.username = user.username;
|
|
req.session.roles = roles;
|
|
req.session.currentRole = defaultRole;
|
|
req.session.firstname = user.firstname;
|
|
req.session.lastname = user.lastname;
|
|
|
|
// Redirect: Immer zu Dashboard wenn Mitarbeiter-Rolle vorhanden, sonst basierend auf Standard-Rolle
|
|
if (roles.includes('mitarbeiter')) {
|
|
res.redirect('/dashboard');
|
|
} else if (defaultRole === 'admin') {
|
|
res.redirect('/admin');
|
|
} else if (defaultRole === 'verwaltung') {
|
|
res.redirect('/verwaltung');
|
|
} else {
|
|
res.redirect('/dashboard');
|
|
}
|
|
} else {
|
|
res.render('login', { error: 'Ungültiger Benutzername oder Passwort' });
|
|
}
|
|
});
|
|
});
|
|
|
|
// Logout
|
|
app.get('/logout', (req, res) => {
|
|
req.session.destroy();
|
|
res.redirect('/login');
|
|
});
|
|
|
|
// Dashboard für Mitarbeiter
|
|
app.get('/dashboard', requireAuth, (req, res) => {
|
|
// Prüfe ob User Mitarbeiter-Rolle hat
|
|
if (!hasRole(req, 'mitarbeiter')) {
|
|
// Wenn User keine Mitarbeiter-Rolle hat, aber andere Rollen, redirecte entsprechend
|
|
if (hasRole(req, 'admin')) {
|
|
return res.redirect('/admin');
|
|
}
|
|
if (hasRole(req, 'verwaltung')) {
|
|
return res.redirect('/verwaltung');
|
|
}
|
|
return res.status(403).send('Zugriff verweigert');
|
|
}
|
|
|
|
res.render('dashboard', {
|
|
user: {
|
|
id: req.session.userId,
|
|
firstname: req.session.firstname,
|
|
lastname: req.session.lastname,
|
|
username: req.session.username,
|
|
roles: req.session.roles || [],
|
|
currentRole: req.session.currentRole || 'mitarbeiter'
|
|
}
|
|
});
|
|
});
|
|
|
|
// API: Letzte bearbeitete Woche abrufen
|
|
app.get('/api/user/last-week', requireAuth, (req, res) => {
|
|
const userId = req.session.userId;
|
|
|
|
db.get('SELECT last_week_start FROM users WHERE id = ?', [userId], (err, user) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Abrufen der letzten Woche' });
|
|
}
|
|
|
|
res.json({ last_week_start: user?.last_week_start || null });
|
|
});
|
|
});
|
|
|
|
// API: Letzte bearbeitete Woche speichern
|
|
app.post('/api/user/last-week', requireAuth, (req, res) => {
|
|
const userId = req.session.userId;
|
|
const { week_start } = req.body;
|
|
|
|
if (!week_start) {
|
|
return res.status(400).json({ error: 'week_start ist erforderlich' });
|
|
}
|
|
|
|
db.run('UPDATE users SET last_week_start = ? WHERE id = ?',
|
|
[week_start, userId],
|
|
(err) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Speichern der letzten Woche' });
|
|
}
|
|
res.json({ success: true });
|
|
});
|
|
});
|
|
|
|
// API: User-Daten abrufen (Wochenstunden)
|
|
app.get('/api/user/data', requireAuth, (req, res) => {
|
|
const userId = req.session.userId;
|
|
|
|
db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
|
|
}
|
|
|
|
res.json({ wochenstunden: user?.wochenstunden || 0 });
|
|
});
|
|
});
|
|
|
|
// API: Rollenwechsel
|
|
app.post('/api/user/switch-role', requireAuth, (req, res) => {
|
|
const { role } = req.body;
|
|
|
|
if (!role) {
|
|
return res.status(400).json({ error: 'Rolle ist erforderlich' });
|
|
}
|
|
|
|
// Prüfe ob User diese Rolle hat
|
|
if (!hasRole(req, role)) {
|
|
return res.status(403).json({ error: 'Sie haben keine Berechtigung für diese Rolle' });
|
|
}
|
|
|
|
// Validiere dass die Rolle eine gültige Rolle ist
|
|
const validRoles = ['mitarbeiter', 'verwaltung', 'admin'];
|
|
if (!validRoles.includes(role)) {
|
|
return res.status(400).json({ error: 'Ungültige Rolle' });
|
|
}
|
|
|
|
// Setze aktuelle Rolle
|
|
req.session.currentRole = role;
|
|
|
|
res.json({ success: true, currentRole: role });
|
|
});
|
|
|
|
// API: Gesamtstatistiken für Mitarbeiter (Überstunden und Urlaubstage)
|
|
app.get('/api/user/stats', requireAuth, (req, res) => {
|
|
const userId = req.session.userId;
|
|
|
|
// User-Daten abrufen
|
|
db.get('SELECT wochenstunden, urlaubstage FROM users WHERE id = ?', [userId], (err, user) => {
|
|
if (err || !user) {
|
|
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
|
|
}
|
|
|
|
const wochenstunden = user.wochenstunden || 0;
|
|
const urlaubstage = user.urlaubstage || 0;
|
|
|
|
// Alle eingereichten Wochen abrufen
|
|
db.all(`SELECT DISTINCT week_start, week_end
|
|
FROM weekly_timesheets
|
|
WHERE user_id = ? AND status = 'eingereicht'
|
|
ORDER BY week_start`,
|
|
[userId],
|
|
(err, weeks) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' });
|
|
}
|
|
|
|
// Wenn keine Wochen vorhanden
|
|
if (!weeks || weeks.length === 0) {
|
|
return res.json({
|
|
currentOvertime: 0,
|
|
remainingVacation: urlaubstage,
|
|
totalOvertimeHours: 0,
|
|
totalOvertimeTaken: 0,
|
|
totalVacationDays: 0,
|
|
urlaubstage: urlaubstage
|
|
});
|
|
}
|
|
|
|
let totalOvertimeHours = 0;
|
|
let totalOvertimeTaken = 0;
|
|
let totalVacationDays = 0;
|
|
let processedWeeks = 0;
|
|
let hasError = false;
|
|
|
|
// Für jede Woche die Statistiken berechnen
|
|
weeks.forEach((week) => {
|
|
// Einträge für diese Woche abrufen
|
|
db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type
|
|
FROM timesheet_entries
|
|
WHERE user_id = ? AND date >= ? AND date <= ?
|
|
ORDER BY date`,
|
|
[userId, week.week_start, week.week_end],
|
|
(err, entries) => {
|
|
if (hasError) return; // Wenn bereits ein Fehler aufgetreten ist, ignoriere weitere Ergebnisse
|
|
|
|
if (err) {
|
|
hasError = true;
|
|
return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' });
|
|
}
|
|
|
|
// Berechnungen für diese Woche
|
|
let weekTotalHours = 0;
|
|
let weekOvertimeTaken = 0;
|
|
let weekVacationDays = 0;
|
|
let weekVacationHours = 0;
|
|
|
|
entries.forEach(entry => {
|
|
if (entry.total_hours) {
|
|
weekTotalHours += entry.total_hours;
|
|
}
|
|
if (entry.overtime_taken_hours) {
|
|
weekOvertimeTaken += entry.overtime_taken_hours;
|
|
}
|
|
if (entry.vacation_type === 'full') {
|
|
weekVacationDays += 1;
|
|
weekVacationHours += 8;
|
|
} else if (entry.vacation_type === 'half') {
|
|
weekVacationDays += 0.5;
|
|
weekVacationHours += 4;
|
|
}
|
|
});
|
|
|
|
// Anzahl Werktage berechnen (Montag-Freitag)
|
|
const startDate = new Date(week.week_start);
|
|
const endDate = new Date(week.week_end);
|
|
let workdays = 0;
|
|
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
|
const day = d.getDay();
|
|
if (day >= 1 && day <= 5) { // Montag bis Freitag
|
|
workdays++;
|
|
}
|
|
}
|
|
|
|
// Sollstunden berechnen
|
|
const sollStunden = (wochenstunden / 5) * workdays;
|
|
|
|
// Überstunden für diese Woche: Urlaub zählt als normale Arbeitszeit
|
|
const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours;
|
|
const weekOvertimeHours = weekTotalHoursWithVacation - sollStunden;
|
|
|
|
// Kumulativ addieren
|
|
totalOvertimeHours += weekOvertimeHours;
|
|
totalOvertimeTaken += weekOvertimeTaken;
|
|
totalVacationDays += weekVacationDays;
|
|
|
|
processedWeeks++;
|
|
|
|
// Wenn alle Wochen verarbeitet wurden, Antwort senden
|
|
if (processedWeeks === weeks.length && !hasError) {
|
|
const currentOvertime = totalOvertimeHours - totalOvertimeTaken;
|
|
const remainingVacation = urlaubstage - totalVacationDays;
|
|
|
|
res.json({
|
|
currentOvertime: currentOvertime,
|
|
remainingVacation: remainingVacation,
|
|
totalOvertimeHours: totalOvertimeHours,
|
|
totalOvertimeTaken: totalOvertimeTaken,
|
|
totalVacationDays: totalVacationDays,
|
|
urlaubstage: urlaubstage
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// Admin-Bereich
|
|
app.get('/admin', requireAdmin, (req, res) => {
|
|
db.all('SELECT id, username, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, created_at FROM users ORDER BY created_at DESC',
|
|
(err, users) => {
|
|
// LDAP-Konfiguration und Sync-Log abrufen
|
|
db.get('SELECT * FROM ldap_config WHERE id = 1', (err, ldapConfig) => {
|
|
db.all('SELECT * FROM ldap_sync_log ORDER BY sync_started_at DESC LIMIT 10', (err, syncLogs) => {
|
|
// Parse Rollen für jeden User
|
|
const usersWithRoles = (users || []).map(u => {
|
|
let roles = [];
|
|
try {
|
|
roles = JSON.parse(u.role);
|
|
if (!Array.isArray(roles)) {
|
|
roles = [u.role];
|
|
}
|
|
} catch (e) {
|
|
roles = [u.role || 'mitarbeiter'];
|
|
}
|
|
return { ...u, roles };
|
|
});
|
|
|
|
res.render('admin', {
|
|
users: usersWithRoles,
|
|
ldapConfig: ldapConfig || null,
|
|
syncLogs: syncLogs || [],
|
|
user: {
|
|
firstname: req.session.firstname,
|
|
lastname: req.session.lastname,
|
|
roles: req.session.roles || [],
|
|
currentRole: req.session.currentRole || 'admin'
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// Benutzer erstellen
|
|
app.post('/admin/users', requireAdmin, (req, res) => {
|
|
const { username, password, firstname, lastname, roles, personalnummer, wochenstunden, urlaubstage } = req.body;
|
|
const hashedPassword = bcrypt.hashSync(password, 10);
|
|
|
|
// Normalisiere die optionalen Felder
|
|
const normalizedPersonalnummer = personalnummer && personalnummer.trim() !== '' ? personalnummer.trim() : null;
|
|
const normalizedWochenstunden = wochenstunden && wochenstunden !== '' ? parseFloat(wochenstunden) : null;
|
|
const normalizedUrlaubstage = urlaubstage && urlaubstage !== '' ? parseFloat(urlaubstage) : null;
|
|
|
|
// Rollen verarbeiten: Erwarte Array, konvertiere zu JSON-String
|
|
let rolesArray = [];
|
|
if (Array.isArray(roles)) {
|
|
rolesArray = roles.filter(r => r && ['mitarbeiter', 'verwaltung', 'admin'].includes(r));
|
|
} else if (roles) {
|
|
// Fallback: Einzelne Rolle als Array
|
|
rolesArray = [roles];
|
|
}
|
|
|
|
// Mindestens eine Rolle erforderlich
|
|
if (rolesArray.length === 0) {
|
|
rolesArray = ['mitarbeiter']; // Standard-Rolle
|
|
}
|
|
|
|
const rolesJson = JSON.stringify(rolesArray);
|
|
|
|
db.run('INSERT INTO users (username, password, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
[username, hashedPassword, firstname, lastname, rolesJson, normalizedPersonalnummer, normalizedWochenstunden, normalizedUrlaubstage],
|
|
(err) => {
|
|
if (err) {
|
|
return res.status(400).json({ error: 'Benutzername existiert bereits' });
|
|
}
|
|
res.json({ success: true });
|
|
});
|
|
});
|
|
|
|
// Benutzer löschen
|
|
app.delete('/admin/users/:id', requireAdmin, (req, res) => {
|
|
const userId = req.params.id;
|
|
|
|
// Admin darf sich nicht selbst löschen
|
|
if (userId == req.session.userId) {
|
|
return res.status(400).json({ error: 'Sie können sich nicht selbst löschen' });
|
|
}
|
|
|
|
db.run('DELETE FROM users WHERE id = ?', [userId], (err) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Löschen' });
|
|
}
|
|
res.json({ success: true });
|
|
});
|
|
});
|
|
|
|
// Benutzer aktualisieren (Personalnummer, Wochenstunden, Urlaubstage, Rollen)
|
|
app.put('/admin/users/:id', requireAdmin, (req, res) => {
|
|
const userId = req.params.id;
|
|
const { personalnummer, wochenstunden, urlaubstage, roles } = req.body;
|
|
|
|
// Rollen verarbeiten falls vorhanden
|
|
let rolesJson = null;
|
|
if (roles !== undefined) {
|
|
let rolesArray = [];
|
|
if (Array.isArray(roles)) {
|
|
rolesArray = roles.filter(r => r && ['mitarbeiter', 'verwaltung', 'admin'].includes(r));
|
|
}
|
|
// Mindestens eine Rolle erforderlich
|
|
if (rolesArray.length === 0) {
|
|
return res.status(400).json({ error: 'Mindestens eine Rolle ist erforderlich' });
|
|
}
|
|
rolesJson = JSON.stringify(rolesArray);
|
|
}
|
|
|
|
// SQL-Query dynamisch zusammenstellen
|
|
if (rolesJson !== null) {
|
|
// Aktualisiere auch Rollen
|
|
db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ?, role = ? WHERE id = ?',
|
|
[
|
|
personalnummer || null,
|
|
wochenstunden ? parseFloat(wochenstunden) : null,
|
|
urlaubstage ? parseFloat(urlaubstage) : null,
|
|
rolesJson,
|
|
userId
|
|
],
|
|
(err) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Aktualisieren' });
|
|
}
|
|
res.json({ success: true });
|
|
});
|
|
} else {
|
|
// Nur andere Felder aktualisieren
|
|
db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ? WHERE id = ?',
|
|
[
|
|
personalnummer || null,
|
|
wochenstunden ? parseFloat(wochenstunden) : null,
|
|
urlaubstage ? parseFloat(urlaubstage) : null,
|
|
userId
|
|
],
|
|
(err) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Aktualisieren' });
|
|
}
|
|
res.json({ success: true });
|
|
});
|
|
}
|
|
});
|
|
|
|
// LDAP-Konfiguration abrufen
|
|
app.get('/admin/ldap/config', requireAdmin, (req, res) => {
|
|
db.get('SELECT * FROM ldap_config WHERE id = 1', (err, config) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Abrufen der Konfiguration' });
|
|
}
|
|
|
|
// Passwort nicht zurückgeben
|
|
if (config) {
|
|
delete config.bind_password;
|
|
}
|
|
|
|
res.json({ config: config || null });
|
|
});
|
|
});
|
|
|
|
// LDAP-Konfiguration speichern
|
|
app.post('/admin/ldap/config', requireAdmin, (req, res) => {
|
|
const {
|
|
enabled,
|
|
url,
|
|
bind_dn,
|
|
bind_password,
|
|
base_dn,
|
|
user_search_filter,
|
|
username_attribute,
|
|
firstname_attribute,
|
|
lastname_attribute,
|
|
sync_interval
|
|
} = req.body;
|
|
|
|
// Validierung - nur wenn aktiviert
|
|
if (enabled && (!url || !base_dn)) {
|
|
return res.status(400).json({ error: 'URL und Base DN sind erforderlich wenn LDAP aktiviert ist' });
|
|
}
|
|
|
|
// Prüfe ob Konfiguration bereits existiert
|
|
db.get('SELECT id FROM ldap_config WHERE id = 1', (err, existing) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Prüfen der Konfiguration' });
|
|
}
|
|
|
|
const configData = {
|
|
enabled: enabled ? 1 : 0,
|
|
url: url.trim(),
|
|
bind_dn: bind_dn ? bind_dn.trim() : null,
|
|
bind_password: bind_password ? bind_password.trim() : null,
|
|
base_dn: base_dn.trim(),
|
|
user_search_filter: user_search_filter ? user_search_filter.trim() : '(objectClass=person)',
|
|
username_attribute: username_attribute ? username_attribute.trim() : 'cn',
|
|
firstname_attribute: firstname_attribute ? firstname_attribute.trim() : 'givenName',
|
|
lastname_attribute: lastname_attribute ? lastname_attribute.trim() : 'sn',
|
|
sync_interval: parseInt(sync_interval) || 0,
|
|
updated_at: new Date().toISOString()
|
|
};
|
|
|
|
if (existing) {
|
|
// Update - Passwort nur aktualisieren wenn angegeben
|
|
if (configData.bind_password) {
|
|
db.run(
|
|
`UPDATE ldap_config SET
|
|
enabled = ?, url = ?, bind_dn = ?, bind_password = ?, base_dn = ?,
|
|
user_search_filter = ?, username_attribute = ?, firstname_attribute = ?,
|
|
lastname_attribute = ?, sync_interval = ?, updated_at = ?
|
|
WHERE id = 1`,
|
|
[
|
|
configData.enabled, configData.url, configData.bind_dn, configData.bind_password,
|
|
configData.base_dn, configData.user_search_filter, configData.username_attribute,
|
|
configData.firstname_attribute, configData.lastname_attribute, configData.sync_interval,
|
|
configData.updated_at
|
|
],
|
|
(err) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Speichern der Konfiguration' });
|
|
}
|
|
res.json({ success: true });
|
|
}
|
|
);
|
|
} else {
|
|
// Passwort nicht ändern
|
|
db.run(
|
|
`UPDATE ldap_config SET
|
|
enabled = ?, url = ?, bind_dn = ?, base_dn = ?,
|
|
user_search_filter = ?, username_attribute = ?, firstname_attribute = ?,
|
|
lastname_attribute = ?, sync_interval = ?, updated_at = ?
|
|
WHERE id = 1`,
|
|
[
|
|
configData.enabled, configData.url, configData.bind_dn,
|
|
configData.base_dn, configData.user_search_filter, configData.username_attribute,
|
|
configData.firstname_attribute, configData.lastname_attribute, configData.sync_interval,
|
|
configData.updated_at
|
|
],
|
|
(err) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Speichern der Konfiguration' });
|
|
}
|
|
res.json({ success: true });
|
|
}
|
|
);
|
|
}
|
|
} else {
|
|
// Insert
|
|
db.run(
|
|
`INSERT INTO ldap_config (
|
|
enabled, url, bind_dn, bind_password, base_dn, user_search_filter,
|
|
username_attribute, firstname_attribute, lastname_attribute, sync_interval, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
configData.enabled, configData.url, configData.bind_dn, configData.bind_password,
|
|
configData.base_dn, configData.user_search_filter, configData.username_attribute,
|
|
configData.firstname_attribute, configData.lastname_attribute, configData.sync_interval,
|
|
configData.updated_at
|
|
],
|
|
(err) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Erstellen der Konfiguration' });
|
|
}
|
|
res.json({ success: true });
|
|
}
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Manuelle LDAP-Synchronisation starten
|
|
app.post('/admin/ldap/sync', requireAdmin, (req, res) => {
|
|
LDAPService.performSync('manual', (err, result) => {
|
|
if (err) {
|
|
return res.status(500).json({
|
|
error: err.message || 'Fehler bei der Synchronisation',
|
|
synced: result ? result.synced : 0,
|
|
errors: result ? result.errors : []
|
|
});
|
|
}
|
|
res.json({
|
|
success: true,
|
|
synced: result.synced,
|
|
errors: result.errors || []
|
|
});
|
|
});
|
|
});
|
|
|
|
// Sync-Log abrufen
|
|
app.get('/admin/ldap/sync/log', requireAdmin, (req, res) => {
|
|
const limit = parseInt(req.query.limit) || 10;
|
|
db.all(
|
|
'SELECT * FROM ldap_sync_log ORDER BY sync_started_at DESC LIMIT ?',
|
|
[limit],
|
|
(err, logs) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Abrufen des Logs' });
|
|
}
|
|
res.json({ logs: logs || [] });
|
|
}
|
|
);
|
|
});
|
|
|
|
// Verwaltungs-Bereich
|
|
app.get('/verwaltung', requireVerwaltung, (req, res) => {
|
|
db.all(`
|
|
SELECT wt.*, u.firstname, u.lastname, u.username, u.personalnummer, u.wochenstunden, u.urlaubstage,
|
|
dl.firstname as downloaded_by_firstname,
|
|
dl.lastname as downloaded_by_lastname,
|
|
(SELECT COUNT(*) FROM weekly_timesheets wt2
|
|
WHERE wt2.user_id = wt.user_id
|
|
AND wt2.week_start = wt.week_start
|
|
AND wt2.week_end = wt.week_end) as total_versions
|
|
FROM weekly_timesheets wt
|
|
JOIN users u ON wt.user_id = u.id
|
|
LEFT JOIN users dl ON wt.pdf_downloaded_by = dl.id
|
|
WHERE wt.status = 'eingereicht'
|
|
ORDER BY wt.week_start DESC, wt.user_id, wt.version DESC
|
|
`, (err, timesheets) => {
|
|
// Gruppiere nach Mitarbeiter, dann nach Kalenderwoche
|
|
// Struktur: { [user_id]: { user: {...}, weeks: { [week_key]: {...} } } }
|
|
const groupedByEmployee = {};
|
|
|
|
(timesheets || []).forEach(ts => {
|
|
const userId = ts.user_id;
|
|
const weekKey = `${ts.week_start}_${ts.week_end}`;
|
|
|
|
// Level 1: Mitarbeiter
|
|
if (!groupedByEmployee[userId]) {
|
|
groupedByEmployee[userId] = {
|
|
user: {
|
|
id: ts.user_id,
|
|
firstname: ts.firstname,
|
|
lastname: ts.lastname,
|
|
username: ts.username,
|
|
personalnummer: ts.personalnummer,
|
|
wochenstunden: ts.wochenstunden,
|
|
urlaubstage: ts.urlaubstage
|
|
},
|
|
weeks: {}
|
|
};
|
|
}
|
|
|
|
// Level 2: Kalenderwoche
|
|
if (!groupedByEmployee[userId].weeks[weekKey]) {
|
|
groupedByEmployee[userId].weeks[weekKey] = {
|
|
week_start: ts.week_start,
|
|
week_end: ts.week_end,
|
|
total_versions: ts.total_versions,
|
|
versions: []
|
|
};
|
|
}
|
|
|
|
// Level 3: Versionen
|
|
groupedByEmployee[userId].weeks[weekKey].versions.push(ts);
|
|
});
|
|
|
|
// Sortierung: Mitarbeiter nach Name, Wochen nach Datum (neueste zuerst)
|
|
const sortedEmployees = Object.values(groupedByEmployee).map(employee => {
|
|
// Wochen innerhalb jedes Mitarbeiters sortieren
|
|
const sortedWeeks = Object.values(employee.weeks).sort((a, b) => {
|
|
return new Date(b.week_start) - new Date(a.week_start);
|
|
});
|
|
|
|
return {
|
|
...employee,
|
|
weeks: sortedWeeks
|
|
};
|
|
}).sort((a, b) => {
|
|
// Mitarbeiter nach Nachname, dann Vorname sortieren
|
|
const nameA = `${a.user.lastname} ${a.user.firstname}`.toLowerCase();
|
|
const nameB = `${b.user.lastname} ${b.user.firstname}`.toLowerCase();
|
|
return nameA.localeCompare(nameB);
|
|
});
|
|
|
|
res.render('verwaltung', {
|
|
groupedByEmployee: sortedEmployees,
|
|
user: {
|
|
firstname: req.session.firstname,
|
|
lastname: req.session.lastname,
|
|
roles: req.session.roles || [],
|
|
currentRole: req.session.currentRole || 'verwaltung'
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// API: Stundenerfassung speichern
|
|
app.post('/api/timesheet/save', requireAuth, (req, res) => {
|
|
const {
|
|
date, start_time, end_time, break_minutes, notes,
|
|
activity1_desc, activity1_hours, activity1_project_number,
|
|
activity2_desc, activity2_hours, activity2_project_number,
|
|
activity3_desc, activity3_hours, activity3_project_number,
|
|
activity4_desc, activity4_hours, activity4_project_number,
|
|
activity5_desc, activity5_hours, activity5_project_number,
|
|
overtime_taken_hours, vacation_type
|
|
} = req.body;
|
|
const userId = req.session.userId;
|
|
|
|
// Normalisiere end_time: Leere Strings werden zu null
|
|
const normalizedEndTime = (end_time && typeof end_time === 'string' && end_time.trim() !== '') ? end_time.trim() : (end_time || null);
|
|
const normalizedStartTime = (start_time && typeof start_time === 'string' && start_time.trim() !== '') ? start_time.trim() : (start_time || null);
|
|
|
|
// Gesamtstunden berechnen (aus Start- und Endzeit, nicht aus Tätigkeiten)
|
|
// Wenn ganzer Tag Urlaub, dann zählt dieser als 8 Stunden normale Arbeitszeit
|
|
let total_hours = 0;
|
|
if (vacation_type === 'full') {
|
|
total_hours = 8; // Ganzer Tag Urlaub = 8 Stunden normale Arbeitszeit
|
|
} else if (normalizedStartTime && normalizedEndTime) {
|
|
const start = new Date(`2000-01-01T${normalizedStartTime}`);
|
|
const end = new Date(`2000-01-01T${normalizedEndTime}`);
|
|
const diffMs = end - start;
|
|
total_hours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60);
|
|
// Bei halbem Tag Urlaub: total_hours bleibt die tatsächlich gearbeiteten Stunden
|
|
// Die 4 Stunden Urlaub werden nur in der Überstunden-Berechnung hinzugezählt
|
|
}
|
|
|
|
// Prüfen ob Eintrag existiert - verwende den neuesten Eintrag falls mehrere existieren
|
|
db.get('SELECT id FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1',
|
|
[userId, date], (err, row) => {
|
|
if (row) {
|
|
// Update
|
|
db.run(`UPDATE timesheet_entries
|
|
SET start_time = ?, end_time = ?, break_minutes = ?, total_hours = ?, notes = ?,
|
|
activity1_desc = ?, activity1_hours = ?, activity1_project_number = ?,
|
|
activity2_desc = ?, activity2_hours = ?, activity2_project_number = ?,
|
|
activity3_desc = ?, activity3_hours = ?, activity3_project_number = ?,
|
|
activity4_desc = ?, activity4_hours = ?, activity4_project_number = ?,
|
|
activity5_desc = ?, activity5_hours = ?, activity5_project_number = ?,
|
|
overtime_taken_hours = ?, vacation_type = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?`,
|
|
[
|
|
normalizedStartTime, normalizedEndTime, break_minutes, total_hours, notes,
|
|
activity1_desc || null, parseFloat(activity1_hours) || 0, activity1_project_number || null,
|
|
activity2_desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null,
|
|
activity3_desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null,
|
|
activity4_desc || null, parseFloat(activity4_hours) || 0, activity4_project_number || null,
|
|
activity5_desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null,
|
|
overtime_taken_hours ? parseFloat(overtime_taken_hours) : null,
|
|
vacation_type || null,
|
|
row.id
|
|
],
|
|
(err) => {
|
|
if (err) {
|
|
console.error('Fehler beim Update:', err);
|
|
return res.status(500).json({ error: 'Fehler beim Speichern: ' + err.message });
|
|
}
|
|
res.json({ success: true, total_hours });
|
|
});
|
|
} else {
|
|
// Insert
|
|
db.run(`INSERT INTO timesheet_entries
|
|
(user_id, date, start_time, end_time, break_minutes, total_hours, notes,
|
|
activity1_desc, activity1_hours, activity1_project_number,
|
|
activity2_desc, activity2_hours, activity2_project_number,
|
|
activity3_desc, activity3_hours, activity3_project_number,
|
|
activity4_desc, activity4_hours, activity4_project_number,
|
|
activity5_desc, activity5_hours, activity5_project_number,
|
|
overtime_taken_hours, vacation_type)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
userId, date, normalizedStartTime, normalizedEndTime, break_minutes, total_hours, notes,
|
|
activity1_desc || null, parseFloat(activity1_hours) || 0, activity1_project_number || null,
|
|
activity2_desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null,
|
|
activity3_desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null,
|
|
activity4_desc || null, parseFloat(activity4_hours) || 0, activity4_project_number || null,
|
|
activity5_desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null,
|
|
overtime_taken_hours ? parseFloat(overtime_taken_hours) : null,
|
|
vacation_type || null
|
|
],
|
|
(err) => {
|
|
if (err) {
|
|
console.error('Fehler beim Insert:', err);
|
|
return res.status(500).json({ error: 'Fehler beim Speichern: ' + err.message });
|
|
}
|
|
res.json({ success: true, total_hours });
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// API: Check-in (Kommen)
|
|
app.get('/api/checkin/:userId', (req, res) => {
|
|
const userId = parseInt(req.params.userId);
|
|
const currentDate = getCurrentDate();
|
|
const currentTime = getCurrentTime();
|
|
|
|
// Prüfe ob User existiert
|
|
db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => {
|
|
if (err || !user) {
|
|
return res.status(404).json({ success: false, error: 'Benutzer nicht gefunden' });
|
|
}
|
|
|
|
// Prüfe ob bereits ein Eintrag für heute existiert
|
|
db.get('SELECT * FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1',
|
|
[userId, currentDate], (err, entry) => {
|
|
if (err) {
|
|
return res.status(500).json({ success: false, error: 'Fehler beim Abrufen des Eintrags' });
|
|
}
|
|
|
|
if (!entry) {
|
|
// Kein Eintrag existiert → Erstelle neuen mit start_time
|
|
db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
[userId, currentDate, currentTime], (err) => {
|
|
if (err) {
|
|
return res.status(500).json({ success: false, error: 'Fehler beim Erstellen des Eintrags' });
|
|
}
|
|
res.json({
|
|
success: true,
|
|
message: `Start-Zeit erfasst: ${currentTime}`,
|
|
start_time: currentTime,
|
|
date: currentDate
|
|
});
|
|
});
|
|
} else if (!entry.start_time) {
|
|
// Eintrag existiert, aber keine Start-Zeit → Setze start_time
|
|
db.run('UPDATE timesheet_entries SET start_time = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
[currentTime, entry.id], (err) => {
|
|
if (err) {
|
|
return res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' });
|
|
}
|
|
res.json({
|
|
success: true,
|
|
message: `Start-Zeit erfasst: ${currentTime}`,
|
|
start_time: currentTime,
|
|
date: currentDate
|
|
});
|
|
});
|
|
} else {
|
|
// Start-Zeit bereits vorhanden → Ignoriere weiteren Check-in
|
|
res.json({
|
|
success: true,
|
|
message: `Bereits eingecheckt um ${entry.start_time}. Check-in ignoriert.`,
|
|
start_time: entry.start_time,
|
|
date: currentDate
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// API: Check-out (Gehen)
|
|
app.get('/api/checkout/:userId', (req, res) => {
|
|
const userId = parseInt(req.params.userId);
|
|
const currentDate = getCurrentDate();
|
|
const currentTime = getCurrentTime();
|
|
|
|
// Prüfe ob User existiert
|
|
db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => {
|
|
if (err || !user) {
|
|
return res.status(404).json({ success: false, error: 'Benutzer nicht gefunden' });
|
|
}
|
|
|
|
// Prüfe ob bereits ein Eintrag für heute existiert
|
|
db.get('SELECT * FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1',
|
|
[userId, currentDate], (err, entry) => {
|
|
if (err) {
|
|
return res.status(500).json({ success: false, error: 'Fehler beim Abrufen des Eintrags' });
|
|
}
|
|
|
|
if (!entry || !entry.start_time) {
|
|
// Kein Eintrag oder keine Start-Zeit → Fehler
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Bitte zuerst einchecken (Kommen).'
|
|
});
|
|
}
|
|
|
|
// Berechne total_hours basierend auf start_time, end_time und break_minutes
|
|
const breakMinutes = entry.break_minutes || 0;
|
|
const totalHours = updateTotalHours(entry.start_time, currentTime, breakMinutes);
|
|
|
|
// Setze end_time (überschreibt vorherige End-Zeit falls vorhanden)
|
|
db.run('UPDATE timesheet_entries SET end_time = ?, total_hours = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
|
[currentTime, totalHours, entry.id], (err) => {
|
|
if (err) {
|
|
return res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' });
|
|
}
|
|
res.json({
|
|
success: true,
|
|
message: `End-Zeit erfasst: ${currentTime}. Gesamtstunden: ${totalHours.toFixed(2)} h`,
|
|
end_time: currentTime,
|
|
total_hours: totalHours,
|
|
date: currentDate
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// API: Stundenerfassung für Woche laden
|
|
app.get('/api/timesheet/week/:weekStart', requireAuth, (req, res) => {
|
|
const userId = req.session.userId;
|
|
const weekStart = req.params.weekStart;
|
|
|
|
// Berechne Wochenende
|
|
const startDate = new Date(weekStart);
|
|
const endDate = new Date(startDate);
|
|
endDate.setDate(endDate.getDate() + 6);
|
|
const weekEnd = endDate.toISOString().split('T')[0];
|
|
|
|
// Prüfe ob die Woche bereits eingereicht wurde (aber ermögliche Bearbeitung)
|
|
db.get(`SELECT id, version FROM weekly_timesheets
|
|
WHERE user_id = ? AND week_start = ? AND week_end = ?
|
|
ORDER BY version DESC LIMIT 1`,
|
|
[userId, weekStart, weekEnd],
|
|
(err, weeklySheet) => {
|
|
const hasSubmittedVersion = !!weeklySheet;
|
|
const latestVersion = weeklySheet ? weeklySheet.version : 0;
|
|
|
|
// Lade alle Einträge für die Woche
|
|
db.all(`SELECT * FROM timesheet_entries
|
|
WHERE user_id = ? AND date >= ? AND date <= ?
|
|
ORDER BY date`,
|
|
[userId, weekStart, weekEnd],
|
|
(err, entries) => {
|
|
// Füge Status-Info hinzu (Bearbeitung ist immer möglich)
|
|
const entriesWithStatus = (entries || []).map(entry => ({
|
|
...entry,
|
|
week_submitted: false, // Immer false, damit Bearbeitung möglich ist
|
|
latest_version: latestVersion,
|
|
has_existing_version: latestVersion > 0
|
|
}));
|
|
res.json(entriesWithStatus);
|
|
});
|
|
});
|
|
});
|
|
|
|
// API: Woche abschicken
|
|
app.post('/api/timesheet/submit', requireAuth, (req, res) => {
|
|
const { week_start, week_end, version_reason } = req.body;
|
|
const userId = req.session.userId;
|
|
|
|
// Validierung: Prüfen ob alle 7 Tage der Woche ausgefüllt sind
|
|
db.all(`SELECT id, date, start_time, end_time, vacation_type, updated_at FROM timesheet_entries
|
|
WHERE user_id = ? AND date >= ? AND date <= ?
|
|
ORDER BY date, updated_at DESC, id DESC`,
|
|
[userId, week_start, week_end],
|
|
(err, entries) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Prüfen der Daten' });
|
|
}
|
|
|
|
// Erstelle Set mit vorhandenen Daten
|
|
// WICHTIG: Wenn mehrere Einträge für denselben Tag existieren, nimm den neuesten
|
|
const entriesByDate = {};
|
|
entries.forEach(entry => {
|
|
const existing = entriesByDate[entry.date];
|
|
// Wenn noch kein Eintrag existiert oder dieser neuer ist, verwende ihn
|
|
if (!existing) {
|
|
entriesByDate[entry.date] = entry;
|
|
} else {
|
|
// Vergleiche updated_at (falls vorhanden) oder id (höhere ID = neuer)
|
|
const existingTime = existing.updated_at ? new Date(existing.updated_at).getTime() : 0;
|
|
const currentTime = entry.updated_at ? new Date(entry.updated_at).getTime() : 0;
|
|
if (currentTime > existingTime || (currentTime === existingTime && entry.id > existing.id)) {
|
|
entriesByDate[entry.date] = entry;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Prüfe nur Werktage (Montag-Freitag, erste 5 Tage)
|
|
// Samstag und Sonntag sind optional
|
|
// Bei ganztägigem Urlaub (vacation_type = 'full') ist der Tag als ausgefüllt zu betrachten
|
|
// week_start ist bereits im Format YYYY-MM-DD
|
|
const startDateParts = week_start.split('-');
|
|
const startYear = parseInt(startDateParts[0]);
|
|
const startMonth = parseInt(startDateParts[1]) - 1; // Monat ist 0-basiert
|
|
const startDay = parseInt(startDateParts[2]);
|
|
|
|
let missingDays = [];
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
// Datum direkt berechnen ohne Zeitzonenprobleme
|
|
const date = new Date(startYear, startMonth, startDay + i);
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const dateStr = `${year}-${month}-${day}`;
|
|
const entry = entriesByDate[dateStr];
|
|
|
|
// Wenn ganztägiger Urlaub, dann ist der Tag als ausgefüllt zu betrachten
|
|
if (entry && entry.vacation_type === 'full') {
|
|
continue; // Tag ist ausgefüllt
|
|
}
|
|
|
|
// Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein
|
|
// start_time und end_time könnten null, undefined oder leer strings sein
|
|
const hasStartTime = entry && entry.start_time && entry.start_time.toString().trim() !== '';
|
|
const hasEndTime = entry && entry.end_time && entry.end_time.toString().trim() !== '';
|
|
|
|
if (!entry || !hasStartTime || !hasEndTime) {
|
|
missingDays.push(dateStr);
|
|
}
|
|
}
|
|
|
|
if (missingDays.length > 0) {
|
|
return res.status(400).json({
|
|
error: `Nicht alle Werktage (Montag bis Freitag) sind ausgefüllt. Fehlende Tage: ${missingDays.join(', ')}. Bitte füllen Sie alle Werktage mit Start- und Endzeit aus. Wochenende ist optional.`
|
|
});
|
|
}
|
|
|
|
// Alle Tage ausgefüllt - Woche abschicken (immer neue Version erstellen)
|
|
// Prüfe welche Version die letzte ist
|
|
db.get(`SELECT MAX(version) as max_version FROM weekly_timesheets
|
|
WHERE user_id = ? AND week_start = ? AND week_end = ?`,
|
|
[userId, week_start, week_end],
|
|
(err, result) => {
|
|
if (err) return res.status(500).json({ error: 'Fehler beim Prüfen der Version' });
|
|
|
|
const maxVersion = result && result.max_version ? result.max_version : 0;
|
|
const newVersion = maxVersion + 1;
|
|
|
|
// Wenn bereits eine Version existiert, ist version_reason erforderlich
|
|
if (maxVersion > 0 && (!version_reason || version_reason.trim() === '')) {
|
|
return res.status(400).json({
|
|
error: 'Bitte geben Sie einen Grund für die neue Version an.'
|
|
});
|
|
}
|
|
|
|
// Neue Version erstellen (nicht überschreiben)
|
|
db.run(`INSERT INTO weekly_timesheets (user_id, week_start, week_end, version, status, version_reason)
|
|
VALUES (?, ?, ?, ?, 'eingereicht', ?)`,
|
|
[userId, week_start, week_end, newVersion, version_reason ? version_reason.trim() : null],
|
|
(err) => {
|
|
if (err) return res.status(500).json({ error: 'Fehler beim Abschicken' });
|
|
|
|
// Status der Einträge aktualisieren (optional - für Nachverfolgung)
|
|
db.run(`UPDATE timesheet_entries
|
|
SET status = 'eingereicht'
|
|
WHERE user_id = ? AND date >= ? AND date <= ?`,
|
|
[userId, week_start, week_end],
|
|
(err) => {
|
|
if (err) return res.status(500).json({ error: 'Fehler beim Aktualisieren des Status' });
|
|
res.json({ success: true, version: newVersion });
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// API: Überstunden- und Urlaubsstatistiken für einen User abrufen
|
|
app.get('/api/verwaltung/user/:id/stats', requireVerwaltung, (req, res) => {
|
|
const userId = req.params.id;
|
|
const { week_start, week_end } = req.query;
|
|
|
|
// User-Daten abrufen
|
|
db.get('SELECT wochenstunden, urlaubstage FROM users WHERE id = ?', [userId], (err, user) => {
|
|
if (err || !user) {
|
|
return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' });
|
|
}
|
|
|
|
const wochenstunden = user.wochenstunden || 0;
|
|
const urlaubstage = user.urlaubstage || 0;
|
|
|
|
// Einträge für die Woche abrufen
|
|
db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type
|
|
FROM timesheet_entries
|
|
WHERE user_id = ? AND date >= ? AND date <= ?
|
|
ORDER BY date`,
|
|
[userId, week_start, week_end],
|
|
(err, entries) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' });
|
|
}
|
|
|
|
// Berechnungen
|
|
let totalHours = 0;
|
|
let overtimeTaken = 0;
|
|
let vacationDays = 0;
|
|
let vacationHours = 0;
|
|
|
|
entries.forEach(entry => {
|
|
if (entry.total_hours) {
|
|
totalHours += entry.total_hours;
|
|
}
|
|
if (entry.overtime_taken_hours) {
|
|
overtimeTaken += entry.overtime_taken_hours;
|
|
}
|
|
if (entry.vacation_type === 'full') {
|
|
vacationDays += 1;
|
|
vacationHours += 8; // Ganzer Tag = 8 Stunden
|
|
} else if (entry.vacation_type === 'half') {
|
|
vacationDays += 0.5;
|
|
vacationHours += 4; // Halber Tag = 4 Stunden
|
|
}
|
|
});
|
|
|
|
// Anzahl Werktage berechnen (Montag-Freitag)
|
|
const startDate = new Date(week_start);
|
|
const endDate = new Date(week_end);
|
|
let workdays = 0;
|
|
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
|
|
const day = d.getDay();
|
|
if (day >= 1 && day <= 5) { // Montag bis Freitag
|
|
workdays++;
|
|
}
|
|
}
|
|
|
|
// Sollstunden berechnen
|
|
const sollStunden = (wochenstunden / 5) * workdays;
|
|
|
|
// Überstunden berechnen: Urlaub zählt als normale Arbeitszeit
|
|
// Überstunden = (Tatsächliche Stunden + Urlaubsstunden) - Sollstunden
|
|
const totalHoursWithVacation = totalHours + vacationHours;
|
|
const overtimeHours = totalHoursWithVacation - sollStunden;
|
|
const remainingOvertime = overtimeHours - overtimeTaken;
|
|
|
|
// Verbleibende Urlaubstage
|
|
const remainingVacation = urlaubstage - vacationDays;
|
|
|
|
res.json({
|
|
wochenstunden,
|
|
urlaubstage,
|
|
totalHours,
|
|
sollStunden,
|
|
overtimeHours,
|
|
overtimeTaken,
|
|
remainingOvertime,
|
|
vacationDays,
|
|
remainingVacation,
|
|
workdays
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// API: Admin-Kommentar speichern
|
|
app.put('/api/verwaltung/timesheet/:id/comment', requireVerwaltung, (req, res) => {
|
|
const timesheetId = req.params.id;
|
|
const { comment } = req.body;
|
|
|
|
db.run('UPDATE weekly_timesheets SET admin_comment = ? WHERE id = ?',
|
|
[comment ? comment.trim() : null, timesheetId],
|
|
(err) => {
|
|
if (err) {
|
|
console.error('Fehler beim Speichern des Kommentars:', err);
|
|
return res.status(500).json({ error: 'Fehler beim Speichern des Kommentars' });
|
|
}
|
|
res.json({ success: true });
|
|
});
|
|
});
|
|
|
|
// API: PDF Download-Info abrufen
|
|
app.get('/api/timesheet/download-info/:id', requireVerwaltung, (req, res) => {
|
|
const timesheetId = req.params.id;
|
|
|
|
db.get(`SELECT wt.pdf_downloaded_at,
|
|
dl.firstname as downloaded_by_firstname,
|
|
dl.lastname as downloaded_by_lastname
|
|
FROM weekly_timesheets wt
|
|
LEFT JOIN users dl ON wt.pdf_downloaded_by = dl.id
|
|
WHERE wt.id = ?`, [timesheetId], (err, result) => {
|
|
|
|
if (err) {
|
|
console.error('Fehler beim Abrufen der Download-Info:', err);
|
|
return res.status(500).json({ error: 'Fehler beim Abrufen der Informationen' });
|
|
}
|
|
|
|
if (!result) {
|
|
return res.status(404).json({ error: 'Stundenzettel nicht gefunden' });
|
|
}
|
|
|
|
res.json({
|
|
downloaded: !!result.pdf_downloaded_at,
|
|
downloaded_at: result.pdf_downloaded_at,
|
|
downloaded_by_firstname: result.downloaded_by_firstname,
|
|
downloaded_by_lastname: result.downloaded_by_lastname
|
|
});
|
|
});
|
|
});
|
|
|
|
// API: PDF generieren
|
|
app.get('/api/timesheet/pdf/:id', requireVerwaltung, (req, res) => {
|
|
const timesheetId = req.params.id;
|
|
|
|
db.get(`SELECT wt.*, u.firstname, u.lastname, u.username, u.wochenstunden
|
|
FROM weekly_timesheets wt
|
|
JOIN users u ON wt.user_id = u.id
|
|
WHERE wt.id = ?`, [timesheetId], (err, timesheet) => {
|
|
|
|
if (err || !timesheet) {
|
|
return res.status(404).send('Stundenzettel nicht gefunden');
|
|
}
|
|
|
|
db.all(`SELECT * FROM timesheet_entries
|
|
WHERE user_id = ? AND date >= ? AND date <= ?
|
|
ORDER BY date`,
|
|
[timesheet.user_id, timesheet.week_start, timesheet.week_end],
|
|
(err, entries) => {
|
|
|
|
const PDFDocument = require('pdfkit');
|
|
const doc = new PDFDocument({ margin: 50 });
|
|
|
|
// Prüfe ob inline angezeigt werden soll (für Vorschau)
|
|
const inline = req.query.inline === 'true';
|
|
|
|
// Kalenderwoche berechnen (Funktion wird später definiert)
|
|
function getCalendarWeek(dateStr) {
|
|
const date = new Date(dateStr);
|
|
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
const dayNum = d.getUTCDay() || 7;
|
|
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
|
return weekNo;
|
|
}
|
|
|
|
// Dateinamen generieren: Stundenzettel_KWxxx_NameMitarbeiter_heutigesDatum.pdf
|
|
const calendarWeek = getCalendarWeek(timesheet.week_start);
|
|
const today = new Date();
|
|
const todayStr = today.getFullYear() + '-' +
|
|
String(today.getMonth() + 1).padStart(2, '0') + '-' +
|
|
String(today.getDate()).padStart(2, '0');
|
|
const employeeName = `${timesheet.firstname}${timesheet.lastname}`.replace(/\s+/g, '');
|
|
const filename = `Stundenzettel_KW${String(calendarWeek).padStart(2, '0')}_${employeeName}_${todayStr}.pdf`;
|
|
|
|
res.setHeader('Content-Type', 'application/pdf');
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
|
|
if (inline) {
|
|
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
|
|
// Zusätzliche Header für iframe-Unterstützung
|
|
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
} else {
|
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
|
|
// Marker setzen, dass PDF heruntergeladen wurde (nur bei Download, nicht bei Vorschau)
|
|
const downloadedBy = req.session.userId; // User der die PDF herunterlädt
|
|
console.log('PDF Download - User ID:', downloadedBy, 'Timesheet ID:', timesheetId);
|
|
|
|
if (downloadedBy) {
|
|
db.run(`UPDATE weekly_timesheets
|
|
SET pdf_downloaded_at = CURRENT_TIMESTAMP,
|
|
pdf_downloaded_by = ?
|
|
WHERE id = ?`,
|
|
[downloadedBy, timesheetId],
|
|
(err) => {
|
|
if (err) {
|
|
console.error('Fehler beim Setzen des Download-Markers:', err);
|
|
} else {
|
|
console.log('Download-Marker erfolgreich gesetzt für User:', downloadedBy);
|
|
}
|
|
// Fehler wird ignoriert, damit PDF trotzdem generiert wird
|
|
});
|
|
} else {
|
|
console.warn('PDF Download - Keine User ID in Session gefunden!');
|
|
}
|
|
}
|
|
|
|
doc.pipe(res);
|
|
|
|
// Header (Kalenderwoche wurde bereits oben berechnet)
|
|
doc.fontSize(20).text(`Stundenzettel für KW ${calendarWeek}`, { align: 'center' });
|
|
doc.moveDown();
|
|
|
|
// Mitarbeiter-Info
|
|
doc.fontSize(12);
|
|
doc.text(`Mitarbeiter: ${timesheet.firstname} ${timesheet.lastname}`);
|
|
doc.text(`Zeitraum: ${formatDate(timesheet.week_start)} - ${formatDate(timesheet.week_end)}`);
|
|
doc.text(`Eingereicht am: ${formatDateTime(timesheet.submitted_at)}`);
|
|
doc.moveDown();
|
|
|
|
// Tabelle - Basis-Informationen
|
|
const tableTop = doc.y;
|
|
const colWidths = [80, 80, 80, 60, 80];
|
|
const headers = ['Datum', 'Start', 'Ende', 'Pause', 'Stunden'];
|
|
|
|
// Tabellen-Header
|
|
doc.fontSize(10).font('Helvetica-Bold');
|
|
let x = 50;
|
|
headers.forEach((header, i) => {
|
|
doc.text(header, x, tableTop, { width: colWidths[i], align: 'left' });
|
|
x += colWidths[i];
|
|
});
|
|
|
|
doc.moveDown();
|
|
let y = doc.y;
|
|
doc.moveTo(50, y).lineTo(430, y).stroke();
|
|
doc.moveDown(0.5);
|
|
|
|
// Tabellen-Daten
|
|
doc.font('Helvetica');
|
|
let totalHours = 0;
|
|
let vacationHours = 0; // Urlaubsstunden für Überstunden-Berechnung
|
|
|
|
entries.forEach((entry) => {
|
|
y = doc.y;
|
|
x = 50;
|
|
|
|
// Basis-Zeile
|
|
const rowData = [
|
|
formatDate(entry.date),
|
|
entry.start_time || '-',
|
|
entry.end_time || '-',
|
|
entry.break_minutes ? `${entry.break_minutes} min` : '-',
|
|
entry.total_hours ? entry.total_hours.toFixed(2) + ' h' : '-'
|
|
];
|
|
|
|
rowData.forEach((data, i) => {
|
|
doc.text(data, x, y, { width: colWidths[i], align: 'left' });
|
|
x += colWidths[i];
|
|
});
|
|
|
|
// Tätigkeiten sammeln
|
|
const activities = [];
|
|
for (let i = 1; i <= 5; i++) {
|
|
const desc = entry[`activity${i}_desc`];
|
|
const hours = entry[`activity${i}_hours`];
|
|
const projectNumber = entry[`activity${i}_project_number`];
|
|
if (desc && desc.trim() && hours > 0) {
|
|
activities.push({
|
|
desc: desc.trim(),
|
|
hours: parseFloat(hours),
|
|
projectNumber: projectNumber ? projectNumber.trim() : null
|
|
});
|
|
}
|
|
}
|
|
|
|
// Tätigkeiten anzeigen
|
|
if (activities.length > 0) {
|
|
doc.moveDown(0.3);
|
|
doc.fontSize(9).font('Helvetica-Oblique');
|
|
doc.text('Tätigkeiten:', 60, doc.y, { width: 380 });
|
|
doc.moveDown(0.2);
|
|
|
|
activities.forEach((activity, idx) => {
|
|
let activityText = `${idx + 1}. ${activity.desc}`;
|
|
if (activity.projectNumber) {
|
|
activityText += ` (Projekt: ${activity.projectNumber})`;
|
|
}
|
|
activityText += ` - ${activity.hours.toFixed(2)} h`;
|
|
doc.fontSize(9).font('Helvetica');
|
|
doc.text(activityText, 70, doc.y, { width: 360 });
|
|
doc.moveDown(0.2);
|
|
});
|
|
doc.fontSize(10);
|
|
}
|
|
|
|
// Überstunden und Urlaub anzeigen
|
|
const overtimeInfo = [];
|
|
if (entry.overtime_taken_hours && parseFloat(entry.overtime_taken_hours) > 0) {
|
|
overtimeInfo.push(`Überstunden genommen: ${parseFloat(entry.overtime_taken_hours).toFixed(2)} h`);
|
|
}
|
|
if (entry.vacation_type) {
|
|
const vacationText = entry.vacation_type === 'full' ? 'Ganzer Tag' : 'Halber Tag';
|
|
overtimeInfo.push(`Urlaub: ${vacationText}`);
|
|
}
|
|
|
|
if (overtimeInfo.length > 0) {
|
|
doc.moveDown(0.2);
|
|
doc.fontSize(9).font('Helvetica-Oblique');
|
|
overtimeInfo.forEach((info, idx) => {
|
|
doc.text(info, 70, doc.y, { width: 360 });
|
|
doc.moveDown(0.15);
|
|
});
|
|
doc.fontSize(10);
|
|
}
|
|
|
|
if (entry.total_hours) {
|
|
totalHours += entry.total_hours;
|
|
}
|
|
|
|
// Urlaubsstunden für Überstunden-Berechnung sammeln
|
|
if (entry.vacation_type === 'full') {
|
|
vacationHours += 8; // Ganzer Tag = 8 Stunden
|
|
} else if (entry.vacation_type === 'half') {
|
|
vacationHours += 4; // Halber Tag = 4 Stunden
|
|
}
|
|
|
|
doc.moveDown(0.5);
|
|
|
|
// Trennlinie zwischen Einträgen
|
|
y = doc.y;
|
|
doc.moveTo(50, y).lineTo(430, y).stroke();
|
|
doc.moveDown(0.3);
|
|
});
|
|
|
|
// Summe
|
|
y = doc.y;
|
|
doc.moveTo(50, y).lineTo(550, y).stroke();
|
|
doc.moveDown(0.5);
|
|
doc.font('Helvetica-Bold');
|
|
doc.text(`Gesamtstunden: ${totalHours.toFixed(2)} h`, 50, doc.y);
|
|
|
|
// Überstunden berechnen und anzeigen
|
|
const wochenstunden = timesheet.wochenstunden || 0;
|
|
// Überstunden = Gesamtstunden - Wochenstunden
|
|
// Urlaub zählt als normale Arbeitszeit, daher sind Urlaubsstunden bereits in totalHours enthalten
|
|
const overtimeHours = totalHours - wochenstunden;
|
|
|
|
doc.moveDown(0.3);
|
|
doc.font('Helvetica-Bold');
|
|
if (overtimeHours > 0) {
|
|
doc.text(`Überstunden: +${overtimeHours.toFixed(2)} h`, 50, doc.y);
|
|
} else if (overtimeHours < 0) {
|
|
doc.text(`Überstunden: ${overtimeHours.toFixed(2)} h`, 50, doc.y);
|
|
} else {
|
|
doc.text(`Überstunden: 0.00 h`, 50, doc.y);
|
|
}
|
|
|
|
doc.end();
|
|
});
|
|
});
|
|
});
|
|
|
|
// Hilfsfunktionen
|
|
function formatDate(dateStr) {
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleDateString('de-DE');
|
|
}
|
|
|
|
function formatDateTime(dateStr) {
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleString('de-DE');
|
|
}
|
|
|
|
// Start-Route
|
|
app.get('/', (req, res) => {
|
|
if (req.session.userId) {
|
|
// Redirect: Immer zu Dashboard wenn Mitarbeiter-Rolle vorhanden, sonst basierend auf currentRole
|
|
const roles = req.session.roles || [];
|
|
if (roles.includes('mitarbeiter')) {
|
|
res.redirect('/dashboard');
|
|
} else {
|
|
const currentRole = req.session.currentRole || getDefaultRole(roles);
|
|
if (currentRole === 'admin') {
|
|
res.redirect('/admin');
|
|
} else if (currentRole === 'verwaltung') {
|
|
res.redirect('/verwaltung');
|
|
} else {
|
|
res.redirect('/dashboard');
|
|
}
|
|
}
|
|
} else {
|
|
res.redirect('/login');
|
|
}
|
|
});
|
|
|
|
// Automatische LDAP-Synchronisation einrichten
|
|
function setupLDAPScheduler() {
|
|
// Prüfe alle 5 Minuten, ob eine Synchronisation notwendig ist
|
|
setInterval(() => {
|
|
db.get('SELECT * FROM ldap_config WHERE id = 1 AND enabled = 1 AND sync_interval > 0', (err, config) => {
|
|
if (err || !config) {
|
|
return; // Keine aktive Konfiguration
|
|
}
|
|
|
|
const now = new Date();
|
|
const lastSync = config.last_sync ? new Date(config.last_sync) : null;
|
|
const syncIntervalMs = config.sync_interval * 60 * 1000; // Minuten in Millisekunden
|
|
|
|
// Prüfe ob Synchronisation fällig ist
|
|
if (!lastSync || (now - lastSync) >= syncIntervalMs) {
|
|
console.log('Starte automatische LDAP-Synchronisation...');
|
|
LDAPService.performSync('scheduled', (err, result) => {
|
|
if (err) {
|
|
console.error('Fehler bei automatischer LDAP-Synchronisation:', err.message);
|
|
} else {
|
|
console.log(`Automatische LDAP-Synchronisation abgeschlossen: ${result.synced} Benutzer synchronisiert`);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}, 5 * 60 * 1000); // Alle 5 Minuten prüfen
|
|
}
|
|
|
|
// Server starten
|
|
app.listen(PORT, () => {
|
|
console.log(`Server läuft auf http://localhost:${PORT}`);
|
|
console.log('Standard-Zugangsdaten:');
|
|
console.log('Admin: admin / admin123');
|
|
console.log('Verwaltung: verwaltung / verwaltung123');
|
|
|
|
// LDAP-Scheduler starten
|
|
setupLDAPScheduler();
|
|
});
|