createZeitbuchung, updateZeitbuchung und deleteZeitbuchung buchen Taetigkeitsbloecke und Pausen ueber module/arbeitszeit/api/. Die erste Buchung eines Tages legt per nachtragen.php die Schicht an, weitere Buchungen kommen in die vorhandene Schicht, die bei Bedarf verlaengert wird. Die Kostenstelle wird an die Notiz gehaengt. Unbekannte Taetigkeiten werden vorher abgelehnt, weil Agrarmonitor sie ohne Fehler speichert. Ablehnungen kommen als ZeitbuchungError mit Grund. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
625 lines
24 KiB
JavaScript
625 lines
24 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const http = require('node:http');
|
|
const test = require('node:test');
|
|
|
|
const { AgrarmonitorConnector, MemoryCookieStore, ZeitbuchungError } = require('../dist');
|
|
|
|
const loginPage = `
|
|
<html>
|
|
<head><title>Anmeldung - AGRARMONITOR</title></head>
|
|
<body><form><input name="nonce" value="test-nonce" /><button>Einloggen</button></form></body>
|
|
</html>
|
|
`;
|
|
|
|
// "dd.mm.yy (HH:MM)" -> "YYYY-MM-DD HH:MM"
|
|
function formularZuVergleich(wert) {
|
|
const [, tag, monat, jahr, uhrzeit] = /^(\d{2})\.(\d{2})\.(\d{2}) \((\d{2}:\d{2})\)$/.exec(wert);
|
|
return `20${jahr}-${monat}-${tag} ${uhrzeit}`;
|
|
}
|
|
|
|
/**
|
|
* Nachbildung der Arbeitszeit-Endpunkte, so wie sie sich am 14.10.2026 im
|
|
* echten Agrarmonitor verhalten haben (siehe docs/zeitbuchung-analyse.md).
|
|
*/
|
|
function createServer() {
|
|
const requests = [];
|
|
const state = { schichten: new Map(), verteilungen: new Map(), pausen: new Map() };
|
|
let loggedIn = false;
|
|
let naechsteId = 100;
|
|
const neueId = () => (naechsteId += 1);
|
|
|
|
const json = (res, status, data) => {
|
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
res.end(data === undefined ? '' : JSON.stringify(data));
|
|
};
|
|
const html = (res, body) => {
|
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
res.end(body);
|
|
};
|
|
const schichtAufraeumen = schichtId => {
|
|
const leer = ![...state.verteilungen.values()].some(v => v.schichtId === schichtId);
|
|
if (leer) {
|
|
state.schichten.get(schichtId).deleted = true;
|
|
}
|
|
return leer;
|
|
};
|
|
|
|
const tagesansicht = (mitarbeiterId, datum) =>
|
|
[...state.schichten.entries()]
|
|
.filter(([, s]) => !s.deleted && s.mitarbeiterId === mitarbeiterId && s.datum === datum)
|
|
.map(([schichtId]) => {
|
|
const zeilen = [
|
|
...[...state.verteilungen.entries()]
|
|
.filter(([, v]) => v.schichtId === schichtId)
|
|
.map(([id, v]) => `<tr id="${id}" class=" betrieb "><td>${v.start.slice(11)} - ${v.ende.slice(11)}</td></tr>`),
|
|
...[...state.pausen.entries()]
|
|
.filter(([, p]) => p.schichtId === schichtId)
|
|
.map(([id, p]) => `<tr id="${id}" class="pause_betrieb text-orange"><td>${p.start.slice(11)} - ${p.ende.slice(11)}</td></tr>`),
|
|
];
|
|
return `<table class="alternate verteilung_table table-fixed" id="verteilung_table_${schichtId}" data-schicht="${schichtId}"><tbody>${zeilen.join('')}</tbody></table>`;
|
|
})
|
|
.join('');
|
|
|
|
// Buchbar sind nur aktive Taetigkeiten; 999 ist unbekannt.
|
|
const taetigkeitAuswahl = ausgewaehlt => `
|
|
<select name="taetigkeit_id" id="taetigkeit_id">
|
|
<option value="870"${ausgewaehlt === '870' ? ' selected="selected"' : ''}>110 Arbeiten</option>
|
|
<option value="869"${ausgewaehlt === '869' ? ' selected="selected"' : ''}>100 Reparatur</option>
|
|
</select>`;
|
|
|
|
const verteilungFormular = id => {
|
|
const v = state.verteilungen.get(id);
|
|
return `
|
|
<form id="verteilung_form" data-id="${id}">
|
|
<input type="text" name="timestamp_start" id="timestamp_start" value="">
|
|
<input type="hidden" name="alte_taetigkeit_id" id="alte_taetigkeit_id" value="${v.taetigkeitId}">
|
|
${taetigkeitAuswahl(v.taetigkeitId)}
|
|
<select name="projekt_id" id="projekt_id">
|
|
<option value="0">Kein Projekt</option>
|
|
<option value="12"${v.projektId === '12' ? ' selected="selected"' : ''}>Projekt</option>
|
|
</select>
|
|
</form>
|
|
<form id="notiz-form">
|
|
<textarea name="notizfeld" id="notizfeld"></textarea>
|
|
<input type="text" name="verfasser_id" id="verfasser_id" value="6">
|
|
</form>
|
|
<table id="notizen_table"><tbody>
|
|
${v.notizen.map(n => `<tr data-id="${n.id}"><td>14.10.26 (11:15)</td><td>Connector</td><td class="notiz">${n.text.replace(/\n/g, '<br />\n')}</td></tr>`).join('')}
|
|
</tbody></table>
|
|
`;
|
|
};
|
|
|
|
const server = http.createServer((req, res) => {
|
|
let body = '';
|
|
req.on('data', chunk => {
|
|
body += chunk;
|
|
});
|
|
req.on('end', () => {
|
|
const url = new URL(req.url, 'http://localhost');
|
|
const istJson = (req.headers['content-type'] ?? '').includes('application/json');
|
|
const daten = istJson && body ? JSON.parse(body) : body;
|
|
requests.push({ method: req.method, path: url.pathname, body: daten });
|
|
|
|
if (req.method === 'POST' && url.pathname === '/login/api/login.php') {
|
|
loggedIn = true;
|
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Set-Cookie': 'agrar_session=test; Path=/' });
|
|
res.end(JSON.stringify({ success: true }));
|
|
return;
|
|
}
|
|
|
|
if (!loggedIn) {
|
|
html(res, loginPage);
|
|
return;
|
|
}
|
|
|
|
const detail = /^\/arbeitszeit\/detail\/(\d+)\/(\d{4}-\d{2}-\d{2})$/.exec(url.pathname);
|
|
|
|
if (detail) {
|
|
html(res, `<html><body>${tagesansicht(Number(detail[1]), detail[2])}</body></html>`);
|
|
return;
|
|
}
|
|
|
|
switch (url.pathname) {
|
|
case '/':
|
|
html(res, '<html><body>Mein AM</body></html>');
|
|
return;
|
|
|
|
case '/module/arbeitszeit/api/nachtragen-form.php':
|
|
html(res, `<form id="nachtragen_form">${taetigkeitAuswahl('0')}</form>`);
|
|
return;
|
|
|
|
case '/module/arbeitszeit/api/getSchicht.php': {
|
|
const id = Number(url.searchParams.get('schicht_id'));
|
|
const s = state.schichten.get(id);
|
|
json(res, 200, {
|
|
id,
|
|
mitarbeiter_id: s.mitarbeiterId,
|
|
timestamp_start: `${s.start}:00`,
|
|
timestamp_ende: `${s.ende}:00`,
|
|
ist_deleted: s.deleted ? 1 : 0,
|
|
});
|
|
return;
|
|
}
|
|
|
|
case '/module/arbeitszeit/api/nachtragen.php': {
|
|
const params = new URLSearchParams(body);
|
|
const datum = params.get('tage[]');
|
|
const schichtId = neueId();
|
|
const start = `${datum} ${params.get('form[arbeitszeit_start]')}`;
|
|
const ende = `${datum} ${params.get('form[arbeitszeit_ende]')}`;
|
|
state.schichten.set(schichtId, { mitarbeiterId: Number(params.get('form[mitarbeiter_id]')), datum, start, ende });
|
|
const notiz = params.get('form[notiz]');
|
|
state.verteilungen.set(neueId(), {
|
|
schichtId,
|
|
start,
|
|
ende,
|
|
taetigkeitId: params.get('form[taetigkeit_id]'),
|
|
maschinenId: params.get('form[maschine_id]'),
|
|
projektId: '0',
|
|
notizen: notiz ? [{ id: neueId(), text: notiz }] : [],
|
|
});
|
|
json(res, 200, { action: 'link', target: 'arbeitszeit/plan' });
|
|
return;
|
|
}
|
|
|
|
case '/module/arbeitszeit/api/editVerteilungForm.php': {
|
|
let id = Number(daten.distributionID);
|
|
if (daten.shiftID) {
|
|
// Wie im echten System: Formular fuer "neu" legt sofort einen Platzhalter an.
|
|
const s = state.schichten.get(daten.shiftID);
|
|
id = neueId();
|
|
state.verteilungen.set(id, {
|
|
schichtId: daten.shiftID,
|
|
start: s.start,
|
|
ende: s.start,
|
|
taetigkeitId: '0',
|
|
maschinenId: '0',
|
|
projektId: '0',
|
|
notizen: [],
|
|
});
|
|
}
|
|
html(res, state.verteilungen.has(id) ? verteilungFormular(id) : '');
|
|
return;
|
|
}
|
|
|
|
case '/module/arbeitszeit/api/updateVerteilung.php': {
|
|
const form = daten.verteilungForm;
|
|
if (form.ganzertag === undefined) {
|
|
json(res, 422, { verteilungForm: 'ganzertag must be present' });
|
|
return;
|
|
}
|
|
if (form.maschine_id === '999') {
|
|
json(res, 422, { verteilungForm: 'Maschine ungueltig' });
|
|
return;
|
|
}
|
|
const v = state.verteilungen.get(Number(daten.verteilungId));
|
|
Object.assign(v, {
|
|
start: formularZuVergleich(form.timestamp_start),
|
|
ende: formularZuVergleich(form.timestamp_ende),
|
|
taetigkeitId: form.taetigkeit_id,
|
|
maschinenId: form.maschine_id,
|
|
projektId: form.projekt_id,
|
|
});
|
|
if (daten.notizForm.notizfeld) {
|
|
v.notizen.push({ id: neueId(), text: daten.notizForm.notizfeld });
|
|
}
|
|
json(res, 200);
|
|
return;
|
|
}
|
|
|
|
case '/module/arbeitszeit/api/deleteVerteilung.php': {
|
|
const v = state.verteilungen.get(daten.distributionID);
|
|
state.verteilungen.delete(daten.distributionID);
|
|
json(res, 200, schichtAufraeumen(v.schichtId) ? { action: 'link', target: 'arbeitszeit' } : null);
|
|
return;
|
|
}
|
|
|
|
case '/module/arbeitszeit/api/editPauseForm.php': {
|
|
const s = state.schichten.get(daten.shiftID);
|
|
const id = neueId();
|
|
state.pausen.set(id, { schichtId: daten.shiftID, start: s.start, ende: s.ende });
|
|
html(res, `<form id="pause_form" data-id="${id}"><input name="pause_start" id="timestamp_start"></form>`);
|
|
return;
|
|
}
|
|
|
|
case '/module/arbeitszeit/api/updatePause.php': {
|
|
const p = state.pausen.get(Number(daten.pause_id));
|
|
p.start = formularZuVergleich(daten.pauseForm.pause_start);
|
|
p.ende = formularZuVergleich(daten.pauseForm.pause_ende);
|
|
json(res, 200, 0);
|
|
return;
|
|
}
|
|
|
|
case '/module/arbeitszeit/api/deletePause.php':
|
|
state.pausen.delete(daten.pauseID);
|
|
json(res, 200, null);
|
|
return;
|
|
|
|
case '/module/arbeitszeit/api/updateSchicht.php': {
|
|
const s = state.schichten.get(daten.shiftID);
|
|
s.start = formularZuVergleich(daten.timestampStart);
|
|
s.ende = formularZuVergleich(daten.timestampEnd);
|
|
json(res, 200, true);
|
|
return;
|
|
}
|
|
|
|
case '/module/notiz/api/delete.php':
|
|
for (const v of state.verteilungen.values()) {
|
|
v.notizen = v.notizen.filter(n => n.id !== daten.noteID);
|
|
}
|
|
json(res, 200, true);
|
|
return;
|
|
|
|
default:
|
|
res.writeHead(404);
|
|
res.end('not found');
|
|
}
|
|
});
|
|
});
|
|
|
|
return {
|
|
server,
|
|
requests,
|
|
state,
|
|
neueId,
|
|
expireSession() {
|
|
loggedIn = false;
|
|
},
|
|
};
|
|
}
|
|
|
|
async function startConnector(t) {
|
|
const fake = createServer();
|
|
|
|
await new Promise(resolve => fake.server.listen(0, '127.0.0.1', resolve));
|
|
t.after(() => new Promise(resolve => fake.server.close(resolve)));
|
|
|
|
const connector = new AgrarmonitorConnector({
|
|
baseUrl: `http://127.0.0.1:${fake.server.address().port}`,
|
|
username: 'demo',
|
|
password: 'secret',
|
|
cookieStore: new MemoryCookieStore(),
|
|
});
|
|
|
|
await connector.init();
|
|
|
|
return { connector, ...fake };
|
|
}
|
|
|
|
/** Legt eine Schicht mit einer Verteilung an, wie nach einem ersten Nachtragen. */
|
|
function schichtAnlegen(state, neueId, { start = '2026-10-14 08:00', ende = '2026-10-14 09:00' } = {}) {
|
|
const schichtId = neueId();
|
|
const verteilungId = neueId();
|
|
state.schichten.set(schichtId, { mitarbeiterId: 3309, datum: '2026-10-14', start, ende });
|
|
state.verteilungen.set(verteilungId, {
|
|
schichtId,
|
|
start,
|
|
ende,
|
|
taetigkeitId: '870',
|
|
maschinenId: '0',
|
|
projektId: '12',
|
|
notizen: [{ id: neueId(), text: 'Alte Notiz' }],
|
|
});
|
|
return { schichtId, verteilungId };
|
|
}
|
|
|
|
const posts = (requests, path) => requests.filter(r => r.method === 'POST' && r.path === path);
|
|
const schreibendePosts = requests =>
|
|
requests.filter(r => r.method === 'POST' && r.path.startsWith('/module/') && !r.path.endsWith('Form.php'));
|
|
|
|
// 14.10.2026 ist Sommerzeit: 06:00Z = 08:00 Uhr in Berlin.
|
|
const block = (start, ende, extra = {}) => ({
|
|
mitarbeiterId: 3309,
|
|
start: new Date(start),
|
|
ende: new Date(ende),
|
|
taetigkeitId: 870,
|
|
...extra,
|
|
});
|
|
|
|
test('createZeitbuchung traegt ohne Schicht Schicht und Taetigkeit nach', async t => {
|
|
const { connector, requests, state } = await startConnector(t);
|
|
|
|
const { ref } = await connector.createZeitbuchung(
|
|
block('2026-10-14T06:00:00Z', '2026-10-14T07:00:20Z', {
|
|
maschinenId: 5033,
|
|
notiz: 'Hydraulik abgedichtet',
|
|
kostenstelle: '4711',
|
|
})
|
|
);
|
|
|
|
const [nachtragen] = posts(requests, '/module/arbeitszeit/api/nachtragen.php');
|
|
const params = new URLSearchParams(nachtragen.body);
|
|
assert.equal(params.get('form[mitarbeiter_id]'), '3309');
|
|
assert.equal(params.get('form[mitarbeitertyp]'), '1');
|
|
assert.equal(params.get('form[taetigkeit_id]'), '870');
|
|
assert.equal(params.get('form[maschine_id]'), '5033');
|
|
assert.equal(params.get('form[arbeitszeit_start]'), '08:00');
|
|
assert.equal(params.get('form[arbeitszeit_ende]'), '09:00', 'Sekunden werden auf die Minute gerundet');
|
|
assert.equal(params.get('form[pause_start]'), '');
|
|
assert.equal(params.get('form[uebernehmen]'), '1');
|
|
assert.equal(params.get('form[notiz]'), 'Hydraulik abgedichtet\nKostenstelle: 4711');
|
|
assert.deepEqual(params.getAll('tage[]'), ['2026-10-14']);
|
|
|
|
const [[verteilungId, verteilung]] = [...state.verteilungen.entries()];
|
|
assert.equal(ref, `verteilung:3309:2026-10-14:${verteilungId}`);
|
|
assert.equal(verteilung.start, '2026-10-14 08:00');
|
|
assert.equal(verteilung.ende, '2026-10-14 09:00');
|
|
});
|
|
|
|
test('createZeitbuchung bucht in die vorhandene Schicht und verlaengert sie', async t => {
|
|
const { connector, requests, state, neueId } = await startConnector(t);
|
|
const { schichtId } = schichtAnlegen(state, neueId);
|
|
|
|
const { ref } = await connector.createZeitbuchung(
|
|
block('2026-10-14T07:00:00Z', '2026-10-14T08:00:00Z', { taetigkeitId: 869, maschinenId: 5033, notiz: 'Zweiter Block' })
|
|
);
|
|
|
|
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/editVerteilungForm.php')[0].body, {
|
|
employeeID: 3309,
|
|
date: '2026-10-14',
|
|
shiftID: schichtId,
|
|
});
|
|
const verteilungId = Number(ref.split(':').at(-1));
|
|
assert.equal(ref, `verteilung:3309:2026-10-14:${verteilungId}`);
|
|
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updateVerteilung.php')[0].body, {
|
|
mitarbeiterId: 3309,
|
|
verteilungId: String(verteilungId),
|
|
verteilungForm: {
|
|
timestamp_start: '14.10.26 (09:00)',
|
|
timestamp_ende: '14.10.26 (10:00)',
|
|
alte_taetigkeit_id: '0',
|
|
taetigkeit_id: '869',
|
|
ganzertag: '1',
|
|
maschine_id: '5033',
|
|
projekt_id: '0',
|
|
},
|
|
notizForm: { notizfeld: 'Zweiter Block', verfasser_id: '6' },
|
|
});
|
|
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updateSchicht.php').map(r => r.body), [
|
|
{ shiftID: schichtId, timestampStart: '14.10.26 (08:00)', timestampEnd: '14.10.26 (10:00)' },
|
|
]);
|
|
assert.equal(posts(requests, '/module/arbeitszeit/api/nachtragen.php').length, 0);
|
|
});
|
|
|
|
test('createZeitbuchung bucht eine Pause in die Schicht', async t => {
|
|
const { connector, requests, state, neueId } = await startConnector(t);
|
|
const { schichtId } = schichtAnlegen(state, neueId);
|
|
|
|
const { ref } = await connector.createZeitbuchung({
|
|
art: 'pause',
|
|
mitarbeiterId: 3309,
|
|
start: new Date('2026-10-14T07:00:00Z'),
|
|
ende: new Date('2026-10-14T07:30:00Z'),
|
|
});
|
|
|
|
const [[pauseId, pause]] = [...state.pausen.entries()];
|
|
assert.equal(ref, `pause:3309:2026-10-14:${pauseId}`);
|
|
assert.deepEqual(pause, { schichtId, start: '2026-10-14 09:00', ende: '2026-10-14 09:30' });
|
|
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updatePause.php')[0].body, {
|
|
mitarbeiterId: 3309,
|
|
pause_id: String(pauseId),
|
|
pauseForm: { pause_start: '14.10.26 (09:00)', pause_ende: '14.10.26 (09:30)' },
|
|
neu: 1,
|
|
});
|
|
});
|
|
|
|
test('createZeitbuchung lehnt eine Pause ohne Schicht ab', async t => {
|
|
const { connector, requests } = await startConnector(t);
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
connector.createZeitbuchung({
|
|
art: 'pause',
|
|
mitarbeiterId: 3309,
|
|
start: new Date('2026-10-14T07:00:00Z'),
|
|
ende: new Date('2026-10-14T07:30:00Z'),
|
|
}),
|
|
error => error instanceof ZeitbuchungError && /keine Schicht vorhanden/.test(error.message)
|
|
);
|
|
assert.equal(schreibendePosts(requests).length, 0);
|
|
});
|
|
|
|
test('createZeitbuchung meldet den Grund einer 422 und raeumt Platzhalter und Schicht auf', async t => {
|
|
const { connector, requests, state, neueId } = await startConnector(t);
|
|
const { schichtId, verteilungId } = schichtAnlegen(state, neueId);
|
|
|
|
await assert.rejects(
|
|
() => connector.createZeitbuchung(block('2026-10-14T07:00:00Z', '2026-10-14T08:00:00Z', { maschinenId: 999 })),
|
|
error =>
|
|
error instanceof ZeitbuchungError &&
|
|
error.status === 422 &&
|
|
error.grund === 'Maschine ungueltig' &&
|
|
/Buchung speichern fehlgeschlagen \(HTTP 422\): Maschine ungueltig/.test(error.message)
|
|
);
|
|
|
|
assert.deepEqual([...state.verteilungen.keys()], [verteilungId], 'Platzhalter wurde wieder geloescht');
|
|
assert.equal(posts(requests, '/module/arbeitszeit/api/deleteVerteilung.php').length, 1);
|
|
const schicht = state.schichten.get(schichtId);
|
|
assert.deepEqual([schicht.start, schicht.ende], ['2026-10-14 08:00', '2026-10-14 09:00'], 'Schicht zurueckgesetzt');
|
|
});
|
|
|
|
test('createZeitbuchung ueber Mitternacht korrigiert Verteilung und Schicht nach dem Nachtragen', async t => {
|
|
const { connector, requests, state } = await startConnector(t);
|
|
|
|
await connector.createZeitbuchung(
|
|
block('2026-10-14T21:30:00Z', '2026-10-14T22:30:00Z', { datum: '2026-10-14' })
|
|
);
|
|
|
|
const params = new URLSearchParams(posts(requests, '/module/arbeitszeit/api/nachtragen.php')[0].body);
|
|
assert.equal(params.get('form[arbeitszeit_start]'), '23:30');
|
|
assert.equal(params.get('form[arbeitszeit_ende]'), '23:59');
|
|
|
|
const update = posts(requests, '/module/arbeitszeit/api/updateVerteilung.php')[0].body;
|
|
assert.equal(update.verteilungForm.timestamp_start, '14.10.26 (23:30)');
|
|
assert.equal(update.verteilungForm.timestamp_ende, '15.10.26 (00:30)');
|
|
assert.equal(update.notizForm.notizfeld, '', 'Notiz nicht doppelt anlegen');
|
|
|
|
const [schicht] = state.schichten.values();
|
|
assert.deepEqual([schicht.start, schicht.ende], ['2026-10-14 23:30', '2026-10-15 00:30']);
|
|
});
|
|
|
|
// Im echten System gespeichert statt abgelehnt (Echttest 14.10.2026) - der Connector muss pruefen.
|
|
test('createZeitbuchung lehnt eine nicht buchbare Taetigkeit ab', async t => {
|
|
const { connector, requests, state, neueId } = await startConnector(t);
|
|
|
|
await assert.rejects(
|
|
() => connector.createZeitbuchung(block('2026-10-14T06:00:00Z', '2026-10-14T07:00:00Z', { taetigkeitId: 999 })),
|
|
error => error instanceof ZeitbuchungError && error.grund === 'taetigkeit nicht buchbar'
|
|
);
|
|
assert.equal(posts(requests, '/module/arbeitszeit/api/nachtragen.php').length, 0, 'ohne Schicht nichts nachtragen');
|
|
|
|
const { verteilungId } = schichtAnlegen(state, neueId);
|
|
|
|
await assert.rejects(
|
|
() => connector.createZeitbuchung(block('2026-10-14T07:00:00Z', '2026-10-14T08:00:00Z', { taetigkeitId: 999 })),
|
|
error => error instanceof ZeitbuchungError && error.grund === 'taetigkeit nicht buchbar'
|
|
);
|
|
assert.equal(posts(requests, '/module/arbeitszeit/api/updateVerteilung.php').length, 0);
|
|
assert.deepEqual([...state.verteilungen.keys()], [verteilungId], 'Platzhalter wurde wieder geloescht');
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
connector.updateZeitbuchung(
|
|
`verteilung:3309:2026-10-14:${verteilungId}`,
|
|
block('2026-10-14T06:00:00Z', '2026-10-14T07:00:00Z', { taetigkeitId: 999 })
|
|
),
|
|
error => error instanceof ZeitbuchungError && error.grund === 'taetigkeit nicht buchbar'
|
|
);
|
|
assert.equal(posts(requests, '/module/arbeitszeit/api/updateVerteilung.php').length, 0);
|
|
});
|
|
|
|
test('updateZeitbuchung aendert die Verteilung und ersetzt die Notiz', async t => {
|
|
const { connector, requests, state, neueId } = await startConnector(t);
|
|
const { verteilungId } = schichtAnlegen(state, neueId);
|
|
const alteNotizId = state.verteilungen.get(verteilungId).notizen[0].id;
|
|
|
|
await connector.updateZeitbuchung(
|
|
`verteilung:3309:2026-10-14:${verteilungId}`,
|
|
block('2026-10-14T06:00:00Z', '2026-10-14T06:45:00Z', { taetigkeitId: 869, maschinenId: 5033, notiz: 'Neue Notiz' })
|
|
);
|
|
|
|
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updateVerteilung.php')[0].body, {
|
|
mitarbeiterId: 3309,
|
|
verteilungId: String(verteilungId),
|
|
verteilungForm: {
|
|
timestamp_start: '14.10.26 (08:00)',
|
|
timestamp_ende: '14.10.26 (08:45)',
|
|
alte_taetigkeit_id: '870',
|
|
taetigkeit_id: '869',
|
|
ganzertag: '1',
|
|
maschine_id: '5033',
|
|
projekt_id: '12',
|
|
},
|
|
notizForm: { notizfeld: 'Neue Notiz', verfasser_id: '6' },
|
|
});
|
|
assert.deepEqual(posts(requests, '/module/notiz/api/delete.php').map(r => r.body), [{ noteID: alteNotizId }]);
|
|
assert.deepEqual(state.verteilungen.get(verteilungId).notizen.map(n => n.text), ['Neue Notiz']);
|
|
});
|
|
|
|
test('updateZeitbuchung laesst eine unveraenderte Notiz stehen', async t => {
|
|
const { connector, requests, state, neueId } = await startConnector(t);
|
|
const { verteilungId } = schichtAnlegen(state, neueId);
|
|
|
|
await connector.updateZeitbuchung(
|
|
`verteilung:3309:2026-10-14:${verteilungId}`,
|
|
block('2026-10-14T06:00:00Z', '2026-10-14T06:45:00Z', { notiz: 'Alte Notiz' })
|
|
);
|
|
|
|
assert.equal(posts(requests, '/module/arbeitszeit/api/updateVerteilung.php')[0].body.notizForm.notizfeld, '');
|
|
assert.equal(posts(requests, '/module/notiz/api/delete.php').length, 0);
|
|
});
|
|
|
|
test('updateZeitbuchung aendert eine Pause', async t => {
|
|
const { connector, requests, state, neueId } = await startConnector(t);
|
|
const { schichtId } = schichtAnlegen(state, neueId);
|
|
const pauseId = neueId();
|
|
state.pausen.set(pauseId, { schichtId, start: '2026-10-14 08:30', ende: '2026-10-14 08:45' });
|
|
|
|
await connector.updateZeitbuchung(`pause:3309:2026-10-14:${pauseId}`, {
|
|
art: 'pause',
|
|
mitarbeiterId: 3309,
|
|
start: new Date('2026-10-14T06:35:00Z'),
|
|
ende: new Date('2026-10-14T06:50:00Z'),
|
|
});
|
|
|
|
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/updatePause.php')[0].body, {
|
|
mitarbeiterId: 3309,
|
|
pause_id: String(pauseId),
|
|
pauseForm: { pause_start: '14.10.26 (08:35)', pause_ende: '14.10.26 (08:50)' },
|
|
neu: 0,
|
|
});
|
|
});
|
|
|
|
test('updateZeitbuchung lehnt ungueltige Referenzen und Wechsel von Art oder Tag ab', async t => {
|
|
const { connector, requests, state, neueId } = await startConnector(t);
|
|
const { verteilungId } = schichtAnlegen(state, neueId);
|
|
const daten = block('2026-10-14T06:00:00Z', '2026-10-14T06:45:00Z');
|
|
|
|
await assert.rejects(() => connector.updateZeitbuchung('1180803', daten), ZeitbuchungError);
|
|
await assert.rejects(
|
|
() => connector.updateZeitbuchung(`pause:3309:2026-10-14:${verteilungId}`, daten),
|
|
/Art der Buchung/
|
|
);
|
|
await assert.rejects(
|
|
() => connector.updateZeitbuchung(`verteilung:3309:2026-10-13:${verteilungId}`, daten),
|
|
/Arbeitstag/
|
|
);
|
|
await assert.rejects(
|
|
() => connector.updateZeitbuchung(`verteilung:3309:2026-10-14:999999`, daten),
|
|
/nicht gefunden/
|
|
);
|
|
assert.equal(schreibendePosts(requests).length, 0);
|
|
});
|
|
|
|
test('deleteZeitbuchung loescht Verteilung und Pause', async t => {
|
|
const { connector, requests, state, neueId } = await startConnector(t);
|
|
const { schichtId, verteilungId } = schichtAnlegen(state, neueId);
|
|
const pauseId = neueId();
|
|
state.pausen.set(pauseId, { schichtId, start: '2026-10-14 08:30', ende: '2026-10-14 08:45' });
|
|
|
|
await connector.deleteZeitbuchung(`pause:3309:2026-10-14:${pauseId}`);
|
|
await connector.deleteZeitbuchung(`verteilung:3309:2026-10-14:${verteilungId}`);
|
|
|
|
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/deletePause.php')[0].body, {
|
|
pauseID: pauseId,
|
|
employeeID: 3309,
|
|
date: '2026-10-14',
|
|
});
|
|
assert.deepEqual(posts(requests, '/module/arbeitszeit/api/deleteVerteilung.php')[0].body, {
|
|
distributionID: verteilungId,
|
|
employeeID: 3309,
|
|
date: '2026-10-14',
|
|
});
|
|
assert.equal(state.verteilungen.size, 0);
|
|
assert.equal(state.pausen.size, 0);
|
|
});
|
|
|
|
test('createZeitbuchung prueft die Eingaben vor dem ersten Request', async t => {
|
|
const { connector, requests } = await startConnector(t);
|
|
const anzahlVorher = requests.length;
|
|
|
|
await assert.rejects(
|
|
() => connector.createZeitbuchung(block('2026-10-14T06:00:00Z', '2026-10-14T06:00:20Z')),
|
|
/mindestens eine Minute/
|
|
);
|
|
await assert.rejects(
|
|
() => connector.createZeitbuchung(block('2026-10-14T06:00:00Z', '2026-10-14T07:00:00Z', { taetigkeitId: 0 })),
|
|
/Taetigkeits-ID/
|
|
);
|
|
assert.equal(requests.length, anzahlVorher);
|
|
});
|
|
|
|
test('createZeitbuchung meldet sich bei abgelaufener Session neu an', async t => {
|
|
const { connector, requests, state, expireSession } = await startConnector(t);
|
|
const logins = () => posts(requests, '/login/api/login.php').length;
|
|
|
|
assert.equal(logins(), 1);
|
|
expireSession();
|
|
|
|
const { ref } = await connector.createZeitbuchung(block('2026-10-14T06:00:00Z', '2026-10-14T07:00:00Z'));
|
|
|
|
assert.equal(logins(), 2, 'genau ein erneuter Login');
|
|
assert.match(ref, /^verteilung:3309:2026-10-14:\d+$/);
|
|
assert.equal(state.verteilungen.size, 1);
|
|
});
|