911 lines
32 KiB
JavaScript
911 lines
32 KiB
JavaScript
// Admin JavaScript
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Benutzer-Formular
|
||
const form = document.getElementById('addUserForm');
|
||
|
||
form.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
// Rollen aus Checkboxen sammeln
|
||
const roleCheckboxes = document.querySelectorAll('input[name="roles"]:checked');
|
||
const roles = Array.from(roleCheckboxes).map(cb => cb.value);
|
||
|
||
// Validierung: Mindestens eine Rolle muss ausgewählt sein
|
||
if (roles.length === 0) {
|
||
alert('Bitte wählen Sie mindestens eine Rolle aus.');
|
||
return;
|
||
}
|
||
|
||
const defaultBreakInput = document.getElementById('defaultBreakMinutes');
|
||
const defaultBreakVal = defaultBreakInput && defaultBreakInput.value !== '' ? parseInt(defaultBreakInput.value, 10) : 30;
|
||
const default_break_minutes = (!isNaN(defaultBreakVal) && defaultBreakVal >= 0) ? defaultBreakVal : 30;
|
||
|
||
const formData = {
|
||
username: document.getElementById('username').value,
|
||
password: document.getElementById('password').value,
|
||
firstname: document.getElementById('firstname').value,
|
||
lastname: document.getElementById('lastname').value,
|
||
roles: roles,
|
||
personalnummer: document.getElementById('personalnummer').value,
|
||
wochenstunden: document.getElementById('wochenstunden').value,
|
||
arbeitstage: document.getElementById('arbeitstage').value,
|
||
urlaubstage: document.getElementById('urlaubstage').value,
|
||
default_break_minutes: default_break_minutes
|
||
};
|
||
|
||
try {
|
||
const response = await fetch('/admin/users', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(formData)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert('Benutzer wurde erfolgreich angelegt!');
|
||
location.reload();
|
||
} else {
|
||
alert('Fehler: ' + (result.error || 'Benutzer konnte nicht angelegt werden'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler:', error);
|
||
alert('Fehler beim Anlegen des Benutzers');
|
||
}
|
||
});
|
||
|
||
// LDAP-Konfiguration laden
|
||
loadLDAPConfig();
|
||
|
||
// Optionen laden
|
||
loadOptions();
|
||
|
||
// 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) {
|
||
optionsForm.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
const formData = {
|
||
saturday_percentage: document.getElementById('saturdayPercentage').value,
|
||
sunday_percentage: document.getElementById('sundayPercentage').value,
|
||
checkin_root_url: document.getElementById('checkinRootUrl') ? document.getElementById('checkinRootUrl').value : null
|
||
};
|
||
|
||
try {
|
||
const response = await fetch('/admin/options', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(formData)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert('Optionen wurden erfolgreich gespeichert!');
|
||
} else {
|
||
alert('Fehler: ' + (result.error || 'Optionen konnten nicht gespeichert werden'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler:', error);
|
||
alert('Fehler beim Speichern der Optionen');
|
||
}
|
||
});
|
||
}
|
||
|
||
// LDAP-Konfigurationsformular
|
||
const ldapConfigForm = document.getElementById('ldapConfigForm');
|
||
if (ldapConfigForm) {
|
||
ldapConfigForm.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
const enabled = document.getElementById('ldapEnabled').checked;
|
||
const url = document.getElementById('ldapUrl').value;
|
||
const baseDn = document.getElementById('ldapBaseDn').value;
|
||
|
||
// Validierung: URL und Base DN sind erforderlich wenn aktiviert
|
||
if (enabled && (!url || !baseDn)) {
|
||
alert('Bitte füllen Sie URL und Base DN aus, wenn LDAP aktiviert ist.');
|
||
return;
|
||
}
|
||
|
||
const formData = {
|
||
enabled: enabled,
|
||
url: url,
|
||
bind_dn: document.getElementById('ldapBindDn').value,
|
||
bind_password: document.getElementById('ldapBindPassword').value,
|
||
base_dn: baseDn,
|
||
user_search_filter: document.getElementById('ldapSearchFilter').value,
|
||
username_attribute: document.getElementById('ldapUsernameAttr').value,
|
||
firstname_attribute: document.getElementById('ldapFirstnameAttr').value,
|
||
lastname_attribute: document.getElementById('ldapLastnameAttr').value,
|
||
sync_interval: parseInt(document.getElementById('ldapSyncInterval').value) || 0
|
||
};
|
||
|
||
try {
|
||
const response = await fetch('/admin/ldap/config', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(formData)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert('LDAP-Konfiguration wurde erfolgreich gespeichert!');
|
||
location.reload();
|
||
} else {
|
||
alert('Fehler: ' + (result.error || 'Konfiguration konnte nicht gespeichert werden'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler:', error);
|
||
alert('Fehler beim Speichern der Konfiguration');
|
||
}
|
||
});
|
||
}
|
||
|
||
// MSSQL-Konfigurationsformular
|
||
const mssqlConfigForm = document.getElementById('mssqlConfigForm');
|
||
if (mssqlConfigForm) {
|
||
mssqlConfigForm.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
const server = document.getElementById('mssqlServer').value;
|
||
const database = document.getElementById('mssqlDatabase').value;
|
||
const username = document.getElementById('mssqlUsername').value;
|
||
const password = document.getElementById('mssqlPassword').value;
|
||
|
||
if (!server || !database || !username) {
|
||
alert('Bitte Server, Datenbankname und Benutzername ausfüllen.');
|
||
return;
|
||
}
|
||
|
||
const formData = { server, database, username, password };
|
||
|
||
try {
|
||
const response = await fetch('/admin/mssql-config', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(formData)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert('MSSQL-Konfiguration wurde erfolgreich gespeichert!');
|
||
// Passwort-Feld leeren nach dem Speichern
|
||
document.getElementById('mssqlPassword').value = '';
|
||
} else {
|
||
alert('Fehler: ' + (result.error || 'MSSQL-Konfiguration konnte nicht gespeichert werden'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler:', error);
|
||
alert('Fehler beim Speichern der MSSQL-Konfiguration');
|
||
}
|
||
});
|
||
}
|
||
|
||
// MSSQL Test-Verbindung
|
||
const mssqlTestBtn = document.getElementById('mssqlTestConnectionBtn');
|
||
if (mssqlTestBtn) {
|
||
mssqlTestBtn.addEventListener('click', async function() {
|
||
const statusEl = document.getElementById('mssqlTestStatus');
|
||
mssqlTestBtn.disabled = true;
|
||
if (statusEl) {
|
||
statusEl.textContent = 'Verbindung wird getestet...';
|
||
statusEl.style.color = 'blue';
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/admin/mssql-test-connection', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok && result && result.success) {
|
||
if (statusEl) {
|
||
statusEl.textContent = 'Verbindung erfolgreich.';
|
||
statusEl.style.color = 'green';
|
||
}
|
||
} else {
|
||
const msg = (result && result.error) ? result.error : 'Testverbindung fehlgeschlagen.';
|
||
if (statusEl) {
|
||
statusEl.textContent = msg;
|
||
statusEl.style.color = 'red';
|
||
}
|
||
alert(msg);
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler bei MSSQL-Testverbindung:', error);
|
||
if (statusEl) {
|
||
statusEl.textContent = 'Fehler bei der Testverbindung.';
|
||
statusEl.style.color = 'red';
|
||
}
|
||
alert('Fehler bei der Testverbindung zur MSSQL-Datenbank.');
|
||
} finally {
|
||
mssqlTestBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Sync-Button
|
||
const syncNowBtn = document.getElementById('syncNowBtn');
|
||
if (syncNowBtn) {
|
||
syncNowBtn.addEventListener('click', async function() {
|
||
if (!confirm('Möchten Sie die LDAP-Synchronisation jetzt starten?')) {
|
||
return;
|
||
}
|
||
|
||
const statusEl = document.getElementById('syncStatus');
|
||
syncNowBtn.disabled = true;
|
||
statusEl.textContent = 'Synchronisation läuft...';
|
||
statusEl.style.color = 'blue';
|
||
|
||
try {
|
||
const response = await fetch('/admin/ldap/sync', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
statusEl.textContent = `Erfolgreich: ${result.synced} Benutzer synchronisiert`;
|
||
statusEl.style.color = 'green';
|
||
|
||
if (result.errors && result.errors.length > 0) {
|
||
alert('Synchronisation abgeschlossen mit Warnungen:\n' + result.errors.join('\n'));
|
||
} else {
|
||
alert(`Synchronisation erfolgreich abgeschlossen: ${result.synced} Benutzer synchronisiert`);
|
||
}
|
||
|
||
// Seite neu laden nach kurzer Verzögerung
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 2000);
|
||
} else {
|
||
statusEl.textContent = 'Fehler: ' + (result.error || 'Synchronisation fehlgeschlagen');
|
||
statusEl.style.color = 'red';
|
||
alert('Fehler: ' + (result.error || 'Synchronisation fehlgeschlagen'));
|
||
syncNowBtn.disabled = false;
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler:', error);
|
||
statusEl.textContent = 'Fehler bei der Synchronisation';
|
||
statusEl.style.color = 'red';
|
||
alert('Fehler bei der Synchronisation');
|
||
syncNowBtn.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// PDF-Archiv (Timesheets)
|
||
const pdfYearSelect = document.getElementById('pdfYearSelect');
|
||
const pdfUserSelect = document.getElementById('pdfUserSelect');
|
||
const pdfFilesContainer = document.getElementById('pdfFilesContainer');
|
||
const pdfRefreshBtn = document.getElementById('pdfRefreshBtn');
|
||
|
||
if (pdfYearSelect && pdfUserSelect && pdfFilesContainer) {
|
||
if (pdfRefreshBtn) {
|
||
pdfRefreshBtn.addEventListener('click', function() {
|
||
clearPdfPreview();
|
||
loadPdfYears();
|
||
});
|
||
}
|
||
|
||
pdfYearSelect.addEventListener('change', function() {
|
||
clearPdfPreview();
|
||
const year = this.value;
|
||
if (year) loadPdfUsers(year);
|
||
});
|
||
|
||
pdfUserSelect.addEventListener('change', function() {
|
||
clearPdfPreview();
|
||
const year = pdfYearSelect.value;
|
||
const userId = this.value;
|
||
if (year && userId) loadPdfFiles(year, userId);
|
||
});
|
||
|
||
loadPdfYears();
|
||
}
|
||
});
|
||
|
||
function setPdfStatus(text, color) {
|
||
const el = document.getElementById('pdfArchiveStatus');
|
||
if (!el) return;
|
||
el.textContent = text || '';
|
||
el.style.color = color || '';
|
||
}
|
||
|
||
function clearPdfPreview() {
|
||
const container = document.getElementById('pdfPreviewContainer');
|
||
const iframe = document.getElementById('pdfPreviewIframe');
|
||
if (container) container.style.display = 'none';
|
||
if (iframe) iframe.src = '';
|
||
}
|
||
|
||
function formatBytes(bytes) {
|
||
if (bytes === null || bytes === undefined || !Number.isFinite(Number(bytes))) return '-';
|
||
const num = Number(bytes);
|
||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
let i = 0;
|
||
let n = num;
|
||
while (n >= 1024 && i < units.length - 1) {
|
||
n /= 1024;
|
||
i += 1;
|
||
}
|
||
const fixed = i === 0 ? 0 : 1;
|
||
return `${n.toFixed(fixed)} ${units[i]}`;
|
||
}
|
||
|
||
async function loadPdfYears() {
|
||
const pdfYearSelect = document.getElementById('pdfYearSelect');
|
||
const pdfUserSelect = document.getElementById('pdfUserSelect');
|
||
const pdfFilesContainer = document.getElementById('pdfFilesContainer');
|
||
if (!pdfYearSelect || !pdfUserSelect || !pdfFilesContainer) return;
|
||
|
||
setPdfStatus('Lade PDF-Jahre...', 'blue');
|
||
pdfYearSelect.innerHTML = '<option value="">-- Laden... --</option>';
|
||
pdfUserSelect.innerHTML = '';
|
||
pdfUserSelect.disabled = true;
|
||
pdfFilesContainer.innerHTML = '';
|
||
clearPdfPreview();
|
||
|
||
try {
|
||
const response = await fetch('/admin/api/pdfs/years');
|
||
const result = await response.json();
|
||
|
||
if (!response.ok) {
|
||
throw new Error(result && result.error ? result.error : 'Fehler beim Laden der PDF-Jahre');
|
||
}
|
||
|
||
const years = Array.isArray(result.years) ? result.years : [];
|
||
if (years.length === 0) {
|
||
pdfYearSelect.innerHTML = '<option value="">Keine PDFs gefunden</option>';
|
||
setPdfStatus('Keine PDF-Dateien im Archiv gefunden.', '');
|
||
return;
|
||
}
|
||
|
||
pdfYearSelect.innerHTML = '';
|
||
years.forEach(year => {
|
||
const opt = document.createElement('option');
|
||
opt.value = year;
|
||
opt.textContent = year;
|
||
pdfYearSelect.appendChild(opt);
|
||
});
|
||
|
||
const firstYear = years[0];
|
||
pdfYearSelect.value = firstYear;
|
||
setPdfStatus('Jahre geladen. Lade User...', '');
|
||
await loadPdfUsers(firstYear);
|
||
} catch (error) {
|
||
console.error('Fehler beim Laden der PDF-Jahre:', error);
|
||
setPdfStatus('Fehler beim Laden der PDF-Jahre', 'red');
|
||
pdfYearSelect.innerHTML = '<option value="">Fehler</option>';
|
||
pdfUserSelect.disabled = true;
|
||
}
|
||
}
|
||
|
||
async function loadPdfUsers(year) {
|
||
const pdfYearSelect = document.getElementById('pdfYearSelect');
|
||
const pdfUserSelect = document.getElementById('pdfUserSelect');
|
||
const pdfFilesContainer = document.getElementById('pdfFilesContainer');
|
||
if (!pdfYearSelect || !pdfUserSelect || !pdfFilesContainer) return;
|
||
|
||
setPdfStatus(`Lade PDFs für ${year}...`, 'blue');
|
||
pdfUserSelect.disabled = true;
|
||
pdfUserSelect.innerHTML = '<option value="">-- Laden... --</option>';
|
||
pdfFilesContainer.innerHTML = '';
|
||
clearPdfPreview();
|
||
|
||
try {
|
||
const response = await fetch(`/admin/api/pdfs/users?year=${encodeURIComponent(year)}`);
|
||
const result = await response.json();
|
||
|
||
if (!response.ok) {
|
||
throw new Error(result && result.error ? result.error : 'Fehler beim Laden der User-Ordner');
|
||
}
|
||
|
||
const users = Array.isArray(result.users)
|
||
? result.users
|
||
: (Array.isArray(result.userIds) ? result.userIds.map(id => ({ id, name: id })) : []);
|
||
|
||
const userIds = users.map(u => u.id);
|
||
if (users.length === 0) {
|
||
pdfUserSelect.innerHTML = '<option value="">Keine PDFs</option>';
|
||
pdfUserSelect.disabled = true;
|
||
setPdfStatus(`Keine PDF-Dateien für das Jahr ${year} gefunden.`, '');
|
||
return;
|
||
}
|
||
|
||
pdfUserSelect.disabled = false;
|
||
pdfUserSelect.innerHTML = '';
|
||
users.forEach(user => {
|
||
const userId = user.id;
|
||
const opt = document.createElement('option');
|
||
opt.value = userId;
|
||
opt.textContent = user.name || userId;
|
||
pdfUserSelect.appendChild(opt);
|
||
});
|
||
|
||
const firstUserId = userIds[0];
|
||
pdfUserSelect.value = firstUserId;
|
||
setPdfStatus('User geladen. Lade Dateien...', '');
|
||
await loadPdfFiles(year, firstUserId);
|
||
} catch (error) {
|
||
console.error('Fehler beim Laden der PDF-User:', error);
|
||
setPdfStatus('Fehler beim Laden der PDF-User', 'red');
|
||
pdfUserSelect.innerHTML = '<option value="">Fehler</option>';
|
||
pdfUserSelect.disabled = true;
|
||
}
|
||
}
|
||
|
||
async function loadPdfFiles(year, userId) {
|
||
const pdfFilesContainer = document.getElementById('pdfFilesContainer');
|
||
if (!pdfFilesContainer) return;
|
||
|
||
setPdfStatus('Lade PDF-Dateien...', 'blue');
|
||
pdfFilesContainer.innerHTML = '';
|
||
clearPdfPreview();
|
||
|
||
try {
|
||
const response = await fetch(`/admin/api/pdfs/files?year=${encodeURIComponent(year)}&userId=${encodeURIComponent(userId)}`);
|
||
const result = await response.json();
|
||
|
||
if (!response.ok) {
|
||
throw new Error(result && result.error ? result.error : 'Fehler beim Laden der PDF-Dateien');
|
||
}
|
||
|
||
const files = Array.isArray(result.files) ? result.files : [];
|
||
if (files.length === 0) {
|
||
pdfFilesContainer.innerHTML = '<p style="color:#666;">Keine PDF-Dateien gefunden.</p>';
|
||
setPdfStatus(`Keine PDFs für User ${userId} (${year}).`, '');
|
||
return;
|
||
}
|
||
|
||
const rowsHtml = files
|
||
.map(file => {
|
||
const mtimeLabel = file.mtime ? new Date(file.mtime).toLocaleString('de-DE') : '-';
|
||
const sizeLabel = formatBytes(file.size);
|
||
// name ist serverseitig streng gefiltert; hier daher direkt in JS-String verwenden.
|
||
return `
|
||
<tr>
|
||
<td style="padding:4px; word-break:break-all;">
|
||
<code>${file.name}</code>
|
||
</td>
|
||
<td style="padding:4px;">${sizeLabel}</td>
|
||
<td style="padding:4px;">${mtimeLabel}</td>
|
||
<td style="padding:4px; white-space:nowrap;">
|
||
<button type="button" class="btn btn-primary btn-sm" onclick="previewPdf('${year}','${userId}','${file.name}')">Vorschau</button>
|
||
<button type="button" class="btn btn-secondary btn-sm" style="margin-left:6px;" onclick="downloadPdf('${year}','${userId}','${file.name}')">Download</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
})
|
||
.join('');
|
||
|
||
pdfFilesContainer.innerHTML = `
|
||
<table style="width: 100%; min-width: 980px; border-collapse: collapse;">
|
||
<thead>
|
||
<tr>
|
||
<th style="text-align:left; padding:4px;">Datei</th>
|
||
<th style="text-align:left; padding:4px;">Größe</th>
|
||
<th style="text-align:left; padding:4px;">Änderung</th>
|
||
<th style="text-align:left; padding:4px;">Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${rowsHtml}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
setPdfStatus(`${files.length} PDF(s) geladen.`, 'green');
|
||
} catch (error) {
|
||
console.error('Fehler beim Laden der PDF-Dateien:', error);
|
||
pdfFilesContainer.innerHTML = '<p style="color: red;">Fehler beim Laden der PDF-Dateien.</p>';
|
||
setPdfStatus('Fehler beim Laden der PDF-Dateien', 'red');
|
||
}
|
||
}
|
||
|
||
function previewPdf(year, userId, name) {
|
||
const iframe = document.getElementById('pdfPreviewIframe');
|
||
const container = document.getElementById('pdfPreviewContainer');
|
||
if (!iframe || !container) return;
|
||
|
||
setPdfStatus(`Vorschau: ${name}`, 'blue');
|
||
container.style.display = 'block';
|
||
iframe.src = `/admin/api/pdfs/file?year=${encodeURIComponent(year)}&userId=${encodeURIComponent(userId)}&name=${encodeURIComponent(name)}&inline=true`;
|
||
}
|
||
|
||
function downloadPdf(year, userId, name) {
|
||
const url = `/admin/api/pdfs/file?year=${encodeURIComponent(year)}&userId=${encodeURIComponent(userId)}&name=${encodeURIComponent(name)}&inline=false`;
|
||
|
||
setPdfStatus('Download gestartet...', '');
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.target = '_blank';
|
||
link.rel = 'noopener';
|
||
link.click();
|
||
}
|
||
|
||
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
|
||
async function loadOptions() {
|
||
try {
|
||
const response = await fetch('/admin/options');
|
||
const result = await response.json();
|
||
|
||
if (result.config) {
|
||
const config = result.config;
|
||
|
||
if (document.getElementById('saturdayPercentage')) {
|
||
document.getElementById('saturdayPercentage').value = config.saturday_percentage || 0;
|
||
}
|
||
if (document.getElementById('sundayPercentage')) {
|
||
document.getElementById('sundayPercentage').value = config.sunday_percentage || 0;
|
||
}
|
||
if (document.getElementById('checkinRootUrl')) {
|
||
document.getElementById('checkinRootUrl').value = config.checkin_root_url || '';
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler beim Laden der Optionen:', error);
|
||
}
|
||
}
|
||
|
||
// LDAP-Konfiguration laden und Formular ausfüllen
|
||
async function loadLDAPConfig() {
|
||
try {
|
||
const response = await fetch('/admin/ldap/config');
|
||
const result = await response.json();
|
||
|
||
if (result.config) {
|
||
const config = result.config;
|
||
|
||
if (document.getElementById('ldapEnabled')) {
|
||
document.getElementById('ldapEnabled').checked = config.enabled === 1;
|
||
}
|
||
if (document.getElementById('ldapUrl')) {
|
||
document.getElementById('ldapUrl').value = config.url || '';
|
||
}
|
||
if (document.getElementById('ldapBaseDn')) {
|
||
document.getElementById('ldapBaseDn').value = config.base_dn || '';
|
||
}
|
||
if (document.getElementById('ldapBindDn')) {
|
||
document.getElementById('ldapBindDn').value = config.bind_dn || '';
|
||
}
|
||
if (document.getElementById('ldapSearchFilter')) {
|
||
document.getElementById('ldapSearchFilter').value = config.user_search_filter || '(objectClass=person)';
|
||
}
|
||
if (document.getElementById('ldapUsernameAttr')) {
|
||
document.getElementById('ldapUsernameAttr').value = config.username_attribute || 'sAMAccountName';
|
||
}
|
||
if (document.getElementById('ldapFirstnameAttr')) {
|
||
document.getElementById('ldapFirstnameAttr').value = config.firstname_attribute || 'givenName';
|
||
}
|
||
if (document.getElementById('ldapLastnameAttr')) {
|
||
document.getElementById('ldapLastnameAttr').value = config.lastname_attribute || 'sn';
|
||
}
|
||
if (document.getElementById('ldapSyncInterval')) {
|
||
document.getElementById('ldapSyncInterval').value = config.sync_interval || 0;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler beim Laden der LDAP-Konfiguration:', error);
|
||
}
|
||
}
|
||
|
||
// MSSQL-Konfiguration laden und Formular ausfüllen
|
||
async function loadMssqlConfig() {
|
||
try {
|
||
const response = await fetch('/admin/mssql-config');
|
||
const result = await response.json();
|
||
|
||
if (result.config) {
|
||
const config = result.config;
|
||
|
||
if (document.getElementById('mssqlServer')) {
|
||
document.getElementById('mssqlServer').value = config.server || '';
|
||
}
|
||
if (document.getElementById('mssqlDatabase')) {
|
||
document.getElementById('mssqlDatabase').value = config.database || '';
|
||
}
|
||
if (document.getElementById('mssqlUsername')) {
|
||
document.getElementById('mssqlUsername').value = config.username || '';
|
||
}
|
||
// Passwort wird aus Sicherheitsgründen nie vorausgefüllt
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler beim Laden der MSSQL-Konfiguration:', error);
|
||
}
|
||
}
|
||
|
||
async function deleteUser(userId, username) {
|
||
const confirmed = confirm(`Möchten Sie den Benutzer "${username}" wirklich löschen?`);
|
||
|
||
if (!confirmed) return;
|
||
|
||
try {
|
||
const response = await fetch(`/admin/users/${userId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert('Benutzer wurde erfolgreich gelöscht!');
|
||
location.reload();
|
||
} else {
|
||
alert('Fehler: ' + (result.error || 'Benutzer konnte nicht gelöscht werden'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler:', error);
|
||
alert('Fehler beim Löschen des Benutzers');
|
||
}
|
||
}
|
||
|
||
// User bearbeiten
|
||
function editUser(userId) {
|
||
const row = document.querySelector(`tr[data-user-id="${userId}"]`);
|
||
if (!row) return;
|
||
|
||
// Alle Display-Felder ausblenden und Edit-Felder einblenden
|
||
row.querySelectorAll('.user-field-display').forEach(display => {
|
||
display.style.display = 'none';
|
||
});
|
||
row.querySelectorAll('.user-field-edit').forEach(edit => {
|
||
edit.style.display = 'inline-block';
|
||
});
|
||
|
||
// Buttons umschalten
|
||
row.querySelector('.edit-user-btn').style.display = 'none';
|
||
row.querySelector('.save-user-btn').style.display = 'inline-block';
|
||
row.querySelector('.cancel-user-btn').style.display = 'inline-block';
|
||
}
|
||
|
||
// User speichern
|
||
async function saveUser(userId) {
|
||
const row = document.querySelector(`tr[data-user-id="${userId}"]`);
|
||
if (!row) return;
|
||
|
||
const personalnummer = row.querySelector('input[data-field="personalnummer"]').value;
|
||
const wochenstunden = row.querySelector('input[data-field="wochenstunden"]').value;
|
||
const arbeitstage = row.querySelector('input[data-field="arbeitstage"]').value;
|
||
const urlaubstage = row.querySelector('input[data-field="urlaubstage"]').value;
|
||
const defaultBreakInput = row.querySelector('input[data-field="default_break_minutes"]');
|
||
const default_break_minutes = defaultBreakInput && defaultBreakInput.value !== '' ? parseInt(defaultBreakInput.value, 10) : 30;
|
||
const normalizedDefaultBreak = (!isNaN(default_break_minutes) && default_break_minutes >= 0) ? default_break_minutes : 30;
|
||
|
||
// Rollen aus Checkboxen sammeln
|
||
const roleCheckboxes = row.querySelectorAll('.role-checkbox:checked');
|
||
const roles = Array.from(roleCheckboxes).map(cb => cb.value);
|
||
|
||
// Validierung: Mindestens eine Rolle erforderlich
|
||
if (roles.length === 0) {
|
||
alert('Mindestens eine Rolle muss ausgewählt sein.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/admin/users/${userId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
personalnummer: personalnummer || null,
|
||
wochenstunden: wochenstunden || null,
|
||
arbeitstage: arbeitstage || 5,
|
||
urlaubstage: urlaubstage || null,
|
||
default_break_minutes: normalizedDefaultBreak,
|
||
roles: roles
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
// Werte in Display-Felder übernehmen
|
||
row.querySelector('span[data-field="personalnummer"]').textContent = personalnummer || '-';
|
||
row.querySelector('span[data-field="wochenstunden"]').textContent = wochenstunden || '-';
|
||
row.querySelector('span[data-field="urlaubstage"]').textContent = urlaubstage || '-';
|
||
const defaultBreakDisplay = row.querySelector('span[data-field="default_break_minutes"]');
|
||
if (defaultBreakDisplay) defaultBreakDisplay.textContent = normalizedDefaultBreak;
|
||
|
||
// Rollen-Display aktualisieren
|
||
const rolesDisplay = row.querySelector('div[data-field="roles"]');
|
||
if (rolesDisplay) {
|
||
const roleLabels = { 'mitarbeiter': 'Mitarbeiter', 'verwaltung': 'Verwaltung', 'admin': 'Admin' };
|
||
rolesDisplay.innerHTML = roles.map(role =>
|
||
`<span class="role-badge role-${role}" style="margin-right: 5px;">${roleLabels[role] || role}</span>`
|
||
).join('');
|
||
}
|
||
|
||
// Bearbeitung beenden
|
||
cancelEditUser(userId);
|
||
alert('Benutzerdaten wurden erfolgreich gespeichert!');
|
||
// Seite neu laden um sicherzustellen dass alles korrekt ist
|
||
location.reload();
|
||
} else {
|
||
alert('Fehler: ' + (result.error || 'Daten konnten nicht gespeichert werden'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Fehler:', error);
|
||
alert('Fehler beim Speichern der Benutzerdaten');
|
||
}
|
||
}
|
||
|
||
// Bearbeitung abbrechen
|
||
function cancelEditUser(userId) {
|
||
const row = document.querySelector(`tr[data-user-id="${userId}"]`);
|
||
if (!row) return;
|
||
|
||
// Alle Edit-Felder ausblenden und Display-Felder einblenden
|
||
row.querySelectorAll('.user-field-edit').forEach(edit => {
|
||
edit.style.display = 'none';
|
||
// Wert zurücksetzen (nur für Input-Felder, nicht für Rollen)
|
||
const field = edit.dataset.field;
|
||
if (field !== 'roles') {
|
||
const display = row.querySelector(`span[data-field="${field}"]`);
|
||
if (display && edit.tagName === 'INPUT') {
|
||
edit.value = display.textContent === '-' ? '' : display.textContent;
|
||
}
|
||
}
|
||
});
|
||
row.querySelectorAll('.user-field-display').forEach(display => {
|
||
if (display.tagName === 'DIV' || display.tagName === 'SPAN') {
|
||
display.style.display = 'block';
|
||
} else {
|
||
display.style.display = 'inline';
|
||
}
|
||
});
|
||
|
||
// Buttons umschalten
|
||
row.querySelector('.edit-user-btn').style.display = 'inline-block';
|
||
row.querySelector('.save-user-btn').style.display = 'none';
|
||
row.querySelector('.cancel-user-btn').style.display = 'none';
|
||
}
|