Zum Inhalt springen

Dienstplanung — Drag&Drop-Schichteditor, Tausch-Workflow, Qualifikations-Gate

Beta — in Erprobung

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.


feature_id: kern/08-dienstplanung
title: Dienstplanung — Drag&Drop-Schichteditor, Tausch-Workflow, Qualifikations-Gate
funktionsumfang_ref: §3.8
roadmap_horizont: V1 # MVP liefert nur Read-Only-Ansicht; Editor ab V1
plattformen:
mobile: nur-Ansicht # Eigener Plan, Team-Woche, Tausch-Anträge
web: vollständig # Drag&Drop-Editor, Bedarfsplanung, BR-Freigabe
desktop: ab V1.5 bei Bedarf
owner_rolle: Manager # Bauleiter / Disponent plant; Admin konfiguriert Schichtmodelle
modul_gate_flag: module.kern.dienstplanung
compliance_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`

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 via dart:html); Backend in Hono + Postgres, Echtzeit-Validierung via Reuse der ArbZG-Engine aus kern/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).

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.

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.1 Mobile App (📱) — iOS + Android, Read-Only + Tausch

Abschnitt betitelt „5.1 Mobile App (📱) — iOS + Android, Read-Only + Tausch“
  • F-M-01Eigener 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-02Team-Woche Read-Only (nur für Rolle Manager/Bauleiter auf Mobile) — kompakte Übersicht, wer wo ist; keine Edit-Gesten.
  • F-M-03Tausch-Antrag starten: „Schicht tauschen mit …“ öffnet Kontakt-Picker (nur Kollegen mit gleicher Quali, nicht im Urlaub, ArbZG-kompatibel). Push an Kollegen.
  • F-M-04Tausch-Antrag akzeptieren/ablehnen: Kollege sieht Push, öffnet App, tippt „Annehmen“/„Ablehnen“. Bei Annahme: Manager erhält Freigabe-Push.
  • F-M-05Kalender-Export iCal: Mitarbeiter kann eigenen Plan als .ics in persönlichen Kalender abonnieren (Tenant-gatewayed URL mit HMAC-Token, read-only, ohne Drittperson-Daten).
  • F-M-06Konflikt-Warnung beim Tausch: System prüft in Echtzeit gegen ArbZG + Qualifikation + Urlaubs-Sperre. Bei Konflikt: Tausch-Antrag nicht absendbar, mit Begründung.
  • F-M-07Mobile Edit ist aus. Bewusst. Wer am Handy draggt, macht Fehler — Dienstplanung findet am Web/Tablet-Browser statt.
  • F-W-01Kalender-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-02Kalender-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-03Multi-Select + Bulk-Assign: Rechteck-Auswahl über mehrere Zellen, Dropdown „alle diese Slots mit Baustelle X füllen“.
  • F-W-04Vorlagen-Anwenden: „Woche wie KW 15“ kopiert die Schicht-Struktur, passt Datum an, prüft Urlaub/Krank.
  • F-W-05Bedarfsplanung pro Baustelle: Soll-Besetzung (Anzahl MA pro Zeitfenster) wird als Hintergrund-Lane im Raster dargestellt; Unterdeckung erzeugt orange Pill „-2 MA“.
  • F-W-06ArbZG-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-07Qualifikations-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-08Urlaubs-/Krank-Sperre: Tage mit Status „beantragter Urlaub“ oder „eAU“ sind im Raster grau-gestreift und nicht bedropbar.
  • F-W-09Tausch-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-10BR-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-11Versions-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-12Push-Publikation an MA: Beim „Plan freigeben“ bekommen alle betroffenen MA eine Push mit Link auf ihren Plan.
  • F-W-13Unter-/Überdeckungs-Dashboard als Widget im Manager-Homescreen: „KW 17: 3 Baustellen mit Unterdeckung, 1 mit Überdeckung“ → Click öffnet Editor am jeweiligen Slot.
  • F-X-01Zeitzone ist Tenant-Zeitzone (Europe/Berlin). Export via iCal nutzt DTSTART/DTEND mit TZID, nicht UTC, um in Outlook/Apple Calendar korrekt anzuzeigen.
  • F-X-02Schicht-Pill-Format: HH:MM–HH:MM · Baustelle-Kurzname, bei Rufbereitschaft ⚓ HH:MM–HH:MM, bei Nachtschicht mit Mond-Glyph.
  • F-X-034-Tage-Ankündigungsfrist-Check (TV-spezifisch, konfigurierbar): Plan-Änderung <4 Tage vor Schicht-Beginn → MA-Zustimmung Pflicht (siehe BR-Integration).
  • F-X-04Soft-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.
  • F-A-01Schichtmodelle 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-02Bedarfsplanung-Stammdaten pro Baustelle / Kostenstelle: Anzahl MA × Schichtmodell × Wochentage. Zeitlich begrenzt (Gültig-von/bis).
  • F-A-03Tenant-Einstellung BR-Freigabe: Flag plan.betriebsrat_freigabe = true|false. Bei true: Publikationsweg geht über BR. Flag-Änderung selbst ist BR-pflichtig (Meta-Audit).
  • F-A-04Ankündigungsfrist: 0/2/4/7 Tage, tenant-global.
  • F-A-05Qualifikations-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.
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

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.

┌────────────────────────────────┐
│ ← 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] │
└────────────────────────────────┘
┌────────────────────────────────┐
│ ✕ 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.]│
└────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────┐
│ 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 │
└────────────┴─────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ ⚠ 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 ] │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ ⛔ 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 ] │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ 🗳 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 ] │
└──────────────────────────────────────────────────────────────────┘

apps/api/src/db/schema/shifts.ts
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 Admin
CREATE 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 setzen
CREATE 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.


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.created
  • plan.shift.corrected
  • plan.published (inkl. BR-Freigabe-Status)
  • plan.swap.requested
  • plan.swap.approved / plan.swap.rejected
  • plan.coverage_alert (aggregiert pro Site, für Bauleiter-Abonnement)

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.


— 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).

  • §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.

— 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.

  • 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_assignments 5 Jahre aufbewahrt (zivilrechtliche Ansprüche), danach Kaskaden-Kassation via dsgvo.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.
  • 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-required an Pflicht-Radio.
  • Kontrast Schicht-Pill #FFFFFF auf #2C5F8D = 9:1 (Stahl-Blau), Warning #C77A0A auf #FBF1DA = 4.8:1.
  • Web-Editor ist kein ESS-Flow (Rolle Manager/Admin), aber wird trotzdem BFSG-tauglich gebaut — Tastatur-DnD via arrow keys + enter als 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-Event betrvg.flag_activation.
  • Workflow bei aktiviertem Flag:
    1. Manager drückt „Publizieren“ → plan_versions.status = pending.
    2. BR-User (separate Rolle betriebsrat) erhält signierten Deep-Link.
    3. BR sieht Diff zur Vorversion, kann „Freigeben“ oder „Widersprechen“.
    4. Widerspruch → Plan bleibt Entwurf; Manager muss überarbeiten oder Einigungsstelle anrufen (außerhalb Scope der App).
    5. Freigabe → plan_versions.status = approved, Hash festgeschrieben, Push an MA.
  • 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).
  • §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).

# 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.

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 erzeugt

  • U-01ArbzgValidator.ruhezeit(prevStop, nextStart, overrides) — erwartet valid=true bei ≥11 h, violation sonst; Override bauTV_10h bei konfiguriertem Tenant.
  • U-02QualificationGate.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 weeklyHoursSum korrekt über Kalenderwoche berechnet (ISO 8601).
  • U-04SwapWorkflow.advance(request, role) — Zustandsmaschine korrekt (pending_colleague → colleague_accepted → pending_manager → approved).
  • U-05PlanVersionHash.verify(version, entries) — Bit-Identität trotz Sortierungs-/Encoding-Permutationen (kanonische JSON).
  • 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-woche Light/Dark × iOS/Android × de-DE.
  • W-04 — Semantics: SemanticsTester.findByLabel('Schicht Montag 20. April, 7 bis 16 Uhr, Lehrer Allee 7') liefert Treffer.
  • I-01Multi-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_reason abgelehnt.
  • 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.
  • 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.

Lokaler 20×-Lauf E-01, E-02 grün vor Merge; DnD-Events sind notorisch flaky, deshalb Fixture mit deterministischem Mouse-Event-Timing.


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).

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.

  • 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-abwesenheitenabsence_requests liefern Urlaubs-Sperre.
    • handwerk/11-qualifikations-kontrollequalifications-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 gegen shift_assignments.shift_start/end, liefert delta_minutes fü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.

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:

  1. Wer plant bei Ihnen heute die Schichten — Chef, Bauleiter, Disponent, Büroleitung? Wie viele Minuten pro Woche?
  2. Haben Sie Betriebsrat? Wie oft gibt es Schichtplan-Widersprüche?
  3. Wie handhaben Sie Urlaub + kurzfristigen Krankheitsfall heute? Wer springt ein?
  4. Gibt es bei Ihnen Azubis? Wie stellen Sie sicher, dass sie nicht falsch eingeplant werden?
  5. Arbeiten Sie mit Leiharbeitern? Wie halten Sie deren Ruhezeit tenant-übergreifend nach?
  6. Wären Sie bereit, 30 Tage Beta-Test gegen 6 Monate Preis-Lock-in?

Build-Sequenz (innerhalb des Features):

  1. Datenmodell + RLS + Multi-Tenant-Test + Plan-Version-Hash-Chain. Das Fundament zuerst — Plan-Versionen sind der Hebel für BR-Audit und Nachvollziehbarkeit.
  2. Backend-API POST /shifts + POST /validate (dry-run) mit ArbZG-Validator-Lib-Reuse. Ohne echte Validierung ist der Editor eine Trugschlussmaschine.
  3. Qualifikations-Gate-Integration mit handwerk/11-Tabelle. Harte Sperre, kein Soft-Warning.
  4. Web-Editor (Flutter Web) mit Drag&Drop + ArbZG-Overlay + Over/Under-Coverage. MVP-primäre Oberfläche.
  5. Mobile Read-Only + Tausch-Antrag (Flutter). Offline-Cache des eigenen Plans via Drift.
  6. BR-Freigabe-Workflow (separate Rolle, Deep-Link, Diff-View).
  7. Compliance-Tests (C-01 Hash, C-03 ArbZG-Property) und Golden-Screenshots.
  8. 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:

  1. plan_br_freigaben — Append-only Status-History pro Plan-Woche (plan_woche_id → plan_versions.id). Jede BR-Entscheidung legt eine neue Row mit neuem status (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-Trigger plan_br_freigaben_chain). RLS: Tenant-Isolation via app.tenant_id, INSERT-Policy erlaubt Rollen betriebsrat|admin|manager|bauleiter, REVOKE UPDATE/DELETE.

  2. plan_swap_requests — Tauschanfragen A↔B mit State-Machine wartet_b → b_akzeptiert → planer_akzeptiert → durchgefuehrt (plus Abbruchpfade b_abgelehnt, abgebrochen). Hash-Chain analog. Bei planer_akzeptiert werden die shift_assignments.user_id innerhalb derselben Drizzle-RLS-Transaction atomar getauscht (optimistic Guards auf alten user_id, TX-Rollback bei Race). Beide Schichten bekommen zusätzlich status='swapped' als Audit-Marker.

  3. mitarbeiter_ical_tokens — 256-bit-random Tokens (crypto.randomBytes(32) → base64url). DB speichert NUR token_hash = sha256(plain) als bytea(32), Unique-Index mit_token_hash_uq. Rotation per POST /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