Implementierung Datenbank pflege wegen doppelten einträgen
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user