Files
Meeting-Mixer/docs/superpowers/specs/2026-04-28-multiple-moderators-per-day-design.md
T
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

10 KiB
Raw Blame History

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 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

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