Bündelt die Neuentwicklung in AssigmentMonitorV2 (eigene Ports/DB), die Umstellung auf numerische Bewertungen, Meeting-Checklisten, KI-Tagging und die geplante Feedback-Kaskade — als Basis für Versionsverwaltung in Gitea. Co-authored-by: Cursor <cursoragent@cursor.com>
244 lines
70 KiB
Markdown
244 lines
70 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.
|
||
|
||
**DEV-Isolation:** Dieses Repo (`AssigmentMonitorV2`) ist von Produktiv (`c:\dev\AssigmentMonitor`) getrennt — Ports **5174/4001**, DB `assignment-monitor-v2-dev.sqlite3`. Regeln: `docs/DEV_ISOLATION.md`. Produktiv-Code und -Daten niemals anfassen.
|
||
|
||
---
|
||
|
||
## 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`
|
||
- **Lokaler Node/Express-Server + SQLite** (seit 2026-07-04) — löst Dexie/IndexedDB als primären Datenspeicher ab, siehe eigener Abschnitt unten
|
||
- React Router v6
|
||
- Capgemini-Brandfarbe: `#0070AD` (Tailwind-Klasse `brand`, JS-Konstante `BRAND_COLOR` in `src/config/constants.ts`)
|
||
|
||
## Projektstruktur (Stand 2026-07-04)
|
||
|
||
```
|
||
src/
|
||
config/constants.ts ← RATING_OPTIONS, INST_COLORS, AI_CONFIG, RATING_NUM_MAP, BRAND_COLOR, DEFAULT_RATING_TREND_WEIGHT_STEP
|
||
db/
|
||
index.ts ← Re-Export (types + queries), rpcClient.ts eingebunden
|
||
types.ts ← alle Interfaces, FeedbackRating, AppSettings — von Client UND Server importiert
|
||
rpcClient.ts ← rpc(module, fn, ...args) — POST /api/rpc, einziger Netzwerk-Kontaktpunkt des Clients
|
||
queries/ ← Client-DB-Zugriffsschicht (siehe eigener Abschnitt unten)
|
||
schema.ts, seeds/ ← Dexie/IndexedDB-Legacy-Code, wird nicht mehr gelesen (siehe Datenbank-Abschnitt)
|
||
pages/
|
||
meeting/ ← MeetingView.tsx, ConversationTab.tsx, AssessmentTab.tsx, CriterionTagPicker.tsx
|
||
RatingWeightConfig.tsx ← Settings-Tab für Zeitgewichtungs-Faktor (in Configuration.tsx eingebunden)
|
||
AssignmentTrash.tsx ← Papierkorb-Ansicht für Assignments (`/assignments/trash`), seit 2026-07-13
|
||
AssignmentArchive.tsx ← Archiv-Ansicht für Assignments (`/assignments/archive`), seit 2026-07-13
|
||
MeetingHistory.tsx ← Rein lesendes chronologisches Gesamtprotokoll aller Meetings eines Assignments (`/assignment/:id/history`), seit 2026-07-13
|
||
AiPromptConfig.tsx ← Settings-Tab „KI-Prompts" — ein editierbares Prompt-Template je Dimension, Default-Vorbelegung + Reset (in Configuration.tsx eingebunden, seit 2026-07-07)
|
||
...übrige Pages unverändert am alten Ort
|
||
utils/
|
||
ratingTrend.ts ← zeitgewichteter Bewertungs-Vorschlag + Trend-Erkennung + weightedAverageScore/weightedAverageRating (genutzt von Evaluation.tsx, AssignmentFeedbackPage.tsx, AssessmentTab.tsx, AssignmentDetail.tsx)
|
||
aiPrompt.ts ← Prompt-Engine für die KI-Feedback-Generierung (seit 2026-07-07): Platzhalter-Kontext pro Dimension, Rendering, Antwort-Parsing — siehe eigener Abschnitt „KI-Feedback-Generierung" unten
|
||
pdfExport.tsx ← PDF-Export (seit 2026-07-14, `@react-pdf/renderer`): Assignment-Report (Gesamtprotokoll + Feedback aller Institutees) und Einzel-Feedback-PDF pro Institutee
|
||
server/
|
||
index.ts ← Express-App, ein Endpunkt POST /api/rpc
|
||
database.ts ← node:sqlite-Verbindung (server/data/assignment-monitor.sqlite3), Schema aus tableRegistry.ts
|
||
rpcModules.ts ← Dispatch-Registry { appSettings, consultants, ..., backup } → server/db/queries/*.ts
|
||
db/
|
||
tableRegistry.ts ← EINE Quelle der Wahrheit: alle 21 Tabellen + Spalten + JSON-/Bool-Codecs (treibt Schema UND Backup)
|
||
crud.ts ← generische SQL-CRUD-Bausteine über die Registry
|
||
queries/ ← Server-Pendant zu src/db/queries/*.ts, gleiche Funktionsnamen, echtes SQL statt Dexie
|
||
scripts/verifyBackupRoundtrip.ts ← automatisierter Vollständigkeits-Beweis für Backup-Export/-Import
|
||
```
|
||
|
||
### Datenbank: lokaler Server statt IndexedDB (seit 2026-07-04)
|
||
|
||
Grund: Mehrere Browser (Chrome, Edge, ...) auf demselben Laptop sollen dieselben Daten sehen, unabhängig vom Browser-Cache — als Testschritt vor einem späteren Umzug auf einen Docker-Container auf einem Linux-Server. Browser können keine SQLite-Datei direkt öffnen, daher läuft ein kleiner Node/Express-Server (`server/`, Port 4000) mit **`node:sqlite`** (Node-eingebautes SQLite-Modul, **kein** `better-sqlite3`!). Alle Browser sprechen über `http://localhost:5173` (Vite-Dev-Proxy `/api` → `http://localhost:4000`) denselben Server an.
|
||
|
||
**Abweichung vom ursprünglich gewählten Stack:** `better-sqlite3` scheiterte lokal an einer fehlenden/kaputten Python-Installation (node-gyp-Kompilierung). `node:sqlite` bietet dieselbe synchrone API (`DatabaseSync`, `.prepare().run()/.get()/.all()`), braucht keine native Kompilierung und ist ab dieser Node-Version (25.x) ohne Flag nutzbar (Node meldet es noch als "experimental" — rein informativ, funktioniert stabil). Falls auf einem anderen Rechner/Server `node:sqlite` fehlt (ältere Node-Version), wäre `better-sqlite3` dort nachzuholen, sofern eine funktionierende Build-Toolchain (Python + C++-Compiler) vorhanden ist.
|
||
|
||
**Spalten-Migration bei bestehenden Tabellen (seit 2026-07-07):** `database.ts` legt fehlende Tabellen per `CREATE TABLE IF NOT EXISTS` an — das migriert aber **nie** eine bereits existierende Tabelle. Wird `tableRegistry.ts` um eine neue Spalte einer bereits vorhandenen Tabelle ergänzt (z.B. `suggestedAchievements` auf `feedbackDimensionTexts`), gleicht `database.ts` das zusätzlich per `PRAGMA table_info` ab und zieht fehlende Spalten per `ALTER TABLE ... ADD COLUMN` automatisch nach. Ohne das bleibt die alte Spaltenmenge bestehen und jeder Zugriff auf die neue Spalte scheitert erst zur Laufzeit mit „no column named ..." — genau das ist beim Einführen des KI-Prompt-Systems passiert und wurde dabei gefixt.
|
||
|
||
**RPC statt REST:** Ein einziger Endpunkt `POST /api/rpc` mit Body `{ module, fn, args }` statt ~80 einzelnen Routen — Client (`src/db/rpcClient.ts`) und Server (`server/rpcModules.ts`) sind darüber 1:1 gekoppelt: jede Funktion aus `src/db/queries/*.ts` hat ein gleichnamiges Pendant in `server/db/queries/*.ts`.
|
||
|
||
**Dexie/IndexedDB ist toter Code, aber noch nicht entfernt:** `src/db/schema.ts` und `src/db/seeds/*.ts` bleiben unverändert im Repo liegen, werden aber von nichts mehr gelesen (Seed-Aufrufe in `main.tsx` wurden entfernt). Vollständiges Entfernen ist eine spätere, bewusste Entscheidung.
|
||
|
||
**Backup ist jetzt server-seitig:** `src/utils/dbBackup.ts` ruft `rpc('backup', 'exportAll'/'importAll', ...)` statt direkt gegen Dexie zu laufen. `server/db/queries/backup.ts` iteriert exakt `tableRegistry.ts` — dieselbe Liste, die auch das SQL-Schema erzeugt, damit eine neue Tabelle nicht mehr unbemerkt aus dem Backup fallen kann (das ist genau einmal mit `appSettings` passiert, siehe Erledigt-Historie). `server/scripts/verifyBackupRoundtrip.ts` (`npm run verify-backup -- <pfad>`) beweist automatisiert Zeilenzahl- und Inhaltsgleichheit pro Tabelle nach einem Import→Export-Zyklus.
|
||
|
||
### DB-Zugriffsschicht (`src/db/queries/`, seit 2026-07-04, intern auf RPC umgestellt)
|
||
|
||
Alle Komponenten greifen auf Daten ausschließlich über benannte Funktionen aus `src/db/queries/*.ts` zu (re-exportiert über `'../db'`) — nach außen unverändert seit der ursprünglichen Extraktion, nur die Innenseite ruft jetzt `rpc(...)` statt `db.<table>.*` auf. Ein Modul pro Domäne: `appSettings.ts`, `consultants.ts` (Groups+Consultants), `assignmentTypes.ts` (AssignmentTypes+PhaseTemplates), `assignments.ts`, `feedbackStructure.ts` (Dimensions/Categories/CriterionItems — `filterVisibleFeedbackStructure()` bleibt reine Client-Funktion ohne RPC), `meetings.ts` (MeetingInstances inkl. kanonischem `!deletedAt && status==='done'`-Filter als `listDoneMeetingsChronological()`, Cascade-Deletes, Export-Datenaggregation `getMeetingExportData()`), `conversationEntries.ts`, `assessments.ts`, `assignmentFeedback.ts`.
|
||
|
||
## Entwicklung
|
||
|
||
```bash
|
||
npm run dev:all # Frontend (Vite, :5173) + lokaler Server (Express, :4000) zusammen starten
|
||
npm run dev # Nur Frontend (Server muss separat laufen, sonst laufen alle DB-Zugriffe ins Leere)
|
||
npm run server # Nur den lokalen Server
|
||
npx tsc -b # TypeScript-Check über alle 3 Projekte (App/Node/Server) — muss vor jeder Abgabe fehlerfrei sein
|
||
```
|
||
|
||
**Wichtig:** `npx tsc --noEmit` am Root prüft **nichts** (0 Dateien, `tsc --noEmit --listFiles` bestätigt das) — der Root-`tsconfig.json` hat `"files": []` und nur `references`. Ohne `-b` (Build-Modus) werden referenzierte Projekte nicht ausgewertet. Der bisher in diesem Dokument dokumentierte Workflow `npx tsc --noEmit` war dadurch vermutlich seit Einführung der Project References ein stiller No-Op — **immer `npx tsc -b` verwenden.**
|
||
|
||
---
|
||
|
||
## 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: `filterVisibleFeedbackStructure(allCategories, allItems, criteriaIds)` in `src/db/queries/feedbackStructure.ts` — seit dem `db/queries/`-Refactoring (2026-07-04) die einzige Implementierung, vorher identisch dupliziert in `MeetingView.tsx` und `AssignmentDetail.tsx`. Nicht erneut inline schreiben.
|
||
|
||
### 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. Seit dem `db/queries/`-Refactoring (2026-07-04) **eine** Implementierung: `weightedAverageScore()`/`weightedAverageRating()` in `src/utils/ratingTrend.ts`, genutzt von `buildGroupMeetingScores()`, `catMode()` (`AssessmentTab.tsx`) und der Meeting-Chip-Anzeige in `AssignmentDetail.tsx` (dort fehlte der `Σweight===0`-Guard vorher — beim Konsolidieren mitgefixt). Bei jedem neuen gewichteten Durchschnitt diese Funktion wiederverwenden, nicht neu implementieren.
|
||
|
||
### 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.
|
||
- **Cross-Boundary-Imports von `src/` nach `server/`**: Dateien, die (wie `src/db/types.ts`) sowohl vom Client als auch vom Server importiert werden, müssen frei von Laufzeit-Abhängigkeiten auf Browser-Only-Code sein — auch `import type` reicht nicht als Schutz, wenn die importierte Datei selbst wieder aus dem `'../db'`-Barrel importiert (der zieht Dexie/rpcClient/Seeds mit, siehe `src/config/constants.ts`-Fix vom 2026-07-07: Import auf `'../db/types'` direkt umgestellt). Jede neue Datei dieser Art muss zusätzlich explizit in `tsconfig.server.json`s `include` aufgenommen werden (sonst „File is not listed within the file list of project" bzw. sie wird stillschweigend nicht mitkompiliert) — Muster: `src/db/types.ts`, `src/config/constants.ts`.
|
||
- `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 |
|
||
|---|---|---|
|
||
| M | **Migration der echten Daten aussteht:** Aktuelle IndexedDB-Daten müssen noch einmalig über Config → Backup → Export (alte Version) exportiert und über Config → Backup → Import (neue, serverbasierte Version) in die SQLite-DB eingespielt werden. Siehe Datenbank-Abschnitt oben. | — |
|
||
| L | Dexie/IndexedDB-Legacy-Code vollständig entfernen (`src/db/schema.ts`, `src/db/seeds/`) — aktuell bewusst nur stillgelegt, nicht gelöscht | `src/db/schema.ts`, `src/db/seeds/` |
|
||
| L | Server-seitiges "Seed-if-empty" für Assignment-Typen/Feedback-Struktur (aktuell nur clientseitig vorhanden und nicht mehr wirksam) — nicht dringend, da der Backup-Import denselben Zweck erfüllt | `server/db/queries/` (neu) |
|
||
| L | SQLite WASM + OPFS Migration — durch den lokalen Server-Ansatz (2026-07-04) überholt/hinfällig | `src/db/schema.ts` |
|
||
|
||
**Erledigt (2026-07-19): Live-Formulierungshilfe, Beobachter-Fokus, Meeting-Checkliste umgesetzt** (Konzept siehe Eintrag unten, dort auch die Design-Begründungen). Neue Dateien: `src/utils/criterionOptions.tsx` (aus `FeedbackStructureConfig.tsx` extrahiertes `criterionOptionGroups()`, jetzt auch von `AssignmentTypeConfig.tsx` genutzt), `server/db/queries/checklists.ts` + `src/db/queries/checklists.ts` (neues RPC-Modul `checklists`), `src/pages/meeting/ChecklistTab.tsx` (neuer dritter Meeting-Tab, nur sichtbar wenn `PhaseTemplate.hasChecklist`). `AssignmentTypeConfig.tsx` bekam pro Phase eine „Checkliste"-Checkbox + aufklappbaren Punkte-Editor (Label, Scope-Auswahl, bei `perInstitutee` optionales Kriterium-Dropdown). `deletePhaseTemplate`/`deleteAssignmentType` cascaden jetzt zusätzlich über `checklistItemDefs` (gleiches Verteidigungsprinzip wie bei `deleteFeedbackCriterionItem`). Live-Formulierungshilfe: neuer State `corpus` in `MeetingView.tsx` (einmal pro Meeting-Load geholt, nicht pro Tastendruck), debounced (~800ms) Aufruf von `findSimilarTaggedLines()` sowohl in `ConversationTab.tsx` (aktive Notiz) als auch für die allgemeinen Notizen — rein lesendes „💭 Ähnliche frühere Notizen"-Panel. Fokus-Übersicht als neue aufklappbare „📊 Fokus-Übersicht"-Box in `FeedbackStructureConfig.tsx` (Top 5 Kriterien nach `getCriterionUsageMap()`-Verwendung, inkl. `learnedPatternHints` als Kontext). Verifiziert: `npx tsc -b` fehlerfrei (dabei einen fehlenden `hasChecklist`-Wert im toten Dexie-Seed-Code `src/db/seeds/assignmentTypes.ts` nachgezogen, da dieser weiterhin vom Client-Build mitkompiliert wird); neue RPC-Endpunkte (`checklists.listChecklistItemDefsForPhaseTemplate`, `assignmentTypes.listPhaseTemplates`) direkt gegen die laufende Dev-Instanz getestet — Schema-Migration (neue Spalte `hasChecklist`, neue Tabellen `checklistItemDefs`/`checklistResponses`) griff automatisch ohne manuellen Server-Neustart. Manueller Ausfüll-Test einer Checkliste in einem echten Meeting (inkl. Prüfung, dass verknüpfte Punkte korrekt in `Assessment` landen) steht noch aus.
|
||
|
||
**Nachtrag (2026-07-19, nach erstem echten Nutzungstest):** Drei Korrekturen an der Checkliste, nachdem sich die ursprüngliche Einschränkung in der Praxis als zu eng erwies:
|
||
1. **Kriterium-Verknüpfung jetzt für beide Scopes möglich** (vorher nur `perInstitutee`). Bei `scope: meetingWide` + verknüpft schreibt `MeetingView.tsx::saveChecklistResponse` dieselbe Bewertung/Notiz per Fan-out in `Assessment` **für jeden aktuellen Institutee gleichzeitig** — ein meeting-weiter Checkpunkt gilt für alle Beteiligten gleichermaßen. Löst das ursprüngliche Problem („`Assessment` verlangt `instituteeId`") durch Duplizieren statt durch Ausschluss.
|
||
2. **`ChecklistResponse` ist jetzt immer die Quelle für die Checkliste-UI selbst**, unabhängig von Kriterium-Verknüpfung (vorher las/schrieb die UI bei verknüpften Punkten direkt `Assessment`, was zu einem unsauberen Round-Trip führte, sobald die Notiz auch das Checklisten-Label enthalten sollte). Bei Verknüpfung spiegelt `saveChecklistResponse()` zusätzlich **einseitig** (Checkliste → Assessment, nie zurück) nach `Assessment`, inkl. komponierter Notiz `Checkliste „<Label>" (<Bewertung>): <Kommentar>` über `composeChecklistNote()` — dadurch landet der Checkpunkt-Name selbst (nicht nur der freie Kommentar) in `aiPrompt.ts::kriterienScores`, das `Assessment.note` bereits einsammelt; kein Änderung an `aiPrompt.ts` nötig. Bekannte Einschränkung: Wird dasselbe Kriterium zusätzlich direkt über den (weiterhin vorhandenen) Gesamtbewertungs-Tab bearbeitet, gewinnt der jeweils letzte Schreibzugriff — gleiches Verhalten wie beim bestehenden „Aus Protokoll"-Button, keine neue Fehlerklasse.
|
||
3. **Keine eigene Checkliste-Tab-Karte mehr** — `ChecklistTab` wird jetzt direkt oberhalb der Gesprächs-Zeitleiste innerhalb des Gesprächsprotokoll-Tabs gerendert (`tab === 'conversation'`), nicht mehr über einen separaten dritten Tab. Der Tab-Button „Gesprächsprotokoll" erscheint entsprechend schon bei `hasConv || hasChecklist`, damit auch reine Checklisten-Phasen (ohne Protokoll-Funktion) eine Heimat haben.
|
||
|
||
**Sechster Nachtrag (2026-07-20): Bug + fehlende Diagnose bei „Vorschläge analysieren" behoben.** Nutzer meldete: Aufruf lieferte keine Vorschläge, ohne jede Rückmeldung, was schiefläuft. Zwei Ursachen gefunden und behoben (`MeetingView.tsx::analyzeSuggestions`):
|
||
1. **Bug:** Checklisten-Punkte ohne effektives Kriterium wurden nur analysiert, wenn bereits ein Kommentar eingetragen war (`if (!comment) continue`) — reine Bewertungen ohne Freitext fielen dadurch komplett aus der Analyse heraus, obwohl der Checklisten-Punkt-Name (Label) selbst schon ein brauchbares Signal für die KI ist. Jetzt wird bei fehlendem Kommentar das Label allein als Text übergeben (`comment ? \`${ci.label}: ${comment}\` : ci.label`).
|
||
2. **Fehlende Diagnose:** Sowohl das stille `return` bei `lineRefs.length === 0` (nichts zu analysieren) als auch der Fall „KI hat für keine Zeile/keinen Punkt einen Vorschlag geliefert" gaben bisher keinerlei Rückmeldung. Neu: `suggestionInfo`-Statuszeile (blau, neben dem bestehenden roten Fehlerbanner) mit Zählung „X Zeile(n)/Punkt(e) analysiert, Y Vorschläge erhalten" sowie ein aufklappbares „🔍 Rohantwort ansehen"-Panel (`lastAnalysisRaw`), das die komplette, ungeparste KI-Antwort zeigt — macht Format-/Parsing-Probleme künftig direkt sichtbar statt raten zu müssen.
|
||
|
||
**Fünfter Nachtrag (2026-07-20): Checkliste in allen Export-/Berichts-Pfaden nachgezogen.** Bei der Prüfung stellte sich heraus, dass sämtliche bestehenden Export-/Berichtsfunktionen die Checkliste komplett ignorierten (vor Einführung der Checkliste geschrieben, nicht mitgepflegt). Nachgezogen:
|
||
- `server/db/queries/meetings.ts::getMeetingExportData()` liefert jetzt zusätzlich `checklistItems`/`checklistResponses` (nur wenn `template.hasChecklist`) — einzige Datenquelle für Meeting-Export, dadurch automatisch in MD/JSON verfügbar.
|
||
- Neue Bulk-Funktionen `listChecklistItemDefsForPhaseTemplates()`/`listChecklistResponsesForMeetings()` (Server + Client) analog zu `listAssessmentsForMeetings()` — für Berichte über mehrere Meetings/Phasentypen hinweg (`MeetingHistory.tsx`, Assignment-Report-PDF), vermeidet N+1-Abfragen.
|
||
- `src/utils/meetingExport.ts` (Markdown + JSON), `src/utils/pdfExport.tsx::AssignmentReportDocument` (PDF) und `src/pages/MeetingHistory.tsx` (Gesamtprotokoll-Ansicht) zeigen die Checkliste jetzt jeweils in derselben Position (nach dem Gesprächsprotokoll, vor der Gesamtbewertung) — Label, effektives Kriterium (individuelle Zuordnung > Konfigurations-Vorgabe, gleiche Auflösung wie in `MeetingView.tsx`), Bewertung, Kommentar.
|
||
- `src/utils/structureExport.ts` (Markdown) und `pdfExport.tsx::AssignmentTypesDocument` (PDF) — der Assignment-Typen-Bericht listet jetzt pro Phase zusätzlich `hasChecklist` und die konfigurierten Checklisten-Punkte (inkl. Scope und ggf. verknüpftem Kriterium), analog zum bereits bestehenden Kriterien-Mapping.
|
||
- Bewusst nicht angefasst: `AssignmentDetail.tsx`s Chip-Reihe (nur Rating-Aggregation, kein direkter Bezug zur Checkliste) — wäre ein separates, kleines UI-Ergänzung, kein Export/Bericht im eigentlichen Sinn.
|
||
|
||
**Vierter Nachtrag (2026-07-19): „Vorschläge analysieren" + Kriterium-Vorschläge jetzt auch für die Checkliste, individuelle Zuordnung pro Antwort.** Bisher war die Kriterium-Verknüpfung eines Checklisten-Punkts ausschließlich in der Config fix hinterlegt (`ChecklistItemDef.linkedCriterionItemId`) und für „🏷 Vorschläge analysieren" unsichtbar. Neu:
|
||
- **`ChecklistResponse.criterionItemId`** (neue, automatisch migrierte Spalte) überschreibt pro Antwort individuell die Konfigurations-Vorgabe — `getEffectiveChecklistCriterion()` in `MeetingView.tsx` liefert `response.criterionItemId ?? ci.linkedCriterionItemId ?? null`. Zuordnung direkt in `ChecklistTab.tsx` über denselben `CriterionTagPicker` wie im Protokoll (Chip + Popover, inkl. Suche und Vorschlägen) — keine separate Komponente nötig.
|
||
- **`analyzeSuggestions()` bezieht jetzt auch Checklisten-Kommentare ein:** Antworten ohne effektives Kriterium (weder individuell noch konfiguriert) mit vorhandenem Kommentar werden mit `${Label}: ${Kommentar}` als zusätzliche Zeile in denselben gebündelten KI-Call eingespeist wie Protokoll-/Notizzeilen. Da Checklisten-Antworten nicht textbasiert (wie `lineTags`) adressierbar sind, führt ein neuer optionaler `checklistKey`-Marker (`${checklistItemId}:${instituteeId ?? ''}`) am `LineRef` das Routing der KI-Antwort in eine eigene Map `checklistSuggestions` statt in die text-basierte `lineSuggestions`-Map.
|
||
- **Lokale (KI-freie) Checklisten-Vorschläge** analog zu `localLineSuggestions`: neuer `useMemo` `localChecklistSuggestions` wendet `findSimilarTaggedLines()` auf den Kommentartext jeder unzugeordneten Antwort an (Schwelle ≥ 0,25) — sofort verfügbar, ohne „Vorschläge analysieren" auszulösen. Merge mit den KI-Vorschlägen (`mergedChecklistSuggestions`, KI hat Vorrang) liefert `getChecklistSuggestions()`, das `ChecklistTab.tsx` genau wie im Protokoll als 💡-Chip im `CriterionTagPicker` anzeigt.
|
||
- **Mirroring nach `Assessment`** nutzt jetzt konsequent das *effektive* Kriterium (individuell > konfiguriert) statt nur `ci.linkedCriterionItemId`. Wechselt die individuelle Zuordnung nachträglich auf ein anderes Kriterium, bleibt ein bereits gespiegelter `Assessment`-Eintrag beim alten Kriterium bewusst unangetastet (keine rückwirkende Korrektur/Löschung) — gleiches einfaches Prinzip wie beim Löschen eines Checklisten-Punkts.
|
||
|
||
**Dritter Nachtrag (2026-07-19): Direkte Kriterien-Vorschläge ohne KI.** Zwei Ergänzungen zur Live-Formulierungshilfe, beide ohne KI-Call: (1) Das „💭 Ähnliche frühere Notizen"-Panel (`ConversationTab.tsx`/`MeetingView.tsx`) zeigt jetzt zusätzlich das Kriterium der jeweiligen Alt-Zeile an (`„Text" → 🏷 Kriterium`), aufgelöst aus dem bereits vorhandenen `criterionItemId` in `SimilarTaggedLine` — reine Zusatzinfo, keine neue Berechnung nötig. (2) Neuer `useMemo` `localLineSuggestions` in `MeetingView.tsx`: berechnet für **alle** noch nicht zugeordneten Zeilen (Gesprächsbeiträge + allgemeine Notizen, gleiche Sammel-/Ausschlusslogik wie `analyzeSuggestions`) lokale Kriterium-Vorschläge über `findSimilarTaggedLines()`, gefiltert auf Ähnlichkeit ≥ 0,25 („mittel", gleiche Schwelle wie in `criterionSuggestionPrompt.ts`) — macht den 💡-Vorschlags-Chip im `CriterionTagPicker` **sofort verfügbar, ohne „🏷 Vorschläge analysieren" auszulösen**. Wird mit den KI-Vorschlägen (`lineSuggestions`) zu `mergedLineSuggestions` zusammengeführt — KI-Vorschläge haben Vorrang, wenn für dieselbe Zeile beides vorliegt (`lineSuggestions`-Einträge überschreiben `localLineSuggestions` beim Merge). Geprüft und bestätigt: „🏷 Vorschläge analysieren" überschreibt nie eine bereits bestehende Zuordnung — `analyzeSuggestions` schließt bereits getaggte Zeilen von vornherein von der KI-Anfrage aus (`taggedTexts.has(line)`-Filter), und `CriterionTagPicker` zeigt bei vorhandener Zuordnung ohnehin immer den Tag-Chip statt des Vorschlags-Chips (Vorschläge sind nur bei leerer Zuordnung überhaupt sichtbar).
|
||
|
||
**Zweiter Nachtrag (2026-07-19):** Zwei UI-Korrekturen nach echtem Nutzungsfeedback. (1) `ChecklistTab.tsx` nahm zu viel Bildschirmplatz ein (eine Karte mit großen Buttons + immer sichtbarem Kommentarfeld pro Institutee/Punkt) — umgebaut auf Zeilen-Darstellung: `<select>` statt fünf breiter Bewertungs-Buttons, Kommentar nur über ein 💬-Icon ausklappbar (Icon zeigt blau eingefärbt an, wenn bereits ein Kommentar hinterlegt ist). (2) Die Live-Formulierungshilfe (`💭 Ähnliche frühere Notizen`, `ConversationTab.tsx` + `MeetingView.tsx`) zeigte Vorschläge bisher nur rein lesend an — jetzt per Klick übernehmbar, ersetzt dabei die gerade getippte (letzte) Zeile der Notiz. Wichtige Falle dabei: In `MeetingView.tsx` hätte ein reiner Klick auf den Vorschlag zuerst den `onBlur`-Handler der Notizen-Textarea ausgelöst (`finishEditingGeneralNotes()`, beendet den Editier-Modus und entfernt damit Textarea+Vorschlagsliste aus dem DOM, bevor der Klick verarbeitet wird) — behoben über `onMouseDown={e => e.preventDefault()}` auf den Vorschlags-Buttons, verhindert den Fokuswechsel/Blur überhaupt erst (gleiches Muster wie `stopPropagation` beim `CriterionTagPicker` in den allgemeinen Notizen, nur mit `preventDefault` statt `stopPropagation`, da hier kein Klick-Bubbling sondern der native Blur-vor-Click-Ablauf das Problem ist).
|
||
|
||
**Konzept (2026-07-18): Live-Formulierungshilfe, Beobachter-Fokus, Meeting-Checkliste.** Zwei unabhängige Erweiterungen, in einer Design-Session gemeinsam geprüft:
|
||
- **Live-Formulierungshilfe (kein KI-Call):** Während des Tippens in `ConversationTab.tsx`/allgemeinen Notizen liefert ein debounced Aufruf der bereits bestehenden `findSimilarTaggedLines()` (`src/utils/criterionSuggestion.ts`) ein rein lesendes Referenz-Panel „Ähnliche frühere Notizen" — bewusst **kein** Autocomplete/Text-Einfügung (würde die 2026-07-02 getroffene „nicht live während des Tippens"-Entscheidung für die Zuordnung unterlaufen; hier geht es nur um unverbindliche Formulierungs-Inspiration, kein Eingriff in den Text). Kein neues Backend nötig.
|
||
- **Beobachter-Fokus (global, keine Bewerter-Identität nötig):** Die App hat kein Login-/Nutzerkonzept — „was wird typischerweise fokussiert" bezieht sich daher bewusst auf alle Bewerter zusammen. Leistet im Kern bereits `learnedPatternHints` (Wie wird formuliert) + `getCriterionUsageMap()` (Was wird häufig kommentiert, bereits für die Config→Feedback-Verwendungs-Badges gebaut). Nur eine neue, rein lesende Anzeige nötig, kein neues Datenmodell.
|
||
- **Meeting-Checkliste pro Phasentyp:** Neues Feld `PhaseTemplate.hasChecklist` (Opt-in wie `hasConversation`/`hasAssessment`) + neue Tabelle `ChecklistItemDef` (`phaseTemplateId`, `label`, `order`, optional `linkedCriterionItemId`, `scope: 'perInstitutee' | 'meetingWide'` — pro Eintrag konfigurierbar). Wichtiger Fund: `PhaseTemplate.criteriaIds` existiert zwar schon, wird aber aktuell nirgends live im Meeting ausgewertet (nur für `getCriterionUsageMap`-Zählung) — bewusst NICHT für die Checkliste umgewidmet, da es bereits eine andere, etablierte Sichtbarkeits-Scoping-Semantik hat. Antwort-Ablage maximiert Wiederverwendung bestehender Strukturen statt einer vierten Bewertungs-Struktur: „pro Institutee + verknüpft" → direkt in `Assessment` (bestehende Funktionen); „meeting-weit + verknüpft" → direkt in `MeetingInstance.generalNoteTags` (bestehender, institutee-unabhängiger Mechanismus); nur „frei definiert" (verknüpft oder nicht, jeweils pro Institutee oder meeting-weit) braucht die einzige wirklich neue Tabelle `ChecklistResponse` (`meetingInstanceId`, `instituteeId?`, `checklistItemId`, `rating`, `note`) — bewusst **ohne** Aggregation in Evaluation/gewichteten Durchschnitt (reine lokale Meeting-Notiz, vermeidet eine dritte manuell synchron zu haltende Bewertungsstruktur neben Assessment/FeedbackCategoryRating). UI: neuer optionaler Tab „Checkliste" in `MeetingView.tsx`, Konfiguration als neuer Sub-Tab in `AssignmentTypeConfig.tsx`, Kriterium-Verknüpfung über das bestehende `criterionOptionGroups()`-Dropdown-Muster aus `FeedbackStructureConfig.tsx`. **Präzisiert bei der Umsetzungsplanung (gleicher Tag):** „meeting-weit + verknüpft" hat keine saubere Heimat (`Assessment` verlangt zwingend `instituteeId` — Kriterien sind in diesem Modell immer Personen-Kompetenzen, nie Meeting-Kollektiv-Eigenschaften; `generalNoteTags` kennt keine Bewertungsstufe, nur Text+Tag). Deshalb: Kriterium-Verknüpfung ist nur bei `scope: perInstitutee` möglich, `scope: meetingWide`-Punkte sind immer „frei" (kein Kriterium-Dropdown im UI). Vereinfacht die Ablage auf 3 statt 4 Fälle, siehe Umsetzungsplan.
|
||
|
||
**Konzept (2026-07-16, noch nicht umgesetzt): Selbstlernendes Kriterien-Tagging.** Löst den Offene-Punkte-Eintrag „Selbstlernendes Tagging" oben ab (bisher nur Idee, jetzt fertig gescoped in einer eigenen Design-Session, siehe Vorgabe oben). Ziel: eine priorisierte, KI-gestützte Vorschlagsliste für die Kriterium-Zuordnung noch nicht getaggter Protokollzeilen, direkt im `CriterionTagPicker` aufrufbar. Kernentscheidungen:
|
||
- **Kein neues Datenmodell für den Lernkorpus.** Die Lernbasis ist der bereits bestehende `lineTags`/`generalNoteTags`-Bestand, global über alle Assignments hinweg (siehe `getCriterionUsageMap()`/`getCriterionUsageDetails()` in `server/db/queries/feedbackStructure.ts`). Jede neu vergebene Zuordnung vergrößert den Korpus automatisch — kein separater Trainingsschritt, keine Migration nötig.
|
||
- **Zweistufiger Ablauf.** (1) Rein lokale Ähnlichkeitssuche ohne KI-Call (Wortüberschneidung/Jaccard über normalisierten Zeilentext; `parseNotationPrefix()`/`splitNoteLines()` zur Normalisierung wiederverwendet) liefert pro noch nicht zugeordneter Zeile ~5–8 ähnlichste Alt-Beispiele als Kontext-Hinweis. (2) EIN gebündelter KI-Call pro Protokoll-Ansicht (nicht pro Zeile) analysiert alle noch nicht zugeordneten Zeilen des gesamten offenen Protokolls auf einmal und liefert pro Zeile eine priorisierte Kandidatenliste (Kriterium-IDs) statt nur eines Einzelvorschlags. Fallback auf Einzel-Zeilen-Calls ist vorgesehen, aber erst nach einem ersten Testlauf zu entscheiden, falls die Bündelung qualitativ nicht ausreicht — nicht vorab bauen.
|
||
- **Expliziter Trigger-Button** („🏷 Vorschläge analysieren"), kein automatischer Call beim Öffnen des Meetings — gleiches Muster wie die bestehende KI-Feedback-Generierung (keine versteckten API-Kosten/Latenz im Hintergrund).
|
||
- **Neuer Platzhalter `{{KRITERIEN_STRUKTUR}}`** für den Tagging-Prompt: vollständige, ID-referenzierte Struktur Dimension → Kategorie (Name+Beschreibung) → Kriterium (Name+Beschreibung), gefiltert über die bestehende `filterVisibleFeedbackStructure()` anhand `AssignmentType.criteriaIds` — dieselbe Filterlogik wie überall sonst im Projekt, nicht neu implementiert. Kriterien werden über ihre ID referenziert statt über den Namen (robust gegen Namenskollisionen beim Parsen der KI-Antwort). `FeedbackDimension` hat aktuell kein `description`-Feld (anders als Kategorie/Kriterium) — „alle Erklärungsebenen" bezieht sich daher auf Dimension-Name + Kategorie-Name/Beschreibung + Kriterium-Name/Beschreibung. Weitere neue Platzhalter: `{{ASSIGNMENT_TYP}}`, `{{PROTOKOLL_ZEILEN}}` (nummerierte, noch nicht zugeordnete Zeilen des gesamten Protokolls), `{{AEHNLICHE_ALT_ZUORDNUNGEN}}` (Ergebnis aus Schritt 1).
|
||
- **EIN gemeinsames Prompt-Template statt eines pro Assignment-Typ** — die Typ-Abhängigkeit steckt im Inhalt von `{{KRITERIEN_STRUKTUR}}`, nicht in der Anzahl der Templates (gleiches Prinzip wie `{{KATEGORIEN_STRUKTUR}}` heute schon). Persistiert als neues Feld `AppSettings.criterionTaggingPromptTemplate` (automatische Spalten-Migration wie gehabt), editierbar in einem neuen, eigenen Abschnitt in `AiPromptConfig.tsx` („KI-Prompt: Kriterien-Zuordnungsvorschläge") — gleiches Aufklapp-Textarea-Muster + eigene Platzhalter-Legende wie der bestehende Standard-Prompt-Bereich. Kein Eintrag pro Dimension nötig, da es nur eine Protokoll-weite Analyse gibt, nicht pro Dimension.
|
||
- **Antwortformat:** Zeilennummer + priorisierte Kriterium-ID-Liste als Klartext-Marker (`ZEILE 3: 12, 47, 5`, `-` bei keinem Vorschlag) statt JSON — bewusst gleiches Prinzip wie `parseDimensionAiResponse` (JSON/Codefence-Parsing-Bugs waren 2026-07-07 der Grund für die Umstellung auf Marker-Text dort). Parser tolerant gegenüber fehlenden/kaputten Zeilen.
|
||
- **Datenschutz-Hinweis:** Gesprächsnotizen gehen für Schritt 2 an dieselbe bereits genutzte externe KI-API wie die bestehende Feedback-Generierung (gleiches Muster, kein neues Risiko-Muster) — vor Produktivnutzung trotzdem kurz mit dem Datenschutzbeauftragten abstimmen, da es eine neue Nutzungsart derselben Datenklasse ist.
|
||
|
||
**Nachtrag (2026-07-17):** Zusätzliche Lernebene beschlossen — bewusster Mittelweg zwischen reiner Wortüberschneidung und einem separaten trainierten Klassifikator (Letzteres verworfen, da ein eigener Trainings-/Aktualisierungsschritt der Prämisse „kein separater Trainingsschritt" widersprochen hätte). Der ohnehin gebündelte KI-Aufruf liefert pro Analyse-Lauf zusätzlich einen `MUSTER:`-Block mit kurzen, generalisierten Formulierungsmustern **pro Kriterium** (nicht pro Zeile), z.B. „Kunde wird häufig nach offenen Fragen gefragt, bevor...". Diese landen in einem neuen, optionalen Feld `FeedbackCriterionItem.learnedPatternHints` (automatische Spalten-Migration wie gehabt, kein neues Datenmodell/keine neue Tabelle) und werden bei jedem weiteren Lauf überschrieben/verfeinert — reines Nebenprodukt des normalen „🏷 Vorschläge analysieren"-Klicks, kein separater Trainingslauf. Neuer Platzhalter `{{GELERNTE_MUSTER}}` speist diese Muster (nur für die im aktuellen Assignment-Typ sichtbaren Kriterien) zusätzlich zu `{{AEHNLICHE_ALT_ZUORDNUNGEN}}` in künftige Prompts ein — verbessert die Vorschlagsqualität über die Zeit, ersetzt den KI-Call aber bewusst nicht.
|
||
|
||
**Erledigt (2026-07-17): Selbstlernendes Kriterien-Tagging umgesetzt** (siehe Konzept + Nachtrag oben). Neuer Button „🏷 Vorschläge analysieren" in `MeetingView.tsx` löst EINEN gebündelten KI-Call für das gesamte offene Protokoll aus (Gesprächsbeiträge + allgemeine Notizen, Kundenzitate ausgenommen). Neue Bausteine: `server/db/queries/criterionSuggestions.ts` (`getGlobalLineTagCorpus()`, flacht `lineTags`/`generalNoteTags` global ab, gleiches Iterationsmuster wie `getCriterionUsageMap()`), `src/utils/criterionSuggestion.ts` (rein lokale Jaccard-Ähnlichkeitssuche, kein KI-Call), `src/utils/criterionSuggestionPrompt.ts` (`buildCriterionStructureText()`/`buildCriterionSuggestionPromptContext()`/`parseCriterionSuggestionResponse()` — Response-Format bewusst Klartext-Marker `ZEILE n: <IDs>` + `MUSTER:`-Block statt JSON, gleiches Robustheitsprinzip wie `parseDimensionAiResponse`). `AppSettings.criterionTaggingPromptTemplate` (ein gemeinsames Template statt eines pro Assignment-Typ, editierbar in `AiPromptConfig.tsx` unter „KI-Prompt: Kriterien-Zuordnungsvorschläge") und `FeedbackCriterionItem.learnedPatternHints` sind neue, automatisch migrierte Spalten. `CriterionTagPicker.tsx` zeigt bei fehlender Zuordnung einen 💡-Vorschlags-Chip (Top-Vorschlag per Klick übernehmbar, Alternativen + volle Suche im Popover) statt des leeren „+"-Buttons. Nebenbei extrahiert: `src/utils/aiClient.ts` (`callOpenRouter()`/`getAiSettings()`/`saveAiSettings()`) aus `AssignmentFeedbackPage.tsx`, damit die bestehende KI-Feedback-Generierung und das neue Tagging dieselbe OpenRouter-Anbindung nutzen statt zwei leicht abweichende Implementierungen zu pflegen — reiner Refactor, kein Verhaltensunterschied. Verifiziert: `npx tsc -b` fehlerfrei; neue RPC-Endpunkte (`criterionSuggestions.getGlobalLineTagCorpus`, `appSettings.getOrSeedCriterionTaggingPromptTemplate`) direkt gegen die laufende Dev-Instanz getestet (der Server läuft unter `tsx watch` und hat Schema-Migration + neue Module automatisch übernommen, ohne manuellen Neustart). Manueller Browser-Test des Gesamtflusses (Klick auf „Vorschläge analysieren" in einem echten Meeting, Übernehmen eines Vorschlags) steht noch aus — sollte vor breiter Nutzung nachgeholt werden.
|
||
|
||
**Erledigt (2026-07-13, dritte Runde):** Kriterium-Zuordnung für „Allgemeine Notizen" (`MeetingInstance.generalNoteTags`, neue Spalte, gleicher Typ `ConversationLineTag[]` wie `ConversationEntry.lineTags`) — anders als die bestehende Zeilen-Zuordnung in Gesprächsbeiträgen ist diese **nicht** an ein Institutee gebunden, sondern gilt für **alle** Institutees des Assignments gleichzeitig, weil allgemeine Meeting-Notizen keinem einzelnen Sprecher zugeordnet sind. Erfassung in `MeetingView.tsx`: derselbe `CriterionTagPicker` wie bei Gesprächsbeiträgen, per `renderLineAddon` an `NotationText` gehängt (Klick auf den Picker mit `stopPropagation`, da der umgebende Notizen-Block sonst in den Editier-Modus wechseln würde); verwaiste Zuordnungen werden beim Verlassen des Editier-Modus anhand der aktuellen Zeilen bereinigt (gleiches Muster wie `saveEdit` bei Gesprächsbeiträgen). Fünf bestehende Konsumenten von `lineTags` wurden konsistent um die neue, ungefilterte Quelle ergänzt (Recherche in zwei unabhängigen Durchläufen bestätigt, dass dies eine vollständige Liste ist): `AssessmentTab.suggestedRating()` (💡-Vorschlags-Badges, jetzt pro Kriterium für jedes Institutee identisch beeinflusst), `aiPrompt.ts::buildDimensionPromptContext` (`{{KRITERIEN_SCORES}}` bekommt die zugeordneten Zeilen zusätzlich, `{{NOTIZEN_ALLGEMEIN_MEETING}}` blendet bereits zugeordnete Zeilen aus — exakt das bestehende Muster von `{{PROTOKOLL_UNZUGEORDNET}}`), `meetingExport.ts` (Kriterien-ID-Sammlung + `_(→ Kriteriumsname)_`-Annotation pro Zeile im Markdown-Export), `FeedbackDraftPage.tsx` (Legacy-Feedback-Entwurf, gleiche Erweiterung von Kriterien-Sammlung und Notizen), `MeetingHistory.tsx` (reine Anzeige, sonst wäre die neue Zuordnung dort unsichtbar geblieben). Bei leerem/fehlendem `generalNoteTags` ist das Verhalten überall identisch zu vorher — bestehende Daten/Prompts brechen nicht.
|
||
|
||
**Erledigt (2026-07-13, zweite Runde):** Chronologisches Gesamtprotokoll aller Meetings eines Assignments (`MeetingHistory.tsx`, Route `/assignment/:id/history`) — Anlass: Recap-Calls nach jedem Kundenmeeting brauchen einen schnellen Blick auf die Historie vorheriger Meetings, zusätzlich hilfreich bei der Gesamtdurchsprache im finalen Feedback-Call. Rein lesende Seite, baut bewusst auf bereits vorhandenen Bulk-Abfragen auf statt neuen Server-Code zu schreiben: `listDoneMeetingsChronological()` (kanonischer Trash-Filter + Sortierung, siehe unten) liefert die Meetings, `listConversationEntriesForMeetings()`/`listAssessmentsForMeetings()` (beide bereits vorhanden, u.a. schon von `AssignmentDetail.tsx` genutzt) liefern Notizen/Bewertungen über alle Meetings auf einen Schlag statt N+1-Abfragen pro Meeting. Rendering nutzt bestehende Bausteine statt Neuimplementierung: `NotationText` (`notationParser.tsx`) für Notiz-Formatierung, dieselbe `weightedAverageRating()`+`RATING_BG`-Chip-Logik wie die Meeting-Kacheln in `AssignmentDetail.tsx`. Kriterium-Tags werden hier bewusst nur als statisches Badge angezeigt (kein interaktiver `CriterionTagPicker`), da die Seite reine Anzeige ist. Das jeweils neueste Meeting wird optisch hervorgehoben und die Seite scrollt beim Laden automatisch dorthin — deckt den Recap-Anwendungsfall ab, während beim Hochscrollen die volle Historie für die Feedback-Gesamtdurchsprache verfügbar bleibt. Erreichbar über neue Buttons sowohl auf `AssignmentDetail.tsx` („📜 Gesamtprotokoll") als auch direkt aus `MeetingView.tsx` heraus („📜 Verlauf"), damit man auch mitten im Recap-Call ohne Umweg über die Detailseite wechseln kann.
|
||
|
||
**Erledigt (2026-07-14):** KI-Feedback-Generierung (siehe 2026-07-07) anhand echter Testläufe nachgeschärft. `RATING_OPTIONS` (`src/config/constants.ts`) hat ein neues `full`-Feld ("Not/Partially/Nearly/Fully Client Ready", "Not Applicable") als einzige Quelle der Wahrheit für ausgeschriebene Bewertungslabels — wichtig fürs KI-Prompting, da die kurzen Labels ("Not") für ein Modell ohne Kontext zum Zweck des Trainee-Programms (Ziel: "Client Ready" werden) nicht aussagekräftig genug sind. `src/utils/notationParser.tsx` hat eine neue Funktion `parseNotationPrefix()` (liefert Rating/Zitat-Flag/Resttext, `detectNotationRating()` ist jetzt darauf aufgebaut, keine doppelte Symbol-Zuordnung mehr). In `aiPrompt.ts` löst `formatNotationLine()` jede Notiz-Zeile (`{{KRITERIEN_SCORES}}`-Notizen, `{{NOTIZEN_ALLGEMEIN_MEETING}}`, `{{PROTOKOLL_UNZUGEORDNET}}`) konsistent auf — Format `Label — Text` (bewusst **keine** eckigen Klammern, da `[Titel]` bereits für Slide-/Themen-Referenzen reserviert ist und sonst mit aufgelösten Bewertungen kollidieren würde). Zwei fachliche Korrekturen dabei: (1) `{{KRITERIEN_SCORES}}` lässt Kriterien ohne jede Bewertung/Notiz jetzt komplett aus (reines Token-Rauschen), ebenso ganze Kategorien ohne verwertbare Kriterien. (2) Mit `>` markierte Kundenzitate werden aus `{{NOTIZEN_ALLGEMEIN_MEETING}}`/`{{PROTOKOLL_UNZUGEORDNET}}` jetzt **komplett herausgefiltert** statt nur aufgelöst — Kundenaussagen sind fachlicher Assignment-Kontext, keine Aussage über den Berater, und würden besonders beim Feedback-Call mit dem Kunden ein verzerrtes Bild erzeugen (der Kunde gibt ohnehin separates Feedback). Explizit einem Kriterium zugeordnete Zitate (bewusste Tagging-Entscheidung des Bewerters) werden davon nicht angefasst. Output-Format umgestellt von Fließtext-Absätzen auf Stichpunkte (`- `-Zeilen) — leichter in die offizielle Feedback-App zu übertragen; Development-Needs-Stichpunkte müssen jetzt explizit als Handlungsempfehlung formuliert sein (z.B. "Sollte darauf achten, dass ..."). Zusätzlich eine explizite Anti-Halluzinations-Anweisung im Prompt ("nutze ausschließlich die angegebenen Daten, erfinde nichts") ergänzt, nachdem die KI vereinzelt Details erfunden hatte. **Wichtige Einschränkung, die bei jeder künftigen Prompt-Engine-Änderung zu beachten ist:** Da jede Dimension einen **vollständig unabhängigen** Prompt-Text in der DB hat (bewusste Design-Entscheidung, siehe 2026-07-07), wirken Verbesserungen an `AI_CONFIG.defaultDimensionPromptTemplate` NICHT automatisch auf bereits existierende Dimensionen — nur auf neu angelegte oder explizit per "Auf Standard zurücksetzen" aktualisierte. Alle Änderungen dieser Runde wurden nach Rückfrage per Skript einmalig auf alle 5 bestehenden Dimensionen nachgezogen (nur unbedenklich, weil vorher per Read-Check bestätigt wurde, dass noch keine individuellen Anpassungen existierten).
|
||
|
||
**Erledigt (2026-07-14, dritte Runde):** PDF-Export ergänzt (`src/utils/pdfExport.tsx`, neue Abhängigkeit `@react-pdf/renderer` — reines JS, kein natives Kompilieren nötig, läuft browser- und Node-seitig). Zwei Dokument-Typen: `AssignmentReportDocument` (Gesamtprotokoll wie `MeetingHistory.tsx` + Feedback-Anhang aller Institutees, unabhängig vom Status Entwurf/Final) und `InstituteeFeedbackDocument` (nur das Feedback einer Person). Wichtig: Der Feedback-Teil zeigt ausschließlich die **finalen** Felder (`achievements`/`developmentNeeds`/`text`/`rating`) — niemals die `suggested*`-KI-Vorschläge, die noch nicht per „Übernehmen" bestätigt wurden. Notation in den Protokollzeilen wird wie in der App selbst als Icon/Farbe dargestellt (✓/✗/✓~/✗~/›, aus `parseNotationPrefix()` abgeleitet) — bewusst anders als die textuelle "Label — Text"-Auflösung in `aiPrompt.ts`, da das PDF für Menschen ist, die die App-Notation schon kennen, nicht für ein KI-Modell ohne Kontext. Erzeugung/Download sind getrennt (`buildAssignmentReportPdfBlob`/`buildInstituteeFeedbackPdfBlob` liefern nur den Blob, `exportAssignmentReportPdf`/`exportInstituteeFeedbackPdf` triggern zusätzlich den Browser-Download) — dadurch auch außerhalb des Browsers (Node/tsx) testbar, was beim Verifizieren genutzt wurde (die Download-Hälfte braucht `document`/`URL`, ist also browser-only). Buttons an drei Stellen: `MeetingHistory.tsx` (Gesamt-Report), `AssignmentFeedbackPage.tsx` (eigenes Feedback), `AssignmentDetail.tsx` (Schnellzugriff für beide Varianten ohne Navigation). Nachträglich (nach erstem Testlauf) Seitenumbrüche korrigiert: `wrap={false}` auf ganzen Meeting-/Dimension-Blöcken erzwang unsinnige Umbrüche/Lücken, sobald ein Block länger als eine Seite war — stattdessen nur noch kleine Kopfzeilen-Blöcke (Meeting-Titel+Datum, Dimension-/Institutee-Name, Achievements/Development-Needs-Label) mit `wrap={false}` + `minPresenceAhead` vor Verwaisung geschützt, der eigentliche Inhalt darf frei umbrechen. Zusätzlich erzwungener Seitenwechsel (`break`) vor dem „Feedback"-Abschnitt und vor jedem weiteren Institutee, damit Protokoll und Feedback klar getrennt bleiben. Ebenfalls ergänzt: Kriterium-Zuordnung pro Protokollzeile (`lineTags`/`generalNoteTags`) wird jetzt wie in `MeetingHistory.tsx`/`meetingExport.ts` als kleiner „→ Kriteriumsname"-Zusatz hinter der Zeile angezeigt (indigo, per `visibleItems`-Lookup) — vorher zeigte das PDF nur die Notation, aber keine Kriterium-Zuordnung.
|
||
|
||
**Erledigt (2026-07-16, zweite Runde):** Sicherer Umbau der Feedback-Struktur (Config → Feedback), motiviert durch laufende Umbenennungen/Löschungen/Verschiebungen von Kriterien ohne Überblick, wo diese bereits verwendet werden. Neu in `server/db/queries/feedbackStructure.ts`: `getCriterionUsageMap()` — EIN Durchlauf über `assessments`, `conversationEntries.lineTags`, `meetingInstances.generalNoteTags`, `assignmentTypes.criteriaIds`, `phaseTemplates.criteriaIds` liefert eine `criterionId → CriterionUsage`-Map (Zählungen + betroffene Meetings), wird beim Laden von `FeedbackStructureConfig.tsx` mitgeladen und zeigt pro Kriterium einen aufklappbaren „N Verwendungen"-Badge. **Verschieben** zwischen Kategorien per einfachem Dropdown (kein Drag & Drop, keine neue Abhängigkeit — nutzt das bereits vorhandene `updateFeedbackCriterionItem(id, { categoryId })`). **Löschen mit Verwendung** zeigt jetzt ein Inline-Panel mit zwei Wegen: „Übertragen auf ▾" (`reassignCriterionReferences(fromId, toId)`, verschiebt Assessments/Tags/Sichtbarkeitslisten auf ein anderes Kriterium, dann Löschen) oder „Trotzdem löschen" (Bewertungen werden gelöscht, Protokoll-Zuordnungen nur entfernt, Notiztext bleibt). **Wichtiger Edge-Case bei `reassignCriterionReferences`:** `assessments` hat keine DB-Unique-Constraint auf `(meetingInstanceId, instituteeId, criteriaId)` — ein blindes Umschreiben von `criteriaId` könnte eine Dubletten-Zeile für dieselbe Meeting/Institutee-Kombination erzeugen. Bei Konflikt wird die Quell-Zeile stattdessen in die bestehende Ziel-Zeile gemergt (Notizen zusammengeführt, vorhandener Score der Zielzeile hat Vorrang) und gelöscht; Rückgabewert `mergedConflicts` macht das der UI gegenüber transparent. `deleteFeedbackCriterionItem()` räumt jetzt selbst alle Referenzen auf (Verteidigung in der Tiefe, unabhängig vom UI-Pfad), `deleteFeedbackCategory()`/`deleteFeedbackDimension()` cascaden ab jetzt **durch** diese Funktion statt per direktem Bulk-Delete, damit die Aufräum-Logik auch bei Kategorie-/Dimension-Löschung greift. Verifiziert per Skript auf rein synthetischen Testdaten (temporäre Dimension/Kategorie/2 Kriterien + ein Test-Meeting unter einem echten Assignment, absichtlich erzeugter Konfliktfall bestätigt korrektes Merge-Verhalten ohne Dublette, danach vollständig wieder entfernt).
|
||
|
||
**Nachtrag (gleicher Tag):** Reine Zählungen/Meeting-Liste reichten nicht — beim Konsolidieren zweier Kriterien musste man sonst weiterhin jedes Meeting einzeln durchsuchen. Neu: `getCriterionUsageDetails(criterionId)` liefert pro Kriterium eine **Einzelinstanzen-Liste** (`CriterionUsageInstance`, discriminated union `assessment`/`lineTag`/`generalNoteTag`, mit Meeting/Assignment/Person/Text aufgelöst) — bewusst lazy pro Kriterium beim Aufklappen geladen, nicht eager für alle (im Unterschied zu `getCriterionUsageMap`), da hier echte Texte/Namen aufgelöst werden. Direkt im aufgeklappten Verwendungs-Badge kann jede einzelne Instanz jetzt per „→ anderes Kriterium…"-Dropdown + „Übertragen" gezielt umgehängt oder per „Entfernen" einzeln entfernt werden (`reassignUsageInstance()`/`removeUsageInstance()`) — ohne das ganze Kriterium zu löschen und ohne alle Verwendungen auf einmal zu bewegen. Die Konflikt-Merge-Logik für Assessments wurde dafür aus `reassignCriterionReferences` in eine gemeinsame Hilfsfunktion `mergeOrReassignAssessment()` extrahiert, damit Bulk- und Einzel-Reassign nicht zwei leicht unterschiedliche Implementierungen derselben Regel pflegen. Ebenfalls per Skript auf synthetischen Testdaten verifiziert (Einzel-Entfernen erhält den Notiztext, Einzel-Übertragen löst denselben Merge-Konflikt korrekt wie die Bulk-Variante).
|
||
|
||
**Zweiter Nachtrag (gleicher Tag):** Nach erstem Testlauf gemeldet: Ein Kriterium zeigte nach Bereinigung aller Bewertungen/Protokoll-Tags weiterhin „4 Verwendungen" — Ursache war, dass `assignmentTypeNames`/`phaseTemplateLabels` (Sichtbarkeits-Zuordnung in `AssignmentType.criteriaIds`/`PhaseTemplate.criteriaIds`) zwar in die Zählung eingehen, aber bis dahin nur als reiner Text angezeigt wurden — keine Einzelinstanz, kein Weg, sie von hier aus zu bereinigen. `CriterionUsageInstance` um zwei weitere Kinds ergänzt (`assignmentTypeVisibility`, `phaseTemplateVisibility`, mit `criterionId`-Feld, damit `reassignUsageInstance`/`removeUsageInstance` wissen, welche ID sie in der jeweiligen `criteriaIds`-Liste ersetzen/entfernen sollen). Damit sind jetzt **alle** in der Zählung enthaltenen Verwendungsarten auch einzeln direkt aus dem aufgeklappten Badge heraus behebbar, nicht nur die drei Protokoll-/Bewertungsarten. Client-Anzeige unterscheidet per `isDataInstance()`-Typprädikat zwischen den beiden Darstellungsformen (Meeting/Person-Kontext vs. reiner Name). Ebenfalls per Skript auf synthetischen Testdaten (temporärer Assignment-Typ + Phase-Vorlage, danach entfernt) verifiziert.
|
||
|
||
**Dritter Nachtrag (gleicher Tag):** Zwei UI-Bugs nach erstem echten Nutzungstest gefixt (`FeedbackStructureConfig.tsx`). (1) Alle „Ziel-Kriterium"-Dropdowns (Verschieben/Übertragen) gruppierten nur nach Dimension und übersprangen die Kategorie-Ebene — HTML kennt kein verschachteltes `<optgroup>`, daher jetzt eine kombinierte Optgroup-Beschriftung `"Dimension → Kategorie"` über die neue gemeinsame Hilfsfunktion `criterionOptionGroups()` (an allen drei Stellen genutzt: Verschieben-Dropdown, Lösch-Dialog-Übertragen, Einzel-Verwendungs-Übertragen). (2) Schwerwiegenderer Bug: Die Ziel-Auswahl pro Einzelverwendung war über den **Array-Index** in der Instanzen-Liste verknüpft (`` `${itemId}:${idx}` ``). Nach jedem „Übertragen" wird die Liste neu vom Server geladen, wodurch sich die Indizes der verbleibenden Instanzen verschieben — eine bei Index 3 gespeicherte Ziel-Auswahl gehörte danach plötzlich zu einer anderen Instanz, und ein Klick auf „Übertragen" verschob nachweislich die falsche Zeile. Gefixt durch einen **stabilen, inhaltsbasierten Schlüssel** (`instanceKey()`: `assessmentId` bzw. `conversationEntryId`/`meetingInstanceId` + Zeilentext bzw. `assignmentTypeId`/`phaseTemplateId`) statt des Index — Auswahlen bleiben dadurch auch nach einem Reload an der richtigen Instanz hängen, und bereits übertragene/entfernte Instanzen verschwinden korrekt aus der Liste statt eine falsche Zeile zu verändern.
|
||
|
||
**Erledigt (2026-07-16):** Zwei weitere Exporte ergänzt, jeweils als Markdown UND PDF. (1) „Feedback-Struktur" (Config → Feedback, neue Buttons „⬇ MD"/„⬇ PDF" im Header): alle Dimensionen/Kategorien/Kriterien inkl. Gewichtung und aller Beschreibungen (`FeedbackCategory.description`/`FeedbackCriterionItem.description`, siehe 2026-07-07/2026-07-15). (2) „Assignment-Typen-Bericht" (Config → Ass.-Typen, gleiche Buttons): pro Assignment-Typ die Phasen sowie das Kriterien-Mapping (`AssignmentType.criteriaIds`) — bei leerer/undefined `criteriaIds` „Alle Kriterien", sonst nach Dimension/Kategorie gruppierte Liste der zugeordneten Kriteriennamen (exakt dieselbe Lookup-Logik wie im „Kriterien"-Tab von `AssignmentTypeConfig.tsx`, nicht neu erfunden). Markdown-Erzeugung in neuer Datei `src/utils/structureExport.ts` (nutzt bestehendes `downloadFile()` aus `meetingExport.ts`), PDF-Dokumente als zwei weitere Komponenten in `pdfExport.tsx` (gleiche Styles/Pagination-Helfer wie die bestehenden Berichte). Bei der Verifikation aufgefallen, aber bewusst nicht angefasst: Phasen-Labels enthalten in den Bestandsdaten teils schon "(Lead)"/"(Kunde)" als Teil des Freitexts, wodurch der Bericht das zusätzlich per `PhaseTemplate.withWhom` angehängte "(Lead)"/"(Kunde)" doppelt zeigt — das ist eine Dateneingabe-Redundanz, keine Automatisierung, mit der ein Freitextfeld sicher bereinigt werden könnte, daher nicht angetastet.
|
||
|
||
**Erledigt (2026-07-15):** Analog zu `FeedbackCriterionItem.description` (2026-07-07) jetzt auch `FeedbackCategory.description` — Erläuterung, welche Themen/Beobachtungen in eine Kategorie einzahlen sollen, editierbar in `FeedbackStructureConfig.tsx` direkt unter dem Kategorienamen. Grund: Die KI kannte bisher nur die Kriterien-Ebene im Detail, nicht aber, wonach auf Kategorie-/Dimensionsebene grundsätzlich gefragt ist. Bewusst **keinen neuen Platzhalter** eingeführt, sondern die bestehende `{{KATEGORIEN_STRUKTUR}}` in `aiPrompt.ts` angereichert (`Name — Erläuterung` statt nur `Name`) — dadurch profitieren auch bereits bestehende, individuell angepasste Dimension-Prompts automatisch, ohne dass sie erneut synchronisiert werden müssen (der Platzhalter-Token selbst ändert sich nicht, nur was dahinter berechnet wird).
|
||
|
||
**Erledigt (2026-07-14, zweite Runde):** Zwei weitere Nachschärfungen der KI-Feedback-Generierung (siehe Eintrag oben). (1) Abschnittsmarker wie `[Background]`/`[Whiteboard - Storyboard]` (reine Navigationshilfe des Bewerters, an welcher Stelle der Präsentation/des Dialogs eine Beobachtung entstand) werden jetzt aus `{{NOTIZEN_ALLGEMEIN_MEETING}}`/`{{PROTOKOLL_UNZUGEORDNET}}` gefiltert, wenn ihnen keine Inhalts-Zeile folgt (`dropDanglingMarkers()` in `aiPrompt.ts`, Lookahead auf die jeweils nächste Zeile) — bleiben aber erhalten, wenn direkt danach noch etwas steht, da sie dann als Kontext-Header dienen. (2) `AI_CONFIG.defaultDimensionPromptTemplate` (der Basis-Prompt für neue Dimensionen und „Auf Standard zurücksetzen") ist nicht mehr rein im Code gepflegt, sondern zusätzlich in `appSettings` (Singleton-Tabelle, neue Spalte `defaultDimensionPromptTemplate`) persistiert und über einen neuen, aufklappbaren Bereich ganz oben im „KI-Prompts"-Tab (`AiPromptConfig.tsx`) direkt in der App editierbar — analog zum bestehenden Zeitgewichtungs-Faktor-Muster (`getOrSeedDefaultDimensionPromptTemplate()`/`saveDefaultDimensionPromptTemplate()` in `server/db/queries/appSettings.ts`, exakt wie `getOrSeedRatingTrendWeightStep()`). Der Code-Konstante dient jetzt nur noch als einmaliger Erstbefüllungswert für neue Installationen; `feedbackStructure.ts` (Auto-Anlage neuer Dimensionen, Backfill, Lazy-Create) liest den Default jetzt ausschließlich aus `appSettings`. Wie bei jeder Custom-Prompt-Änderung gilt weiterhin: bereits bestehende, individuell abweichende Dimension-Prompts ändern sich dadurch nicht automatisch mit.
|
||
|
||
**Erledigt (2026-07-13):** Papierkorb + Archiv für Assignments umgesetzt. Dashboard zeigt standardmäßig nur noch aktive Assignments (`statusFilter`-Default auf `'active'`), trashed/archivierte Assignments sind in keinem Status-Tab mehr sichtbar. `Assignment` hat zwei neue optionale Felder `deletedAt`/`archivedAt` (analog zu `MeetingInstance.deletedAt`). Löschen (`softDeleteAssignment`) ist nur möglich, wenn `status === 'done'` UND für **alle** `instituteeIds` ein Feedback mit Status `'final'` vorliegt (`isAssignmentFeedbackComplete()` in `server/db/queries/assignments.ts`) — Gate wird sowohl serverseitig erzwungen (wirft sonst) als auch clientseitig auf `AssignmentDetail.tsx` zum Deaktivieren des Buttons genutzt. Löschung ist zweistufig: `softDeleteAssignment` setzt nur `deletedAt` (Papierkorb, neue Seite `/assignments/trash`), `restoreAssignment` löscht es wieder (zurück nach „Abgeschlossen"). Nach 30 Tagen wird der Eintrag automatisch endgültig entfernt — kein Cron/Timer, sondern ein Lazy-Sweep (`purgeExpiredAssignmentTrash()`), der am Anfang von `listAssignmentsWithDetails()` mitläuft (Server läuft nicht dauerhaft, ein echter Scheduler wäre unzuverlässig). `hardDeleteAssignment()` kaskadiert manuell durch den kompletten Assignment-Baum (`meetingInstances` inkl. deren Kindern über das bestehende `hardDeleteMeetingInstance()`, `feedbackDrafts`, `assignmentFeedbacks` inkl. `feedbackCategoryRatings`/`feedbackDimensionTexts`/`feedbackCategoryTexts`) — Muster 1:1 von der bestehenden Meeting-Papierkorb-Kaskade in `server/db/queries/meetings.ts` übernommen. Zusätzlich unabhängiges Archiv (`archiveAssignment`/`unarchiveAssignment`, neue Seite `/assignments/archive`) für dauerhafte Aufbewahrung (spätere Auswertung/ML) — bewusst ohne Feedback-Zwang und ohne Lösch-Option in der UI, aber umkehrbar (Rückholen möglich). Neue Spalten `deletedAt`/`archivedAt` auf `assignments` werden automatisch per bestehendem `ALTER TABLE`-Migrationsmechanismus in `database.ts` nachgezogen, kein manuelles Migrationsskript nötig.
|
||
|
||
**Erledigt (2026-07-07):** KI-Feedback-Generierung neu konzipiert und umgesetzt — löst den zuvor diagnostizierten `response_format`/Markdown-Codefence-Bug strukturell statt punktuell. Statt einem Monolith-Call für alle Dimensionen macht `generateAllDimensions()` (`AssignmentFeedbackPage.tsx`) jetzt **sequenziell einen Call pro Dimension**, mit Fortschrittsanzeige und Teil-Fehlerbehandlung (fehlgeschlagene Dimensionen einzeln erneut versuchbar, `dimensionErrors`). Jede Dimension hat einen **vollständig frei editierbaren Prompt** (`FeedbackDimensionPrompt`, neuer Admin-Tab „KI-Prompts" → `AiPromptConfig.tsx`), der bei `addFeedbackDimension()` automatisch mit einem Default-Template vorbelegt wird (`AI_CONFIG.defaultDimensionPromptTemplate`) — neue Dimensionen funktionieren dadurch ohne Zutun. Antwortformat ist jetzt einfacher Text statt JSON (`KATEGORIE:`/`ACHIEVEMENTS:`/`DEVELOPMENT_NEEDS:`-Marker, geparst in `src/utils/aiPrompt.ts::parseDimensionAiResponse`), da pro Call ohnehin nur eine Dimension behandelt wird — macht den JSON/Codefence-Bug strukturell unmöglich statt ihn nur zu patchen. KI-generierte Texte landen zunächst als **Vorschlag** (`suggestedAchievements`/`suggestedDevelopmentNeeds` auf `FeedbackDimensionText`, `suggestedText` auf neuer Tabelle `FeedbackCategoryText`) und werden erst durch expliziten „Übernehmen"-Klick in die echten Felder kopiert (gleiches Muster wie die bestehende Rating-Vorschlags-Badge) — persistiert, übersteht also einen Seitenwechsel. Neu: kurzer KI-Text zusätzlich auf Kategorie-Ebene (`FeedbackCategoryText`), nicht nur auf Dimension-Ebene. Cross-Boundary-Falle dabei gefunden: `src/config/constants.ts` importierte bisher `FeedbackRating` aus dem Barrel `'../db'` (re-exportiert Dexie/rpcClient/Seeds) statt direkt aus `'../db/types'` — beim Import von `AI_CONFIG` in `server/db/queries/feedbackStructure.ts` hätte das transitiv Browser-only-Code (`fetch`, `localStorage`) in den Server-TS-Scope gezogen; auf direkten Typ-Import umgestellt und `src/config/constants.ts` zusätzlich explizit in `tsconfig.server.json`s `include` aufgenommen (gleiches Muster wie das dort schon gelistete `src/db/types.ts`). Nachträglich (gleicher Tag, nach erstem Testlauf) zwei Lücken gefixt/ergänzt: (1) `database.ts` migrierte nur neue Tabellen, nie Spalten einer bereits bestehenden Tabelle — `suggestedAchievements` u.ä. auf `feedbackDimensionTexts` fehlten dadurch auf bereits laufenden Installationen und scheiterten erst zur Laufzeit; jetzt gleicht `database.ts` zusätzlich per `PRAGMA table_info` ab und zieht fehlende Spalten per `ALTER TABLE ... ADD COLUMN` nach (siehe Datenbank-Abschnitt oben). (2) Prompt-Vorschau ergänzt: pro Dimension ein „👁 KI-Prompt ansehen"-Toggle auf `AssignmentFeedbackPage.tsx`, der den vollständig gerenderten Prompt (Template + aufgelöste Platzhalter) ohne API-Call anzeigt — Feedback war, dass die generierten Texte „brauchbar, aber nicht konkret genug" waren und ohne Einblick in den tatsächlich gesendeten Prompt nicht nachvollziehbar. Zusätzlich neues optionales Feld `FeedbackCriterionItem.description` (editierbar in `FeedbackStructureConfig.tsx`, direkt unter dem Kriteriumsnamen), das automatisch in `{{KRITERIEN_SCORES}}` einfließt (`src/utils/aiPrompt.ts`) — macht die KI-Vorgabe pro Kriterium einmal zentral pflegbar statt in jedem Dimension-Prompt wiederholt werden zu müssen. Bewusst **nicht** reaktiviert: die bereits vorhandene, aber seit Langem unverdrahtete Legacy-Tabelle `CriterionLevelDescription` (Text pro Kriterium UND Rating-Stufe) — wäre eine präzisere Rubrik gewesen, aber deutlich mehr Pflege-/UI-Aufwand; bei Bedarf spätere, bewusste Entscheidung.
|
||
|
||
**Erledigt (2026-07-04, zweite Runde):** Lokaler Node/Express-Server mit `node:sqlite` (statt IndexedDB) eingeführt, RPC-Bridge (`POST /api/rpc`) zwischen `src/db/queries/*.ts` (Client) und neuem `server/db/queries/*.ts` (Server) — siehe eigener Abschnitt "Datenbank: lokaler Server statt IndexedDB" oben für Details, Abweichungen (node:sqlite statt better-sqlite3) und die wichtige `npx tsc -b`-statt-`--noEmit`-Korrektur. Ziel: mehrere Browser auf demselben Laptop teilen sich dieselben Daten, unabhängig vom Browser-Cache, als Zwischenschritt vor einem Docker/Linux-Deployment.
|
||
|
||
**Erledigt (2026-07-04):** `db/queries/`-Layer eingeführt — alle 20 Komponenten/Utils mit vorherigem direktem `db.<table>.*`-Zugriff nutzen jetzt benannte Funktionen aus `src/db/queries/*.ts` (siehe eigener Abschnitt oben), reine Extraktion ohne beabsichtigte Verhaltensänderung. Dabei bewusst mitkonsolidiert: die drei unabhängig implementierten Varianten der gewichteten-Durchschnitt-Logik (`ratingTrend.ts`, `AssessmentTab.tsx`, `AssignmentDetail.tsx`) laufen jetzt über eine einzige Funktion `weightedAverageScore()`/`weightedAverageRating()` in `ratingTrend.ts` — dabei einen fehlenden `Σweight===0`-Guard in `AssignmentDetail.tsx` gefixt (identische Bug-Klasse wie die zweite Bug-Klasse oben, dort war er noch nicht behoben). Ebenfalls konsolidiert: die "AssignmentType.criteriaIds schränkt sichtbare Kriterien ein"-Filterlogik (vorher dupliziert in `MeetingView.tsx`/`AssignmentDetail.tsx`, jetzt `filterVisibleFeedbackStructure()`) und die doppelten Datenabfragen in `meetingExport.ts`s Markdown-/JSON-Export (jetzt `getMeetingExportData()` in `meetings.ts`). `dbBackup.ts` und Seed-Code bleiben bewusst bei direktem `db`-Zugriff (siehe Abschnitt oben). `npx tsc --noEmit` läuft fehlerfrei.
|
||
|
||
**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). N/A-Aggregationsbug in `buildAiPrompt` (`AssignmentFeedbackPage.tsx`) gefixt — war die letzte offene Stelle dieser Bug-Klasse außer der jetzt migrierten `FeedbackDraftPage.tsx`. `FeedbackDraftPage.tsx` vollständig auf `feedbackCriterionItems`/`feedbackCategories` migriert (letzter Konsument der Legacy-Tabellen ist jetzt nur noch der Seed-Code), Notizen jetzt korrekt pro Kriterium über `lineTags` statt aller Protokoll-Notizen ungefiltert an jedes Kriterium gehängt, Score-Anzeige auf `RATING_OPTIONS`-Labels umgestellt (statt `/5`-Rohwert). Toter Code (verwaiste AssignmentType/PhaseTemplate-CRUD-Reste) aus `FeedbackStructureConfig.tsx` entfernt. „Dashboard zeigt keine Assignment-Nummer" war bereits gelöst, nur die Doku war veraltet — richtiggestellt.
|
||
|
||
---
|
||
|
||
## Datenschutz
|
||
|
||
- Alle Daten lokal — seit 2026-07-04 in `server/data/assignment-monitor.sqlite3` (lokaler Server auf demselben Rechner) statt im Browser-IndexedDB; weiterhin **kein Cloud-Upload**, Server ist nur unter `localhost` erreichbar (kein Auth-Schutz nötig, solange das so bleibt — siehe Offene Punkte, falls sich das ändert, z.B. bei Docker/Linux-Deployment mit Netzwerkzugriff)
|
||
- `server/data/` ist per `.gitignore` ausgeschlossen — Datenbankdatei nie committen
|
||
- 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
|