fix(kalender): Abwesenheiten jahresweise abfragen, Feiertagsnamen

- HRworks /absences erlaubt hoechstens ein Jahr je Aufruf; der Kalender
  reicht weiter und fragt jetzt in Abschnitten von 365 Tagen.
- HRworks liefert zu Feiertagen keinen Namen. Der Name kommt jetzt aus
  dem Datum (feste Feiertage, Ostern-abhaengige, Buss- und Bettag).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-25 11:41:49 +02:00
co-authored by Claude Opus 5.5
parent c0eb00faee
commit b6ba7860bf
5 changed files with 97 additions and 8 deletions
+11 -4
View File
@@ -1,6 +1,6 @@
import { pool, withTransaction } from '../db.js';
import { getSettings, saveSetting } from './settings.js';
import { addDays, today } from './dates.js';
import { addDays, germanHolidayName, today } from './dates.js';
import { Hrworks, ABSENCE_STATES } from './hrworks.js';
import { chunks, matchUsers } from './hrworksSync.js';
@@ -22,13 +22,15 @@ import { chunks, matchUsers } from './hrworksSync.js';
/** Wie weit reichen lange Urlaube vor das Fenster zurueck? Fuer die Artabfrage. */
const LOOKBACK_DAYS = 90;
/** HRworks: "The maximum date interval for this action is one year." */
const ABSENCES_MAX_DAYS = 365;
/**
* Was steht an einem Tag im Kalender? Feiertag vor Urlaub; Wochenenden und
* freie Tage (Soll 0) tragen keinen Urlaub.
*/
export function kalenderEintrag(day, meta, types, typeMap = {}) {
if (day.holiday) return { kind: 'holiday', label: day.holiday_name || 'Feiertag', half: false };
if (day.holiday) return { kind: 'holiday', label: day.holiday_name || germanHolidayName(day.date) || 'Feiertag', half: false };
if (!(day.regular_hours > 0)) return null;
for (const id of day.absences) {
const info = meta.get(id);
@@ -63,8 +65,13 @@ export async function syncCalendar({ dryRun = true, from, to } = {}) {
const [users] = await pool.query("SELECT id, name, email FROM pulse_users WHERE is_active = 1 AND role <> 'admin'");
const { matched, unmatched } = matchUsers(persons, users);
const absences = await hr.absences(addDays(von, -LOOKBACK_DAYS), bis);
const meta = new Map(absences.map((a) => [a.number, { type: String(a.type), name: a.name, status: a.statusIdentifier }]));
// /absences erlaubt hoechstens ein Jahr je Aufruf.
const meta = new Map();
for (const [start, end] of chunks(addDays(von, -LOOKBACK_DAYS), bis, ABSENCES_MAX_DAYS)) {
for (const a of await hr.absences(start, end)) {
meta.set(a.number, { type: String(a.type), name: a.name, status: a.statusIdentifier });
}
}
// Je Person und Tag ein Eintrag - auch wenn zwei HRworks-Konten auf
// denselben Pulse-Benutzer zeigen.
+62
View File
@@ -114,3 +114,65 @@ export function periodLabel(granularity, from) {
if (granularity === 'quarter') return `Q${Math.floor((Number(m) - 1) / 3) + 1} ${y.slice(2)}`;
return y;
}
/** Ostersonntag (Gauss/Meeus, gregorianisch) als 'YYYY-MM-DD'. */
export function easterSunday(year) {
const a = year % 19;
const b = Math.floor(year / 100);
const c = year % 100;
const d = Math.floor(b / 4);
const e = b % 4;
const f = Math.floor((b + 8) / 25);
const g = Math.floor((b - f + 1) / 3);
const h = (19 * a + b - d - g + 15) % 30;
const i = Math.floor(c / 4);
const k = c % 4;
const l = (32 + 2 * e + 2 * i - h - k) % 7;
const m = Math.floor((a + 11 * h + 22 * l) / 451);
const month = Math.floor((h + l - 7 * m + 114) / 31);
const day = ((h + l - 7 * m + 114) % 31) + 1;
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}
/**
* Name eines deutschen Feiertags zum Datum. HRworks sagt nur, DASS ein Tag
* Feiertag ist (welcher, haengt am Standort der Person); der Name kommt von
* hier. Unbekannt: null.
*/
export function germanHolidayName(value) {
const year = Number(value.slice(0, 4));
const fixed = {
'01-01': 'Neujahr',
'01-06': 'Heilige Drei Könige',
'03-08': 'Frauentag',
'05-01': 'Tag der Arbeit',
'08-08': 'Friedensfest',
'08-15': 'Mariä Himmelfahrt',
'09-20': 'Weltkindertag',
'10-03': 'Tag der Deutschen Einheit',
'10-31': 'Reformationstag',
'11-01': 'Allerheiligen',
'12-24': 'Heiligabend',
'12-25': '1. Weihnachtstag',
'12-26': '2. Weihnachtstag',
'12-31': 'Silvester',
};
if (fixed[value.slice(5)]) return fixed[value.slice(5)];
const easter = easterSunday(year);
const offset = daysBetween(easter, value);
const moving = {
'-2': 'Karfreitag',
0: 'Ostersonntag',
1: 'Ostermontag',
39: 'Christi Himmelfahrt',
49: 'Pfingstsonntag',
50: 'Pfingstmontag',
60: 'Fronleichnam',
};
if (moving[offset]) return moving[offset];
// Buss- und Bettag: Mittwoch vor dem 23. November.
const nov23 = parseDate(`${year}-11-23`);
const back = ((nov23.getUTCDay() - 3 + 7) % 7) || 7;
if (value === addDays(`${year}-11-23`, -back)) return 'Buß- und Bettag';
return null;
}
+3 -3
View File
@@ -56,12 +56,12 @@ export function matchUsers(persons, users) {
return { matched, unmatched };
}
/** Tage in Abschnitte von hoechstens CHUNK_DAYS zerlegen. */
export function chunks(from, to) {
/** Tage in Abschnitte von hoechstens `size` Tagen zerlegen. */
export function chunks(from, to, size = CHUNK_DAYS) {
const out = [];
let start = from;
while (start <= to) {
let end = addDays(start, CHUNK_DAYS - 1);
let end = addDays(start, size - 1);
if (end > to) end = to;
out.push([start, end]);
start = addDays(end, 1);