Urlaub, Krankheit, Elternzeit und Feiertage stehen in HRworks. Pulse holt sie einmal taeglich und bucht sie auf das nicht abrechenbare Projekt "Abwesenheiten". Nach HRworks geht nichts zurueck, es wird nur gelesen. Die Stunden rechnet HRworks selbst: available-working-hours liefert je Person und Tag das Soll und die verfuegbaren Stunden, die Differenz sind die Abwesenheitsstunden. Damit stimmen Teilzeit und halbe Tage ohne eigenen Kalender. relatedEvents nennt den Auslöser; Vorrang hat der Feiertag, dann Krankheit, dann Abwesenheit. Abgesprochene Regeln: nur genehmigte Abwesenheiten (validatedOk), bei Krankmeldungen auch offene Bescheinigungen; nur bis heute buchen, der Rest waechst taeglich nach; halbe Tage feste 4 Stunden; jeder aktive Mitarbeiter wird dem Projekt zugewiesen, auch neue. Jede Buchung traegt hrworks_ref = "<Nummer>@<Tag>" (unique). Ein zweiter Lauf aendert nichts, Stornos und Verschiebungen verschwinden im naechsten Lauf, DELETE /api/hrworks/entries entfernt genau diese Buchungen. Nicht abrechenbare Projekte brauchen keinen Budgettopf mehr. Ein Topf traegt Volumen, Laufzeit und Satz - das hat nur bei Billable und Festpreis Sinn. Am Abwesenheitsprojekt war er nur ein Ablaufdatum, das irgendwann Buchungen blockiert haette. Taeglicher Lauf ueber systemd (perinco-pulse-hrworks.timer, 5:30 Uhr Berliner Zeit, Persistent). Bedienung unter Einstellungen -> Abwesenheiten (HRworks) mit Verbindungstest und Probelauf; der geheime Schluessel liegt AES-verschluesselt in der Datenbank. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
69 lines
3.3 KiB
JavaScript
69 lines
3.3 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { chunks, matchUsers, ursache } from '../src/lib/hrworksSync.js';
|
|
import { ABSENCE_STATES, SICK_STATES } from '../src/lib/hrworks.js';
|
|
|
|
test('ein Feiertag gewinnt gegen Urlaub am selben Tag', () => {
|
|
assert.deepEqual(ursache({ holiday: true, absences: ['30_25'], sick_leaves: [] }), { art: 'holiday', ref: null });
|
|
assert.deepEqual(ursache({ holiday: false, absences: ['30_25'], sick_leaves: [] }), { art: 'absence', ref: '30_25' });
|
|
// Krankheit vor Urlaub: wer im Urlaub krank wird, ist krank.
|
|
assert.deepEqual(ursache({ holiday: false, absences: ['30_25'], sick_leaves: ['31_9'] }), { art: 'sick', ref: '31_9' });
|
|
assert.equal(ursache({ holiday: false, absences: [], sick_leaves: [] }), null);
|
|
});
|
|
|
|
test('Zuordnung: Mailadresse zuerst, dann Name', () => {
|
|
const users = [
|
|
{ id: 1, name: 'Kai Schwickert', email: 'kai.schwickert@icon-experts.de' },
|
|
{ id: 2, name: 'Nico Necknig', email: 'nico.necknig@perinco.de' },
|
|
{ id: 3, name: 'Renè Kleinau', email: 'rene.kleinau@perinco.de' },
|
|
];
|
|
const persons = [
|
|
// andere Domain in HRworks - greift ueber den Namen
|
|
{ person_id: 'KaiS', name: 'Kai Schwickert', email: 'kai.schwickert@perinco.de' },
|
|
// private Adresse in HRworks - greift ueber den Namen
|
|
{ person_id: 'NicoN', name: 'Nico Necknig', email: 'nico.necknig@web.de' },
|
|
// Akzent unterschiedlich geschrieben
|
|
{ person_id: 'ReneK', name: 'Rene Kleinau', email: 'andere@perinco.de' },
|
|
{ person_id: 'ServiceA', name: 'Service Account', email: 'sa.hrworks@perinco.de' },
|
|
];
|
|
const { matched, unmatched } = matchUsers(persons, users);
|
|
assert.equal(matched.get('KaiS').id, 1);
|
|
assert.equal(matched.get('NicoN').id, 2);
|
|
assert.equal(matched.get('ReneK').id, 3);
|
|
assert.deepEqual(unmatched.map((u) => u.person_id), ['ServiceA']);
|
|
});
|
|
|
|
test('doppelte Namen ergeben keinen Treffer', () => {
|
|
const users = [
|
|
{ id: 5, name: 'Luisa Sauerwald', email: 'lusia.sauerwald@perinco.de' },
|
|
{ id: 23, name: 'Luisa Sauerwald', email: 'luisa.sauerwald@perinco.de' },
|
|
];
|
|
// Mail passt: eindeutig.
|
|
let r = matchUsers([{ person_id: 'LuisaS', name: 'Luisa Sauerwald', email: 'luisa.sauerwald@perinco.de' }], users);
|
|
assert.equal(r.matched.get('LuisaS').id, 23);
|
|
// Mail passt nicht: der Name ist zweimal da, also lieber melden als raten.
|
|
r = matchUsers([{ person_id: 'LuisaS', name: 'Luisa Sauerwald', email: 'x@y.de' }], users);
|
|
assert.equal(r.matched.size, 0);
|
|
assert.equal(r.unmatched.length, 1);
|
|
});
|
|
|
|
test('Zeitraum wird in Abschnitte von hoechstens 31 Tagen zerlegt', () => {
|
|
assert.deepEqual(chunks('2026-09-01', '2026-09-05'), [['2026-09-01', '2026-09-05']]);
|
|
const lang = chunks('2026-07-23', '2026-09-21');
|
|
assert.equal(lang.length, 2);
|
|
assert.equal(lang[0][0], '2026-07-23');
|
|
assert.equal(lang[lang.length - 1][1], '2026-09-21');
|
|
for (const [von, bis] of lang) {
|
|
const tage = (new Date(bis) - new Date(von)) / 86400000 + 1;
|
|
assert.ok(tage <= 31, `${von}-${bis} hat ${tage} Tage`);
|
|
}
|
|
});
|
|
|
|
test('nur genehmigte Abwesenheiten, bei Krankheit auch offene Bescheinigungen', () => {
|
|
assert.deepEqual(ABSENCE_STATES, ['validatedOk']);
|
|
assert.ok(SICK_STATES.includes('certificatePending'));
|
|
assert.ok(SICK_STATES.includes('reported'));
|
|
assert.ok(!SICK_STATES.includes('cancelled'));
|
|
assert.ok(!ABSENCE_STATES.includes('requested'));
|
|
});
|