- 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>
179 lines
6.0 KiB
JavaScript
179 lines
6.0 KiB
JavaScript
/*
|
|
* Datumsrechnung ohne Zeitzonenfallen: alles laeuft ueber 'YYYY-MM-DD' und
|
|
* UTC-Mitternacht, damit ein Tag nie durch die Sommerzeit verrutscht.
|
|
*/
|
|
export function parseDate(value) {
|
|
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
|
|
const date = new Date(`${value}T00:00:00Z`);
|
|
return Number.isNaN(date.getTime()) || iso(date) !== value ? null : date;
|
|
}
|
|
|
|
export function iso(date) {
|
|
return date.toISOString().slice(0, 10);
|
|
}
|
|
|
|
export function today() {
|
|
const now = new Date();
|
|
// Lokales Datum des Servers (Europe/Berlin), nicht UTC.
|
|
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60000);
|
|
return local.toISOString().slice(0, 10);
|
|
}
|
|
|
|
export function addDays(value, days) {
|
|
const date = parseDate(value);
|
|
date.setUTCDate(date.getUTCDate() + days);
|
|
return iso(date);
|
|
}
|
|
|
|
export function daysBetween(from, to) {
|
|
return Math.round((parseDate(to) - parseDate(from)) / 86400000);
|
|
}
|
|
|
|
/** Arbeitstage Mo-Fr im geschlossenen Intervall. */
|
|
export function workdays(from, to) {
|
|
let count = 0;
|
|
const end = parseDate(to);
|
|
for (const d = parseDate(from); d <= end; d.setUTCDate(d.getUTCDate() + 1)) {
|
|
const day = d.getUTCDay();
|
|
if (day !== 0 && day !== 6) count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
export function monthRange(month) {
|
|
if (!/^\d{4}-\d{2}$/.test(month || '')) return null;
|
|
const [y, m] = month.split('-').map(Number);
|
|
if (m < 1 || m > 12) return null;
|
|
const last = new Date(Date.UTC(y, m, 0)).getUTCDate();
|
|
return { from: `${month}-01`, to: `${month}-${String(last).padStart(2, '0')}` };
|
|
}
|
|
|
|
export function isoWeek(value) {
|
|
const date = parseDate(value);
|
|
const day = date.getUTCDay() || 7;
|
|
date.setUTCDate(date.getUTCDate() + 4 - day);
|
|
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
|
|
return { year: date.getUTCFullYear(), week: Math.ceil(((date - yearStart) / 86400000 + 1) / 7) };
|
|
}
|
|
|
|
export function mondayOf(value) {
|
|
const date = parseDate(value);
|
|
const day = date.getUTCDay() || 7;
|
|
date.setUTCDate(date.getUTCDate() - day + 1);
|
|
return iso(date);
|
|
}
|
|
|
|
export const MONTHS_DE = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
|
|
|
|
export function formatDateDe(value) {
|
|
const [y, m, d] = String(value).slice(0, 10).split('-');
|
|
return `${d}.${m}.${y}`;
|
|
}
|
|
|
|
/** Zeitraum, in dem `value` liegt: day | week | month | quarter | year. */
|
|
export function periodRange(granularity, value) {
|
|
const [y, m] = value.split('-').map(Number);
|
|
switch (granularity) {
|
|
case 'day':
|
|
return { from: value, to: value };
|
|
case 'week': {
|
|
const from = mondayOf(value);
|
|
return { from, to: addDays(from, 6) };
|
|
}
|
|
case 'month':
|
|
return monthRange(value.slice(0, 7));
|
|
case 'quarter': {
|
|
const q = Math.floor((m - 1) / 3);
|
|
const startMonth = String(q * 3 + 1).padStart(2, '0');
|
|
const end = monthRange(`${y}-${String(q * 3 + 3).padStart(2, '0')}`);
|
|
return { from: `${y}-${startMonth}-01`, to: end.to };
|
|
}
|
|
case 'year':
|
|
return { from: `${y}-01-01`, to: `${y}-12-31` };
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Beginn des vorherigen Zeitraums (fuer Reihen und Vergleich). */
|
|
export function previousStart(granularity, from) {
|
|
const d = parseDate(from);
|
|
if (granularity === 'day') d.setUTCDate(d.getUTCDate() - 1);
|
|
if (granularity === 'week') d.setUTCDate(d.getUTCDate() - 7);
|
|
if (granularity === 'month') d.setUTCMonth(d.getUTCMonth() - 1);
|
|
if (granularity === 'quarter') d.setUTCMonth(d.getUTCMonth() - 3);
|
|
if (granularity === 'year') d.setUTCFullYear(d.getUTCFullYear() - 1);
|
|
return iso(d);
|
|
}
|
|
|
|
export function periodLabel(granularity, from) {
|
|
const [y, m, d] = from.split('-');
|
|
if (granularity === 'day') return `${d}.${m}.`;
|
|
if (granularity === 'week') return `KW ${isoWeek(from).week}`;
|
|
if (granularity === 'month') return `${MONTHS_DE[Number(m) - 1].slice(0, 3)} ${y.slice(2)}`;
|
|
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;
|
|
}
|