Vertriebsunterstützung an Reisezeit koppeln

- Reisezeit 0 -> Vertriebsunterstützung 0
- Reisezeit < 2h -> Vertriebsunterstützung = Reisezeit (Feld gesperrt)
- Reisezeit >= 2h -> Vertriebsunterstützung 2h, aber editierbar
- Regel zentral in lib/calc.ts (applyVertriebsregel), im Formular interaktiv
  und in der API-Validierung erzwungen

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 10:23:07 +02:00
parent 9ee78d2dcf
commit 1e221a3c2c
3 changed files with 54 additions and 7 deletions
+32 -5
View File
@@ -2,7 +2,14 @@
import { useState } from 'react';
import { ORTE, ARBEITSZEIT_KUNDE_FIX, type ArbeitszeitEintrag } from '@/types/arbeitszeit';
import { toDecimalHours, mehrarbeit, weekdayName, formatHours, hatZeiten } from '@/lib/calc';
import {
toDecimalHours,
mehrarbeit,
weekdayName,
formatHours,
hatZeiten,
vertriebEditierbar,
} from '@/lib/calc';
import CustomSelect from './CustomSelect';
import TimeInput from './TimeInput';
@@ -25,6 +32,11 @@ function toNum(raw: string): number {
return isNaN(n) ? 0 : n;
}
// Zahl als deutscher Dezimalstring (Komma)
function numToStr(n: number): string {
return String(n).replace('.', ',');
}
export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }: Props) {
// Diese Komponente wird beim Wechsel von editEntry über `key` neu gemountet,
// daher genügt eine einmalige Initialisierung aus editEntry (kein Sync-Effekt nötig).
@@ -32,8 +44,8 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
const [ort, setOrt] = useState<string>(editEntry?.ort ?? ORTE[0]);
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 [reisezeit, setReisezeit] = useState(editEntry ? numToStr(editEntry.reisezeit) : '0');
const [vertrieb, setVertrieb] = useState(editEntry ? numToStr(editEntry.vertriebsunterstuetzung) : '0');
const [kommentar, setKommentar] = useState(editEntry?.kommentar ?? '');
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
@@ -42,6 +54,19 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
const zeiten = hatZeiten(beginn, ende);
const arbeitszeit = toDecimalHours(beginn, ende);
const mehr = mehrarbeit(arbeitszeit, pause);
const vertriebFrei = vertriebEditierbar(toNum(reisezeit));
// Vertriebsunterstützung an die Reisezeit koppeln (siehe applyVertriebsregel):
// =0 ⇒ 0, <2 ⇒ = Reisezeit, ≥2 ⇒ 2 beim Überschreiten der Schwelle, danach frei änderbar.
function handleReisezeit(raw: string) {
const alt = toNum(reisezeit);
const neu = toNum(raw);
setReisezeit(raw);
if (neu <= 0) setVertrieb('0');
else if (neu < 2) setVertrieb(numToStr(neu));
else if (alt < 2) setVertrieb('2'); // Schwelle 2h überschritten → Standard 2h
// neu ≥ 2 und alt ≥ 2: bestehenden (ggf. geänderten) Wert beibehalten
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
@@ -145,7 +170,7 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
type="text"
inputMode="decimal"
value={reisezeit}
onChange={(e) => setReisezeit(e.target.value)}
onChange={(e) => handleReisezeit(e.target.value)}
className={numCls}
/>
</div>
@@ -156,7 +181,9 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
inputMode="decimal"
value={vertrieb}
onChange={(e) => setVertrieb(e.target.value)}
className={numCls}
disabled={!vertriebFrei}
title={vertriebFrei ? undefined : 'Wird automatisch aus der Reisezeit berechnet (< 2 h)'}
className={`${numCls} disabled:bg-gray-100 disabled:text-gray-500`}
/>
</div>
</div>
+17
View File
@@ -43,6 +43,23 @@ export function hatZeiten(beginn: string, ende: string): boolean {
return parseHM(beginn) !== null && parseHM(ende) !== null;
}
/** True, wenn die Vertriebsunterstützung bei dieser Reisezeit frei änderbar ist (Reisezeit ≥ 2h). */
export function vertriebEditierbar(reisezeit: number): boolean {
return reisezeit >= 2;
}
/**
* Vertriebsunterstützung an die Reisezeit koppeln:
* Reisezeit = 0 → 0
* 0 < Reisezeit < 2 → = Reisezeit
* Reisezeit ≥ 2 → vorhandener Wert (änderbar), Standard 2
*/
export function applyVertriebsregel(reisezeit: number, vorhanden: number): number {
if (reisezeit <= 0) return 0;
if (reisezeit < 2) return reisezeit;
return vorhanden;
}
/** 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 ?? '');
+5 -2
View File
@@ -1,5 +1,5 @@
import { ORTE, type ArbeitszeitInput, type Ort } from '@/types/arbeitszeit';
import { parseHM } from './calc';
import { parseHM, applyVertriebsregel } from './calc';
/**
* Validiert und normalisiert einen JSON-Body zu einem ArbeitszeitInput.
@@ -25,10 +25,13 @@ export function parseArbeitszeitInput(body: unknown): ArbeitszeitInput {
}
const reisezeit = toNumber(b.reisezeit, 0);
const vertriebsunterstuetzung = toNumber(b.vertriebsunterstuetzung, 2);
let vertriebsunterstuetzung = toNumber(b.vertriebsunterstuetzung, 2);
if (reisezeit < 0 || vertriebsunterstuetzung < 0) {
throw new Error('Zeitwerte dürfen nicht negativ sein.');
}
// Regel: Vertriebsunterstützung an die Reisezeit koppeln.
// 0h ⇒ 0; <2h ⇒ = Reisezeit; ≥2h ⇒ bereitgestellter Wert (änderbar, default 2).
vertriebsunterstuetzung = applyVertriebsregel(reisezeit, vertriebsunterstuetzung);
const kommentar = String(b.kommentar ?? '').slice(0, 500);