diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..26969b0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,66 @@ +# Dashboard-Prototyp: Einstieg für Coding-Agenten + +Dieses Repo ist **kein Produkt und keine Bibliothek**. Es ist eine +Gestaltungsvorlage, die in `HiveGrid-Cloud` eingebaut wird. + +## Wenn du hier etwas ändern sollst + +Lies zuerst `README.md`, dann die Datei zum Thema in `docs/`. Vor jedem Commit: + +```bash +npx tsc --noEmit && npm run lint && npm run build +npm run verify:carbon # Palette, Tooltip, Achsenraster gegen @carbon/charts +npm run verify:layout # Textkollisionen, 7 Seiten x 9 Breiten (Server muss laufen) +``` + +Beide `verify:*` sind keine Formsache. `verify:layout` hat in dieser Sitzung +mehrfach Fehler gefunden, die auf einem Screenshot nicht zu sehen waren. + +## Wenn du das hier in HiveGrid einbauen sollst + +**→ `EINBAU.md` lesen, vollständig, vor der ersten Änderung.** + +Die drei Dinge, an denen ein Einbau sonst scheitert: + +1. **Das Dashboard existiert in HiveGrid bereits**, mit Route, + Widget-Registry, Rollen-Layouts, Drilldown, Export und Live-Updates. Dieses + Repo liefert die *Gestaltung*, nicht den Rahmen. Wer dort eine neue Seite + anlegt, wirft funktionierende Software weg. Analytics dagegen gibt es noch + nicht und wird neu gebaut. + +2. **HiveGrid gewinnt bei allen globalen Regeln**: Schrift, Größen, Abstände, + Farben, Ansprache. Was dieses Repo dazu setzt, existiert nur, damit es + allein lauffähig ist. Verbindlich sind hier die *Diagrammregeln* und die + *Kennzahl-Definitionen*. + +3. **Alle Texte sind hartkodiertes Deutsch**, HiveGrid verlangt `t()` und + prüft das mit `pnpm run verify:i18n`. Die Schlüsselliste liegt fertig unter + `docs/port/`. + +## Zwei Ticket-Docs liegen bereit + +`docs/port/TICKET-ANALYTICS.md` und `docs/port/TICKET-DASHBOARD.md` sind im +Format von `.planning/ticket-docs/` geschrieben und können dort hineinkopiert +werden. Dann greift der normale HiveGrid-Ablauf. + +## Wo finde ich was + +| Was | Wo | +|---|---| +| **Einbau in HiveGrid** | `EINBAU.md` | +| Ticket-Docs für den Einbau | `docs/port/TICKET-*.md` | +| Texte und i18n-Schlüssel | `docs/port/TEXTE.md`, `i18n-de.json` | +| Was übergeben wird, was Attrappe ist | `HANDOVER.md` | +| Diagramm- und Farbregeln | `docs/DESIGN-SYSTEM.md` | +| Jede Kennzahl mit Formel und Zeitbezug | `docs/KPI-KATALOG.md` | +| Feldherkunft, Lücken im Modell | `docs/DATENMODELL.md` | +| Rangfolge Prototyp ↔ HiveGrid | `docs/ZIELSYSTEM.md` Abschnitt 0 | +| Zuschnitt Analytics ↔ Dashboard | `docs/SCOPE-ANALYTICS-DASHBOARD.md` | +| Navigation und Seitenaufbau | `docs/NAVIGATION.md` | +| Fertige vs. Interim-UI im Zielsystem | `docs/LEITPLANKEN.md` | +| Woher die Gestaltung stammt | `docs/HERKUNFT.md` | +| Fragen für die Abnahme | `docs/PRUEFFRAGEN.md` | + +## Sprache +Code-Kommentare, Doku und Oberfläche sind auf Deutsch. Die Anwendung duzt. +Keine Geviertstriche. diff --git a/EINBAU.md b/EINBAU.md new file mode 100644 index 0000000..78a40d3 --- /dev/null +++ b/EINBAU.md @@ -0,0 +1,172 @@ +# Einbau in HiveGrid Cloud + +Diese Datei richtet sich an den Entwickler und den Coding-Agenten, die diesen +Prototyp in `HiveGrid-Cloud` einbauen. Sie ist der Einstieg; alles andere in +`docs/` vertieft nur. + +**Reihenfolge zum Lesen:** diese Datei ganz → dann `docs/port/TICKET-ANALYTICS.md` +oder `docs/port/TICKET-DASHBOARD.md`, je nachdem, was du baust. + +--- + +## 0. Das Wichtigste zuerst: Es steht schon etwas da + +**Das Dashboard in HiveGrid ist kein leeres Blatt.** Wer es als „neue Seite +bauen" anfasst, zerstört funktionierende Software. Vorhanden sind: + +| Was | Wo | +|---|---| +| Route mit Loader, Action, Export, Stream | `apps/web/app/routes/app.$tenant/dashboard.tsx` | +| Widget-Registry mit Gating je Modul | `apps/web/app/services/dashboard-registry/registry.server.ts` | +| Rollen-Layouts, Drilldown, Live-Updates, Layout-Speicherung | `apps/web/app/services/dashboard/dashboard.server.ts` | +| Screen mit Widget-Renderer und 12-Spalten-Raster | `apps/web/app/components/screens/dashboard/DashboardPage.tsx` | +| View-Model-Vertrag | `packages/view-models/src/dashboard/dashboard-page.vm.ts` | +| Übersetzungen für 17 Widgets | `apps/web/public/locales/de/translations.json` → `app.dashboard.widgets.*` | + +Dazu kommen Berechtigungen (`dashboard.read`), Audit-Events und der +First-Steps-Checklist-Widget. + +Das ist nicht meine Einschätzung, sondern die eurer eigenen Planung. Der Brief +`.planning/designer-briefs/P30-dashboard-deluxe-querschnitt.md` sagt es +wörtlich: + +> **Delta-Phase, kein Neubau:** M20/M22/M30 sind großteils v1-fähig +> (Dashboard/Widget-Registry voll gebaut, notify() vorhanden, i18n-Library + +> DE-Locale da). + +**Was dieser Prototyp beisteuert, ist Gestaltung, nicht Infrastruktur.** +Konkret: wie eine Kachel aussieht, welche Kennzahlen zusammengehören, in +welcher Reihenfolge sie stehen, wie ein Diagramm gezeichnet wird. Der Rahmen +darum existiert und bleibt. + +Für **Analytics** gilt das Gegenteil: Diesen Bereich gibt es noch nicht. Er +wird neu gebaut, nach dem Muster von `leads.analytics.tsx`. + +--- + +## 1. Zwei Arbeitspakete, nicht eines + +| | **Analytics** | **Dashboard** | +|---|---|---| +| Art | neuer Bereich | bestehenden Screen ergänzen | +| Route | neu: `app.$tenant/analytics*` | vorhanden | +| Screen | neu | vorhanden, wird erweitert | +| Sidebar | ein neuer Eintrag | Eintrag existiert | +| Aufwand | groß | klein bis mittel | +| Ticket-Doc | `docs/port/TICKET-ANALYTICS.md` | `docs/port/TICKET-DASHBOARD.md` | + +Sie sind unabhängig voneinander baubar. Analytics zuerst ist die einfachere +Reihenfolge, weil das Dashboard Diagramme aus Analytics benutzt. + +--- + +## 2. Was portiert wird und was nicht + +### Portiert wird + +| Aus diesem Repo | Wird in HiveGrid zu | +|---|---| +| `features/dashboard/data/calculations.ts` | Server-Selektoren, z. B. `apps/web/app/services/analytics/analytics.server.ts` | +| `features/dashboard/view-models/*.vm.ts` | `packages/view-models/src/analytics/*.vm.ts` | +| `features/dashboard/pages/*Seite.tsx` | `apps/web/app/components/screens/analytics/*Page.tsx` | +| `features/dashboard/components/charts/*` | `apps/web/app/components/screens/analytics/charts/*` | +| `features/dashboard/components/cards/*` | dito, `cards/` | +| `features/dashboard/components/tables/DataTable.tsx` | dito, oder gegen eine vorhandene Tabelle tauschen | +| `features/dashboard/utils/chartTheme.ts` | `apps/web/app/components/screens/analytics/chartTheme.ts` | +| `features/dashboard/utils/formatters.ts` | prüfen: `packages/foundation` hat teilweise Entsprechungen | + +### Portiert wird **nicht** + +| Datei | Warum | +|---|---| +| `components/layout/AppShell.tsx` | Nachbau der Seitenleiste. In HiveGrid übernimmt `SubframeAppShell` + `AppSidebar`. | +| `components/layout/AnalyticsInnerNav.tsx` | Nachbau. In HiveGrid ist das `SidebarIconsLayout`, das Leads, Termine, HR und Einstellungen schon benutzen. | +| `components/layout/PageHeader.tsx` | HiveGrid hat `PageHeader` und `PageShell`. | +| `app/globals.css`, Schriftdateien | Globale Regeln kommen ausschließlich aus HiveGrid. | +| `data/seed.ts`, `data/nutzer.ts` | Attrappen. Ersetzt durch echte Abfragen und `requireMember`. | +| `hooks/useAnalyseDaten.ts`, `context/FilterContext.tsx` | Prototyp-Verdrahtung. Ersetzt durch Loader und URL-Parameter. | +| `pages/KomponentenSeite.tsx` | Bausteingalerie für die Abstimmung, kein Produktfeature. | +| `routes/*Route.tsx` | Ersetzt durch die Loader der Software. | + +**Die Trennung ist mit Absicht so gebaut:** Die Seiten unter `pages/` sind reine +Komponenten mit der Signatur `({ vm })`. Sie holen sich nichts selbst. Deshalb +lässt sich der Route-Container ersetzen, ohne die Seite anzufassen. + +--- + +## 3. Der Stolperstein, der jede Datei betrifft: Texte + +**`pnpm run verify:i18n` ist ein Gate in eurer CI: keine hartkodierten Strings.** +Dieser Prototyp ist zu 100 % hartkodiertes Deutsch. Das ist der größte +mechanische Posten am ganzen Einbau, und er ist hier vorbereitet: + +| Datei | Inhalt | +|---|---| +| `docs/port/i18n-inventar.json` | 185 sichtbare Texte, je mit Schlüsselvorschlag und Fundstelle im Prototyp | +| `docs/port/i18n-de.json` | dieselben Texte als verschachtelter Baum, direkt nach `apps/web/public/locales/de/translations.json` einfügbar | +| `docs/port/TEXTE.md` | Erläuterung, Namensschema, und welche Schlüssel es in HiveGrid **schon gibt** | + +**Vor dem Anlegen neuer Schlüssel: `app.dashboard.widgets.*` prüfen.** Dort +stehen bereits 17 Widgets, darunter genau die, die dieser Prototyp gestaltet +(`receivables`, `orderIntake`, `daysToPayment`, `winRate`, `stageDistribution`) +und genau die drei, die er als reserviert markiert (`myTasks`, `hrRequests`, +`scheduleToday`). Neue Schlüssel dafür wären Dubletten. + +Die Anrede stimmt bereits überein: HiveGrid duzt („Termine und Buchungen für +deinen Tag"), dieser Prototyp auch. + +--- + +## 4. Rangfolge bei Widersprüchen + +1. **HiveGrid gewinnt immer bei globalen Regeln.** Schrift, Schriftgrößen, + Abstände, Farben, Control-Höhen, Ansprache. Dieser Prototyp setzt sie nur, + damit er allein lauffähig ist. +2. **Der Prototyp gewinnt bei Diagrammregeln**: Achsen, Palette, Tooltip, + Kategoriengrenzen, Leerzustände. Das ist der Teil, den es in HiveGrid noch + nicht gibt, und er ist gegen IBM Carbon geprüft (`npm run verify:carbon`). +3. **Bei Kennzahl-Definitionen gilt `docs/KPI-KATALOG.md`**, aber nur, solange + es dem Datenmodell nicht widerspricht. `.planning/ENTITY-MODEL.md` hat + Vorrang (AD-20, projektzentrisch). + +Ausführlich: `docs/ZIELSYSTEM.md` Abschnitt 0. + +--- + +## 5. Ablauf + +Beide Ticket-Docs sind im Format eurer `.planning/ticket-docs/` geschrieben und +können dort hineinkopiert werden. Dann greift euer normaler Ablauf: Agent +bekommt die Ticket-ID, liest die Doc, arbeitet die Checkliste ab +(**in HiveGrid**: `docs/AGENT-CHECKLIST.md`). + +``` +1. docs/port/TICKET-ANALYTICS.md → .planning/ticket-docs/ +2. docs/port/TICKET-DASHBOARD.md → .planning/ticket-docs/ +3. IDs in Plane anlegen, Docs verlinken +4. Agent wie gewohnt starten +``` + +--- + +## 6. Womit du prüfst, ob es stimmt + +In HiveGrid, aus dessen `docs/AGENT-CHECKLIST.md`: + +```bash +pnpm run verify:i18n # keine hartkodierten Strings +pnpm run lint +pnpm run test +pnpm run verify:push +``` + +Zusätzlich aus diesem Repo übernehmbar, beide ohne HiveGrid-Abhängigkeit: + +```bash +npm run verify:carbon # Palette, Tooltip, Achsenraster gegen @carbon/charts +npm run verify:layout # Textkollisionen über 7 Seiten x 9 Breiten +``` + +`scripts/verify-layout.mjs` braucht nur eine laufende URL und eine Seitenliste. +Er ist die billigste Absicherung dagegen, dass beim Einbau in andere Container +Text ineinanderläuft, und lohnt die zehn Minuten Anpassung. diff --git a/HANDOVER.md b/HANDOVER.md index 8d4b533..9e6ea90 100644 --- a/HANDOVER.md +++ b/HANDOVER.md @@ -9,6 +9,9 @@ > Navigationsstruktur als Verabredung. Ausführlich: > [`docs/ZIELSYSTEM.md`](docs/ZIELSYSTEM.md) Abschnitt 0. > +> **Der Einbau selbst steht in [`EINBAU.md`](EINBAU.md)**: Zielpfade, +> Ticket-Docs im Format von `.planning/ticket-docs/`, i18n-Schlüssel. +> > **Ebenfalls vor dem Einbau lesen:** > [`docs/SCOPE-ANALYTICS-DASHBOARD.md`](docs/SCOPE-ANALYTICS-DASHBOARD.md), > insbesondere Abschnitt 3a. Der Handlungsbedarf steht dort **absichtlich diff --git a/README.md b/README.md index d673051..f90148f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # HiveGrid Analytics Dashboard: UI/UX-Prototyp +> **Wenn du das hier einbaust: [`EINBAU.md`](EINBAU.md) zuerst lesen.** +> Dort steht, was in HiveGrid schon existiert (das Dashboard ist kein leeres +> Blatt), welche Datei wohin wandert, was ausdrücklich *nicht* mitwandert, und +> wo die 185 Oberflächentexte mit fertigen i18n-Schlüsseln liegen. + Gestaltungsvorlage für die Analytics-Dashboards in HiveGrid, fachlich auf einen SHK-/Handwerksbetrieb zugeschnitten. **Ziel dieses Repos ist die Übergabe an die Entwicklung**, nicht der Produktivbetrieb. diff --git a/docs/port/TEXTE.md b/docs/port/TEXTE.md new file mode 100644 index 0000000..45a2eb2 --- /dev/null +++ b/docs/port/TEXTE.md @@ -0,0 +1,117 @@ +# Texte und Übersetzungsschlüssel + +`pnpm run verify:i18n` ist in HiveGrid ein Gate: keine hartkodierten Strings. +Dieser Prototyp ist zu 100 % hartkodiertes Deutsch, weil er allein lauffähig +sein muss. Damit ist die Übersetzung der größte mechanische Posten am Einbau. +Diese drei Dateien nehmen ihn ab. + +| Datei | Wofür | +|---|---| +| `i18n-inventar.json` | 185 Einträge: Schlüssel, deutscher Text, Fundstelle im Prototyp | +| `i18n-de.json` | dieselben Texte als Baum, nach `apps/web/public/locales/de/translations.json` einfügbar | +| diese Datei | Namensschema, Grenzen, und was in HiveGrid schon existiert | + +--- + +## 1. Zuerst prüfen: vieles gibt es schon + +`app.dashboard.widgets.*` in HiveGrid enthält bereits 17 Widgets. Fünf davon +sind genau die Kacheln, die dieser Prototyp gestaltet, drei weitere genau die, +die er als „kommt noch" markiert: + +| Schlüssel in HiveGrid | Deutscher Text dort | Entspricht im Prototyp | +|---|---|---| +| `receivables` | Offene Posten | Kennzahl „Offene Forderungen", Finanzen-Diagramm | +| `orderIntake` | Auftragseingang | Aufträge-Seite, Säulendiagramm | +| `daysToPayment` | Zahlungsdauer | Kennzahl in der Statistikzeile | +| `winRate` | Auftragsquote | Trichter-Übergangsquote | +| `stageDistribution` | Offene Projekte nach Phase | Vertriebstrichter | +| `myTasks` | Meine Aufgaben | reservierte Kachel | +| `scheduleToday` | Heute | reservierte Kachel „Termine heute" | +| `hrRequests` | HR-Anträge | reservierte Kachel | + +**Für diese acht keine neuen Schlüssel anlegen.** Die Vorschläge im Inventar +sind dann zu verwerfen und durch den bestehenden Schlüssel zu ersetzen. + +Die Anrede passt bereits zusammen: HiveGrid duzt („Termine und Buchungen für +deinen Tag"), dieser Prototyp seit dem 31.08. auch. + +--- + +## 2. Namensschema der Vorschläge + +``` +app... + +app.analytics.finanzen.umsatzNachGewerk +app.dashboard.page.handlungsbedarf +app.shared.chart.keineDatenGewaehlten +``` + +`` ist `analytics`, `dashboard` oder `shared`, `` die Seite +oder der Baustein. Die Kurzform ist maschinell aus dem deutschen Text gebildet +und **ausdrücklich ein Vorschlag**. Sie ist eindeutig und kollisionsfrei, aber +kein Ersatz für einen Blick. Wo HiveGrid schon eine bessere Benennung hat, gilt +die. + +Verteilung: + +| `app.dashboard.page.*` | 28 | +| `app.analytics.auftraege.*` | 26 | +| `app.analytics.kunden.*` | 26 | +| `app.analytics.finanzen.*` | 24 | +| `app.analytics.team.*` | 22 | +| `app.analytics.alerts.*` | 13 | +| `app.analytics.nav.*` | 12 | +| `app.analytics.uebersicht.*` | 8 | +| `app.shared.chart.*` | 6 | +| `app.analytics.filter.*` | 5 | +| `app.analytics.actions.*` | 5 | +| `app.analytics.shell.*` | 4 | +| `app.shared.table.*` | 3 | +| `app.shared.alerts.*` | 1 | +| `app.shared.kpi.*` | 1 | +| `app.dashboard.widget.*` | 1 | +| `app.shared.state.*` | 1 | + +--- + +## 3. Was das Inventar nicht abdeckt + +**Texte mit Zahlen darin.** Sätze wie „25 überfällige Rechnungen" oder +„Ältester seit 99 Tagen fällig" entstehen im Prototyp durch +String-Interpolation in `data/calculations.ts`. Sie brauchen in HiveGrid +i18next-Platzhalter und teils Pluralformen: + +```json +"ueberfaelligeRechnungen": "{{count}} überfällige Rechnung", +"ueberfaelligeRechnungen_other": "{{count}} überfällige Rechnungen" +``` + +Im Inventar stehen sie mit der Musterzeichenkette, nicht mit Platzhaltern. +Betroffen ist vor allem `app.analytics.alerts.*` (13 Einträge). + +**Die Bausteingalerie.** `pages/KomponentenSeite.tsx` ist bewusst +ausgenommen, sie wird nicht portiert. + +**Zahlen- und Datumsformate.** Die stecken in `utils/formatters.ts` und sind +keine Übersetzung, sondern Formatierung. HiveGrid hat dafür eigene Helfer unter +`apps/web/app/locale/format.ts`; die des Prototyps sind zu vergleichen, nicht +zu übernehmen. + +--- + +## 4. Wie das Inventar entstanden ist + +Maschinell, mit `npm run i18n:inventar` (`scripts/i18n-inventar.py`): alle +Zeichenketten in den Text tragenden Props (`titel`, `untertitel`, +`beschreibung`, `label`, `hinweis`, `zusatz`, `aktion`, …) und alle Textknoten +in JSX. Kommentare, die Bausteingalerie und die Daten-Attrappen sind +ausgenommen. + +Der Befehl ist wiederholbar: Ändert sich am Prototyp noch etwas, erzeugt er +beide Dateien neu, statt dass sie auseinanderlaufen. + +Das heißt auch: **Vollständigkeit ist wahrscheinlich, aber nicht bewiesen.** +Der verlässliche Nachweis ist `pnpm run verify:i18n` in HiveGrid. Was das +Inventar übersieht, findet dieses Gate. diff --git a/docs/port/TICKET-ANALYTICS.md b/docs/port/TICKET-ANALYTICS.md new file mode 100644 index 0000000..fdc54f5 --- /dev/null +++ b/docs/port/TICKET-ANALYTICS.md @@ -0,0 +1,88 @@ +# ANA-1 · Analytics-Bereich aus dem Dashboard-Prototyp einbauen + +| | | +|---|---| +| **Typ** | Dev (UI + View-Models + Server-Selektoren) | +| **Owner** | *(offen)* | +| **Phase** | *(offen)* | +| **Cycle/Fenster** | *(offen)* | +| **Status** | geplant | + +> **Vorlage:** `HiveGrid-Cloud`-fremdes Repo `toolctrl-labs/Dashboard`. Dort ist +> der Bereich vollständig gestaltet und lauffähig. Diese Doc destilliert daraus, +> was in HiveGrid entsteht. Einstieg dort: `EINBAU.md`. + +## Abhängigkeiten & Parallelität +- **Blocked by:** nichts. Alle benötigten Daten liegen im bestehenden Modell. +- **Blocks:** `ANA-2` (Dashboard) benutzt vier Diagramme aus diesem Ticket. +- **Läuft parallel zu:** allem außer `ANA-2`. Neuer Bereich, keine bestehende + Datei wird umgebaut, mit **zwei Ausnahmen**: `AppSidebar.tsx` (ein Eintrag) + und `uiRegistry.ts` (eine Nav-ID). + +## Was wir bauen +Einen Analytics-Bereich als **einzelnen Sidebar-Eintrag** mit fünf Analyse-Seiten, +die über eine innere Leiste erreichbar sind: Übersicht, Finanzen, Aufträge, +Kunden, Team. Ein Zeitraumfilter und fünf Dimensionsfilter wirken seitenweit. + +Das Muster dafür existiert bereits zweimal im Haus: `leads.analytics.tsx` für +Route und Loader, `SidebarIconsLayout` für die innere Leiste (Leads, Termine, HR +und Einstellungen benutzen sie). + +## Benötigte Screens +Keine neuen Designs nötig. Der Prototyp liefert alle Seiten fertig gestaltet, +gegen IBM Carbon geprüft und über neun Fensterbreiten auf Kollisionsfreiheit +gemessen. + +## Definition of Done +- [ ] Fünf Routen unter `apps/web/app/routes/app.$tenant/analytics*.tsx`, je mit + Loader nach dem Muster von `leads.analytics.tsx` +- [ ] Fünf Screens unter `apps/web/app/components/screens/analytics/`, rein + (`{ vm }`), ohne eigene Datenbeschaffung +- [ ] View-Models unter `packages/view-models/src/analytics/`, mit Tests wie + `lead-analytics.vm.test.ts` +- [ ] Server-Selektoren aus `data/calculations.ts` übersetzt, tenant-gescoped, + RLS-konform +- [ ] Innere Navigation über das vorhandene `SidebarIconsLayout`, **nicht** über + den Nachbau aus dem Prototyp +- [ ] Ein Sidebar-Eintrag in `AppSidebar.tsx` (`overviewItems`), Nav-ID in + `uiRegistry.ts`, Berechtigung analog `dashboard.read` +- [ ] Alle Texte über `t()`, Schlüssel aus `docs/port/i18n-de.json` des + Prototyps; `pnpm run verify:i18n` grün +- [ ] Alle fünf `VMState`-Zustände bedient (idle · loading · ready · empty · error) +- [ ] `pnpm run lint`, `pnpm run test`, `pnpm run verify:push` grün + +## Technische Bausteine +| Baustein | Quelle im Prototyp | Ziel | +|---|---|---| +| Kennzahl-Berechnungen | `features/dashboard/data/calculations.ts` | `apps/web/app/services/analytics/analytics.server.ts` | +| View-Models | `features/dashboard/view-models/*.vm.ts` | `packages/view-models/src/analytics/` | +| Seiten | `features/dashboard/pages/*Seite.tsx` | `apps/web/app/components/screens/analytics/` | +| Diagramme | `features/dashboard/components/charts/` | dito, `charts/` | +| Chart-Theme (Carbon) | `features/dashboard/utils/chartTheme.ts` | dito | +| Innere Leiste | *(nicht portieren)* | `~/components/ui/layouts/SidebarIconsLayout` | +| Seitenrahmen | *(nicht portieren)* | `~/components/screens/shared/PageShell` | + +**Filter:** Im Prototyp ein `localStorage`-Store, damit er ohne Server läuft. +In HiveGrid gehören sie in die URL. Dann sind Ansichten teilbar und der Loader +kann serverseitig filtern. `leads.analytics.tsx` macht das bereits vor +(`parseLeadAnalyticsFilters(new URL(request.url), new Date())`). + +**Datenmodell:** `docs/DATENMODELL.md` im Prototyp nennt je Kennzahl die +Herkunft und markiert, wo der Prototyp mehr annimmt als das Modell hergibt. +Bei Widerspruch gilt `.planning/ENTITY-MODEL.md` (AD-20). + +## Quellen (granular) +- `toolctrl-labs/Dashboard` → `EINBAU.md`, `docs/KPI-KATALOG.md` (jede Kennzahl + mit Formel und Zeitbezug), `docs/DESIGN-SYSTEM.md` (Diagrammregeln), + `docs/DATENMODELL.md` (Feldherkunft), `docs/NAVIGATION.md` +- Muster im Haus: `apps/web/app/routes/app.$tenant/leads.analytics.tsx`, + `packages/view-models/src/leads/lead-analytics.vm.ts` +- `.planning/ENTITY-MODEL.md` → Vorrang bei Datenfragen +- `.planning/designer-briefs/P11-suche-dashboard.md` → Suche und Dashboard + +## Dev-Nachzug +- **Team-Seite:** braucht Zeiterfassung. Die steht im v2-Backlog. Ohne sie + bleibt die Seite ohne Datenquelle. Entweder mitziehen oder die Seite in + diesem Ticket weglassen. Im Prototyp ist sie gestaltet und wartet. +- **Übersichtsseite:** überschneidet sich inhaltlich mit dem Dashboard. Vor dem + Bau entscheiden, siehe `docs/SCOPE-ANALYTICS-DASHBOARD.md` Abschnitt 5.1. diff --git a/docs/port/TICKET-DASHBOARD.md b/docs/port/TICKET-DASHBOARD.md new file mode 100644 index 0000000..da429a3 --- /dev/null +++ b/docs/port/TICKET-DASHBOARD.md @@ -0,0 +1,88 @@ +# ANA-2 · Dashboard-Gestaltung aus dem Prototyp übernehmen + +| | | +|---|---| +| **Typ** | Dev (UI, bestehenden Screen erweitern) | +| **Owner** | *(offen)* | +| **Phase** | *(offen)* | +| **Cycle/Fenster** | *(offen)* | +| **Status** | geplant | + +> **Achtung, zuerst lesen:** Das Dashboard existiert bereits vollständig: +> Route, Widget-Registry, Rollen-Layouts, Drilldown, Export, Live-Updates, +> Layout-Speicherung, Berechtigungen. Dieses Ticket **gestaltet**, es baut +> nicht neu. Wer hier eine neue Seite anlegt, wirft funktionierende Software weg. + +## Abhängigkeiten & Parallelität +- **Blocked by:** `ANA-1`, vier der Kacheln zeigen Diagramme aus Analytics. +- **Blocks:** nichts. +- **Läuft parallel zu:** allem. Berührt `DashboardPage.tsx` und die + Widget-Definitionen, sonst nichts. + +## Was wir bauen +Die Kacheln des Begrüßungsbildschirms bekommen die Gestaltung aus dem +Prototyp: Begrüßung, vier Kennzahlen mit Absprung in die Analyse, +Handlungsbedarf über die volle Breite, vier Diagramme. + +Das 12-Spalten-Raster bleibt unangetastet. Der Prototyp hat es aus +`DashboardPage.tsx` übernommen; die Kachelbreiten dort (`breite`/`start`) +sind eins zu eins `widget.size.w` und `widget.position.x`. + +## Benötigte Screens +Keine neuen Designs. Der Prototyp zeigt den fertigen Stand unter `/dashboard`. + +## Definition of Done +- [ ] Kachelrahmen entspricht dem Prototyp: Titel, Beschreibung, Domänen-Icon, + **Zeitraum im Kopf**, Absprung als Fußzeile +- [ ] Die vier Kennzahlen sind Links in die zugehörige Analyse-Seite + (`widget.href`, wie es die `StatsCard` heute schon kann) +- [ ] Handlungsbedarf-Liste als Widget über die volle Breite +- [ ] Vier Diagramme aus `ANA-1` als Widgets eingehängt +- [ ] Kein seitenweiter Filter. Jede Kachel nennt ihren Zeitraum selbst + (P11: „Edit = nur Widget-Filter/Zeitraum") +- [ ] Bestehende Funktionen unverändert lauffähig: Rollenwechsel, + Bearbeitungsmodus, Drilldown, Export, Live-Updates, Gating +- [ ] `pnpm run verify:i18n`, `lint`, `test`, `verify:push` grün + +## Technische Bausteine +| Baustein | Quelle im Prototyp | Ziel | +|---|---|---| +| Kachelrahmen | `components/dashboard/WidgetKachel.tsx` | Widget-Renderer in `DashboardPage.tsx` | +| Begrüßung (`hero`) | `components/dashboard/Begruessung.tsx` | `hero`-Zweig in `DashboardPage.tsx` | +| Kachelraster | `components/dashboard/WidgetRaster.tsx` | **vorhanden**, nur abgleichen | +| Kennzahl-Kachel | `components/cards/KPICard.tsx` | `StatsCard` bzw. deren Gestaltung | +| Handlungsbedarf | `components/cards/AlertList.tsx` | neues `list`-Widget | + +**Eine Zeile fehlt im Raster.** `dashboardWidgetGridClass` kennt keinen Fall +`position.x === 9`, weil bisher keine Rolle vier Kacheln nebeneinander stellt. +Die Kennzahlenreihe tut das. Ohne die Zeile fließt die vierte Kachel dank +`grid-flow-row-dense` trotzdem richtig, nur ohne festen Anfang. Vermerkt in +`components/dashboard/WidgetRaster.tsx` des Prototyps. + +**Übersetzungen: erst suchen, dann anlegen.** `app.dashboard.widgets.*` enthält +bereits `receivables`, `orderIntake`, `daysToPayment`, `winRate` und +`stageDistribution`. Das sind genau die Kacheln aus diesem Ticket. Neue +Schlüssel dafür wären Dubletten. + +## Quellen (granular) +- `toolctrl-labs/Dashboard` → `EINBAU.md`, `docs/SCOPE-ANALYTICS-DASHBOARD.md` + (Abschnitt 3a: was gebaut wurde und warum), `docs/DESIGN-SYSTEM.md` +- `.planning/designer-briefs/P11-suche-dashboard.md` → Rollen-Layouts GF / Büro / Monteur +- `.planning/designer-briefs/P30-dashboard-deluxe-querschnitt.md` → bestätigt die + Ausgangslage wörtlich: „Delta-Phase, kein Neubau … Dashboard/Widget-Registry + voll gebaut" +- Bestehender Code: `apps/web/app/components/screens/dashboard/DashboardPage.tsx`, + `services/dashboard/dashboard.server.ts`, `services/dashboard-registry/registry.server.ts` + +## Dev-Nachzug +- **Handlungsbedarf steht doppelt.** Im Prototyp absichtlich: auf dem Dashboard + und auf der Analytics-Übersicht. Das Entfernen hätte eine offene Produktfrage + mitentschieden. **Vor dem Port auflösen, nicht übernehmen.** Siehe + `docs/SCOPE-ANALYTICS-DASHBOARD.md` Abschnitt 3a. +- **Drei Bereiche kommen später:** eigene Aufgaben, Termine des Tages, + HR-Anträge. Alle drei haben in HiveGrid schon Widget-Domäne **und** + Übersetzungsschlüssel (`myTasks`, `scheduleToday`, `hrRequests`). Im + Prototyp sind sie gestaltet unter `/komponenten`; die Stelle im Raster ist in + `pages/DashboardSeite.tsx` als Kommentarblock markiert. +- **Rollen Büro und Monteur** sind nicht gestaltet. Sie entstehen aus denselben + Kacheln in anderer Zusammenstellung; das ist eine Produktentscheidung. diff --git a/docs/port/i18n-de.json b/docs/port/i18n-de.json new file mode 100644 index 0000000..4524f50 --- /dev/null +++ b/docs/port/i18n-de.json @@ -0,0 +1,229 @@ +{ + "app": { + "shared": { + "alerts": { + "keineOffenenPunkte": "Keine offenen Punkte" + }, + "kpi": { + "vorperiode": "Vorperiode" + }, + "chart": { + "aktuellerZeitraum": "Aktueller Zeitraum", + "vorjahr": "Vorjahr", + "wert": "Wert", + "keineDatenGewaehltenZeitraum": "Keine Daten im gewählten Zeitraum", + "161616": "#161616", + "sonstige": "Sonstige" + }, + "state": { + "kennzahlenBerechnet": "Kennzahlen werden berechnet …" + }, + "table": { + "vorherigeSeite": "Vorherige Seite", + "naechsteSeite": "Nächste Seite", + "keineEintraege": "Keine Einträge" + } + }, + "dashboard": { + "widget": { + "detailsAnsehen": "Details ansehen" + }, + "page": { + "handlungsbedarf": "Handlungsbedarf", + "sortiertDringlichkeitJedeZeile": "Sortiert nach Dringlichkeit, jede Zeile führt in die zugehörige Arbeitsliste", + "zahlungseingangZeitverlauf": "Zahlungseingang im Zeitverlauf", + "aktuellerZeitraumGegenVorjahreszeitraum": "Aktueller Zeitraum gegen den Vorjahreszeitraum", + "gezaehltTagZahlungseingangsNicht": "Gezählt wird der Tag des Zahlungseingangs, nicht das Rechnungsdatum. Dadurch entspricht die Kurve dem tatsächlichen Geldfluss.", + "finanzenAuswerten": "In den Finanzen auswerten", + "umsatzGewerk": "Umsatz nach Gewerk", + "zahlungseingangLetzten30Tage": "Zahlungseingang der letzten 30 Tage", + "fuenfGewerkeFesterRangfolge": "Fünf Gewerke in fester Rangfolge: dieselbe Kodierung wie auf der Finanzseite.", + "angebotsgeschaeftAb6000": "Angebotsgeschäft ab 6.000 €, zum Stichtag", + "nurVorhabenFuerSchriftliches": "Nur Vorhaben, für die ein schriftliches Angebot erstellt wird. Wartung und Notdienst laufen ohne Angebot und würden die Quoten verfälschen.", + "auftraegeAuswerten": "Aufträge auswerten", + "auslastungMonteure": "Auslastung Monteure", + "produktiveStundenGegenVerfuegbare": "Produktive Stunden gegen verfügbare Stunden", + "nurAuftraegeGebuchteStunden": "Nur auf Aufträge gebuchte Stunden zählen als produktiv. Über 100 % bedeutet Überstunden.", + "ganzesTeamAnsehen": "Ganzes Team ansehen", + "zahlungseingang": "Zahlungseingang", + "beauftragtArbeit": "beauftragt oder in Arbeit", + "meineAufgaben": "Meine Aufgaben", + "wasHeuteAnstehtWer": "Was heute ansteht und wer zugewiesen ist.", + "aufgaben": "Aufgaben", + "termineHeute": "Termine heute", + "einsaetzeAufmasseAbnahmenTages": "Einsätze, Aufmaße und Abnahmen des Tages.", + "termine": "Termine", + "hrAntraege": "HR-Anträge", + "offeneUrlaubsZeitkorrekturantraege": "Offene Urlaubs- und Zeitkorrekturanträge.", + "hrHub": "HR-Hub" + } + }, + "analytics": { + "filter": { + "gewerk": "Gewerk", + "status": "Status", + "quelle": "Quelle", + "region": "Region", + "mitarbeiter": "Mitarbeiter" + }, + "shell": { + "navigation": "Navigation", + "suchen": "Suchen", + "benachrichtigungen": "Benachrichtigungen", + "hilfe": "Hilfe" + }, + "actions": { + "ansichtAktualisieren": "Ansicht aktualisieren", + "aktualisieren": "Aktualisieren", + "ansichtExportieren": "Ansicht exportieren", + "export": "Export", + "ansichtTeilen": "Ansicht teilen" + }, + "nav": { + "uebersicht": "Übersicht", + "kennzahlenDenenBetriebGesteuert": "Die Kennzahlen, nach denen der Betrieb gesteuert wird: Umsatz, Auslastung, offene Posten und Handlungsbedarf.", + "finanzen": "Finanzen", + "umsatzZahlungseingangOffeneForderungen": "Umsatz, Zahlungseingang und offene Forderungen: die Sicht der Buchhaltung auf den Betrieb.", + "anfrageBisAbnahmeWo": "Von der Anfrage bis zur Abnahme: wo die Vorgänge stehen, was gebunden ist und was über Termin läuft.", + "kunden": "Kunden", + "woherKundenKommenWie": "Woher Kunden kommen, wie viel sie bringen und wo Nachfassen lohnt.", + "team": "Team", + "auslastungProduktiveStundenAbschluesse": "Auslastung, produktive Stunden und Abschlüsse der Monteure: die Kennzahl hinter der Terminplanung.", + "dashboard": "Dashboard", + "analytics": "Analytics", + "komponenten": "Komponenten" + }, + "alerts": { + "zahlungszielUeberschritten": "Zahlungsziel überschritten", + "mahnlaufStarten": "Mahnlauf starten", + "forderungenAelter90Tage": "Forderungen älter als 90 Tage", + "einbringungsquoteSinktDeutlich": "Einbringungsquote sinkt deutlich", + "inkassoPruefen": "Inkasso prüfen", + "termineNeuPlanen": "Termine neu planen", + "seitUeber21Tagen": "Seit über 21 Tagen offen", + "nachfassen": "Nachfassen", + "bescheidAusstehend": "Bescheid ausstehend", + "statusPruefen": "Status prüfen", + "bewilligt": "Bewilligt", + "beantragt": "Beantragt", + "nichtBeantragt": "Nicht beantragt" + }, + "auftraege": { + "neueVorgaenge": "Neue Vorgänge", + "anlagedatumGezaehlt": "Nach Anlagedatum gezählt", + "aktiveAuftraege": "Aktive Aufträge", + "ueberTermin": "Über Termin", + "abschlussquote": "Abschlussquote", + "angebotAuftrag": "Aus Angebot wird Auftrag", + "durchlaufzeit": "Ø Durchlaufzeit", + "anfrageBisAbnahme": "Anfrage bis Abnahme", + "auftragseingang": "Auftragseingang", + "neuAngelegteVorgaengeZeitverlauf": "Neu angelegte Vorgänge im Zeitverlauf", + "auftragswert": "Ø Auftragswert", + "vertriebstrichter": "Vertriebstrichter", + "angebotsgeschaeftAb6000": "Angebotsgeschäft ab 6.000 €, nach erreichter Stufe", + "vorgaengeStatus": "Vorgänge nach Status", + "alleVorgaengeSeitBeginn": "Alle Vorgänge seit Beginn, unabhängig vom Zeitraumfilter", + "vorgaengeGewerk": "Vorgänge nach Gewerk", + "anzahlSeitBeginnStichtag": "Anzahl seit Beginn, zum Stichtag", + "foerderquoteBafaKfw": "Förderquote BAFA / KfW", + "bezugsgroesseNurFoerderfaehigeLeistungen": "Bezugsgröße sind nur förderfähige Leistungen: Wärmepumpe, PV-Anlage, Batteriespeicher und Lüftungsanlage. Über alle Aufträge gerechnet wäre die Quote wertlos.", + "foerdersumme": "Fördersumme", + "jeAuftrag": "Ø je Auftrag", + "beauftragtArbeit": "Beauftragt oder in Arbeit", + "keineAktivenAuftraege": "Keine aktiven Aufträge", + "auftrag": "Auftrag", + "verantwortlich": "Verantwortlich", + "termin": "Termin" + }, + "finanzen": { + "umsatzBezahlt": "Umsatz (bezahlt)", + "zahlungseingangZeitraum": "Zahlungseingang im Zeitraum", + "offeneForderungen": "Offene Forderungen", + "zahlungsdauer": "Ø Zahlungsdauer", + "rechnungBisZahlungseingang": "Rechnung bis Zahlungseingang", + "deckungsbeitrag": "Deckungsbeitrag", + "gewichtetUeberAbschluesse": "Gewichtet über Abschlüsse", + "aktuellerZeitraumGegenVorjahr": "Aktueller Zeitraum gegen Vorjahr", + "grundlageBezahlteAusgangsrechnungenZahlungsdatum": "Grundlage sind bezahlte Ausgangsrechnungen zum Zahlungsdatum.", + "veraenderung": "Veränderung", + "offenerBestandStatus": "Offener Bestand nach Status", + "wieVielOffenenBestands": "Wie viel des offenen Bestands bereits überfällig ist", + "bezugsgroesseAusschliesslichOffeneBestand": "Bezugsgröße ist ausschließlich der offene Bestand zum Stichtag. Bezahlte Rechnungen sind nicht enthalten, sie würden über 90 % des Rings belegen und die Aussage erdrücken.", + "offeneForderungenAlter": "Offene Forderungen nach Alter", + "alterAbFaelligkeitsdatumAb": "Alter ab Fälligkeitsdatum. Ab 60 Tagen sinkt die Einbringungsquote erfahrungsgemäß deutlich.", + "aelter90Tage": "Älter als 90 Tage", + "abschreibungsrisiko": "Abschreibungsrisiko", + "ueberfaelligGesamt": "Überfällig gesamt", + "sortiertFaelligkeitArbeitslisteFuer": "Sortiert nach Fälligkeit, die Arbeitsliste für den Mahnlauf", + "keineOffenenRechnungen": "Keine offenen Rechnungen", + "rechnung": "Rechnung", + "gestellt": "Gestellt", + "ueberfaellig": "Überfällig", + "betrag": "Betrag" + }, + "kunden": { + "neueKunden": "Neue Kunden", + "wiederkehrendeKunden": "Wiederkehrende Kunden", + "mehrAuftrag": "Mehr als ein Auftrag", + "kundenwert": "Ø Kundenwert", + "jeKundeMindestensEinem": "Je Kunde mit mindestens einem Auftrag", + "reaktivierbar": "Reaktivierbar", + "ueber12MonateOhne": "Über 12 Monate ohne Auftrag", + "neukundenZeitverlauf": "Neukunden im Zeitverlauf", + "anlagedatumKundendatensatzes": "Anlagedatum des Kundendatensatzes", + "kundenQuelle": "Kunden nach Quelle", + "wieErstkontaktZustandeKam": "Wie der Erstkontakt zustande kam", + "empfehlungenBestandskundenZusammenZeigen": "Empfehlungen und Bestandskunden zusammen zeigen, wie stark der Betrieb ohne Werbebudget wächst.", + "kundenstruktur": "Kundenstruktur", + "privatGewerbeVerwaltung": "Privat, Gewerbe, Verwaltung", + "topKundenUmsatz": "Top-Kunden nach Umsatz", + "bezahlterGesamtumsatz": "Bezahlter Gesamtumsatz", + "kundenHistorieAberUeber": "Kunden mit Historie, aber über 12 Monate ohne Auftrag", + "alleKundenWarenZuletzt": "Alle Kunden waren zuletzt aktiv", + "durchschnittlichUeber30Tage": "Durchschnittlich über 30 Tage bis zum Zahlungseingang", + "keineAuffaelligkeiten": "Keine Auffälligkeiten", + "kunde": "Kunde", + "auftraege": "Aufträge", + "umsatz": "Umsatz", + "zahlung": "Ø Zahlung", + "letzterAuftrag": "Letzter Auftrag", + "kundenGesamt": "Kunden gesamt" + }, + "team": { + "einsatz": "Im Einsatz", + "auslastung": "Ø Auslastung", + "produktiveStundenZuVerfuegbaren": "Produktive Stunden zu verfügbaren", + "produktivquote": "Produktivquote", + "anteilAuftragsbezogenerStunden": "Anteil auftragsbezogener Stunden", + "erfassteStunden": "Erfasste Stunden", + "abwesenheit": "Abwesenheit", + "urlaubKrankheitFortbildung": "Urlaub, Krankheit, Fortbildung", + "auslastungJeMitarbeiter": "Auslastung je Mitarbeiter", + "produktiveStundenGegenVertraglich": "Produktive Stunden gegen vertraglich verfügbare Stunden", + "werteUeber100Bedeuten": "Werte über 100 % bedeuten Überstunden. Dauerhaft über 95 % ist ein Warnsignal für Terminrisiken.", + "abschluesseJeMitarbeiter": "Abschlüsse je Mitarbeiter", + "anzahlGewaehltenZeitraumDahinter": "Anzahl im gewählten Zeitraum, dahinter der zugehörige Auftragswert", + "abwesenheiten": "Abwesenheiten", + "stundenGewaehltenZeitraum": "Stunden im gewählten Zeitraum", + "mitarbeiteruebersicht": "Mitarbeiterübersicht", + "alleGewerblichBeschaeftigtenKennzahlen": "Alle gewerblich Beschäftigten mit Kennzahlen", + "stunden": "Stunden", + "davonProduktiv": "davon produktiv", + "auslastung2": "Auslastung", + "abgeschlossen": "Abgeschlossen", + "auftragswert": "Auftragswert" + }, + "uebersicht": { + "auslastungMonteure": "Ø Auslastung Monteure", + "zahlungseingangGewaehltenZeitraum": "Zahlungseingang im gewählten Zeitraum", + "fuenfGewerkeFesterRangfolge": "Fünf Gewerke in fester Rangfolge. Dieselbe Kodierung wie auf der Finanzseite. Ein Ring und eine Balkenliste für dieselbe Zahl wären zwei Bilder für einen Sachverhalt.", + "nurVorhabenFuerSchriftliches": "Nur Vorhaben, für die ein schriftliches Angebot erstellt wird. Wartung und Notdienst laufen ohne Angebot und würden die Quoten verfälschen. Rechte Spalte: Übergang von der vorherigen Stufe.", + "faelligkeitsdatumUeberschrittenNochNicht": "Fälligkeitsdatum überschritten, noch nicht abgeschlossen", + "keinAuftragUeberTermin": "Kein Auftrag ist über Termin: sehr gut.", + "faelligSeit": "Fällig seit", + "umsatzZahlungseingang": "Umsatz (Zahlungseingang)" + } + } + } +} diff --git a/docs/port/i18n-inventar.json b/docs/port/i18n-inventar.json new file mode 100644 index 0000000..015f8e5 --- /dev/null +++ b/docs/port/i18n-inventar.json @@ -0,0 +1,927 @@ +[ + { + "key": "app.shared.alerts.keineOffenenPunkte", + "de": "Keine offenen Punkte", + "quelle": "features/dashboard/components/cards/AlertList.tsx:25" + }, + { + "key": "app.shared.kpi.vorperiode", + "de": "Vorperiode", + "quelle": "features/dashboard/components/cards/KPICard.tsx:36" + }, + { + "key": "app.shared.chart.aktuellerZeitraum", + "de": "Aktueller Zeitraum", + "quelle": "features/dashboard/components/charts/AreaVerlauf.tsx:112" + }, + { + "key": "app.shared.chart.vorjahr", + "de": "Vorjahr", + "quelle": "features/dashboard/components/charts/AreaVerlauf.tsx:124" + }, + { + "key": "app.shared.chart.wert", + "de": "Wert", + "quelle": "features/dashboard/components/charts/BalkenVertikal.tsx:94" + }, + { + "key": "app.shared.chart.keineDatenGewaehltenZeitraum", + "de": "Keine Daten im gewählten Zeitraum", + "quelle": "features/dashboard/components/charts/ChartLeer.tsx:6" + }, + { + "key": "app.dashboard.widget.detailsAnsehen", + "de": "Details ansehen", + "quelle": "features/dashboard/components/dashboard/WidgetKachel.tsx:51" + }, + { + "key": "app.analytics.filter.gewerk", + "de": "Gewerk", + "quelle": "features/dashboard/components/filters/AnalyticsToolbar.tsx:111" + }, + { + "key": "app.analytics.filter.status", + "de": "Status", + "quelle": "features/dashboard/components/filters/AnalyticsToolbar.tsx:121" + }, + { + "key": "app.analytics.filter.quelle", + "de": "Quelle", + "quelle": "features/dashboard/components/filters/AnalyticsToolbar.tsx:134" + }, + { + "key": "app.analytics.filter.region", + "de": "Region", + "quelle": "features/dashboard/components/filters/AnalyticsToolbar.tsx:144" + }, + { + "key": "app.analytics.filter.mitarbeiter", + "de": "Mitarbeiter", + "quelle": "features/dashboard/components/filters/AnalyticsToolbar.tsx:154" + }, + { + "key": "app.analytics.shell.navigation", + "de": "Navigation", + "quelle": "features/dashboard/components/layout/AppShell.tsx:265" + }, + { + "key": "app.analytics.shell.suchen", + "de": "Suchen", + "quelle": "features/dashboard/components/layout/AppShell.tsx:287" + }, + { + "key": "app.analytics.shell.benachrichtigungen", + "de": "Benachrichtigungen", + "quelle": "features/dashboard/components/layout/AppShell.tsx:294" + }, + { + "key": "app.analytics.shell.hilfe", + "de": "Hilfe", + "quelle": "features/dashboard/components/layout/AppShell.tsx:300" + }, + { + "key": "app.analytics.actions.ansichtAktualisieren", + "de": "Ansicht aktualisieren", + "quelle": "features/dashboard/components/layout/PageHeader.tsx:60" + }, + { + "key": "app.analytics.actions.aktualisieren", + "de": "Aktualisieren", + "quelle": "features/dashboard/components/layout/PageHeader.tsx:63" + }, + { + "key": "app.analytics.actions.ansichtExportieren", + "de": "Ansicht exportieren", + "quelle": "features/dashboard/components/layout/PageHeader.tsx:68" + }, + { + "key": "app.analytics.actions.export", + "de": "Export", + "quelle": "features/dashboard/components/layout/PageHeader.tsx:71" + }, + { + "key": "app.analytics.actions.ansichtTeilen", + "de": "Ansicht teilen", + "quelle": "features/dashboard/components/layout/PageHeader.tsx:76" + }, + { + "key": "app.shared.state.kennzahlenBerechnet", + "de": "Kennzahlen werden berechnet …", + "quelle": "features/dashboard/components/layout/SeitenZustand.tsx:29" + }, + { + "key": "app.analytics.nav.uebersicht", + "de": "Übersicht", + "quelle": "features/dashboard/components/layout/navigation.ts:108" + }, + { + "key": "app.analytics.nav.kennzahlenDenenBetriebGesteuert", + "de": "Die Kennzahlen, nach denen der Betrieb gesteuert wird: Umsatz, Auslastung, offene Posten und Handlungsbedarf.", + "quelle": "features/dashboard/components/layout/navigation.ts:110" + }, + { + "key": "app.analytics.nav.finanzen", + "de": "Finanzen", + "quelle": "features/dashboard/components/layout/navigation.ts:115" + }, + { + "key": "app.analytics.nav.umsatzZahlungseingangOffeneForderungen", + "de": "Umsatz, Zahlungseingang und offene Forderungen: die Sicht der Buchhaltung auf den Betrieb.", + "quelle": "features/dashboard/components/layout/navigation.ts:117" + }, + { + "key": "app.analytics.nav.anfrageBisAbnahmeWo", + "de": "Von der Anfrage bis zur Abnahme: wo die Vorgänge stehen, was gebunden ist und was über Termin läuft.", + "quelle": "features/dashboard/components/layout/navigation.ts:124" + }, + { + "key": "app.analytics.nav.kunden", + "de": "Kunden", + "quelle": "features/dashboard/components/layout/navigation.ts:129" + }, + { + "key": "app.analytics.nav.woherKundenKommenWie", + "de": "Woher Kunden kommen, wie viel sie bringen und wo Nachfassen lohnt.", + "quelle": "features/dashboard/components/layout/navigation.ts:131" + }, + { + "key": "app.analytics.nav.team", + "de": "Team", + "quelle": "features/dashboard/components/layout/navigation.ts:136" + }, + { + "key": "app.analytics.nav.auslastungProduktiveStundenAbschluesse", + "de": "Auslastung, produktive Stunden und Abschlüsse der Monteure: die Kennzahl hinter der Terminplanung.", + "quelle": "features/dashboard/components/layout/navigation.ts:138" + }, + { + "key": "app.analytics.nav.dashboard", + "de": "Dashboard", + "quelle": "features/dashboard/components/layout/navigation.ts:52" + }, + { + "key": "app.analytics.nav.analytics", + "de": "Analytics", + "quelle": "features/dashboard/components/layout/navigation.ts:60" + }, + { + "key": "app.analytics.nav.komponenten", + "de": "Komponenten", + "quelle": "features/dashboard/components/layout/navigation.ts:83" + }, + { + "key": "app.shared.table.vorherigeSeite", + "de": "Vorherige Seite", + "quelle": "features/dashboard/components/tables/DataTable.tsx:200" + }, + { + "key": "app.shared.table.naechsteSeite", + "de": "Nächste Seite", + "quelle": "features/dashboard/components/tables/DataTable.tsx:211" + }, + { + "key": "app.shared.table.keineEintraege", + "de": "Keine Einträge", + "quelle": "features/dashboard/components/tables/DataTable.tsx:37" + }, + { + "key": "app.analytics.alerts.zahlungszielUeberschritten", + "de": "Zahlungsziel überschritten", + "quelle": "features/dashboard/data/calculations.ts:478" + }, + { + "key": "app.analytics.alerts.mahnlaufStarten", + "de": "Mahnlauf starten", + "quelle": "features/dashboard/data/calculations.ts:480" + }, + { + "key": "app.analytics.alerts.forderungenAelter90Tage", + "de": "Forderungen älter als 90 Tage", + "quelle": "features/dashboard/data/calculations.ts:487" + }, + { + "key": "app.analytics.alerts.einbringungsquoteSinktDeutlich", + "de": "Einbringungsquote sinkt deutlich", + "quelle": "features/dashboard/data/calculations.ts:488" + }, + { + "key": "app.analytics.alerts.inkassoPruefen", + "de": "Inkasso prüfen", + "quelle": "features/dashboard/data/calculations.ts:490" + }, + { + "key": "app.analytics.alerts.termineNeuPlanen", + "de": "Termine neu planen", + "quelle": "features/dashboard/data/calculations.ts:500" + }, + { + "key": "app.analytics.alerts.seitUeber21Tagen", + "de": "Seit über 21 Tagen offen", + "quelle": "features/dashboard/data/calculations.ts:512" + }, + { + "key": "app.analytics.alerts.nachfassen", + "de": "Nachfassen", + "quelle": "features/dashboard/data/calculations.ts:514" + }, + { + "key": "app.analytics.alerts.bescheidAusstehend", + "de": "Bescheid ausstehend", + "quelle": "features/dashboard/data/calculations.ts:524" + }, + { + "key": "app.analytics.alerts.statusPruefen", + "de": "Status prüfen", + "quelle": "features/dashboard/data/calculations.ts:526" + }, + { + "key": "app.analytics.alerts.bewilligt", + "de": "Bewilligt", + "quelle": "features/dashboard/data/calculations.ts:563" + }, + { + "key": "app.analytics.alerts.beantragt", + "de": "Beantragt", + "quelle": "features/dashboard/data/calculations.ts:564" + }, + { + "key": "app.analytics.alerts.nichtBeantragt", + "de": "Nicht beantragt", + "quelle": "features/dashboard/data/calculations.ts:565" + }, + { + "key": "app.analytics.auftraege.neueVorgaenge", + "de": "Neue Vorgänge", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:102" + }, + { + "key": "app.analytics.auftraege.anlagedatumGezaehlt", + "de": "Nach Anlagedatum gezählt", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:104" + }, + { + "key": "app.analytics.auftraege.aktiveAuftraege", + "de": "Aktive Aufträge", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:110" + }, + { + "key": "app.analytics.auftraege.ueberTermin", + "de": "Über Termin", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:117" + }, + { + "key": "app.analytics.auftraege.abschlussquote", + "de": "Abschlussquote", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:129" + }, + { + "key": "app.analytics.auftraege.angebotAuftrag", + "de": "Aus Angebot wird Auftrag", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:131" + }, + { + "key": "app.analytics.auftraege.durchlaufzeit", + "de": "Ø Durchlaufzeit", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:136" + }, + { + "key": "app.analytics.auftraege.anfrageBisAbnahme", + "de": "Anfrage bis Abnahme", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:138" + }, + { + "key": "app.analytics.auftraege.auftragseingang", + "de": "Auftragseingang", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:148" + }, + { + "key": "app.analytics.auftraege.neuAngelegteVorgaengeZeitverlauf", + "de": "Neu angelegte Vorgänge im Zeitverlauf", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:149" + }, + { + "key": "app.analytics.auftraege.auftragswert", + "de": "Ø Auftragswert", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:158" + }, + { + "key": "app.analytics.auftraege.vertriebstrichter", + "de": "Vertriebstrichter", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:167" + }, + { + "key": "app.analytics.auftraege.angebotsgeschaeftAb6000", + "de": "Angebotsgeschäft ab 6.000 €, nach erreichter Stufe", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:168" + }, + { + "key": "app.analytics.auftraege.vorgaengeStatus", + "de": "Vorgänge nach Status", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:178" + }, + { + "key": "app.analytics.auftraege.alleVorgaengeSeitBeginn", + "de": "Alle Vorgänge seit Beginn, unabhängig vom Zeitraumfilter", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:179" + }, + { + "key": "app.analytics.auftraege.vorgaengeGewerk", + "de": "Vorgänge nach Gewerk", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:191" + }, + { + "key": "app.analytics.auftraege.anzahlSeitBeginnStichtag", + "de": "Anzahl seit Beginn, zum Stichtag", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:192" + }, + { + "key": "app.analytics.auftraege.foerderquoteBafaKfw", + "de": "Förderquote BAFA / KfW", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:204" + }, + { + "key": "app.analytics.auftraege.bezugsgroesseNurFoerderfaehigeLeistungen", + "de": "Bezugsgröße sind nur förderfähige Leistungen: Wärmepumpe, PV-Anlage, Batteriespeicher und Lüftungsanlage. Über alle Aufträge gerechnet wäre die Quote wertlos.", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:208" + }, + { + "key": "app.analytics.auftraege.foerdersumme", + "de": "Fördersumme", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:219" + }, + { + "key": "app.analytics.auftraege.jeAuftrag", + "de": "Ø je Auftrag", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:221" + }, + { + "key": "app.analytics.auftraege.beauftragtArbeit", + "de": "Beauftragt oder in Arbeit", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:232" + }, + { + "key": "app.analytics.auftraege.keineAktivenAuftraege", + "de": "Keine aktiven Aufträge", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:241" + }, + { + "key": "app.analytics.auftraege.auftrag", + "de": "Auftrag", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:42" + }, + { + "key": "app.analytics.auftraege.verantwortlich", + "de": "Verantwortlich", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:63" + }, + { + "key": "app.analytics.auftraege.termin", + "de": "Termin", + "quelle": "features/dashboard/pages/AuftraegeSeite.tsx:73" + }, + { + "key": "app.dashboard.page.handlungsbedarf", + "de": "Handlungsbedarf", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:100" + }, + { + "key": "app.dashboard.page.sortiertDringlichkeitJedeZeile", + "de": "Sortiert nach Dringlichkeit, jede Zeile führt in die zugehörige Arbeitsliste", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:101" + }, + { + "key": "app.dashboard.page.zahlungseingangZeitverlauf", + "de": "Zahlungseingang im Zeitverlauf", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:111" + }, + { + "key": "app.dashboard.page.aktuellerZeitraumGegenVorjahreszeitraum", + "de": "Aktueller Zeitraum gegen den Vorjahreszeitraum", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:112" + }, + { + "key": "app.dashboard.page.gezaehltTagZahlungseingangsNicht", + "de": "Gezählt wird der Tag des Zahlungseingangs, nicht das Rechnungsdatum. Dadurch entspricht die Kurve dem tatsächlichen Geldfluss.", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:113" + }, + { + "key": "app.dashboard.page.finanzenAuswerten", + "de": "In den Finanzen auswerten", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:117" + }, + { + "key": "app.dashboard.page.umsatzGewerk", + "de": "Umsatz nach Gewerk", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:140" + }, + { + "key": "app.dashboard.page.zahlungseingangLetzten30Tage", + "de": "Zahlungseingang der letzten 30 Tage", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:141" + }, + { + "key": "app.dashboard.page.fuenfGewerkeFesterRangfolge", + "de": "Fünf Gewerke in fester Rangfolge: dieselbe Kodierung wie auf der Finanzseite.", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:142" + }, + { + "key": "app.dashboard.page.angebotsgeschaeftAb6000", + "de": "Angebotsgeschäft ab 6.000 €, zum Stichtag", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:158" + }, + { + "key": "app.dashboard.page.nurVorhabenFuerSchriftliches", + "de": "Nur Vorhaben, für die ein schriftliches Angebot erstellt wird. Wartung und Notdienst laufen ohne Angebot und würden die Quoten verfälschen.", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:159" + }, + { + "key": "app.dashboard.page.auftraegeAuswerten", + "de": "Aufträge auswerten", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:163" + }, + { + "key": "app.dashboard.page.auslastungMonteure", + "de": "Auslastung Monteure", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:171" + }, + { + "key": "app.dashboard.page.produktiveStundenGegenVerfuegbare", + "de": "Produktive Stunden gegen verfügbare Stunden", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:172" + }, + { + "key": "app.dashboard.page.nurAuftraegeGebuchteStunden", + "de": "Nur auf Aufträge gebuchte Stunden zählen als produktiv. Über 100 % bedeutet Überstunden.", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:173" + }, + { + "key": "app.dashboard.page.ganzesTeamAnsehen", + "de": "Ganzes Team ansehen", + "quelle": "features/dashboard/pages/DashboardSeite.tsx:177" + }, + { + "key": "app.analytics.finanzen.umsatzBezahlt", + "de": "Umsatz (bezahlt)", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:105" + }, + { + "key": "app.analytics.finanzen.zahlungseingangZeitraum", + "de": "Zahlungseingang im Zeitraum", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:107" + }, + { + "key": "app.analytics.finanzen.offeneForderungen", + "de": "Offene Forderungen", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:113" + }, + { + "key": "app.analytics.finanzen.zahlungsdauer", + "de": "Ø Zahlungsdauer", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:128" + }, + { + "key": "app.analytics.finanzen.rechnungBisZahlungseingang", + "de": "Rechnung bis Zahlungseingang", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:130" + }, + { + "key": "app.analytics.finanzen.deckungsbeitrag", + "de": "Deckungsbeitrag", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:136" + }, + { + "key": "app.analytics.finanzen.gewichtetUeberAbschluesse", + "de": "Gewichtet über Abschlüsse", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:138" + }, + { + "key": "app.analytics.finanzen.aktuellerZeitraumGegenVorjahr", + "de": "Aktueller Zeitraum gegen Vorjahr", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:147" + }, + { + "key": "app.analytics.finanzen.grundlageBezahlteAusgangsrechnungenZahlungsdatum", + "de": "Grundlage sind bezahlte Ausgangsrechnungen zum Zahlungsdatum.", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:148" + }, + { + "key": "app.analytics.finanzen.veraenderung", + "de": "Veränderung", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:171" + }, + { + "key": "app.analytics.finanzen.offenerBestandStatus", + "de": "Offener Bestand nach Status", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:180" + }, + { + "key": "app.analytics.finanzen.wieVielOffenenBestands", + "de": "Wie viel des offenen Bestands bereits überfällig ist", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:181" + }, + { + "key": "app.analytics.finanzen.bezugsgroesseAusschliesslichOffeneBestand", + "de": "Bezugsgröße ist ausschließlich der offene Bestand zum Stichtag. Bezahlte Rechnungen sind nicht enthalten, sie würden über 90 % des Rings belegen und die Aussage erdrücken.", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:182" + }, + { + "key": "app.analytics.finanzen.offeneForderungenAlter", + "de": "Offene Forderungen nach Alter", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:198" + }, + { + "key": "app.analytics.finanzen.alterAbFaelligkeitsdatumAb", + "de": "Alter ab Fälligkeitsdatum. Ab 60 Tagen sinkt die Einbringungsquote erfahrungsgemäß deutlich.", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:200" + }, + { + "key": "app.analytics.finanzen.aelter90Tage", + "de": "Älter als 90 Tage", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:213" + }, + { + "key": "app.analytics.finanzen.abschreibungsrisiko", + "de": "Abschreibungsrisiko", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:217" + }, + { + "key": "app.analytics.finanzen.ueberfaelligGesamt", + "de": "Überfällig gesamt", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:219" + }, + { + "key": "app.analytics.finanzen.sortiertFaelligkeitArbeitslisteFuer", + "de": "Sortiert nach Fälligkeit, die Arbeitsliste für den Mahnlauf", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:242" + }, + { + "key": "app.analytics.finanzen.keineOffenenRechnungen", + "de": "Keine offenen Rechnungen", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:249" + }, + { + "key": "app.analytics.finanzen.rechnung", + "de": "Rechnung", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:50" + }, + { + "key": "app.analytics.finanzen.gestellt", + "de": "Gestellt", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:65" + }, + { + "key": "app.analytics.finanzen.ueberfaellig", + "de": "Überfällig", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:71" + }, + { + "key": "app.analytics.finanzen.betrag", + "de": "Betrag", + "quelle": "features/dashboard/pages/FinanzenSeite.tsx:92" + }, + { + "key": "app.analytics.kunden.neueKunden", + "de": "Neue Kunden", + "quelle": "features/dashboard/pages/KundenSeite.tsx:105" + }, + { + "key": "app.analytics.kunden.wiederkehrendeKunden", + "de": "Wiederkehrende Kunden", + "quelle": "features/dashboard/pages/KundenSeite.tsx:113" + }, + { + "key": "app.analytics.kunden.mehrAuftrag", + "de": "Mehr als ein Auftrag", + "quelle": "features/dashboard/pages/KundenSeite.tsx:115" + }, + { + "key": "app.analytics.kunden.kundenwert", + "de": "Ø Kundenwert", + "quelle": "features/dashboard/pages/KundenSeite.tsx:120" + }, + { + "key": "app.analytics.kunden.jeKundeMindestensEinem", + "de": "Je Kunde mit mindestens einem Auftrag", + "quelle": "features/dashboard/pages/KundenSeite.tsx:122" + }, + { + "key": "app.analytics.kunden.reaktivierbar", + "de": "Reaktivierbar", + "quelle": "features/dashboard/pages/KundenSeite.tsx:127" + }, + { + "key": "app.analytics.kunden.ueber12MonateOhne", + "de": "Über 12 Monate ohne Auftrag", + "quelle": "features/dashboard/pages/KundenSeite.tsx:129" + }, + { + "key": "app.analytics.kunden.neukundenZeitverlauf", + "de": "Neukunden im Zeitverlauf", + "quelle": "features/dashboard/pages/KundenSeite.tsx:139" + }, + { + "key": "app.analytics.kunden.anlagedatumKundendatensatzes", + "de": "Anlagedatum des Kundendatensatzes", + "quelle": "features/dashboard/pages/KundenSeite.tsx:140" + }, + { + "key": "app.analytics.kunden.kundenQuelle", + "de": "Kunden nach Quelle", + "quelle": "features/dashboard/pages/KundenSeite.tsx:148" + }, + { + "key": "app.analytics.kunden.wieErstkontaktZustandeKam", + "de": "Wie der Erstkontakt zustande kam", + "quelle": "features/dashboard/pages/KundenSeite.tsx:149" + }, + { + "key": "app.analytics.kunden.empfehlungenBestandskundenZusammenZeigen", + "de": "Empfehlungen und Bestandskunden zusammen zeigen, wie stark der Betrieb ohne Werbebudget wächst.", + "quelle": "features/dashboard/pages/KundenSeite.tsx:150" + }, + { + "key": "app.analytics.kunden.kundenstruktur", + "de": "Kundenstruktur", + "quelle": "features/dashboard/pages/KundenSeite.tsx:162" + }, + { + "key": "app.analytics.kunden.privatGewerbeVerwaltung", + "de": "Privat, Gewerbe, Verwaltung", + "quelle": "features/dashboard/pages/KundenSeite.tsx:162" + }, + { + "key": "app.analytics.kunden.topKundenUmsatz", + "de": "Top-Kunden nach Umsatz", + "quelle": "features/dashboard/pages/KundenSeite.tsx:174" + }, + { + "key": "app.analytics.kunden.bezahlterGesamtumsatz", + "de": "Bezahlter Gesamtumsatz", + "quelle": "features/dashboard/pages/KundenSeite.tsx:175" + }, + { + "key": "app.analytics.kunden.kundenHistorieAberUeber", + "de": "Kunden mit Historie, aber über 12 Monate ohne Auftrag", + "quelle": "features/dashboard/pages/KundenSeite.tsx:185" + }, + { + "key": "app.analytics.kunden.alleKundenWarenZuletzt", + "de": "Alle Kunden waren zuletzt aktiv", + "quelle": "features/dashboard/pages/KundenSeite.tsx:192" + }, + { + "key": "app.analytics.kunden.durchschnittlichUeber30Tage", + "de": "Durchschnittlich über 30 Tage bis zum Zahlungseingang", + "quelle": "features/dashboard/pages/KundenSeite.tsx:199" + }, + { + "key": "app.analytics.kunden.keineAuffaelligkeiten", + "de": "Keine Auffälligkeiten", + "quelle": "features/dashboard/pages/KundenSeite.tsx:206" + }, + { + "key": "app.analytics.kunden.kunde", + "de": "Kunde", + "quelle": "features/dashboard/pages/KundenSeite.tsx:35" + }, + { + "key": "app.analytics.kunden.auftraege", + "de": "Aufträge", + "quelle": "features/dashboard/pages/KundenSeite.tsx:53" + }, + { + "key": "app.analytics.kunden.umsatz", + "de": "Umsatz", + "quelle": "features/dashboard/pages/KundenSeite.tsx:60" + }, + { + "key": "app.analytics.kunden.zahlung", + "de": "Ø Zahlung", + "quelle": "features/dashboard/pages/KundenSeite.tsx:67" + }, + { + "key": "app.analytics.kunden.letzterAuftrag", + "de": "Letzter Auftrag", + "quelle": "features/dashboard/pages/KundenSeite.tsx:81" + }, + { + "key": "app.analytics.kunden.kundenGesamt", + "de": "Kunden gesamt", + "quelle": "features/dashboard/pages/KundenSeite.tsx:98" + }, + { + "key": "app.analytics.team.einsatz", + "de": "Im Einsatz", + "quelle": "features/dashboard/pages/TeamSeite.tsx:112" + }, + { + "key": "app.analytics.team.auslastung", + "de": "Ø Auslastung", + "quelle": "features/dashboard/pages/TeamSeite.tsx:119" + }, + { + "key": "app.analytics.team.produktiveStundenZuVerfuegbaren", + "de": "Produktive Stunden zu verfügbaren", + "quelle": "features/dashboard/pages/TeamSeite.tsx:121" + }, + { + "key": "app.analytics.team.produktivquote", + "de": "Produktivquote", + "quelle": "features/dashboard/pages/TeamSeite.tsx:126" + }, + { + "key": "app.analytics.team.anteilAuftragsbezogenerStunden", + "de": "Anteil auftragsbezogener Stunden", + "quelle": "features/dashboard/pages/TeamSeite.tsx:128" + }, + { + "key": "app.analytics.team.erfassteStunden", + "de": "Erfasste Stunden", + "quelle": "features/dashboard/pages/TeamSeite.tsx:132" + }, + { + "key": "app.analytics.team.abwesenheit", + "de": "Abwesenheit", + "quelle": "features/dashboard/pages/TeamSeite.tsx:138" + }, + { + "key": "app.analytics.team.urlaubKrankheitFortbildung", + "de": "Urlaub, Krankheit, Fortbildung", + "quelle": "features/dashboard/pages/TeamSeite.tsx:140" + }, + { + "key": "app.analytics.team.auslastungJeMitarbeiter", + "de": "Auslastung je Mitarbeiter", + "quelle": "features/dashboard/pages/TeamSeite.tsx:148" + }, + { + "key": "app.analytics.team.produktiveStundenGegenVertraglich", + "de": "Produktive Stunden gegen vertraglich verfügbare Stunden", + "quelle": "features/dashboard/pages/TeamSeite.tsx:149" + }, + { + "key": "app.analytics.team.werteUeber100Bedeuten", + "de": "Werte über 100 % bedeuten Überstunden. Dauerhaft über 95 % ist ein Warnsignal für Terminrisiken.", + "quelle": "features/dashboard/pages/TeamSeite.tsx:150" + }, + { + "key": "app.analytics.team.abschluesseJeMitarbeiter", + "de": "Abschlüsse je Mitarbeiter", + "quelle": "features/dashboard/pages/TeamSeite.tsx:165" + }, + { + "key": "app.analytics.team.anzahlGewaehltenZeitraumDahinter", + "de": "Anzahl im gewählten Zeitraum, dahinter der zugehörige Auftragswert", + "quelle": "features/dashboard/pages/TeamSeite.tsx:166" + }, + { + "key": "app.analytics.team.abwesenheiten", + "de": "Abwesenheiten", + "quelle": "features/dashboard/pages/TeamSeite.tsx:183" + }, + { + "key": "app.analytics.team.stundenGewaehltenZeitraum", + "de": "Stunden im gewählten Zeitraum", + "quelle": "features/dashboard/pages/TeamSeite.tsx:184" + }, + { + "key": "app.analytics.team.mitarbeiteruebersicht", + "de": "Mitarbeiterübersicht", + "quelle": "features/dashboard/pages/TeamSeite.tsx:197" + }, + { + "key": "app.analytics.team.alleGewerblichBeschaeftigtenKennzahlen", + "de": "Alle gewerblich Beschäftigten mit Kennzahlen", + "quelle": "features/dashboard/pages/TeamSeite.tsx:198" + }, + { + "key": "app.analytics.team.stunden", + "de": "Stunden", + "quelle": "features/dashboard/pages/TeamSeite.tsx:59" + }, + { + "key": "app.analytics.team.davonProduktiv", + "de": "davon produktiv", + "quelle": "features/dashboard/pages/TeamSeite.tsx:66" + }, + { + "key": "app.analytics.team.auslastung2", + "de": "Auslastung", + "quelle": "features/dashboard/pages/TeamSeite.tsx:73" + }, + { + "key": "app.analytics.team.abgeschlossen", + "de": "Abgeschlossen", + "quelle": "features/dashboard/pages/TeamSeite.tsx:92" + }, + { + "key": "app.analytics.team.auftragswert", + "de": "Auftragswert", + "quelle": "features/dashboard/pages/TeamSeite.tsx:99" + }, + { + "key": "app.analytics.uebersicht.auslastungMonteure", + "de": "Ø Auslastung Monteure", + "quelle": "features/dashboard/pages/UebersichtSeite.tsx:111" + }, + { + "key": "app.analytics.uebersicht.zahlungseingangGewaehltenZeitraum", + "de": "Zahlungseingang im gewählten Zeitraum", + "quelle": "features/dashboard/pages/UebersichtSeite.tsx:173" + }, + { + "key": "app.analytics.uebersicht.fuenfGewerkeFesterRangfolge", + "de": "Fünf Gewerke in fester Rangfolge. Dieselbe Kodierung wie auf der Finanzseite. Ein Ring und eine Balkenliste für dieselbe Zahl wären zwei Bilder für einen Sachverhalt.", + "quelle": "features/dashboard/pages/UebersichtSeite.tsx:174" + }, + { + "key": "app.analytics.uebersicht.nurVorhabenFuerSchriftliches", + "de": "Nur Vorhaben, für die ein schriftliches Angebot erstellt wird. Wartung und Notdienst laufen ohne Angebot und würden die Quoten verfälschen. Rechte Spalte: Übergang von der vorherigen Stufe.", + "quelle": "features/dashboard/pages/UebersichtSeite.tsx:189" + }, + { + "key": "app.analytics.uebersicht.faelligkeitsdatumUeberschrittenNochNicht", + "de": "Fälligkeitsdatum überschritten, noch nicht abgeschlossen", + "quelle": "features/dashboard/pages/UebersichtSeite.tsx:214" + }, + { + "key": "app.analytics.uebersicht.keinAuftragUeberTermin", + "de": "Kein Auftrag ist über Termin: sehr gut.", + "quelle": "features/dashboard/pages/UebersichtSeite.tsx:221" + }, + { + "key": "app.analytics.uebersicht.faelligSeit", + "de": "Fällig seit", + "quelle": "features/dashboard/pages/UebersichtSeite.tsx:64" + }, + { + "key": "app.analytics.uebersicht.umsatzZahlungseingang", + "de": "Umsatz (Zahlungseingang)", + "quelle": "features/dashboard/pages/UebersichtSeite.tsx:88" + }, + { + "key": "app.shared.chart.161616", + "de": "#161616", + "quelle": "features/dashboard/utils/chartTheme.ts:149" + }, + { + "key": "app.shared.chart.sonstige", + "de": "Sonstige", + "quelle": "features/dashboard/utils/chartTheme.ts:232" + }, + { + "key": "app.dashboard.page.zahlungseingang", + "de": "Zahlungseingang", + "quelle": "features/dashboard/view-models/dashboard.vm.ts:180" + }, + { + "key": "app.dashboard.page.beauftragtArbeit", + "de": "beauftragt oder in Arbeit", + "quelle": "features/dashboard/view-models/dashboard.vm.ts:204" + }, + { + "key": "app.dashboard.page.meineAufgaben", + "de": "Meine Aufgaben", + "quelle": "features/dashboard/view-models/dashboard.vm.ts:254" + }, + { + "key": "app.dashboard.page.wasHeuteAnstehtWer", + "de": "Was heute ansteht und wer zugewiesen ist.", + "quelle": "features/dashboard/view-models/dashboard.vm.ts:255" + }, + { + "key": "app.dashboard.page.aufgaben", + "de": "Aufgaben", + "quelle": "features/dashboard/view-models/dashboard.vm.ts:257" + }, + { + "key": "app.dashboard.page.termineHeute", + "de": "Termine heute", + "quelle": "features/dashboard/view-models/dashboard.vm.ts:262" + }, + { + "key": "app.dashboard.page.einsaetzeAufmasseAbnahmenTages", + "de": "Einsätze, Aufmaße und Abnahmen des Tages.", + "quelle": "features/dashboard/view-models/dashboard.vm.ts:263" + }, + { + "key": "app.dashboard.page.termine", + "de": "Termine", + "quelle": "features/dashboard/view-models/dashboard.vm.ts:265" + }, + { + "key": "app.dashboard.page.hrAntraege", + "de": "HR-Anträge", + "quelle": "features/dashboard/view-models/dashboard.vm.ts:270" + }, + { + "key": "app.dashboard.page.offeneUrlaubsZeitkorrekturantraege", + "de": "Offene Urlaubs- und Zeitkorrekturanträge.", + "quelle": "features/dashboard/view-models/dashboard.vm.ts:271" + }, + { + "key": "app.dashboard.page.hrHub", + "de": "HR-Hub", + "quelle": "features/dashboard/view-models/dashboard.vm.ts:273" + } +] diff --git a/package.json b/package.json index 3d86581..7e8cdae 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "start": "next start -p 4300", "lint": "eslint", "verify:carbon": "node scripts/verify-carbon.mjs", - "verify:layout": "node scripts/verify-layout.mjs" + "verify:layout": "node scripts/verify-layout.mjs", + "i18n:inventar": "python3 scripts/i18n-inventar.py" }, "dependencies": { "@radix-ui/react-slot": "^1.2.4", diff --git a/scripts/i18n-inventar.py b/scripts/i18n-inventar.py new file mode 100644 index 0000000..f79abc0 --- /dev/null +++ b/scripts/i18n-inventar.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Sammelt alle sichtbaren Oberflaechentexte und schlaegt i18n-Schluessel vor. + +Warum es das gibt: HiveGrid prueft mit `pnpm run verify:i18n`, dass keine +Strings hartkodiert sind. Dieser Prototyp ist zu 100 % hartkodiertes Deutsch, +weil er allein lauffaehig sein muss. Das Inventar nimmt dem Einbau die +Sucharbeit ab. + +Erzeugt zwei Dateien: + docs/port/i18n-inventar.json Schluessel, Text, Fundstelle + docs/port/i18n-de.json derselbe Inhalt als Baum, einfuegbar nach + apps/web/public/locales/de/translations.json + +Aufruf: npm run i18n:inventar + +Die Schluesselnamen sind maschinell aus dem deutschen Text gebildet und ein +Vorschlag, kein Ergebnis. Wo HiveGrid schon einen Schluessel hat, gilt dessen +Name; welche das sind, steht in docs/port/TEXTE.md Abschnitt 1. +""" + +import json +import pathlib +import re +import unicodedata + +# Props und Objektfelder, die im Prototyp sichtbaren Text tragen. +PROPS = ("titel", "untertitel", "beschreibung", "label", "hinweis", "leerText", + "zusatz", "aktion", "text", "detail", "titleKey", "absprungText", + "trendLabel", "quelle", "lage", "anrede", "name", "leerHinweis") +PROP_RE = re.compile(r'\b(' + "|".join(PROPS) + r')\s*[=:]\s*"([^"]{2,200})"') +JSX_RE = re.compile(r'>\s*([A-ZÄÖÜ][^<>{}\n]{3,120}?)\s*<') + +# Nicht portiert: die Bausteingalerie. Keine Oberflaechentexte: die Attrappen. +UEBERSPRINGEN = {"KomponentenSeite.tsx", "nutzer.ts", "seed.ts"} + +SEITE = { + "UebersichtSeite": ("analytics", "uebersicht"), "FinanzenSeite": ("analytics", "finanzen"), + "AuftraegeSeite": ("analytics", "auftraege"), "KundenSeite": ("analytics", "kunden"), + "TeamSeite": ("analytics", "team"), "DashboardSeite": ("dashboard", "page"), + "dashboard.vm": ("dashboard", "page"), "navigation": ("analytics", "nav"), + "calculations": ("analytics", "alerts"), "AnalyticsToolbar": ("analytics", "filter"), + "AppShell": ("analytics", "shell"), "PageHeader": ("analytics", "actions"), + "DataTable": ("shared", "table"), "SeitenZustand": ("shared", "state"), + "ChartLeer": ("shared", "chart"), "AlertList": ("shared", "alerts"), + "KPICard": ("shared", "kpi"), "AreaVerlauf": ("shared", "chart"), + "BalkenVertikal": ("shared", "chart"), "WidgetKachel": ("dashboard", "widget"), + "chartTheme": ("shared", "chart"), +} + +STOPP = {"der", "die", "das", "und", "oder", "im", "in", "den", "dem", "des", "zum", + "zur", "für", "von", "mit", "auf", "aus", "bei", "nach", "als", "ein", + "eine", "einen", "ist", "sind", "wird", "werden"} + + +def slug(text, laenge=4): + """Kurzform fuer den Schluesselnamen: die ersten Sinnwoerter in camelCase.""" + t = (text.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue") + .replace("ß", "ss").replace("Ä", "Ae").replace("Ö", "Oe").replace("Ü", "Ue")) + t = unicodedata.normalize("NFKD", t).encode("ascii", "ignore").decode() + woerter = [w for w in re.findall(r"[A-Za-z0-9]+", t) if w.lower() not in STOPP][:laenge] + if not woerter: + woerter = ["text"] + return woerter[0].lower() + "".join(w.capitalize() for w in woerter[1:]) + + +def sammle(): + dateien = sorted(pathlib.Path("features/dashboard").rglob("*.tsx")) + \ + sorted(pathlib.Path("features/dashboard").rglob("*.ts")) + treffer = {} + for p in dateien: + if p.name in UEBERSPRINGEN: + continue + for nr, zeile in enumerate(p.read_text().split("\n"), 1): + if zeile.lstrip().startswith(("*", "//")): + continue + for m in PROP_RE.finditer(zeile): + treffer.setdefault(m.group(2), []).append(f"{p}:{nr}") + for m in JSX_RE.finditer(zeile): + t = m.group(1).strip() + if t and not t.startswith("{") and any(c.isalpha() for c in t): + treffer.setdefault(t, []).append(f"{p}:{nr}") + return dict(sorted(treffer.items())) + + +def main(): + texte = sammle() + zeilen, baum, benutzt = [], {}, set() + for text, orte in sorted(texte.items(), key=lambda kv: kv[1][0]): + datei = pathlib.Path(orte[0].split(":")[0]).stem + bereich, gruppe = SEITE.get(datei, ("shared", "misc")) + basis = f"app.{bereich}.{gruppe}.{slug(text)}" + schluessel, n = basis, 2 + while schluessel in benutzt: + schluessel, n = f"{basis}{n}", n + 1 + benutzt.add(schluessel) + zeilen.append((schluessel, text, orte[0])) + knoten = baum + for teil in schluessel.split(".")[:-1]: + knoten = knoten.setdefault(teil, {}) + knoten[schluessel.split(".")[-1]] = text + + ziel = pathlib.Path("docs/port") + ziel.mkdir(parents=True, exist_ok=True) + (ziel / "i18n-de.json").write_text(json.dumps(baum, ensure_ascii=False, indent=2) + "\n") + (ziel / "i18n-inventar.json").write_text(json.dumps( + [{"key": k, "de": t, "quelle": o} for k, t, o in zeilen], + ensure_ascii=False, indent=1) + "\n") + print(f"{len(zeilen)} Texte, {len(zeilen)} Schluessel -> docs/port/") + + +if __name__ == "__main__": + main()