# Mehrere Moderatoren pro Tag ## Ziel Die Morning Meeting Planner App soll pro Tag **N Moderatoren** zuweisen können (statt bislang fix 1). N ist eine globale Einstellung mit gültigem Bereich 1–5, Default **1** (sowohl bei Neuinstallation als auch wenn das Feld in vorhandenen Daten fehlt — Abwärtskompatibilität, Bestandsverhalten unverändert). Der Nutzer hebt den Wert in der UI bewusst auf 2 (oder höher) an. Sichtbarstes Ergebnis: Excel-Export joint mehrere Personen eines Tages mit `" & "` zu `"Vorname1 Nachname1 & Vorname2 Nachname2"`. ## Entscheidungen aus dem Brainstorming | # | Entscheidung | |---|---| | 1 | Globale Einstellung `moderatorsPerDay` (1–5), nicht pro Tag konfigurierbar | | 2 | Wenn weniger Kandidaten verfügbar als `moderatorsPerDay` → Tag bleibt **leer**, Warnung im UI | | 3 | Algorithmus: **Top-N nach Einzelscore**, keine Pairing-Vielfalt | | 4 | UI: **N dynamische Dropdowns** pro Tag, dieselbe Person nicht zweimal wählbar | | 5 | Setting-Control: **oben im Planungs-Reiter**, neben Monat/Jahr | | 6 | Datenmodell: **flach**, mehrere Einträge pro Datum erlaubt | ## Datenmodell ### Neues Feld in `state` ```js state.settings = { moderatorsPerDay: 2 } // gültig: 1, 2, 3, 4, 5 ``` - App-Start ohne `settings` im LocalStorage → Default `{ moderatorsPerDay: 1 }` (Bestandsnutzer behalten Verhalten). - `loadFromJSON` ohne `settings` → ebenfalls `{ moderatorsPerDay: 1 }`. - Wert wird in `localStorage` und in `saveToJSON` mitpersistiert. ### History-Einträge Form **unverändert**: `{ date: "YYYY-MM-DD", employeeId: "...", manual: bool }`. Neu: **mehrere Einträge mit demselben `date` sind erlaubt**. Eindeutigkeit liegt jetzt auf `(date, employeeId)`. ### Geänderte Dedup-Logik in `data.js` `addHistoryEntries(state, entries)`: - Vorher: löschte alle History-Einträge mit Datum aus `entries.map(e => e.date)`. - Nachher: löscht nur Einträge mit demselben `(date, employeeId)`-Tupel wie ein eingefügter Eintrag. `removeHistoryEntry(state, isoDate, employeeId?)`: - Ohne `employeeId`: löscht **alle** Einträge des Tages (Robustheits-Fallback). - Mit `employeeId`: löscht nur den einen Eintrag — Standardfall für Slot-Editierungen und Verlauf-✕. Plan-Generierung verwendet weiterhin den bestehenden Mechanismus (Prefix-Filter entfernt den ganzen Monat aus History, danach werden die neu generierten Einträge per `addHistoryEntries` hinzugefügt). Eine separate `replaceDayEntries`-Funktion ist nicht nötig. ## Algorithmus ### Signatur-Änderung in `algorithm.js` ```js generatePlan(workdays, employees, history, randomFn = Math.random, slotsPerDay = 1) → { assignments, skippedDates } ``` - Alter Rückgabewert war `Array`. Neu: Objekt `{ assignments, skippedDates }`. Es gibt nur einen Aufrufer (`calendar.js`), der wird hart umgestellt. ### Pro Tag 1. Eligible Kandidaten ermitteln (wie bisher: `neverDays`, `onlyDays`, `vacations`, `maxPerMonth`, `minGapDays`). 2. Zusätzlicher Filter: Mitarbeiter, die heute schon in `monthAssignments` stehen, sind nicht erneut wählbar (verhindert Doppelung im selben Tag). 3. Wenn `candidates.length < slotsPerDay`: Tag überspringen, `iso` an `skippedDates` anhängen. 4. Sonst: alle scoren (`scoreCandidate` unverändert), nach Score absteigend sortieren, Top-`slotsPerDay` nehmen. 5. Für jede gewählte Person ein Eintrag `{ date, employeeId, manual: false }` an `monthAssignments` hängen — **bevor** der nächste Tag verarbeitet wird (damit `maxPerMonth`/`minGapDays` korrekt zählen). ### Scoring Unverändert: weniger bisherige Einsätze → höherer Score, `lowPriorityDays` → −5, Jitter ±0.49. ## UI: Planungs-Reiter ### Neue Setting-Steuerung Neben den bestehenden Monat/Jahr-Selects, in derselben `controls`-Row: ``` Pro Tag: [Select 1 | 2 | 3 | 4 | 5] ``` - Initialwert: `state.settings.moderatorsPerDay`. - Bei Änderung: `state.settings.moderatorsPerDay = N`, `saveState()`, `renderGrid()`. - Existierende Einträge im Monat bleiben unverändert; nur die Zahl der sichtbaren Slots pro Zelle ändert sich. ### Pro Werktag-Zelle (Wochentag, kein Feiertag/Betriebsurlaub) - **N Dropdowns** untereinander rendern (`N = state.settings.moderatorsPerDay`). - Jeder Dropdown: `— Leer —` als erste Option, dann alle aktiven Mitarbeiter. - Vorhandene History-Einträge des Tages **alphabetisch nach Mitarbeiternamen sortieren**, dann auf die ersten Slots verteilen. Restliche Slots zeigen `— Leer —`. - Im Slot bereits gewählte Personen werden in den anderen Slots desselben Tages **disabled** (verhindert Doppel-Auswahl). - **Mehr Einträge als Slots** (Setting wurde reduziert): überzählige Einträge bleiben in `state.history` gespeichert, werden im Kalender nicht gezeigt, aber im Excel-Export weiterhin berücksichtigt. ### Slot-Änderungen - Wert auf einen Mitarbeiter setzen: - Falls Slot vorher leer: `addHistoryEntries(state, [{ date, employeeId, manual: true }])`. - Falls Slot vorher belegt war: erst `removeHistoryEntry(state, date, prevEmployeeId)`, dann `addHistoryEntries(...)`. - Wert auf `— Leer —`: `removeHistoryEntry(state, date, prevEmployeeId)`. - `currentPlan` synchron mitführen, `saveState()` nach jeder Änderung. ### Plan-Generierung - Aktuelle Monatseinträge aus History per Prefix-Filter entfernen (`state.history = state.history.filter(h => !h.date.startsWith(prefix))`). - `generatePlan(..., state.settings.moderatorsPerDay)` aufrufen. - `result.assignments` per `addHistoryEntries` in History schreiben. - `result.skippedDates`: nicht-blockierende **Inline-Warn-Box** über dem Kalender: > „2 Tage konnten nicht vollständig besetzt werden: 14.04., 22.04." - Box hat ein dezentes Warn-Styling (gelb/orange), schließbar via ✕. - Bei `skippedDates.length === 0` keine Box rendern. ## Excel-Export ### Format Unverändert: 2-Zeilen-Layout (Namen oben, Datum unten), eine Spalte pro Tag mit ≥ 1 Eintrag. **Zellinhalt der Namen-Zeile:** - 1 Person: `"Vorname Nachname"` - 2 Personen: `"Vorname1 Nachname1 & Vorname2 Nachname2"` - N ≥ 3: `"A & B & C"` (mit `" & "` joinen) - **Reihenfolge:** alphabetisch nach Mitarbeitername — deterministisch, unabhängig von Insertion-Order. ### Verhalten bei Diskrepanz Setting ↔ History Der Export folgt **der History**, nicht dem aktuellen `moderatorsPerDay`. Hat ein Tag 2 Einträge gespeichert und das Setting steht aktuell auf 1, exportiert die App trotzdem beide Personen (joined mit `" & "`). Begründung: User hat das so eingetragen, niemand soll stillschweigend „verschwinden". ### Implementierung in `excel.js` `exportSchedule` - Eingabe-Form unverändert: `assignments: [{ date, employeeId }]` — aber jetzt mehrere Einträge mit demselben `date` möglich. - Vor dem Spalten-Aufbau: nach `date` gruppieren, je Gruppe Namen alphabetisch sortieren, mit `" & "` joinen. - Spaltenreihenfolge: Datum aufsteigend. ## Verlauf-Tab `history.js` zeigt eine Tabelle `Datum | Mitarbeiter | Typ | ✕`. Mit mehreren Personen pro Tag: - **Eine Zeile pro Eintrag** (nicht pro Tag). Bei 2 Personen am 14.04. erscheinen 2 Zeilen. - Sortierung: Datum **absteigend**, sekundär Name **aufsteigend** (stabile Tagesgruppen). - ✕-Button: löscht **nur diese eine Person/diesen einen Eintrag** via `removeHistoryEntry(state, date, employeeId)`. Beispiel-Render: ``` 14.04.2026 | Anna Müller | Auto | ✕ 14.04.2026 | Bernd Schulz | Auto | ✕ 13.04.2026 | Carla Weber | Manuell | ✕ ``` ## Persistenz / Migration - `app.js`: `STORAGE_KEY` bleibt; gespeichertes Objekt wird um `settings` erweitert. - Beim Laden ohne `settings`: Default `{ moderatorsPerDay: 1 }` setzen — keine Verhaltensänderung für bestehende Nutzer, bis sie im UI umstellen. - `data.js` `saveToJSON` / `loadFromJSON` analog erweitern. - Bestehende JSON-Dateien (Pre-Feature) laden ohne Migration — sie haben einfach 1 Eintrag pro Datum, was im neuen Modell ein gültiger Spezialfall ist. ## Tests `tests/algorithm.test.mjs`: **Bestehende Tests** rufen `generatePlan` ohne `slotsPerDay` auf → Default 1 → müssen aber auf neue Rückgabe-Form `{ assignments, skippedDates }` umgestellt werden. **Neue Tests:** 1. `slotsPerDay=2`: 2 verschiedene Personen pro Tag, Top-2 nach Score. 2. Eine Person darf am selben Tag **nicht doppelt** vorkommen. 3. `skippedDates` enthält Tage mit `< slotsPerDay` Kandidaten (z. B. 1 Mitarbeiter mit `onlyDays=[1]` an einem Mo, `slotsPerDay=2`). 4. `maxPerMonth` zählt korrekt, wenn eine Person an mehreren Tagen 2-Slot-Generierung trifft (nicht öfter als `maxPerMonth` zugewiesen). 5. `minGapDays`-Test bleibt grün auch bei `slotsPerDay=2`. ## Edge Cases | Situation | Verhalten | |---|---| | Setting 2→1 reduziert, Tag hat 2 History-Einträge | History bleibt unverändert; Kalender zeigt nur Slot 1 (alphabetisch erste); Excel-Export zeigt weiterhin beide | | Setting 1→2 erhöht, alle Tage haben 1 Eintrag | Slot 2 zeigt `— Leer —`; „Plan generieren" überschreibt den ganzen Monat (Reset-Semantik) | | Mitarbeiter gelöscht, der in History steht | Anzeige `(unbekannt)` wie heute; Eintrag bleibt erhalten, bis manuell entfernt | | Tag hat 0 Einträge | Wie heute: nicht in Excel-Export, alle Slots leer im Kalender | | User wählt in Slot 2 dieselbe Person wie in Slot 1 | Verhindert: Person ist in Slot 2 disabled | ## Änderungs-Übersicht (zur Implementierungsschätzung) | Datei | Änderung | |---|---| | `js/app.js` | `state.settings` ergänzen, persistieren | | `js/data.js` | Dedup nach `(date, employeeId)`, `removeHistoryEntry` mit optionalem `employeeId`, `loadFromJSON`/`saveToJSON` um `settings` | | `js/algorithm.js` | `slotsPerDay`-Param, neuer Rückgabewert, „nicht doppelt am selben Tag"-Filter | | `js/calendar.js` | Setting-Selector, N Dropdowns pro Zelle, Slot-Disable, Skipped-Dates-Warnbox, Plan-Gen-Aufruf mit `slotsPerDay` | | `js/excel.js` | Gruppierung nach Datum, alphabetisches Join mit `" & "` | | `js/history.js` | Sortierung Datum desc, Name asc; ✕ löscht einzelnen Eintrag | | `style.css` | ggf. kleines Spacing für mehrere Dropdowns pro Zelle, Warnbox-Styling | | `tests/algorithm.test.mjs` | Neue Tests, Anpassung an neue Rückgabe-Form |