Sortieungsoptionen in Verwaltung, Umstellung der PDF generation auf generierung bei abgabe und ablage auf dem Sateisystem um einen Festen Stand zu garantieren

This commit is contained in:
2026-03-17 16:20:36 +01:00
parent 2aa4e6f037
commit a92694f693
8 changed files with 386 additions and 14 deletions

View File

@@ -73,6 +73,45 @@
<div id="bulkDownloadStatus" style="margin-top: 12px; font-size: 13px; color: #666; display: none;"></div>
</div>
<!-- Sortierung der Mitarbeiter-Gruppen -->
<div style="margin-bottom: 25px; padding: 12px 16px; background-color: #f8f9fa; border-radius: 8px; border: 1px solid #dee2e6;">
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 10px;">
<span style="font-size: 13px; color: #555; font-weight: 500; margin-right: 5px;">Sortieren nach:</span>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
<button type="button" class="btn btn-secondary btn-sm timesheet-sort-btn" data-sort-field="firstname" data-sort-direction="asc">
Vorname ▲
</button>
<button type="button" class="btn btn-secondary btn-sm timesheet-sort-btn" data-sort-field="firstname" data-sort-direction="desc">
Vorname ▼
</button>
<button type="button" class="btn btn-secondary btn-sm timesheet-sort-btn" data-sort-field="name" data-sort-direction="asc">
Name ▲
</button>
<button type="button" class="btn btn-secondary btn-sm timesheet-sort-btn" data-sort-field="name" data-sort-direction="desc">
Name ▼
</button>
<button type="button" class="btn btn-secondary btn-sm timesheet-sort-btn" data-sort-field="personalnummer" data-sort-direction="asc">
Personalnr. ▲
</button>
<button type="button" class="btn btn-secondary btn-sm timesheet-sort-btn" data-sort-field="personalnummer" data-sort-direction="desc">
Personalnr. ▼
</button>
<button type="button" class="btn btn-secondary btn-sm timesheet-sort-btn" data-sort-field="overtime" data-sort-direction="asc">
Aktuelle Überstunden ▲
</button>
<button type="button" class="btn btn-secondary btn-sm timesheet-sort-btn" data-sort-field="overtime" data-sort-direction="desc">
Aktuelle Überstunden ▼
</button>
<button type="button" class="btn btn-secondary btn-sm timesheet-sort-btn" data-sort-field="remainingVacation" data-sort-direction="asc">
Verbleibender Urlaub ▲
</button>
<button type="button" class="btn btn-secondary btn-sm timesheet-sort-btn" data-sort-field="remainingVacation" data-sort-direction="desc">
Verbleibender Urlaub ▼
</button>
</div>
</div>
</div>
<% if (!groupedByEmployee || groupedByEmployee.length === 0) { %>
<div class="empty-state">
<p>Keine eingereichten Stundenzettel vorhanden.</p>
@@ -81,7 +120,14 @@
<div class="timesheet-groups">
<% groupedByEmployee.forEach(function(employee, employeeIndex) { %>
<!-- Level 1: Mitarbeiter -->
<div class="employee-group" data-employee-id="<%= employee.user.id %>" data-employee-index="<%= employeeIndex %>">
<div
class="employee-group"
data-employee-id="<%= employee.user.id %>"
data-employee-index="<%= employeeIndex %>"
data-lastname="<%= employee.user.lastname || '' %>"
data-firstname="<%= employee.user.firstname || '' %>"
data-personalnummer="<%= employee.user.personalnummer || '' %>"
>
<div class="employee-header">
<div class="employee-info">
<div class="employee-name">
@@ -513,9 +559,16 @@
if (value === null) {
el.textContent = '-';
el.style.color = '';
el.dataset.overtimeValue = '';
} else {
el.textContent = (value >= 0 ? '+' : '') + formatHoursMin(value);
el.style.color = value >= 0 ? '#27ae60' : '#e74c3c';
el.dataset.overtimeValue = String(value);
}
const group = document.querySelector(`.employee-group[data-employee-id="${userId}"]`);
if (group) {
group.dataset.overtimeValue = el.dataset.overtimeValue || '';
}
});
} catch (error) {
@@ -523,6 +576,7 @@
elements.forEach(el => {
el.textContent = '-';
el.style.color = '';
el.dataset.overtimeValue = '';
});
}
}
@@ -573,6 +627,12 @@
} else {
el.textContent = value.toFixed(1);
}
// Wert für Sortierung am Element und an der Mitarbeiter-Gruppe hinterlegen
el.dataset.remainingVacation = value !== null ? String(value) : '';
const group = el.closest('.employee-group') || document.querySelector(`.employee-group[data-employee-id="${el.dataset.userId}"]`);
if (group) {
group.dataset.remainingVacation = value !== null ? String(value) : '';
}
});
} catch (err) {
console.error('Fehler beim Laden des verbleibenden Urlaubs für User', userId, err);
@@ -585,6 +645,91 @@
loadSickDays();
loadCurrentOvertime();
loadRemainingVacation();
function sortEmployeeGroups(field, direction) {
const container = document.querySelector('.timesheet-groups');
if (!container) return;
const groups = Array.from(container.querySelectorAll('.employee-group'));
if (groups.length === 0) return;
const dir = direction === 'desc' ? -1 : 1;
function compareStrings(a, b) {
const sa = (a || '').toString().toLowerCase();
const sb = (b || '').toString().toLowerCase();
return sa.localeCompare(sb, 'de-DE');
}
function parseNumberOrNull(value) {
if (value === null || value === undefined || value === '') return null;
const n = Number(value);
return Number.isFinite(n) ? n : null;
}
groups.sort((a, b) => {
let va;
let vb;
if (field === 'name') {
const aLast = a.dataset.lastname || '';
const aFirst = a.dataset.firstname || '';
const bLast = b.dataset.lastname || '';
const bFirst = b.dataset.firstname || '';
const cmp = compareStrings(`${aLast} ${aFirst}`, `${bLast} ${bFirst}`);
return cmp * dir;
}
if (field === 'firstname') {
const aFirst = a.dataset.firstname || '';
const bFirst = b.dataset.firstname || '';
const firstCmp = compareStrings(aFirst, bFirst);
if (firstCmp !== 0) return firstCmp * dir;
// Bei gleichem Vornamen nach Nachname sortieren
const aLast = a.dataset.lastname || '';
const bLast = b.dataset.lastname || '';
const lastCmp = compareStrings(aLast, bLast);
return lastCmp * dir;
}
if (field === 'personalnummer') {
const aRaw = a.dataset.personalnummer || '';
const bRaw = b.dataset.personalnummer || '';
if (!aRaw && !bRaw) return 0;
if (!aRaw) return 1;
if (!bRaw) return -1;
const digitsOnly = /^\d+$/;
const aIsNum = digitsOnly.test(aRaw);
const bIsNum = digitsOnly.test(bRaw);
if (aIsNum && bIsNum) {
va = Number(aRaw);
vb = Number(bRaw);
if (va === vb) return 0;
return va < vb ? -1 * dir : 1 * dir;
}
const cmp = compareStrings(aRaw, bRaw);
return cmp * dir;
}
if (field === 'overtime') {
va = parseNumberOrNull(a.dataset.overtimeValue);
vb = parseNumberOrNull(b.dataset.overtimeValue);
} else if (field === 'remainingVacation') {
va = parseNumberOrNull(a.dataset.remainingVacation);
vb = parseNumberOrNull(b.dataset.remainingVacation);
}
if (va === null && vb === null) return 0;
if (va === null) return 1;
if (vb === null) return -1;
if (va === vb) return 0;
return va < vb ? -1 * dir : 1 * dir;
});
groups.forEach(group => container.appendChild(group));
}
// Überstunden-Korrektur-Historie laden/anzeigen
function parseSqliteDatetime(value) {
@@ -1057,6 +1202,30 @@
}
});
}
// Sortier-Buttons für Mitarbeiter-Gruppen
(function initTimesheetGroupSorting() {
const sortButtons = document.querySelectorAll('.timesheet-sort-btn');
if (!sortButtons.length) return;
let activeSortButton = null;
sortButtons.forEach(btn => {
btn.addEventListener('click', function() {
const field = this.dataset.sortField;
const direction = this.dataset.sortDirection || 'asc';
if (!field) return;
sortEmployeeGroups(field, direction);
if (activeSortButton && activeSortButton !== this) {
activeSortButton.classList.remove('active-sort');
}
this.classList.add('active-sort');
activeSortButton = this;
});
});
})();
</script>
<script>
// Rollenwechsel-Handler