139 lines
13 KiB
Markdown
139 lines
13 KiB
Markdown
# CLAUDE.md — Assignment Monitor
|
||
|
||
Progressive Web App zur Live-Bewertung von Capgemini-Trainees (Institutees) während Kunden-Assignments.
|
||
|
||
**Vollständige Dokumentation:** `docs/` — bei neuer Session zuerst `docs/HANDOVER.md` lesen.
|
||
|
||
---
|
||
|
||
## Stack
|
||
|
||
- React 18 + TypeScript + Vite (PWA)
|
||
- Tailwind CSS v4 — `@import "tailwindcss"` in CSS, **kein** `tailwind.config.js`; Brandfarbe als `@theme { --color-brand }` in `src/index.css`
|
||
- Dexie.js v5 — IndexedDB-Wrapper, kein Backend
|
||
- React Router v6
|
||
- Capgemini-Brandfarbe: `#0070AD` (Tailwind-Klasse `brand`, JS-Konstante `BRAND_COLOR` in `src/config/constants.ts`)
|
||
|
||
## Projektstruktur (nach Architektur-Refactoring, Stand 2026-07-02)
|
||
|
||
```
|
||
src/
|
||
config/constants.ts ← RATING_OPTIONS, INST_COLORS, AI_CONFIG, RATING_NUM_MAP, BRAND_COLOR, DEFAULT_RATING_TREND_WEIGHT_STEP
|
||
db/
|
||
index.ts ← reiner Re-Export (types + schema + seeds)
|
||
types.ts ← alle Interfaces, FeedbackRating, AppSettings
|
||
schema.ts ← Dexie-Klasse + Migrationsversionen
|
||
seeds/ ← assignmentTypes.ts, feedbackStructure.ts, migrations.ts
|
||
pages/
|
||
meeting/ ← MeetingView.tsx, ConversationTab.tsx, AssessmentTab.tsx, CriterionTagPicker.tsx
|
||
RatingWeightConfig.tsx ← Settings-Tab für Zeitgewichtungs-Faktor (in Configuration.tsx eingebunden)
|
||
...übrige Pages unverändert am alten Ort
|
||
utils/
|
||
ratingTrend.ts ← zeitgewichteter Bewertungs-Vorschlag + Trend-Erkennung (genutzt von Evaluation.tsx & AssignmentFeedbackPage.tsx)
|
||
```
|
||
|
||
Bestehende Imports aus `'../db'` funktionieren weiterhin unverändert (Re-Export).
|
||
|
||
## Entwicklung
|
||
|
||
```bash
|
||
npm run dev # Dev-Server starten
|
||
npx tsc --noEmit # TypeScript-Check — muss vor jeder Abgabe fehlerfrei sein
|
||
```
|
||
|
||
---
|
||
|
||
## Kritische Eigenheiten
|
||
|
||
### Doppelte `criteriaId`-Semantik
|
||
|
||
```
|
||
Assessment.criteriaId → FeedbackCriterionItem.id (Gesamtbewertung)
|
||
ConversationSkillScore.criteriaId → FeedbackCategory.id (Schnellbewertung pro Protokoll-Eintrag)
|
||
ConversationLineTag.criterionItemId → FeedbackCriterionItem.id (Kriterium-Zuordnung einer einzelnen Zeile innerhalb eines ConversationEntry.note-Blocks, seit 2026-07-02 — bewusst anders benannt als "criteriaId" um obige Verwechslungsgefahr nicht fortzuschreiben)
|
||
```
|
||
|
||
### AssignmentType.criteriaIds
|
||
|
||
Referenziert `FeedbackCriterionItem.id[]`. Leer = alle Kriterien anzeigen.
|
||
|
||
Filterlogik (immer so anwenden):
|
||
```typescript
|
||
const itemFilter = aType?.criteriaIds?.length ? new Set(aType.criteriaIds) : null
|
||
const filteredItems = itemFilter ? allItems.filter(i => itemFilter.has(i.id!)) : allItems
|
||
const visibleCatIds = new Set(filteredItems.map(i => i.categoryId))
|
||
const filteredCats = itemFilter ? allCats.filter(c => visibleCatIds.has(c.id!)) : allCats
|
||
```
|
||
|
||
### Rating-Skala
|
||
|
||
```typescript
|
||
type FeedbackRating = 'na' | 'not_client_ready' | 'partially_client_ready' | 'nearly_client_ready' | 'client_ready'
|
||
// Numerisch: na=0, not=1, partially=2, nearly=3, fully=4
|
||
// N/A gilt als "nicht gesetzt" — aus allen Berechnungen ausschließen!
|
||
```
|
||
|
||
Aggregation: gewichteter Durchschnitt → `avg < 1.5` Not · `< 2.5` Partially · `< 3.5` Nearly · `≥ 3.5` Fully
|
||
|
||
**Bug-Klasse, auf die immer prüfen:** Filter der Form `a.score !== null` schließen N/A NICHT aus (N/A ist der String `'na'`, nicht `null`!). Richtig ist immer `a.score !== null && a.score !== 'na'`. Gefunden und gefixt in `Evaluation.tsx` (2026-07-02); dieselbe Prüfung nötig überall, wo `Assessment.score` oder `ConversationSkillScore.score` gemittelt wird.
|
||
|
||
**Zweite Bug-Klasse, auf die immer prüfen (Kriterium-Gewichtung):** `FeedbackCriterionItem.weight` geht seit 2026-07-03 von `0,0` bis `2,0` (Config → Feedback, vorher grobe `×1`–`×5`-Stufen). Jede gewichtete Mittelung MUSS normieren — `Σ(score×weight) / Σ(weight)`, niemals `score×weight` direkt als Endwert interpretieren (würde die Bewertungsstufe verzerren, z.B. Score 3 „Nearly" × Gewicht 0,5 = 1,5, was fälschlich zwischen Not/Partially läge). Zusätzlich: wenn `Σweight === 0` (z.B. alle beteiligten Kriterien einer Kategorie auf Gewicht 0 gesetzt), **nicht** dividieren — `NaN < 1.5/2.5/3.5` ist überall `false` und fällt sonst still auf den letzten Bucket „Fully" durch. Immer `if (wSum === 0) return null` (bzw. `continue`) vor der Division. Umgesetzt in `catMode()` (`AssessmentTab.tsx`) und `buildGroupMeetingScores()` (`ratingTrend.ts`) — beim nächsten neuen gewichteten Durchschnitt genauso prüfen.
|
||
|
||
### Routing
|
||
|
||
```
|
||
/assignment/:id/meeting/:meetingId ← meetingId ist MeetingInstance.id, nicht Assignment-ID
|
||
```
|
||
|
||
MeetingView liest: `const { id, meetingId: meetingIdParam } = useParams()`
|
||
|
||
### Sonstige Fallstricke
|
||
|
||
- `MeetingInstance.generalNotes` kann `undefined` sein → immer `?? ''` als Fallback
|
||
- `PhaseKey` ist `string`, kein Union-Typ
|
||
- Tailwind v4: Brandfarbe ist `brand` (Tailwind-Klasse, via `@theme` in `index.css`) — **kein** `bg-[#0070AD]` mehr neu schreiben
|
||
- Auswertungslogik greift auf **zwei komplett unabhängige Bewertungs-Strukturen** zu, die nicht automatisch synchron sind:
|
||
- `Assessment` (Tabelle `assessments`) — pro Meeting erfasste Kriterium-Bewertungen, Basis für `/evaluation`
|
||
- `FeedbackCategoryRating` — ausschließlich manuell auf `AssignmentFeedbackPage` gesetzt, unabhängig von Meeting-Daten, Basis für das finale Abschluss-Feedback
|
||
- Seit 2026-07-02: `src/utils/ratingTrend.ts` berechnet aus den `Assessment`-Daten einen **zeitgewichteten Vorschlag** pro Kategorie (spätere Meetings zählen stärker, Gewichtungsfaktor konfigurierbar unter Config → Gewichtung) inkl. Trend-Erkennung (Verbesserung/Verschlechterung). Der Vorschlag wird auf `AssignmentFeedbackPage` als Badge angezeigt und bleibt frei überschreibbar — die zwei Strukturen bleiben bewusst getrennt.
|
||
- Meeting-Filter bei Aggregationen über mehrere Meetings: immer `!m.deletedAt && m.status === 'done'` — sonst fließen Papierkorb-/unfertige Meetings mit ein
|
||
- **Weichmacher/Füllwörter in Formulierungen** (z.B. "ein bisschen") werden bewusst **nicht** über eine eigene Text-Erkennungslogik behandelt — stattdessen legt man dafür ein eigenes, niedrig gewichtetes Kriterium an (z.B. "Sprachliche Präzision", Gewicht < 1) und taggt/notiert entsprechende Zeilen ganz normal (meist mit `(!)`). Die bestehende Tagging- + Notation-Vorschlag-Mechanik deckt das automatisch ab.
|
||
- Der `äh`-Zähler (`ConversationEntry.fillerCount`) bleibt bewusst **rein informativ** — keine automatische Schwellenwert-Bewertung. Würde eine eigene Design-Entscheidung brauchen (ab wann gilt die Zahl als "zu hoch"), aktuell reicht die reine Anzeige als Grundlage für die manuelle Bewertung.
|
||
- `ConversationEntry` entspricht weiterhin **einem ganzen Sprecher-Turn** (ein zusammenhängender, mehrzeiliger Text-Block bis "✓ Schließen") — bewusst so belassen, ein Zwischenstand mit "ein Entry pro Zeile" wurde am 2026-07-02 wieder verworfen, weil er die Lesbarkeit des Protokolls zerstört hat. Kriterium-Zuordnung passiert stattdessen **pro Zeile innerhalb** eines Entry über `ConversationEntry.lineTags` (Matching per exaktem Zeilentext, nicht per Index — robust gegenüber nachträglichem Einfügen/Löschen von Zeilen). Zeilen splitten immer über `splitNoteLines()` in `src/utils/notationParser.tsx`, nicht erneut `note.split('\n')` inline schreiben (sonst laufen Trim-/Filter-Verhalten auseinander).
|
||
|
||
---
|
||
|
||
## Stil-Regeln
|
||
|
||
- **Keine Kommentare** außer für nicht-offensichtliche Invarianten (kein „Was", nur „Warum")
|
||
- **Keine Auto-Resize-Textarea via `rows={1}` + JS** — stattdessen View/Edit-Toggle oder `fieldSizing: 'content'` als Inline-Style
|
||
- **`RATING_OPTIONS`-Array** nicht neu definieren — immer aus dem bestehenden Array in der jeweiligen Datei verwenden
|
||
- **Kein `useEffect` für DB-Calls** außer bei initialem Load — lieber direkte `async`-Funktionen
|
||
|
||
---
|
||
|
||
## Offene Punkte (priorisiert)
|
||
|
||
| Prio | Aufgabe | Datei |
|
||
|---|---|---|
|
||
| H | N/A-Aggregationsbug fixen (siehe Bug-Klasse oben) | `src/pages/AssignmentFeedbackPage.tsx`, `src/pages/FeedbackDraftPage.tsx` |
|
||
| H | **KI-Generierung derzeit funktionsunfähig**: `generateWithAi()` schlägt beim Ausführen fehl, vermutlich blockiert das Capgemini-Netzwerk/Firewall den Call zu `openrouter.ai`. Root Cause noch nicht untersucht — ggf. mit internem IT-Support klären. | `src/pages/AssignmentFeedbackPage.tsx` |
|
||
| H | **Größeres Vorhaben (noch nicht gescoped):** Mehrstufiges, pro Dimension konfigurierbares Prompt-System mit Spezialwissen je Dimension + Platzhaltersystem — Ablösung des aktuellen Einzel-Prompts in `buildAiPrompt`. Braucht eigene Design-Session. | `src/pages/AssignmentFeedbackPage.tsx` |
|
||
| M | `FeedbackDraftPage.tsx` auf neue Datenstruktur migrieren (nutzt noch `categories`/`criteria`) | `src/pages/FeedbackDraftPage.tsx` |
|
||
| M | `db/queries/`-Layer einführen (Vorbereitung SQLite-Migration, DB-Calls aus Komponenten extrahieren) | `src/db/queries/` (neu) |
|
||
| M | **Selbstlernendes Tagging (Idee, noch nicht gescoped):** Zeile-zu-Kriterium-Zuordnungen sammeln und daraus beim Tippen automatische Tag-Vorschläge ableiten (ähnliche Formulierungen → gleiches Kriterium). Braucht eigene Design-Session (Ähnlichkeits-/Lernmechanik, Datenbasis, UI). | `src/pages/meeting/CriterionTagPicker.tsx` |
|
||
| N | Dashboard: Assignment-Nummer anzeigen | `src/pages/Dashboard.tsx` |
|
||
| N | Toter Code in FeedbackStructureConfig entfernen | `src/pages/FeedbackStructureConfig.tsx` |
|
||
| L | SQLite WASM + OPFS Migration (kein festes Datum) | `src/db/schema.ts` |
|
||
|
||
**Erledigt (2026-07-02):** Konfiguration zentralisiert (`config/constants.ts`), `db/index.ts` in types/schema/seeds aufgeteilt, `MeetingView.tsx` in 3 Dateien aufgeteilt (`pages/meeting/`), `Evaluation.tsx` auf `feedbackCriterionItems` migriert + nach Assignment gruppiert + Papierkorb/Status-Filter + N/A-Bug gefixt, `zustand` entfernt (war ungenutzt). Zeitgewichteter Bewertungs-Vorschlag mit Trend-Erkennung umgesetzt (`src/utils/ratingTrend.ts`, neue Tabelle `appSettings`, neuer Config-Tab „Gewichtung“, Vorschlags-Badge + Übernehmen/Trend-in-Text auf `AssignmentFeedbackPage`, `buildAiPrompt` um Trend-Abschnitt erweitert und Legacy-`db.criteria`-Bug dabei gefixt). Kriterium-Zuordnung für Freitext-Notizen umgesetzt: `ConversationEntry.lineTags` (Zeilentext → Kriterium, nach einem verworfenen Zwischenstand mit "ein Entry pro Zeile" — siehe Sonstige Fallstricke), `CriterionTagPicker.tsx` (Chip+Popover mit Suche, unverändert seit erster Version) wird pro Zeile direkt an der Zeile via `NotationText`s neuer optionaler `renderLineAddon`-Prop gerendert; Zuordnung passiert bewusst nur nachträglich (nicht live während des Tippens), um den Schreibfluss nicht zu stören. `buildAiPrompt` führt Assessment-Notizen und getaggte Zeilen jetzt pro Kriterium zusammen, unzugeordnete Zeilen bleiben im allgemeinen Protokoll-Block. **(2026-07-03)** Vier Folge-Fixes: `meetingExport.ts` löste Kriterien über die Legacy-Tabelle `db.criteria` auf (identischer Bug wie der frühere `buildAiPrompt`-Fix) — dadurch war die Gesamtbewertung im Export faktisch leer; jetzt auf `feedbackCriterionItems` umgestellt, Markdown-Export zeigt zusätzlich das Kriterium pro getaggter Protokollzeile. `dbBackup.ts`s `TABLE_NAMES` enthielt `appSettings` nicht — Voll-Backups verloren die Gewichtungs-Config stillschweigend; jetzt ergänzt samt Kommentar, dass neue Tabellen dort manuell nachgezogen werden müssen. Notation → Bewertungs-Vorschlag: `detectNotationRating()` in `notationParser.tsx` (`!`→Not, `(!)`→Partially, `(+)`→Nearly, `+`→Fully) kombiniert mit `lineTags` liefert in `AssessmentTab.tsx` einen "Vorschlag aus Protokoll"-Badge pro Kriterium-Item, unabhängig vom bestehenden "Aus Protokoll"-Button (andere Datenquelle). `AssignmentCreate.tsx`: Assignment-Nummer ist jetzt Pflichtfeld (nur auf Formular-Ebene, Typ bleibt optional wegen Altdaten). Kriterium-Gewichtung auf `0,0–2,0` in `0,1`-Schritten verfeinert (`FeedbackStructureConfig.tsx`, vorher `×1`–`×5`), dabei einen latenten Division-durch-Null-Bug gefixt, der durch das neu erlaubte Gewicht `0` real wurde (`catMode()` in `AssessmentTab.tsx`, `buildGroupMeetingScores()` in `ratingTrend.ts` — siehe zweite Bug-Klasse oben). Sprecher eines bereits erfassten Protokoll-Beitrags kann nachträglich geändert werden (`ConversationTab.tsx`, im Bearbeiten-Modus neben Füllwörtern — ändert sofort `ConversationEntry.instituteeId`, kein separater Speichern-Schritt nötig).
|
||
|
||
---
|
||
|
||
## Datenschutz
|
||
|
||
- Alle Daten lokal (IndexedDB) — **kein Public Cloud Upload**
|
||
- GDPR-relevant: Institutee-Namen, Kundennamen, Gesprächsnotizen
|
||
- KI-API-Key in `localStorage` — nie loggen oder ausgeben
|
||
- Backup-JSON enthält alle personenbezogenen Daten → vertraulich behandeln
|