Zum Inhalt springen

Reporting & Auswertung (Tag/Woche/Monat, Drill-Down, Dashboards, BFSG-konform)

Live in Produktion

Gruppe: Kern · Quelle: FUNKTIONSUMFANG §3.7 Autor: Senior-Consulting-Sparte Reporting & Barrierefreiheit · Stand: 2026-04-19 HTML-Hero: 07-reporting-auswertung.html Cross-Link: Reports sind über die öffentliche API exportierbar — siehe 12-api-entwickler-portal.md.


feature_id: kern/07-reporting-auswertung
title: Reporting & Auswertung (Tag/Woche/Monat, Drill-Down, Dashboards, BFSG-konform)
funktionsumfang_ref: §3.7
roadmap_horizont: MVP (Basis) · V1 (Drill-Down, Dashboards, XLSX) · V1.5 (Dashboard-Konfigurator)
plattformen:
mobile: vollständig # Tages-/Wochen-/Monats-Saldo, Team-Status (Manager)
web: mit-Bulk # Mitarbeiter-/Projekt-/Kunden-Reports, Drill-Down, Dashboards, Bulk-Export
desktop: ab V1.5 bei Bedarf
owner_rolle: Manager · HR · Buchhaltung · Bauleitung · Admin · Mitarbeiter (eigene Reports)
modul_gate_flag: module.kern.reporting
compliance_flags:
gobd: true # Reports lesen GoBD-pflichtige Buchungen, PDF-Export ist revisionssicher zu siegeln
arbzg: true # Dashboards zeigen ArbZG-Verletzungen (§3 Höchstarbeitszeit, §5 Ruhezeit)
vob: false # VOB liegt bei §4.5 Bau-Abrechnung
dsgvo: true # Art. 12 Transparenz, Art. 15 Auskunft, Art. 88 BDSG-Beschäftigtendaten
bfsg: true # Reporting ist ESS-/Admin-Oberfläche → WCAG 2.2 AA ab 28.06.2025
betrvg: true # §87(1)6 — jede Form aggregierter Leistungskontrolle braucht BR-Zustimmung
stvg: false
weitere: ["§147 AO (10 Jahre Aufbewahrung Export-Belege)", "EN 301 549 (WCAG 2.2 AA harmonisiert)"]
referenzkunde:
status: TBD
name: "zu klären mit Sales — Kandidat: Malerbetrieb Musterbetrieb, 25 MA, Bayern (Stundennachweis-PDF), SHK Gebrüder Schmidt, 80 MA, NRW (Drill-Down Projekt→MA→Tag)"
quelle: Sales-Call 2026-03 (unverbindlich)
estimate_eng_tage: 32 (MVP 12 · V1 14 · V1.5 6)
abhängigkeiten:
- kern/01-zeiterfassung # Quelldaten: Stempelungen
- kern/02-arbeitszeit-compliance # ArbZG-Widgets
- kern/03-abwesenheiten # Krankenstand / Urlaubssaldo
- kern/11-audit-log # jeder Export erzeugt Audit-Eintrag
- kern/12-api-entwickler-portal # Report-Endpunkte sind Teil der öffentlichen API

Marktrealität (DE-Handwerk). In 80 % der von uns interviewten Handwerksbetriebe landet die „Stundenauswertung“ am Monatsende als Excel-Tabelle auf dem Schreibtisch der Buchhaltung — kopiert aus Stempelzetteln, Stundenzetteln, WhatsApp-Screenshots des Poliers. Das BAG-Urteil 13.09.2022 (1 ABR 22/21) zwingt den Arbeitgeber zur vollständigen Arbeitszeiterfassung, das Nachweisgesetz zur monatlichen Aushändigung eines Stundennachweises. Beides kollidiert mit dem heute gelebten Excel-Prozess: kein Audit-Trail, keine ArbZG-Prüfung, keine revisionssichere Ablage. Parallel kommt das Barrierefreiheitsstärkungsgesetz (BFSG) ab 28.06.2025 ins Spiel — B2B-Oberflächen, die Arbeitnehmer-Ansprüche abbilden (Lohnzettel, Stundennachweis, Urlaubsantrag), gelten als „Verbraucherprodukt“ im Arbeitsverhältnis und sind WCAG-2.2-AA-pflichtig.

Reports sind damit nicht ein Nice-to-have-Dashboard, sondern die Beweisschicht des Produkts: Was der Monteur gestempelt hat, was der Manager genehmigt hat, was die Buchhaltung an DATEV übergeben hat — ein einziger konsistenter Bericht, zu dem Mitarbeiter, Betriebsrat, Steuerberater und Prüfer Zugriff haben.

Schmerzpunkt der Alt-App. LESSONS-LEARNED §2 und §7 beschreiben zwei relevante Mängel:

  1. Reports in der Alt-App waren CRUD-Listen („kann man Einträge abrufen?“), keine echten Auswertungen. Kein Drill-Down, kein Ist-vs-Soll, keine ArbZG-Aggregation.
  2. Export-Formate waren inkonsistent — CSV ohne BOM, ohne Trenner-Deklaration, Datum als ISO-String, den Excel-DE falsch interpretiert. Mehrere Support-Tickets pro Monat wegen „Excel zeigt Zahlen als Text“.

Zusätzlich fiel in der Alt-App die BFSG-Vorprüfung aus — Screenreader-Test an der großen Zeitenübersichts-Tabelle ergab keine Überschriften-Hierarchie, keine Spalten-Header-Assoziation, unlesbare Kontraste im Dark-Theme (Kontrast 3.1:1 statt 4.5:1).

Erwarteter Outcome.

  • Stundennachweis-Generierung für 25 MA von heute ≈45 min/Monat Handarbeit auf <5 min (gemessen an Design-Partner).
  • ArbZG-Abweichungen erscheinen im selben Report wie die Arbeitszeit — kein separater Prüflauf.
  • Drill-Down Monat → Mitarbeiter → Tag → Stempelung in ≤3 Klicks, jeder Pfad bookmark-fähig.
  • BFSG-Audit durch externen Prüfer ohne Critical-Findings auf Reporting + Dashboard (Re-Audit-Budget 3–5 k€/Jahr, siehe TECH-STACK §11.2).
  • Exporte bestehen DATEV-Validator-Import (CSV) und libreoffice --headless-Öffnung (XLSX) ohne Warnungen.

Rolle Aktion Scope Plattform
Mitarbeiter Eigener Stundennachweis (Monat) ansehen, PDF herunterladen, per E-Mail an sich schicken own 📱 🌐
Manager Team-Report (Tag/Woche/Monat), Drill-Down zu einzelnen MA und Buchungen, Bulk-PDF-Versand team 📱 (Read) 🌐 (Bulk)
HR Krankenstand / Urlaubs-Saldo / Abwesenheitskalender (Jahres-Sicht) all 🌐
Buchhaltung Monats-Aggregate für DATEV-Export, XLSX-Pivot-Vorlage, CSV für Lohnabrechnung all 🌐
Bauleitung Projekt-/Baustellen-Report (Ist vs. Budget, abrechenbare vs. interne Zeit, Forecast) team 🌐
Admin Dashboard-Konfiguration, Sichtbarkeit von Widgets, BR-Freischaltung pro Widget all 🌐
Betriebsrat (Rolle br_viewer) Aggregierte Auswertungen ohne Personenbezug einsehen (BetrVG §80) all (aggregiert) 🌐

Persona-Skizzen.

  • Hannes Krüger (Monteur, 38, SHK). Lädt am Ende des Monats „seinen Wisch“ als PDF runter, schickt ihn an den Kundendienst seiner Hausbank für die Kreditantrag-Gehaltsnachweis-Anlage. Muss den Nachweis mit einem Klick in der Mobile-App finden, ohne durch Menüs zu klicken. Kennt den Begriff „Drill-Down“ nicht.
  • Sabine Maier (Manager / Polier, 45, Hochbau). Sitzt abends 15 Minuten im Büro, will sehen: „Wer von meinen 12 Leuten hat diese Woche Überstunden, wer hat noch Kapazität für Samstag?“ Scrollen verboten — will eine einzige Übersicht.
  • Frau Müller (Buchhaltung, 56, Musterbetrieb-Maler). Öffnet Excel, hat Pivot-Tabellen seit 1998 im Schlaf. Will XLSX mit Pivot-Vorlage, nicht CSV-Import-Assistent durchklicken. Screenreader braucht sie nicht, aber Kontrast muss stimmen (Augenbehinderung, neue Brille).
  • Michael Bauer (Betriebsrat, 52, SHK Gebrüder Schmidt). Hat laut BR-Vereinbarung das Recht auf aggregierte Auswertungen — Krankenstand pro Abteilung, Überstunden-Gesamtvolumen — aber nicht auf Einzeldaten. Eine versehentliche Personen-Anzeige wäre ein BetrVG-Verstoß.

