Reisezeit aus Start/Ende, Pause pro Eintrag in Minuten

- Reisezeit wird aus Reise-Beginn/-Ende zur Laufzeit berechnet (Spalte reisezeit entfernt),
  Anzeige in Vorschau und Liste wie Mehrarbeit
- Pause pro Eintrag in Minuten (Feld zwischen Beginn und Ende), Default aus globaler Einstellung
- Eingabefelder werden nach dem Speichern eines neuen Eintrags geleert
- DB-Migrationen für reise_beginn/reise_ende/pause-Spalten
- Version 0.1.2

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 17:00:57 +02:00
parent 74eedf7c3d
commit cb168fbd18
10 changed files with 147 additions and 66 deletions
+72 -21
View File
@@ -44,7 +44,12 @@ 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 ? numToStr(editEntry.reisezeit) : '0');
// Pause pro Eintrag in Minuten; Standard ist die globale Einstellung (Stunden → Minuten).
const [pauseMin, setPauseMin] = useState(
String(Math.round((editEntry?.pause ?? pause) * 60))
);
const [reiseBeginn, setReiseBeginn] = useState(editEntry?.reise_beginn ?? '');
const [reiseEnde, setReiseEnde] = useState(editEntry?.reise_ende ?? '');
const [vertrieb, setVertrieb] = useState(editEntry ? numToStr(editEntry.vertriebsunterstuetzung) : '0');
const [kommentar, setKommentar] = useState(editEntry?.kommentar ?? '');
const [saving, setSaving] = useState(false);
@@ -53,21 +58,31 @@ 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));
const pauseStd = Math.max(0, toNum(pauseMin)) / 60;
const mehr = mehrarbeit(arbeitszeit, pauseStd);
const reisezeit = toDecimalHours(reiseBeginn, reiseEnde);
const reiseZeiten = hatZeiten(reiseBeginn, reiseEnde);
const vertriebFrei = vertriebEditierbar(reisezeit);
// Vertriebsunterstützung an die Reisezeit koppeln (siehe applyVertriebsregel):
// Vertriebsunterstützung an die (aus den Reise-Zeiten berechnete) 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
function koppleVertrieb(altReise: number, neuReise: number) {
if (neuReise <= 0) setVertrieb('0');
else if (neuReise < 2) setVertrieb(numToStr(neuReise));
else if (altReise < 2) setVertrieb('2'); // Schwelle 2h überschritten → Standard 2h
// neu ≥ 2 und alt ≥ 2: bestehenden (ggf. geänderten) Wert beibehalten
}
function handleReiseBeginn(raw: string) {
koppleVertrieb(reisezeit, toDecimalHours(raw, reiseEnde));
setReiseBeginn(raw);
}
function handleReiseEnde(raw: string) {
koppleVertrieb(reisezeit, toDecimalHours(reiseBeginn, raw));
setReiseEnde(raw);
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError('');
@@ -84,6 +99,17 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
return;
}
const reiseBeginnLeer = reiseBeginn === '';
const reiseEndeLeer = reiseEnde === '';
if (reiseBeginnLeer !== reiseEndeLeer) {
setError('Bitte Reise-Start und -Ende gemeinsam angeben oder beide leer lassen.');
return;
}
if (!reiseBeginnLeer && !reiseEndeLeer && reisezeit <= 0) {
setError('Reise-Ende muss nach dem Reise-Start liegen.');
return;
}
setSaving(true);
const body = {
@@ -91,7 +117,9 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
ort,
beginn,
ende,
reisezeit: toNum(reisezeit),
reise_beginn: reiseBeginn,
reise_ende: reiseEnde,
pause: pauseStd,
vertriebsunterstuetzung: toNum(vertrieb),
kommentar,
};
@@ -111,6 +139,18 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
}
setSuccess(true);
setTimeout(() => setSuccess(false), 4000);
// Beim Anlegen die Eingabefelder wieder auf die Startwerte zurücksetzen.
if (!editEntry) {
setDatum(todayDate());
setOrt(ORTE[0]);
setBeginn('');
setEnde('');
setPauseMin(String(Math.round(pause * 60)));
setReiseBeginn('');
setReiseEnde('');
setVertrieb('0');
setKommentar('');
}
onSaved();
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Speichern.');
@@ -160,19 +200,27 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
<label className={labelCls}>Beginn</label>
<TimeInput value={beginn} onChange={setBeginn} className="w-24" />
</div>
<div className="shrink-0">
<label className={labelCls}>Pause (min)</label>
<input
type="text"
inputMode="numeric"
value={pauseMin}
onChange={(e) => setPauseMin(e.target.value.replace(/[^0-9]/g, ''))}
className={numCls}
/>
</div>
<div className="shrink-0">
<label className={labelCls}>Ende</label>
<TimeInput value={ende} onChange={setEnde} className="w-24" />
</div>
<div className="shrink-0">
<label className={labelCls}>Reisezeit (h)</label>
<input
type="text"
inputMode="decimal"
value={reisezeit}
onChange={(e) => handleReisezeit(e.target.value)}
className={numCls}
/>
<label className={labelCls}>Reise-Beginn</label>
<TimeInput value={reiseBeginn} onChange={handleReiseBeginn} className="w-24" />
</div>
<div className="shrink-0">
<label className={labelCls}>Reise-Ende</label>
<TimeInput value={reiseEnde} onChange={handleReiseEnde} className="w-24" />
</div>
<div className="shrink-0">
<label className={labelCls}>Vertriebsunterst. (h)</label>
@@ -212,7 +260,7 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
Arbeitszeit Kunde: <strong>{zeiten ? `${formatHours(ARBEITSZEIT_KUNDE_FIX)} h` : '—'}</strong>
</span>
<span>
Pause: <strong>{formatHours(pause)} h</strong>
Pause: <strong>{formatHours(pauseStd)} h</strong>
</span>
<span>
Mehrarbeit:{' '}
@@ -220,6 +268,9 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
{zeiten ? `${formatHours(mehr)} h` : '—'}
</strong>
</span>
<span>
Reisezeit: <strong>{reiseZeiten ? `${formatHours(reisezeit)} h` : '—'}</strong>
</span>
</div>
{error && (
+4 -4
View File
@@ -5,7 +5,6 @@ import { ARBEITSZEIT_KUNDE_FIX, type ArbeitszeitEintrag } from '@/types/arbeitsz
import { toDecimalHours, mehrarbeit, weekdayName, formatHours, hatZeiten } from '@/lib/calc';
interface Props {
pause: number;
refreshKey: number;
onEdit: (entry: ArbeitszeitEintrag) => void;
limit?: number;
@@ -36,7 +35,7 @@ function formatDate(d: string) {
return m ? `${m[3]}.${m[2]}.${m[1]}` : d;
}
export default function ArbeitszeitList({ pause, refreshKey, onEdit, limit = 15, compact = false }: Props) {
export default function ArbeitszeitList({ refreshKey, onEdit, limit = 15, compact = false }: Props) {
const [entries, setEntries] = useState<ArbeitszeitEintrag[]>([]);
const [month, setMonth] = useState(compact ? '' : currentMonth());
const [deleteId, setDeleteId] = useState<number | null>(null);
@@ -132,7 +131,8 @@ export default function ArbeitszeitList({ pause, refreshKey, onEdit, limit = 15,
</tr>
) : entries.map((e) => {
const zeiten = hatZeiten(e.beginn, e.ende);
const mehr = mehrarbeit(toDecimalHours(e.beginn, e.ende), pause);
const mehr = mehrarbeit(toDecimalHours(e.beginn, e.ende), e.pause);
const reisezeit = toDecimalHours(e.reise_beginn, e.reise_ende);
return (
<tr key={e.id} className="hover:bg-gray-50">
<td className={`${cell} whitespace-nowrap`}>{formatDate(e.datum)}</td>
@@ -140,7 +140,7 @@ export default function ArbeitszeitList({ pause, refreshKey, onEdit, limit = 15,
<td className={cell}>{e.ort}</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`}>{e.reisezeit ? formatHours(e.reisezeit) : '—'}</td>
<td className={`${cell} text-right`}>{reisezeit ? formatHours(reisezeit) : '—'}</td>
<td className={`${cell} text-right`}>{e.vertriebsunterstuetzung ? formatHours(e.vertriebsunterstuetzung) : '—'}</td>
<td className={cell}>{e.kommentar}</td>
<td className={`${cell} text-center whitespace-nowrap print:hidden`}>