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:
@@ -48,10 +48,16 @@ und liefern 401 ohne Session.
|
|||||||
## Datenmodell & Berechnungen
|
## Datenmodell & Berechnungen
|
||||||
|
|
||||||
Tabelle `arbeitszeit`: `datum`, `ort` (Kunde FFM | Homeoffice | Andrena), `beginn`, `ende`
|
Tabelle `arbeitszeit`: `datum`, `ort` (Kunde FFM | Homeoffice | Andrena), `beginn`, `ende`
|
||||||
(je HH:MM), `reisezeit`, `vertriebsunterstuetzung` (default 2), `kommentar` (max. 500).
|
(je HH:MM), `reise_beginn`, `reise_ende` (je HH:MM, optional), `pause` (dezimale Stunden),
|
||||||
|
`vertriebsunterstuetzung` (default 2), `kommentar` (max. 500).
|
||||||
|
|
||||||
- `Arbeitszeit = Ende − Beginn` (dezimale Stunden)
|
- `Arbeitszeit = Ende − Beginn` (dezimale Stunden)
|
||||||
|
- `Reisezeit = Reise-Ende − Reise-Beginn`: im Formular werden Start/Ende eingegeben, die
|
||||||
|
Reisezeit wird wie Mehrarbeit zur Laufzeit berechnet (nicht gespeichert) und in Vorschau
|
||||||
|
und Liste angezeigt. Beim Speichern koppelt sie nur die Vertriebsunterstützung.
|
||||||
- `Mehrarbeit = Arbeitszeit − 8 − Pause`
|
- `Mehrarbeit = Arbeitszeit − 8 − Pause`
|
||||||
- Die Spalte „Arbeitszeit Kunde" ist fix 8 h (`ARBEITSZEIT_KUNDE_FIX`).
|
- Die Spalte „Arbeitszeit Kunde" ist fix 8 h (`ARBEITSZEIT_KUNDE_FIX`).
|
||||||
- Die **Pause** ist eine globale Einstellung in der `settings`-Tabelle (Tab „Einstellungen").
|
- Die **Pause** wird pro Eintrag gespeichert (Spalte `pause`, dezimale Stunden; Eingabe im
|
||||||
Mehrarbeit wird zur Laufzeit berechnet, nicht gespeichert.
|
Formular in Minuten zwischen Beginn und Ende). Standard ist die globale Pause-Einstellung
|
||||||
|
in der `settings`-Tabelle (Tab „Einstellungen"). Mehrarbeit wird je Eintrag aus dieser
|
||||||
|
Pause zur Laufzeit berechnet, nicht gespeichert.
|
||||||
|
|||||||
+1
-2
@@ -121,7 +121,6 @@ export default function MainClient({ username }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ArbeitszeitList
|
<ArbeitszeitList
|
||||||
pause={pause}
|
|
||||||
refreshKey={refreshKey}
|
refreshKey={refreshKey}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
limit={10}
|
limit={10}
|
||||||
@@ -135,7 +134,7 @@ export default function MainClient({ username }: Props) {
|
|||||||
{/* Liste-Tab */}
|
{/* Liste-Tab */}
|
||||||
{activeTab === 'liste' && (
|
{activeTab === 'liste' && (
|
||||||
<div className="border-2 border-gray-400 rounded-xl bg-white p-3 print:border-0 print:rounded-none print:p-0">
|
<div className="border-2 border-gray-400 rounded-xl bg-white p-3 print:border-0 print:rounded-none print:p-0">
|
||||||
<ArbeitszeitList pause={pause} refreshKey={refreshKey} onEdit={handleEdit} limit={100} />
|
<ArbeitszeitList refreshKey={refreshKey} onEdit={handleEdit} limit={100} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
|
|||||||
const [ort, setOrt] = useState<string>(editEntry?.ort ?? ORTE[0]);
|
const [ort, setOrt] = useState<string>(editEntry?.ort ?? ORTE[0]);
|
||||||
const [beginn, setBeginn] = useState(editEntry?.beginn ?? '');
|
const [beginn, setBeginn] = useState(editEntry?.beginn ?? '');
|
||||||
const [ende, setEnde] = useState(editEntry?.ende ?? '');
|
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 [vertrieb, setVertrieb] = useState(editEntry ? numToStr(editEntry.vertriebsunterstuetzung) : '0');
|
||||||
const [kommentar, setKommentar] = useState(editEntry?.kommentar ?? '');
|
const [kommentar, setKommentar] = useState(editEntry?.kommentar ?? '');
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -53,21 +58,31 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
|
|||||||
|
|
||||||
const zeiten = hatZeiten(beginn, ende);
|
const zeiten = hatZeiten(beginn, ende);
|
||||||
const arbeitszeit = toDecimalHours(beginn, ende);
|
const arbeitszeit = toDecimalHours(beginn, ende);
|
||||||
const mehr = mehrarbeit(arbeitszeit, pause);
|
const pauseStd = Math.max(0, toNum(pauseMin)) / 60;
|
||||||
const vertriebFrei = vertriebEditierbar(toNum(reisezeit));
|
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.
|
// =0 ⇒ 0, <2 ⇒ = Reisezeit, ≥2 ⇒ 2 beim Überschreiten der Schwelle, danach frei änderbar.
|
||||||
function handleReisezeit(raw: string) {
|
function koppleVertrieb(altReise: number, neuReise: number) {
|
||||||
const alt = toNum(reisezeit);
|
if (neuReise <= 0) setVertrieb('0');
|
||||||
const neu = toNum(raw);
|
else if (neuReise < 2) setVertrieb(numToStr(neuReise));
|
||||||
setReisezeit(raw);
|
else if (altReise < 2) setVertrieb('2'); // Schwelle 2h überschritten → Standard 2h
|
||||||
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
|
// 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) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
@@ -84,6 +99,17 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
|
|||||||
return;
|
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);
|
setSaving(true);
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
@@ -91,7 +117,9 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
|
|||||||
ort,
|
ort,
|
||||||
beginn,
|
beginn,
|
||||||
ende,
|
ende,
|
||||||
reisezeit: toNum(reisezeit),
|
reise_beginn: reiseBeginn,
|
||||||
|
reise_ende: reiseEnde,
|
||||||
|
pause: pauseStd,
|
||||||
vertriebsunterstuetzung: toNum(vertrieb),
|
vertriebsunterstuetzung: toNum(vertrieb),
|
||||||
kommentar,
|
kommentar,
|
||||||
};
|
};
|
||||||
@@ -111,6 +139,18 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
|
|||||||
}
|
}
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
setTimeout(() => setSuccess(false), 4000);
|
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();
|
onSaved();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Fehler beim Speichern.');
|
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>
|
<label className={labelCls}>Beginn</label>
|
||||||
<TimeInput value={beginn} onChange={setBeginn} className="w-24" />
|
<TimeInput value={beginn} onChange={setBeginn} className="w-24" />
|
||||||
</div>
|
</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">
|
<div className="shrink-0">
|
||||||
<label className={labelCls}>Ende</label>
|
<label className={labelCls}>Ende</label>
|
||||||
<TimeInput value={ende} onChange={setEnde} className="w-24" />
|
<TimeInput value={ende} onChange={setEnde} className="w-24" />
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<label className={labelCls}>Reisezeit (h)</label>
|
<label className={labelCls}>Reise-Beginn</label>
|
||||||
<input
|
<TimeInput value={reiseBeginn} onChange={handleReiseBeginn} className="w-24" />
|
||||||
type="text"
|
</div>
|
||||||
inputMode="decimal"
|
<div className="shrink-0">
|
||||||
value={reisezeit}
|
<label className={labelCls}>Reise-Ende</label>
|
||||||
onChange={(e) => handleReisezeit(e.target.value)}
|
<TimeInput value={reiseEnde} onChange={handleReiseEnde} className="w-24" />
|
||||||
className={numCls}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<label className={labelCls}>Vertriebsunterst. (h)</label>
|
<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>
|
Arbeitszeit Kunde: <strong>{zeiten ? `${formatHours(ARBEITSZEIT_KUNDE_FIX)} h` : '—'}</strong>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Pause: <strong>{formatHours(pause)} h</strong>
|
Pause: <strong>{formatHours(pauseStd)} h</strong>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Mehrarbeit:{' '}
|
Mehrarbeit:{' '}
|
||||||
@@ -220,6 +268,9 @@ export default function ArbeitszeitForm({ editEntry, pause, onSaved, onCancel }:
|
|||||||
{zeiten ? `${formatHours(mehr)} h` : '—'}
|
{zeiten ? `${formatHours(mehr)} h` : '—'}
|
||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
|
<span>
|
||||||
|
Reisezeit: <strong>{reiseZeiten ? `${formatHours(reisezeit)} h` : '—'}</strong>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { ARBEITSZEIT_KUNDE_FIX, type ArbeitszeitEintrag } from '@/types/arbeitsz
|
|||||||
import { toDecimalHours, mehrarbeit, weekdayName, formatHours, hatZeiten } from '@/lib/calc';
|
import { toDecimalHours, mehrarbeit, weekdayName, formatHours, hatZeiten } from '@/lib/calc';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pause: number;
|
|
||||||
refreshKey: number;
|
refreshKey: number;
|
||||||
onEdit: (entry: ArbeitszeitEintrag) => void;
|
onEdit: (entry: ArbeitszeitEintrag) => void;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
@@ -36,7 +35,7 @@ function formatDate(d: string) {
|
|||||||
return m ? `${m[3]}.${m[2]}.${m[1]}` : d;
|
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 [entries, setEntries] = useState<ArbeitszeitEintrag[]>([]);
|
||||||
const [month, setMonth] = useState(compact ? '' : currentMonth());
|
const [month, setMonth] = useState(compact ? '' : currentMonth());
|
||||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||||
@@ -132,7 +131,8 @@ export default function ArbeitszeitList({ pause, refreshKey, onEdit, limit = 15,
|
|||||||
</tr>
|
</tr>
|
||||||
) : entries.map((e) => {
|
) : entries.map((e) => {
|
||||||
const zeiten = hatZeiten(e.beginn, e.ende);
|
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 (
|
return (
|
||||||
<tr key={e.id} className="hover:bg-gray-50">
|
<tr key={e.id} className="hover:bg-gray-50">
|
||||||
<td className={`${cell} whitespace-nowrap`}>{formatDate(e.datum)}</td>
|
<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}>{e.ort}</td>
|
||||||
<td className={`${cell} text-right`}>{zeiten ? formatHours(ARBEITSZEIT_KUNDE_FIX) : '—'}</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 ${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} text-right`}>{e.vertriebsunterstuetzung ? formatHours(e.vertriebsunterstuetzung) : '—'}</td>
|
||||||
<td className={cell}>{e.kommentar}</td>
|
<td className={cell}>{e.kommentar}</td>
|
||||||
<td className={`${cell} text-center whitespace-nowrap print:hidden`}>
|
<td className={`${cell} text-center whitespace-nowrap print:hidden`}>
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export function getDb(): Database.Database {
|
|||||||
ort TEXT NOT NULL,
|
ort TEXT NOT NULL,
|
||||||
beginn TEXT NOT NULL,
|
beginn TEXT NOT NULL,
|
||||||
ende TEXT NOT NULL,
|
ende TEXT NOT NULL,
|
||||||
reisezeit REAL NOT NULL DEFAULT 0,
|
reise_beginn TEXT NOT NULL DEFAULT '',
|
||||||
|
reise_ende TEXT NOT NULL DEFAULT '',
|
||||||
|
pause REAL NOT NULL DEFAULT 0.5,
|
||||||
vertriebsunterstuetzung REAL NOT NULL DEFAULT 2,
|
vertriebsunterstuetzung REAL NOT NULL DEFAULT 2,
|
||||||
kommentar TEXT NOT NULL DEFAULT '',
|
kommentar TEXT NOT NULL DEFAULT '',
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
@@ -49,11 +51,37 @@ export function getDb(): Database.Database {
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Migration: Reise-Start/-Ende für bestehende Datenbanken ergänzen.
|
||||||
|
const cols = (db.prepare(`PRAGMA table_info(arbeitszeit)`).all() as { name: string }[]).map(
|
||||||
|
(c) => c.name
|
||||||
|
);
|
||||||
|
if (!cols.includes('reise_beginn')) {
|
||||||
|
db.exec(`ALTER TABLE arbeitszeit ADD COLUMN reise_beginn TEXT NOT NULL DEFAULT ''`);
|
||||||
|
}
|
||||||
|
if (!cols.includes('reise_ende')) {
|
||||||
|
db.exec(`ALTER TABLE arbeitszeit ADD COLUMN reise_ende TEXT NOT NULL DEFAULT ''`);
|
||||||
|
}
|
||||||
|
// Reisezeit wird zur Laufzeit aus reise_beginn/reise_ende berechnet, nicht mehr gespeichert.
|
||||||
|
if (cols.includes('reisezeit')) {
|
||||||
|
db.exec(`ALTER TABLE arbeitszeit DROP COLUMN reisezeit`);
|
||||||
|
}
|
||||||
|
|
||||||
// Standard-Pause beim ersten Start setzen (0,5 h)
|
// Standard-Pause beim ersten Start setzen (0,5 h)
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT INTO settings (key, value) VALUES ('pause', '0.5')
|
`INSERT INTO settings (key, value) VALUES ('pause', '0.5')
|
||||||
ON CONFLICT(key) DO NOTHING`
|
ON CONFLICT(key) DO NOTHING`
|
||||||
).run();
|
).run();
|
||||||
|
|
||||||
|
// Migration: Pause pro Eintrag ergänzen. Bestehende Zeilen bekommen die aktuelle
|
||||||
|
// globale Pause-Einstellung als Wert.
|
||||||
|
if (!cols.includes('pause')) {
|
||||||
|
const row = db.prepare(`SELECT value FROM settings WHERE key = 'pause'`).get() as
|
||||||
|
| { value: string }
|
||||||
|
| undefined;
|
||||||
|
const globalPause = row ? parseFloat(row.value) : 0.5;
|
||||||
|
const def = isNaN(globalPause) ? 0.5 : globalPause;
|
||||||
|
db.exec(`ALTER TABLE arbeitszeit ADD COLUMN pause REAL NOT NULL DEFAULT ${def}`);
|
||||||
|
}
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -43,8 +43,8 @@ export function createArbeitszeit(input: ArbeitszeitInput): ArbeitszeitEintrag {
|
|||||||
const db = getDb();
|
const db = getDb();
|
||||||
const info = db
|
const info = db
|
||||||
.prepare(
|
.prepare(
|
||||||
`INSERT INTO arbeitszeit (datum, ort, beginn, ende, reisezeit, vertriebsunterstuetzung, kommentar)
|
`INSERT INTO arbeitszeit (datum, ort, beginn, ende, reise_beginn, reise_ende, pause, vertriebsunterstuetzung, kommentar)
|
||||||
VALUES (@datum, @ort, @beginn, @ende, @reisezeit, @vertriebsunterstuetzung, @kommentar)`
|
VALUES (@datum, @ort, @beginn, @ende, @reise_beginn, @reise_ende, @pause, @vertriebsunterstuetzung, @kommentar)`
|
||||||
)
|
)
|
||||||
.run(input);
|
.run(input);
|
||||||
return getArbeitszeit(Number(info.lastInsertRowid))!;
|
return getArbeitszeit(Number(info.lastInsertRowid))!;
|
||||||
@@ -58,7 +58,8 @@ export function updateArbeitszeit(
|
|||||||
db.prepare(
|
db.prepare(
|
||||||
`UPDATE arbeitszeit SET
|
`UPDATE arbeitszeit SET
|
||||||
datum = @datum, ort = @ort, beginn = @beginn, ende = @ende,
|
datum = @datum, ort = @ort, beginn = @beginn, ende = @ende,
|
||||||
reisezeit = @reisezeit, vertriebsunterstuetzung = @vertriebsunterstuetzung,
|
reise_beginn = @reise_beginn, reise_ende = @reise_ende,
|
||||||
|
pause = @pause, vertriebsunterstuetzung = @vertriebsunterstuetzung,
|
||||||
kommentar = @kommentar
|
kommentar = @kommentar
|
||||||
WHERE id = @id`
|
WHERE id = @id`
|
||||||
).run({ ...input, id });
|
).run({ ...input, id });
|
||||||
|
|||||||
+21
-4
@@ -1,5 +1,6 @@
|
|||||||
import { ORTE, type ArbeitszeitInput, type Ort } from '@/types/arbeitszeit';
|
import { ORTE, type ArbeitszeitInput, type Ort } from '@/types/arbeitszeit';
|
||||||
import { parseHM, applyVertriebsregel } from './calc';
|
import { parseHM, applyVertriebsregel, toDecimalHours } from './calc';
|
||||||
|
import { getPause } from './repo';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validiert und normalisiert einen JSON-Body zu einem ArbeitszeitInput.
|
* Validiert und normalisiert einen JSON-Body zu einem ArbeitszeitInput.
|
||||||
@@ -24,9 +25,25 @@ export function parseArbeitszeitInput(body: unknown): ArbeitszeitInput {
|
|||||||
throw new Error('Bitte Start- und Endzeit gemeinsam angeben oder beide leer lassen.');
|
throw new Error('Bitte Start- und Endzeit gemeinsam angeben oder beide leer lassen.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const reisezeit = toNumber(b.reisezeit, 0);
|
// Reise-Start/-Ende sind optional (analog Beginn/Ende). Wenn angegeben,
|
||||||
|
// müssen sie gültig sein; entweder beide leer oder beide gesetzt.
|
||||||
|
const reise_beginn = String(b.reise_beginn ?? '');
|
||||||
|
const reise_ende = String(b.reise_ende ?? '');
|
||||||
|
if (reise_beginn !== '' && parseHM(reise_beginn) === null)
|
||||||
|
throw new Error('Ungültige Reise-Startzeit.');
|
||||||
|
if (reise_ende !== '' && parseHM(reise_ende) === null)
|
||||||
|
throw new Error('Ungültige Reise-Endzeit.');
|
||||||
|
if ((reise_beginn === '') !== (reise_ende === '')) {
|
||||||
|
throw new Error('Bitte Reise-Start und -Ende gemeinsam angeben oder beide leer lassen.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reisezeit wird zur Laufzeit aus den Reise-Zeiten berechnet (nicht gespeichert),
|
||||||
|
// dient hier aber zur Kopplung der Vertriebsunterstützung.
|
||||||
|
const reisezeit = toDecimalHours(reise_beginn, reise_ende);
|
||||||
|
// Pause pro Eintrag (dezimale Stunden). Standard ist die globale Einstellung.
|
||||||
|
const pause = toNumber(b.pause, getPause());
|
||||||
let vertriebsunterstuetzung = toNumber(b.vertriebsunterstuetzung, 2);
|
let vertriebsunterstuetzung = toNumber(b.vertriebsunterstuetzung, 2);
|
||||||
if (reisezeit < 0 || vertriebsunterstuetzung < 0) {
|
if (pause < 0 || vertriebsunterstuetzung < 0) {
|
||||||
throw new Error('Zeitwerte dürfen nicht negativ sein.');
|
throw new Error('Zeitwerte dürfen nicht negativ sein.');
|
||||||
}
|
}
|
||||||
// Regel: Vertriebsunterstützung an die Reisezeit koppeln.
|
// Regel: Vertriebsunterstützung an die Reisezeit koppeln.
|
||||||
@@ -35,7 +52,7 @@ export function parseArbeitszeitInput(body: unknown): ArbeitszeitInput {
|
|||||||
|
|
||||||
const kommentar = String(b.kommentar ?? '').slice(0, 500);
|
const kommentar = String(b.kommentar ?? '').slice(0, 500);
|
||||||
|
|
||||||
return { datum, ort, beginn, ende, reisezeit, vertriebsunterstuetzung, kommentar };
|
return { datum, ort, beginn, ende, reise_beginn, reise_ende, pause, vertriebsunterstuetzung, kommentar };
|
||||||
}
|
}
|
||||||
|
|
||||||
function toNumber(value: unknown, fallback: number): number {
|
function toNumber(value: unknown, fallback: number): number {
|
||||||
|
|||||||
Generated
+3
-26
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "arbeitszeit",
|
"name": "arbeitszeit",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "arbeitszeit",
|
"name": "arbeitszeit",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^13.3.0",
|
"@simplewebauthn/browser": "^13.3.0",
|
||||||
"@simplewebauthn/server": "^13.3.1",
|
"@simplewebauthn/server": "^13.3.1",
|
||||||
@@ -282,30 +282,6 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
|
||||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/wasi-threads": "1.2.1",
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/runtime": {
|
|
||||||
"version": "1.11.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
|
|
||||||
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
@@ -3691,6 +3667,7 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "arbeitszeit",
|
"name": "arbeitszeit",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ export interface ArbeitszeitEintrag {
|
|||||||
ort: Ort;
|
ort: Ort;
|
||||||
beginn: string; // HH:MM
|
beginn: string; // HH:MM
|
||||||
ende: string; // HH:MM
|
ende: string; // HH:MM
|
||||||
reisezeit: number; // dezimale Stunden
|
reise_beginn: string; // HH:MM, leer wenn keine Reise
|
||||||
|
reise_ende: string; // HH:MM, leer wenn keine Reise
|
||||||
|
pause: number; // dezimale Stunden (Eingabe im Formular in Minuten)
|
||||||
vertriebsunterstuetzung: number; // dezimale Stunden
|
vertriebsunterstuetzung: number; // dezimale Stunden
|
||||||
kommentar: string;
|
kommentar: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user