Dienstplanung — Drag&Drop-Schichteditor, Tausch-Workflow, Qualifikations-Gate
Feinkonzept · §3.8 Dienstplanung — Drag&Drop-Editor, Tausch-Workflow, Qualifikations-Gate
Abschnitt betitelt „Feinkonzept · §3.8 Dienstplanung — Drag&Drop-Editor, Tausch-Workflow, Qualifikations-Gate“Einordnung. Dieses Feinkonzept setzt FUNKTIONSUMFANG §3.8 um. Die Dienstplanung ist die Brücke zwischen Stammdaten (Mitarbeiter, Qualifikationen, Baustellen), den harten Leitplanken des ArbZG (Ruhezeit, Höchstarbeitszeit, §3, §4, §5) und der operativen Realität des Handwerks: Bauleiter Thomas Schmidt verteilt am Donnerstag-Nachmittag 18 Monteure auf 4 Baustellen für die kommende Woche, tauscht Schichten im Feldeinsatz per Handy und muss sicherstellen, dass niemand mit abgelaufenem DGUV-G25 auf ein Gerüst geht. Wir folgen LESSONS-LEARNED §5 („Business-Substanz fehlte im Alt-Code, insbesondere bei Plan/Soll-Ist“) und bauen die Dienstplanung bewusst als Web-primären Drag&Drop-Editor mit Mobile-Read-Only + Tauschanträgen, nicht als überladenes Mobile-Planungstool.
1. Header & Metadaten
Abschnitt betitelt „1. Header & Metadaten“feature_id: kern/08-dienstplanungtitle: Dienstplanung — Drag&Drop-Schichteditor, Tausch-Workflow, Qualifikations-Gatefunktionsumfang_ref: §3.8roadmap_horizont: V1 # MVP liefert nur Read-Only-Ansicht; Editor ab V1plattformen: mobile: nur-Ansicht # Eigener Plan, Team-Woche, Tausch-Anträge web: vollständig # Drag&Drop-Editor, Bedarfsplanung, BR-Freigabe desktop: ab V1.5 bei Bedarfowner_rolle: Manager # Bauleiter / Disponent plant; Admin konfiguriert Schichtmodellemodul_gate_flag: module.kern.dienstplanungcompliance_flags: gobd: false # Planung ist Vorausschau, nicht Buchführung — erst die Ist-Zeit wird GoBD arbzg: true # Echtzeit-Prüfung beim Setzen jeder Schicht vob: false dsgvo: true # Personenbezogener Plan, Vertretungs-Daten bfsg: true # Eigener Plan + Tauschanträge sind ESS-Flow betrvg: true # §87(1)2 — Schichtplan ist mitbestimmungspflichtig (nicht §87(1)6) stvg: false weitere: - "§87 Abs. 1 Nr. 2 BetrVG (Schichtpläne, Beginn/Ende der Arbeitszeit)" - "§106 GewO (Direktionsrecht-Grenzen)" - "§4 TzBfG (Teilzeit-Gleichbehandlung)" - "TV Maler/Lackierer §3 (Schicht-Ankündigungsfrist 4 Tage)" - "DGUV V1 §7 (Unterweisung vor Zuweisung)"referenzkunde: status: TBD name: "zu klären — Zielprofil: SHK-Betrieb 40–80 MA mit Bereitschaftsdienst-Rotation" quelle: "Interview-Leitfaden 'Schichten planen' + ZDH-Sektor-Workshop Bau"estimate_eng_tage: 48 # Drag&Drop-Editor (18), Tausch-Workflow (8), Quali-Gate (6), # Bedarfsplanung (8), BR-Freigabe (4), Mobile-Read-Only (4)abhängigkeiten: - kern/01-zeiterfassung # Ist-Zeit wird gegen Soll-Schicht abgeglichen - kern/02-arbeitszeit-compliance # ArbZG-Policy-Engine - kern/03-abwesenheiten # Urlaub/Krank sperrt Einplanung - handwerk/11-qualifikations-kontrolle # §4.11 — Zertifikats-Ablauf blockiert - kern/09-auth-self-service # Rolle Manager / Planer, Scope `team` / `all`2. Kontext & Problem
Abschnitt betitelt „2. Kontext & Problem“Marktrealität (DE-Handwerk). Im SHK-Betrieb mit 80 Monteuren ist die Wochenplanung ein Donnerstag-Ritual: Thomas Schmidt (Bauleiter) setzt sich mit einem A3-Excel-Ausdruck an den Küchentisch, telefoniert mit zwei Baustellenleitern, checkt bei Frau Müller (Buchhaltung), wer noch im Urlaub ist, und malt Kreuze in eine Tabelle. Am Freitag hängt der Plan als PDF in der Werkstatt, per WhatsApp-Gruppe geht er an die Monteure. Treffen Schicht-Tauschanfragen ein („kannst du Montag für mich übernehmen?“), wird das auf Zuruf geregelt — dokumentiert wird nichts. Im Maler-Betrieb mit 25 MA ist das Ganze eine Zeile weniger formal, dafür genauso fehleranfällig. Zwei rechtlich harte Risiken daraus: (a) Ein Monteur mit abgelaufener DGUV-G25-Eignungsuntersuchung wird auf ein Gerüst >3 m geschickt → Arbeitgeber haftet persönlich (§21 ArbSchG + §618 BGB). (b) Der Betriebsrat wurde bei der Schichtplan-Änderung nicht angehört (§87 Abs. 1 Nr. 2 BetrVG) → BR kann die Schicht kippen lassen, Arbeitgeber zahlt Ausfall.
Rechtlicher Rahmen. §87 Abs. 1 Nr. 2 BetrVG macht den Schichtplan selbst mitbestimmungspflichtig — das ist ein anderer Tatbestand als §87(1)6 (Leistungs- und Verhaltenskontrolle). Der BR hat ein Initiativrecht und ein Veto-Recht bei Abweichungen vom vereinbarten Schichtmodell. Tarifverträge (z.B. TV Maler/Lackierer §3) verlangen 4 Tage Ankündigungsfrist, kurzfristige Änderungen nur mit Zustimmung des Mitarbeiters. ArbZG §5 (11 h Ruhezeit) muss vor dem Setzen einer Schicht geprüft werden, nicht erst beim Stempeln — sonst entsteht ein rechtswidriger Plan. §106 GewO begrenzt das Direktionsrecht: die Zuweisung muss „billigem Ermessen“ entsprechen, Familienlage berücksichtigen, keine willkürlichen Nachtschichten.
Schmerzpunkt der Alt-App. LESSONS-LEARNED §5 hebt hervor: Die Alt-App hatte einen TourenplanungScreen, aber keinen echten Schichtplan-Editor. Es gab keine Soll-Ist-Überdeckung, keine Qualifikations-Warnung, keine Tausch-Workflows. Schichtpläne lebten in Excel neben der App; die App kannte nur „Wer ist heute da“, nicht „wer sollte heute wo sein“. Folge: keine Planungs-Transparenz, keine ArbZG-Prävention, keine BR-Beteiligungs-Historie. Wir machen das anders: Ein Plan, der die Quelle der Soll-Stunden ist — die Ist-Zeit des Zeiterfassungs-Feinkonzepts vergleicht sich an genau diesem Plan, Über-/Unterdeckung ist sichtbar, jede Änderung hinterlässt Audit-Spuren, die den BR-Pfad einhalten.
Erwarteter Outcome (SMART).
- Spezifisch: Web-primärer Drag&Drop-Editor (Kalender-Woche/Monat), Mobile-Read-Only mit Tausch-Antragsflow, automatische ArbZG-Echtzeit-Validierung (§3, §5), harte Qualifikations-Sperre bei abgelaufenen Zertifikaten, optionaler BR-Freigabe-Workflow pro Tenant.
- Messbar: Wochenplanung-Zeitaufwand von 2,5 h (Excel + WhatsApp) auf < 25 min; Zahl der ArbZG-Ruhezeit-Verletzungen pro Quartal ≥ 80 % runter; Quote eingeplanter MA mit abgelaufenem Zertifikat = 0 % (harte Blockade); BR-Freigabe-Nachweise 100 % lückenlos bei aktiviertem Flag.
- Achievable: Frontend in Flutter Web mit
table_calendar+ eigenem Drag&Drop-Layer (HTML5 DnD API viadart:html); Backend in Hono + Postgres, Echtzeit-Validierung via Reuse der ArbZG-Engine auskern/02. - Relevant: Ohne Dienstplan keine Soll-Stunden, ohne Soll-Stunden kein Gleitzeit-Saldo, ohne Saldo keine Lohnabrechnung. Dienstplanung ist die operative Drehscheibe.
- Time-bound: V1-Release (Monat 11), Beta ab Sprint T+8 (ca. 2026-09-21).
3. Personas & Rollen
Abschnitt betitelt „3. Personas & Rollen“| Rolle | Aktion | Scope | Plattform |
|---|---|---|---|
| Mitarbeiter | Eigenen Plan sehen, Tausch beantragen, Tausch akzeptieren | own |
📱🌐 |
| Manager | Wochenplan erstellen, Bedarf definieren, Tausch genehmigen | team |
🌐 (Editor) · 📱 (Read) |
| Bauleiter | Schicht setzen/ändern für eigenes Team, Über-/Unterdeckung sehen | team |
🌐🔵📱 |
| Admin | Schichtmodelle anlegen, BR-Freigabe-Flag setzen | all |
🌐 |
| Betriebsrat | Schichtplan-Freigabe (wenn Flag aktiv), BR-Audit-Export | all |
🌐 |
| HR | Urlaubs-/Kranktage im Plan sichtbar, keine Schichtzuweisung | all |
🌐 |
Persona-Skizzen.
- Hannes Krüger (38, Maler-Geselle, musterbetrieb-maler) — sieht seinen Plan morgens im Mobile („Mi: Lehrer Allee 7, 07:00–16:00“). Er braucht Freitag-Nachmittag frei → beantragt Tausch mit Mehmet.
- Sabine Maier (43, Manager, musterbetrieb-maler) — plant freitags für die nächste Woche am Web-Editor. Zieht Namen auf Baustellen-Zeilen, sieht gelbe Quali-Warnung bei einem Azubi, grünen Haken wenn Soll=Ist.
- Thomas Schmidt (51, Bauleiter, shk-gebruder-schmidt) — koordiniert 4 Baustellen parallel, nutzt den Editor primär am Tablet im Büro, akzeptiert Tauschanträge zwischendurch auf dem Handy.
- Frau Müller (56, Buchhaltung) — braucht den Plan nicht als Editor, aber als Soll-Stunden-Basis für die Lohnabrechnung via DATEV LODAS.
- Herr Gärtner (58, Betriebsrat SHK) — bekommt Donnerstag-Nachmittag die Woche zur Freigabe, klickt per Web „freigegeben“ oder reicht Widerspruch ein; sein Click-Track ist Pflicht-Nachweis.
4. User-Stories
Abschnitt betitelt „4. User-Stories“US-01 [V1] Als Manager Sabine möchte ich per Drag&Drop einen Wochenplan für 25 MA auf 6 Baustellen in < 25 min fertig bekommen, damit ich meinen Freitagnachmittag nicht verliere.
US-02 [V1] Als Bauleiter Thomas möchte ich beim Setzen einer Nachtschicht sofort eine Warnung sehen, wenn die 11 h Ruhezeit zur Vorschicht unterschritten wird, damit ich nicht erst am Tag selbst vom ArbZG-Alarm überrascht werde.
US-03 [V1] Als Monteur Hannes möchte ich per Mobile einen Tausch mit einem Kollegen beantragen, ohne meinen Chef anrufen zu müssen — der Chef bekommt eine Push, der Kollege auch.
US-04 [V1] Als Manager möchte ich verhindern, dass ein Monteur mit abgelaufenem DGUV-G25 auf einer Gerüst-Baustelle eingeplant wird — das System soll mich HART blockieren, nicht nur warnen.
US-05 [V1] Als Admin möchte ich im Tenant den Betriebsrat-Freigabe-Workflow aktivieren, damit der BR jede veröffentlichte Woche signieren muss, bevor sie an die Monteure pusht.
US-06 [V1] Als Manager möchte ich Soll-Besetzung pro Baustelle und Tageszeitfenster vorgeben („Lehrer Allee: Mo-Fr 07:00-16:00 je 4 Monteure"), damit ich eine Unter-/Überdeckungs-Warnung bekomme.
US-07 [V1] Als Mitarbeiter möchte ich meinen eigenen Plan für die kommende Woche einsehen, sowohl auf Mobile als auch Web, inklusive Einsicht, welcher Kollege für mich einspringen soll falls ich krank werde.
US-08 [V1.5] Als HR möchte ich sehen, welche Mitarbeiter wegen Urlaub/Krankheit in der kommenden Woche ausfallen, damit der Manager die Lücken sieht, ohne dass ich sie nacherklären muss.
US-09 [V2] Als Planer möchte ich eine Vorlage („Schicht-Woche Gerüstbau Team Nord") speichern und anwenden, um Wiederholungen nicht jedes Mal neu zu bauen.5. Funktionale Anforderungen
Abschnitt betitelt „5. Funktionale Anforderungen“5.1 Mobile App (📱) — iOS + Android, Read-Only + Tausch
Abschnitt betitelt „5.1 Mobile App (📱) — iOS + Android, Read-Only + Tausch“- F-M-01 — Eigener Wochenplan als Liste (Mo–So) und optional Kalender-Monat. Jede Schicht zeigt: Datum, Beginn, Ende, Baustelle, Zeitart (Arbeit/Rufbereitschaft), Vertretung (falls krank), Quali-Anforderung.
- F-M-02 — Team-Woche Read-Only (nur für Rolle Manager/Bauleiter auf Mobile) — kompakte Übersicht, wer wo ist; keine Edit-Gesten.
- F-M-03 — Tausch-Antrag starten: „Schicht tauschen mit …“ öffnet Kontakt-Picker (nur Kollegen mit gleicher Quali, nicht im Urlaub, ArbZG-kompatibel). Push an Kollegen.
- F-M-04 — Tausch-Antrag akzeptieren/ablehnen: Kollege sieht Push, öffnet App, tippt „Annehmen“/„Ablehnen“. Bei Annahme: Manager erhält Freigabe-Push.
- F-M-05 — Kalender-Export iCal: Mitarbeiter kann eigenen Plan als
.icsin persönlichen Kalender abonnieren (Tenant-gatewayed URL mit HMAC-Token, read-only, ohne Drittperson-Daten). - F-M-06 — Konflikt-Warnung beim Tausch: System prüft in Echtzeit gegen ArbZG + Qualifikation + Urlaubs-Sperre. Bei Konflikt: Tausch-Antrag nicht absendbar, mit Begründung.
- F-M-07 — Mobile Edit ist aus. Bewusst. Wer am Handy draggt, macht Fehler — Dienstplanung findet am Web/Tablet-Browser statt.
5.2 Web-App (🌐) — Drag&Drop-Editor
Abschnitt betitelt „5.2 Web-App (🌐) — Drag&Drop-Editor“- F-W-01 — Kalender-Raster Woche: Spalten = Tage (Mo–So), Zeilen = Mitarbeiter (sortierbar nach Team/Kolonne/Qualifikation). Zellen zeigen aktive Schicht-Pills. Drag eines Mitarbeiters in eine Zelle erzeugt Schicht; Drag einer Schicht zwischen Zellen verschiebt.
- F-W-02 — Kalender-Raster Monat: Kompakter, Read-Mostly mit Click-to-Edit. Zeilen = MA, Spalten = Kalendertage. Farbkodierung (grün = Arbeit, blau = Rufbereitschaft, rot = Abwesenheit, grau = frei).
- F-W-03 — Multi-Select + Bulk-Assign: Rechteck-Auswahl über mehrere Zellen, Dropdown „alle diese Slots mit Baustelle X füllen“.
- F-W-04 — Vorlagen-Anwenden: „Woche wie KW 15“ kopiert die Schicht-Struktur, passt Datum an, prüft Urlaub/Krank.
- F-W-05 — Bedarfsplanung pro Baustelle: Soll-Besetzung (Anzahl MA pro Zeitfenster) wird als Hintergrund-Lane im Raster dargestellt; Unterdeckung erzeugt orange Pill „-2 MA“.
- F-W-06 — ArbZG-Echtzeit-Overlay: Jede neu gesetzte Schicht wird beim Drop gegen ArbZG geprüft (Ruhezeit, Höchstarbeitszeit, Wochenhöchst). Verstoß → Roter Border + Tooltip „§5 Ruhezeit 9 h statt 11 h, Quelle: vorige Schicht Do 22:00“.
- F-W-07 — Qualifikations-Gate: Drag eines MA auf eine Baustelle, deren Anforderung er nicht erfüllt → Drop wird verweigert, Toast „Azubi Jonas Brehm hat kein DGUV-G25 für Gerüst-Baustelle Lehrer Allee 7. Zertifikats-Übersicht öffnen?“.
- F-W-08 — Urlaubs-/Krank-Sperre: Tage mit Status „beantragter Urlaub“ oder „eAU“ sind im Raster grau-gestreift und nicht bedropbar.
- F-W-09 — Tausch-Workflow-Panel: Eingehende Tausch-Anträge mit Kollegen-Zustimmung werden dem Manager als Liste rechts angezeigt; „Freigeben“ committed den Tausch, schreibt Audit-Log.
- F-W-10 — BR-Freigabe-Workflow (wenn Flag aktiv): „Plan freigeben“ öffnet Modal → BR-Benutzer wird per E-Mail mit signiertem Deep-Link benachrichtigt. BR-User klickt „Freigeben“ oder „Widersprechen“. Ohne Freigabe: Plan bleibt „Entwurf“, MA sehen keine Push-Notification.
- F-W-11 — Versions-Historie: Jede Publikation einer Woche wird als Version festgehalten (Hash-ID). Diff-View zeigt, was sich zur Vor-Version geändert hat — Pflicht für BR-Akzeptanz.
- F-W-12 — Push-Publikation an MA: Beim „Plan freigeben“ bekommen alle betroffenen MA eine Push mit Link auf ihren Plan.
- F-W-13 — Unter-/Überdeckungs-Dashboard als Widget im Manager-Homescreen: „KW 17: 3 Baustellen mit Unterdeckung, 1 mit Überdeckung“ → Click öffnet Editor am jeweiligen Slot.
5.3 Cross-Plattform (🔄)
Abschnitt betitelt „5.3 Cross-Plattform (🔄)“- F-X-01 — Zeitzone ist Tenant-Zeitzone (
Europe/Berlin). Export via iCal nutzt DTSTART/DTEND mit TZID, nicht UTC, um in Outlook/Apple Calendar korrekt anzuzeigen. - F-X-02 — Schicht-Pill-Format:
HH:MM–HH:MM · Baustelle-Kurzname, bei Rufbereitschaft⚓ HH:MM–HH:MM, bei Nachtschicht mit Mond-Glyph. - F-X-03 — 4-Tage-Ankündigungsfrist-Check (TV-spezifisch, konfigurierbar): Plan-Änderung <4 Tage vor Schicht-Beginn → MA-Zustimmung Pflicht (siehe BR-Integration).
- F-X-04 — Soft-Conflict-Badge: Wenn ein MA an mehreren Baustellen gleichzeitig eingeplant wird (konkurrente Drop-Aktion / Fehler) → beide Schichten behalten, rotes Hard-Conflict-Badge, Manager muss auflösen.
5.4 Admin-Konfiguration
Abschnitt betitelt „5.4 Admin-Konfiguration“- F-A-01 — Schichtmodelle anlegen (Frühschicht 06:00–14:30, Spätschicht 14:30–22:00, Nachtschicht 22:00–06:00, Rufbereitschaft …). Pro Modell: Zeitart-Default, Pausen-Regel (§4 ArbZG), Zuschlag-Hinweis (§6 ArbZG Nachtzuschlag).
- F-A-02 — Bedarfsplanung-Stammdaten pro Baustelle / Kostenstelle: Anzahl MA × Schichtmodell × Wochentage. Zeitlich begrenzt (Gültig-von/bis).
- F-A-03 — Tenant-Einstellung BR-Freigabe: Flag
plan.betriebsrat_freigabe = true|false. Beitrue: Publikationsweg geht über BR. Flag-Änderung selbst ist BR-pflichtig (Meta-Audit). - F-A-04 — Ankündigungsfrist: 0/2/4/7 Tage, tenant-global.
- F-A-05 — Qualifikations-Anforderungen je Baustelle: Dropdown aus
qualification_types(DGUV G25, Asbest-Schein §10 TRGS 519, Höhenrettung, etc.). Wird beim Anlegen der Baustelle gesetzt, vererbt sich auf alle Slots.
5.5 Roadmap-Schichtung
Abschnitt betitelt „5.5 Roadmap-Schichtung“| Anforderung-ID | MVP | V1 | V1.5 | V2 |
|---|---|---|---|---|
| F-M-01 Eigener Plan | Read-Only ohne Editor | ✅ | ||
| F-M-03 Tausch-Antrag | ✅ | |||
| F-W-01 Drag&Drop | ✅ | |||
| F-W-05 Bedarfsplanung | ✅ | |||
| F-W-06 ArbZG-Echtzeit | ✅ | |||
| F-W-07 Quali-Gate | ✅ | |||
| F-W-10 BR-Freigabe | ✅ | |||
| F-M-05 iCal-Export | ✅ | |||
| F-W-04 Vorlagen | ✅ | |||
| F-W-03 Bulk-Assign | ✅ |
6. Mockups & Flows
Abschnitt betitelt „6. Mockups & Flows“HTML-Hero-Mockup: 08-dienstplanung.html — Mobile-Plan (Hannes, KW 17) + Web-Drag&Drop-Editor (Sabine, musterbetrieb-maler, KW 17, 20.–26.04.2026) mit Qualifikations-Warning.
6.1 Eigener Plan Mobile (Mitarbeiter)
Abschnitt betitelt „6.1 Eigener Plan Mobile (Mitarbeiter)“┌────────────────────────────────┐│ ← Mein Plan 🔄 📅 │├────────────────────────────────┤│ 🛡 DSGVO · BetrVG §87(1)2 │├────────────────────────────────┤│ KW 17 · 20.–26.04.2026 ││ [◀ KW 16] [KW 18 ▶] ││ ││ ┌─ Mo 20.04 ─────────────────┐ ││ │ 07:00–16:00 · Arbeit │ ││ │ 📍 Lehrer Allee 7, München │ ││ │ Kolonne: Hannes, Mehmet, A │ ││ │ [🔄 Tausch beantragen] │ ││ └────────────────────────────┘ ││ ┌─ Di 21.04 ─────────────────┐ ││ │ 07:00–16:00 · Arbeit │ ││ │ 📍 Lehrer Allee 7 │ ││ └────────────────────────────┘ ││ ┌─ Mi 22.04 ─────────────────┐ ││ │ 06:00–14:30 · Frühschicht │ ││ │ 📍 Schulstraße 12 │ ││ └────────────────────────────┘ ││ ┌─ Do 23.04 ─────────────────┐ ││ │ 🟡 Urlaub beantragt │ ││ │ Vertretung: Mehmet Y. │ ││ └────────────────────────────┘ ││ ┌─ Fr 24.04 ─────────────────┐ ││ │ FREI │ ││ └────────────────────────────┘ ││ ││ Woche Soll: 32:00 h ││ Urlaubsrest: 14 Tage │├────────────────────────────────┤│ [Zeit] [Plan] [Doku] [Mehr] │└────────────────────────────────┘6.2 Tausch-Antrag Mobile
Abschnitt betitelt „6.2 Tausch-Antrag Mobile“┌────────────────────────────────┐│ ✕ Schicht tauschen │├────────────────────────────────┤│ Meine Schicht ││ Fr 24.04 · 07:00–16:00 ││ 📍 Lehrer Allee 7 ││ ││ Tausch mit … 🔴 ││ ( ) Mehmet Yilmaz ││ ✅ ArbZG ok · ✅ Quali ok ││ (•) Andreas Weber ││ ✅ ArbZG ok · ✅ Quali ok ││ ( ) Frank Dreher ││ ⛔ im Urlaub 24.04. ││ ( ) Jonas Brehm (Azubi) ││ ⚠ DGUV-G25 fehlt — nur wenn││ Baustelle kein Gerüst ││ ││ Grund (optional, 200 Z.) ││ ┌──────────────────────────┐ ││ │ Zahnarzttermin 14:30. │ ││ └──────────────────────────┘ ││ ││ ℹ Andreas bekommt Push-Anfrage ││ Manager Sabine M. muss nach ││ Andreas' Annahme freigeben. ││ ││ [ Abbrechen ] [ ✉ Antrag send.]│└────────────────────────────────┘6.3 Web Drag&Drop-Editor Hauptansicht (Manager)
Abschnitt betitelt „6.3 Web Drag&Drop-Editor Hauptansicht (Manager)“┌──────────────────────────────────────────────────────────────────────────────┐│ Werkszeit · musterbetrieb-maler Sabine M. · Manager [Profil▾] │├────────────┬─────────────────────────────────────────────────────────────────┤│ ⏱ Zeit │ Dienstplan · KW 17 · 20.–26.04.2026 🛡 BetrVG §87(1)2 ││ 📅 Plan ← ├─────────────────────────────────────────────────────────────────┤│ 📋 Doku │ [◀] [Heute] [▶] Woche ▾ Team ▾ [Vorlage ▾] [⤓ PDF] [Publi-││ 🗓 Abwes. │ zieren →] ││ 🏗 Proj. ├─────────────────────────────────────────────────────────────────┤│ 👥 Kunden │ Baustellen-Lane (Soll) ↓ ││ ⤓ DATEV │ Lehrer Allee 7 ● 4 MA 07:00–16:00 Mo-Fr [Ist: 3] 🟠 -1 ││ 🛡 Audit │ Schulstr. 12 ● 2 MA 06:00–14:30 Mo-Mi [Ist: 2] 🟢 ││ ⚙ Einstell │ Mustergasse 3 ● 3 MA 08:00–17:00 Mo-Fr [Ist: 3] 🟢 ││ ──────────│─────────────────────────────────────────────────────────────────││ SM │ Mitarbeiter (15)│ Mo 20│ Di 21│ Mi 22│ Do 23│ Fr 24│ Sa 25│ So 26││ Manager │─────────────────┼──────┼──────┼──────┼──────┼──────┼──────┼──────││ │ Hannes K. │Lehrer│Lehrer│Schul-│🟡Url.│ FREI │ — │ — ││ │ │07-16 │07-16 │06-14 │ │ │ │ ││ │ Mehmet Y. │Lehrer│Lehrer│Lehrer│Lehrer│Lehrer│ — │ — ││ │ Andreas W. │Schul-│Schul-│Schul-│ FREI │Lehrer│ — │ — ││ │ Frank D. │🔴eAU │🔴eAU │🔴eAU │Lehrer│Lehrer│ — │ — ││ │ Jonas B. ⚠Azubi │Schul-│Schul-│⛔kein│⛔kein│⛔kein│ — │ — ││ │ │ │ │DGUV │DGUV │DGUV │ │ ││ │ Dimitri P. │Muster│Muster│Muster│Muster│Muster│ — │ — ││ │ Lukas H. │Muster│Muster│Muster│Muster│Muster│ — │ — ││ │ Tobias M. │Muster│ FREI │ FREI │Lehrer│Lehrer│ — │ — ││ │ │ │ │ │ │ │ │ ││ │ Legende: 🟢 ok 🟠 Unterdeckung 🔴 Abwesenheit ⚠ Quali-Hinweis ││ │ ⛔ hartes Gate 🟡 Urlaub beantragt │└────────────┴─────────────────────────────────────────────────────────────────┘6.4 Web Drop-Konflikt: ArbZG-Ruhezeit-Warnung
Abschnitt betitelt „6.4 Web Drop-Konflikt: ArbZG-Ruhezeit-Warnung“┌──────────────────────────────────────────────────────────────────┐│ ⚠ ArbZG §5 — Ruhezeit 9:30 h statt 11:00 h │├──────────────────────────────────────────────────────────────────┤│ Hannes Krüger kann nicht um 06:00 Do 23.04. beginnen. ││ Letzter Dienstschluss: Mi 22.04. 20:30 (Schulstr. 12 Spätschicht)││ Aktueller Drop würde 9:30 h Ruhe ergeben — gesetzlich 11 h. ││ ││ Optionen: ││ ( ) Andere Uhrzeit wählen (frühester Start: 07:30 Do) ││ ( ) Spätschicht Mi kürzen (Schulstr. neuer Schluss 19:00) ││ ( ) Drop verwerfen ││ ││ ℹ §7 ArbZG-Ausnahmen TV Maler: nicht anwendbar (keine Tarif- ││ Klausel hinterlegt). ││ ││ [ Drop verwerfen ] [ Trotzdem setzen mit Begründung ] │└──────────────────────────────────────────────────────────────────┘6.5 Qualifikations-Gate (Hard-Block)
Abschnitt betitelt „6.5 Qualifikations-Gate (Hard-Block)“┌──────────────────────────────────────────────────────────────────┐│ ⛔ Qualifikation fehlt — Drop nicht möglich │├──────────────────────────────────────────────────────────────────┤│ Jonas Brehm (Azubi, 17) kann nicht auf Lehrer Allee 7 arbeiten. ││ ││ Baustelle erfordert: DGUV-G25 (Höhentauglichkeit) ││ Jonas' Status: — nicht vorhanden — §18 JArbSchG-Block ││ ││ Wir lassen das nicht zu, weil: ││ • §21 StVG + §618 BGB — Arbeitgeber-Halterhaftung ││ • §18 JArbSchG — Jugendliche nicht an Gerüsten >2 m ││ • DGUV V1 §7 — Unterweisung vor Zuweisung ││ ││ [ Zertifikat-Matrix öffnen → §4.11 ] [ OK, Drop verwerfen ] │└──────────────────────────────────────────────────────────────────┘6.6 BR-Freigabe-Modal (Manager publiziert)
Abschnitt betitelt „6.6 BR-Freigabe-Modal (Manager publiziert)“┌──────────────────────────────────────────────────────────────────┐│ 🗳 Plan zur Betriebsrat-Freigabe senden │├──────────────────────────────────────────────────────────────────┤│ Woche: KW 17 · 20.–26.04.2026 ││ Geändert: 12 Schichten neu, 3 getauscht, 2 gestrichen ││ Diff zu KW 16: [ Versions-Vergleich anzeigen → ] ││ ││ BR-Freigabe-Empfänger: ││ ▸ Herr Gärtner (Betriebsrat-Vorsitz) ││ Letzter Zugriff: Do 16.04. 14:12 ││ ││ Frist (TV Maler §3 — 4 Tage vor Schicht-Beginn): ││ ▸ Entscheidung bis: Do 16.04. 24:00 — ✅ noch in Frist ││ ││ Nach BR-Freigabe: ││ • Plan wird als Version v17 festgeschrieben (Hash) ││ • 15 Mitarbeiter erhalten Push-Benachrichtigung ││ • Audit-Log-Eintrag `plan.published` mit BR-Signatur ││ ││ [ Abbrechen ] [ ✉ An Betriebsrat senden ] │└──────────────────────────────────────────────────────────────────┘7. Datenmodell-Skizze
Abschnitt betitelt „7. Datenmodell-Skizze“export const shiftModelsTable = pgTable('shift_models', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull().references(() => tenantsTable.id), name: varchar('name', { length: 80 }).notNull(), // "Frühschicht" startLocal: time('start_local').notNull(), // 06:00 endLocal: time('end_local').notNull(), // 14:30 timeArt: varchar('time_art', { length: 32 }).notNull(), // "Arbeit"|"Rufbereitschaft" pauseMinutes: smallint('pause_minutes').notNull().default(30), nachtzuschlag: boolean('nacht_zuschlag').notNull().default(false), // §6 ArbZG activeFrom: date('active_from').notNull(), activeTo: date('active_to'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),}, (t) => ({ tenantNameUniq: uniqueIndex('sm_tenant_name_uq').on(t.tenantId, t.name),}));
export const staffingDemandsTable = pgTable('staffing_demands', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), siteId: uuid('site_id').notNull().references(() => sitesTable.id), shiftModelId: uuid('shift_model_id').notNull().references(() => shiftModelsTable.id), weekdays: smallint('weekdays').array().notNull(), // [1..7] headCount: smallint('head_count').notNull(), // Soll-MA qualifications: text('qualifications').array(), // ['dguv-g25','asbest-trgs519'] validFrom: date('valid_from').notNull(), validTo: date('valid_to'),});
export const shiftAssignmentsTable = pgTable('shift_assignments', { id: uuid('id').primaryKey(), // UUID v7 tenantId: uuid('tenant_id').notNull(), userId: uuid('user_id').notNull().references(() => usersTable.id), siteId: uuid('site_id').references(() => sitesTable.id), shiftModelId: uuid('shift_model_id').references(() => shiftModelsTable.id), shiftStart: timestamp('shift_start', { withTimezone: true }).notNull(), shiftEnd: timestamp('shift_end', { withTimezone: true }).notNull(), timeArt: varchar('time_art', { length: 32 }).notNull(), status: varchar('status', { length: 24 }).notNull().default('draft'), // draft | pending_br | published | swapped | cancelled publishedAt: timestamp('published_at', { withTimezone: true }), publishedVersion: uuid('published_version'), // Batch-Version arbzgFlags: jsonb('arbzg_flags').$type<ArbzgFlagSet>().default({}), supersedesId: uuid('supersedes_id').references(() => shiftAssignmentsTable.id), reasonText: varchar('reason_text', { length: 500 }), // Pflicht bei Änderung <4 Tagen createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), createdBy: uuid('created_by').notNull().references(() => usersTable.id),}, (t) => ({ userRangeIdx: index('sa_user_range_idx').on(t.tenantId, t.userId, t.shiftStart.desc()), siteRangeIdx: index('sa_site_range_idx').on(t.tenantId, t.siteId, t.shiftStart.desc()), conflictIdx: index('sa_conflict_idx').on(t.tenantId, t.userId, t.shiftStart),}));
export const shiftSwapRequestsTable = pgTable('shift_swap_requests', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), requestorUserId: uuid('requestor_user_id').notNull(), originalShiftId: uuid('original_shift_id').notNull().references(() => shiftAssignmentsTable.id), targetUserId: uuid('target_user_id').notNull(), targetShiftId: uuid('target_shift_id').references(() => shiftAssignmentsTable.id), // null = Abgabe ohne Gegenschicht reasonText: varchar('reason_text', { length: 500 }), status: varchar('status', { length: 24 }).notNull().default('pending_colleague'), // pending_colleague | colleague_accepted | colleague_rejected | // pending_manager | approved | rejected colleagueDecidedAt: timestamp('colleague_decided_at', { withTimezone: true }), managerDecidedAt: timestamp('manager_decided_at', { withTimezone: true }), managerUserId: uuid('manager_user_id'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),});
export const planVersionsTable = pgTable('plan_versions', { id: uuid('id').primaryKey().defaultRandom(), tenantId: uuid('tenant_id').notNull(), weekIso: varchar('week_iso', { length: 8 }).notNull(), // "2026-W17" publishedBy: uuid('published_by').notNull(), publishedAt: timestamp('published_at', { withTimezone: true }).notNull().defaultNow(), brApprovalStatus: varchar('br_approval_status', { length: 16 }), // null | pending | approved | rejected brApprovedBy: uuid('br_approved_by'), brApprovedAt: timestamp('br_approved_at', { withTimezone: true }), hashSelf: bytea('hash_self').notNull(), hashPrev: bytea('hash_prev'),}, (t) => ({ tenantWeekIdx: uniqueIndex('pv_tenant_week_uq').on(t.tenantId, t.weekIso, t.publishedAt),}));RLS-Policy-Sketch.
ALTER TABLE shift_assignments ENABLE ROW LEVEL SECURITY;ALTER TABLE shift_assignments FORCE ROW LEVEL SECURITY;
-- (a) Tenant-Isolation (Pflicht)CREATE POLICY sa_tenant_iso ON shift_assignments USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- (b) Scope-Lesen: eigene + Team-Mitglieder für Manager, alles für AdminCREATE POLICY sa_scope_read ON shift_assignments FOR SELECT USING ( tenant_id = current_setting('app.tenant_id')::uuid AND ( current_setting('app.role') IN ('admin','hr') OR user_id = current_setting('app.user_id')::uuid OR (current_setting('app.role') IN ('manager','bauleiter') AND user_id = ANY( SELECT user_id FROM team_memberships WHERE team_id = ANY(current_setting('app.team_ids')::uuid[]))) ) );
-- (c) Editieren nur Manager/Admin; Mitarbeiter darf nicht setzenCREATE POLICY sa_write ON shift_assignments FOR INSERT WITH CHECK ( tenant_id = current_setting('app.tenant_id')::uuid AND current_setting('app.role') IN ('manager','bauleiter','admin') );
-- (d) Delete nur über Supersede (append-only-ähnlich, wenn Plan veröffentlicht)CREATE POLICY sa_no_hard_delete ON shift_assignments FOR DELETE USING (status = 'draft' AND current_setting('app.role') IN ('manager','admin'));ER-Bezug. users, tenants, sites, projects, team_memberships, qualifications, absence_requests (aus §3.3), audit_log.
8. API-Endpunkte
Abschnitt betitelt „8. API-Endpunkte“Alle Endpunkte unter /v1/kern/plan/…, hinter Hono-Modul-Gate module.kern.dienstplanung.
| Methode | Pfad | Auth-Scope | Rate-Limit | Idempotenz | Beschreibung |
|---|---|---|---|---|---|
| GET | /v1/kern/plan/week?iso=2026-W17 |
plan:read:<scope> |
Standard | — | Wochenplan (für Rolle sichtbarer Ausschnitt) |
| POST | /v1/kern/plan/shifts |
plan:write |
Standard | Pflicht | Schicht setzen (Editor-Drop) |
| POST | /v1/kern/plan/shifts/{id}/correct |
plan:write |
Standard | Pflicht | Korrektur als neue Version (supersede) |
| DELETE | /v1/kern/plan/shifts/{id} |
plan:write |
Standard | — | Nur bei status=draft |
| POST | /v1/kern/plan/validate |
plan:write |
Standard | — | ArbZG/Quali-Probe ohne Persistenz |
| POST | /v1/kern/plan/publish?iso=2026-W17 |
plan:publish |
Privileged | Pflicht | Plan veröffentlichen (+ BR-Pfad) |
| POST | /v1/kern/plan/swap-requests |
plan:swap:own |
Standard | Pflicht | Tausch beantragen |
| POST | /v1/kern/plan/swap-requests/{id}/colleague-decide |
plan:swap:own |
Standard | Pflicht | Kollegen-Antwort |
| POST | /v1/kern/plan/swap-requests/{id}/manager-decide |
plan:swap:approve |
Privileged | Pflicht | Manager-Freigabe |
| POST | /v1/kern/plan/br-approval/{versionId} |
plan:br:approve |
Privileged | Pflicht | BR-Freigabe/Widerspruch |
| GET | /v1/kern/plan/ical/me |
plan:read:own |
Standard | — | iCal-Feed (HMAC-signierter Pfad) |
| GET | /v1/kern/plan/coverage?iso=2026-W17 |
plan:read:team |
Standard | — | Über-/Unterdeckung pro Site |
paths: /v1/kern/plan/shifts: post: operationId: createShiftAssignment x-werkszeit-scope: plan:write x-werkszeit-rate-limit: standard x-werkszeit-idempotency: required requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/ShiftAssignmentCreateInput' } responses: '201': { description: 'Schicht angelegt; enthält arbzg_flags und warnings' } '409': { description: 'Idempotency-Replay oder Konflikt (harte Quali-Sperre)' } '422': { description: 'Validation (ArbZG-Blocker mit Begründungs-Aufforderung)' } '423': { description: 'Woche bereits BR-freigegeben — Änderung nur via Korrektur' }Webhook-Events:
plan.shift.createdplan.shift.correctedplan.published(inkl. BR-Freigabe-Status)plan.swap.requestedplan.swap.approved/plan.swap.rejectedplan.coverage_alert(aggregiert pro Site, für Bauleiter-Abonnement)
9. Offline-Profil
Abschnitt betitelt „9. Offline-Profil“| Profil | Begründung | Mechanik |
|---|---|---|
| Mobile: Best-effort-offline | MA muss den eigenen Plan auch ohne Empfang lesen (Heizungskeller), Editierung ist Web-exklusiv | Drift-Cache des eigenen Wochen-/Monatsplans (30 Tage lokal), letzter Sync-Zeitstempel sichtbar, Tausch-Anträge landen in Outbox mit UUID-v7-Idempotency-Key |
| Web-Editor: Online-only | Drag&Drop-Editor braucht Live-Validierung (ArbZG, Konflikte) | Drop wartet auf Server-Response (<500 ms p95), zeigt Spinner. Bei Verbindungsabbruch: Editor wechselt in Read-Only-Banner „Verbindung prüfen“ |
Konflikt-Strategie. Append-only mit supersedes_id ab Publikation. Im Draft-Status sind direkte Updates erlaubt (Editor-Performance). Swap-Requests können im Feld offline gestartet werden; die Kollegen-Seite braucht online, weil die gegenseitige Validierung gegen ArbZG/Quali online erfolgt.
Foto-/Datei-Sync. Keine Foto-Anhänge an Schichten. Ggf. Notizen nur als Text.
10. Compliance-Mapping
Abschnitt betitelt „10. Compliance-Mapping“10.1 GoBD
Abschnitt betitelt „10.1 GoBD“— nicht primär zutreffend, weil Planung Prognose ist, nicht Buchführung. Aber: sobald eine Schicht in der Ist-Welt gestempelt wird, wandern die Ist-Zeiten in die time_entries-Hash-Chain von kern/01. Die Schicht-Versionen (Tabelle plan_versions) tragen eigenständig hash_prev/hash_self zum Zweck der BR-Nachvollziehbarkeit — 5 Jahre Aufbewahrung (nicht 10, weil kein §147 AO-Pflichtbeleg, aber zivilrechtliche Anspruchsfrist §195 BGB).
10.2 ArbZG — Echtzeit-Validierung
Abschnitt betitelt „10.2 ArbZG — Echtzeit-Validierung“- §3 Höchstarbeitszeit (8 h, bis 10 h mit Ausgleich). Beim Drop Summe der Tages-/Wochen-Schichten geprüft. Bei >10 h ohne TV-Override → Drop-Sperre. Bei >8 h mit Ausgleichs-Plan → Warnung, Begründung Pflicht.
- §4 Pausen. Schichten ≥6 h erhalten automatisch 30-min-Pause im Schichtmodell (konfigurierbar); ≥9 h 45 min. Bei manueller Über-Schreibung: Pflicht-Begründung.
- §5 Ruhezeit 11 h. Beim Drop wird die letzte Schicht des MA gelesen (auch tenant-übergreifend, wenn Leiharbeit mit Quell-Tenant-Hint gemeldet). <11 h → rote Warnung + Korrektur-Optionen.
- §6 Nachtzuschlag. Informativ im Drop-Dialog: „diese Schicht berührt Nachtzeit 23:00–06:00, Zuschlag nach TV XY wird beim Lohn berechnet.“
- §7 Ausnahmen. Konfigurierbar pro Tenant via TV-Override (z.B. „Bau: 10-h-Tage bis 4× pro Woche“). Property-Tests prüfen Bit-Identität der resultierenden
arbzg_flags. - BAG 1 ABR 22/21. Nicht direkt — das Urteil verlangt Erfassung, die Planung ist vorgelagert. Aber: ohne Plan kein Soll, ohne Soll kein BAG-Report.
10.3 VOB/B
Abschnitt betitelt „10.3 VOB/B“— nicht zutreffend. Dienstplan ist arbeits-, nicht werkvertragsrechtlich. Hinweis: Sub-Einsätze (§4.8) laufen separat und erzeugen keine shift_assignments für den Sub — die koordiniert §4.8.
10.4 DSGVO
Abschnitt betitelt „10.4 DSGVO“- Art. 5 Datenminimierung. Der Plan enthält nur Name, Baustelle, Zeitfenster; keine Adresse/Telefon. Vertretungs-Feld zeigt Name des Kollegen, keine Kontaktdaten.
- Art. 6 Rechtsgrundlage. Vertragserfüllung (Art. 6 Abs. 1 b) — Arbeitszeit-Zuweisung.
- Art. 17 Löschung. Bei MA-Austritt:
shift_assignments5 Jahre aufbewahrt (zivilrechtliche Ansprüche), danach Kaskaden-Kassation viadsgvo.kassation-Job. Namen werden durch Pseudo-UUID ersetzt, Baustelle/Zeitfenster bleibt (betriebliche Historie). - Art. 20 Portabilität. Eigener Plan als JSON + iCal im ESS-Export.
- Art. 30 VVZ. Eintrag „Dienstplanung Mitarbeiter“ mit Kategorien = MA, Daten = Schichten/Vertretungen/Tauschanträge, Empfänger = Lohnbuchhaltung (via Soll-Stunden), Löschfrist = 5 J.
- DSFA. Nicht Pflicht — keine systematische Überwachung, keine Profilbildung; aber Verfahrensdoku-Eintrag.
10.5 BFSG / WCAG 2.2 AA
Abschnitt betitelt „10.5 BFSG / WCAG 2.2 AA“- Eigener Plan Mobile (ESS-Flow). Screenreader-Tags: VoiceOver „Montag 20. April, Schicht 7 bis 16 Uhr, Lehrer Allee 7“. Live-Region meldet eingehenden Tauschantrag-Push.
- Tausch-Antrag-Modal — Fokus-Trap, Tastatur-
Esc-Schliess,role="dialog",aria-requiredan Pflicht-Radio. - Kontrast Schicht-Pill
#FFFFFFauf#2C5F8D= 9:1 (Stahl-Blau), Warning#C77A0Aauf#FBF1DA= 4.8:1. - Web-Editor ist kein ESS-Flow (Rolle Manager/Admin), aber wird trotzdem BFSG-tauglich gebaut — Tastatur-DnD via
arrow keys + enterals Alternative zu Maus-DnD.
10.6 BetrVG §87 Abs. 1 Nr. 2 — Schichtplan-Mitbestimmung
Abschnitt betitelt „10.6 BetrVG §87 Abs. 1 Nr. 2 — Schichtplan-Mitbestimmung“Dies ist nicht §87(1)6 (Überwachung), sondern §87(1)2 (Beginn/Ende der Arbeitszeit, Verteilung). Mechanik:
- Tenant-Flag
plan.betriebsrat_freigabe = true. Aktivierung selbst ist BR-pflichtig und erzeugt Audit-Eventbetrvg.flag_activation. - Workflow bei aktiviertem Flag:
- Manager drückt „Publizieren“ →
plan_versions.status = pending. - BR-User (separate Rolle
betriebsrat) erhält signierten Deep-Link. - BR sieht Diff zur Vorversion, kann „Freigeben“ oder „Widersprechen“.
- Widerspruch → Plan bleibt Entwurf; Manager muss überarbeiten oder Einigungsstelle anrufen (außerhalb Scope der App).
- Freigabe →
plan_versions.status = approved, Hash festgeschrieben, Push an MA.
- Manager drückt „Publizieren“ →
- Ankündigungsfrist (TV-konfigurierbar, default 4 Tage). Änderung <4 Tage → MA-Zustimmung als separates Event, keine automatische Push-Verteilung.
- BR-Audit-Export: quartalsweiser CSV-Export der BR-Freigaben und -Widersprüche (Spalten: Woche, Manager, BR-User, Entscheidung, Zeit, Diff-Hash).
10.7 Weitere Spezial-Compliance
Abschnitt betitelt „10.7 Weitere Spezial-Compliance“- §18 JArbSchG — Jugendliche. Azubis <18 J werden bei Drop auf gefährdende Baustellen (Gerüst >2 m, Asbest, Nachtschicht nach 22:00) hart blockiert.
- DGUV V1 §7 — Unterweisung vor Zuweisung. Quali-Katalog enthält auch Unterweisungs-Nachweise (
unterweisung_arbeitsschutz_jahr_YYYY); Ablauf blockiert analog DGUV-G25. - §106 GewO — billiges Ermessen. Systemseitig nicht automatisierbar, aber Audit-Log stellt die Beweislage für Gericht bereit (wer hat wann geändert, mit welchem Grund).
11. Edge-Cases
Abschnitt betitelt „11. Edge-Cases“| # | Szenario | Erwartetes Verhalten |
|---|---|---|
| EC-01 | Zeitzonenwechsel — Hannes wird für Baustelle in Zürich eingeplant (CH, andere Zeitzone). | Schicht speichert UTC; Anzeige in Europe/Zurich wenn Baustellen-TZ abweicht; iCal mit TZID korrekt. |
| EC-02 | Tagesgrenze 00:00 — Nachtschicht Di 22:00 bis Mi 06:00. | Zwei-Tages-Bezug, beide Kalendertage angezeigt; ArbZG-Wochen-Kumulation nutzt ISO-Woche des Schicht-Beginns. |
| EC-03 | Multi-Tenant-Bleed — Manager hat Scope in 2 Tenants, wechselt aktiven Tenant. | Editor lädt neu, RLS setzt app.tenant_id; Cross-Tenant-Abfrage liefert 404. |
| EC-04 | Sync-Konflikt — Manager A und B draggen gleichzeitig Hannes auf Mo 07:00 auf verschiedenen Baustellen. | Optimistic Concurrency via ETag/If-Match auf Assignment-Range; zweiter Drop bekommt 409 + Reload-Aufforderung. |
| EC-05 | Großmenge — 80-MA-Tenant, 4-Wochen-Copy-Paste erzeugt ~640 Schichten. | Bulk-API mit Chunking (50 Slots/Batch), Fortschrittsbalken im Editor; Idempotency-Key pro Batch. |
| EC-06 | Abgelaufenes Zertifikat während bereits publizierter Woche. | Schicht bleibt bestehen, Alert-Ticket „Zertifikat läuft am Mi ab, 2 Schichten betroffen“ an Manager; MA bekommt Push. |
| EC-07 | Plan-Limit (Starter) — 11. MA soll eingeplant werden. | API 402 plan_limit_exceeded; Editor zeigt Upgrade-Hinweis. |
| EC-08 | Rollen-Demotion mid-action — Manager wird während offenem Editor demoted. | Nächste API-Request 403; Editor zeigt „Berechtigung entzogen“-Toast + Redirect auf Read-Only. |
| EC-09 | Sommerzeit-Umstellung 29.03.2026 — Schicht 01:00–09:00 an diesem Tag. | Schicht dauert real 7 h (Uhr springt vor); arbzg_flags.dst_transition = true; Lohnabrechnung nutzt reale Stunden. |
| EC-10 | Urlaubs-Sperre nachträglich — MA reicht Urlaub ein für Tag, an dem er bereits eingeplant ist. | absence_requests-Event triggert Plan-Alert; Manager muss Schicht stornieren oder Urlaub ablehnen. |
| EC-11 | BR-Widerspruch nach Publikation — BR widerspricht erst nach Push an MA. | Status der Version wechselt auf rejected_post_publish; MA bekommen Push „Schicht-Plan widerrufen, neue Version folgt“; Audit-Event. |
| EC-12 | Tausch-Kette — Hannes → Mehmet → Andreas (Mehmet akzeptiert und will selbst tauschen). | Jeder Swap ist einzeln atomar; wir erlauben keine 3-Wege-Tausche in einem Request. Mehmet muss nach Annahme selbst neuen Antrag stellen. |
| EC-13 | Kurzfristige Änderung <4 Tage (TV-Verletzung) trotz MA-Ablehnung. | API 409 tv_advance_notice_violation; Manager kann mit separatem „Notfall“-Grund und MA-Bestätigung per Klick überschreiben. |
12. Akzeptanzkriterien (Gherkin)
Abschnitt betitelt „12. Akzeptanzkriterien (Gherkin)“Funktionalität: Dienstplanung — Drag&Drop, ArbZG-Gate, Qualifikations-Sperre, Tausch-Workflow
Hintergrund: Angenommen ein Tenant "musterbetrieb-maler" mit Modul-Gate "module.kern.dienstplanung" Und ein Manager "Sabine Maier" mit Scope "team" Und ein Monteur "Hannes Krüger" mit aktivem Zertifikat "DGUV-G25" bis 31.12.2026 Und ein Azubi "Jonas Brehm" (17 Jahre, ohne DGUV-G25) Und eine Baustelle "Lehrer Allee 7" mit Qualifikations-Anforderung "DGUV-G25" Und das Datum ist "Mo 20.04.2026"
Szenario: Happy Path — Schicht per Drag&Drop setzen Wenn Sabine im Editor Hannes auf Mo 20.04. 07:00–16:00 "Lehrer Allee 7" zieht Dann sendet der Client POST /v1/kern/plan/shifts mit Idempotency-Key Und das System validiert ArbZG, Qualifikation, Urlaub synchron Und persistiert die Schicht mit status="draft" Und Audit-Log enthält "plan.shift.created" mit Aktor Sabine
Szenario: Grenzfall — ArbZG §5 Ruhezeit-Verletzung Angenommen Hannes hat am So 19.04. eine Schicht bis 22:30 gearbeitet Wenn Sabine versucht, Hannes auf Mo 20.04. 06:00–14:30 Frühschicht zu droppen Dann antwortet die API mit 422 "arbzg.ruhezeit_verletzt" Und der Editor zeigt Modal "Ruhezeit 7:30 h statt 11 h — Optionen" Und die Schicht wird NICHT persistiert, außer der Manager wählt "mit Begründung überschreiben"
Szenario: Grenzfall — Qualifikations-Gate harte Sperre Wenn Sabine versucht, Jonas Brehm (Azubi, 17, kein DGUV-G25) auf Lehrer Allee 7 zu droppen Dann antwortet die API mit 409 "qualification.missing" Und der Editor zeigt Modal "⛔ Qualifikation fehlt — §18 JArbSchG + DGUV V1 §7" Und keine Schicht wird erzeugt Und Audit-Log enthält "plan.qualification_block" mit Aktor Sabine
Szenario: Grenzfall — Multi-Tenant-Isolation Angenommen ein zweiter Tenant "shk-gebruder-schmidt" mit Monteur "Thomas Schmidt" Wenn Sabine (Tenant A) per ID einen Plan-Slot von Thomas (Tenant B) abruft Dann antwortet die API mit 404 Not Found Und es entsteht KEIN Audit-Log-Eintrag in Tenant B Und die RLS-Policy sa_tenant_iso hat den Zugriff auf DB-Ebene verhindert
Szenario: Grenzfall — Tausch-Workflow mit BR-Freigabe-Flag Angenommen der Tenant hat "plan.betriebsrat_freigabe = true" Und Hannes beantragt Tausch seiner Fr-Schicht mit Andreas Wenn Andreas den Tausch akzeptiert Dann wechselt der Status auf "pending_manager" Und Sabine erhält Push-Notification Wenn Sabine "Freigeben" klickt Dann wird eine neue Plan-Version "pending_br" erzeugt Und der Betriebsrat "Herr Gärtner" bekommt signierten Deep-Link Wenn Herr Gärtner "Freigeben" klickt Dann wechselt die Plan-Version auf "approved" Und Hannes und Andreas bekommen Push "Plan geändert ab Fr 24.04."
Szenario: Grenzfall — TV-Ankündigungsfrist <4 Tage Angenommen heute ist Do 23.04. und der TV fordert 4 Tage Ankündigung Wenn Sabine versucht, Hannes kurzfristig für Fr 24.04. einzuplanen Dann antwortet die API mit 409 "tv_advance_notice_violation" Und der Editor fordert Begründung + MA-Zustimmungs-Link Und ohne MA-Bestätigung bleibt die Schicht im Status "needs_employee_consent"
Szenario: Grenzfall — Urlaubs-Sperre Angenommen Hannes hat für Mi 22.04.–Fr 24.04. Urlaub beantragt mit Status "approved" Wenn Sabine versucht, Hannes auf Mi 22.04. zu droppen Dann verweigert der Editor den Drop clientseitig Und auf API-Ebene antwortet POST /shifts mit 409 "absence_conflict" Und die Schicht wird nicht erzeugt13. Test-Cases
Abschnitt betitelt „13. Test-Cases“13.1 Unit-Tests
Abschnitt betitelt „13.1 Unit-Tests“- U-01 —
ArbzgValidator.ruhezeit(prevStop, nextStart, overrides)— erwartetvalid=truebei ≥11 h,violationsonst; OverridebauTV_10hbei konfiguriertem Tenant. - U-02 —
QualificationGate.canAssign(userId, siteId, date)— true/false/hard_block, abhängig von Zertifikats-Status und JArbSchG. - U-03 — Property-based (glados): für zufällige Schicht-Ketten wird
weeklyHoursSumkorrekt über Kalenderwoche berechnet (ISO 8601). - U-04 —
SwapWorkflow.advance(request, role)— Zustandsmaschine korrekt (pending_colleague → colleague_accepted → pending_manager → approved). - U-05 —
PlanVersionHash.verify(version, entries)— Bit-Identität trotz Sortierungs-/Encoding-Permutationen (kanonische JSON).
13.2 Widget-/Component-Tests (Flutter)
Abschnitt betitelt „13.2 Widget-/Component-Tests (Flutter)“- W-01 — Drag-Widget zeigt Live-ArbZG-Overlay beim Hover über Ziel-Zelle (axe-core clean).
- W-02 — Mobile Tausch-Antrag — Radio-Liste filtert ungültige Kollegen (Urlaub, Quali fehlt) vor API-Call.
- W-03 — Golden-Test
plan/eigener-plan-wocheLight/Dark × iOS/Android ×de-DE. - W-04 — Semantics:
SemanticsTester.findByLabel('Schicht Montag 20. April, 7 bis 16 Uhr, Lehrer Allee 7')liefert Treffer.
13.3 Integrations-Tests
Abschnitt betitelt „13.3 Integrations-Tests“- I-01 — Multi-Tenant-Isolation (Pflicht DOD §2.2): Tenant A erzeugt Schicht, Tenant B bekommt 0 Zeilen.
- I-02 — RLS Scope
own: MA sieht nur eigene; Manager sieht Team; Admin sieht alle. - I-03 — Idempotenz: Doppelter Drop mit gleichem Key → 1 Row, zweites Response = erstes.
- I-04 — ArbZG-Blocker: Schicht, die Ruhezeit <11 h erzeugt, wird ohne
override_reasonabgelehnt. - I-05 — Quali-Gate: Drop Azubi auf DGUV-Baustelle → 409, kein DB-Eintrag.
- I-06 — Optimistic Concurrency: zwei parallele Drops auf gleiche (user, range) → einer 409.
- I-07 — BR-Freigabe-Flow: publish → pending_br → BR-approve → approved; jede Transition Audit-Event.
- I-08 — Swap-Kette: Tausch-Request-State-Machine durchläuft alle Stati, niemals rückwärts.
13.4 E2E-Tests (Patrol + Playwright, Pre-Merge-Matrix)
Abschnitt betitelt „13.4 E2E-Tests (Patrol + Playwright, Pre-Merge-Matrix)“- E-01 — Happy-Path Drag&Drop (Web Chromium/Firefox/WebKit), inkl. Toast-Bestätigung.
- E-02 — Mobile-Tausch-Flow: Hannes beantragt, Mehmet akzeptiert, Sabine gibt frei (3 Patrol-Runs synchronisiert).
- E-03 — ArbZG-Warning-Dialog erscheint on-hover (Web), Keyboard-navigierbar.
- E-04 — Visuelle Regression: Editor, Modal, Mobile-Plan, Tausch-Dialog.
- E-05 — Accessibility: axe-core Critical/Serious = 0 auf Mobile-ESS-Flows; Flutter-Semantics grün.
13.5 Compliance-Tests
Abschnitt betitelt „13.5 Compliance-Tests“- C-01 — Plan-Version Hash-Chain: Manipulation einer Schicht bricht Version-Hash-Chain (Detection).
- C-03 — ArbZG-Property-Tests (§3, §5) mit 10 000 random Schicht-Ketten, Tenant-Overrides berücksichtigt.
- C-04 — BR-Freigabe-Audit-Export: generierter CSV enthält alle erwarteten Spalten und keine Klartext-Kommentare von MA-Stammdaten (DSGVO-Datenminimierung).
- C-06 — Sommerzeit-Umstellung 29.03.2026: Schicht 01:00–09:00 → reale 7 h,
arbzg_flags.dst_transition = true.
13.6 Flakiness-Schutz
Abschnitt betitelt „13.6 Flakiness-Schutz“Lokaler 20×-Lauf E-01, E-02 grün vor Merge; DnD-Events sind notorisch flaky, deshalb Fixture mit deterministischem Mouse-Event-Timing.
14. Nicht-Ziele
Abschnitt betitelt „14. Nicht-Ziele“| Nicht-Ziel | Begründung |
|---|---|
| Mobile Drag&Drop-Editor | Dienstplan auf 6“ Display = Fehlerquelle. Bewusste Einschränkung. Tablet-Browser statt Mobile-App, wenn am Baustellen-Tablet geplant wird. |
| 3-Wege-Schicht-Tausch (A → B → C in einem Schritt) | Kombinatorik explodiert, DB-Locking wird komplex. Jeder Swap ist atomar-bilateral. |
| Auto-Planner / KI-Schichtoptimierung | Kein Referenzkunde fragt es an; Black-Box-Planung ist BR-politisch toxisch. Frühestens V3 mit BR-Einwilligungs-Workflow. |
| Integration externer Schichtplan-Tools (Vivendi, PEP, …) | Wir sind die Quelle, nicht der Konsument. Import-Wizard nur für Excel (V1.5). |
| GPS-basierte Anwesenheits-Plausibilisierung | BetrVG §87(1)6, siehe kern/01-Feinkonzept — nicht in Plan-Modul. |
| Einsatz-Zeiten an einem Tag splitten auf mehrere Baustellen | Brauchen Referenzkunden; im MVP genau eine Hauptbaustelle pro Schicht. Zeit-Buchung kann mehrere Baustellen abbilden (§3.1). |
| Automatische Lohnabrechnung aus Soll-Schichten | Liegt bei DATEV via kern/10; Plan liefert nur Soll-Stunden, nicht Abrechnung. |
| Sub-Unternehmer in den Plan einplanen | Sub-Koordination hat eigenen Scope (§4.8); andere Datenstruktur, andere Compliance (§13b UStG). |
15. Risiken & offene Annahmen
Abschnitt betitelt „15. Risiken & offene Annahmen“| Risiko / Annahme | Impact | Wahrscheinlichkeit | Gegenmaßnahme |
|---|---|---|---|
| Drag&Drop-Performance in Flutter Web bei 80 MA × 7 Tage × 3 Schichten | hoch | mittel | Virtualisierte Zeilen, inkrementelles Rendering, Load-Test gegen Seed-Tenant B; Fallback: einfacher Click-to-Edit statt Drag. |
| BR-Deep-Link-Sicherheit — BR-User ist kein „normaler“ Werkszeit-User, hat begrenzten Scope | mittel | niedrig | HMAC-signierte Links mit 7-Tage-TTL; nach Klick nur Read + approve/reject; kein Read in andere Module. |
| TV-Vielfalt — Maler/Lackierer-TV, SHK-Metall-TV, Bau-TV unterscheiden sich bei Ankündigungsfrist | mittel | hoch | Admin-UI erlaubt TV-Override; Default 4 Tage; konkrete TV-Klauseln werden im Config-Doku dokumentiert. |
| Cross-Tenant-Leiharbeit — MA wird zwischen 2 Werkszeit-Tenants ausgeliehen, Ruhezeit muss übergreifen | hoch | niedrig | MVP: jeder Tenant rechnet isoliert; beim Zieltenant muss der MA Ruhezeit manuell melden. V2: „Leih-Link“ zwischen Tenants mit expliziter Freigabe. |
| Betriebsrat hat keinen Werkszeit-Zugang — kleinerer Betrieb, BR existiert aber hat kein Gerät | mittel | mittel | Alternative: signierter PDF-Export per E-Mail + Rücksendung mit Unterschrift, Upload durch Manager mit Pflicht-Begründung. |
| Optimistic Concurrency Overhead — zwei Planer arbeiten gleichzeitig | mittel | mittel | Pro (tenant, user, week) eine versioned Row; ETag-basiert; Presence-Indicator im UI (Y schreibt gerade). |
| ArbZG-Grenzen vs. Tarif — manche TV erlauben explizit >10 h, BAG-Rechtsprechung zur Auslegung | hoch | mittel | Override via Admin + Legal-Review; im UI wird immer auf die zugrundeliegende TV-Klausel verlinkt. |
16. Abhängigkeiten
Abschnitt betitelt „16. Abhängigkeiten“-
Vorbedingung:
kern/09-auth-self-service— Rollen Manager/Bauleiter/Betriebsrat mit passenden Scopes.kern/02-arbeitszeit-compliance— ArbZG-Policy-Engine als Library (shared DART-Paket), weil Mobile-Voransicht dieselbe Prüfung braucht.kern/03-abwesenheiten—absence_requestsliefern Urlaubs-Sperre.handwerk/11-qualifikations-kontrolle—qualifications-Tabelle mit Ablaufdaten.kern/11-compliance-audit— Audit-Log für Plan-Versionen, BR-Freigaben, Quali-Gates.
-
Schnittstelle zu:
kern/01-zeiterfassung— Ist-Zeit vergleicht sich gegenshift_assignments.shift_start/end, liefertdelta_minutesfür Saldo.kern/07-reporting— Soll/Ist-Report, Plan-Treue-KPI.kern/10-datev-integration— Soll-Stunden fließen in Lohnberechnung (Gleitzeit-Saldo-Basis).
-
Wird konsumiert von:
- Mitarbeiter-Dashboard (Home) zeigt „Heute: Schicht 07:00–16:00“.
- Bauleiter-Dashboard zeigt Over-/Under-Coverage-Alarm.
17. Referenzkunde-Slot
Abschnitt betitelt „17. Referenzkunde-Slot“Status: TBD — zu klären mit Sales vor V1-Sprint-Start (Monat 9).
DOR §1.1.1 verlangt: mindestens ein zahlender Design-Partner, der das Feature konkret fordert.
Kandidaten-Profile:
- SHK-Betrieb 40–80 MA mit Bereitschaftsdienst-Rotation (Notdienst 24/7); aktuell Excel + WhatsApp; BR vorhanden, aktiv; Motiv: SOKA-Bau-Compliance + rechtssichere Schichtplanung.
- Bauunternehmen 60–100 MA mit 3–5 parallelen Baustellen, Kolonnen-System; aktuell Windows-PC-Planungstool aus den 2000ern; Motiv: mobile Lesbarkeit, Integration Stundennachweis.
- Gebäudereiniger 30–50 MA mit Schicht-Modellen früh/spät/nacht; aktuell PEP oder Vivendi; Motiv: günstigerer Preis, Integration mit Zeiterfassung.
Validierungs-Fragen für das Erst-Gespräch:
- Wer plant bei Ihnen heute die Schichten — Chef, Bauleiter, Disponent, Büroleitung? Wie viele Minuten pro Woche?
- Haben Sie Betriebsrat? Wie oft gibt es Schichtplan-Widersprüche?
- Wie handhaben Sie Urlaub + kurzfristigen Krankheitsfall heute? Wer springt ein?
- Gibt es bei Ihnen Azubis? Wie stellen Sie sicher, dass sie nicht falsch eingeplant werden?
- Arbeiten Sie mit Leiharbeitern? Wie halten Sie deren Ruhezeit tenant-übergreifend nach?
- Wären Sie bereit, 30 Tage Beta-Test gegen 6 Monate Preis-Lock-in?
18. Senior-Berater-Empfehlung
Abschnitt betitelt „18. Senior-Berater-Empfehlung“Build-Sequenz (innerhalb des Features):
- Datenmodell + RLS + Multi-Tenant-Test + Plan-Version-Hash-Chain. Das Fundament zuerst — Plan-Versionen sind der Hebel für BR-Audit und Nachvollziehbarkeit.
- Backend-API
POST /shifts+POST /validate(dry-run) mit ArbZG-Validator-Lib-Reuse. Ohne echte Validierung ist der Editor eine Trugschlussmaschine. - Qualifikations-Gate-Integration mit
handwerk/11-Tabelle. Harte Sperre, kein Soft-Warning. - Web-Editor (Flutter Web) mit Drag&Drop + ArbZG-Overlay + Over/Under-Coverage. MVP-primäre Oberfläche.
- Mobile Read-Only + Tausch-Antrag (Flutter). Offline-Cache des eigenen Plans via Drift.
- BR-Freigabe-Workflow (separate Rolle, Deep-Link, Diff-View).
- Compliance-Tests (C-01 Hash, C-03 ArbZG-Property) und Golden-Screenshots.
- iCal-Export und Vorlagen (V1.5, kein Blocker für MVP-Kern-Demo).
Risiko-Reihenfolge (was zuerst absichern bei Zeitnot):
- Multi-Tenant-Isolation und Quali-Gate sind nicht verhandelbar — fallen nie raus.
- Bulk-Assign, Vorlagen, iCal-Export, Editor-Polish sind die ersten Streich-Kandidaten.
- BR-Workflow kann für MVP als „Flag off“ ausgeliefert werden, wenn kein Referenzkunde ihn braucht.
Stop-the-Bus-Triggers:
- Multi-Tenant-Bleed in Integrationstest → Deploy stoppen.
- Schicht wird gesetzt trotz ArbZG-Ruhezeit-Verletzung ohne Override-Grund → P0-Incident.
- Qualifikations-Gate wird von Azubi-Fall gebrochen → Legal-Eskalation (§18 JArbSchG).
- BR-Freigabe-Link leitet auf fremden Tenant → Sicherheits-Incident.
Was uns 2027 dankbar macht:
Plan-Versionen als eigene Hash-Chain-Tabelle (nicht nur einzelne Assignments) geben uns bei BR-Konflikten, Einigungsstellen-Verfahren und Arbeitsgerichtsfällen eine beweissichere Versionshistorie, die nicht nachträglich glattgezogen werden kann. Die frühe Entkopplung „Plan = Prognose, Zeit = Ist“ verhindert, dass wir in 2027 dieselben Sünden wiederholen wie die Alt-App, deren Stundenzettel aus einem amorphen State-Blob rekonstruiert werden mussten. Und die Entscheidung, Drag&Drop auf Web zu beschränken, erspart uns die Qualitäts-Hölle eines untauglichen Mobile-Editors.
19. Patch 2026-04-23 — BR-Freigabe + Swap-v2 + iCal-Feinschliff
Abschnitt betitelt „19. Patch 2026-04-23 — BR-Freigabe + Swap-v2 + iCal-Feinschliff“Migration 0027 (0027_kern_08_plan_extras.sql) ergänzt drei Tabellen + einen Unique-Index:
-
plan_br_freigaben— Append-only Status-History pro Plan-Woche (plan_woche_id → plan_versions.id). Jede BR-Entscheidung legt eine neue Row mit neuemstatus(Enum:entwurf → einreichung → br_review → br_freigegeben | br_abgelehnt → zurueck_entwurf → veroeffentlicht). Pro Tenant verkettete Hash-Chain (hash_prev/hash_self, SHA-256 canonical, BEFORE-INSERT-Triggerplan_br_freigaben_chain). RLS: Tenant-Isolation viaapp.tenant_id, INSERT-Policy erlaubt Rollenbetriebsrat|admin|manager|bauleiter, REVOKE UPDATE/DELETE. -
plan_swap_requests— Tauschanfragen A↔B mit State-Machinewartet_b → b_akzeptiert → planer_akzeptiert → durchgefuehrt(plus Abbruchpfadeb_abgelehnt,abgebrochen). Hash-Chain analog. Beiplaner_akzeptiertwerden dieshift_assignments.user_idinnerhalb derselben Drizzle-RLS-Transaction atomar getauscht (optimistic Guards auf altenuser_id, TX-Rollback bei Race). Beide Schichten bekommen zusätzlichstatus='swapped'als Audit-Marker. -
mitarbeiter_ical_tokens— 256-bit-random Tokens (crypto.randomBytes(32)→ base64url). DB speichert NURtoken_hash = sha256(plain)als bytea(32),Unique-Index mit_token_hash_uq. Rotation perPOST /v1/kern/plan/ical/mitarbeiter-ical-token/regenerate(alte Tokens →revoked=true).
Neue Endpoints (§8-Tabelle ergänzt):
| Endpoint | Scope | Hash-Chain |
|---|---|---|
POST /v1/kern/plan/br-approval/plan-woche/:id/br-einreichen |
plan:publish | ja |
POST /v1/kern/plan/br-approval/br-freigabe/:id/entscheiden |
plan:br:approve | ja |
GET /v1/kern/plan/br-approval/br-freigabe |
plan:br:approve | — |
POST /v1/kern/plan/swap-requests/v2 |
plan:swap:own | ja |
POST /v1/kern/plan/swap-requests/v2/:id/b-entscheiden |
plan:swap:own | ja |
POST /v1/kern/plan/swap-requests/v2/:id/planer-freigabe |
plan:swap:approve | ja + atomar |
GET /v1/kern/plan/swap-requests/v2 |
plan:read | — |
GET /v1/kern/plan/ical/mitarbeiter/:token |
öffentlich (Token-Auth) | — |
GET /v1/kern/plan/ical/team/:team_id |
plan:read:team | — |
POST /v1/kern/plan/ical/mitarbeiter-ical-token/regenerate |
plan:read:own | — |
iCal-RFC-5545-Feinschliff. VCALENDAR enthält VTIMEZONE Europe/Berlin (DST-Regeln als RRULE), X-WR-CALNAME = Werkszeit · {displayName}, X-WR-TIMEZONE = Europe/Berlin. Pro Schicht ein VEVENT mit UID=<shift-id>@werkszeit.de, DTSTART;TZID=Europe/Berlin:YYYYMMDDTHHMMSS, DTEND analog, DTSTAMP in UTC, STATUS-Mapping published→CONFIRMED / cancelled→CANCELLED / sonst→TENTATIVE. CRLF-Line-Endings + Line-Folding bei 75 Oktetts. BEGIN/END-Balance automatisch per Builder sichergestellt.
Feinkonzept-Stand: 2026-04-23 · Werkszeit Konzeptions-Team · Greenfield Phase 1 · Version v0.2