US-01 [MVP] Als Mitarbeiter möchte ich meinen Monats-Stundennachweis als PDF herunterladen,
um ihn meiner Bank für den Kreditantrag zu geben.
US-02 [MVP] Als Manager möchte ich am Smartphone den Team-Status sehen (wer stempelt,
wer Pause, wer krank), damit ich abends in 30 Sekunden weiß, wo ich stehe.
US-03 [MVP] Als Buchhaltung möchte ich am Monatsende eine CSV mit allen Zeiten der
Kostenstelle exportieren, die sich in Excel-DE ohne Klick-Stress öffnet.
US-04 [V1] Als Manager möchte ich vom Monats-Übersichts-Widget auf einen Mitarbeiter,
dann auf einen Tag, dann auf eine einzelne Stempelung klicken können,
ohne die URL zu verlieren (bookmark-fähig).
US-05 [V1] Als HR möchte ich den Krankenstand des letzten Quartals pro Abteilung
sehen, damit ich in der Jahresplanung Rückmeldung geben kann.
US-06 [V1] Als Buchhaltung möchte ich eine XLSX mit vorbereiteter Pivot-Tabelle
herunterladen, damit ich die Daten direkt dem Steuerberater senden kann.
US-07 [V1] Als Bauleiter möchte ich einen Projekt-Report mit Ist-Budget vs. Plan-Budget,
abrechenbare vs. interne Zeit, Forecast bis Projektende,
damit ich im wöchentlichen Jour-Fixe eine harte Zahl habe.
US-08 [V1.5] Als Admin möchte ich ein Dashboard aus den 9 Kern-Widgets pro Rolle
zusammenstellen, damit jede Rolle nur die für sie relevanten Zahlen sieht.
US-09 [V1.5] Als Betriebsrat-Viewer möchte ich nur die aggregierten Kennzahlen sehen,
und die UI muss mich explizit darauf hinweisen, dass Personenbezug
ausgeblendet ist (DSGVO Art. 12 Transparenzgebot).
US-10 [V2] Als Entwickler (Drittanbieter-Integration) möchte ich denselben Monats-Report
über die öffentliche API abrufen, den ich im Web sehe — als JSON mit Links
auf PDF/CSV/XLSX-Varianten.

  • F-M-01 — Tagesübersicht (Mitarbeiter). Heutige Stempelungen, Tagesbrutto, Pause, Nettoarbeitszeit, Saldo-Delta zum Sollwert. Offline-fähig aus Drift-Cache.
  • F-M-02 — Wochen-/Monats-Saldo. Kompakte Karte „Woche 16: 32:15 / 40:00 h — +6:42 auf Konto“. Tap öffnet die Detail-Ansicht der Woche.
  • F-M-03 — Stundennachweis-PDF abrufen. Dropdown Monat → Vormonat → Vorvormonat (max. 13 Monate rückwirkend auf Mobile — ältere nur Web, um Bundle-Größe zu begrenzen). Tippen auf „PDF“ öffnet native Share-Sheet (iOS UIActivityViewController, Android Intent.ACTION_SEND).
  • F-M-04 — Team-Status (Manager). Live-Liste der Team-Mitglieder mit Pill-Status (🟢 stempelt / 🟡 Pause / 🔴 abwesend), aktuelle Baustelle, Tagessaldo. Pull-to-refresh, SSE-Live-Update falls online.
  • F-M-05 — ArbZG-Kacheln. Heutige ArbZG-Risiko-Karte: „1 Mitarbeiter > 9 h“ (tap → Drill-Down zur Person).
  • F-M-06 — Offline-Banner. Wenn Daten aus Drift stammen (>5 min alt), ist oben ein Banner sichtbar: „Daten vom 19.04.2026 09:12, offline gecached“.
  • F-M-07 — Keine Excel-/XLSX-Generierung auf Mobile. Bewusste Einschränkung: Bulk-Exporte laufen am Server, Mobile zeigt nur Thumbnail-PDF an, nicht XLSX/CSV-Writing.
  • F-W-01 — Report-Matrix-Startseite. Kartengrid mit den 5 Haupt-Reports: Mitarbeiter / Projekt / Kunde / Kostenstelle / Abwesenheit. Jede Karte zeigt den zuletzt ausgeführten Report.
  • F-W-02 — Drill-Down-Hierarchie. Pfad Monat (Aggregat) → Mitarbeiter → Tag → Stempelung. Jede Ebene ist ein eigener URL-State:
    • /reports/monthly/2026-04
    • /reports/monthly/2026-04/employees/hannes-krueger
    • /reports/monthly/2026-04/employees/hannes-krueger/days/2026-04-19
    • /reports/monthly/2026-04/employees/hannes-krueger/days/2026-04-19/entries/01J8K... Alle URLs sind bookmark-fähig, Browser-Back funktioniert semantisch.
  • F-W-03 — Filter-Leiste mit Persistenz. Filter (Zeitraum, Mitarbeiter-Auswahl, Projekt, Kostenstelle, Abwesenheitsart) werden pro (user_id, report_type) in report_filter_states persistiert. Default beim nächsten Öffnen: letzter verwendeter Filter. Filter-Pills mit „ד zum Einzelentfernen, „Alle zurücksetzen“ als Ghost-Button.
  • F-W-04 — KPI-Kopfleiste. Oberhalb der Tabelle: 4 KPI-Karten (Ist-Stunden, Soll-Stunden, Krankenstand, ArbZG-Verletzungen). Karten sind klickbar → filtert die Tabelle auf die jeweilige Dimension.
  • F-W-05 — Datentabelle mit Bulk-Action-Bar. Sortierung pro Spalte (ARIA aria-sort), Checkbox-Column, „Alle auswählen“-Mehrstufig (checkbox--mixed-State). Bei n≥1 ausgewählten Zeilen erscheint die Bulk-Action-Bar (Design-System .wz-bulkbar) mit: Genehmigen / Ablehnen / Als PDF versenden / Als CSV exportieren. Bulk-Aktionen werden serverseitig als Batch-Job ausgeführt, Fortschritt im Toast.
  • F-W-06 — Export-Profile. Dropdown „Export“ mit drei festen Profilen plus Profil verwalten…:
    • PDF — Gestempelter Stundennachweis (DIN A4, Kopf mit Firmenlogo + Mitarbeiter-Stammdaten, Fuß mit Signatur-Zeile und Hash-Chain-Referenz, QR-Code zur Online-Verifikation).
    • CSV — Excel-DE-kompatibel (UTF-8 + BOM \uFEFF, Trennzeichen ;, Datum TT.MM.JJJJ, Dezimal-Komma, Tausender-Punkt, erste Zeile sep=; für Excel-Legacy).
    • XLSX — mit Pivot-Vorlage (Blatt 1 „Daten“ als ListObject, Blatt 2 „Pivot“ mit vorkonfigurierter PivotTable: Zeilen = Mitarbeiter, Spalten = Tag, Wert = Summe Nettoarbeitszeit; Blatt 3 „Sollstunden-Abgleich“ mit Formel-Spalten).
  • F-W-07 — Bulk-PDF-Versand. „Monats-Nachweise an alle 25 MA per E-Mail versenden“ — erzeugt n PDFs im Batch, BullMQ-Job, SES-Versand an user.email, Audit-Log-Eintrag report.bulk.sent pro Empfänger.
  • F-W-08 — Dashboard-Widgets (V1). 9 vordefinierte Widgets, rollen-sichtbar per default, admin-konfigurierbar:
    1. Sollstunden-Erfüllung (Donut: Ist/Soll-% pro Mitarbeiter, farbig: grün >95 %, gelb 80–95 %, rot <80 %)
    2. Krankenstand letzte 30 Tage (Linie)
    3. Top-5-Projekte nach Stunden
    4. ArbZG-Verletzungen (Tages-Balken mit Drill-Down)
    5. Überstunden-Konto Team (Tabelle mit Saldo-Trend-Pfeilen)
    6. Urlaubs-Saldo Team (gestapelter Balken)
    7. Abrechenbare Zeit vs. Intern (Tortendiagramm pro Projekt)
    8. Forecast Projekt-Budget (Burndown mit Plan-Linie)
    9. Letzte ArbZG-Warnungen (Event-Log-Feed)
  • F-W-09 — Betriebsrat-Sicht (V1.5). Rolle br_viewer sieht nur Widgets 1 (aggregiert ohne MA-Namen), 2, 4 (nur Counts), 6 (Summe). Jede Widget-Karte trägt einen Banner: „Aggregiert — keine personenbezogenen Daten sichtbar (DSGVO Art. 12, BetrVG §80)“.
  • F-W-10 — Bookmark-fähige URLs + Teilen. „Link teilen“-Knopf kopiert die aktuelle URL inkl. Filter-State in die Zwischenablage. Empfänger öffnet dieselbe Sicht (RLS prüft Zugriff).
  • F-W-11 — Print-Stylesheet. CSS @media print rendert die Tabelle saubergedruckt (keine Sidebar, keine Bulk-Bar, Kopf mit Datum + Tenant, Fußzeile mit Hash-Referenz).
  • F-X-01 — Deterministische Report-IDs. Jeder generierte Report hat eine ID rpt_{uuid-v7}, die sowohl als Filename-Suffix (stundennachweis-krueger-2026-04_rpt_01J8K4...pdf) als auch im Audit-Log als auch als QR-Code im PDF erscheint. GET /v1/kern/reports/{id} liefert die Metadaten, die Datei und den Hash zurück.
  • F-X-02 — Aggregations-Konsistenz. Tagesbrutto = Σ Buchungen, Tagesnetto = Brutto − Pausen. Rundung auf 1 min. Dieselbe Formel in Dart (Client) und TypeScript (Server) — Property-based Test gegen dieselben Fixtures (siehe §13 C-05).
  • F-A-01 — Export-Profil-Editor. Admin kann benannte Export-Profile anlegen (z. B. „DATEV LODAS Variante Tenant X“) mit eigenem Spalten-Set, Dateinamenschema, Header-Zeile.
  • F-A-02 — Dashboard-Zusammenstellung pro Rolle. Admin wählt pro Rolle, welche Widgets default sichtbar sind. Jeder User kann innerhalb des erlaubten Sets selbst umsortieren (drag-and-drop); individuelle Reihenfolge pro User persistiert.
  • F-A-03 — Betriebsrat-Widget-Freigabe. Widgets mit Leistungs-/Verhaltenskontroll-Charakter (Widget 1, 4, 5) zeigen im Admin-Editor einen Banner „BetrVG §87(1)6 — Aktivierung erfordert Betriebsrats-Zustimmung. Zustimmung hochladen…“. Default: deaktiviert.
  • F-A-04 — Aufbewahrungs-Config für Exporte. S3 Object Lock Compliance-Mode, 10 Jahre (§147 AO) — für jeden generierten Report-PDF-Beleg. Nicht konfigurierbar runter, konfigurierbar rauf (bis 30 Jahre).
