Massdownload
This commit is contained in:
@@ -27,6 +27,16 @@
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: flex-start; cursor: pointer;">
|
||||
<input type="checkbox" name="remember_me" id="remember_me" style="margin-right: 8px; margin-top: 2px;">
|
||||
<span style="display: flex; flex-direction: column;">
|
||||
<span>Angemeldet bleiben</span>
|
||||
<span style="font-size: 0.9em; color: #666;">(30 Tage)</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,41 @@
|
||||
<div class="verwaltung-panel">
|
||||
<h2>Postfach - Eingereichte Stundenzettel</h2>
|
||||
|
||||
<!-- Massendownload für Kalenderwoche -->
|
||||
<div style="margin-bottom: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; border: 1px solid #dee2e6;">
|
||||
<h3 style="margin-top: 0; margin-bottom: 15px; font-size: 16px; color: #333;">Massendownload für Kalenderwoche</h3>
|
||||
<div style="display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap;">
|
||||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||
<label for="bulkDownloadYear" style="font-size: 13px; color: #555; font-weight: 500;">Jahr:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="bulkDownloadYear"
|
||||
min="2000"
|
||||
max="2100"
|
||||
value="<%= new Date().getFullYear() %>"
|
||||
style="padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; width: 100px;"
|
||||
placeholder="2024">
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||
<label for="bulkDownloadWeek" style="font-size: 13px; color: #555; font-weight: 500;">Kalenderwoche:</label>
|
||||
<input
|
||||
type="number"
|
||||
id="bulkDownloadWeek"
|
||||
min="1"
|
||||
max="53"
|
||||
style="padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; width: 100px;"
|
||||
placeholder="5">
|
||||
</div>
|
||||
<button
|
||||
id="bulkDownloadBtn"
|
||||
class="btn btn-primary"
|
||||
style="padding: 8px 20px; font-size: 14px; white-space: nowrap;">
|
||||
Alle PDFs für KW herunterladen
|
||||
</button>
|
||||
</div>
|
||||
<div id="bulkDownloadStatus" style="margin-top: 12px; font-size: 13px; color: #666; display: none;"></div>
|
||||
</div>
|
||||
|
||||
<% if (!groupedByEmployee || groupedByEmployee.length === 0) { %>
|
||||
<div class="empty-state">
|
||||
<p>Keine eingereichten Stundenzettel vorhanden.</p>
|
||||
@@ -53,6 +88,25 @@
|
||||
<div style="display: inline-block; margin-right: 20px;">
|
||||
<strong>Urlaubstage:</strong> <span><%= employee.user.urlaubstage || '-' %></span>
|
||||
</div>
|
||||
<div style="display: inline-flex; gap: 8px; align-items: center; margin-right: 20px;">
|
||||
<strong>Überstunden-Offset:</strong>
|
||||
<input
|
||||
type="number"
|
||||
step="0.25"
|
||||
class="overtime-offset-input"
|
||||
data-user-id="<%= employee.user.id %>"
|
||||
value="<%= (employee.user.overtime_offset_hours !== undefined && employee.user.overtime_offset_hours !== null) ? employee.user.overtime_offset_hours : 0 %>"
|
||||
style="width: 90px; padding: 4px 6px; border: 1px solid #ddd; border-radius: 4px;"
|
||||
title="Manuelle Korrektur (positiv oder negativ) in Stunden" />
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success btn-sm save-overtime-offset-btn"
|
||||
data-user-id="<%= employee.user.id %>"
|
||||
style="padding: 6px 10px; white-space: nowrap;"
|
||||
title="Überstunden-Offset speichern">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
<div style="display: inline-block; margin-right: 20px;">
|
||||
<strong>Kalenderwochen:</strong> <span><%= employee.weeks.length %></span>
|
||||
</div>
|
||||
@@ -70,7 +124,20 @@
|
||||
<div class="week-header">
|
||||
<div class="week-info">
|
||||
<div class="week-dates">
|
||||
<strong>Kalenderwoche:</strong> <%= new Date(week.week_start).toLocaleDateString('de-DE') %> -
|
||||
<%
|
||||
// Kalenderwoche berechnen
|
||||
function getCalendarWeek(dateStr) {
|
||||
const date = new Date(dateStr);
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
||||
return weekNo;
|
||||
}
|
||||
const calendarWeek = getCalendarWeek(week.week_start);
|
||||
%>
|
||||
<strong>Kalenderwoche <%= String(calendarWeek).padStart(2, '0') %>:</strong> <%= new Date(week.week_start).toLocaleDateString('de-DE') %> -
|
||||
<%= new Date(week.week_end).toLocaleDateString('de-DE') %>
|
||||
</div>
|
||||
<div class="group-stats" data-user-id="<%= employee.user.id %>" data-week-start="<%= week.week_start %>" data-week-end="<%= week.week_end %>" style="margin-top: 10px;">
|
||||
@@ -205,13 +272,12 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Statistiken für alle Wochen laden
|
||||
document.querySelectorAll('.group-stats').forEach(statsDiv => {
|
||||
async function loadStatsForDiv(statsDiv) {
|
||||
const userId = statsDiv.dataset.userId;
|
||||
const weekStart = statsDiv.dataset.weekStart;
|
||||
const weekEnd = statsDiv.dataset.weekEnd;
|
||||
|
||||
fetch(`/api/verwaltung/user/${userId}/stats?week_start=${weekStart}&week_end=${weekEnd}`)
|
||||
return fetch(`/api/verwaltung/user/${userId}/stats?week_start=${weekStart}&week_end=${weekEnd}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const loadingDiv = statsDiv.querySelector('.stats-loading');
|
||||
@@ -219,17 +285,26 @@
|
||||
loadingDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
// vorherige Stats entfernen (wenn reloaded)
|
||||
statsDiv.querySelectorAll('.stats-inline').forEach(n => n.remove());
|
||||
|
||||
// Statistiken anzeigen
|
||||
let statsHTML = '';
|
||||
if (data.overtimeHours !== undefined) {
|
||||
statsHTML += `<div style="display: inline-block; margin-right: 20px;">
|
||||
statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;">
|
||||
<strong>Überstunden:</strong> <span>${data.overtimeHours.toFixed(2)} h</span>
|
||||
${data.overtimeTaken > 0 ? `<span style="color: #666;">(davon genommen: ${data.overtimeTaken.toFixed(2)} h)</span>` : ''}
|
||||
${data.remainingOvertime !== data.overtimeHours ? `<span style="color: #28a745;">(verbleibend: ${data.remainingOvertime.toFixed(2)} h)</span>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
if (data.overtimeOffsetHours !== undefined && data.overtimeOffsetHours !== 0) {
|
||||
statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;">
|
||||
<strong>Offset:</strong> <span>${Number(data.overtimeOffsetHours).toFixed(2)} h</span>
|
||||
${data.remainingOvertimeWithOffset !== undefined ? `<span style="color: #28a745;">(verbleibend inkl. Offset: ${Number(data.remainingOvertimeWithOffset).toFixed(2)} h)</span>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
if (data.vacationDays !== undefined) {
|
||||
statsHTML += `<div style="display: inline-block; margin-right: 20px;">
|
||||
statsHTML += `<div class="stats-inline" style="display: inline-block; margin-right: 20px;">
|
||||
<strong>Urlaub genommen:</strong> <span>${data.vacationDays.toFixed(1)} Tag${data.vacationDays !== 1 ? 'e' : ''}</span>
|
||||
${data.remainingVacation !== undefined ? `<span style="color: #28a745;">(verbleibend: ${data.remainingVacation.toFixed(1)} Tage)</span>` : ''}
|
||||
</div>`;
|
||||
@@ -247,6 +322,71 @@
|
||||
loadingDiv.style.color = 'red';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Statistiken für alle Wochen initial laden
|
||||
document.querySelectorAll('.group-stats').forEach(statsDiv => loadStatsForDiv(statsDiv));
|
||||
|
||||
// Überstunden-Offset speichern
|
||||
document.querySelectorAll('.save-overtime-offset-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async function() {
|
||||
const userId = this.dataset.userId;
|
||||
const input = document.querySelector(`.overtime-offset-input[data-user-id="${userId}"]`);
|
||||
if (!input) return;
|
||||
|
||||
const originalText = this.textContent;
|
||||
this.disabled = true;
|
||||
this.textContent = '...';
|
||||
|
||||
// leere Eingabe => 0 (Backend macht das auch, aber UI soll sauber sein)
|
||||
const raw = (input.value || '').trim();
|
||||
const value = raw === '' ? '' : Number(raw);
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/verwaltung/user/${userId}/overtime-offset`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ overtime_offset_hours: value })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) {
|
||||
alert(data.error || 'Fehler beim Speichern des Offsets');
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalisiere Input auf Zahl (Backend gibt number zurück)
|
||||
input.value = (data.overtime_offset_hours !== undefined && data.overtime_offset_hours !== null)
|
||||
? Number(data.overtime_offset_hours)
|
||||
: 0;
|
||||
|
||||
// Stats für diesen User neu laden
|
||||
const statDivs = document.querySelectorAll(`.group-stats[data-user-id="${userId}"]`);
|
||||
statDivs.forEach(div => {
|
||||
// loading indicator optional wieder anzeigen
|
||||
const loading = div.querySelector('.stats-loading');
|
||||
if (loading) {
|
||||
loading.style.display = 'inline-block';
|
||||
loading.style.color = '#666';
|
||||
loading.textContent = 'Lade Statistiken...';
|
||||
}
|
||||
loadStatsForDiv(div);
|
||||
});
|
||||
|
||||
this.textContent = '✓';
|
||||
setTimeout(() => {
|
||||
this.textContent = originalText;
|
||||
this.disabled = false;
|
||||
}, 900);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Speichern des Offsets:', e);
|
||||
alert('Fehler beim Speichern des Offsets');
|
||||
} finally {
|
||||
if (this.textContent === '...') {
|
||||
this.textContent = originalText;
|
||||
this.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Mitarbeiter-Gruppen auf-/zuklappen (zeigt/versteckt Wochen)
|
||||
@@ -469,6 +609,88 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Massendownload für Kalenderwoche
|
||||
const bulkDownloadBtn = document.getElementById('bulkDownloadBtn');
|
||||
const bulkDownloadYear = document.getElementById('bulkDownloadYear');
|
||||
const bulkDownloadWeek = document.getElementById('bulkDownloadWeek');
|
||||
const bulkDownloadStatus = document.getElementById('bulkDownloadStatus');
|
||||
|
||||
if (bulkDownloadBtn) {
|
||||
bulkDownloadBtn.addEventListener('click', async function() {
|
||||
const year = parseInt(bulkDownloadYear.value);
|
||||
const week = parseInt(bulkDownloadWeek.value);
|
||||
|
||||
// Validierung
|
||||
if (!year || year < 2000 || year > 2100) {
|
||||
bulkDownloadStatus.textContent = 'Bitte geben Sie ein gültiges Jahr ein (2000-2100)';
|
||||
bulkDownloadStatus.style.display = 'block';
|
||||
bulkDownloadStatus.style.color = '#dc3545';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!week || week < 1 || week > 53) {
|
||||
bulkDownloadStatus.textContent = 'Bitte geben Sie eine gültige Kalenderwoche ein (1-53)';
|
||||
bulkDownloadStatus.style.display = 'block';
|
||||
bulkDownloadStatus.style.color = '#dc3545';
|
||||
return;
|
||||
}
|
||||
|
||||
// Button deaktivieren und Status anzeigen
|
||||
bulkDownloadBtn.disabled = true;
|
||||
bulkDownloadBtn.textContent = 'Lädt...';
|
||||
bulkDownloadStatus.textContent = 'PDFs werden generiert und ZIP erstellt...';
|
||||
bulkDownloadStatus.style.display = 'block';
|
||||
bulkDownloadStatus.style.color = '#666';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/verwaltung/bulk-download/${year}/${week}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' }));
|
||||
throw new Error(errorData.error || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
// ZIP-Download starten
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `Stundenzettel_KW${String(week).padStart(2, '0')}_${year}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
// Erfolgsmeldung
|
||||
bulkDownloadStatus.textContent = `✓ ZIP erfolgreich heruntergeladen (KW ${week}/${year})`;
|
||||
bulkDownloadStatus.style.color = '#28a745';
|
||||
|
||||
// Seite nach kurzer Verzögerung neu laden, um Download-Marker zu aktualisieren
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Massendownload:', error);
|
||||
bulkDownloadStatus.textContent = `Fehler: ${error.message || 'Unbekannter Fehler'}`;
|
||||
bulkDownloadStatus.style.color = '#dc3545';
|
||||
bulkDownloadBtn.disabled = false;
|
||||
bulkDownloadBtn.textContent = 'Alle PDFs für KW herunterladen';
|
||||
}
|
||||
});
|
||||
|
||||
// Enter-Taste in Eingabefeldern
|
||||
[bulkDownloadYear, bulkDownloadWeek].forEach(input => {
|
||||
if (input) {
|
||||
input.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
bulkDownloadBtn.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
// Rollenwechsel-Handler
|
||||
|
||||
Reference in New Issue
Block a user