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 += ` +| ID | +Start | +Ende | +Pause (Min) | +Stunden (total_hours) | +Status | +Erstellt | +Aktualisiert | +Aktionen | +
|---|---|---|---|---|---|---|---|---|
| ${entry.id} | +${entry.start_time || '-'} | +${entry.end_time || '-'} | +${breakMinutes} | +${totalHours} | +${status} | +${created} | +${updated} | ++ + | +
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 @@ + + @@ -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() {