From 068c4cdcfd84819e8bc9de5a3af71ff121e24ac6 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 2 Jul 2026 10:24:03 +0200 Subject: [PATCH] refactor: db/index.ts in types, schema und seeds aufgeteilt - types.ts: alle Interfaces und FeedbackRating-Typ - schema.ts: Dexie-Klasse + Migrationsversionen - seeds/assignmentTypes.ts: seedIfEmpty() - seeds/feedbackStructure.ts: seedFeedbackStructureIfEmpty() - seeds/migrations.ts: seedCriterionMappingsIfEmpty() + migrateToUnifiedCriteriaIfNeeded() - index.ts: reiner Re-Export, alle bestehenden Imports bleiben kompatibel --- src/db/index.ts | 610 +----------------------------- src/db/schema.ts | 135 +++++++ src/db/seeds/assignmentTypes.ts | 87 +++++ src/db/seeds/feedbackStructure.ts | 65 ++++ src/db/seeds/index.ts | 3 + src/db/seeds/migrations.ts | 106 ++++++ src/db/types.ts | 202 ++++++++++ 7 files changed, 606 insertions(+), 602 deletions(-) create mode 100644 src/db/schema.ts create mode 100644 src/db/seeds/assignmentTypes.ts create mode 100644 src/db/seeds/feedbackStructure.ts create mode 100644 src/db/seeds/index.ts create mode 100644 src/db/seeds/migrations.ts create mode 100644 src/db/types.ts diff --git a/src/db/index.ts b/src/db/index.ts index 6257947..9634b21 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,602 +1,8 @@ -import Dexie, { type Table } from 'dexie' - -// ─── Bewertungs-Skala (einheitlich für Meetings und Feedback) ───────────────── - -export type FeedbackRating = - | 'na' - | 'not_client_ready' - | 'partially_client_ready' - | 'nearly_client_ready' - | 'client_ready' - -// ─── Konfiguration ─────────────────────────────────────────────────────────── - -export interface Category { - id?: number - name: string - order: number -} - -export interface Criterion { - id?: number - categoryId: number - name: string - description: string - weight: number - order: number -} - -// ─── Assignment-Typen & Phase-Templates ────────────────────────────────────── - -export type PhaseKey = string - -export interface AssignmentType { - id?: number - name: string - defaultPhaseKeys: PhaseKey[] - criteriaIds?: number[] // FeedbackCategory IDs; leer = alle Kategorien anzeigen -} - -export interface PhaseTemplate { - id?: number - assignmentTypeId: number - phaseKey: PhaseKey - label: string - withWhom: 'lead' | 'client' - isRepeatable: boolean - hasConversation: boolean - hasAssessment: boolean - criteriaIds: number[] - order: number -} - -// ─── Stammdaten ─────────────────────────────────────────────────────────────── - -export interface Group { - id?: number - name: string - cohortYear: number -} - -export interface Consultant { - id?: number - groupId: number - firstName: string - lastName: string -} - -// ─── Assignments ───────────────────────────────────────────────────────────── - -export interface Assignment { - id?: number - title: string - client: string - assignmentTypeId: number - instituteeIds: number[] - leadName: string - status: 'active' | 'done' - createdAt: string - assignmentNumber?: string // Offizielle Assignment-Nummer aus Systemsystemen - description?: string - instituteeEnrollments?: Array<{ - instituteeId: number - from?: string // ISO date string - to?: string // ISO date string - }> -} - -// ─── Meeting-Instanzen ──────────────────────────────────────────────────────── - -export interface MeetingInstance { - id?: number - assignmentId: number - phaseTemplateId: number - phaseIndex: number - date: string - status: 'planned' | 'active' | 'done' - generalNotes: string - deletedAt?: string -} - -// ─── Gesprächs-Zeitleiste ───────────────────────────────────────────────────── - -export interface ConversationEntry { - id?: number - meetingInstanceId: number - instituteeId: number - sequenceIndex: number - note: string - fillerCount: number - updatedAt: string -} - -// criteriaId here now holds a FeedbackCategory.id (unified criteria system) -export interface ConversationSkillScore { - id?: number - conversationEntryId: number - criteriaId: number // → FeedbackCategory.id - score: FeedbackRating | null -} - -// ─── Gesamtbewertung ───────────────────────────────────────────────────────── - -// criteriaId here now holds a FeedbackCategory.id (unified criteria system) -export interface Assessment { - id?: number - meetingInstanceId: number - instituteeId: number - criteriaId: number // → FeedbackCategory.id - score: FeedbackRating | null - note: string - updatedAt: string -} - -// ─── Feedback-Entwurf (Legacy, bleibt erhalten) ────────────────────────────── - -export interface FeedbackDraft { - id?: number - assignmentId: number - instituteeId: number - content: string - updatedAt: string -} - -// ─── Strukturiertes Assignment-Feedback ─────────────────────────────────────── - -export interface FeedbackDimension { - id?: number - name: string - order: number -} - -export interface FeedbackCategory { - id?: number - dimensionId: number - name: string - order: number -} - -export interface FeedbackCriterionItem { - id?: number - categoryId: number - name: string - weight?: number // Gewichtung, default 1 - order: number -} - -export interface AssignmentFeedback { - id?: number - assignmentId: number - instituteeId: number - status: 'draft' | 'final' - aiModel: string - updatedAt: string -} - -export interface FeedbackCategoryRating { - id?: number - assignmentFeedbackId: number - feedbackCategoryId: number - rating: FeedbackRating -} - -export interface FeedbackDimensionText { - id?: number - assignmentFeedbackId: number - feedbackDimensionId: number - achievements: string - developmentNeeds: string -} - -// Mapping: Assessment-Kriterium → Feedback-Kategorie (n:m, Legacy-Kompatibilität) -export interface CriterionCategoryMapping { - id?: number - criterionId: number - feedbackCategoryId: number -} - -// Beschreibungstext pro FeedbackCriterionItem und Bewertungsstufe -export interface CriterionLevelDescription { - id?: number - criterionItemId: number // → FeedbackCriterionItem.id - rating: FeedbackRating - text: string -} - -// ─── Dexie DB ──────────────────────────────────────────────────────────────── - -class AssignmentMonitorDB extends Dexie { - categories!: Table - criteria!: Table - assignmentTypes!: Table - phaseTemplates!: Table - groups!: Table - consultants!: Table - assignments!: Table - meetingInstances!: Table - conversationEntries!: Table - conversationSkillScores!: Table - assessments!: Table - feedbackDrafts!: Table - feedbackDimensions!: Table - feedbackCategories!: Table - feedbackCriterionItems!: Table - assignmentFeedbacks!: Table - feedbackCategoryRatings!: Table - feedbackDimensionTexts!: Table - criterionCategoryMappings!: Table - criterionLevelDescriptions!: Table - - constructor() { - super('AssignmentMonitorDB') - this.version(1).stores({ - categories: '++id, order', - criteria: '++id, categoryId, order', - groups: '++id', - consultants: '++id, groupId', - meetings: '++id, date, status, groupId', - assessments: '++id, meetingId, consultantId, criteriaId', - consultantNotes: '++id, meetingId, consultantId', - }) - this.version(2).stores({ - categories: '++id, order', - criteria: '++id, categoryId, order', - assignmentTypes: '++id', - phaseTemplates: '++id, assignmentTypeId, order', - groups: '++id', - consultants: '++id, groupId', - assignments: '++id, status, createdAt', - meetingInstances: '++id, assignmentId, phaseTemplateId', - conversationEntries: '++id, meetingInstanceId, instituteeId, sequenceIndex', - conversationSkillScores: '++id, conversationEntryId, criteriaId', - assessments: '++id, meetingInstanceId, instituteeId, criteriaId', - feedbackDrafts: '++id, assignmentId, instituteeId', - // v1 tables no longer used but kept for safe migration - meetings: null, - consultantNotes: null, - }) - this.version(3).stores({ - categories: '++id, order', - criteria: '++id, categoryId, order', - assignmentTypes: '++id', - phaseTemplates: '++id, assignmentTypeId, order', - groups: '++id', - consultants: '++id, groupId', - assignments: '++id, status, createdAt', - meetingInstances: '++id, assignmentId, phaseTemplateId', - conversationEntries: '++id, meetingInstanceId, instituteeId, sequenceIndex', - conversationSkillScores: '++id, conversationEntryId, criteriaId', - assessments: '++id, meetingInstanceId, instituteeId, criteriaId', - feedbackDrafts: '++id, assignmentId, instituteeId', - feedbackDimensions: '++id, order', - feedbackCategories: '++id, dimensionId, order', - feedbackCriterionItems: '++id, categoryId, order', - assignmentFeedbacks: '++id, assignmentId, instituteeId', - feedbackCategoryRatings: '++id, assignmentFeedbackId, feedbackCategoryId', - feedbackDimensionTexts: '++id, assignmentFeedbackId, feedbackDimensionId', - meetings: null, - consultantNotes: null, - }) - this.version(4).stores({ - categories: '++id, order', - criteria: '++id, categoryId, order', - assignmentTypes: '++id', - phaseTemplates: '++id, assignmentTypeId, order', - groups: '++id', - consultants: '++id, groupId', - assignments: '++id, status, createdAt', - meetingInstances: '++id, assignmentId, phaseTemplateId', - conversationEntries: '++id, meetingInstanceId, instituteeId, sequenceIndex', - conversationSkillScores: '++id, conversationEntryId, criteriaId', - assessments: '++id, meetingInstanceId, instituteeId, criteriaId', - feedbackDrafts: '++id, assignmentId, instituteeId', - feedbackDimensions: '++id, order', - feedbackCategories: '++id, dimensionId, order', - feedbackCriterionItems: '++id, categoryId, order', - assignmentFeedbacks: '++id, assignmentId, instituteeId', - feedbackCategoryRatings: '++id, assignmentFeedbackId, feedbackCategoryId', - feedbackDimensionTexts: '++id, assignmentFeedbackId, feedbackDimensionId', - criterionCategoryMappings: '++id, criterionId, feedbackCategoryId', - meetings: null, - consultantNotes: null, - }) - this.version(5).stores({ - categories: '++id, order', - criteria: '++id, categoryId, order', - assignmentTypes: '++id', - phaseTemplates: '++id, assignmentTypeId, order', - groups: '++id', - consultants: '++id, groupId', - assignments: '++id, status, createdAt', - meetingInstances: '++id, assignmentId, phaseTemplateId', - conversationEntries: '++id, meetingInstanceId, instituteeId, sequenceIndex', - conversationSkillScores: '++id, conversationEntryId, criteriaId', - assessments: '++id, meetingInstanceId, instituteeId, criteriaId', - feedbackDrafts: '++id, assignmentId, instituteeId', - feedbackDimensions: '++id, order', - feedbackCategories: '++id, dimensionId, order', - feedbackCriterionItems: '++id, categoryId, order', - assignmentFeedbacks: '++id, assignmentId, instituteeId', - feedbackCategoryRatings: '++id, assignmentFeedbackId, feedbackCategoryId', - feedbackDimensionTexts: '++id, assignmentFeedbackId, feedbackDimensionId', - criterionCategoryMappings: '++id, criterionId, feedbackCategoryId', - criterionLevelDescriptions: '++id, criterionItemId, rating', - meetings: null, - consultantNotes: null, - }) - } -} - -export const db = new AssignmentMonitorDB() - -// ─── Seed ───────────────────────────────────────────────────────────────────── - -export async function seedIfEmpty() { - const count = await db.assignmentTypes.count() - if (count > 0) return - - // ── Gemeinsame Kategorien & Kriterien ──────────────────────────────────── - const catKomm = await db.categories.add({ name: 'Kommunikation', order: 1 }) - const catFach = await db.categories.add({ name: 'Fachkompetenz', order: 2 }) - const catKunde = await db.categories.add({ name: 'Kundenorientierung', order: 3 }) - const catSelf = await db.categories.add({ name: 'Selbstorganisation', order: 4 }) - const catCase = await db.categories.add({ name: 'Case-Kompetenz', order: 5 }) - - const kritKlarheit = await db.criteria.add({ categoryId: catKomm, name: 'Klarheit & Struktur', description: 'Klare, strukturierte Aussagen', weight: 2, order: 1 }) - const kritUeberzeug = await db.criteria.add({ categoryId: catKomm, name: 'Überzeugungskraft', description: 'Argumente überzeugend präsentieren', weight: 2, order: 2 }) - const kritZuhoeren = await db.criteria.add({ categoryId: catKomm, name: 'Aktives Zuhören', description: 'Eingehen auf Gesprächspartner', weight: 1, order: 3 }) - const kritFragen = await db.criteria.add({ categoryId: catKomm, name: 'Umgang mit Fragen', description: 'Reaktion auf Kunden-Fragen', weight: 2, order: 4 }) - const kritTiefe = await db.criteria.add({ categoryId: catFach, name: 'Fachliche Tiefe', description: 'Inhaltliche Korrektheit und Tiefe', weight: 3, order: 1 }) - const kritProblem = await db.criteria.add({ categoryId: catFach, name: 'Problemlösung', description: 'Analytisches Vorgehen, Hypothesen', weight: 3, order: 2 }) - const kritStakeholder = await db.criteria.add({ categoryId: catKunde, name: 'Stakeholder-Verständnis', description: 'Erkennt Bedürfnisse und Erwartungen', weight: 2, order: 1 }) - const kritEmpathie = await db.criteria.add({ categoryId: catKunde, name: 'Empathie', description: 'Einfühlungsvermögen im Gespräch', weight: 1, order: 2 }) - const kritVorb = await db.criteria.add({ categoryId: catSelf, name: 'Vorbereitung', description: 'Zeigt gute Meeting-Vorbereitung', weight: 2, order: 1 }) - const kritInitiative = await db.criteria.add({ categoryId: catSelf, name: 'Eigeninitiative', description: 'Handelt proaktiv ohne Aufforderung', weight: 2, order: 2 }) - const kritStruktur = await db.criteria.add({ categoryId: catCase, name: 'Problemstrukturierung', description: 'Systematische Zerlegung des Problems', weight: 3, order: 1 }) - const kritHypo = await db.criteria.add({ categoryId: catCase, name: 'Hypothesenbildung', description: 'Klare, testbare Hypothesen', weight: 3, order: 2 }) - const kritAnalyse = await db.criteria.add({ categoryId: catCase, name: 'Rechnen & Analyse', description: 'Datenbasiertes Vorgehen, Schätzfähigkeit', weight: 2, order: 3 }) - const kritZeitmanage = await db.criteria.add({ categoryId: catCase, name: 'Zeitmanagement', description: 'Effizienter Umgang mit der Case-Zeit', weight: 2, order: 4 }) - - // ── Hilfsfunktion: PhaseTemplate anlegen ──────────────────────────────── - const addPhase = ( - assignmentTypeId: number, - phaseKey: PhaseKey, - label: string, - withWhom: 'lead' | 'client', - opts: { repeatable?: boolean; conv?: boolean; assess?: boolean; criteriaIds?: number[]; order: number } - ) => db.phaseTemplates.add({ - assignmentTypeId, - phaseKey, - label, - withWhom, - isRepeatable: opts.repeatable ?? false, - hasConversation: opts.conv ?? false, - hasAssessment: opts.assess ?? false, - criteriaIds: opts.criteriaIds ?? [], - order: opts.order, - }) - - // ── AssignmentType 1: Generisch ────────────────────────────────────────── - const genericId = await db.assignmentTypes.add({ - name: 'Generisch', - defaultPhaseKeys: ['preBriefing', 'briefing', 'alignmentCall', 'preDelivery', 'delivery', 'feedbackCallClient', 'feedbackCallLead'], - }) - const genericCriteria = [kritKlarheit, kritUeberzeug, kritZuhoeren, kritTiefe, kritProblem, kritStakeholder, kritEmpathie, kritVorb, kritInitiative] - await addPhase(genericId, 'preBriefing', 'Pre-Briefing (Lead)', 'lead', { order: 1 }) - await addPhase(genericId, 'briefing', 'Briefing (Kunde)', 'client', { conv: true, assess: true, criteriaIds: genericCriteria, order: 2 }) - await addPhase(genericId, 'alignmentCall', 'Alignment Call (Kunde)', 'client', { repeatable: true, conv: true, assess: true, criteriaIds: genericCriteria, order: 3 }) - await addPhase(genericId, 'preDelivery', 'Pre-Delivery (Lead)', 'lead', { order: 4 }) - await addPhase(genericId, 'delivery', 'Delivery (Kunde)', 'client', { conv: true, assess: true, criteriaIds: genericCriteria, order: 5 }) - await addPhase(genericId, 'feedbackCallClient','Feedback Call (Kunde)', 'client', { conv: true, assess: true, criteriaIds: genericCriteria, order: 6 }) - await addPhase(genericId, 'feedbackCallLead', 'Feedback Call (Lead)', 'lead', { order: 7 }) - - // ── AssignmentType 2: Case Interview ──────────────────────────────────── - const caseId = await db.assignmentTypes.add({ - name: 'Case Interview', - defaultPhaseKeys: ['preBriefing', 'briefing', 'delivery', 'feedbackCallClient', 'feedbackCallLead'], - }) - const caseCriteria = [kritStruktur, kritHypo, kritAnalyse, kritKlarheit, kritZeitmanage, kritUeberzeug, kritFragen] - await addPhase(caseId, 'preBriefing', 'Pre-Briefing (Lead)', 'lead', { order: 1 }) - await addPhase(caseId, 'briefing', 'Case Briefing (Kunde)', 'client', { conv: true, assess: true, criteriaIds: caseCriteria, order: 2 }) - await addPhase(caseId, 'delivery', 'Case Delivery (Kunde)', 'client', { conv: true, assess: true, criteriaIds: caseCriteria, order: 3 }) - await addPhase(caseId, 'feedbackCallClient','Feedback Call (Kunde)', 'client', { conv: true, assess: true, criteriaIds: caseCriteria, order: 4 }) - await addPhase(caseId, 'feedbackCallLead', 'Feedback Call (Lead)', 'lead', { order: 5 }) - - // ── AssignmentType 3: Stakeholder Meeting / Pitch ──────────────────────── - const pitchId = await db.assignmentTypes.add({ - name: 'Stakeholder Meeting / Pitch', - defaultPhaseKeys: ['preBriefing', 'briefing', 'alignmentCall', 'preDelivery', 'delivery', 'feedbackCallClient', 'feedbackCallLead'], - }) - const pitchCriteria = [kritVorb, kritKlarheit, kritUeberzeug, kritStakeholder, kritFragen, kritEmpathie, kritInitiative] - await addPhase(pitchId, 'preBriefing', 'Pre-Briefing (Lead)', 'lead', { order: 1 }) - await addPhase(pitchId, 'briefing', 'Briefing (Kunde)', 'client', { conv: true, assess: true, criteriaIds: pitchCriteria, order: 2 }) - await addPhase(pitchId, 'alignmentCall', 'Alignment Call (Kunde)', 'client', { repeatable: true, conv: true, assess: true, criteriaIds: pitchCriteria, order: 3 }) - await addPhase(pitchId, 'preDelivery', 'Pre-Delivery (Lead)', 'lead', { order: 4 }) - await addPhase(pitchId, 'delivery', 'Pitch / Delivery (Kunde)', 'client', { conv: true, assess: true, criteriaIds: pitchCriteria, order: 5 }) - await addPhase(pitchId, 'feedbackCallClient','Feedback Call (Kunde)', 'client', { conv: true, assess: true, criteriaIds: pitchCriteria, order: 6 }) - await addPhase(pitchId, 'feedbackCallLead', 'Feedback Call (Lead)', 'lead', { order: 7 }) -} - -// ─── Seed: Feedback-Dimensionsstruktur ─────────────────────────────────────── - -export async function seedFeedbackStructureIfEmpty() { - const count = await db.feedbackDimensions.count() - if (count > 0) return - - type CatSeed = { name: string; items: string[] } - type DimSeed = { name: string; cats: CatSeed[] } - - const dimensions: DimSeed[] = [ - { - name: 'Mindset, Commitment, Collaboration', - cats: [ - { name: 'Persönliche Einstellung', items: ['Ruhig bei Stress', 'Offen für neue Themen', 'Auftreten / Ausstrahlung', 'Pro aktiv', 'Lösungsorientiert'] }, - { name: 'Professionalität', items: ['Pünktlichkeit', 'Professionell bei informellen Gesprächen', 'Adressatengerecht', 'Hohes Maß an Selbststeuerung', 'Klarheit', 'Zuverlässigkeit'] }, - { name: 'Commitment', items: ['Offen für spontane Änderungen', 'Selbständiger Lösungsansatz', 'Pro aktive Ideen', 'Verantwortungsbewusst', 'Hilfsbereit', 'Teamorientiert'] }, - { name: 'Kommunikationsfähigkeit', items: ['To the point', 'Recap-Fähigkeit', 'Active Listening', 'Souveränität'] }, - { name: 'Zusammenarbeit', items: ['Regelmäßige Abstimmung', 'Unterstützung bei Bedarf', 'Know-How Sharing', 'Starkes Teamverhalten', 'Verantwortung und Vertrauen', 'Inhaltlich unterstützend'] }, - ], - }, - { - name: 'Running Our Projects', - cats: [ - { name: 'Ergebnisverantwortung', items: ['Fokussierung', 'Priorisierung (Impact Driven)', 'Machbarkeit / Umsetzbarkeit', 'Alternativvorschläge', 'Interne Abstimmung und Feedback'] }, - { name: 'Arbeitsstil', items: ['Komplexitätsmanagement', 'Eigenverantwortung', 'Feedbackaffinität', 'Verantwortungsbewusst / vorausschauend', 'Selbstreflektiert, lösungsorientiert', 'Offen, initiativ, kollegial', 'Organisiert, kommunikationsfähig', 'Lernfähig, anpassungsbereit, flexibel'] }, - { name: 'Kundenmanagement', items: ['Aktives Erwartungsmanagement', 'Selbstsicherheit und fachliche Überzeugung', 'Aktive Advisory', 'Realistisch', 'Kundenfokussiert'] }, - { name: 'Lieferzeiten', items: ['Verlässlichkeit', 'Fortschrittskontrolle und Planadaption', 'Aktives Scoping', 'Meetingsvorbereitung', 'Transparent'] }, - ], - }, - { - name: 'Growing our People', - cats: [ - { name: 'Networking', items: ['Cross-Collaboration', 'Network Growth', 'Network Utilization'] }, - { name: 'Learning und Development', items: ['Kritikfähigkeit', 'Wissensaufbau (Kompetenzausstrahlung)', 'Umsetzungsbereitschaft Feedback'] }, - ], - }, - { - name: 'Functional & Technical Skills', - cats: [ - { name: 'Qualität des Inhalts', items: ['Belastbarkeit', 'Nachvollziehbarkeit', 'Logische Storyline'] }, - { name: 'Inhaltliche Vollständigkeit', items: ['Wesentliche Elemente vorhanden', 'Realitätsnähe', 'Nutzenfokus'] }, - { name: 'Qualität der Unterlage', items: ['Design nach Standardvorgaben', 'Sprachliche Präzision', 'Visualisierungen unterstützen Kernbotschaften'] }, - { name: 'Prozesstreue', items: ['Kenne und befolge Prozesse und Vorgaben', 'Nachvollziehbarkeit und Transparenz', 'Qualitätschecks'] }, - ], - }, - { - name: 'Innovation und Asset Development', - cats: [ - { name: 'Innovation', items: ['Aktiver Vorschlag für Verbesserungen', 'Neue Ideen zur Verbesserung', 'Nutze aktuellen Wissensstand', 'Liefere über den Kundenerwartungen'] }, - ], - }, - ] - - for (let di = 0; di < dimensions.length; di++) { - const dim = dimensions[di] - const dimId = await db.feedbackDimensions.add({ name: dim.name, order: di + 1 }) - for (let ci = 0; ci < dim.cats.length; ci++) { - const cat = dim.cats[ci] - const catId = await db.feedbackCategories.add({ dimensionId: dimId, name: cat.name, order: ci + 1 }) - for (let ii = 0; ii < cat.items.length; ii++) { - await db.feedbackCriterionItems.add({ categoryId: catId, name: cat.items[ii], order: ii + 1 }) - } - } - } -} - -// ─── Seed: Kriterien-Mapping ────────────────────────────────────────────────── - -export async function seedCriterionMappingsIfEmpty() { - const count = await db.criterionCategoryMappings.count() - if (count > 0) return - - // Warte bis Kriterien und Feedback-Kategorien vorhanden sind - const [crits, cats] = await Promise.all([ - db.criteria.toArray(), - db.feedbackCategories.toArray(), - ]) - if (crits.length === 0 || cats.length === 0) return - - const c = (name: string) => crits.find(x => x.name === name)?.id - const f = (name: string) => cats.find(x => x.name === name)?.id - - const mappings: [string, string][] = [ - // Kriterium → Feedback-Kategorie - ['Klarheit & Struktur', 'Kommunikationsfähigkeit'], - ['Klarheit & Struktur', 'Qualität des Inhalts'], - ['Überzeugungskraft', 'Kommunikationsfähigkeit'], - ['Überzeugungskraft', 'Kundenmanagement'], - ['Aktives Zuhören', 'Kommunikationsfähigkeit'], - ['Umgang mit Fragen', 'Kommunikationsfähigkeit'], - ['Umgang mit Fragen', 'Kundenmanagement'], - ['Fachliche Tiefe', 'Qualität des Inhalts'], - ['Fachliche Tiefe', 'Inhaltliche Vollständigkeit'], - ['Problemlösung', 'Ergebnisverantwortung'], - ['Problemlösung', 'Arbeitsstil'], - ['Stakeholder-Verständnis', 'Kundenmanagement'], - ['Stakeholder-Verständnis', 'Persönliche Einstellung'], - ['Empathie', 'Persönliche Einstellung'], - ['Empathie', 'Kommunikationsfähigkeit'], - ['Vorbereitung', 'Professionalität'], - ['Vorbereitung', 'Lieferzeiten'], - ['Eigeninitiative', 'Commitment'], - ['Eigeninitiative', 'Persönliche Einstellung'], - ['Problemstrukturierung', 'Ergebnisverantwortung'], - ['Problemstrukturierung', 'Qualität des Inhalts'], - ['Hypothesenbildung', 'Ergebnisverantwortung'], - ['Hypothesenbildung', 'Arbeitsstil'], - ['Rechnen & Analyse', 'Qualität des Inhalts'], - ['Rechnen & Analyse', 'Inhaltliche Vollständigkeit'], - ['Zeitmanagement', 'Lieferzeiten'], - ['Zeitmanagement', 'Arbeitsstil'], - ] - - for (const [critName, catName] of mappings) { - const criterionId = c(critName) - const feedbackCategoryId = f(catName) - if (criterionId && feedbackCategoryId) { - await db.criterionCategoryMappings.add({ criterionId, feedbackCategoryId }) - } - } -} - -// ─── Migration: Unified Criteria System ────────────────────────────────────── -// Löscht nur Bewertungs-Scores (noch nicht befüllt), nie Protokoll-Texte. -// Verknüpft PhaseTemplates mit FeedbackCategory-IDs statt alten Criterion-IDs. - -export async function migrateToUnifiedCriteriaIfNeeded() { - // Prüfen ob Migration schon gelaufen: erste PhaseTemplate mit criteriaIds die - // auf eine FeedbackCategory zeigen (vs. alte Criterion-IDs die niedrig sein werden) - const cats = await db.feedbackCategories.toArray() - if (cats.length === 0) return // Feedback-Struktur noch nicht geseeded - - const templates = await db.phaseTemplates.toArray() - if (templates.length === 0) return - - // Prüfen ob irgendeine Template-criteriaId in den FeedbackCategory-IDs liegt - const catIds = new Set(cats.map(c => c.id!)) - const firstWithIds = templates.find(t => t.criteriaIds.length > 0) - if (firstWithIds && catIds.has(firstWithIds.criteriaIds[0])) return // schon migriert - - // Kategorien nach Name nachschlagen - const cat = (name: string) => cats.find(c => c.name === name)?.id - - // Scores löschen (keine Textinhalte, nur Bewertungszahlen) - await db.conversationSkillScores.clear() - await db.assessments.clear() - - // Phase-Templates mit neuen Kategorie-IDs befüllen - // Kategorien-Sets pro Assignment-Typ und Phasentyp - const genericCats = [ - cat('Kommunikationsfähigkeit'), cat('Persönliche Einstellung'), - cat('Kundenmanagement'), cat('Ergebnisverantwortung'), cat('Qualität des Inhalts'), - ].filter(Boolean) as number[] - - const caseCats = [ - cat('Kommunikationsfähigkeit'), cat('Ergebnisverantwortung'), - cat('Arbeitsstil'), cat('Qualität des Inhalts'), cat('Inhaltliche Vollständigkeit'), - ].filter(Boolean) as number[] - - const pitchCats = [ - cat('Kommunikationsfähigkeit'), cat('Persönliche Einstellung'), - cat('Kundenmanagement'), cat('Ergebnisverantwortung'), - cat('Qualität des Inhalts'), cat('Qualität der Unterlage'), - ].filter(Boolean) as number[] - - const types = await db.assignmentTypes.toArray() - for (const tmpl of templates) { - if (!tmpl.hasAssessment && !tmpl.hasConversation) { - await db.phaseTemplates.update(tmpl.id!, { criteriaIds: [] }) - continue - } - const aType = types.find(t => t.id === tmpl.assignmentTypeId) - let newIds: number[] - if (aType?.name === 'Case Interview') { - newIds = caseCats - } else if (aType?.name === 'Stakeholder Meeting / Pitch') { - newIds = pitchCats - } else { - newIds = genericCats - } - await db.phaseTemplates.update(tmpl.id!, { criteriaIds: newIds }) - } -} +export * from './types' +export { db } from './schema' +export { + seedIfEmpty, + seedFeedbackStructureIfEmpty, + seedCriterionMappingsIfEmpty, + migrateToUnifiedCriteriaIfNeeded, +} from './seeds' diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..50a81ec --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,135 @@ +import Dexie, { type Table } from 'dexie' +import type { + Category, Criterion, + AssignmentType, PhaseTemplate, + Group, Consultant, + Assignment, MeetingInstance, + ConversationEntry, ConversationSkillScore, + Assessment, FeedbackDraft, + FeedbackDimension, FeedbackCategory, FeedbackCriterionItem, + AssignmentFeedback, FeedbackCategoryRating, FeedbackDimensionText, + CriterionCategoryMapping, CriterionLevelDescription, +} from './types' + +class AssignmentMonitorDB extends Dexie { + categories!: Table + criteria!: Table + assignmentTypes!: Table + phaseTemplates!: Table + groups!: Table + consultants!: Table + assignments!: Table + meetingInstances!: Table + conversationEntries!: Table + conversationSkillScores!: Table + assessments!: Table + feedbackDrafts!: Table + feedbackDimensions!: Table + feedbackCategories!: Table + feedbackCriterionItems!: Table + assignmentFeedbacks!: Table + feedbackCategoryRatings!: Table + feedbackDimensionTexts!: Table + criterionCategoryMappings!: Table + criterionLevelDescriptions!: Table + + constructor() { + super('AssignmentMonitorDB') + this.version(1).stores({ + categories: '++id, order', + criteria: '++id, categoryId, order', + groups: '++id', + consultants: '++id, groupId', + meetings: '++id, date, status, groupId', + assessments: '++id, meetingId, consultantId, criteriaId', + consultantNotes: '++id, meetingId, consultantId', + }) + this.version(2).stores({ + categories: '++id, order', + criteria: '++id, categoryId, order', + assignmentTypes: '++id', + phaseTemplates: '++id, assignmentTypeId, order', + groups: '++id', + consultants: '++id, groupId', + assignments: '++id, status, createdAt', + meetingInstances: '++id, assignmentId, phaseTemplateId', + conversationEntries: '++id, meetingInstanceId, instituteeId, sequenceIndex', + conversationSkillScores: '++id, conversationEntryId, criteriaId', + assessments: '++id, meetingInstanceId, instituteeId, criteriaId', + feedbackDrafts: '++id, assignmentId, instituteeId', + meetings: null, + consultantNotes: null, + }) + this.version(3).stores({ + categories: '++id, order', + criteria: '++id, categoryId, order', + assignmentTypes: '++id', + phaseTemplates: '++id, assignmentTypeId, order', + groups: '++id', + consultants: '++id, groupId', + assignments: '++id, status, createdAt', + meetingInstances: '++id, assignmentId, phaseTemplateId', + conversationEntries: '++id, meetingInstanceId, instituteeId, sequenceIndex', + conversationSkillScores: '++id, conversationEntryId, criteriaId', + assessments: '++id, meetingInstanceId, instituteeId, criteriaId', + feedbackDrafts: '++id, assignmentId, instituteeId', + feedbackDimensions: '++id, order', + feedbackCategories: '++id, dimensionId, order', + feedbackCriterionItems: '++id, categoryId, order', + assignmentFeedbacks: '++id, assignmentId, instituteeId', + feedbackCategoryRatings: '++id, assignmentFeedbackId, feedbackCategoryId', + feedbackDimensionTexts: '++id, assignmentFeedbackId, feedbackDimensionId', + meetings: null, + consultantNotes: null, + }) + this.version(4).stores({ + categories: '++id, order', + criteria: '++id, categoryId, order', + assignmentTypes: '++id', + phaseTemplates: '++id, assignmentTypeId, order', + groups: '++id', + consultants: '++id, groupId', + assignments: '++id, status, createdAt', + meetingInstances: '++id, assignmentId, phaseTemplateId', + conversationEntries: '++id, meetingInstanceId, instituteeId, sequenceIndex', + conversationSkillScores: '++id, conversationEntryId, criteriaId', + assessments: '++id, meetingInstanceId, instituteeId, criteriaId', + feedbackDrafts: '++id, assignmentId, instituteeId', + feedbackDimensions: '++id, order', + feedbackCategories: '++id, dimensionId, order', + feedbackCriterionItems: '++id, categoryId, order', + assignmentFeedbacks: '++id, assignmentId, instituteeId', + feedbackCategoryRatings: '++id, assignmentFeedbackId, feedbackCategoryId', + feedbackDimensionTexts: '++id, assignmentFeedbackId, feedbackDimensionId', + criterionCategoryMappings: '++id, criterionId, feedbackCategoryId', + meetings: null, + consultantNotes: null, + }) + this.version(5).stores({ + categories: '++id, order', + criteria: '++id, categoryId, order', + assignmentTypes: '++id', + phaseTemplates: '++id, assignmentTypeId, order', + groups: '++id', + consultants: '++id, groupId', + assignments: '++id, status, createdAt', + meetingInstances: '++id, assignmentId, phaseTemplateId', + conversationEntries: '++id, meetingInstanceId, instituteeId, sequenceIndex', + conversationSkillScores: '++id, conversationEntryId, criteriaId', + assessments: '++id, meetingInstanceId, instituteeId, criteriaId', + feedbackDrafts: '++id, assignmentId, instituteeId', + feedbackDimensions: '++id, order', + feedbackCategories: '++id, dimensionId, order', + feedbackCriterionItems: '++id, categoryId, order', + assignmentFeedbacks: '++id, assignmentId, instituteeId', + feedbackCategoryRatings: '++id, assignmentFeedbackId, feedbackCategoryId', + feedbackDimensionTexts: '++id, assignmentFeedbackId, feedbackDimensionId', + criterionCategoryMappings: '++id, criterionId, feedbackCategoryId', + criterionLevelDescriptions: '++id, criterionItemId, rating', + meetings: null, + consultantNotes: null, + }) + } +} + +export const db = new AssignmentMonitorDB() diff --git a/src/db/seeds/assignmentTypes.ts b/src/db/seeds/assignmentTypes.ts new file mode 100644 index 0000000..162a534 --- /dev/null +++ b/src/db/seeds/assignmentTypes.ts @@ -0,0 +1,87 @@ +import { db } from '../schema' +import type { PhaseKey } from '../types' + +export async function seedIfEmpty() { + const count = await db.assignmentTypes.count() + if (count > 0) return + + // ── Gemeinsame Kategorien & Kriterien (Legacy-Tabellen für Evaluation.tsx) ── + const catKomm = await db.categories.add({ name: 'Kommunikation', order: 1 }) + const catFach = await db.categories.add({ name: 'Fachkompetenz', order: 2 }) + const catKunde = await db.categories.add({ name: 'Kundenorientierung', order: 3 }) + const catSelf = await db.categories.add({ name: 'Selbstorganisation', order: 4 }) + const catCase = await db.categories.add({ name: 'Case-Kompetenz', order: 5 }) + + const kritKlarheit = await db.criteria.add({ categoryId: catKomm, name: 'Klarheit & Struktur', description: 'Klare, strukturierte Aussagen', weight: 2, order: 1 }) + const kritUeberzeug = await db.criteria.add({ categoryId: catKomm, name: 'Überzeugungskraft', description: 'Argumente überzeugend präsentieren', weight: 2, order: 2 }) + const kritZuhoeren = await db.criteria.add({ categoryId: catKomm, name: 'Aktives Zuhören', description: 'Eingehen auf Gesprächspartner', weight: 1, order: 3 }) + const kritFragen = await db.criteria.add({ categoryId: catKomm, name: 'Umgang mit Fragen', description: 'Reaktion auf Kunden-Fragen', weight: 2, order: 4 }) + const kritTiefe = await db.criteria.add({ categoryId: catFach, name: 'Fachliche Tiefe', description: 'Inhaltliche Korrektheit und Tiefe', weight: 3, order: 1 }) + const kritProblem = await db.criteria.add({ categoryId: catFach, name: 'Problemlösung', description: 'Analytisches Vorgehen, Hypothesen', weight: 3, order: 2 }) + const kritStakeholder = await db.criteria.add({ categoryId: catKunde, name: 'Stakeholder-Verständnis', description: 'Erkennt Bedürfnisse und Erwartungen', weight: 2, order: 1 }) + const kritEmpathie = await db.criteria.add({ categoryId: catKunde, name: 'Empathie', description: 'Einfühlungsvermögen im Gespräch', weight: 1, order: 2 }) + const kritVorb = await db.criteria.add({ categoryId: catSelf, name: 'Vorbereitung', description: 'Zeigt gute Meeting-Vorbereitung', weight: 2, order: 1 }) + const kritInitiative = await db.criteria.add({ categoryId: catSelf, name: 'Eigeninitiative', description: 'Handelt proaktiv ohne Aufforderung', weight: 2, order: 2 }) + const kritStruktur = await db.criteria.add({ categoryId: catCase, name: 'Problemstrukturierung', description: 'Systematische Zerlegung des Problems', weight: 3, order: 1 }) + const kritHypo = await db.criteria.add({ categoryId: catCase, name: 'Hypothesenbildung', description: 'Klare, testbare Hypothesen', weight: 3, order: 2 }) + const kritAnalyse = await db.criteria.add({ categoryId: catCase, name: 'Rechnen & Analyse', description: 'Datenbasiertes Vorgehen, Schätzfähigkeit', weight: 2, order: 3 }) + const kritZeitmanage = await db.criteria.add({ categoryId: catCase, name: 'Zeitmanagement', description: 'Effizienter Umgang mit der Case-Zeit', weight: 2, order: 4 }) + + const addPhase = ( + assignmentTypeId: number, + phaseKey: PhaseKey, + label: string, + withWhom: 'lead' | 'client', + opts: { repeatable?: boolean; conv?: boolean; assess?: boolean; criteriaIds?: number[]; order: number } + ) => db.phaseTemplates.add({ + assignmentTypeId, + phaseKey, + label, + withWhom, + isRepeatable: opts.repeatable ?? false, + hasConversation: opts.conv ?? false, + hasAssessment: opts.assess ?? false, + criteriaIds: opts.criteriaIds ?? [], + order: opts.order, + }) + + // ── AssignmentType 1: Generisch ────────────────────────────────────────── + const genericId = await db.assignmentTypes.add({ + name: 'Generisch', + defaultPhaseKeys: ['preBriefing', 'briefing', 'alignmentCall', 'preDelivery', 'delivery', 'feedbackCallClient', 'feedbackCallLead'], + }) + const genericCriteria = [kritKlarheit, kritUeberzeug, kritZuhoeren, kritTiefe, kritProblem, kritStakeholder, kritEmpathie, kritVorb, kritInitiative] + await addPhase(genericId, 'preBriefing', 'Pre-Briefing (Lead)', 'lead', { order: 1 }) + await addPhase(genericId, 'briefing', 'Briefing (Kunde)', 'client', { conv: true, assess: true, criteriaIds: genericCriteria, order: 2 }) + await addPhase(genericId, 'alignmentCall', 'Alignment Call (Kunde)', 'client', { repeatable: true, conv: true, assess: true, criteriaIds: genericCriteria, order: 3 }) + await addPhase(genericId, 'preDelivery', 'Pre-Delivery (Lead)', 'lead', { order: 4 }) + await addPhase(genericId, 'delivery', 'Delivery (Kunde)', 'client', { conv: true, assess: true, criteriaIds: genericCriteria, order: 5 }) + await addPhase(genericId, 'feedbackCallClient', 'Feedback Call (Kunde)', 'client', { conv: true, assess: true, criteriaIds: genericCriteria, order: 6 }) + await addPhase(genericId, 'feedbackCallLead', 'Feedback Call (Lead)', 'lead', { order: 7 }) + + // ── AssignmentType 2: Case Interview ──────────────────────────────────── + const caseId = await db.assignmentTypes.add({ + name: 'Case Interview', + defaultPhaseKeys: ['preBriefing', 'briefing', 'delivery', 'feedbackCallClient', 'feedbackCallLead'], + }) + const caseCriteria = [kritStruktur, kritHypo, kritAnalyse, kritKlarheit, kritZeitmanage, kritUeberzeug, kritFragen] + await addPhase(caseId, 'preBriefing', 'Pre-Briefing (Lead)', 'lead', { order: 1 }) + await addPhase(caseId, 'briefing', 'Case Briefing (Kunde)', 'client', { conv: true, assess: true, criteriaIds: caseCriteria, order: 2 }) + await addPhase(caseId, 'delivery', 'Case Delivery (Kunde)', 'client', { conv: true, assess: true, criteriaIds: caseCriteria, order: 3 }) + await addPhase(caseId, 'feedbackCallClient', 'Feedback Call (Kunde)', 'client', { conv: true, assess: true, criteriaIds: caseCriteria, order: 4 }) + await addPhase(caseId, 'feedbackCallLead', 'Feedback Call (Lead)', 'lead', { order: 5 }) + + // ── AssignmentType 3: Stakeholder Meeting / Pitch ──────────────────────── + const pitchId = await db.assignmentTypes.add({ + name: 'Stakeholder Meeting / Pitch', + defaultPhaseKeys: ['preBriefing', 'briefing', 'alignmentCall', 'preDelivery', 'delivery', 'feedbackCallClient', 'feedbackCallLead'], + }) + const pitchCriteria = [kritVorb, kritKlarheit, kritUeberzeug, kritStakeholder, kritFragen, kritEmpathie, kritInitiative] + await addPhase(pitchId, 'preBriefing', 'Pre-Briefing (Lead)', 'lead', { order: 1 }) + await addPhase(pitchId, 'briefing', 'Briefing (Kunde)', 'client', { conv: true, assess: true, criteriaIds: pitchCriteria, order: 2 }) + await addPhase(pitchId, 'alignmentCall', 'Alignment Call (Kunde)', 'client', { repeatable: true, conv: true, assess: true, criteriaIds: pitchCriteria, order: 3 }) + await addPhase(pitchId, 'preDelivery', 'Pre-Delivery (Lead)', 'lead', { order: 4 }) + await addPhase(pitchId, 'delivery', 'Pitch / Delivery (Kunde)', 'client', { conv: true, assess: true, criteriaIds: pitchCriteria, order: 5 }) + await addPhase(pitchId, 'feedbackCallClient', 'Feedback Call (Kunde)', 'client', { conv: true, assess: true, criteriaIds: pitchCriteria, order: 6 }) + await addPhase(pitchId, 'feedbackCallLead', 'Feedback Call (Lead)', 'lead', { order: 7 }) +} diff --git a/src/db/seeds/feedbackStructure.ts b/src/db/seeds/feedbackStructure.ts new file mode 100644 index 0000000..01e1d76 --- /dev/null +++ b/src/db/seeds/feedbackStructure.ts @@ -0,0 +1,65 @@ +import { db } from '../schema' + +export async function seedFeedbackStructureIfEmpty() { + const count = await db.feedbackDimensions.count() + if (count > 0) return + + type CatSeed = { name: string; items: string[] } + type DimSeed = { name: string; cats: CatSeed[] } + + const dimensions: DimSeed[] = [ + { + name: 'Mindset, Commitment, Collaboration', + cats: [ + { name: 'Persönliche Einstellung', items: ['Ruhig bei Stress', 'Offen für neue Themen', 'Auftreten / Ausstrahlung', 'Pro aktiv', 'Lösungsorientiert'] }, + { name: 'Professionalität', items: ['Pünktlichkeit', 'Professionell bei informellen Gesprächen', 'Adressatengerecht', 'Hohes Maß an Selbststeuerung', 'Klarheit', 'Zuverlässigkeit'] }, + { name: 'Commitment', items: ['Offen für spontane Änderungen', 'Selbständiger Lösungsansatz', 'Pro aktive Ideen', 'Verantwortungsbewusst', 'Hilfsbereit', 'Teamorientiert'] }, + { name: 'Kommunikationsfähigkeit', items: ['To the point', 'Recap-Fähigkeit', 'Active Listening', 'Souveränität'] }, + { name: 'Zusammenarbeit', items: ['Regelmäßige Abstimmung', 'Unterstützung bei Bedarf', 'Know-How Sharing', 'Starkes Teamverhalten', 'Verantwortung und Vertrauen', 'Inhaltlich unterstützend'] }, + ], + }, + { + name: 'Running Our Projects', + cats: [ + { name: 'Ergebnisverantwortung', items: ['Fokussierung', 'Priorisierung (Impact Driven)', 'Machbarkeit / Umsetzbarkeit', 'Alternativvorschläge', 'Interne Abstimmung und Feedback'] }, + { name: 'Arbeitsstil', items: ['Komplexitätsmanagement', 'Eigenverantwortung', 'Feedbackaffinität', 'Verantwortungsbewusst / vorausschauend', 'Selbstreflektiert, lösungsorientiert', 'Offen, initiativ, kollegial', 'Organisiert, kommunikationsfähig', 'Lernfähig, anpassungsbereit, flexibel'] }, + { name: 'Kundenmanagement', items: ['Aktives Erwartungsmanagement', 'Selbstsicherheit und fachliche Überzeugung', 'Aktive Advisory', 'Realistisch', 'Kundenfokussiert'] }, + { name: 'Lieferzeiten', items: ['Verlässlichkeit', 'Fortschrittskontrolle und Planadaption', 'Aktives Scoping', 'Meetingsvorbereitung', 'Transparent'] }, + ], + }, + { + name: 'Growing our People', + cats: [ + { name: 'Networking', items: ['Cross-Collaboration', 'Network Growth', 'Network Utilization'] }, + { name: 'Learning und Development', items: ['Kritikfähigkeit', 'Wissensaufbau (Kompetenzausstrahlung)', 'Umsetzungsbereitschaft Feedback'] }, + ], + }, + { + name: 'Functional & Technical Skills', + cats: [ + { name: 'Qualität des Inhalts', items: ['Belastbarkeit', 'Nachvollziehbarkeit', 'Logische Storyline'] }, + { name: 'Inhaltliche Vollständigkeit', items: ['Wesentliche Elemente vorhanden', 'Realitätsnähe', 'Nutzenfokus'] }, + { name: 'Qualität der Unterlage', items: ['Design nach Standardvorgaben', 'Sprachliche Präzision', 'Visualisierungen unterstützen Kernbotschaften'] }, + { name: 'Prozesstreue', items: ['Kenne und befolge Prozesse und Vorgaben', 'Nachvollziehbarkeit und Transparenz', 'Qualitätschecks'] }, + ], + }, + { + name: 'Innovation und Asset Development', + cats: [ + { name: 'Innovation', items: ['Aktiver Vorschlag für Verbesserungen', 'Neue Ideen zur Verbesserung', 'Nutze aktuellen Wissensstand', 'Liefere über den Kundenerwartungen'] }, + ], + }, + ] + + for (let di = 0; di < dimensions.length; di++) { + const dim = dimensions[di] + const dimId = await db.feedbackDimensions.add({ name: dim.name, order: di + 1 }) + for (let ci = 0; ci < dim.cats.length; ci++) { + const cat = dim.cats[ci] + const catId = await db.feedbackCategories.add({ dimensionId: dimId, name: cat.name, order: ci + 1 }) + for (let ii = 0; ii < cat.items.length; ii++) { + await db.feedbackCriterionItems.add({ categoryId: catId, name: cat.items[ii], order: ii + 1 }) + } + } + } +} diff --git a/src/db/seeds/index.ts b/src/db/seeds/index.ts new file mode 100644 index 0000000..f8a5057 --- /dev/null +++ b/src/db/seeds/index.ts @@ -0,0 +1,3 @@ +export { seedIfEmpty } from './assignmentTypes' +export { seedFeedbackStructureIfEmpty } from './feedbackStructure' +export { seedCriterionMappingsIfEmpty, migrateToUnifiedCriteriaIfNeeded } from './migrations' diff --git a/src/db/seeds/migrations.ts b/src/db/seeds/migrations.ts new file mode 100644 index 0000000..ef85183 --- /dev/null +++ b/src/db/seeds/migrations.ts @@ -0,0 +1,106 @@ +import { db } from '../schema' + +export async function seedCriterionMappingsIfEmpty() { + const count = await db.criterionCategoryMappings.count() + if (count > 0) return + + const [crits, cats] = await Promise.all([ + db.criteria.toArray(), + db.feedbackCategories.toArray(), + ]) + if (crits.length === 0 || cats.length === 0) return + + const c = (name: string) => crits.find(x => x.name === name)?.id + const f = (name: string) => cats.find(x => x.name === name)?.id + + const mappings: [string, string][] = [ + ['Klarheit & Struktur', 'Kommunikationsfähigkeit'], + ['Klarheit & Struktur', 'Qualität des Inhalts'], + ['Überzeugungskraft', 'Kommunikationsfähigkeit'], + ['Überzeugungskraft', 'Kundenmanagement'], + ['Aktives Zuhören', 'Kommunikationsfähigkeit'], + ['Umgang mit Fragen', 'Kommunikationsfähigkeit'], + ['Umgang mit Fragen', 'Kundenmanagement'], + ['Fachliche Tiefe', 'Qualität des Inhalts'], + ['Fachliche Tiefe', 'Inhaltliche Vollständigkeit'], + ['Problemlösung', 'Ergebnisverantwortung'], + ['Problemlösung', 'Arbeitsstil'], + ['Stakeholder-Verständnis', 'Kundenmanagement'], + ['Stakeholder-Verständnis', 'Persönliche Einstellung'], + ['Empathie', 'Persönliche Einstellung'], + ['Empathie', 'Kommunikationsfähigkeit'], + ['Vorbereitung', 'Professionalität'], + ['Vorbereitung', 'Lieferzeiten'], + ['Eigeninitiative', 'Commitment'], + ['Eigeninitiative', 'Persönliche Einstellung'], + ['Problemstrukturierung', 'Ergebnisverantwortung'], + ['Problemstrukturierung', 'Qualität des Inhalts'], + ['Hypothesenbildung', 'Ergebnisverantwortung'], + ['Hypothesenbildung', 'Arbeitsstil'], + ['Rechnen & Analyse', 'Qualität des Inhalts'], + ['Rechnen & Analyse', 'Inhaltliche Vollständigkeit'], + ['Zeitmanagement', 'Lieferzeiten'], + ['Zeitmanagement', 'Arbeitsstil'], + ] + + for (const [critName, catName] of mappings) { + const criterionId = c(critName) + const feedbackCategoryId = f(catName) + if (criterionId && feedbackCategoryId) { + await db.criterionCategoryMappings.add({ criterionId, feedbackCategoryId }) + } + } +} + +// Löscht nur Bewertungs-Scores (keine Textinhalte), verknüpft PhaseTemplates +// mit FeedbackCategory-IDs statt veralteten Criterion-IDs. +export async function migrateToUnifiedCriteriaIfNeeded() { + const cats = await db.feedbackCategories.toArray() + if (cats.length === 0) return + + const templates = await db.phaseTemplates.toArray() + if (templates.length === 0) return + + const catIds = new Set(cats.map(c => c.id!)) + const firstWithIds = templates.find(t => t.criteriaIds.length > 0) + if (firstWithIds && catIds.has(firstWithIds.criteriaIds[0])) return + + const cat = (name: string) => cats.find(c => c.name === name)?.id + + await db.conversationSkillScores.clear() + await db.assessments.clear() + + const genericCats = [ + cat('Kommunikationsfähigkeit'), cat('Persönliche Einstellung'), + cat('Kundenmanagement'), cat('Ergebnisverantwortung'), cat('Qualität des Inhalts'), + ].filter(Boolean) as number[] + + const caseCats = [ + cat('Kommunikationsfähigkeit'), cat('Ergebnisverantwortung'), + cat('Arbeitsstil'), cat('Qualität des Inhalts'), cat('Inhaltliche Vollständigkeit'), + ].filter(Boolean) as number[] + + const pitchCats = [ + cat('Kommunikationsfähigkeit'), cat('Persönliche Einstellung'), + cat('Kundenmanagement'), cat('Ergebnisverantwortung'), + cat('Qualität des Inhalts'), cat('Qualität der Unterlage'), + ].filter(Boolean) as number[] + + const types = await db.assignmentTypes.toArray() + for (const tmpl of templates) { + if (!tmpl.hasAssessment && !tmpl.hasConversation) { + await db.phaseTemplates.update(tmpl.id!, { criteriaIds: [] }) + continue + } + const aType = types.find(t => t.id === tmpl.assignmentTypeId) + let newIds: number[] + if (aType?.name === 'Case Interview') { + newIds = caseCats + } else if (aType?.name === 'Stakeholder Meeting / Pitch') { + newIds = pitchCats + } else { + newIds = genericCats + } + await db.phaseTemplates.update(tmpl.id!, { criteriaIds: newIds }) + } +} diff --git a/src/db/types.ts b/src/db/types.ts new file mode 100644 index 0000000..3049c6e --- /dev/null +++ b/src/db/types.ts @@ -0,0 +1,202 @@ +// ─── Bewertungs-Skala ──────────────────────────────────────────────────────── + +export type FeedbackRating = + | 'na' + | 'not_client_ready' + | 'partially_client_ready' + | 'nearly_client_ready' + | 'client_ready' + +// ─── Legacy-Konfiguration (nur noch für Evaluation.tsx) ─────────────────────── + +export interface Category { + id?: number + name: string + order: number +} + +export interface Criterion { + id?: number + categoryId: number + name: string + description: string + weight: number + order: number +} + +// ─── Assignment-Typen & Phase-Templates ────────────────────────────────────── + +export type PhaseKey = string + +export interface AssignmentType { + id?: number + name: string + defaultPhaseKeys: PhaseKey[] + criteriaIds?: number[] // FeedbackCriterionItem IDs; leer = alle anzeigen +} + +export interface PhaseTemplate { + id?: number + assignmentTypeId: number + phaseKey: PhaseKey + label: string + withWhom: 'lead' | 'client' + isRepeatable: boolean + hasConversation: boolean + hasAssessment: boolean + criteriaIds: number[] + order: number +} + +// ─── Stammdaten ─────────────────────────────────────────────────────────────── + +export interface Group { + id?: number + name: string + cohortYear: number +} + +export interface Consultant { + id?: number + groupId: number + firstName: string + lastName: string +} + +// ─── Assignments ───────────────────────────────────────────────────────────── + +export interface Assignment { + id?: number + title: string + client: string + assignmentTypeId: number + instituteeIds: number[] + leadName: string + status: 'active' | 'done' + createdAt: string + assignmentNumber?: string + description?: string + instituteeEnrollments?: Array<{ + instituteeId: number + from?: string + to?: string + }> +} + +// ─── Meeting-Instanzen ──────────────────────────────────────────────────────── + +export interface MeetingInstance { + id?: number + assignmentId: number + phaseTemplateId: number + phaseIndex: number + date: string + status: 'planned' | 'active' | 'done' + generalNotes: string + deletedAt?: string +} + +// ─── Gesprächs-Zeitleiste ───────────────────────────────────────────────────── + +export interface ConversationEntry { + id?: number + meetingInstanceId: number + instituteeId: number + sequenceIndex: number + note: string + fillerCount: number + updatedAt: string +} + +// criteriaId → FeedbackCategory.id (Schnellbewertung während Gesprächsbeitrag) +export interface ConversationSkillScore { + id?: number + conversationEntryId: number + criteriaId: number + score: FeedbackRating | null +} + +// ─── Gesamtbewertung ───────────────────────────────────────────────────────── + +// criteriaId → FeedbackCriterionItem.id (Gesamtbewertung am Meeting-Ende) +export interface Assessment { + id?: number + meetingInstanceId: number + instituteeId: number + criteriaId: number + score: FeedbackRating | null + note: string + updatedAt: string +} + +// ─── Feedback-Entwurf (Legacy) ──────────────────────────────────────────────── + +export interface FeedbackDraft { + id?: number + assignmentId: number + instituteeId: number + content: string + updatedAt: string +} + +// ─── Strukturiertes Assignment-Feedback ─────────────────────────────────────── + +export interface FeedbackDimension { + id?: number + name: string + order: number +} + +export interface FeedbackCategory { + id?: number + dimensionId: number + name: string + order: number +} + +export interface FeedbackCriterionItem { + id?: number + categoryId: number + name: string + weight?: number + order: number +} + +export interface AssignmentFeedback { + id?: number + assignmentId: number + instituteeId: number + status: 'draft' | 'final' + aiModel: string + updatedAt: string +} + +export interface FeedbackCategoryRating { + id?: number + assignmentFeedbackId: number + feedbackCategoryId: number + rating: FeedbackRating +} + +export interface FeedbackDimensionText { + id?: number + assignmentFeedbackId: number + feedbackDimensionId: number + achievements: string + developmentNeeds: string +} + +// ─── Legacy-Mappings ────────────────────────────────────────────────────────── + +export interface CriterionCategoryMapping { + id?: number + criterionId: number + feedbackCategoryId: number +} + +export interface CriterionLevelDescription { + id?: number + criterionItemId: number + rating: FeedbackRating + text: string +}