feat: einstellbarer Monatsbeginn zum Gehaltstag
Wer nach dem Gehaltseingang plant, stellt unter Einstellungen den Tag ein, ab dem ein neuer Monat zählt. Der Zeitraum läuft dann vom Gehaltstag bis zum Vortag des nächsten und trägt den Namen des Monats, in dem er beginnt: Mit dem 25. umfasst „September 2026“ den 25.09. bis zum 24.10. Ein Starttag jenseits der Monatslänge rutscht auf den Monatsletzten, sodass 31 verlässlich den letzten Tag des Monats meint. Dashboard, Cashflow-Kalender, Budgets, Zwölf-Monats-Vorschau, die Kategorienauswertung, der Monatsexport und die Benachrichtigung über überschrittene Budgets rechnen mit diesem Zeitraum. Budgets bleiben je Monat gepflegt; der Bezeichner ist weiterhin der Monatserste, nur der Schnitt verschiebt sich. Bestandsinstallationen bleiben beim Ersten. Die Einstellung liegt in einer einzeiligen Tabelle hinter GET/PUT /api/settings; die Monatsauswertungen liefern zusätzlich period_start und period_end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fj7mB1PGA1aDHyfdSGHgzD
This commit is contained in:
co-authored by
Claude Opus 5
parent
998d5867df
commit
0a6261fc55
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user