Implementierung Datenbank pflege wegen doppelten einträgen

This commit is contained in:
2026-03-16 15:59:33 +01:00
parent 164cd78f3d
commit 5a8dcf2cb5
3 changed files with 249 additions and 0 deletions

View File

@@ -66,6 +66,14 @@ document.addEventListener('DOMContentLoaded', function() {
// MSSQL-Konfiguration laden // MSSQL-Konfiguration laden
loadMssqlConfig(); loadMssqlConfig();
// Timesheet-Duplikate Button
const loadTimesheetDuplicatesBtn = document.getElementById('loadTimesheetDuplicatesBtn');
if (loadTimesheetDuplicatesBtn) {
loadTimesheetDuplicatesBtn.addEventListener('click', function() {
loadTimesheetDuplicates();
});
}
// Optionen-Formular // Optionen-Formular
const optionsForm = document.getElementById('optionsForm'); const optionsForm = document.getElementById('optionsForm');
if (optionsForm) { if (optionsForm) {
@@ -298,6 +306,123 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
async function loadTimesheetDuplicates() {
const container = document.getElementById('timesheetDuplicatesContainer');
if (!container) return;
container.innerHTML = '<p style="color: #666;">Lade Timesheet-Duplikate...</p>';
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 = `<p style="color: red;">${msg}</p>`;
return;
}
const groups = Array.isArray(result.groups) ? result.groups : [];
if (groups.length === 0) {
container.innerHTML = '<p style="color: green;">Es wurden keine doppelten Timesheet-Einträge gefunden. Alles sauber.</p>';
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 += `
<div class="timesheet-duplicate-group" style="border: 1px solid #ddd; border-radius: 4px; margin-bottom: 15px;">
<div style="padding: 10px 15px; background-color: #f5f5f5; display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>Gruppe ${index + 1}</strong>: ${headerLabel}
</div>
</div>
<div style="padding: 10px 15px; overflow-x: auto;">
<table style="width: 100%; min-width: 800px; border-collapse: collapse;">
<thead>
<tr>
<th style="text-align:left; padding:4px;">ID</th>
<th style="text-align:left; padding:4px;">Start</th>
<th style="text-align:left; padding:4px;">Ende</th>
<th style="text-align:left; padding:4px;">Pause (Min)</th>
<th style="text-align:left; padding:4px;">Stunden (total_hours)</th>
<th style="text-align:left; padding:4px;">Status</th>
<th style="text-align:left; padding:4px;">Erstellt</th>
<th style="text-align:left; padding:4px;">Aktualisiert</th>
<th style="text-align:left; padding:4px;">Aktionen</th>
</tr>
</thead>
<tbody>
`;
(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 += `
<tr>
<td style="padding:4px;">${entry.id}</td>
<td style="padding:4px;">${entry.start_time || '-'}</td>
<td style="padding:4px;">${entry.end_time || '-'}</td>
<td style="padding:4px;">${breakMinutes}</td>
<td style="padding:4px;">${totalHours}</td>
<td style="padding:4px;">${status}</td>
<td style="padding:4px;">${created}</td>
<td style="padding:4px;">${updated}</td>
<td style="padding:4px;">
<button type="button" class="btn btn-danger btn-sm" onclick="deleteTimesheetEntry(${entry.id})">
Eintrag löschen
</button>
</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
</div>
`;
});
container.innerHTML = html;
} catch (error) {
console.error('Fehler beim Laden der Timesheet-Duplikate:', error);
container.innerHTML = '<p style="color: red;">Fehler beim Laden der Timesheet-Duplikate.</p>';
}
}
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 // Optionen laden und Formular ausfüllen
async function loadOptions() { async function loadOptions() {
try { try {

View File

@@ -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; module.exports = registerAdminRoutes;

View File

@@ -441,6 +441,26 @@
</div> </div>
</div> </div>
</div> </div>
<div class="options-section" style="margin-top: 40px;">
<div class="collapsible-header" onclick="toggleTimesheetMaintenanceSection()" style="cursor: pointer; padding: 15px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
<h2 style="margin: 0;">Datenbankpflege doppelte Timesheet-Einträge</h2>
<span id="timesheetMaintenanceToggleIcon" style="font-size: 18px; transition: transform 0.3s;">▼</span>
</div>
<div id="timesheetMaintenanceContent" style="display: none; padding: 20px; border: 1px solid #ddd; border-top: none; border-radius: 0 0 4px 4px; background-color: #fff;">
<p style="margin-bottom: 15px; color: #666;">
Es werden alle Tage angezeigt, an denen ein Mitarbeiter mehr als einen Eintrag in der Tabelle <code>timesheet_entries</code> hat
(Schlüssel: Benutzer + Datum). Über diese Übersicht können Sie fehlerhafte Einträge gezielt löschen.
</p>
<button id="loadTimesheetDuplicatesBtn" class="btn btn-secondary" type="button" style="margin-bottom: 15px;">
Timesheet-Duplikate laden
</button>
<div id="timesheetDuplicatesContainer">
<p style="color: #888;">Noch keine Daten geladen. Klicken Sie auf „Timesheet-Duplikate laden“, um die Übersicht anzuzeigen.</p>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -516,6 +536,20 @@
} }
} }
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 // Rollenwechsel-Handler
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const roleSwitcher = document.getElementById('roleSwitcher'); const roleSwitcher = document.getElementById('roleSwitcher');