diff --git a/CLAUDE.md b/CLAUDE.md
index 65ea664..066de99 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -116,17 +116,13 @@ MeetingView liest: `const { id, meetingId: meetingIdParam } = useParams()`
| 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).
+**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.
---
diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md
index 40f150c..b0d5e84 100644
--- a/docs/HANDOVER.md
+++ b/docs/HANDOVER.md
@@ -109,20 +109,16 @@ MeetingView liest den Param als: `const { id, meetingId: meetingIdParam } = useP
## Bekannte offene Punkte (Priorität nach Dringlichkeit)
-### Sofort-Fixes (einfach)
-1. **Dashboard zeigt keine Assignment-Nummer** — `Dashboard.tsx` Zeile ~57: `{a.assignmentNumber && ...{a.assignmentNumber}}` hinzufügen
-2. **Toter Code in FeedbackStructureConfig** — State-Variablen `assignmentTypes`, `phaseTemplates`, `showCatalogs`, `showTypeConfig`, `expandedType`, `newTypeName`, `newPhase`, `typeError` + alle Handler `toggleCatalogCategory`, `addAssignmentType`, `updateAssignmentTypeName`, `deleteAssignmentType`, `addPhaseTemplate`, `updatePhaseTemplate`, `deletePhaseTemplate` entfernen
-3. **N/A-Aggregationsbug**: Filter der Form `a.score !== null` schließen `'na'` nicht aus (N/A ist ein String, kein `null`!) → verfälscht Mittelwerte. Bereits gefixt in `Evaluation.tsx`. Noch offen in `AssignmentFeedbackPage.tsx:63` (buildAiPrompt) und `FeedbackDraftPage.tsx:66`.
-
### Mittlere Aufgaben
-4. **FeedbackDraftPage.tsx** — nutzt noch Legacy `categories`/`criteria`, sollte auf `feedbackCriterionItems` migriert werden (analog zu `Evaluation.tsx`); letzter Konsument der Legacy-Tabellen außer den Seeds
-5. **`db/queries/`-Layer** — DB-Zugriffe aus Komponenten in domänenspezifische Query-Funktionen extrahieren, bereitet SQLite-Migration vor
-6. **KI-Generierung funktionsunfähig** — `generateWithAi()` schlägt fehl, vermutlich Capgemini-Netzwerk/Firewall blockiert `openrouter.ai`. Root Cause nicht untersucht.
+1. **`db/queries/`-Layer** — DB-Zugriffe aus Komponenten in domänenspezifische Query-Funktionen extrahieren, bereitet SQLite-Migration vor
+2. **KI-Generierung funktionsunfähig** — `generateWithAi()` schlägt fehl, vermutlich Capgemini-Netzwerk/Firewall blockiert `openrouter.ai`. Root Cause nicht untersucht.
### Große Aufgaben (eigene Design-Session nötig)
-7. **SQLite WASM + OPFS** — Migration weg von IndexedDB. Konzept steht, Implementierung steht aus.
-8. **Mehrstufiges, pro Dimension konfigurierbares Prompt-System** — löst den aktuellen Einzel-Prompt in `buildAiPrompt` ab, braucht Spezialwissen je Dimension + Platzhaltersystem.
-9. **Selbstlernendes Tagging** — aus bisherigen Zeile-zu-Kriterium-Zuordnungen automatische Tag-Vorschläge beim Tippen ableiten.
+3. **SQLite WASM + OPFS** — Migration weg von IndexedDB. Konzept steht, Implementierung steht aus.
+4. **Mehrstufiges, pro Dimension konfigurierbares Prompt-System** — löst den aktuellen Einzel-Prompt in `buildAiPrompt` ab, braucht Spezialwissen je Dimension + Platzhaltersystem.
+5. **Selbstlernendes Tagging** — aus bisherigen Zeile-zu-Kriterium-Zuordnungen automatische Tag-Vorschläge beim Tippen ableiten.
+
+**Erledigt (2026-07-03, zweite Runde):** N/A-Aggregationsbug in `buildAiPrompt` gefixt. `FeedbackDraftPage.tsx` vollständig auf `feedbackCriterionItems`/`feedbackCategories` migriert (Notizen jetzt korrekt pro Kriterium über `lineTags`, Score-Anzeige über `RATING_OPTIONS`). Toter Code in `FeedbackStructureConfig.tsx` entfernt. "Dashboard zeigt keine Assignment-Nummer" war bereits gelöst — nur die Doku war veraltet.
**Erledigt (2026-07-02):** Zeitgewichteter Bewertungs-Vorschlag mit Trend-Erkennung — `src/utils/ratingTrend.ts` berechnet pro Kategorie einen zeitgewichteten Vorschlag aus den Meeting-Assessments (Gewichtungsfaktor konfigurierbar unter Config → Gewichtung, neue Tabelle `appSettings`), erkennt Verbesserung/Verschlechterung über die Laufzeit und zeigt beides als Badge auf `AssignmentFeedbackPage` (mit „Übernehmen" + „Trend in Text übernehmen"). `buildAiPrompt` wurde um einen Trend-Abschnitt erweitert; dabei wurde auch der alte `db.criteria`-Bug mitgefixt.
diff --git a/docs/STATUS_UND_OFFENE_PUNKTE.md b/docs/STATUS_UND_OFFENE_PUNKTE.md
index ec8cb65..e5819f4 100644
--- a/docs/STATUS_UND_OFFENE_PUNKTE.md
+++ b/docs/STATUS_UND_OFFENE_PUNKTE.md
@@ -45,6 +45,9 @@ Stand: 2026-07-03
| Assignment-Nummer ist Pflichtfeld beim Anlegen | `AssignmentCreate.tsx` |
| Kriterium-Gewichtung feiner (0,0–2,0 in 0,1-Schritten statt `×1`–`×5`), inkl. Division-durch-Null-Fix bei Gewicht 0 in allen gewichteten Mittelwerten | `FeedbackStructureConfig.tsx`, `AssessmentTab.tsx`, `ratingTrend.ts` |
| Sprecher eines Protokoll-Beitrags nachträglich änderbar (im Bearbeiten-Modus) | `ConversationTab.tsx`, `MeetingView.tsx` |
+| N/A-Aggregationsbug in `buildAiPrompt` gefixt (letzte offene Stelle außer Legacy-Code) | `AssignmentFeedbackPage.tsx` |
+| `FeedbackDraftPage.tsx` auf `feedbackCriterionItems`/`feedbackCategories` migriert, Notizen korrekt pro Kriterium über `lineTags` (nicht mehr alle Protokoll-Notizen an jedes Kriterium), Score-Anzeige auf `RATING_OPTIONS`-Labels | `FeedbackDraftPage.tsx` |
+| Toter Code (verwaiste AssignmentType/PhaseTemplate-CRUD-Reste) entfernt | `FeedbackStructureConfig.tsx` |
---
@@ -56,7 +59,6 @@ Stand: 2026-07-03
|---|---|---|---|
| H1 | **IndexedDB-Robustheit**: Browser kann IndexedDB unter Speicherdruck löschen. Migration zu SQLite WASM + OPFS geplant. Bis dahin: regelmäßige Backups als einzige Sicherheit. | `db/schema.ts` | L (Wochen) |
| H2 | **Backup-Reminder fehlt**: Es gibt keine automatische Erinnerung / keinen Hinweis nach abgeschlossenem Meeting, dass ein Backup erstellt werden sollte. | `pages/meeting/MeetingView.tsx` | S |
-| H3 | **N/A-Aggregationsbug**: Filter `a.score !== null` schließt den String `'na'` nicht aus, verfälscht Mittelwerte ("Fantasiewerte"). Gefixt in `Evaluation.tsx`, noch offen in `AssignmentFeedbackPage.tsx:63` und `FeedbackDraftPage.tsx:66`. | `AssignmentFeedbackPage.tsx`, `FeedbackDraftPage.tsx` | XS |
| H6 | **KI-Generierung funktionsunfähig**: `generateWithAi()` schlägt beim Ausführen fehl, vermutlich Capgemini-Netzwerk/Firewall blockiert den Call zu `openrouter.ai`. Root Cause noch nicht untersucht. | `AssignmentFeedbackPage.tsx` | ? |
| H7 | **Prompt-System-Neubau (noch nicht gescoped)**: Mehrstufiges, pro Dimension konfigurierbares Prompt-System mit Spezialwissen je Dimension + Platzhaltersystem, löst den aktuellen Einzel-Prompt in `buildAiPrompt` ab. Braucht eigene Design-Session. | `AssignmentFeedbackPage.tsx` | L |
@@ -64,7 +66,6 @@ Stand: 2026-07-03
| # | Beschreibung | Datei | Aufwand |
|---|---|---|---|
-| M4 | **FeedbackDraftPage.tsx veraltet**: Nutzt noch Legacy-`Category`/`Criterion`-Tabellen, analog zum ehemaligen Zustand von `Evaluation.tsx`. Letzter verbleibender Konsument der Legacy-Tabellen außer den Seeds. | `FeedbackDraftPage.tsx` | M |
| M5 | **`db/queries/`-Layer fehlt**: DB-Zugriffe direkt aus Komponenten (`db.*`), keine Abstraktionsschicht. Erschwert die geplante SQLite-Migration (H1). | `src/db/queries/` (neu) | M |
| M6 | **Selbstlernendes Tagging (Idee, noch nicht gescoped)**: Zeile-zu-Kriterium-Zuordnungen sammeln und daraus beim Tippen automatische Tag-Vorschläge ableiten. Braucht eigene Design-Session. | `CriterionTagPicker.tsx` | ? |
@@ -72,8 +73,6 @@ Stand: 2026-07-03
| # | Beschreibung | Datei | Aufwand |
|---|---|---|---|
-| N1 | **Dashboard**: Zeigt keine Assignment-Nummer. Nur Titel + Kunde + Typ. | `Dashboard.tsx` | XS |
-| N3 | **FeedbackStructureConfig**: Toter Code — Funktionen für AssignmentType-CRUD wurden verschoben, Deklarationen blieben (`assignmentTypes`, `phaseTemplates` State + alle zugehörigen Handler). | `FeedbackStructureConfig.tsx` | XS |
| N5 | **AssignmentCreate**: Erstellt keine `instituteeEnrollments`. Von/Bis-Daten können nur über Edit gepflegt werden. | `AssignmentCreate.tsx` | S |
| N6 | **Meeting-Route**: URL ist `/assignment/:id/meeting/:meetingId` — `:id` ist die Assignment-ID, aber die Assignment-Nummer (z.B. CGI-2024-001) ist nicht in der URL. Akzeptiert als nice-to-have. | `App.tsx` | M |
diff --git a/docs/TECHNISCHE_ARCHITEKTUR.md b/docs/TECHNISCHE_ARCHITEKTUR.md
index 4a907df..061bd9a 100644
--- a/docs/TECHNISCHE_ARCHITEKTUR.md
+++ b/docs/TECHNISCHE_ARCHITEKTUR.md
@@ -43,7 +43,7 @@ src/
AssessmentTab.tsx ← Gesamtbewertungs-UI (Accordion je Kategorie), Notation-Vorschlag
CriterionTagPicker.tsx ← Chip+Popover mit Suche, Kriterium-Zuordnung pro Protokoll-Zeile
AssignmentFeedbackPage.tsx ← Strukturiertes Abschluss-Feedback + KI-Generierung + Trend-Vorschlag
- FeedbackDraftPage.tsx ← Einfacher Feedback-Entwurf (Legacy, nutzt noch alte Criterion-Tabelle)
+ FeedbackDraftPage.tsx ← Einfacher Feedback-Entwurf (Legacy-UI, Datenzugriff auf feedbackCriterionItems migriert)
Evaluation.tsx ← Auswertung — zeitgewichteter Bewertungs-Vorschlag + Trend, gruppiert nach Assignment
Configuration.tsx ← Tab-Container für alle Konfigurationsseiten
FeedbackStructureConfig.tsx ← Dimensionen/Kategorien/Kriterien CRUD + Gewichtung
@@ -66,7 +66,7 @@ src/
### DB-Version: 6
```typescript
-// Konfiguration (Legacy, nur noch für FeedbackDraftPage.tsx + Seeds)
+// Konfiguration (Legacy, kein aktiver Seiten-Konsument mehr)
categories: '++id, order'
criteria: '++id, categoryId, order'
@@ -286,7 +286,7 @@ const buildFilename = (ext: string) => {
Die App legt beim ersten Start automatisch an:
1. **3 Assignment-Typen** mit je vollständigem Phasen-Flow (Generisch, Case Interview, Stakeholder Meeting / Pitch)
-2. **Legacy-Kriterien** in `categories` + `criteria` (nur noch von `FeedbackDraftPage.tsx` genutzt, `Evaluation.tsx` ist längst auf `feedbackCriterionItems` migriert)
+2. **Legacy-Kriterien** in `categories` + `criteria` (kein aktiver Seiten-Konsument mehr — `Evaluation.tsx` und `FeedbackDraftPage.tsx` sind beide auf `feedbackCriterionItems` migriert; die Tabellen bleiben aus historischen Gründen im Backup/Import)
3. **Feedback-Dimensionsstruktur** mit 5 Dimensionen, 14 Kategorien, ~70 Kriterium-Items (Capgemini-Kompetenzmodell)
4. **Criterion-Mappings** (Legacy, nicht mehr aktiv genutzt)
@@ -296,13 +296,9 @@ Die App legt beim ersten Start automatisch an:
| Bereich | Problem | Priorität |
|---|---|---|
-| `FeedbackDraftPage.tsx` | Nutzt alte `Category`/`Criterion`-Tabellen statt neue `feedbackCriterionItems`; letzter Konsument der Legacy-Tabellen außer den Seeds | Mittel |
| `meetingExport.ts` | Bewertungs-Labels im Markdown-Export zeigen `FeedbackRating`-Keys (z.B. `client_ready`) statt sprechender Labels | Niedrig |
-| `FeedbackStructureConfig.tsx` | Enthält toten Code (Funktionen für gelöschte AssignmentType-CRUD) | Niedrig |
-| AI-Prompt & `FeedbackDraftPage` | N/A-Aggregationsbug: `score !== null` schließt `'na'` nicht aus, verfälscht Mittelwerte | Hoch |
| KI-Generierung | `generateWithAi()` schlägt fehl (vermutlich Netzwerk/Firewall), Root Cause offen | Hoch |
| Prompt-System | Aktueller Einzel-Prompt soll mehrstufig/pro Dimension konfigurierbar werden — eigene Design-Session nötig | Hoch (nicht gescoped) |
| Selbstlernendes Tagging | Idee, automatische Tag-Vorschläge aus bisherigen Zuordnungen abzuleiten — eigene Design-Session nötig | Mittel (nicht gescoped) |
-| Dashboard | Zeigt keine Assignment-Nummer | Niedrig |
| IndexedDB | Keine robuste Persistenz-Garantie (Browser kann löschen) | Hoch → SQLite WASM geplant |
| `db/queries/`-Layer | Fehlt — DB-Zugriffe direkt aus Komponenten, erschwert SQLite-Migration | Mittel |
diff --git a/src/pages/AssignmentFeedbackPage.tsx b/src/pages/AssignmentFeedbackPage.tsx
index 773d4ea..8fe0405 100644
--- a/src/pages/AssignmentFeedbackPage.tsx
+++ b/src/pages/AssignmentFeedbackPage.tsx
@@ -71,7 +71,7 @@ async function buildAiPrompt(
const crits = items.filter(i => critIds.includes(i.id!))
const scoreLines = crits.map(c => {
- const scores = instAssessments.filter(a => a.criteriaId === c.id && a.score !== null)
+ const scores = instAssessments.filter(a => a.criteriaId === c.id && a.score !== null && a.score !== 'na')
const avg = scores.length > 0
? (scores.reduce((s, a) => s + ratingToNum(a.score), 0) / scores.length).toFixed(1)
: '—'
diff --git a/src/pages/FeedbackDraftPage.tsx b/src/pages/FeedbackDraftPage.tsx
index e15e8bb..d283ada 100644
--- a/src/pages/FeedbackDraftPage.tsx
+++ b/src/pages/FeedbackDraftPage.tsx
@@ -4,19 +4,30 @@ import {
db,
type Assignment,
type Consultant,
- type Category,
- type Criterion,
+ type FeedbackCategory,
+ type FeedbackCriterionItem,
type FeedbackDraft,
type FeedbackRating,
} from '../db'
+import { RATING_OPTIONS, RATING_NUM_MAP } from '../config/constants'
+import { numberToRating } from '../utils/ratingTrend'
-const ratingToNum = (r: FeedbackRating | null | undefined): number => {
- const m: Record = { na: 0, not_client_ready: 1, partially_client_ready: 2, nearly_client_ready: 3, client_ready: 4 }
- return r ? (m[r] ?? 0) : 0
+const ratingToNum = (r: FeedbackRating | null | undefined): number =>
+ r ? (RATING_NUM_MAP[r] ?? 0) : 0
+
+const ratingLabel = (avg: number | null): string => {
+ if (avg === null) return '—'
+ const r = RATING_OPTIONS.find(x => x.value === numberToRating(avg))
+ return r ? `${r.short} (${avg.toFixed(1)})` : avg.toFixed(1)
+}
+
+const scoreColor = (avg: number | null): string => {
+ if (avg === null) return 'bg-gray-100 text-gray-400'
+ return RATING_OPTIONS.find(r => r.value === numberToRating(avg))?.color ?? 'bg-gray-100 text-gray-400'
}
interface CriterionSummary {
- criterion: Criterion
+ criterion: FeedbackCriterionItem
avg: number | null
notes: string[]
}
@@ -29,7 +40,7 @@ export default function FeedbackDraftPage() {
const [assignment, setAssignment] = useState(null)
const [institutee, setInstitutee] = useState(null)
- const [categories, setCategories] = useState([])
+ const [categories, setCategories] = useState([])
const [summary, setSummary] = useState([])
const [draft, setDraft] = useState(null)
const [content, setContent] = useState('')
@@ -40,7 +51,7 @@ export default function FeedbackDraftPage() {
const [a, cons, cats] = await Promise.all([
db.assignments.get(assignmentId),
db.consultants.get(instId),
- db.categories.orderBy('order').toArray(),
+ db.feedbackCategories.orderBy('order').toArray(),
])
setAssignment(a ?? null)
setInstitutee(cons ?? null)
@@ -56,21 +67,25 @@ export default function FeedbackDraftPage() {
const instAssessments = allAssessments.filter(a => a.instituteeId === instId)
const instEntries = allEntries.filter(e => e.instituteeId === instId)
+ const allLineTags = instEntries.flatMap(e => e.lineTags ?? [])
- const criteriaIds = [...new Set(instAssessments.map(a => a.criteriaId))]
+ const criteriaIds = [...new Set([
+ ...instAssessments.map(a => a.criteriaId),
+ ...allLineTags.map(t => t.criterionItemId),
+ ])]
const crits = criteriaIds.length > 0
- ? await db.criteria.where('id').anyOf(criteriaIds).toArray()
+ ? await db.feedbackCriterionItems.where('id').anyOf(criteriaIds).toArray()
: []
const sum: CriterionSummary[] = crits.map(crit => {
- const relevant = instAssessments.filter(a => a.criteriaId === crit.id && a.score !== null)
+ const relevant = instAssessments.filter(a => a.criteriaId === crit.id && a.score !== null && a.score !== 'na')
const avg = relevant.length > 0
? relevant.reduce((s, a) => s + ratingToNum(a.score), 0) / relevant.length
: null
const notes = [
...instAssessments.filter(a => a.criteriaId === crit.id && a.note).map(a => a.note),
- ...instEntries.filter(e => e.note).map(e => e.note),
- ].filter(Boolean)
+ ...allLineTags.filter(t => t.criterionItemId === crit.id).map(t => t.text),
+ ]
return { criterion: crit, avg, notes }
})
setSummary(sum)
@@ -104,8 +119,7 @@ export default function FeedbackDraftPage() {
for (const [catId, items] of grouped) {
text += `## ${catMap.get(catId) ?? 'Sonstige'}\n`
for (const item of items) {
- const avgStr = item.avg !== null ? item.avg.toFixed(1) : '—'
- text += `${item.criterion.name}: ${avgStr}/5\n`
+ text += `${item.criterion.name}: ${ratingLabel(item.avg)}\n`
if (item.notes.length > 0) {
const uniqueNotes = [...new Set(item.notes)]
text += uniqueNotes.map(n => ` • ${n}`).join('\n') + '\n'
@@ -146,13 +160,6 @@ export default function FeedbackDraftPage() {
}, 600)
}
- const scoreColor = (avg: number | null) => {
- if (avg === null) return 'bg-gray-100 text-gray-400'
- if (avg >= 4) return 'bg-green-100 text-green-700'
- if (avg >= 3) return 'bg-yellow-100 text-yellow-700'
- return 'bg-red-100 text-red-700'
- }
-
if (!assignment || !institutee) return Lädt…
return (
@@ -179,7 +186,7 @@ export default function FeedbackDraftPage() {
className="flex items-center justify-between py-1.5 border-b border-gray-50 last:border-0">
{s.criterion.name}
- {s.avg !== null ? s.avg.toFixed(1) : '—'}
+ {ratingLabel(s.avg)}
))}
diff --git a/src/pages/FeedbackStructureConfig.tsx b/src/pages/FeedbackStructureConfig.tsx
index ce3cd54..96f0167 100644
--- a/src/pages/FeedbackStructureConfig.tsx
+++ b/src/pages/FeedbackStructureConfig.tsx
@@ -1,20 +1,12 @@
import { useEffect, useState } from 'react'
-import { db, type FeedbackDimension, type FeedbackCategory, type FeedbackCriterionItem, type AssignmentType, type PhaseTemplate } from '../db'
+import { db, type FeedbackDimension, type FeedbackCategory, type FeedbackCriterionItem } from '../db'
export default function FeedbackStructureConfig() {
const [dimensions, setDimensions] = useState([])
const [categories, setCategories] = useState([])
const [items, setItems] = useState([])
- const [assignmentTypes, setAssignmentTypes] = useState([])
const [expanded, setExpanded] = useState>(new Set())
const [expandedCat, setExpandedCat] = useState>(new Set())
- const [showCatalogs, setShowCatalogs] = useState(false)
- const [phaseTemplates, setPhaseTemplates] = useState([])
- const [showTypeConfig, setShowTypeConfig] = useState(false)
- const [expandedType, setExpandedType] = useState(null)
- const [newTypeName, setNewTypeName] = useState('')
- const [newPhase, setNewPhase] = useState>({})
- const [typeError, setTypeError] = useState>({})
// Neue Einträge
const [newDimName, setNewDimName] = useState('')
@@ -22,18 +14,14 @@ export default function FeedbackStructureConfig() {
const [newItemName, setNewItemName] = useState>({})
const load = async () => {
- const [dims, cats, itms, types, phases] = await Promise.all([
+ const [dims, cats, itms] = await Promise.all([
db.feedbackDimensions.orderBy('order').toArray(),
db.feedbackCategories.orderBy('order').toArray(),
db.feedbackCriterionItems.orderBy('order').toArray(),
- db.assignmentTypes.toArray(),
- db.phaseTemplates.toArray(),
])
setDimensions(dims)
setCategories(cats)
setItems(itms)
- setAssignmentTypes(types)
- setPhaseTemplates(phases)
}
useEffect(() => { load() }, [])
@@ -125,70 +113,6 @@ export default function FeedbackStructureConfig() {
setItems(prev => prev.map(i => i.id === id ? { ...i, weight } : i))
}
- const toggleCatalogCategory = async (type: AssignmentType, catId: number) => {
- const current = type.criteriaIds ?? []
- const next = current.includes(catId) ? current.filter(id => id !== catId) : [...current, catId]
- await db.assignmentTypes.update(type.id!, { criteriaIds: next })
- setAssignmentTypes(prev => prev.map(t => t.id === type.id ? { ...t, criteriaIds: next } : t))
- }
-
- // ── AssignmentType CRUD ───────────────────────────────────────────────────
-
- const addAssignmentType = async () => {
- if (!newTypeName.trim()) return
- await db.assignmentTypes.add({ name: newTypeName.trim(), defaultPhaseKeys: [] })
- setNewTypeName('')
- await load()
- }
-
- const updateAssignmentTypeName = async (id: number, name: string) => {
- await db.assignmentTypes.update(id, { name })
- setAssignmentTypes(prev => prev.map(t => t.id === id ? { ...t, name } : t))
- }
-
- const deleteAssignmentType = async (id: number) => {
- const count = await db.assignments.where('assignmentTypeId').equals(id).count()
- if (count > 0) {
- setTypeError(prev => ({ ...prev, [id]: `Kann nicht gelöscht werden – ${count} Assignment(s) verwenden diesen Typ.` }))
- return
- }
- await db.phaseTemplates.where('assignmentTypeId').equals(id).delete()
- await db.assignmentTypes.delete(id)
- setTypeError(prev => { const n = { ...prev }; delete n[id]; return n })
- await load()
- }
-
- // ── PhaseTemplate CRUD ────────────────────────────────────────────────────
-
- const addPhaseTemplate = async (typeId: number) => {
- const p = newPhase[typeId]
- if (!p?.label.trim()) return
- const existing = phaseTemplates.filter(t => t.assignmentTypeId === typeId)
- await db.phaseTemplates.add({
- assignmentTypeId: typeId,
- phaseKey: p.label.trim().toLowerCase().replace(/\s+/g, '_'),
- label: p.label.trim(),
- withWhom: p.withWhom,
- isRepeatable: p.isRepeatable,
- hasConversation: p.hasConversation,
- hasAssessment: p.hasAssessment,
- criteriaIds: [],
- order: existing.length + 1,
- })
- setNewPhase(prev => ({ ...prev, [typeId]: { label: '', withWhom: 'lead', isRepeatable: false, hasConversation: false, hasAssessment: false } }))
- await load()
- }
-
- const updatePhaseTemplate = async (id: number, fields: Partial) => {
- await db.phaseTemplates.update(id, fields)
- setPhaseTemplates(prev => prev.map(t => t.id === id ? { ...t, ...fields } : t))
- }
-
- const deletePhaseTemplate = async (id: number) => {
- await db.phaseTemplates.delete(id)
- setPhaseTemplates(prev => prev.filter(t => t.id !== id))
- }
-
const sorted = [...dimensions].sort((a, b) => a.order - b.order)
return (