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:
@@ -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 && (
|
||||
|
||||
@@ -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`}>
|
||||
|
||||
Reference in New Issue
Block a user