Anforderung-ID MVP V1 V1.5
F-M-01 Tagesübersicht
F-M-02 Wochen-/Monats-Saldo
F-M-03 Stundennachweis-PDF (Mobile Download)
F-M-04 Team-Status (Manager)
F-M-05 ArbZG-Kachel
F-W-01 Report-Matrix
F-W-02 Drill-Down-URLs
F-W-03 Filter-Persistenz
F-W-04 KPI-Kopfleiste
F-W-05 Bulk-Action-Bar
F-W-06 Export-Profile PDF/CSV/XLSX ✅ (PDF+CSV) ✅ (XLSX)
F-W-07 Bulk-PDF-Versand
F-W-08 Dashboard 9 Widgets
F-W-09 BR-Sicht
F-W-10 URL-Teilen
F-W-11 Print-Stylesheet
F-A-01 Export-Profil-Editor
F-A-02 Rollen-Dashboard
F-A-03 BR-Widget-Freigabe
F-A-04 Object-Lock-Config

HTML-Hero-Mockup: 07-reporting-auswertung.html — Mobile (Monteur Hannes, Monats-Stundennachweis) + Web (Manager Sabine, Monats-Report mit Drill-Down) side-by-side, Design-Tokens aus werkszeit-design-system.css, Daten aus Seed-Tenants musterbetrieb-maler (Bayern) und shk-gebruder-schmidt (NRW).

┌────────────────────────────────┐
│ ← Mein Stundennachweis ⚙ │
├────────────────────────────────┤
│ April 2026 ▼ │
│ │
│ ┌──────────────────────────┐ │
│ │ Summe Netto 167:12 h │ │
│ │ Soll 168:00 h │ │
│ │ Saldo −0:48 h │ │
│ │ ArbZG-Warnungen 0 │ │
│ └──────────────────────────┘ │
│ │
│ Pro Woche: │
│ ┌──────────────────────────┐ │
│ │ KW 14 40:05 / 40:00 🟢│ │
│ │ KW 15 38:20 / 40:00 🟡│ │
│ │ KW 16 42:10 / 40:00 🟢│ │
│ │ KW 17 46:37 / 48:00 🟢│ │
│ └──────────────────────────┘ │
│ │
│ [ ⬇ PDF-Stundennachweis ] │
│ [ ✉ Per E-Mail an mich ] │
│ │
├────────────────────────────────┤
│ [Zeit] [Plan] [Doku] [Mehr] │
└────────────────────────────────┘

6.2 Mobile — Manager-Team-Status (ArbZG-Risiko sichtbar)

Abschnitt betitelt „6.2 Mobile — Manager-Team-Status (ArbZG-Risiko sichtbar)“
┌────────────────────────────────┐
│ ← Team-Status Mi19.04│
├────────────────────────────────┤
│ ┌────────────┬────────────┐ │
│ │ Einge- │ In Pause │ │
│ │ stempelt │ │ │
│ │ 18 │ 3 │ │
│ ├────────────┼────────────┤ │
│ │ Krank/Url. │ ArbZG⚠ │ │
│ │ 4 │ 1 → │ │
│ └────────────┴────────────┘ │
│ │
│ ArbZG-Risiko heute: │
│ ┌──────────────────────────┐ │
│ │ 🟡 Hannes Krüger │ │
│ │ 9:42 h · noch nicht │ │
│ │ ausgestempelt │ │
│ │ (Limit §3 ArbZG 10 h) │ │
│ │ [Jetzt informieren →] │ │
│ └──────────────────────────┘ │
│ │
├────────────────────────────────┤
│ [Zeit] [Plan] [Doku] [Mehr] │
└────────────────────────────────┘

6.3 Web — Monats-Report (Aggregat-Ebene, Drill-Down-Einstieg)

Abschnitt betitelt „6.3 Web — Monats-Report (Aggregat-Ebene, Drill-Down-Einstieg)“
┌────────────────────────────────────────────────────────────────────────────────┐
│ Werkszeit · shk-gebruder-schmidt Thomas S. · Bauleitung [Profil ▾] │
├──────────────┬─────────────────────────────────────────────────────────────────┤
│ Sidebar │ Reports / Mitarbeiter / 2026-04 [⌨ Shortcuts]│
│ │ ─────────────────────────────────────────────────────────────── │
│ Zeit │ Pfad: Monat ▸ Mitarbeiter │
│ Plan │ │
│▶Reports │ Filter: [Zeitraum: April 2026 ×] [Abteilung: SHK-Nord ×] │
│ Bautagebuch │ [Nur ArbZG-Verletzungen ☐] [Filter zurücksetzen] │
│ … │ │
│ │ ┌──────────┬──────────┬──────────┬──────────┐ │
│ │ │Ist h │Soll h │Krankstand│ArbZG-Verl│ ← KPI-Kacheln │
│ │ │ 3.247:12 │ 3.360:00 │ 4,1 % │ 3 │ │
│ │ └──────────┴──────────┴──────────┴──────────┘ │
│ │ │
│ │ ┌──[Bulk-Bar]── 3 ausgewählt ─ [PDF-Versand] [CSV] [Genehmigen]┐│
│ │ │ ☑ │ Mitarbeiter │Abt. │Ist h │Soll h│Saldo│ArbZG│ ▸ ││
│ │ │───┼──────────────────┼─────┼──────┼──────┼─────┼─────┼───────││
│ │ │ ☑ │ Hannes Krüger │Nord │167:12│168:00│−0:48│ 0 │ [>] ◀─┼─ Drill-Down
│ │ │ ☑ │ Mehmet Yılmaz │Nord │172:40│168:00│+4:40│ 1 │ [>] ││
│ │ │ ☑ │ Frank Demir │Süd │ 88:15│168:00│Krank│ 0 │ [>] ││
│ │ │ ☐ │ Sabine Maier │Büro │152:00│168:00│−16:0│ 0 │ [>] ││
│ │ └─────────────────────────────────────────────────────────────┘│
│ │ │
│ │ [⬇ PDF] [⬇ CSV (Excel-DE)] [⬇ XLSX mit Pivot] [🔗 Link teilen]│
└──────────────┴─────────────────────────────────────────────────────────────────┘

6.4 Web — Drill-Down-Ebene 2: Mitarbeiter Hannes Krüger, April 2026 (Tagesliste)

Abschnitt betitelt „6.4 Web — Drill-Down-Ebene 2: Mitarbeiter Hannes Krüger, April 2026 (Tagesliste)“
┌────────────────────────────────────────────────────────────────────────────────┐
│ Reports / Mitarbeiter / 2026-04 / Hannes Krüger [⌨ Shortcuts] │
│ ───────────────────────────────────────────────────────────────────────────── │
│ Pfad: Monat ▸ Mitarbeiter ▸ Hannes Krüger │
│ │
│ Hannes Krüger · Personalnr. 0042 · SHK-Nord │
│ Ist 167:12 h / Soll 168:00 h · Saldo −0:48 h · 0 ArbZG-Warnungen │
│ │
│ ┌────┬──────────┬───────┬────────┬─────┬──────────────────────────────┐ │
│ │ Tag│ Datum │Brutto │Pausen │Netto│ Projekt │ │
│ │ Mo │ 01.04. │ 8:30 │ 0:45 │7:45 │ Bürogebäude Lehrer Allee 7 │ [>] ◀─┼─ Drill-Down
│ │ Di │ 02.04. │ 8:15 │ 0:45 │7:30 │ Bürogebäude Lehrer Allee 7 │ [>] │
│ │ Mi │ 03.04. │ 8:45 │ 0:45 │8:00 │ Villa Waldheim │ [>] │
│ │ … │ … │ … │ … │ … │ … │ │
│ │ Fr │ 30.04. │ 8:00 │ 0:30 │7:30 │ Bürogebäude Lehrer Allee 7 │ [>] │
│ └────┴──────────┴───────┴────────┴─────┴──────────────────────────────┘ │
│ │
│ [⬇ PDF dieses Mitarbeiters] [⬇ CSV nur Hannes K.] [🔗 Link teilen] │
└────────────────────────────────────────────────────────────────────────────────┘

6.5 Web — Drill-Down-Ebene 4: Einzel-Stempelung (Audit-Sicht)

