init
This commit is contained in:
247
src/routes/admin/print/[userId]/[year]/[month]/+page.svelte
Normal file
247
src/routes/admin/print/[userId]/[year]/[month]/+page.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user