Files
craftvia/docs/craftvia/lanes/planung.md
T
msolarczekandClaude Opus 5 1e1c154a8a Plantafel: Beispielwoche relativ zu heute; Live-Lage mit Kachelansicht
Neues Skript scripts/planning-demo.ts plant über createWorkOrder + scheduleWorkOrder eine
Woche für beide Kolonnen (Auslastung, Überbuchung, Überschneidung, Mehrtagesauftrag,
ungeplante Aufträge); die Demo-Termine aus dem Seed liegen relativ zum Seed-Tag und wandern
sonst aus dem Standardzeitraum.

Live-Lage: Umschalter Karte · Kacheln · Liste, Kachelansicht mit Status, Auftrag und Ort;
LiveMap meldet nicht erreichbare Kartenkacheln und die Ansicht wechselt automatisch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 09:44:24 +02:00

25 KiB
Raw Blame History

Lane L13 – Planung (lane/planung)

Stand: 2026-09-15 · Basis d3bc7f2 (feature/craftvia-mvp, L1–L11 integriert) · Spec §11, §21, §35/§36, §44 · Brandbook §12

Menüpunkt „Planung“ mit Plantafel (Standard: heute + nächste 4 Werktage) und Live-Lage (ohne GPS), Kolonnenkapazität, Konflikte, Verzugswarnungen, „früher fertig“ mit Vorzieh-Vorschlägen, Einsatz-Empfehlungen nach Luftlinie + Terminlage, Geocoding über OpenStreetMap Nominatim. Alles sind Hinweise und Vorschläge – es wird nie automatisch umgeplant.

1. Umfang / umgesetzte Nutzerentscheidungen

