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>
10 KiB
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
state.settings = { moderatorsPerDay: 2 } // gültig: 1, 2, 3, 4, 5
- App-Start ohne
settingsim LocalStorage → Default{ moderatorsPerDay: 1 }(Bestandsnutzer behalten Verhalten). loadFromJSONohnesettings→ ebenfalls{ moderatorsPerDay: 1 }.- Wert wird in
localStorageund insaveToJSONmitpersistiert.
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
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
- Eligible Kandidaten ermitteln (wie bisher:
neverDays,onlyDays,vacations,maxPerMonth,minGapDays). - Zusätzlicher Filter: Mitarbeiter, die heute schon in
monthAssignmentsstehen, sind nicht erneut wählbar (verhindert Doppelung im selben Tag). - Wenn
candidates.length < slotsPerDay: Tag überspringen,isoanskippedDatesanhängen. - Sonst: alle scoren (
scoreCandidateunverändert), nach Score absteigend sortieren, Top-slotsPerDaynehmen. - Für jede gewählte Person ein Eintrag
{ date, employeeId, manual: false }anmonthAssignmentshängen — bevor der nächste Tag verarbeitet wird (damitmaxPerMonth/minGapDayskorrekt 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.historygespeichert, 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), dannaddHistoryEntries(...).
- Falls Slot vorher leer:
- Wert auf
— Leer —:removeHistoryEntry(state, date, prevEmployeeId). currentPlansynchron 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.assignmentsperaddHistoryEntriesin 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 === 0keine 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 demselbendatemöglich. - Vor dem Spalten-Aufbau: nach
dategruppieren, 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_KEYbleibt; gespeichertes Objekt wird umsettingserweitert.- Beim Laden ohne
settings: Default{ moderatorsPerDay: 1 }setzen — keine Verhaltensänderung für bestehende Nutzer, bis sie im UI umstellen. data.jssaveToJSON/loadFromJSONanalog 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:
slotsPerDay=2: 2 verschiedene Personen pro Tag, Top-2 nach Score.- Eine Person darf am selben Tag nicht doppelt vorkommen.
skippedDatesenthält Tage mit< slotsPerDayKandidaten (z. B. 1 Mitarbeiter mitonlyDays=[1]an einem Mo,slotsPerDay=2).maxPerMonthzählt korrekt, wenn eine Person an mehreren Tagen 2-Slot-Generierung trifft (nicht öfter alsmaxPerMonthzugewiesen).minGapDays-Test bleibt grün auch beislotsPerDay=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 |