From 5a8dcf2cb5d49b1dcc4477b63880c5f9ca4cafe6 Mon Sep 17 00:00:00 2001 From: Carsten Graf Date: Mon, 16 Mar 2026 15:59:33 +0100 Subject: [PATCH] =?UTF-8?q?Implementierung=20Datenbank=20pflege=20wegen=20?= =?UTF-8?q?doppelten=20eintr=C3=A4gen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/admin.js | 125 +++++++++++++++++++++++++++++++++++++++++ routes/admin-routes.js | 90 +++++++++++++++++++++++++++++ views/admin.ejs | 34 +++++++++++ 3 files changed, 249 insertions(+) diff --git a/public/js/admin.js b/public/js/admin.js index 898b87a..3947b43 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -66,6 +66,14 @@ document.addEventListener('DOMContentLoaded', function() { // MSSQL-Konfiguration laden loadMssqlConfig(); + // Timesheet-Duplikate Button + const loadTimesheetDuplicatesBtn = document.getElementById('loadTimesheetDuplicatesBtn'); + if (loadTimesheetDuplicatesBtn) { + loadTimesheetDuplicatesBtn.addEventListener('click', function() { + loadTimesheetDuplicates(); + }); + } + // Optionen-Formular const optionsForm = document.getElementById('optionsForm'); if (optionsForm) { @@ -298,6 +306,123 @@ document.addEventListener('DOMContentLoaded', function() { } }); +async function loadTimesheetDuplicates() { + const container = document.getElementById('timesheetDuplicatesContainer'); + if (!container) return; + + container.innerHTML = '

Lade Timesheet-Duplikate...

'; + + try { + const response = await fetch('/admin/api/timesheet-duplicates'); + const result = await response.json(); + + if (!response.ok) { + const msg = result && result.error ? result.error : 'Fehler beim Laden der Timesheet-Duplikate.'; + container.innerHTML = `

${msg}

`; + return; + } + + const groups = Array.isArray(result.groups) ? result.groups : []; + + if (groups.length === 0) { + container.innerHTML = '

Es wurden keine doppelten Timesheet-Einträge gefunden. Alles sauber.

'; + return; + } + + let html = ''; + groups.forEach((group, index) => { + const headerLabel = `${group.user_name || group.username || ('User #' + group.user_id)} – ${group.date} (Anzahl Einträge: ${group.entry_count})`; + html += ` +
+
+
+ Gruppe ${index + 1}: ${headerLabel} +
+
+
+ + + + + + + + + + + + + + + + `; + + (group.entries || []).forEach(entry => { + const created = entry.created_at ? new Date(entry.created_at).toLocaleString('de-DE') : '-'; + const updated = entry.updated_at ? new Date(entry.updated_at).toLocaleString('de-DE') : '-'; + const totalHours = entry.total_hours != null ? entry.total_hours : '-'; + const status = entry.status || '-'; + const breakMinutes = entry.break_minutes != null ? entry.break_minutes : 0; + + html += ` + + + + + + + + + + + + `; + }); + + html += ` + +
IDStartEndePause (Min)Stunden (total_hours)StatusErstelltAktualisiertAktionen
${entry.id}${entry.start_time || '-'}${entry.end_time || '-'}${breakMinutes}${totalHours}${status}${created}${updated} + +
+
+
+ `; + }); + + container.innerHTML = html; + } catch (error) { + console.error('Fehler beim Laden der Timesheet-Duplikate:', error); + container.innerHTML = '

Fehler beim Laden der Timesheet-Duplikate.

'; + } +} + +async function deleteTimesheetEntry(entryId) { + + try { + const response = await fetch(`/admin/api/timesheet-entry/${entryId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }); + + const result = await response.json(); + + if (response.ok && result && result.success) { + // Liste neu laden, um den aktuellen Stand anzuzeigen + loadTimesheetDuplicates(); + } else { + const msg = (result && result.error) ? result.error : 'Timesheet-Eintrag konnte nicht gelöscht werden.'; + alert('Fehler: ' + msg); + } + } catch (error) { + console.error('Fehler beim Löschen des Timesheet-Eintrags:', error); + alert('Fehler beim Löschen des Timesheet-Eintrags.'); + } +} + // Optionen laden und Formular ausfüllen async function loadOptions() { try { diff --git a/routes/admin-routes.js b/routes/admin-routes.js index d15e7fc..2c98689 100644 --- a/routes/admin-routes.js +++ b/routes/admin-routes.js @@ -322,6 +322,96 @@ function registerAdminRoutes(app) { }); } }); + + // Timesheet-Duplikate (mehr als ein Eintrag pro Benutzer und Datum) als Übersicht + app.get('/admin/api/timesheet-duplicates', requireAdmin, (req, res) => { + const sql = ` + SELECT + te.*, + dup.entry_count, + u.firstname, + u.lastname, + u.username + FROM timesheet_entries te + INNER JOIN ( + SELECT user_id, date, COUNT(*) AS entry_count + FROM timesheet_entries + GROUP BY user_id, date + HAVING COUNT(*) > 1 + ) dup + ON dup.user_id = te.user_id + AND dup.date = te.date + INNER JOIN users u + ON u.id = te.user_id + ORDER BY te.user_id, te.date, te.id + `; + + db.all(sql, [], (err, rows) => { + if (err) { + console.error('Fehler beim Laden der Timesheet-Duplikate:', err); + return res.status(500).json({ error: 'Fehler beim Laden der Timesheet-Duplikate' }); + } + + const groupsMap = new Map(); + + (rows || []).forEach(row => { + const key = `${row.user_id}|${row.date}`; + if (!groupsMap.has(key)) { + const userNameParts = []; + if (row.firstname) userNameParts.push(row.firstname); + if (row.lastname) userNameParts.push(row.lastname); + const user_name = userNameParts.join(' ') || row.username || `User #${row.user_id}`; + + groupsMap.set(key, { + user_id: row.user_id, + user_name, + username: row.username, + date: row.date, + entry_count: row.entry_count, + entries: [] + }); + } + + const group = groupsMap.get(key); + group.entries.push({ + id: row.id, + start_time: row.start_time, + end_time: row.end_time, + break_minutes: row.break_minutes, + total_hours: row.total_hours, + status: row.status, + notes: row.notes, + created_at: row.created_at, + updated_at: row.updated_at + }); + }); + + const groups = Array.from(groupsMap.values()); + res.json({ groups }); + }); + }); + + // Einzelnen Timesheet-Eintrag löschen (zur manuellen Bereinigung von Duplikaten) + app.delete('/admin/api/timesheet-entry/:id', requireAdmin, (req, res) => { + const entryId = parseInt(req.params.id, 10); + + if (!Number.isInteger(entryId) || entryId <= 0) { + return res.status(400).json({ error: 'Ungültige Eintrags-ID' }); + } + + db.run('DELETE FROM timesheet_entries WHERE id = ?', [entryId], function(err) { + if (err) { + console.error('Fehler beim Löschen des Timesheet-Eintrags:', err); + return res.status(500).json({ error: 'Fehler beim Löschen des Timesheet-Eintrags' }); + } + + if (this.changes === 0) { + return res.status(404).json({ error: 'Timesheet-Eintrag nicht gefunden' }); + } + + res.json({ success: true }); + }); + }); } module.exports = registerAdminRoutes; diff --git a/views/admin.ejs b/views/admin.ejs index d26c772..15b577e 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -441,6 +441,26 @@ + +
+
+

Datenbankpflege – doppelte Timesheet-Einträge

+ +
+ + +
@@ -515,6 +535,20 @@ icon.style.transform = 'rotate(0deg)'; } } + + function toggleTimesheetMaintenanceSection() { + const content = document.getElementById('timesheetMaintenanceContent'); + const icon = document.getElementById('timesheetMaintenanceToggleIcon'); + if (!content) return; + + if (content.style.display === 'none' || content.style.display === '') { + content.style.display = 'block'; + if (icon) icon.style.transform = 'rotate(180deg)'; + } else { + content.style.display = 'none'; + if (icon) icon.style.transform = 'rotate(0deg)'; + } + } // Rollenwechsel-Handler document.addEventListener('DOMContentLoaded', function() {