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
This commit is contained in:
Lars 2026-07-02 10:24:03 +02:00
parent 97e3eeb8a6
commit 068c4cdcfd
7 changed files with 606 additions and 602 deletions

View File

@ -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<Category>
criteria!: Table<Criterion>
assignmentTypes!: Table<AssignmentType>
phaseTemplates!: Table<PhaseTemplate>
groups!: Table<Group>
consultants!: Table<Consultant>
assignments!: Table<Assignment>
meetingInstances!: Table<MeetingInstance>
conversationEntries!: Table<ConversationEntry>
conversationSkillScores!: Table<ConversationSkillScore>
assessments!: Table<Assessment>
feedbackDrafts!: Table<FeedbackDraft>
feedbackDimensions!: Table<FeedbackDimension>
feedbackCategories!: Table<FeedbackCategory>
feedbackCriterionItems!: Table<FeedbackCriterionItem>
assignmentFeedbacks!: Table<AssignmentFeedback>
feedbackCategoryRatings!: Table<FeedbackCategoryRating>
feedbackDimensionTexts!: Table<FeedbackDimensionText>
criterionCategoryMappings!: Table<CriterionCategoryMapping>
criterionLevelDescriptions!: Table<CriterionLevelDescription>
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'

135
src/db/schema.ts Normal file
View File

@ -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<Category>
criteria!: Table<Criterion>
assignmentTypes!: Table<AssignmentType>
phaseTemplates!: Table<PhaseTemplate>
groups!: Table<Group>
consultants!: Table<Consultant>
assignments!: Table<Assignment>
meetingInstances!: Table<MeetingInstance>
conversationEntries!: Table<ConversationEntry>
conversationSkillScores!: Table<ConversationSkillScore>
assessments!: Table<Assessment>
feedbackDrafts!: Table<FeedbackDraft>
feedbackDimensions!: Table<FeedbackDimension>
feedbackCategories!: Table<FeedbackCategory>
feedbackCriterionItems!: Table<FeedbackCriterionItem>
assignmentFeedbacks!: Table<AssignmentFeedback>
feedbackCategoryRatings!: Table<FeedbackCategoryRating>
feedbackDimensionTexts!: Table<FeedbackDimensionText>
criterionCategoryMappings!: Table<CriterionCategoryMapping>
criterionLevelDescriptions!: Table<CriterionLevelDescription>
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()

View File

@ -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 })
}

View File

@ -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 })
}
}
}
}

3
src/db/seeds/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { seedIfEmpty } from './assignmentTypes'
export { seedFeedbackStructureIfEmpty } from './feedbackStructure'
export { seedCriterionMappingsIfEmpty, migrateToUnifiedCriteriaIfNeeded } from './migrations'

106
src/db/seeds/migrations.ts Normal file
View File

@ -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 })
}
}

202
src/db/types.ts Normal file
View File

@ -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
}