Datenmodell — Scheduler-Slice (Stammdaten)
Bezug: docs/fachlich/Lastenheft.md §3–§6 · Scope: nur die Entitäten, die der Constraint-Solver (A-3) als Eingabe konsumiert — die Grundlage für UC-A / Angebotsmodus (Lastenheft §5.2.2).
Dies ist das Stammdaten-Modell = die Eingabe des Solvers. Das operative Modell (Auftrag R7, Teilschritt-Instanz) ist die Ausgabe des Solvers und gehört zum Auftrags-Slice — es wird hier bewusst weggelassen (siehe §4 Weggelassen).
Code:
server/src/model/types.ts(Typen) +server/src/model/seed.ts(lauffähiger Beispiel-Datensatz für den Solver-Spike).
1. Design-Entscheidungen
Drei Regeln aus dem Lastenheft prägen jede Entität:
D-1 — Taxonomien sind Daten, keine Enums (G-1)
Alle admin-editierbaren Aufzählungswerte (Abteilung, Arbeitsplatz-Typ, Ausstattung, Sperre-Grund, Abwesenheit-Typ, Leistung-Kategorie) werden als Zeilen in einer generischen TaxonomyValue-Tabelle modelliert und per id referenziert — nicht als hartkodierte TS-Enums. Admins fügen hinzu, benennen um, deaktivieren (Soft, aktiv: false).
Ausnahme (G-1): logik-tragende Enums bleiben fixe TS-Unions, weil der Solver auf ihnen verzweigt:
| Enum | Werte | Wirkung |
|---|---|---|
Abhaengigkeit (R3a-F07) | nach_vorherigem / parallel / frei | steuert Reihenfolge-Constraint §6.1 |
MitarbeiterStatus (R1-F10) | aktiv / pause / urlaub / krank / inaktiv | urlaub/krank/inaktiv → aus Verfügbarkeit raus |
ArbeitsplatzKonfigStatus (R2-F04) | aktiv / in_wartung / gesperrt | nicht-aktiv → Bay nicht buchbar |
D-2 — Derived State wird nie gespeichert (S-2 / §7.7)
Kein verfügbarkeit-Feld, kein Live-status auf Arbeitsplatz. Abgeleitete Werte entstehen ausschließlich aus Berechnung:
- Mitarbeiter-Verfügbarkeit =
Σ Schicht-Slots − Σ Abwesenheiten(Lastenheft R1-S3, S-5). - Arbeitsplatz-Live-Status =
KonfigStatus + aktive Sperren + Zuweisungen vs. Kapazität(R2 derived).
Damit gilt G-7.7 (Single-Source-of-Truth) by construction — der Folierbucht-1-Bug des Altsystems ist strukturell ausgeschlossen.
D-3 — Einheiten explizit und exakt wie spezifiziert
| Größe | Einheit | Quelle |
|---|---|---|
| Standzeit | Stunden | R3a-F04 |
| Standarddauer Teilschritt | Minuten | R3b-F02 |
| Cool-down / Settle Time | Minuten | R3b-F07 |
| Wechsel-Pönale | Minuten (Default 5) | G-2 |
| Min. Auto-Scheduling-Fenster | Minuten (15) | G-3 |
2. ER-Überblick
3. Entitäten
| Entität | Lastenheft | Rolle im Solver |
|---|---|---|
TaxonomyValue | G-1 | admin-editierbare Aufzählungswerte |
Skill | 4.1 | Backbone (S-4); Match Mitarbeiter↔Teilschritt |
SchichtKatalog | 4.4 | Quelle der Mitarbeiter-Verfügbarkeit |
Mitarbeiter | R1 (Teilmenge) | Ressource „wer"; Skill-Träger |
Arbeitsplatz | R2 | Ressource „wo"; Kapazität + Sperren |
Sperre | R2-S1 | hartes Constraint: Bay blockiert |
Abwesenheit | R1-S3 | reduziert Verfügbarkeit |
Teilschritt | R3b | Arbeitspaket: Dauer, Skill-Bedarf, Bay-Typ, Cool-down |
Leistung | R3a | geordnete Komposition von Teilschritten + Standzeit |
Feld-für-Feld-Definitionen mit // F##-Annotation in server/src/model/types.ts. Jedes Feld trägt seine Lastenheft-ID als Kommentar, damit Code ↔ Spec rückverfolgbar bleibt (agents.md §3).
4. Bewusst weggelassen
Nicht nötig, um die Solver-These zu beweisen — wird nach der Feasibility sauber angedockt:
- Operatives Modell: Auftrag (R7), Teilschritt-Instanz, Reservierungsmodi (§6.3) — das ist die Ausgabe des Solvers, gehört zum Auftrags-Slice.
- Kunde (R5), Fahrzeug (R6) inkl. Mangel — UC-A braucht im Angebotsmodus nur optional einen Kunden.
- Formulare/Protokolle (R4), Attachments (4.10) — UC-C/E.
- Mitarbeiter-Restfelder (E-Mail, Telefon, Eintritts-/Geburtsdatum, Notizen, QR, abgeleitete Kennzahlen).
- Zweigstelle (FM-1), Fahrzeug-Besitzerhistorie (FR-1) — Future.
5. Solver-Spike (A-3) — gebaut ✅
server/src/scheduler/ enthält einen lauffähigen Greedy-Spike, der dieses Modell + seed.ts konsumiert und den Angebotsmodus (§5.2.2) demonstriert:
cd server && npm run spike
| Datei | Inhalt |
|---|---|
time.ts | Zeit-/Intervall-Helfer (Minuten seit Epoch, merge/subtract/covers) |
availability.ts | Verfügbarkeit als derived state (D-2): Schichten−Abwesenheiten, KonfigStatus−Sperren |
schedule.ts | Greedy-Scheduler für einen Auftrag; prüft alle harten Constraints §6.1 + Parkplatz-Kapazität |
angebot.ts | Angebotsmodus-Layer: früheste + späteste Anlieferung für eine Wunsch-Abholung |
multi.ts | Mehrauftrags-Planung (Greedy-Insertion) gegen geteilte Ressourcen |
spike.ts | Runner: alle Szenarien (Angebotsmodus, Nacht-Scheduling, Parkplatz, Mehrauftrag) |
Verifizierte harte Constraints (§6.1): Skill-Match (1 Skill je Schritt) · N Personen je Skill · Bucht-Skill · Schichten−Abwesenheiten · Sperren · Cool-down · Abhängigkeit · Standzeit. Der Seed ist so gebaut, dass jedes Constraint im Ergebnis sichtbar greift (z. B. Keramik wartet auf Sinas Spätschicht; QM weicht auf Folierbucht 2 aus, weil Bucht 1 gesperrt ist).
Angebotsmodus-Ausgabe (§5.2.2): statt einer langen Slot-Liste genau zwei Fenster zu einer Wunsch-Abholung — früheste Anlieferung (ASAP) und späteste Anlieferung, die noch rechtzeitig fertig wird. Die späteste wird durch die Standzeit (R3a-F04) und die Schicht-/Skill-Kette begrenzt.
Parkplatz als Kapazitätsressource (mehrere Typen): Ein Fahrzeug ist von Anlieferung bis Abholung on-site, aber nur während [step.start … step.bayEnd] in einer Bucht. Die Differenz (frühe Anlieferung, Wartezeit zwischen Schritten, späte Abholung) belegt einen Stellplatz. Stellplätze gibt es in mehreren Typen mit getrennter Kapazität (Seed):
| Typ (Arbeitsplatz-Typ, R2-F02) | Pool (freie Bezeichnung, F01) | Kapazität |
|---|---|---|
| Parkplatz Indoor (gesichert) | Halle A – Indoor | 2 |
| Parkplatz Außen (gesichert) | Hof Nord – gesichert | 3 |
| Parkplatz Außen (ungesichert) | Hof Süd – offen | 5 |
scheduleLeistung wählt First-Fit in Präferenz-Reihenfolge (Taxonomie-Reihenfolge, Indoor zuerst) den ersten Typ, der über alle Parkintervalle des Fahrzeugs Kapazität frei hat, prüft gegen bereits belegte Plätze (existingParking, je Typ) und lehnt den Auftrag ab, wenn kein Typ frei ist — so bleibt die Kapazität auch bei zu früher Anlieferung / zu später Abholung in Kontrolle.
Fahrzeug/Auftrag-Restriktion (erlaubte Parkplatz-Typen): Ein Fahrzeug bzw. Auftrag kann auf eine Teilmenge der Parkplatz-Typen beschränkt werden — z. B. Luxusfahrzeug → nur „Parkplatz Indoor (gesichert)". Modelliert als Allow-Liste erlaubteParkplatzTypIds (M:N → Arbeitsplatz-Typ, Parkplatz-Teilmenge); leer/undefiniert = alle erlaubt. Künftiges Feld: primär Fahrzeug (R6) (Luxus ist fahrzeug-intrinsisch), pro Auftrag (R7) überschreibbar. Im Spike fließt es über ScheduleOpts.erlaubteParkplatzTypIds ein.
Bestplatz-Präferenz + Umparken: Die Zuweisung ist gemeinsam für alle Fahrzeuge (neues + bereits parkende, ScheduleOpts.existingParking: ParkedCar[]). Präferenz = bester Pool zuerst (Indoor). Reihenfolge der Zuweisung: am stärksten eingeschränkte Fahrzeuge zuerst (Luxus = nur Indoor), bei Gleichstand bestehende vor dem neuen Auto. Folgen:
- Ein Standard-Auto nimmt den besten freien Platz, ohne andere zu stören.
- Ein ankommendes Luxusauto bekommt seinen Indoor-Platz; ein flexibles Auto wird ggf. umgeparkt (Ergebnis:
ScheduleResult.umparken: UmparkAktion[]), statt das Luxusauto abzulehnen. - Erst wenn auch nach Umparken kein erlaubter Platz frei ist, wird abgelehnt.
Kein Platz wird freigehalten + Folierbucht als Stellplatz: Parkbedarf entsteht nur außerhalb der Bucht. Während ein Fahrzeug in der Folierbucht steht (oft lange, inkl. Cool-down), belegt es keinen Stellplatz, und es wird kein Platz „auf Vorrat"/zum Rangieren reserviert. Zwei Folgen:
- Zwischen-Schritt-Wartezeiten: Das Auto bleibt in der vorherigen Bucht stehen (kein Umparken/Rangieren), sofern diese frei ist (
ScheduleResult.inBayStays). Nur Anlieferung-vor-Arbeitsbeginn und die Wartezeit nach dem letzten Schritt (Standzeit/späte Abholung) erzeugen echten Parkbedarf. - Folierbuchten zählen als Überlauf-Stellplatz: Sind alle dedizierten Parkplätze belegt, dient eine freie Folierbucht als Stellplatz (niedrigste Präferenz), statt den Auftrag abzulehnen.
Nachtrockenzeit/Ruhezeit indoor (Teilschritt.coolDownIndoorPflicht, F07a): Bei bestimmten Schritten (z. B. Keramik-Aushärtung) muss die Trockenzeit (= Cool-down) indoor erfolgen. Der Scheduler plant solche Schritte nur in Indoor-Buchten und lässt das Fahrzeug dort stehen — die anschließende Nachkontrolle soll möglichst in derselben Bucht ohne Umparken erfolgen (der nächste Schritt bevorzugt diese Bucht; ist sie z. B. durch eine Sperre belegt, weicht er aus).
Nacht-Scheduling der Trockenzeit (Soft-Goal §6.2, ScheduleOpts.nachtScheduling, Default an): Bei langem Cool-down wählt der Scheduler den Start so, dass die Trockenzeit in arbeitsfreie Zeit (Pausen/Nachtruhe) fällt, statt während der Arbeitszeit eine Bucht zu blockieren. Bound: der Schritt wird nur so weit nach hinten geschoben, dass sein Cool-down-Ende ≤ Beginn der nächsten Arbeitszeit bleibt — dann startet der Folgeschritt ohnehin nicht später, die Fertigstellung bleibt gleich. Messgröße: ScheduleResult.coolDownInStaffedMin (Trockenzeit, die in Arbeitszeit fällt). Im Seed verschiebt das die Keramik-Aushärtung von 12:00 auf 18:00 (härtet 20:00→08:00 über Nacht): „Trocknung in Arbeitszeit" 6.5 h → 0.5 h, Fertigstellung unverändert.
Arbeitszeit-Kalender = Vereinigung aller aktiven Schichten mit ≥1 aktivem Mitarbeiter (staffedCalendar); Abwesenheiten Einzelner schließen die Werkstatt nicht. Greedy-Grenze (→ A-5): das ist ein lokaler, bound-gesicherter Heuristik-Schritt pro Teilschritt; ein CP-SAT/MIP-Solver würde Trockenzeiten global über mehrere Aufträge gegen Durchsatz und Bucht-Belegung optimieren (inkl. Abwägung „Auto in der Bucht warten lassen vs. ausparken, um die Bucht tagsüber freizugeben").
Indoor-Erkennung (Spike-Konvention): per Label /indoor|folierbucht/i; Überlauf-Stellplätze per /folierbucht/i.
Konventionen / Vereinfachungen im Spike: Parkplatz-Typen werden per Label (/parkplatz/i) erkannt; Stellplätze sind 24/7 verfügbar (keine Schichtbindung); Sperren auf Stellplätzen und Ausstattungs-Anforderungen werden ignoriert. Offen (parken): einzelne Stellplatz-Nummern (= capacity-1-Einträge je Pool) — später ergänzbar, ohne das Modell zu brechen.
5.1 Mehrauftrags-Planung (multi.ts)
scheduleMulti plant mehrere Aufträge per Greedy-Insertion gegen einen geteilten WorkshopState (Bay-/Mitarbeiter-Belegungen + geparkte Fahrzeuge). Aufträge werden nach Priorität sortiert — Eilauftrag (R7-F06) zuerst, dann früheste Wunsch-Abholung — und nacheinander eingefügt; jeder sieht die Belegungen der vorher eingefügten. So werden Ressourcen-Konkurrenz und Durchsatz sichtbar:
- Geteilte harte Constraints über alle Aufträge: dieselbe Bucht/derselbe Mitarbeiter kann nicht doppelt belegt werden; Stellplatz-Kapazität gilt über alle parkenden Fahrzeuge; ein ankommendes Luxusauto verdrängt ggf. ein flexibles Auto eines früheren Auftrags.
- Im Seed-Szenario (3× „Vollfolierung Premium", alle Di-Anlieferung) erzwingen die Engpässe Keramik = nur Sina, PPF = nur Julian, 2 Folierbuchten (+ Marcos Krankheit Di) eine Serialisierung: Eilauftrag fertig Mi, dann Do, dann Fr — ~1 Fahrzeug/Tag. Jede Keramik-Aushärtung landet (Nacht-Scheduling) in einer eigenen Nacht.
Greedy-Grenze (→ A-5): feste Einfüge-Reihenfolge statt globaler Optimierung; ein CP-SAT/MIP-Solver würde Bucht-/Mitarbeiter-Zuteilung gemeinsam über alle Aufträge gegen den Durchsatz optimieren (und z. B. abwägen, welche Aufträge welche knappen Skills wann bekommen).
Bewusst NICHT im Spike (→ v1-Solver A-5, CP-SAT/MIP): die weichen Ziele §6.2 — globale Durchsatz-Maximierung, Wechsel-Pönale (G-2), Lücken-Vermeidung < 15 Min (G-3). Der Greedy nimmt je Schritt/Auftrag das frühest-mögliche Set (mit lokalem Nacht-Scheduling), nicht das global durchsatz-optimale. Das ist die Grenze, die der Spike absteckt: Modell + Greedy tragen §6.1 nachweislich, die globale Optimierung §6.2 braucht den echten Solver.
↩ Zurück zur Doku-Landkarte · Lesepfade · Register (alle OPs)