This commit is contained in:
2026-04-24 10:49:56 +02:00
commit 669521b4dd
41 changed files with 6143 additions and 0 deletions

View File

@@ -0,0 +1,247 @@
<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>