Files
zeiterfassung/src/routes/admin/print/[userId]/[year]/[month]/+page.svelte
2026-04-24 10:49:56 +02:00

248 lines
6.2 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
const monthName = new Date(data.year, data.month - 1).toLocaleDateString('de-DE', {
month: 'long',
year: 'numeric'
});
const lastDay = new Date(data.year, data.month, 0).getDate();
const days = Array.from({ length: lastDay }, (_, i) => {
const d = String(i + 1).padStart(2, '0');
return `${data.year}-${String(data.month).padStart(2, '0')}-${d}`;
});
function getEntry(date: string) {
return data.entries.find((e) => e.date === date);
}
function getAbsence(date: string) {
return data.absences.find((a) => a.dateFrom <= date && a.dateTo >= date);
}
function getHoliday(date: string) {
return data.holidays.find((h) => h.date === date);
}
function isWeekend(date: string) {
const d = new Date(date + 'T00:00:00');
return d.getDay() === 0 || d.getDay() === 6;
}
function formatTime(t: string | null | undefined) {
if (!t) return '';
return t.slice(0, 5);
}
function calcHours(start: string | null, end: string | null, breakMin: number): string {
if (!start || !end) return '';
const [sh, sm] = start.split(':').map(Number);
const [eh, em] = end.split(':').map(Number);
const mins = (eh * 60 + em) - (sh * 60 + sm) - breakMin;
if (mins <= 0) return '0:00';
return `${Math.floor(mins / 60)}:${String(mins % 60).padStart(2, '0')}`;
}
function weekdayShort(date: string) {
return new Date(date + 'T00:00:00').toLocaleDateString('de-DE', { weekday: 'short' });
}
function dayNum(date: string) {
return new Date(date + 'T00:00:00').getDate();
}
const absenceLabels: Record<string, string> = {
urlaub: 'Urlaub',
krank: 'Krank',
sonderurlaub: 'Sonderurlaub'
};
</script>
<svelte:head>
<title>Stundennachweis {monthName} {data.user.lastName}</title>
</svelte:head>
<div class="print-page">
<div class="header-block">
<div>
<h1>Stundennachweis</h1>
<h2>{monthName}</h2>
</div>
<div class="employee-info">
<strong>{data.user.firstName} {data.user.lastName}</strong><br />
{data.user.email}<br />
Standardzeit: {formatTime(data.user.standardStart)} {formatTime(data.user.standardEnd)}<br />
Wochenstunden: {data.user.weeklyHours}h
</div>
</div>
<table>
<thead>
<tr>
<th>Tag</th>
<th>Beginn</th>
<th>Ende</th>
<th>Pause</th>
<th>Arbeitszeit</th>
<th>Bemerkung</th>
</tr>
</thead>
<tbody>
{#each days as day}
{@const entry = getEntry(day)}
{@const absence = getAbsence(day)}
{@const holiday = getHoliday(day)}
{@const weekend = isWeekend(day)}
<tr class:weekend class:holiday={!!holiday}>
<td class="date-cell">
<span class="weekday">{weekdayShort(day)}</span>
<span class="daynum">{dayNum(day)}.</span>
</td>
{#if holiday}
<td colspan="4" class="remark-cell">{holiday.name}</td>
<td></td>
{:else if absence}
<td colspan="4" class="remark-cell">{absenceLabels[absence.type] ?? absence.type}</td>
<td class="remark-cell">{absence.notes ?? ''}</td>
{:else if weekend}
<td colspan="5" class="remark-cell muted"></td>
{:else}
<td>{formatTime(entry?.startTime)}</td>
<td>{formatTime(entry?.endTime)}</td>
<td>{entry?.breakMinutes != null ? entry.breakMinutes + ' Min' : ''}</td>
<td class="hours">{entry ? calcHours(entry.startTime, entry.endTime, entry.breakMinutes) : ''}</td>
<td class="remark-cell">
{#if entry?.isAutoFilled && !entry?.confirmedAt}
<span class="auto-note">Standard (unbestätigt)</span>
{:else if entry?.isCorrected}
Korrigiert
{:else if entry?.confirmedAt}
{/if}
</td>
{/if}
</tr>
{/each}
</tbody>
<tfoot>
<tr class="total-row">
<td colspan="4"><strong>Gesamt Arbeitszeit</strong></td>
<td class="hours"><strong>{data.totalFormatted} Std</strong></td>
<td></td>
</tr>
</tfoot>
</table>
<div class="signature-block">
<div class="sig-field">
<div class="sig-line"></div>
<p>Datum, Unterschrift Mitarbeiter</p>
</div>
<div class="sig-field">
<div class="sig-line"></div>
<p>Datum, Unterschrift Vorgesetzter</p>
</div>
</div>
<div class="print-btn no-print">
<button onclick={() => window.print()}>Drucken / Als PDF speichern</button>
</div>
</div>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:global(body) { background: white; }
.print-page {
font-family: 'Arial', sans-serif;
font-size: 10pt;
max-width: 210mm;
margin: 0 auto;
padding: 15mm 15mm 10mm;
color: #000;
}
.header-block {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8mm;
border-bottom: 2px solid #000;
padding-bottom: 4mm;
}
h1 { font-size: 16pt; font-weight: 700; }
h2 { font-size: 12pt; font-weight: 400; color: #444; }
.employee-info { font-size: 9pt; line-height: 1.5; text-align: right; }
table {
width: 100%;
border-collapse: collapse;
font-size: 9pt;
margin-bottom: 8mm;
}
th {
background: #f0f0f0;
border: 1px solid #ccc;
padding: 3pt 5pt;
text-align: left;
font-weight: 600;
}
td {
border: 1px solid #ddd;
padding: 2.5pt 5pt;
vertical-align: middle;
}
tr.weekend { background: #f9f9f9; color: #999; }
tr.holiday { background: #fffde7; }
.date-cell { display: flex; gap: 4pt; white-space: nowrap; }
.weekday { color: #777; min-width: 20pt; }
.daynum { font-weight: 500; }
.remark-cell { color: #555; font-size: 8pt; }
.muted { color: #bbb; }
.hours { font-weight: 500; }
.auto-note { color: #999; font-style: italic; }
.total-row { background: #f5f5f5; }
.total-row td { border-top: 2px solid #888; }
.signature-block {
display: flex;
gap: 30mm;
margin-top: 15mm;
}
.sig-field { flex: 1; }
.sig-line { border-bottom: 1px solid #000; height: 15mm; margin-bottom: 3pt; }
.sig-field p { font-size: 8pt; color: #555; }
.print-btn {
margin-top: 8mm;
text-align: center;
}
.print-btn button {
padding: 0.6rem 1.5rem;
background: #2563eb;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 0.9rem;
}
.print-btn button:hover { background: #1d4ed8; }
@media print {
.no-print { display: none; }
.print-page { padding: 0; max-width: none; }
@page { margin: 15mm; size: A4; }
}
</style>