Arbeitszeit: Arbeitstag beginnen, beenden, aendern und loeschen

beginneArbeitstag legt per saveSchicht.php eine laufende Schicht fuer
beliebige Mitarbeiter an (der Menuepunkt "Arbeitstag beginnen" wirkt nur
fuer den angemeldeten Benutzer). Damit kann auch eine Pause die erste
Buchung des Tages sein. Zeitbuchungen lassen laufende Schichten offen
und ziehen nur den Beginn bei Bedarf vor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bjoernpoettkerandClaude Opus 5.5 committed 2026-10-14 11:54:58 +02:00
1 parent d703e46125
commit 659fdb5293
11 files changed
+373 -24

No files matched your search

+93 -2
View File
@@ -135,7 +135,8 @@ function createServer() {
id,
mitarbeiter_id: s.mitarbeiterId,
timestamp_start: `${s.start}:00`,
timestamp_ende: `${s.ende}:00`,
// Laufende Schicht: Agrarmonitor liefert ein Null-Datum.
timestamp_ende: s.ende === null ? '0000-00-00 00:00:00' : `${s.ende}:00`,
ist_deleted: s.deleted ? 1 : 0,
});
return;
@@ -238,11 +239,30 @@ function createServer() {
case '/module/arbeitszeit/api/updateSchicht.php': {
const s = state.schichten.get(daten.shiftID);
s.start = formularZuVergleich(daten.timestampStart);
s.ende = formularZuVergleich(daten.timestampEnd);
s.ende = daten.timestampEnd === '' ? null : formularZuVergleich(daten.timestampEnd);
json(res, 200, true);
return;
}
case '/module/auftraege/api/saveSchicht.php': {
const id = neueId();
const start = formularZuVergleich(daten.formdata.timestamp_start);
state.schichten.set(id, {
mitarbeiterId: daten.mitarbeiter_id,
datum: start.slice(0, 10),
start,
ende: daten.formdata.timestamp_ende === '' ? null : formularZuVergleich(daten.formdata.timestamp_ende),
});
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(String(id));
return;
}
case '/module/arbeitszeit/api/deleteSchicht.php':
state.schichten.get(daten.shiftID).deleted = true;
json(res, 200, { action: 'link', target: 'arbeitszeit' });
return;
case '/module/notiz/api/delete.php':
for (const v of state.verteilungen.values()) {
v.notizen = v.notizen.filter(n => n.id !== daten.noteID);
@@ -622,3 +642,74 @@ test('createZeitbuchung meldet sich bei abgelaufener Session neu an', async t =>
assert.match(ref, /^verteilung:3309:2026-10-14:\d+$/);
assert.equal(state.verteilungen.size, 1);
});
test('beginneArbeitstag legt eine laufende Schicht an und liefert die vorhandene bei Wiederholung', async t => {
const { connector, requests, state } = await startConnector(t);
const { ref } = await connector.beginneArbeitstag(3309, new Date('2026-10-14T04:58:40Z'));
assert.deepEqual(posts(requests, '/module/auftraege/api/saveSchicht.php').map(r => r.body), [
{ vorgang_id: '-1', mitarbeiter_id: 3309, formdata: { timestamp_start: '14.10.26 (06:59)', timestamp_ende: '' } },
]);
const [[schichtId, schicht]] = [...state.schichten.entries()];
assert.equal(ref, `schicht:3309:2026-10-14:${schichtId}`);
assert.equal(schicht.ende, null, 'Schicht laeuft');
const wiederholt = await connector.beginneArbeitstag(3309, new Date('2026-10-14T04:58:40Z'));
assert.equal(wiederholt.ref, ref);
assert.equal(posts(requests, '/module/auftraege/api/saveSchicht.php').length, 1);
});
test('Pause und Taetigkeit in laufender Schicht lassen sie offen', async t => {
const { connector, requests, state } = await startConnector(t);
const { ref } = await connector.beginneArbeitstag(3309, new Date('2026-10-14T05:00:00Z'));
const schichtId = Number(ref.split(':').at(-1));
const pause = await connector.createZeitbuchung({
art: 'pause',
mitarbeiterId: 3309,
start: new Date('2026-10-14T05:00:00Z'),
ende: new Date('2026-10-14T05:15:00Z'),
});
assert.match(pause.ref, /^pause:3309:2026-10-14:\d+$/, 'Pause als erste Buchung des Tages');
// Block beginnt vor der Schicht: Start wird vorgezogen, Ende bleibt offen.
await connector.createZeitbuchung(block('2026-10-14T04:50:00Z', '2026-10-14T05:00:00Z'));
assert.equal(posts(requests, '/module/arbeitszeit/api/nachtragen.php').length, 0);
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updateSchicht.php').map(r => r.body), [
{ shiftID: schichtId, timestampStart: '14.10.26 (06:50)', timestampEnd: '' },
]);
assert.equal(state.schichten.get(schichtId).ende, null);
});
test('beendeArbeitstag, updateArbeitstag und deleteArbeitstag', async t => {
const { connector, requests, state } = await startConnector(t);
const { ref } = await connector.beginneArbeitstag(3309, new Date('2026-10-14T05:00:00Z'));
const schichtId = Number(ref.split(':').at(-1));
await connector.beendeArbeitstag(ref, new Date('2026-10-14T15:30:00Z'));
await connector.updateArbeitstag(ref, { start: new Date('2026-10-14T04:45:00Z'), ende: null });
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updateSchicht.php').map(r => r.body), [
{ shiftID: schichtId, timestampStart: '14.10.26 (07:00)', timestampEnd: '14.10.26 (17:30)' },
{ shiftID: schichtId, timestampStart: '14.10.26 (06:45)', timestampEnd: '' },
]);
await assert.rejects(
() => connector.beendeArbeitstag(ref, new Date('2026-10-14T04:00:00Z')),
error => error instanceof ZeitbuchungError && error.grund === 'dauer'
);
await assert.rejects(() => connector.beendeArbeitstag(`verteilung:3309:2026-10-14:${schichtId}`, new Date()), /Arbeitstag-Referenz/);
await connector.deleteArbeitstag(ref);
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/deleteSchicht.php')[0].body, {
shiftID: schichtId,
employeeID: 3309,
date: '2026-10-14',
});
assert.equal(state.schichten.get(schichtId).deleted, true);
await assert.rejects(() => connector.beendeArbeitstag(ref, new Date('2026-10-14T15:30:00Z')), /nicht gefunden/);
});