248 lines
6.2 KiB
Svelte
248 lines
6.2 KiB
Svelte
<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>
|