Punkt Umsetzung
Teams = Kolonnen Ein Team fährt und arbeitet gemeinsam (2+ Personen). Kapazität = Team.dailyCapacityMinutes (Kolonnen-Arbeitstag, Default 480) an Team.workingDays (Bitmaske Mo=1 … So=64, Default Mo–Fr) – nicht Personenstunden; ohne aktive Mitglieder 0. Auftragsdauer ist Kolonnenzeit. Auslastung = Summe geplanter Kolonnendauern / Kolonnenkapazität. Popup „Teamkapazität“ auf der Plantafel (team:manage, Audit).
Dauer WorkOrder.plannedDurationMinutes → Beginn/Ende am selben Tag (mit Uhrzeit) → OrderType.defaultDurationMinutes → 120 min. Mehrtägig: explizite Dauer gleichmäßig verteilt, sonst je Tag Auftragsart-Standard bzw. 480 min. Ende exakt um Mitternacht zählt zum Vortag.
Konflikte overbooked (Minuten über Kapazität), overlap (zwei Aufträge derselben Kolonne mit Uhrzeit überlappen), assignee_double_booked (Person in zwei Kolonnen/Aufträgen gleichzeitig; in beiden Zeilen sichtbar), outside_working_days; Hinweis crew_incomplete (an Arbeitstagen < 2 aktive Mitglieder; severity: hint, gelb mit Info-Icon, zählt nicht als Konflikt).
Menü „Planung“ Top-Level direkt unter „Dashboard“ mit Unterpunkten „Plantafel“ (Standard, /planning) und „Live-Lage“ (/planning/live); sichtbar für work_order:read_all und Teamleiter (report:approve_team).
Plantafel /planning Standard „Heute + nächste 4 Werktage“ (5 Spalten, erste Spalte „Heute“ hervorgehoben), Kolonnen als Zeilen mit Name, Mitglieder-Kurzliste und Kolonnenkapazität; je Zelle Auftragskarten (Nummer, Kunde, Ort, Uhrzeit, Dauer, Statusgruppe mit Icon, Priorität, Notdienst), Auslastungsbalken („6 h / 8 h · 75 %“), Konflikt-/Hinweis-Liste, Verzugs- und Gefährdungs-Badges; heutige Spalte zusätzlich Live-Status je Kolonne (Unterwegs/In Arbeit/Pause/frei + aktive Personen, Verzug, „früher fertig“). Umschalter Heute (Stundenraster 6–20 Uhr, Kolonnen als parallele Spalten, Zeit vertikal, überlappende Aufträge nebeneinander) · 5 Tage · Woche · Nächste Woche; vor/zurück/heute. Filter Team/Auftragsart/Priorität (wirken auf Karten, nicht auf die Auslastung). Seitenleiste „Ungeplante Aufträge“ mit „Vorschläge“. Panel „Früher fertig“ mit „Vorziehen“. Unter 1024 px Hinweis + einfache Liste.
Drag & Drop @dnd-kit/core (Pointer- und Keyboard-Sensor am Griff): Ablegen auf Tag (Uhrzeit bleibt bzw. 08:00) oder Stunde (Heute-Ansicht) → Bestätigungs-Popover (Team, Datum, Beginn, „ohne Uhrzeit“, Dauer vorbelegt) → POST /api/v1/planning/schedule. Tastatur-Alternative: „Einplanen …“/„Verschieben …“ je Karte öffnet dasselbe Popover (Fokus hinein, Escape schließt, Fokus zurück). Ergebnis in role="status" inkl. Konfliktanzahl; Versionskonflikt → Meldung + Neuladen.
scheduleWorkOrder inTransaction: baseVersion Pflicht → conflict „Auftrag wurde zwischenzeitlich geändert“; nur Status draft/review_required/planned/assigned/accepted; L2 assignWorkOrder (Teamwechsel/Erstzuweisung, Event work_order.assigned; Einzelzuweisungen bleiben nur als aktive Mitglieder des Zielteams) + L2 updateWorkOrder (Beginn/Ende) + Dauer (writeWithVersion); Audit „planning“ (before/after); Länge bleibt beim Verschieben ohne neues Ende erhalten; Antwort mit Konflikten des Zieltages. Rechte work_order:assign + work_order:write.
Live-Lage ohne GPS /planning/live Status aus der aktiven WorkSession je Person (en_route → Unterwegs, running → In Arbeit, paused → Pause, keine → frei), „seit“ aus dem offenen Zeitabschnitt. Ein Marker je Kolonne am Objekt des laufenden Auftrags (Auftrag des aktivsten Mitglieds), Popup/Liste mit Mitgliedern und deren individuellem Status (inkl. abweichendem Auftrag); Monteure ohne Team einzeln. Farbe + inline-SVG-Icon + Text, roter Ring bei Verzug. Karte (Leaflet, OSM-Kacheln, Namensnennung) + Liste, mobil Tabs; Filter Team/Status; Polling 30 s + „Zuletzt aktualisiert“; „Heute ohne Beginn“; „Früher fertig“ mit Vorzieh-Links. Hinweis „Standort = Einsatzort des laufenden Auftrags, keine GPS-Ortung.“
Früher fertig getFreedCapacity(ctx, { date }): Aufträge des Tages mit Uhrzeit, Status technically_completed/signature_pending/in_review/released_for_billing/billed, Ende der letzten (Uhr-)Session < geplanter Beginn + Dauer (≥ 15 min) → freie Minuten je Kolonne (früher fertig gesamt, ab jetzt verfügbar), tatsächliche Dauer aus freigegebenen Zeiten. Vorschläge (nur heute): spätere Aufträge derselben Kolonne heute/nächste 5 Tage, die in die freie Zeit passen („Vorziehen“), und ungeplante Aufträge im Umkreis 15 km (Luftlinie). Badge „Team Nord · 2 h früher fertig“ in Live-Lage und Plantafel; „Vorziehen“ öffnet das Einplan-Popover vorbelegt.
Verzugswarnungen evaluateDelays: erfasste Kolonnen-Arbeitszeit des laufenden Auftrags = zeitliche Vereinigung der work-Segmente aller Sessions des Auftrags (nicht über Personen summiert) vs. geplante Dauer: ≥ 80 % → „Verzug droht“, ≥ 100 % → „Dauer überschritten“ (Live-Lage Marker/Liste, Plantafel Karte, Text + Icon). Folgeauftrag gefährdet: nächster geplanter Auftrag derselben Kolonne heute; Restzeit (≥ 0) + Fahrzeit (Luftlinie × 1,3 / 50 km/h, mind. 10 min) → Beginn (mit Uhrzeit) überschritten oder Überziehung + Fahrzeit sprengt die Tageskapazität → Karte „gefährdet durch Verzug A-00041“.
Meldungen Job planning-watch (BullMQ Job-Scheduler alle 5 min, alle Mandanten): planning.overrun (dedupe je Auftrag, erneut je weitere +50 %), planning.followup_at_risk (je Folgeauftrag/Tag), planning.capacity_freed (je Kolonne/Tag/Auftrag) → nur In-App an Backoffice (work_order:read_all + report:approve) + Teamleiter der Kolonne. Dedupe-Ledger: append-only Audit-Log (entity = planning_alert, Schlüssel je Mandant). Anzeige wird bei jedem Laden serverseitig berechnet (auch ohne Redis); Events entstehen nur im Job.
Empfehlungen recommendAssignments(ctx, { workOrderId, days = 10, radiusKm = 25 }): Zielobjekt braucht Koordinaten (sonst Hinweis „Adresse nicht verortet“ + Geocoding-Job; not_found eigener Hinweis). Je Arbeitstag und Kolonne: nächster geplanter Auftrag (Luftlinie) + freie Kolonnenkapazität; ohne freie Kapazität ausgeschlossen, zu wenig → „knapp“. Score 0,6 Distanz + 0,2 Kapazität + Datum (Gewicht nach Priorität) − 0,25 knapp; Top 5 mit Text („3,0 km von A-00001 · Team Nord hat am Mo 28.09. noch 4 h frei“) und vorgeschlagenem Beginn. findNearbyUnplanned (5 km). UI in der Seitenleiste und im Auftragsdetail; „Übernehmen“ nur vorbelegt, kein Auto-Speichern.
Geocoding services/geo: GeocodingProvider, nominatim.ts (strukturiert, Timeout 8 s, Accept-Language: de, countrycodes, User-Agent Craftvia/<version> (+APP_BASE_URL) bzw. GEOCODING_USER_AGENT, Limiter 1/s je Prozess), none. Job geocode-site (Concurrency 1 + BullMQ-Limiter 1/s, failed → Retry). Cache Site.latitude/longitude + geocodeQuery + geocodeStatus + geocodedAt; unveränderte Adresse nie erneut; manuelle Koordinaten nie überschrieben. Auslöser Objekt anlegen/Adressänderung/Import-Bestätigung – nur Queue, nie inline. scripts/geocode-backfill.ts. Ohne Netz/Provider: „ohne Ortsangabe“, nie Fehler.
Dashboard Kacheln „Planung heute“ (Kolonnen im Einsatz / gesamt, Konflikte heute, ungeplante Aufträge) und „Konflikte diese Woche“ – additiv.

