# Technische Architektur — Assignment Monitor ## Stack | Schicht | Technologie | Version | Zweck | |---|---|---|---| | UI-Framework | React | 18 | Component-basiertes UI | | Sprache | TypeScript | 5 | Typsicherheit | | Build | Vite | 6 | Dev-Server, HMR, PWA-Plugin | | CSS | Tailwind CSS | v4 | Utility-First, `@import "tailwindcss"` | | Datenbank | Dexie.js | 5 | IndexedDB-Wrapper | | Routing | React Router | v6 | Client-Side-Routing (SPA) | | PWA | vite-plugin-pwa | — | Offline-Fähigkeit, installierbar | | KI | OpenRouter API | — | Feedback-Generierung (extern, opt-in) | **Capgemini Brand Color:** `#0070AD` --- ## Projektstruktur (aktualisiert nach Architektur-Refactoring, 2026-07-02) ``` src/ config/ constants.ts ← RATING_OPTIONS, INST_COLORS, AI_CONFIG, RATING_NUM_MAP, BRAND_COLOR, DEFAULT_RATING_TREND_WEIGHT_STEP, RATING_TREND_DELTA_THRESHOLD db/ index.ts ← reiner Re-Export von types/schema/seeds (bestehende Imports bleiben kompatibel) types.ts ← alle Interfaces, FeedbackRating-Typ schema.ts ← Dexie DB-Klasse + alle Migrationsversionen seeds/ assignmentTypes.ts ← seedIfEmpty() — 3 AssignmentTypen + Legacy-Kriterien feedbackStructure.ts ← seedFeedbackStructureIfEmpty() — Dimensionen/Kategorien/Items migrations.ts ← seedCriterionMappingsIfEmpty(), migrateToUnifiedCriteriaIfNeeded() index.ts ← Re-Export der Seed-Funktionen pages/ Dashboard.tsx ← Assignment-Liste (Home) AssignmentCreate.tsx ← Neues Assignment anlegen AssignmentDetail.tsx ← Phasen-Flow, Meeting-Übersicht, Bewertungs-Chips AssignmentEdit.tsx ← Assignment nachträglich bearbeiten meeting/ MeetingView.tsx ← Route-Komponente: State, DB-Handler, Header, Tabs ConversationTab.tsx ← Gesprächsprotokoll-UI 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-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 AssignmentTypeConfig.tsx ← AssignmentTypen + Phasen + Kriterien-Auswahl (Baum) RatingWeightConfig.tsx ← Config-Tab "Gewichtung": Zeitgewichtungs-Faktor für Bewertungs-Vorschlag Consultants.tsx ← Berater/Gruppen-Verwaltung utils/ meetingExport.ts ← Markdown + JSON Export-Logik dbBackup.ts ← Vollständiges DB-Backup (JSON Export/Import) notationParser.tsx ← +/!/>/[…] Notation → farbiges JSX, splitNoteLines(), detectNotationRating() ratingTrend.ts ← Zeitgewichteter Bewertungs-Vorschlag + Trend-Erkennung App.tsx ← Route-Definitionen, Navigation main.tsx ← React-Einstiegspunkt, DB-Seed-Aufruf ``` --- ## Datenmodell (IndexedDB via Dexie v5) ### DB-Version: 6 ```typescript // Konfiguration (Legacy, kein aktiver Seiten-Konsument mehr) categories: '++id, order' criteria: '++id, categoryId, order' // Assignment-Typen assignmentTypes: '++id' phaseTemplates: '++id, assignmentTypeId, order' // Stammdaten groups: '++id' consultants: '++id, groupId' // Assignments assignments: '++id, status, createdAt' meetingInstances: '++id, assignmentId, phaseTemplateId' // Gesprächsprotokoll conversationEntries: '++id, meetingInstanceId, instituteeId, sequenceIndex' conversationSkillScores: '++id, conversationEntryId, criteriaId' // Meeting-Assessment assessments: '++id, meetingInstanceId, instituteeId, criteriaId' // Feedback-Entwürfe (Legacy) feedbackDrafts: '++id, assignmentId, instituteeId' // Strukturiertes Feedback feedbackDimensions: '++id, order' feedbackCategories: '++id, dimensionId, order' feedbackCriterionItems: '++id, categoryId, order' assignmentFeedbacks: '++id, assignmentId, instituteeId' feedbackCategoryRatings: '++id, assignmentFeedbackId, feedbackCategoryId' feedbackDimensionTexts: '++id, assignmentFeedbackId, feedbackDimensionId' // Mappings (Legacy, kaum genutzt) criterionCategoryMappings: '++id, criterionId, feedbackCategoryId' criterionLevelDescriptions: '++id, criterionItemId, rating' // App-Einstellungen (Singleton, id immer 1), seit Version 6 appSettings: '++id' ``` `ConversationEntry.lineTags` (Kriterium-Zuordnung pro Zeile) ist ein reines Objektfeld ohne eigenen Index — kein Versions-Bump nötig, da Dexies `.stores()` nur Indizes definiert, nicht das volle Objekt-Schema. ### Wichtige Interface-Details ```typescript type FeedbackRating = 'na' | 'not_client_ready' | 'partially_client_ready' | 'nearly_client_ready' | 'client_ready' interface AssignmentType { id?: number name: string defaultPhaseKeys: string[] criteriaIds?: number[] // FeedbackCriterionItem IDs; leer = alle anzeigen } interface Assignment { id?: number title: string client: string assignmentTypeId: number instituteeIds: number[] leadName: string status: 'active' | 'done' createdAt: string assignmentNumber?: string // z.B. "CGI-2024-001" description?: string instituteeEnrollments?: Array<{ instituteeId: number from?: string // ISO date to?: string // ISO date }> } interface MeetingInstance { id?: number assignmentId: number phaseTemplateId: number phaseIndex: number // 1 = erste Instanz, 2+ = Wiederholungen date: string status: 'planned' | 'active' | 'done' generalNotes: string deletedAt?: string // Soft-Delete } interface FeedbackCriterionItem { id?: number categoryId: number name: string weight?: number // 0,0–2,0 in 0,1-Schritten, default 1 (seit 2026-07-03; vorher grobe ×1–×5-Stufen) order: number } interface ConversationLineTag { text: string // exakter Zeilentext, 1:1 wie in ConversationEntry.note criterionItemId: number // → FeedbackCriterionItem.id } interface ConversationEntry { id?: number meetingInstanceId: number instituteeId: number sequenceIndex: number note: string // ganzer Sprecher-Turn, mehrzeilig fillerCount: number updatedAt: string lineTags?: ConversationLineTag[] // Kriterium-Zuordnung pro Zeile, gematcht per Zeilentext } interface AppSettings { id?: number // Singleton, immer 1 ratingTrendWeightStep: number // Zeitgewichtungs-Faktor für den Bewertungs-Vorschlag } // Assessment.criteriaId → FeedbackCriterionItem.id (Gesamtbewertung) // ConversationSkillScore.criteriaId → FeedbackCategory.id (Schnellbewertung) // ConversationLineTag.criterionItemId → FeedbackCriterionItem.id (Zeilen-Zuordnung, bewusst anders benannt) ``` --- ## Routing ``` / Dashboard (Assignment-Liste) /assignment/new Neues Assignment anlegen /assignment/:id AssignmentDetail (Phasen-Flow) /assignment/:id/edit Assignment bearbeiten /assignment/:id/meeting/:meetingId MeetingView (Protokoll + Bewertung) /assignment/:id/feedback/:instituteeId AssignmentFeedbackPage (strukturiertes Feedback) /assignment/:id/feedback-draft/:instituteeId FeedbackDraftPage (Legacy) /evaluation Auswertungsübersicht /consultants Berater-Verwaltung /config Konfiguration (4 Tabs: Feedback | Ass.-Typen | Gewichtung | Backup) ``` --- ## Konfigurationsbereich ### Tab "Feedback" → `FeedbackStructureConfig` CRUD für: - Feedback-Dimensionen (mit Reihenfolge ↑/↓) - Feedback-Kategorien je Dimension - Feedback-Kriterium-Items je Kategorie (mit Gewichtung `0,0–2,0` in `0,1`-Schritten) ### Tab "Ass.-Typen" → `AssignmentTypeConfig` CRUD für: - Assignment-Typen (Name, Löschschutz wenn Assignments vorhanden) - Phasen-Templates je Typ (Label, Lead/Kunde, Wiederholbar, Protokoll, Bewertung) - Kriterien-Auswahl je Typ: Baumstruktur Dimension → Kategorie → Item (mit Checkbox, Indeterminate-State) - `criteriaIds = []` → alle Kriterien werden angezeigt ### Tab "Gewichtung" → `RatingWeightConfig` - Ein Regler für `AppSettings.ratingTrendWeightStep` (Zeitgewichtungs-Faktor des Bewertungs-Vorschlags), mit Live-Vorschau der resultierenden Gewichte ### Tab "Backup" → in `Configuration.tsx` - JSON-Export aller Tabellen - Zweistufiger JSON-Import mit Überschreib-Warnung --- ## Bewertungs-Aggregation (Implementierungsdetail) ```typescript const NUM_MAP: Record = { na: 0, not_client_ready: 1, partially_client_ready: 2, nearly_client_ready: 3, client_ready: 4 } function catMode(cat: FeedbackCategory, fbItems: FeedbackCriterionItem[], getScore: (itemId: number) => FeedbackRating | null): FeedbackRating | null { const scored = fbItems .filter(i => i.categoryId === cat.id) .map(i => ({ score: getScore(i.id!), w: i.weight ?? 1 })) .filter((x): x is { score: FeedbackRating; w: number } => x.score !== null && x.score !== 'na') if (scored.length === 0) return null const wSum = scored.reduce((s, x) => s + x.w, 0) if (wSum === 0) return null // alle beteiligten Kriterien auf Gewicht 0 → sonst NaN < x überall false, fällt auf "Fully" durch const avg = scored.reduce((s, x) => s + NUM_MAP[x.score] * x.w, 0) / wSum if (avg < 1.5) return 'not_client_ready' if (avg < 2.5) return 'partially_client_ready' if (avg < 3.5) return 'nearly_client_ready' return 'client_ready' } ``` Dieselbe `Σweight === 0`-Absicherung gilt für jede gewichtete Mittelung in der App (auch `buildGroupMeetingScores()` in `ratingTrend.ts`) — seit Gewicht `0` ein gültiger Wert ist, ist das kein theoretischer Randfall mehr. --- ## Export-Dateiname-Schema ```typescript const buildFilename = (ext: string) => { const date = new Date(meeting.date).toLocaleDateString('de-DE').replace(/\./g, '-') const label = template.label // z.B. "Alignment Call" const idx = phaseIndex > 1 ? `-${phaseIndex}` : '' const id = assignment.assignmentNumber ?? assignment.title return `${id}_${date}_${label}${idx}.${ext}` // Beispiel: CGI-2024-001_02-07-2026_Alignment Call-2.md } ``` --- ## KI-Integration (AssignmentFeedbackPage) - **Provider:** OpenRouter (https://openrouter.ai) — ermöglicht Modellwechsel ohne eigenen API-Zugang - **Modelle:** GPT-4o mini, GPT-4o, Claude Sonnet 4.5, Claude Haiku 4.5, Gemini Flash 1.5 - **API-Key:** Lokal in `localStorage` gespeichert (`ai_api_key`, `ai_model`) - **Datenweitergabe:** Kriterien-Scores + Notizen (Assessment und getaggte Protokoll-Zeilen zusammengeführt pro Kriterium), Kategorie-Bewertungen, zeitgewichteter Trend pro Kategorie werden im Prompt strukturiert übermittelt (`buildAiPrompt`, nutzt korrekt `feedbackCriterionItems`, alter `db.criteria`-Bug ist gefixt) - **Bekanntes Problem:** Der Call schlägt aktuell fehl (vermutlich Capgemini-Netzwerk/Firewall blockiert `openrouter.ai`), Root Cause nicht untersucht - **Geplanter Umbau (nicht gescoped):** Mehrstufiges, pro Dimension konfigurierbares Prompt-System mit Platzhaltersystem soll den aktuellen Einzel-Prompt ablösen --- ## Seed-Daten (beim ersten Start) 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` (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) --- ## Bekannte technische Schulden | Bereich | Problem | Priorität | |---|---|---| | `meetingExport.ts` | Bewertungs-Labels im Markdown-Export zeigen `FeedbackRating`-Keys (z.B. `client_ready`) statt sprechender Labels | Niedrig | | 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) | | 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 |