Start-/Endzeit optional: ohne Zeiten Arbeitszeit Kunde & Mehrarbeit leer

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 10:05:50 +02:00
parent 01a7ac8e0b
commit 9ee78d2dcf
5 changed files with 35 additions and 13 deletions
+17 -7
View File
@@ -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<string>(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 */}
<div className="flex flex-wrap gap-x-6 gap-y-1 text-sm bg-[#EEF7EE] border border-green-200 rounded-lg px-3 py-2">
<span>
Arbeitszeit: <strong>{formatHours(arbeitszeit)} h</strong>
Arbeitszeit: <strong>{zeiten ? `${formatHours(arbeitszeit)} h` : '—'}</strong>
</span>
<span>
Arbeitszeit Kunde: <strong>{formatHours(ARBEITSZEIT_KUNDE_FIX)} h</strong>
Arbeitszeit Kunde: <strong>{zeiten ? `${formatHours(ARBEITSZEIT_KUNDE_FIX)} h` : '—'}</strong>
</span>
<span>
Pause: <strong>{formatHours(pause)} h</strong>
</span>
<span>
Mehrarbeit: <strong className={mehr < 0 ? 'text-red-600' : 'text-green-800'}>{formatHours(mehr)} h</strong>
Mehrarbeit:{' '}
<strong className={zeiten ? (mehr < 0 ? 'text-red-600' : 'text-green-800') : ''}>
{zeiten ? `${formatHours(mehr)} h` : '—'}
</strong>
</span>
</div>
+4 -3
View File
@@ -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,
</td>
</tr>
) : entries.map((e) => {
const zeiten = hatZeiten(e.beginn, e.ende);
const mehr = mehrarbeit(toDecimalHours(e.beginn, e.ende), pause);
return (
<tr key={e.id} className="hover:bg-gray-50">
<td className={`${cell} whitespace-nowrap`}>{formatDate(e.datum)}</td>
<td className={cell}>{weekdayName(e.datum)}</td>
<td className={cell}>{e.ort}</td>
<td className={`${cell} text-right`}>{formatHours(ARBEITSZEIT_KUNDE_FIX)}</td>
<td className={`${cell} text-right ${mehr < 0 ? 'text-red-600' : ''}`}>{formatHours(mehr)}</td>
<td className={`${cell} text-right`}>{zeiten ? formatHours(ARBEITSZEIT_KUNDE_FIX) : '—'}</td>
<td className={`${cell} text-right ${zeiten && mehr < 0 ? 'text-red-600' : ''}`}>{zeiten ? formatHours(mehr) : '—'}</td>
<td className={`${cell} text-right`}>{formatHours(e.reisezeit)}</td>
<td className={`${cell} text-right`}>{formatHours(e.vertriebsunterstuetzung)}</td>
<td className={cell}>{e.kommentar}</td>
+2 -1
View File
@@ -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;
+5
View File
@@ -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 ?? '');
+7 -2
View File
@@ -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);