25 KiB
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,deviceInfound 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_URLmuss beimnext buildgesetzt sein (Kachel-Host in CSPimg-src). - Env (in
.env.example):GEOCODING_PROVIDER=nominatim|none,GEOCODING_URL,GEOCODING_USER_AGENT,MAP_TILE_URL(Defaulthttps://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 überDEFAULT_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}.tssrc/server/services/geo/{config,provider,nominatim,rate-limit,normalize,geocode-site,dispatch}.tssrc/server/services/planning/{access,data,board,schedule,recommend,live,watch,summary,team-settings,time-tracking}.tssrc/server/jobs/processors/{geocode-site,planning-watch}.tssrc/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.tsxsrc/app/api/v1/planning/{board,schedule,recommendations,live}/route.tssrc/components/planning/{planning-board,schedule-dialog,recommendations-panel,live-situation,live-map,live-tone,capacity-form,suggestions-section,conflicts-tile}.tsx|tsmessages/{de,en}/planning.jsonprisma/migrations/20260915120000_planung/migration.sqlscripts/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 gategrün (Vordergrund): prisma generate, tsc, lint (0 Fehler; 3 bestehende Warnungen inscripts/test-betrieb-api.ts,src/app/(app)/layout.tsxCraftviaLogo,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 testgrün: 70/70 (Lane-DBcraftvia_planung,RLS_DATABASE_URLauf dieselbe DB). -
Smoke
BASE=http://localhost:3114 npx tsx scripts/smoke-auth.tsgegen 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 ohnestartLat/startLng/deviceInfo), Dashboard „Planung heute“ + „Konflikte diese Woche“; Teamleiter/planning+/planning/livenur 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 devim Worktree blieb beim zweiten Start beim Kompilieren von/dashboardhä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 gegennext start. -
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.exampleprü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
- L12-Abgleich beim Merge: Feldnamen
TimeEntry.source/approvalStatus,WorkSession.manualsind 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 liestWorkSession.status(en_route|running|paused|ended) – ändert L12 die Semantik,SESSION_STATUSinservices/planning/live.tsnachziehen. - 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“.
- 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.
- Events in Transaktionen (bekannt aus L10b):
scheduleWorkOrderruftassignWorkOrder/updateWorkOrderinnerhalbinTransaction; In-App-Benachrichtigungen rollen mit zurück, eine eingereihte E-Mail kann trotzdem versendet werden. - Auftragsart-Dauer nicht in
/settings/order-typeseditierbar (L2-Seite); Kolonnenkapazität nur im Plantafel-Popup, nicht im Teams-Formular (L1). - 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).
- 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).
- Ungeplant umfasst zusätzlich zugewiesene/angenommene Aufträge ohne Termin. Seitenleiste max. 200, Plantafel max. 3 000 Aufträge je Zeitraum.
- Empfehlungen nur Luftlinie (keine Routen, Spec §35) und nur Kolonnen mit geplantem Auftrag in der Nähe.
- CSP-Erweiterung wird beim Build ausgewertet (
MAP_TILE_URLvornext 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 |