feat(hrworks): Abwesenheiten taeglich aus HRworks uebernehmen

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>
This commit is contained in:
schakal00783
2026-09-21 14:50:54 +02:00
co-authored by Claude Opus 5
parent 994f39daff
commit 532b15ce99
18 changed files with 1236 additions and 6 deletions
+43
View File
@@ -30,6 +30,47 @@ export const DEFAULTS = {
signature_left: 'Datum, Unterschrift Berater',
signature_right: 'Datum, Unterschrift Kunde',
},
/*
* Abwesenheiten aus HRworks. Urlaub und Krankheit werden dort gefuehrt; Pulse
* holt sie taeglich und buchtsie auf ein nicht abrechenbares Projekt.
*/
hrworks: {
enabled: false,
access_key: '',
secret_access_key: '',
// Projekt, auf das gebucht wird (Projektcode, nicht die id - der Code
// bleibt beim Umziehen der Datenbank gleich).
project_code: 'PERINCO-2024-001',
// Wie viele Tage rueckwaerts jeder Lauf prueft.
window_days: 60,
/*
* Abwesenheitsart -> Taskname in Pulse. Schluessel ist "absence:<key>" bzw.
* "sick:<key>" aus HRworks. Fehlt ein Eintrag, gilt der HRworks-Name des
* Typs - dann muss in Pulse ein Task mit genau diesem Namen stehen.
*/
type_map: {
'absence:1': 'Urlaub',
'absence:3': 'Elternzeit',
'absence:5': 'Mutterschutz',
'absence:6': 'Geburt',
'absence:7': 'Hochzeit',
'absence:8': 'Sabbatical',
'absence:9': 'Freistellung',
'absence:10': 'Bildungsurlaub',
'sick:KMA': 'Krankheit',
'sick:KOA': 'Krankheit',
'sick:K28': 'Krankheit',
'sick:KOL': 'Krankheit',
'sick:KWW': 'Krankheit',
'sick:KEK': 'Kind krank',
},
// Task fuer Feiertage; leer = Feiertage nicht buchen.
holiday_task: 'Feiertag',
// Halber Tag: feste Stundenzahl (0 = aus HRworks uebernehmen).
half_day_hours: 4,
last_run: null,
last_result: null,
},
sso: {
enabled: false,
button_label: 'Mit Microsoft anmelden',
@@ -62,6 +103,7 @@ export async function getSettings() {
}
}
if (result.sso.client_secret) result.sso.client_secret = decrypt(result.sso.client_secret);
if (result.hrworks.secret_access_key) result.hrworks.secret_access_key = decrypt(result.hrworks.secret_access_key);
return result;
}
@@ -69,6 +111,7 @@ export async function saveSetting(key, value) {
if (!(key in DEFAULTS)) throw new Error(`Unbekannte Einstellung: ${key}`);
let stored = value;
if (key === 'sso' && value.client_secret) stored = { ...value, client_secret: encrypt(value.client_secret) };
if (key === 'hrworks' && value.secret_access_key) stored = { ...value, secret_access_key: encrypt(value.secret_access_key) };
await pool.query(
'INSERT INTO pulse_settings (k, v) VALUES (?, ?) ON DUPLICATE KEY UPDATE v = VALUES(v)',
[key, JSON.stringify(stored)]