Abschnitt betitelt „6.5 Web — Drill-Down-Ebene 4: Einzel-Stempelung (Audit-Sicht)“
┌────────────────────────────────────────────────────────────────────────────────┐
│ Reports / … / 2026-04-19 / Stempelung 01J8K4NJ… │
│ ───────────────────────────────────────────────────────────────────────────── │
│ │
│ 📌 Stempelung-ID 01J8K4NJAR9TQ5KXFN8C7V0YW1 │
│ 👤 Mitarbeiter Hannes Krüger (Personalnr. 0042) │
│ ⏱ Start Mo 19.04.2026 07:02:18 MESZ │
│ ⏱ Ende Mo 19.04.2026 16:42:05 MESZ │
│ ⏸ Pause 12:00–12:45 (automatisch, ArbZG §4) │
│ 🏗 Projekt Bürogebäude Lehrer Allee 7, München │
│ 📍 Geo (Start) 48.1351, 11.5820 (± 12 m) │
│ 📱 Erfasst via iOS 18.4, NFC-Siegel (Seriennr. LAP-WZ-442) │
│ │
│ Hash-Chain: │
│ prev fd3c…ae71 │
│ self 8f02…91bc ✓ validiert │
│ │
│ Änderungshistorie: │
│ ┌─────────────┬─────────────────┬──────────────────────────────────┐ │
│ │ 19.04 07:02 │ created │ Hannes Krüger (offline gestempelt)│ │
│ │ 19.04 12:00 │ pause.started │ System (ArbZG-Regel §4) │ │
│ │ 19.04 16:42 │ stopped │ Hannes Krüger │ │
│ └─────────────┴─────────────────┴──────────────────────────────────┘ │
│ │
│ [🔗 Audit-Log öffnen] [🔒 Als Beleg zur Akte ablegen] │
└────────────────────────────────────────────────────────────────────────────────┘

6.6 Web — Dashboard (Bauleitung-Rolle) mit 4 sichtbaren Widgets

Abschnitt betitelt „6.6 Web — Dashboard (Bauleitung-Rolle) mit 4 sichtbaren Widgets“
┌────────────────────────────────────────────────────────────────────────────────┐
│ Dashboard · Bauleitung [⚙ Layout] │
├────────────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Sollstunden-Erf. │ │ Krankenstand 30 Tage│ │
│ │ ○ 94 % │ │ ▁▂▃▂▃▄▃▂▁ │ │
│ │ 12 MA >95% · 4 MA… │ │ Ø 4,1 % (Vj 3,8 %) │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Top-5 Projekte h │ │ Forecast Projekt X │ │
│ │ 1. Lehrer Allee 234 │ │ Ist 78 % · Plan 82 %│ │
│ │ 2. Villa Waldh. 156 │ │ Prognose +3 Tage │ │
│ │ 3. Schule Poing 112 │ │ │ │
│ │ 4. Kita Haar 89 │ │ │ │
│ │ 5. KH Ramers. 72 │ │ │ │
│ └─────────────────────┘ └─────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────────┘

Symbol-Konvention wie TEMPLATE.md §6.


Reports lesen überwiegend aus bestehenden Tabellen (time_entries, absences, projects, employees). Neu sind drei Tabellen zur Persistenz der Report-Konfiguration und der generierten Export-Belege (GoBD-Pflicht).