2. Datenschutz – Live-Lage (verbindliche Entscheidung)

  • Keine Standortdaten des Geräts. Angezeigt wird ausschließlich der Einsatzort des laufenden Auftrags (Objektadresse bzw. deren Koordinaten). WorkSession.startLat/startLng, deviceInfo und Foto-Koordinaten werden nicht selektiert und sind nie Teil einer Antwort (per Test über das serialisierte Ergebnis aller Sichten geprüft).
  • Status und Verzug stammen aus der Zeiterfassung (aktive Uhr-Sessions, getrackte Arbeitssegmente). Kein Tracking, keine Historie der Live-Lage, keine zusätzlich gespeicherten Personendaten; das Dedupe-Ledger enthält nur Auftrags-/Team-IDs und Prozentwerte.
  • Sichtbarkeit: Backoffice sieht alle Kolonnen des Mandanten, Teamleiter nur ihre geleiteten Kolonnen (Aufträge außerhalb des Scopes als „Einsatz außerhalb Ihres Bereichs“), Monteure keinen Zugriff. Verzugs-/Früher-fertig-Meldungen gehen nur an Backoffice und den Teamleiter der Kolonne.
  • Empfehlung vor Produktivbetrieb (Spec §44 „Umgang mit Standortdaten“): Information der Beschäftigten / ggf. Betriebsvereinbarung, da sichtbar ist, wer gerade an welchem Einsatzort arbeitet und ob ein Einsatz länger dauert.
  • Geocoding überträgt nur Objektadressen an den Geocoding-Dienst; Kartenkacheln lädt der Browser vom Kachelserver (IP-Adresse des Nutzers wird übertragen) → Datenschutzhinweis bzw. eigener Kacheldienst.

