Files
Meeting-Mixer/docs/superpowers/specs/2026-04-28-multiple-moderators-per-day-design.md
ferdi2goandClaude Opus 4.7 b794581a06 docs: spec for multiple moderators per day
Globale Einstellung moderatorsPerDay (1-5), Top-N-Algorithmus, dynamische
N Slots pro Tag im Kalender, Excel joint Namen mit " & ", Verlauf zeigt
eine Zeile pro Person.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 09:24:42 +00:00

193 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<assignment>`. 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 |