apps/api/src/db/schema/reports.ts
import { pgTable, uuid, text, jsonb, timestamp, bytea, index, pgEnum } from 'drizzle-orm/pg-core';
export const reportFormatEnum = pgEnum('report_format', ['pdf', 'csv', 'xlsx', 'json']);
export const reportKindEnum = pgEnum('report_kind', ['monthly', 'weekly', 'daily', 'project', 'customer', 'absence']);
// 1) Persistierte Filter pro User und Report-Typ (F-W-03)
export const reportFilterStates = pgTable('report_filter_states', {
id: uuid('id').primaryKey().defaultRandom(),
tenantId: uuid('tenant_id').notNull().references(() => tenants.id),
userId: uuid('user_id').notNull().references(() => users.id),
reportKind: reportKindEnum('report_kind').notNull(),
filter: jsonb('filter').notNull(), // { from, to, employees[], projects[], ... }
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (t) => ({
uq: index('rfs_user_kind_uq').on(t.tenantId, t.userId, t.reportKind),
}));
// 2) Generierte Report-Dokumente — GoBD-Pflichtbeleg, append-only, Object-Lock in S3
export const reportDocuments = pgTable('report_documents', {
id: uuid('id').primaryKey().defaultRandom(), // UUID v7 = rpt_...
tenantId: uuid('tenant_id').notNull().references(() => tenants.id),
kind: reportKindEnum('kind').notNull(),
format: reportFormatEnum('format').notNull(),
params: jsonb('params').notNull(), // exakt die Filter, mit denen generiert wurde
s3Key: text('s3_key').notNull(), // s3://wz-reports-eu/{tenant}/{yyyy}/{mm}/rpt_...
sha256: bytea('sha256').notNull(), // Datei-Hash
sizeBytes: text('size_bytes').notNull(),
rowCount: text('row_count'), // bei CSV/XLSX
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
createdBy: uuid('created_by').notNull().references(() => users.id),
// GoBD Hash-Chain:
hashPrev: bytea('hash_prev'),
hashSelf: bytea('hash_self').notNull(),
objectLockRetainUntil: timestamp('object_lock_retain_until', { withTimezone: true }).notNull(),
}, (t) => ({
byTenantCreated: index('rd_tenant_created_idx').on(t.tenantId, t.createdAt.desc()),
byKindParams: index('rd_kind_params_idx').on(t.tenantId, t.kind, t.createdAt.desc()),
}));
// 3) Dashboard-Widget-Konfiguration pro Tenant+Rolle (F-A-02)
export const dashboardLayouts = pgTable('dashboard_layouts', {
id: uuid('id').primaryKey().defaultRandom(),
tenantId: uuid('tenant_id').notNull().references(() => tenants.id),
role: text('role').notNull(), // 'manager' | 'bauleitung' | 'hr' | ...
widgets: jsonb('widgets').notNull(), // [{ id, order, enabled, brApproved }]
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
updatedBy: uuid('updated_by').notNull().references(() => users.id),
}, (t) => ({
uq: index('dl_tenant_role_uq').on(t.tenantId, t.role),
}));

RLS-Policies (Ausschnitt für report_documents — Prinzip gilt für alle drei Tabellen):

ALTER TABLE report_documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY rd_tenant_isolation ON report_documents
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- Scope-Regel: Mitarbeiter sehen nur Reports, die sie selbst erzeugt haben ODER die sie als Empfänger benennen
CREATE POLICY rd_scope_own ON report_documents
FOR SELECT
USING (
tenant_id = current_setting('app.tenant_id')::uuid
AND (
current_setting('app.scope') = 'all'
OR (current_setting('app.scope') = 'team' AND created_by IN (
SELECT user_id FROM team_members WHERE team_id = ANY(current_setting('app.team_ids')::uuid[])
))
OR (current_setting('app.scope') = 'own' AND created_by = current_setting('app.user_id')::uuid)
)
);
-- Append-only: kein UPDATE, kein DELETE
CREATE POLICY rd_no_update ON report_documents FOR UPDATE USING (false);
CREATE POLICY rd_no_delete ON report_documents FOR DELETE USING (false);

ER-Bezug. Referenziert: time_entries (Faktentabelle), absences, projects, customers, cost_centers, employees, users, tenants, audit_log. Neue Tabellen: drei oben. Keine Denormalisierung — Aggregate werden zur Laufzeit berechnet (pgvector nicht benötigt; HNSW-Index nicht nötig).

Performance-Leitplanke. Für Monats-Aggregate über >80 MA + >90 Tage: Materialized View mv_daily_employee_totals mit REFRESH … CONCURRENTLY nachts. Alt-Suche fragt ausschließlich auf MV. p95 für Monats-Report bei 80 MA: Ziel <500 ms (siehe §13 E-05).


Pfad-Konvention: /v1/kern/reports/…, alle hinter Modul-Gate module.kern.reporting.

Methode Pfad Auth-Scope Rate-Limit-Klasse Idempotenz Beschreibung
GET /v1/kern/reports/monthly reports:read:<scope> Standard Monats-Aggregat, Query-Params month, employees[], projects[]
GET /v1/kern/reports/monthly/{month}/employees/{employeeId} reports:read:<scope> Standard Drill-Down Mitarbeiter
GET /v1/kern/reports/monthly/{month}/employees/{employeeId}/days/{date} reports:read:<scope> Standard Drill-Down Tag
GET /v1/kern/reports/entries/{entryId} reports:read:<scope> Standard Drill-Down einzelne Stempelung mit Hash-Chain
POST /v1/kern/reports/exports reports:export Privileged (10/min) Idempotency-Key Pflicht Exportjob anlegen (format, params)
GET /v1/kern/reports/exports/{id} reports:read:<scope> Standard Status abfragen, `state: queued
GET /v1/kern/reports/exports/{id}/download reports:read:<scope> Privileged Presigned S3 URL, 10 min gültig
POST /v1/kern/reports/exports/bulk-email reports:export + reports:send_email Privileged (5/min) Idempotency-Key Pflicht Bulk-PDF-Versand an n Empfänger
GET /v1/kern/reports/filter-states/{kind} reports:read:own Standard Persistierten Filter-State des Users laden
PUT /v1/kern/reports/filter-states/{kind} reports:write:own Standard Filter-State speichern
GET /v1/kern/dashboards/{role} reports:read:<scope> Standard Dashboard-Layout für Rolle
PUT /v1/kern/dashboards/{role} dashboards:admin Standard Dashboard-Layout speichern (Admin)

OpenAPI-Schema-Skizze:

paths:
/v1/kern/reports/exports:
post:
operationId: createReportExport
x-werkszeit-scope: reports:export
x-werkszeit-rate-limit: privileged
parameters:
- in: header
name: Idempotency-Key
required: true
schema: { type: string, format: uuid }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ReportExportCreate' }
responses:
'202':
description: Export-Job angenommen
headers:
Location: { schema: { type: string }, description: "/v1/kern/reports/exports/{id}" }
content:
application/json:
schema: { $ref: '#/components/schemas/ReportExport' }
'409':
description: Idempotency-Konflikt
components:
schemas:
ReportExportCreate:
type: object
required: [kind, format, params]
properties:
kind: { type: string, enum: [monthly, weekly, daily, project, customer, absence] }
format: { type: string, enum: [pdf, csv, xlsx, json] }
params: { type: object, additionalProperties: true }
deliverEmailTo: { type: array, items: { type: string, format: email } }

Webhook-Events (vgl. §3.12 / Feinkonzept 12):

  • report.export.requested
  • report.export.ready — Payload enthält downloadUrl (10 min gültig) und sha256
  • report.export.failed
  • report.bulk.sent — pro Empfänger ein Event

Profil Begründung Mechanik
Best-effort-offline (Mobile, Ansicht) Mitarbeiter und Manager wollen auch im Keller sehen, wie viel sie heute schon gestempelt haben. Wir blockieren die Ansicht nicht, aber kennzeichnen Datenstand. Drift-Cache der letzten 13 Monate eigener Daten; Team-Daten werden bei Online-Sync aktualisiert. Offline-Banner bei Stand >5 min.
Online-only (Web, Exporte, Drill-Down) Exports brauchen Server (Typst, Excel-Writer, BullMQ). Drill-Down auf einzelne Stempelungen braucht Live-Hash-Check. UI-Block bei connectivity == none, explizite Fehlermeldung „Keine Verbindung zum Werkszeit-Server“.

Konflikt-Strategie. Reports sind lesend — es gibt keinen Konflikt. Die Konfiguration (Filter-States, Dashboard-Layout) wird per If-Match/ETag optimistic-locked: bei Konkurrenz-Schreib-Situation erhält der zweite Client 412 Precondition Failed und kann reload anbieten.

Foto-/Datei-Sync. Keine Fotos in Reports; PDFs werden serverseitig erzeugt, Client lädt nur via presigned URL. S3-Multipart nicht erforderlich (PDFs typisch <2 MB).


  • Unveränderbarkeit Report-Belege: Jeder generierte Export (PDF, CSV, XLSX) wird in S3 mit Object Lock Compliance-Mode, 10 Jahre (§147 AO) gespeichert. Selbst Tenant-Admin kann das Retention nicht verkürzen. Bucket-Policy verbietet s3:PutObjectRetention mit Mode=Governance.
  • Hash-Chain: report_documents.hashSelf = SHA-256(canonicalize(id, tenant_id, kind, format, params, s3_key, sha256, size_bytes, row_count, created_at, created_by, hash_prev)). Hash-Chain-Validierungs-Test in apps/api/test/compliance/reports-hashchain.test.ts (§13 C-01).
  • Quelle der Aggregate: Reports lesen nur aus time_entries (append-only), nicht aus einer Zwischen-Aggregat-Tabelle, die mutiert werden könnte. Materialized View ist explizit als „derived“ markiert und ausschließlich für Read-Performance; jeder MV-Refresh läuft mit SECURITY DEFINER-Funktion, die in audit_log protokolliert.
  • PDF-Stundennachweis trägt einen QR-Code mit URL https://app.werkszeit.de/verify/{rpt_id}?h={sha256_prefix}. Die öffentliche Verifikationsseite prüft die Hash-Chain im Backend und gibt grün/rot zurück — ohne eingeloggten User (Prüfungs-Szenario durch Finanzamt).
  • §3 Höchstarbeitszeit (10 h/Tag): Tages-Widget markiert rot ab 10 h, gelb ab 9 h. Aggregat zählt Tage pro MA, an denen >10 h erreicht wurden.
  • §4 Pausenregelung: Im Tages-Detail wird die automatische Pausen-Einfügung (ArbZG-konforme 30/45 min) als Systemeintrag angezeigt und nicht als Mitarbeiter-Stempelung kaschiert.
  • §5 Ruhezeit (11 h): Dashboard-Widget „ArbZG-Verletzungen“ zählt Tage mit Ruhezeitverletzung <11 h. Detail zeigt konkret: „Zwischen Stempel-Out 22:10 und Stempel-In 07:02 nur 8:52 h“.
  • §7 Ausnahmen (Tarifvertrag): Als Ausnahme markierte Einträge erscheinen gesondert mit Hinweis „§7 ArbZG — tarifliche Ausnahme“.

— nicht zutreffend, weil VOB-Abrechnung in §4.5 Bau-Modul liegt. Reports konsumieren nur Zeit-/Abwesenheits-/Projekt-Daten.

  • Art. 5 Datenminimierung: Exporte enthalten nur die vom User explizit über Filter ausgewählten Felder. „Alle Felder“ ist kein Default-Profil. Geo-Koordinaten nur, wenn Rolle bauleitung + Widget-Feature-Flag geo_in_reports vom BR freigegeben ist.
  • Art. 6 Rechtsgrundlage: Vertrag (Art. 6 Abs. 1 b, Arbeitsverhältnis) für Mitarbeiter-Sichten; berechtigtes Interesse (Art. 6 Abs. 1 f, Unternehmenssteuerung) für Manager-/Bauleitungs-Aggregate; rechtliche Verpflichtung (Art. 6 Abs. 1 c, §147 AO) für die Aufbewahrung der Exporte.
  • Art. 12 Transparenz: Jede Report-Seite enthält einen Fuß-Disclaimer: „Diese Auswertung nutzt personenbezogene Arbeitszeit-Daten gemäß Art. 6 Abs. 1 b/f DSGVO. Einsichtsrecht nach Art. 15 DSGVO beim Datenschutzbeauftragten.“ Link auf Tenant-spezifische DSB-Mail.
  • Art. 15 Auskunft: Mitarbeiter sehen in der Mobile-App ihren vollständigen eigenen Report — das erfüllt de facto das Auskunftsrecht ohne separaten Antrag.
  • Art. 17 Löschung: Zeitbuchungen sind GoBD-pflichtig nicht löschbar. Bei DSGVO-Löschantrag wird der Name des Mitarbeiters in den generierten Exporten auf Mitarbeiter #0042 (anonymisiert 2029-06-01) maskiert; der Export-Beleg bleibt als Hash-gekettete Datei erhalten (Konflikt DSGVO ↔ GoBD, gelöst durch Maskierung). Dokumentiert in gobd-verfahrensdoku.md §4.2.
  • Art. 20 Datenexport: Mitarbeiter erhalten ihre eigenen Reports im JSON-Format über GET /v1/kern/ess/data-export (Feinkonzept 11).
  • Art. 30 Verarbeitungsverzeichnis: Eintrag „Arbeitszeit-Reporting“, Rechtsgrundlage Vertrag, Empfänger Steuerberater (via DATEV-Export), Speicherdauer 10 Jahre (§147 AO), technisch-organisatorische Maßnahmen: RLS, S3 Object Lock, TLS 1.3, Audit-Log.
  • DSFA: Pflicht-DSFA, weil systematische Überwachung im Sinne Art. 35 Abs. 3 b (Beschäftigtendaten-Verarbeitung mit Leistungs-/Verhaltensbezug). DSFA liegt als docs/werkszeit/dsfa/reporting.md vor — nicht Scope dieses Feinkonzepts, aber Voraussetzung vor Go-Live.
  • Screenreader-Labels (VoiceOver / TalkBack / NVDA):
    • KPI-Karten: aria-label="Soll-Stunden 168, Ist-Stunden 167 Stunden 12 Minuten, Abweichung minus 48 Minuten".
    • Tabelle: <caption> mit Report-Titel, scope="col" auf <th>, scope="row" auf Mitarbeiternamen, aria-sort mit Live-Update.
    • Filter-Pills: role="listitem" mit Remove-Button aria-label="Filter 'Abteilung SHK-Nord' entfernen".
    • Drill-Down-Links: aria-label="Details von Hannes Krüger öffnen, 167 Stunden 12 Minuten".
  • Tastatur-Navigation: Tab-Reihenfolge Sidebar → Hauptinhalt → Bulk-Bar → Tabelle → Pagination → Export. Skip-Link <a href="#report-content">Zum Report-Inhalt</a> am Seitenanfang. Enter auf Tabellenzeile = Drill-Down; Shift+Enter = öffnet in neuem Tab. Ctrl+E = Export-Dropdown öffnen. Bulk-Bar mit Shift+Klick-Range-Selection; Leertaste auf Checkbox toggelt.
  • Focus-Visible: :focus-visible mit 3 px Outline in --wz-stahl (Design-System Token), nicht :focus pauschal — sonst Maus-Klicks zeigen den Focus-Ring.
  • Kontrast (WCAG 2.2 AA ≥4.5:1):
    • Primärtext #14181F auf #FAFAF7 → 18.3:1 ✓
    • Orange #FF6B1A als CTA auf Anthrazit #1A1F2B → 4.89:1 ✓ (Normal-Text AA 4.5:1, Large AAA)
    • Status-Pill Warning #C77A0A auf #FBF1DA → 4.52:1 ✓ (grenzwertig, validiert)
    • KPI-Delta-Rot #B5391A auf #FFFFFF → 6.28:1 ✓
    • Drill-Down-Link Stahl #2C5F8D auf #FFFFFF → 6.24:1 ✓
  • Kein Farbe-einziges-Indiz: ArbZG-Verletzungen sind rot UND tragen das Symbol ⚠ und Text „ArbZG-Verletzung“. Sort-Pfeile sind Pfeile UND aria-sort.
  • Media-Queries: prefers-reduced-motion deaktiviert Diagramm-Animationen; prefers-contrast: more wechselt in High-Contrast-Theme (Token-Set --wz-ink-900 wird auf #000, Background auf #FFF, Border 2 px).
  • Touch-Targets Mobile: 48 × 48 dp (Design-System --wz-touch), Handschuh-tauglich.
  • Test-Harness: axe-core in Playwright-E2E läuft auf jeder Report-Seite, Critical/Serious = Build fail. Flutter-Semantics-Test über SemanticsHarness deckt die Mobile-Views.
  • Externer Audit: Pre-Launch-Audit (≈5 k€, TECH-STACK §11.2) durch BFSG-zertifizierten Prüfer. Re-Audit bei jedem Major-UI-Release.
  • Widget-Freischaltung: Widgets mit Leistungs-/Verhaltenskontroll-Charakter (Sollstunden-Erfüllung pro MA, ArbZG-Verletzungen pro MA, Überstunden-Saldo) sind per Default deaktiviert. Admin sieht in Einstellungen → Dashboards → Widgets einen Banner mit dem Gesetzestext, einen Upload-Slot für die BR-Zustimmung (PDF) und ein Datum-Feld „Beschluss vom“. Erst nach Upload + Bestätigung wird das Widget für die Rollen sichtbar. Audit-Log-Eintrag dashboard.widget.br_approved.
  • BR-Sicht (br_viewer): Separate Rolle mit Scope „aggregiert ohne Personenbezug“. In F-W-09 definiert.
  • Mengengerüst-Grenze: Bei Teams <5 MA wird Aggregat nicht angezeigt („zu kleine Gruppengröße — Re-Identifikation möglich“). Empirisch abgeleitet aus k-Anonymität k=5.
  • §147 AO — 10 Jahre Aufbewahrung: S3 Object Lock Compliance-Mode, siehe 10.1.
  • EN 301 549 / WCAG 2.2 AA: harmonisiert, Audit-Basis.
  • BDSG §26 (Beschäftigtendatenschutz): dokumentiert in DSFA.

# Szenario Erwartetes Verhalten
EC-01 Zeitzonenwechsel — Monteur reist 18.04. nach Österreich (UTC+2), stempelt dort aus, kehrt zurück Alle Zeiten werden als UTC gespeichert; Report-Rendering nutzt Europe/Berlin als Tenant-Default. Grenzwert: Stempelung landet am korrekten Kalendertag, selbst wenn lokal die Datumsgrenze überschritten wurde. Visualisierung zeigt kleinen Globus-Indikator „Erfasst in UTC+2“ neben der Zeile.
EC-02 Tagesgrenze 00:00 — Nachtschicht 22:00–06:00 Buchung wird an beide Tage in der Tagesansicht angezeigt (Verweis-Text „beginnt Vortag“ bzw. „endet Folgetag“). Im Monats-Aggregat wird die gesamte Schicht dem Datum des Stempel-In zugerechnet (ArbZG §3 Arbeitstag-Definition). Property-based Test C-05.
EC-03 Multi-Tenant-Bleed-Versuch — User gehört zu Tenant A, hat zufällig einen Link auf report_documents.id aus Tenant B geraten RLS-Policy rd_tenant_isolation liefert 0 rows. HTTP-Response: 404 Not Found (nicht 403 — Existenz nicht offenbaren, vgl. TEMPLATE.md §12 Grenzfall 3). Kein Audit-Log in Tenant B.
EC-04 Sync-Konflikt Filter-State — User öffnet Report auf Laptop + Handy, speichert auf beiden unterschiedliche Filter Optimistic-Locking via ETag: zweiter Request erhält 412 Precondition Failed; UI zeigt Dialog „Filter wurde auf anderem Gerät geändert — überschreiben oder neu laden?“.
EC-05 Großmenge — Bauleitung fordert CSV mit 250.000 Zeilen (3 Jahre, 80 MA) Synchroner Response wäre Timeout-Risiko. Endpunkt antwortet 202 Accepted + Job-ID; BullMQ-Worker streamt Postgres-Cursor direkt in S3 (kein Memory-Peak), report.export.ready-Webhook + Email-Benachrichtigung mit Download-Link. p95-Ziel: <60 s für 250k Zeilen.
EC-06 Abgelaufener Referenzkunde-Zugriff — Ex-Mitarbeiter versucht nach Austritt einen Monats-PDF-Link zu öffnen Presigned URL läuft nach 10 min ab; Benutzer-Account ist deaktiviert → RLS users.active = true-Check greift. Response 404, Audit-Log report.access.denied im Tenant.
EC-07 Plan-Limit erreicht — Starter-Plan (10 MA), Admin fordert XLSX-Pivot-Export, aber Feature ist Business+ Hono-Route-Gate module.reporting.xlsx_pivot prüft Plan-Limit, antwortet 402 Payment Required mit RFC 7807 Problem Details, Link auf Upgrade-Seite. UI zeigt Upsell-Banner statt Fehler.
EC-08 Rollen-Demotion mid-action — Manager wird während laufendem Bulk-Export zu Mitarbeiter demoted Batch-Job verifiziert vor jeder Mail-Zustellung die Scope-Berechtigung erneut (Postgres SELECT ... FOR SHARE auf users.role). Nach Demotion: verbleibende Mails werden nicht versendet, report.bulk.aborted mit Grund „scope_revoked“.
EC-09 Druckansicht bei dunklem System-Theme Print-Stylesheet erzwingt color-scheme: light + schwarze Schrift auf Weiß, unabhängig vom User-Theme. Golden-Test C-06.
EC-10 ReDoS-Versuch im Filter-Textfeld (Freitext-Suche über Projekt-Name) Valibot validiert Input auf maxLength: 128, verwirft Regex-Meta-Chars oder escapt sie; Server-Query nutzt Postgres plainto_tsquery, keine raw-Input-Regexe.

Funktionalität: Reporting & Auswertung (Werkszeit Kern §3.7)
Hintergrund:
Angenommen ein Tenant "musterbetrieb-maler" mit aktivem Modul-Gate "module.kern.reporting"
Und ein Mitarbeiter "Hannes Krüger" mit Rolle "Mitarbeiter" und Scope "own"
Und ein Manager "Sabine Maier" mit Rolle "Manager" und Scope "team"
Und Seed-Daten aus dem Zeitraum 2026-04-01 bis 2026-04-30
Szenario: Happy Path — Monats-PDF-Stundennachweis auf Mobile
Wenn Hannes die App öffnet
Und im Tab "Mehr" auf "Mein Stundennachweis" tippt
Und den Monat "April 2026" auswählt
Und auf "PDF" tippt
Dann wird eine PDF-Datei "stundennachweis-krueger-2026-04_rpt_<uuid>.pdf" erzeugt
Und die PDF enthält einen QR-Code mit URL "https://app.werkszeit.de/verify/rpt_<uuid>"
Und ein Audit-Log-Eintrag "report.export.downloaded" wird mit Hannes' user_id erzeugt
Und das Dokument wird in S3 mit Object-Lock-Retention 10 Jahre abgelegt
Szenario: Grenzfall — Drill-Down mit Bookmark-URL
Angenommen Sabine hat den URL "/reports/monthly/2026-04/employees/hannes-krueger/days/2026-04-19" gebookmarkt
Wenn Sabine den Bookmark am nächsten Tag öffnet
Dann wird genau die Tagesansicht von Hannes am 19.04.2026 geladen
Und der Filter-State (falls in report_filter_states persistiert) wird angewendet
Und die KPI-Karten zeigen den Tag: Brutto 9:40, Netto 8:55, 1 ArbZG-Warnung
Szenario: Grenzfall — CSV-Export ist Excel-DE-kompatibel
Wenn Frau Müller auf "CSV-Export" klickt
Dann startet der Download einer Datei "export-2026-04_rpt_<uuid>.csv"
Und die Datei beginnt mit den Bytes "0xEF 0xBB 0xBF" (UTF-8 BOM)
Und die erste Datenzeile ist "sep=;"
Und Datumsfelder haben das Format "TT.MM.JJJJ"
Und Dezimalzahlen nutzen Komma als Trenner
Und Excel 2021 DE öffnet die Datei ohne Import-Assistent
Und LibreOffice Calc Headless öffnet die Datei ohne Warnung
Szenario: Grenzfall — BetrVG-gesperrtes Widget
Angenommen das Widget "Sollstunden-Erfüllung pro MA" ist im Tenant nicht BR-freigegeben
Wenn der Manager Sabine das Dashboard öffnet
Dann ist das Widget nicht sichtbar
Und im Admin-Panel erscheint ein Banner "BetrVG §87(1)6 — Zustimmung erforderlich"
Und ein Versuch, das Widget über die API direkt abzufragen, antwortet 403 mit Code "br_approval_missing"
Szenario: Grenzfall — Multi-Tenant-Isolation (Negativ-Test)
Angenommen ein zweiter Tenant "shk-gebruder-schmidt"
Und eine report_document-ID "rpt_01J8K…" gehört zu shk-gebruder-schmidt
Wenn Sabine (Tenant musterbetrieb-maler) GET /v1/kern/reports/exports/rpt_01J8K… aufruft
Dann antwortet die API mit 404
Und im Audit-Log von shk-gebruder-schmidt wird KEIN Eintrag erzeugt
Und im Audit-Log von musterbetrieb-maler wird "report.access.denied" mit Grund "not_found" erzeugt
Szenario: Grenzfall — Screenreader-Navigation über Tabelle
Angenommen Sabine nutzt NVDA mit Firefox auf Windows
Wenn Sabine Tab drückt, um in die Datentabelle zu springen
Dann liest NVDA "Tabelle Mitarbeiter-Monatsreport, 25 Zeilen, 8 Spalten"
Und beim Pfeiltasten-Navigieren liest NVDA für jede Zelle "Mitarbeiter Hannes Krüger, Abteilung Nord, Ist 167 Stunden 12 Minuten, Saldo minus 48 Minuten"
Und beim Drücken von Enter auf einer Zeile wird auf die Drill-Down-Seite navigiert
Und das axe-core-Playwright-Script meldet 0 Critical- und 0 Serious-Findings

  • U-01 — CSV-BOM + sep=; in apps/api/src/export/csv.ts (Property-based: beliebige Spaltennamen, Bytes 0..2 müssen 0xEF 0xBB 0xBF sein).
  • U-02 — Aggregations-Formel tagesnetto = brutto − Σ pausen (Dart + TS identische Fixtures).
  • U-03 — Hash-Chain-Serializer canonicalize(reportDocument) — deterministisch, Reihenfolge-invariant bei JSON, Unicode-NFC-normalisiert.
  • U-04 — Kontrast-Checker in Design-Token-Test (packages/design-tokens/test/contrast.test.ts validiert alle in werkszeit-design-system.css dokumentierten Farbpaare ≥4.5:1).
  • U-05 — ArbZG-Aggregat: Property-based Test für §3 (10 h), §5 (11 h Ruhezeit), §7 (Ausnahme-Markierung). Gleiche Fixtures in TypeScript (Server-Compute) und Dart (Client-Preview).
  • W-01 — Stundennachweis-Mobile-Screen zeigt bei connectivity == none && cacheAge > 5min Banner „Daten offline gecached“.
  • W-02 — Golden-Test Monatskarte de-DE Light + Dark Theme + High-Contrast.
  • W-03 — Semantics-Harness: VoiceOver-Label für KPI-Karten enthält Einheit („Stunden“, „Minuten“) ausgeschrieben.

13.3 Integrations-Tests (Backend, Drizzle In-Memory)

Abschnitt betitelt „13.3 Integrations-Tests (Backend, Drizzle In-Memory)“
  • I-01Multi-Tenant-Isolation (pflicht): zwei Tenants, Cross-Read auf report_documents liefert 404; RLS-Policy-Test.
  • I-02 — Scope own sieht nur eigene Exports; team sieht Exports der Team-Mitglieder; all sieht alle.
  • I-03 — Outbox-Idempotenz: zweimal POST /exports mit gleichem Idempotency-Key → 1 Datenbank-Eintrag, zweite Response 409.
  • I-04 — ETag auf filter_states — konkurrierender Write → 412.
  • I-05 — S3 Object Lock Retention ist gesetzt (mock S3 mit x-amz-object-lock-retain-until-date).

13.4 E2E-Tests (Patrol iOS/Android, Playwright Web)

Abschnitt betitelt „13.4 E2E-Tests (Patrol iOS/Android, Playwright Web)“
  • E-01 — Happy Path (Gherkin §12) auf iOS-Simulator, Android-Emulator, Chromium, Firefox, WebKit.
  • E-02 — Offline-PDF-Anzeige: Patrol simuliert network off, öffnet Stundennachweis (aus Cache).
  • E-03Visuelle Regression: Golden-Image des Monats-Reports + Drill-Down auf jeder Plattform/Theme-Kombination. Abweichung >0.1 % blockt Merge.
  • E-04Accessibility: axe-core im Playwright-Report-Test, 0 Critical/Serious; Flutter-Semantics-Harness grün.
  • E-05Performance: Monats-Report 80 MA × 30 Tage, p95 <500 ms (lokal), <800 ms (Staging-RDS). Test markiert @perf läuft nightly, nicht PR-blocking.
  • E-06Bulk-Export 250k Zeilen: Job-Lifecycle, Download via Presigned URL, SHA-256 stimmt.
  • E-07Keyboard-only: Playwright-Test navigiert Report + Drill-Down + Export ausschließlich mit Tab/Enter/Arrow-Keys, alle interaktiven Elemente erreichbar.
  • C-01Hash-Chain-Integrität: Report-Dokument generieren, manuelle DB-Manipulation eines einzelnen Bytes → Validator schlägt Alarm.
  • C-02CSV-Validator: DATEV-Testimport der exportierten CSV (Fixture aus DATEV-Partner-Toolkit) — keine Fehler, keine Warnungen.
  • C-03XLSX-Validator: libxlsxwriter-basierter Reader öffnet die Datei, findet genau eine PivotTable, Pivot-Feld-Liste stimmt mit Spezifikation überein.
  • C-04PDF-Signatur-Check: QR-Code-Inhalt stimmt mit rpt_id; /verify/{id}-Endpunkt gibt grün zurück; nach Hash-Manipulation rot.
  • C-05ArbZG Property-based: §3/§4/§5/§7 Grenzfälle (9:59, 10:00, 10:01 h; 10:59, 11:00, 11:01 h Ruhezeit; Nachtschicht über 00:00 Uhr).
  • C-06BFSG-Snapshot: Pre-Launch-Audit-Skript führt eine Smoke-Reihe über Report + Dashboard, vergleicht axe-core-Output gegen Referenz-Manifest; neue Findings = Build fail.
  • Lokaler 20× Re-Run der neuen E2E-Tests grün (DOD §2.2). Report-Tests sind tendenziell flaky wegen Chart-Render-Timing → Playwright wartet auf data-testid="chart-rendered" statt networkidle.

Nicht-Ziel Begründung
Free-Form-Report-Builder mit beliebiger Pivot-/Filter-Kombination Scope-Explosions-Risiko. Wir liefern 5 feste Report-Typen + 9 Dashboard-Widgets. „Eigener Report aus beliebigen Feldern“ ist ein V3-Thema, wenn BI-Integratoren anfragen. FUNKTIONSUMFANG §10 „Vollständige CRM-Pipeline“ analog ausgeschlossen.
Eigene Formeln in Widgets (wie Excel-Formeln) Sicherheitsrisiko (Code-Execution) und UX-Komplexität. Stattdessen: XLSX-Export öffnet die Daten in Excel, dort macht die Buchhaltung ihre Formeln.
Echtzeit-Streaming-Dashboard (<1 s Latenz) Stempelungen kommen per SSE, Dashboard-Widgets aktualisieren sich alle 60 s — mehr braucht kein Handwerks-Monitoring. Live-Stream-Dashboards sind Consumer-Feature.
KI-generierte Report-Narrative („Diese Woche haben eure Leute 4 % mehr gearbeitet als letzte Woche, weil…“) Halluzinations-Risiko bei Lohn-relevanten Aussagen. Opt-in-KI-Paket (FUNKTIONSUMFANG §7) ist für OCR und Nachtrag-Textvorschläge — nicht für Reporting.
Eigene Marketing-Subdomain reports.werkszeit.de Wir haben app.werkszeit.de/reports. Eine weitere Subdomain würde Auth-/Session-Komplexität erzeugen.
Jasper/BIRT-artige druckbare Layouts mit Designer Scope-Creep. Typst-Templates in Git sind versionierbar, nachvollziehbar, Tenant-brandbar über 3 Token (Logo, Primärfarbe, Firmenname).
Excel-AddIn mit Live-Daten Liegt bei Drittanbietern, die unsere öffentliche API nutzen (§3.12).
Reports auf Desktop-App (V1.5) Web-App im Browser reicht. Desktop-App ist bewusst auf Offline-Feld-Arbeit fokussiert, nicht auf Büro-Reporting.

Risiko / Annahme Impact Wahrs. Gegenmaßnahme
BFSG-Audit findet Critical-Finding (z. B. Flutter-Web-Charts nicht screenreader-lesbar) hoch mittel fl_chart wird durch syncfusion_flutter_charts ersetzt oder Chart-Daten als HTML-Fallback-Tabelle (aria-hidden=false) zusätzlich gerendert. Entscheidungspunkt im Sprint 4 nach erstem Audit-Vorbericht.
DATEV-CSV-Format hat Tenant-spezifische Abweichungen (Lohnarten-Mapping, Spalten-Reihenfolge) mittel hoch Export-Profil-Editor (F-A-01) erlaubt Tenant-spezifische Konfiguration; Default-Profil ist DATEV LODAS Standard 2026.
Performance bei 80 MA × 90 Tage auf RDS t4g.medium mittel mittel Materialized View + HNSW-Index (nicht pgvector, sondern B-Tree). Last-Test Phase 2 mit Seed-Tenant B. Upscale auf t4g.large ist Plan B (Kosten +30 €/Monat).
BetrVG-Freigabe-Workflow zu restriktiv — Kunde ohne BR will trotzdem Widgets nutzen niedrig hoch Bei Tenants ohne Betriebsrat (Feldwert has_works_council = false) wird der Workflow durch einen einfacheren Disclaimer ersetzt: „Als Arbeitgeber bestätige ich, dass keine Mitbestimmung erforderlich ist.“ Audit-Log dokumentiert den Self-Approval.
Hash-Chain-Break in Produktion (z. B. durch Timezone-Bug bei created_at) hoch niedrig Canonicalisation-Funktion hat Unit-Test U-03 + Property-based Fuzz. Hotfix-Pfad: dedizierte Detection-Suite läuft alle 6 h, Alarmierung via Sentry + PagerDuty.
XLSX-Pivot in alten Excel-Versionen (2013) nicht lesbar niedrig niedrig XLSX-Format Office-2013-kompatibel erzeugen (xlsx-lib-Option compatibility: 'excel2013'). Dokumentierte Einschränkung: Excel <2010 nicht unterstützt.
Druckansicht bei Firmen-Netzwerk-Druckern (ältere GDI-Drucker mit Encoding-Problemen) niedrig niedrig PDF-Download bevorzugen, Print-Stylesheet ist Zweitweg. Handwerks-Kunden drucken selten direkt aus dem Browser.

  • Vorbedingung: feinkonzepte/kern/01-zeiterfassung (Datenbasis time_entries) und feinkonzepte/kern/11-audit-log (Audit-Infrastruktur) müssen umgesetzt sein.
  • Schnittstelle zu: Auth (Scope-Token-Header), DATEV-Export (datev.csv & datev.extf), S3 Object Lock, BullMQ, Typst-Service.
  • Wird konsumiert von:
    • feinkonzepte/kern/12-api-entwickler-portal — Report-Endpunkte sind Teil der öffentlichen API, Report-Webhooks (report.export.ready) sind im AsyncAPI-Katalog.
    • feinkonzepte/kern/10-datev-integration — Monatsabschluss-Workflow löst automatisch Monats-Report-Generierung aus.
    • feinkonzepte/handwerk/4.5-bau-abrechnung (Zukunft V1) — Rechnung referenziert den Ist-Stunden-Stand aus dem Monats-Report.

Cross-Link zur Gruppe: Siehe 12-api-entwickler-portal.md §5 — Report-Export ist einer der primären Use-Cases für den OAuth-Client der Steuerberater-Tools (DATEV-Connector) und prägt das SLA-Versprechen für Monats-Abschluss-APIs.


Status: TBD — zu klären mit Sales (Q2 2026).

DOR §1.1.1 verlangt einen zahlenden Design-Partner.

Kandidaten-Profile:

  • Malermeister mit 20–35 MA in Bayern/BaWü — pain: Monatsabschluss 2 Tage Handarbeit, braucht PDF-Stundennachweis für Mitarbeiter + CSV für Steuerberater.
  • SHK-Betrieb mit 60–100 MA in NRW — pain: Bauleitung verliert Überblick über Ist-vs-Budget auf 20 parallelen Baustellen, braucht Dashboard-Widgets.
  • Gebäudeservice mit aktivem Betriebsrat (~80 MA) — pain: BR verlangt monatliche aggregierte Auswertung, heute Excel-Bastelei. Für den BR-Sicht-Validierungs-Use-Case.

Validierungs-Fragen für das Erst-Gespräch:

  1. Wie erstellen Sie heute den monatlichen Stundennachweis für jeden Mitarbeiter? Wie lange dauert es?
  2. Welche Excel-Pivot-Tabelle nutzt Ihr Steuerberater? Können Sie uns eine Beispieldatei geben?
  3. Wenn Sie Ihren Monatsbericht „Ist-vs-Soll pro Abteilung“ machen — welche Spalten brauchen Sie?
  4. Haben Sie einen Betriebsrat? Welche aggregierten Reports muss er monatlich erhalten?
  5. Welche Browser nutzen Ihre Admins? Nutzt jemand Screenreader (Brille/Augenbehinderung)?
  6. Würden Sie das Feature im Beta-Status für 60 Tage kostenlos testen, gegen 3-monatige Preis-Lock-in-Garantie?

Build-Sequenz (innerhalb des Features):

  1. Datenmodell + RLS + Multi-Tenant-Test. report_filter_states, report_documents, dashboard_layouts mit Drizzle-Migration und RLS-Policy. Zwei-Tenant-Integration-Test (I-01) muss grün sein, bevor eine API-Route geschrieben wird.
  2. Backend-API + OpenAPI-Spec. GET /reports/monthly + Drill-Down-Endpunkte. Spec wird committed, Dart-Client generiert, Build grün.
  3. CSV-Export (MVP) + Hash-Chain + S3 Object Lock. Typst kommt erst in Schritt 4, CSV ist der einfachste Schnitt und deckt den DATEV-Fall.
  4. Typst-PDF-Stundennachweis mit Logo, QR-Code, Verifikationsseite.
  5. Mobile-MVP (eigene Monats-Sicht + Manager-Team-Status) mit Drift-Cache + Offline-Banner.
  6. Web-Drill-Down + Filter-Persistenz + Bulk-Bar. An dieser Stelle BFSG-Audit Zwischenstand (in-house mit axe-core).
  7. Dashboard-Widgets + BR-Freigabe-Workflow.
  8. XLSX mit Pivot-Vorlage (erfordert präzise Excel-XML-Generierung, ist das fehleranfälligste Stück).
  9. E2E-Suite vervollständigen, inkl. Keyboard-only- und Screenreader-Smoke.
  10. Externer BFSG-Audit (≈5 k€). Erst nach grünem Audit: GA-Rollout.
  11. Doku-PR (docs/features/kern/reporting.md) mit auto-generierten Screenshots aus E2E.

Risiko-Reihenfolge (wenn Zeitnot):

  • Nicht verhandelbar: Multi-Tenant-Isolation (I-01), Hash-Chain (C-01), BFSG Critical-Findings = 0, ArbZG-Property-Tests (C-05).
  • Erste Streich-Kandidaten: XLSX-Pivot-Vorlage (Business-Plan-Feature, MVP akzeptiert CSV-only), Dashboard-Konfigurator (V1.5), Print-Stylesheet (nice-to-have, PDF reicht).

Stop-the-Bus-Triggers:

  • Multi-Tenant-Bleed in einem Report-Test.
  • Hash-Chain-Bruch in der Compliance-Suite.
  • BFSG-Audit meldet eine Critical-Finding (z. B. Chart nicht bedienbar per Tastatur) — Launch bis zur Behebung gestoppt.
  • DSGVO-Löschantrag eines Mitarbeiters kollidiert in Produktion mit GoBD-Aufbewahrung (Maskierungs-Logik ist der dokumentierte Ausweg; wenn die Logik versagt: Stop).
  • p95 Monats-Report >2 s in Staging mit Seed-Tenant B (80 MA).

Was uns 2027 dankbar macht:

  • Report-Dokumente sind seit Tag 1 in Hash-Chain + S3 Object Lock — wenn das Finanzamt 2029 nach Beleg-Integrität fragt, haben wir eine Antwort, nicht ein Projekt.
  • Die URL-als-State-Architektur macht die UI wartbar (kein Modal-Tiefenstapel) und macht die BFSG-Tastaturnavigation natürlich — Back-Button ist Teil der A11y-Strategie, nicht ein Bug-Report.
  • Export-Profile als benannte Konfigurationen sind die Vorlage für die DATEV-Partner-Zertifizierung in Jahr 2 — wir liefern die Datei ohne Nachprogrammierung, sondern durch Auswahl eines Profils.
  • Die 9 Widgets sind Rollen-gebunden + BR-gated von Anfang an. Das erspart in Jahr 3 den schmerzhaften Rückbau, den die Alt-App nach dem ersten BR-Audit hatte.

Feinkonzept-Autor: Senior-Consulting-Sparte Reporting & Barrierefreiheit · Werkszeit v0.1 · 19.04.2026

Für Entwickler — API-Endpoints7
MethodePfadAuthZweck
POST/v1/kern/reports/exportsbearerAuthExport-Job anlegen (PDF oder CSV)
GET/v1/kern/reports/exports/{id}bearerAuthExport-Status abfragen
GET/v1/kern/reports/exports/{id}/downloadbearerAuthPresigned S3-URL für fertigen Export
GET/v1/kern/reports/filter-states/{kind}bearerAuthFilter-State laden
PUT/v1/kern/reports/filter-states/{kind}bearerAuthFilter-State speichern
GET/v1/kern/reports/monthlybearerAuthMonats-Aggregat laden
GET/v1/kern/reports/monthly/{month}/employees/{employeeId}bearerAuthDrill-Down auf Mitarbeiter-Tagesebene