3. OpenStreetMap-Nutzungsrichtlinien & Produktionsempfehlung

  • Nominatim (https://operations.osmfoundation.org/policies/nominatim/): nur serverseitig (Worker/Skript), max. 1 Anfrage/s (Limiter + BullMQ-Limiter + Concurrency 1), eindeutiger User-Agent mit Kontakt-URL, Ergebnisse am Objekt gecacht, kein Massen-Geocoding im Request, keine Autocomplete-Suche. Limiter gilt je Prozess – bei mehreren Worker-Replikas nur eine mit GEOCODING_PROVIDER=nominatim.
  • Kacheln (https://operations.osmfoundation.org/policies/tiles/): Namensnennung sichtbar, Browser-Referrer, kein Vorabladen; der öffentliche Kachelserver ist nicht für hohe Last gedacht.
  • Produktion: eigener bzw. vertraglich gebundener Geocoding- und Kacheldienst (selbst gehostetes Nominatim/Photon + Tile-Server oder EU-Anbieter mit AVV). Umschalten per GEOCODING_URL/GeocodingProvider, MAP_TILE_URL + MAP_ATTRIBUTION. MAP_TILE_URL muss beim next build gesetzt sein (Kachel-Host in CSP img-src).
  • Env (in .env.example): GEOCODING_PROVIDER=nominatim|none, GEOCODING_URL, GEOCODING_USER_AGENT, MAP_TILE_URL (Default https://tile.openstreetmap.org/{z}/{x}/{y}.png), MAP_ATTRIBUTION (Default „© OpenStreetMap-Mitwirkende“).

4. Datenmodell – Migration 20260915120000_planung

Nur Spalten (RLS unverändert, keine neue Tabelle, keine TENANT_MODELS-/PII-Änderung):

  • Team.dailyCapacityMinutes Int @default(480) (Kolonnen-Arbeitstag), Team.workingDays Int @default(31).
  • OrderType.defaultDurationMinutes Int? + Backfill der Standardarten (Montage 480, Reparatur 180, Wartung 120, Störung 120, Notdienst 120, Besichtigung 60, Abnahme 60, Nacharbeit 120); neue Mandanten über DEFAULT_ORDER_TYPES.
  • WorkOrder.plannedDurationMinutes Int?.
  • Site.geocodedAt DateTime?, Site.geocodeStatus String? (ok|not_found|failed|skipped), Site.geocodeQuery String?.

Zeiterfassung (L12): keine Schemaänderung durch L13. services/planning/time-tracking.ts nutzt TimeEntry.source = tracked (Live/Verzug), TimeEntry.approvalStatus = approved (tatsächliche Dauer abgeschlossener Aufträge) und WorkSession.manual = false – die Filter schalten sich automatisch ein, sobald der generierte Prisma-Client die L12-Felder kennt (vorher wirkungslos, damit die Lane gegen beide Schemata kompiliert). WorkSessions/TimeEntries werden nur gelesen.

5. Dateien

Neu (Lane-Pfade):

  • src/lib/geo/distance.ts, src/lib/planning/{days,capacity,text}.ts
  • src/server/services/geo/{config,provider,nominatim,rate-limit,normalize,geocode-site,dispatch}.ts
  • src/server/services/planning/{access,data,board,schedule,recommend,live,watch,summary,team-settings,time-tracking}.ts
  • src/server/jobs/processors/{geocode-site,planning-watch}.ts
  • src/server/actions/work_orders/planning.ts (Kolonnenkapazität, moduleGuard("work_orders") + guard("team:manage"))
  • src/app/(app)/planning/{layout,page}.tsx, src/app/(app)/planning/live/page.tsx
  • src/app/api/v1/planning/{board,schedule,recommendations,live}/route.ts
  • src/components/planning/{planning-board,schedule-dialog,recommendations-panel,live-situation,live-map,live-tone,capacity-form,suggestions-section,conflicts-tile}.tsx|ts
  • messages/{de,en}/planning.json
  • prisma/migrations/20260915120000_planung/migration.sql
  • scripts/test-planung-{core,recommend,live,geocode,watch}.ts, scripts/geocode-backfill.ts

Eingriffe außerhalb planning/geo (minimal, additiv):

Datei Änderung
prisma/schema.prisma 7 Spalten (s. §4)
src/lib/work-orders/defaults.ts defaultDurationMinutes je Standard-Auftragsart
src/server/services/sites/sites.ts je 1 Zeile nach Anlage/Adressänderung requestSiteGeocoding (+ 2 Importe)
src/server/services/imports/confirm.ts 1 Zeile nach Bestätigung mit neuem Objekt (+ 1 Import)
src/server/jobs/queues.ts Queue-Namen geocode-site, planning-watch + Job-Scheduler „alle 5 min“ in scheduleRecurringJobs
src/server/jobs/processors/index.ts 2 Registrierungen
scripts/craftvia-worker.ts Concurrency 1 + Limiter 1/s für geocode-site
src/lib/events.ts Event-Typen planning.capacity_freed, planning.overrun, planning.followup_at_risk
src/server/services/notifications/recipients.ts Regel für die 3 Events (Backoffice + Teamleiter, mailToUsers: false) + 3 Facts
src/server/services/notifications/handle-event.ts Textvariablen team, minutes, percent, blocker
messages/{de,en}/notifications.json Typ-Labels + Texte der 3 Events, Audit-Objektart planning_alert
next.config.ts CSP img-src um Kachel-Host(s) aus MAP_TILE_URL (Default https://tile.openstreetmap.org https://*.tile.openstreetmap.org)
src/lib/nav.ts, messages/{de,en}/nav.json „Planung“ + Unterpunkte „Plantafel“/„Live-Lage“; NavItem.sub/exact (optional)
src/components/nav-link.tsx optionales exact (Unterpunkt nicht auf Unterpfaden aktiv)
src/app/(app)/layout.tsx Einrückung für sub, eindeutiger key, exact durchgereicht (Hauptnavigation)
src/app/(app)/dashboard/page.tsx Kacheln „Planung heute“ + „Konflikte diese Woche“ (1 Import + 1 Zeile)
src/app/(app)/work-orders/[id]/page.tsx Abschnitt „Einsatz-Vorschläge“ auf der Übersicht (2 Importe + 1 Zeile)
src/lib/api/openapi.ts, docs/craftvia/API.md 4 Planungs-Endpunkte + Tag „Planung“
scripts/smoke-auth.ts Planungsseiten/-API je Rolle
.env.example, docs/craftvia/ABNAHME.md Env-Block; §3 Kalender/Live-Lage/Verzug/Empfehlungen, §4 Standortdaten
package.json, package-lock.json neue Abhängigkeiten (s. §7)

Nicht angefasst: services/field/**, components/field/**, app/(field)/m/**, TimeEntry/WorkSession-Schema, Zeit-Freigaben, time.*-Events (L12), src/server/rbac.ts (keine neuen Rechte).

6. Tests

Skript ✓ Inhalt
test-planung-core 92 Haversine/Formatierung; Tage (Bitmaske, Zeitumstellung, heute + nächste Werktage, Werktage vor/zurück); Dauerregeln inkl. mehrtägig/Mitternacht; Kolonnenkapazität (aktive Mitglieder gültig ab/bis/inaktiv/doppelt, Kapazität nicht × Personen, 0 ohne Mitglieder, Wochenende); Auslastung; Konflikte rein + DB (overbooked 10 h bei 225 %, overlap, anschließend ≠ overlap, assignee_double_booked in beiden Zeilen, outside_working_days); crew_incomplete als Hinweis (nicht im Konfliktzähler, färbt keine Karte); Vereinigung von Arbeitssegmenten, Fahrzeitschätzung (50 km → 78 min, min. 10), Meldestufen 100/150/200 %; Mitgliedschaft „gültig bis“; Kolonnenkapazität setzen (Wirkung, Audit, Teamleiter forbidden, Mandant B not_found, invalid); Filter; Rollen (Teamleiter nur eigene Kolonne lesend, Monteur forbidden) und Mandantentrennung; Zeitraum-Validierung; Dashboard-Zähler; scheduleWorkOrder: Zuweisung + Termin + Dauer, Version, Konflikte im Ergebnis, Event work_order.assigned, Audit, Länge beim Verschieben, Versionskonflikt mit Klartext, atomar (Zuweisung, Statushistorie, Audit zurückgerollt), Rechte/Scope/Mandant B, ungültiges Datum, laufender Auftrag
test-planung-recommend 27 nächste Kolonne mit freier Kolonnenzeit (3 km, 4 h frei, Text, Beginn nach letztem Auftrag), Kolonne ohne Kapazität trotz größerer Nähe ausgeschlossen, außerhalb Radius ausgeschlossen, nächster Kandidat mit Kapazität gewinnt, „knapp“, Top 5, englischer Text, Radius-Hinweis; ohne Koordinaten → Hinweis, not_found, ohne Objekt; nahe ungeplante Aufträge; Rollen; Mandantentrennung (identische Koordinaten in B nie in A-Vorschlägen, B → not_found)
test-planung-live 26 Status je Person aus Session, „seit“, beendete Session zählt nicht, Standort = Objekt, ohne Ortsangabe, heute noch geplant, „Heute ohne Beginn“; ein Eintrag je Kolonne mit individuellen Mitgliederstatus und abweichendem Auftrag, Kolonne Süd Pause, Monteure gehören zur Kolonne; keine Geräte-Koordinaten/Geräteinfo im Ergebnis; Filter; Teamleiter nur eigene Kolonne; Monteur forbidden; Mandant B
test-planung-watch 41 Job/Queue registriert, L12-Adapter je Schema; Kolonnenzeit vereinigt (100 statt 140 min), 50 % ok, 83 % „Verzug droht“, Folgeauftrag rechtzeitig (10:31 < 10:45) bzw. gefährdet (10:51 > 10:45, Fahrzeit 31 min für 20 km); Plantafel-Karten (Verzug, Dauer überschritten, gefährdet), Live-Lage Kolonne mit Verzug; Meldungen: keine bei 83 %, planning.overrun bei 108 %, Dedupe, erneut bei 150 %, nicht vor 200 %, followup_at_risk einmal je Tag, Ledger-Einträge; Empfänger Backoffice + Teamleiter der Kolonne, nicht Monteure/andere Teamleiter, nur In-App, Texte; Gefährdung durch Tageskapazität (Folgeauftrag ohne Uhrzeit); früher fertig: 2 h früher, 90 min ab jetzt, Vorziehen (passend ja, zu lang nein), Umkreis (1 km ja, fern nein), vorbelegter Beginn, keine automatische Änderung, Live-Lage-Anzeige, capacity_freed einmal + Dedupe + Text; Rollen (Monteur forbidden, anderer Teamleiter sieht nichts); Mandantentrennung (B: keine Auswertung/Meldungen/Ledger, Dedupe-Schlüssel in B blockiert A nicht)
test-planung-geocode 39 Normalisierung/Änderungserkennung; Nominatim-URL, User-Agent-Default/Override, Provider none, Kachel-Defaults; Provider mit injiziertem fetch (Treffer, Header, Limiter, not_found, HTTP-Fehler); Drosselung mit Fake-Uhr; geocodeSite mit Fake-Provider (ok + Audit, Cache, Neuverortung, not_found, failed → Retry, manuelle Koordinaten, none, unvollständig, Mandant B); Processor; Hook nie inline; Nominatim nie aufgerufen

Gesamt: 225 Prüfungen in 5 neuen Skripten. Gate/RLS/Smoke: §8.

7. Neue Abhängigkeiten (begründet)

Paket Lizenz Größe Begründung
leaflet 1.9.4 BSD-2-Clause ~42 KB min+gz JS, 4 KB CSS Kartenanzeige der Live-Lage (Nutzervorgabe); nur auf /planning/live per next/dynamic (ssr: false); Styles lokal, Marker inline SVG, keine CDN-Assets.
@dnd-kit/core 6.3.1 MIT ~15 KB min+gz Drag & Drop der Plantafel (Nutzervorgabe, war nicht vorhanden); Pointer- und Keyboard-Sensor, Screenreader-Ansagen; nur auf /planning.
@types/leaflet 1.9.22 (dev) MIT – Typen für tsc.

8. Gate, RLS, Smoke

  • npm run gate grün (Vordergrund): prisma generate, tsc, lint (0 Fehler; 3 bestehende Warnungen in scripts/test-betrieb-api.ts, src/app/(app)/layout.tsx CraftviaLogo, src/server/services/field/mime.ts), build inkl. Modul-Guard-Check (33 Action-Dateien), 70/70 Testskripte (davon 5 neu, 225 Prüfungen).

  • RLS_ENFORCED=true npm run test grün: 70/70 (Lane-DB craftvia_planung, RLS_DATABASE_URL auf dieselbe DB).

  • Smoke BASE=http://localhost:3114 npx tsx scripts/smoke-auth.ts gegen den Produktions-Build (next start -p 3114): OK — 111 Prüfungen (alle Rollen). Neu: Backoffice /planning (Standard 5 Tage: data-planning-view="days", data-day-count="5", „Heute“, Team Nord + Team Süd, Seitenleiste), ?view=today (Kolonnen als Spalten, „Ganztägig“), ?view=week (7 Tage), Prefill ?schedule=, /planning/live (Datenschutzhinweis, OSM-Namensnennung, keine Start-Koordinaten), GET /api/v1/planning/{board,live,recommendations} (live ohne startLat/startLng/deviceInfo), Dashboard „Planung heute“ + „Konflikte diese Woche“; Teamleiter /planning + /planning/live nur Team Nord, „Nur Lesezugriff“; Monteur /planning* → 404; Mandant demo2 ohne Demo-Teams/-Monteure, Empfehlungen für Demo-Auftrag → 404. Renderzeiten im Produktions-Build 30–170 ms.

  • Hinweis Dev-Server: next dev im Worktree blieb beim zweiten Start beim Kompilieren von /dashboard hängen (Turbopack wählt wegen mehrerer Lockfiles das Hauptrepo inkl. aller Worktrees als Workspace-Root); der erste Dev-Lauf renderte alle Planungsseiten fehlerfrei, der finale Smoke lief daher gegen next start.

  • Beispieldaten für die Plantafel: npx tsx scripts/planning-demo.ts plant relativ zu heute eine Woche für beide Kolonnen (hohe Auslastung, Überbuchung, Überschneidung, Mehrtagesauftrag, ungeplante Aufträge). Die Demo-Daten aus demo-seed.ts liegen relativ zum Seed-Tag und wandern sonst aus dem Standardzeitraum; --reset entfernt die Beispiele wieder.

  • Nicht durchgeführt: visuelle Prüfung im Browser (Drag & Drop, Karte, Tagesansicht mit parallelen Kolonnen, 1024/768/375 px) – erfordert eine angemeldete Browsersitzung (Passwort-/Token-Eingabe durch den Agenten). Bitte manuell mit backoffice@demo.example prüfen. Demo-Objekte haben ohne Geocoding keine Koordinaten: npx tsx scripts/geocode-backfill.ts --tenant=demo (≈ 1 s je Objekt) oder Koordinaten am Objekt eintragen.

9. Bekannte Lücken / offene Punkte

  1. L12-Abgleich beim Merge: Feldnamen TimeEntry.source/approvalStatus, WorkSession.manual sind gegen die Vorgabe implementiert (Adapter aktiviert sich automatisch); tatsächliche Enum-Werte (tracked, approved) und „eine Uhr je Nutzer“ beim Merge prüfen. Die Live-Lage liest WorkSession.status (en_route|running|paused|ended) – ändert L12 die Semantik, SESSION_STATUS in services/planning/live.ts nachziehen.
  2. Dedupe-Ledger im Audit-Log (append-only, je Mandant): bei parallel laufenden Worker-Replikas sind vereinzelte Doppelmeldungen möglich (kein Unique-Index). Meldungen erscheinen im Audit-Viewer als „Planungshinweis“.
  3. Früher fertig/Verzug nur für Aufträge mit Uhrzeit; „Folgeauftrag gefährdet“ betrachtet nur den nächsten Auftrag derselben Kolonne am selben Tag. Die Kolonnen-Marker zeigen den Auftrag des aktivsten Mitglieds; arbeitet eine Kolonne getrennt an zwei Orten, stehen abweichende Aufträge nur im Popup/in der Liste.
  4. Events in Transaktionen (bekannt aus L10b): scheduleWorkOrder ruft assignWorkOrder/updateWorkOrder innerhalb inTransaction; In-App-Benachrichtigungen rollen mit zurück, eine eingereihte E-Mail kann trotzdem versendet werden.
  5. Auftragsart-Dauer nicht in /settings/order-types editierbar (L2-Seite); Kolonnenkapazität nur im Plantafel-Popup, nicht im Teams-Formular (L1).
  6. Nominatim-Limiter je Prozess (s. §3); Notdienst-Objekte (L8) und Altbestände werden nicht automatisch verortet → Backfill bzw. Adressänderung. Manuelle Koordinaten werden nie ersetzt (zum Neuverorten leeren).
  7. Mehrtägige Aufträge: Kapazitätsanteil heuristisch; keine Überschneidungsprüfung für mehrtägige/ganztägige Aufträge. Heute-Ansicht: Aufträge vor 6 bzw. nach 20 Uhr am Rand. „5 Tage“ blendet Wochenendtage aus (Aufträge am Wochenende in der Wochenansicht).
  8. Ungeplant umfasst zusätzlich zugewiesene/angenommene Aufträge ohne Termin. Seitenleiste max. 200, Plantafel max. 3 000 Aufträge je Zeitraum.
  9. Empfehlungen nur Luftlinie (keine Routen, Spec §35) und nur Kolonnen mit geplantem Auftrag in der Nähe.
  10. CSP-Erweiterung wird beim Build ausgewertet (MAP_TILE_URL vor next build). Dashboard-Kachel „Planung heute“ berechnet Plantafel + Live-Lage je Aufruf (für MVP-Größen unkritisch).

10. Routen

Route Inhalt
/planning Plantafel: Standard 5 Werktage ab heute; ?view=today (Kolonnen als Spalten), ?view=week, &date=YYYY-MM-DD; Filter teamId/orderTypeId/priority; ?capacity=<teamId> Popup; ?schedule=<id>&scheduleTeam=&day=&time= vorbelegtes Einplan-Popover
/planning/live Live-Lage (Kolonnen-Marker, Liste, früher fertig, Filter, Polling)
GET /api/v1/planning/board Plantafel-Daten
POST /api/v1/planning/schedule Einplanen
GET /api/v1/planning/recommendations?workOrderId= Empfehlungen + nahe ungeplante Aufträge
GET /api/v1/planning/live Live-Lage inkl. Kolonnen und „früher fertig“
Job planning-watch alle 5 min: Verzug/Überschreitung, gefährdete Folgeaufträge, früher fertig → Meldungen