/** * Abrechnungsmonate im Frontend. * * Ein Abrechnungsmonat beginnt am eingestellten Gehaltstag und endet am Vortag * des nächsten. Benannt wird er nach dem Monat, in dem er beginnt: Der 25.09. * eröffnet den Zeitraum „September“, der bis zum 24.10. läuft. Liegt der * Starttag jenseits der Monatslänge, rutscht er auf den Monatsletzten – so * bedeutet 31 verlässlich „letzter Tag des Monats“. * * Dieselbe Rechnung steht im Backend; hier steht sie, damit Seitenköpfe den * Zeitraum schon vor der ersten Antwort benennen können. */ import { formatDate, formatMonth, toIsoDate } from "@/lib/format"; export const DEFAULT_MONTH_START_DAY = 1; export const MIN_MONTH_START_DAY = 1; export const MAX_MONTH_START_DAY = 31; /** Der Gehaltstag im Monat von `jahr`/`monat`, gekürzt auf den Monatsletzten. */ function ankerTag(jahr: number, monat: number, startTag: number): Date { const letzter = new Date(jahr, monat + 1, 0).getDate(); return new Date(jahr, monat, Math.min(startTag, letzter)); } function ausIso(monatsErster: string): Date { const teile = monatsErster.split("-").map(Number); return new Date(teile[0] ?? 1970, (teile[1] ?? 1) - 1, 1); } /** Bezeichner des Abrechnungsmonats, in dem `datum` liegt – immer ein Monatserster. */ export function periodKey(startDay: number, datum: Date = new Date()): string { const tag = new Date(datum.getFullYear(), datum.getMonth(), datum.getDate()); const anker = ankerTag(tag.getFullYear(), tag.getMonth(), startDay); const versatz = tag >= anker ? 0 : -1; return toIsoDate(new Date(tag.getFullYear(), tag.getMonth() + versatz, 1)); } /** Erster und letzter Tag des Abrechnungsmonats mit dem Bezeichner `monthKey`. */ export function periodBounds(startDay: number, monthKey: string): { start: string; end: string } { const schluessel = ausIso(monthKey); const beginn = ankerTag(schluessel.getFullYear(), schluessel.getMonth(), startDay); const naechster = ankerTag(schluessel.getFullYear(), schluessel.getMonth() + 1, startDay); naechster.setDate(naechster.getDate() - 1); return { start: toIsoDate(beginn), end: toIsoDate(naechster) }; } /** * Beschriftung für Seitenköpfe: „September 2026“, bei abweichendem Monatsbeginn * ergänzt um die Grenzen des Zeitraums. */ export function periodLabel(startDay: number, monthKey: string): string { const name = formatMonth(monthKey); if (startDay === DEFAULT_MONTH_START_DAY) return name; const { start, end } = periodBounds(startDay, monthKey); return `${name} · ${formatDate(start)} – ${formatDate(end)}`; }