Files
Dashboard/features/dashboard/utils/dateUtils.ts
T
dejanandClaude Opus 5 b8bf987d0e feat(dashboard): Analytics-Dashboard als Gestaltungsvorlage zur Übergabe
UI/UX-Prototyp der Analytics-Dashboards in HiveGrid, fachlich auf einen
SHK-/Handwerksbetrieb zugeschnitten. Ziel ist die Übergabe an die Entwicklung,
nicht der Produktivbetrieb.

Inhalt: sechs Seiten (Übersicht, Finanzen, Aufträge, Kunden, Team, Komponenten),
26 KPIs, neun Chart-Bausteine, Filtersystem mit seitenabhängiger Relevanz und ein
deterministischer Demo-Datensatz (420 Kunden, 1.900 Aufträge, Stichtag 30.07.2026).

Drei Quellen sind zusammengeführt: die fachliche Planung mit dem Branchenpartner
(Januar 2026), die Subframe-Komponentenbibliothek aus dem HiveGrid-Pitch und
IBM Carbon Data Visualization für die Diagrammregeln. Herkunft und bewusste
Abweichungen stehen in docs/HERKUNFT.md.

Dokumentation: README, HANDOVER (was echte Logik braucht), docs/DESIGN-SYSTEM,
KPI-KATALOG, DATENMODELL, NAVIGATION, HERKUNFT, ZIELSYSTEM (Einbauort in
HiveGrid Cloud) und LEITPLANKEN (welche Cloud-Screens als Vorbild taugen).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
2026-08-29 17:25:20 +02:00

133 lines
4.3 KiB
TypeScript

import type { ZeitraumPreset } from "../data/types";
/**
* Fester "Heute"-Bezugspunkt des Prototyps.
*
* Wichtig: Die Dummy-Daten sind deterministisch relativ zu diesem Datum erzeugt.
* Dadurch bleiben alle Zahlen ueber Reloads und Screenshots hinweg identisch.
* In der echten Software wird das durch `new Date()` ersetzt.
*/
export const HEUTE = new Date("2026-07-30T12:00:00");
export interface Zeitfenster {
von: Date;
bis: Date;
}
export function addTage(datum: Date, tage: number): Date {
const d = new Date(datum);
d.setDate(d.getDate() + tage);
return d;
}
export function addMonate(datum: Date, monate: number): Date {
const d = new Date(datum);
d.setMonth(d.getMonth() + monate);
return d;
}
export function toISO(datum: Date): string {
return datum.toISOString().slice(0, 10);
}
/** Zeitfenster eines Presets, bezogen auf HEUTE. */
export function zeitfenster(preset: ZeitraumPreset): Zeitfenster {
const bis = new Date(HEUTE);
bis.setHours(23, 59, 59, 999);
switch (preset) {
case "heute": {
const von = new Date(HEUTE);
von.setHours(0, 0, 0, 0);
return { von, bis };
}
case "7_tage":
return { von: addTage(bis, -7), bis };
case "30_tage":
return { von: addTage(bis, -30), bis };
case "quartal":
return { von: addMonate(bis, -3), bis };
case "jahr":
return { von: addMonate(bis, -12), bis };
case "alles":
default:
return { von: new Date("2000-01-01"), bis };
}
}
/** Gleich langes Fenster direkt davor — Basis fuer Trend-Vergleiche. */
export function vorperiode(preset: ZeitraumPreset): Zeitfenster {
const { von, bis } = zeitfenster(preset);
const dauer = bis.getTime() - von.getTime();
return {
von: new Date(von.getTime() - dauer),
bis: new Date(von.getTime() - 1),
};
}
export function imFenster(iso: string | null, fenster: Zeitfenster): boolean {
if (!iso) return false;
const t = new Date(iso).getTime();
return t >= fenster.von.getTime() && t <= fenster.bis.getTime();
}
/** ISO-Kalenderwoche als sortierbarer Schluessel: "2026-W31". */
export function isoWoche(datum: Date): string {
const d = new Date(Date.UTC(datum.getFullYear(), datum.getMonth(), datum.getDate()));
// Donnerstag derselben Woche bestimmt nach ISO 8601 Jahr und Wochennummer.
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
const jahresstart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
const woche = Math.ceil(((d.getTime() - jahresstart.getTime()) / 86_400_000 + 1) / 7);
return `${d.getUTCFullYear()}-W${String(woche).padStart(2, "0")}`;
}
/** "2026-W31" -> "KW 31" */
export function formatWoche(schluessel: string): string {
return `KW ${schluessel.slice(6)}`;
}
/**
* Passende Buckets fuer eine Zeitreihe.
*
* Die Aufloesung folgt der Frage, nicht dem Kalender: Bei rund zwei Vorgaengen
* pro Tag ist eine Tagesreihe ueber 30 Tage Rauschen, kein Verlauf. Deshalb
* Tage nur bis eine Woche, danach Kalenderwochen, ab einem Jahr Monate.
*/
export function bucketsFuer(preset: ZeitraumPreset): {
art: "tag" | "woche" | "monat";
schluessel: string[];
} {
const { von, bis } = zeitfenster(preset);
if (preset === "heute" || preset === "7_tage") {
const schluessel: string[] = [];
for (let d = new Date(von); d <= bis; d = addTage(d, 1)) schluessel.push(toISO(d));
return { art: "tag", schluessel };
}
if (preset === "30_tage" || preset === "quartal") {
const schluessel: string[] = [];
for (let d = new Date(von); d <= bis; d = addTage(d, 7)) {
const key = isoWoche(d);
if (!schluessel.includes(key)) schluessel.push(key);
}
const letzte = isoWoche(bis);
if (!schluessel.includes(letzte)) schluessel.push(letzte);
return { art: "woche", schluessel };
}
const monate: string[] = [];
const start = preset === "alles" ? addMonate(bis, -23) : von;
for (let d = new Date(start.getFullYear(), start.getMonth(), 1); d <= bis; d = addMonate(d, 1)) {
monate.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`);
}
return { art: "monat", schluessel: monate };
}
/** Bucket-Schluessel eines Datums fuer die gewaehlte Aufloesung. */
export function bucketKey(iso: string, art: "tag" | "woche" | "monat"): string {
if (art === "monat") return iso.slice(0, 7);
if (art === "woche") return isoWoche(new Date(iso));
return iso.slice(0, 10);
}