467 Stellen. Nicht durch einen anderen Strich ersetzt, sondern durch das
Satzzeichen, das an der jeweiligen Stelle richtig ist: Doppelpunkt, wo eine
Erlaeuterung folgt; Komma, wo der Satz weiterlaeuft; Punkt, wo zwei
Hauptsaetze aneinanderstiessen. Ein Halbgeviertstrich waere nur derselbe
Griff mit kuerzerem Strich gewesen.
Regelbasiert ersetzt und danach nachgeprueft, weil eine Regel allein nicht
reicht. Gefunden und einzeln korrigiert:
- Eingeschobene Saetze mit Strichpaar. Nur der oeffnende Strich passte ins
Muster, der schliessende blieb als "—," stehen. Drei Stellen, jetzt mit
Klammern oder als eigener Satz.
- Doppelte Doppelpunkte, wo beide Striche eines Paars zu Doppelpunkten wurden
("Globale Regeln: Schrift, Groessen, Abstaende: kommen aus HiveGrid").
Sieben Stellen in Doku und Oberflaeche, jetzt in Klammern.
- Pfade, denen ein Komma statt eines Doppelpunkts folgte.
- Ausgerichtete Kommentartabellen, in denen der Strich eine Spalte war.
Nicht angefasst: drei Geviertstriche, die keine Prosa sind, sondern
Tabellenplatzhalter fuer "kein Wert" (Trichter, Auftraege, Kunden). Dort ist
der Strich die Angabe selbst; ihn zu entfernen liesse leere Zellen zurueck.
tsc, lint, build, verify:carbon und verify:layout laufen sauber.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
276 lines
8.9 KiB
TypeScript
276 lines
8.9 KiB
TypeScript
/**
|
|
* View-Model des Dashboards.
|
|
*
|
|
* Das Dashboard ist **kein zweiter Analytics-Bereich**, sondern der
|
|
* Begrüßungsbildschirm: „Wie ist meine Lage?" statt „Warum ist das so?".
|
|
* Es zeigt heute ausschließlich Analytics-Kacheln, ist aber als Widget-Fläche
|
|
* gebaut: andere Bereiche (Aufgaben, Termine, HR-Anträge) kommen später als
|
|
* weitere Kacheln dazu, nicht als Umbau.
|
|
*
|
|
* Die Typen unten spiegeln `DashboardWidgetDomain`, `DashboardWidgetKind` und
|
|
* `DashboardWidgetPeriod` aus `@hivegrid/view-models` wortgleich. Beim Einbau
|
|
* wird der Import getauscht, die Kacheln bleiben.
|
|
*
|
|
* Wichtig: Anders als in Analytics gibt es **keinen seitenweiten Filter**.
|
|
* Jede Kachel trägt ihren eigenen Zeitraum und sagt ihn an. So hält es die
|
|
* Software (P11: „Edit = nur Widget-Filter/Zeitraum").
|
|
*/
|
|
|
|
import {
|
|
aktiveAuftraege,
|
|
handlungsbedarf,
|
|
offenePosten,
|
|
teamKennzahlen,
|
|
umsatz,
|
|
umsatzNachGewerk,
|
|
umsatzVerlauf,
|
|
umsatzVorperiode,
|
|
vertriebsTrichter,
|
|
type AlertEintrag,
|
|
} from "../data/calculations";
|
|
import type { ChartPunkt, Datensatz, ZeitreihePunkt } from "../data/types";
|
|
import { berechneTrend } from "../utils/formatters";
|
|
import { bereit, leer, type VMState } from "./index";
|
|
|
|
export type WidgetDomaene =
|
|
| "general"
|
|
| "sales"
|
|
| "finance"
|
|
| "projects"
|
|
| "tasks"
|
|
| "hr"
|
|
| "activity"
|
|
| "comms"
|
|
| "schedule";
|
|
|
|
export type WidgetArt = "hero" | "kpi" | "list" | "trend" | "chart";
|
|
export type WidgetZeitraum = "heute" | "30_tage" | "stichtag" | "gesamt";
|
|
|
|
export const ZEITRAUM_LABEL: Record<WidgetZeitraum, string> = {
|
|
heute: "heute",
|
|
"30_tage": "letzte 30 Tage",
|
|
stichtag: "zum Stichtag",
|
|
gesamt: "über alle Zeiträume",
|
|
};
|
|
|
|
/** Eine Kennzahl-Kachel: Wert, Einordnung, Sprung in die Analyse. */
|
|
export interface KennzahlKachel {
|
|
readonly key: string;
|
|
readonly domaene: WidgetDomaene;
|
|
readonly titel: string;
|
|
readonly wert: number;
|
|
/** Wie der Wert zu schreiben ist: Formatierung ist Darstellung, nicht Modell. */
|
|
readonly formatAls: "euro" | "zahl" | "prozent";
|
|
readonly zusatz: string;
|
|
readonly zeitraum: WidgetZeitraum;
|
|
readonly icon: string;
|
|
readonly href: string;
|
|
readonly trend?: { wert: number; richtung: "up" | "down" | "flat" };
|
|
readonly richtungGut?: "positiv" | "negativ";
|
|
/** Nur setzen, wenn die Farbe aus einem Schwellenvergleich kommt. */
|
|
readonly akzent?: "neutral" | "positiv" | "warnung" | "negativ" | "info";
|
|
}
|
|
|
|
/**
|
|
* Ein Bereich, der später eigene Kacheln bekommt.
|
|
*
|
|
* Bewusst als Datensatz und nicht als hartkodiertes Stück Oberfläche: Sobald
|
|
* das jeweilige Feature Daten liefert, wird aus dem Eintrag eine echte Kachel,
|
|
* ohne dass das Raster angefasst werden muss.
|
|
*
|
|
* Diese Bereiche werden auf dem Dashboard **nicht angezeigt**. Ein
|
|
* Begrüßungsbildschirm ist kein Ort für Ankündigungen. Sie sind eine
|
|
* Verabredung für den Einbau und stehen deshalb in der Bausteingalerie
|
|
* (`/komponenten`), nicht auf der Seite selbst.
|
|
*/
|
|
export interface ReservierterBereich {
|
|
readonly key: string;
|
|
readonly domaene: WidgetDomaene;
|
|
readonly titel: string;
|
|
readonly beschreibung: string;
|
|
readonly icon: string;
|
|
/** Woher die Daten kommen werden, als Verabredung, nicht als Versprechen. */
|
|
readonly quelle: string;
|
|
}
|
|
|
|
export interface DashboardDaten {
|
|
readonly begruessung: {
|
|
readonly anrede: string;
|
|
readonly name: string;
|
|
readonly rolle: string;
|
|
readonly datum: string;
|
|
};
|
|
/** Ein Satz zur Lage, was der Betrachter wissen muss, bevor er weiterliest. */
|
|
readonly lage: string;
|
|
readonly kennzahlen: KennzahlKachel[];
|
|
readonly handlungsbedarf: AlertEintrag[];
|
|
readonly umsatzVerlauf: ZeitreihePunkt[];
|
|
readonly umsatzGesamt: number;
|
|
readonly umsatzVorher: number;
|
|
readonly gewerkUmsatz: ChartPunkt[];
|
|
readonly trichter: ChartPunkt[];
|
|
/** Sparkline unter der Umsatzkachel: dieselbe Reihe wie in Analytics. */
|
|
readonly sparkline: number[];
|
|
readonly auslastung: { name: string; wert: number; zusatz: string }[];
|
|
}
|
|
|
|
/**
|
|
* Tageszeit-Anrede. Die Uhrzeit kommt von außen, damit diese Funktion rein
|
|
* bleibt, der Prototyp reicht seinen festen Stichtag herein, die Software
|
|
* die echte Uhr.
|
|
*/
|
|
function anredeFuer(stunde: number): string {
|
|
if (stunde < 11) return "Guten Morgen";
|
|
if (stunde < 18) return "Guten Tag";
|
|
return "Guten Abend";
|
|
}
|
|
|
|
/** Der angemeldete Benutzer, in der Software aus der Sitzung, hier eine Attrappe. */
|
|
export interface Nutzer {
|
|
readonly vorname: string;
|
|
readonly rolle: string;
|
|
}
|
|
|
|
export function baueDashboardVM(
|
|
daten: Datensatz,
|
|
nutzer: Nutzer,
|
|
jetzt: Date
|
|
): VMState<DashboardDaten> {
|
|
if (!daten.auftraege.length && !daten.rechnungen.length) return leer();
|
|
|
|
const posten = offenePosten(daten.rechnungen);
|
|
const aktiv = aktiveAuftraege(daten.auftraege);
|
|
const team = teamKennzahlen(daten.mitarbeiter, daten.zeiten, daten.auftraege, "30_tage");
|
|
const monteure = team.filter((t) => t.mitarbeiter.status === "aktiv");
|
|
const auslastung = monteure.length
|
|
? monteure.reduce((s, t) => s + t.auslastungProzent, 0) / monteure.length
|
|
: 0;
|
|
const verlauf = umsatzVerlauf(daten.rechnungen, "30_tage");
|
|
const umsatzAktuell = umsatz(daten.rechnungen, "30_tage");
|
|
const umsatzVorher = umsatzVorperiode(daten.rechnungen, "30_tage");
|
|
const alerts = handlungsbedarf(daten);
|
|
|
|
// Die Anwendung duzt. Das ist eine Festlegung fuer den ganzen Prototyp und
|
|
// nicht nur fuer diesen Satz: halb geduzt liest sich schlechter als
|
|
// durchgehend gesiezt.
|
|
const kritisch = alerts.filter((a) => a.art === "kritisch").length;
|
|
const lage = kritisch
|
|
? `${kritisch} Punkte brauchen heute deine Aufmerksamkeit.`
|
|
: "Keine kritischen Punkte offen.";
|
|
|
|
return bereit({
|
|
begruessung: {
|
|
anrede: anredeFuer(jetzt.getHours()),
|
|
name: nutzer.vorname,
|
|
rolle: nutzer.rolle,
|
|
datum: jetzt.toLocaleDateString("de-DE", {
|
|
weekday: "long",
|
|
day: "2-digit",
|
|
month: "long",
|
|
}),
|
|
},
|
|
lage,
|
|
kennzahlen: [
|
|
{
|
|
key: "umsatz",
|
|
wert: umsatzAktuell,
|
|
formatAls: "euro",
|
|
domaene: "finance",
|
|
titel: "Umsatz",
|
|
zusatz: "Zahlungseingang",
|
|
zeitraum: "30_tage",
|
|
icon: "Euro",
|
|
href: "/analytics/finanzen",
|
|
trend: berechneTrend(umsatzAktuell, umsatzVorher),
|
|
},
|
|
{
|
|
key: "forderungen",
|
|
wert: posten.gesamt,
|
|
formatAls: "euro",
|
|
domaene: "finance",
|
|
titel: "Offene Forderungen",
|
|
zusatz: `${posten.anzahl} Rechnungen · ${posten.ueberfaelligAnzahl} überfällig`,
|
|
zeitraum: "stichtag",
|
|
icon: "FileText",
|
|
href: "/analytics/finanzen",
|
|
akzent: posten.ueberfaelligAnzahl > 0 ? "warnung" : "neutral",
|
|
},
|
|
{
|
|
key: "auftraege",
|
|
wert: aktiv.length,
|
|
formatAls: "zahl",
|
|
domaene: "projects",
|
|
titel: "Aktive Aufträge",
|
|
zusatz: "beauftragt oder in Arbeit",
|
|
zeitraum: "stichtag",
|
|
icon: "ClipboardList",
|
|
href: "/analytics/auftraege",
|
|
},
|
|
{
|
|
key: "auslastung",
|
|
wert: auslastung,
|
|
formatAls: "prozent",
|
|
domaene: "hr",
|
|
titel: "Ø Auslastung",
|
|
zusatz: `${monteure.length} Personen im Einsatz`,
|
|
zeitraum: "30_tage",
|
|
icon: "HardHat",
|
|
href: "/analytics/team",
|
|
akzent: auslastung > 95 ? "warnung" : auslastung < 70 ? "info" : "positiv",
|
|
},
|
|
],
|
|
handlungsbedarf: alerts,
|
|
umsatzVerlauf: verlauf,
|
|
umsatzGesamt: umsatzAktuell,
|
|
umsatzVorher,
|
|
gewerkUmsatz: umsatzNachGewerk(daten.auftraege, daten.rechnungen, "30_tage"),
|
|
trichter: vertriebsTrichter(daten.auftraege),
|
|
sparkline: verlauf.slice(-10).map((p) => p.value),
|
|
// Sechs Zeilen: mehr passt nicht in eine halbe Kachel, ohne dass die
|
|
// Balken auf Haarbreite schrumpfen. Die vollständige Liste steht in
|
|
// Analytics, deshalb führt die Kachel dorthin.
|
|
auslastung: monteure.slice(0, 6).map((t) => ({
|
|
name: t.mitarbeiter.name,
|
|
wert: t.auslastungProzent,
|
|
zusatz: t.mitarbeiter.gewerk,
|
|
})),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Bereiche, die das Dashboard später trägt.
|
|
*
|
|
* Alle drei sind im Designer-Brief P11 für die Rollen-Dashboards benannt
|
|
* („Büro: … meine Tasks, Anrufe heute, HR-Anträge") und haben in HiveGrid
|
|
* bereits ihre Widget-Domäne. Sie stehen hier, damit beim Einbau feststeht,
|
|
* was noch dazukommt und wo, und damit niemand das Raster dafür umbaut.
|
|
*
|
|
* Gezeigt werden sie in der Bausteingalerie unter `/komponenten`.
|
|
*/
|
|
export const RESERVIERTE_BEREICHE: ReservierterBereich[] = [
|
|
{
|
|
key: "tasks",
|
|
domaene: "tasks",
|
|
titel: "Meine Aufgaben",
|
|
beschreibung: "Was heute ansteht und wer zugewiesen ist.",
|
|
icon: "CheckSquare",
|
|
quelle: "Aufgaben",
|
|
},
|
|
{
|
|
key: "schedule",
|
|
domaene: "schedule",
|
|
titel: "Termine heute",
|
|
beschreibung: "Einsätze, Aufmaße und Abnahmen des Tages.",
|
|
icon: "Calendar",
|
|
quelle: "Termine",
|
|
},
|
|
{
|
|
key: "hr",
|
|
domaene: "hr",
|
|
titel: "HR-Anträge",
|
|
beschreibung: "Offene Urlaubs- und Zeitkorrekturanträge.",
|
|
icon: "UserCheck",
|
|
quelle: "HR-Hub",
|
|
},
|
|
];
|