From 9ee78d2dcfd20c150ada3e222753927e65c2793d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20X=2E=20F=C3=BCrst?= Date: Mon, 22 Jun 2026 10:05:50 +0200 Subject: [PATCH] Start-/Endzeit optional: ohne Zeiten Arbeitszeit Kunde & Mehrarbeit leer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Start- und Endzeit nicht mehr vorbelegt (leeres Feld, Platzhalter HH:MM) - TimeInput erlaubt Leeren eines Feldes - Validierung: beide Zeiten zusammen angeben oder beide leer lassen - Ohne Zeiten zeigen Vorschau und Liste "—" für Arbeitszeit Kunde und Mehrarbeit (Eintrag mit nur Reisezeit/Vertriebsunterstützung möglich) Co-Authored-By: Claude Opus 4.8 (1M context) --- components/ArbeitszeitForm.tsx | 24 +++++++++++++++++------- components/ArbeitszeitList.tsx | 7 ++++--- components/TimeInput.tsx | 3 ++- lib/calc.ts | 5 +++++ lib/validate.ts | 9 +++++++-- 5 files changed, 35 insertions(+), 13 deletions(-) diff --git a/components/ArbeitszeitForm.tsx b/components/ArbeitszeitForm.tsx index 27cd45f..87ef65f 100644 --- a/components/ArbeitszeitForm.tsx +++ b/components/ArbeitszeitForm.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { ORTE, ARBEITSZEIT_KUNDE_FIX, type ArbeitszeitEintrag } from '@/types/arbeitszeit'; -import { toDecimalHours, mehrarbeit, weekdayName, formatHours } from '@/lib/calc'; +import { toDecimalHours, mehrarbeit, weekdayName, formatHours, hatZeiten } from '@/lib/calc'; import CustomSelect from './CustomSelect'; import TimeInput from './TimeInput'; @@ -30,8 +30,8 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }: // daher genügt eine einmalige Initialisierung aus editEntry (kein Sync-Effekt nötig). const [datum, setDatum] = useState(editEntry?.datum ?? todayDate()); const [ort, setOrt] = useState(editEntry?.ort ?? ORTE[0]); - const [beginn, setBeginn] = useState(editEntry?.beginn ?? '09:00'); - const [ende, setEnde] = useState(editEntry?.ende ?? '17:00'); + const [beginn, setBeginn] = useState(editEntry?.beginn ?? ''); + const [ende, setEnde] = useState(editEntry?.ende ?? ''); const [reisezeit, setReisezeit] = useState(editEntry ? String(editEntry.reisezeit) : '0'); const [vertrieb, setVertrieb] = useState(editEntry ? String(editEntry.vertriebsunterstuetzung) : '2'); const [kommentar, setKommentar] = useState(editEntry?.kommentar ?? ''); @@ -39,6 +39,7 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }: const [error, setError] = useState(''); const [success, setSuccess] = useState(false); + const zeiten = hatZeiten(beginn, ende); const arbeitszeit = toDecimalHours(beginn, ende); const mehr = mehrarbeit(arbeitszeit, pause); @@ -47,7 +48,13 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }: setError(''); setSuccess(false); - if (arbeitszeit <= 0) { + const beginnLeer = beginn === ''; + const endeLeer = ende === ''; + if (beginnLeer !== endeLeer) { + setError('Bitte Start- und Endzeit gemeinsam angeben oder beide leer lassen.'); + return; + } + if (!beginnLeer && !endeLeer && arbeitszeit <= 0) { setError('Endzeit muss nach der Startzeit liegen.'); return; } @@ -172,16 +179,19 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }: {/* Berechnungs-Vorschau */}
- Arbeitszeit: {formatHours(arbeitszeit)} h + Arbeitszeit: {zeiten ? `${formatHours(arbeitszeit)} h` : '—'} - Arbeitszeit Kunde: {formatHours(ARBEITSZEIT_KUNDE_FIX)} h + Arbeitszeit Kunde: {zeiten ? `${formatHours(ARBEITSZEIT_KUNDE_FIX)} h` : '—'} Pause: {formatHours(pause)} h - Mehrarbeit: {formatHours(mehr)} h + Mehrarbeit:{' '} + + {zeiten ? `${formatHours(mehr)} h` : '—'} +
diff --git a/components/ArbeitszeitList.tsx b/components/ArbeitszeitList.tsx index 08a0cc1..8b1d63b 100644 --- a/components/ArbeitszeitList.tsx +++ b/components/ArbeitszeitList.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { ARBEITSZEIT_KUNDE_FIX, type ArbeitszeitEintrag } from '@/types/arbeitszeit'; -import { toDecimalHours, mehrarbeit, weekdayName, formatHours } from '@/lib/calc'; +import { toDecimalHours, mehrarbeit, weekdayName, formatHours, hatZeiten } from '@/lib/calc'; interface Props { pause: number; @@ -131,14 +131,15 @@ export default function ArbeitszeitList({ pause, refreshKey, onEdit, limit = 15, ) : entries.map((e) => { + const zeiten = hatZeiten(e.beginn, e.ende); const mehr = mehrarbeit(toDecimalHours(e.beginn, e.ende), pause); return ( {formatDate(e.datum)} {weekdayName(e.datum)} {e.ort} - {formatHours(ARBEITSZEIT_KUNDE_FIX)} - {formatHours(mehr)} + {zeiten ? formatHours(ARBEITSZEIT_KUNDE_FIX) : '—'} + {zeiten ? formatHours(mehr) : '—'} {formatHours(e.reisezeit)} {formatHours(e.vertriebsunterstuetzung)} {e.kommentar} diff --git a/components/TimeInput.tsx b/components/TimeInput.tsx index 4526a4c..8e8524b 100644 --- a/components/TimeInput.tsx +++ b/components/TimeInput.tsx @@ -52,8 +52,9 @@ export default function TimeInput({ value, onChange, className = '', clearOnFocu function handleBlur() { if (local === '') { - setLocal(value); + // Leeres Feld ist erlaubt (keine Uhrzeit) — leeren Wert übernehmen. setError(false); + if (value !== '') onChange(''); return; } const expanded = /^\d{1,2}:?$/.test(local) ? local.replace(/:$/, '') + ':00' : local; diff --git a/lib/calc.ts b/lib/calc.ts index cb97ae5..140b215 100644 --- a/lib/calc.ts +++ b/lib/calc.ts @@ -38,6 +38,11 @@ export function mehrarbeit(arbeitszeitStd: number, pauseStd: number): number { return arbeitszeitStd - 8 - pauseStd; } +/** True, wenn sowohl Beginn als auch Ende gültige Uhrzeiten sind (beide angegeben). */ +export function hatZeiten(beginn: string, ende: string): boolean { + return parseHM(beginn) !== null && parseHM(ende) !== null; +} + /** Deutscher Wochentagsname für ein "YYYY-MM-DD"-Datum, oder "" bei ungültig. */ export function weekdayName(datum: string): string { const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(datum ?? ''); diff --git a/lib/validate.ts b/lib/validate.ts index ba5b96e..2bdc5f6 100644 --- a/lib/validate.ts +++ b/lib/validate.ts @@ -14,10 +14,15 @@ export function parseArbeitszeitInput(body: unknown): ArbeitszeitInput { const ort = String(b.ort ?? '') as Ort; if (!ORTE.includes(ort)) throw new Error('Ungültiger Ort.'); + // Beginn/Ende sind optional. Wenn angegeben, müssen sie gültig sein; + // entweder beide leer oder beide gesetzt. const beginn = String(b.beginn ?? ''); const ende = String(b.ende ?? ''); - if (parseHM(beginn) === null) throw new Error('Ungültige Startzeit.'); - if (parseHM(ende) === null) throw new Error('Ungültige Endzeit.'); + if (beginn !== '' && parseHM(beginn) === null) throw new Error('Ungültige Startzeit.'); + if (ende !== '' && parseHM(ende) === null) throw new Error('Ungültige Endzeit.'); + if ((beginn === '') !== (ende === '')) { + throw new Error('Bitte Start- und Endzeit gemeinsam angeben oder beide leer lassen.'); + } const reisezeit = toNumber(b.reisezeit, 0); const vertriebsunterstuetzung = toNumber(b.vertriebsunterstuetzung, 2);