import { getAllRows, getRowById, getRowsWhereIn, insertRow, updateRow, deleteRowById, deleteRowsWhereEquals, deleteRowsWhereIn, } from '../crud' import type { Group, Consultant, Assignment, AssignmentFeedback, FeedbackCategoryRating, FeedbackCategory, ConversationEntry, RatingScore, ConsultantImportRow, ConsultantImportResult, HardDeleteConsultantResult, ConsultantAssignmentSummary, ConsultantDetail, } from '../../../src/db/types' export type { ConsultantImportRow, ConsultantImportResult, HardDeleteConsultantResult, ConsultantAssignmentSummary, ConsultantDetail, } export function normalizeEmail(email: string): string { return email.trim().toLowerCase() } export function listGroups(): Group[] { return getAllRows('groups').sort((a, b) => a.cohortYear !== b.cohortYear ? b.cohortYear - a.cohortYear : a.name.localeCompare(b.name), ) } export const getGroup = (id: number): Group | undefined => getRowById('groups', id) export const addGroup = (group: Omit): number => insertRow('groups', group) export function updateGroup(id: number, patch: Partial): number { return updateRow('groups', id, patch) } /** Nur leere Gruppen — sonst Fehlertext. */ export function deleteGroup(id: number): { ok: true } | { ok: false; reason: string; memberCount: number } { const members = getAllRows('consultants').filter(c => c.groupId === id) if (members.length > 0) { return { ok: false, reason: 'Gruppe hat noch Personen — zuerst umziehen oder archivieren/löschen.', memberCount: members.length, } } deleteRowById('groups', id) return { ok: true } } export const listConsultants = (): Consultant[] => getAllRows('consultants') export function listActiveConsultants(): Consultant[] { return listConsultants().filter(c => !c.archivedAt) } export function listArchivedConsultants(): Consultant[] { return listConsultants().filter(c => !!c.archivedAt) } export const getConsultant = (id: number): Consultant | undefined => getRowById('consultants', id) export const getConsultantsByIds = (ids: number[]): Consultant[] => getRowsWhereIn('consultants', 'id', ids) export function findConsultantByEmail(email: string): Consultant | undefined { const key = normalizeEmail(email) if (!key) return undefined return listConsultants().find(c => normalizeEmail(c.email ?? '') === key) } function assertUniqueEmail(email: string, exceptId?: number): void { const key = normalizeEmail(email) if (!key) throw new Error('E-Mail ist Pflicht.') const hit = listConsultants().find(c => normalizeEmail(c.email ?? '') === key && c.id !== exceptId) if (hit) throw new Error(`E-Mail „${key}" ist bereits vergeben (${hit.firstName} ${hit.lastName}).`) } export function addConsultant(consultant: Omit): number { const email = normalizeEmail(consultant.email ?? '') assertUniqueEmail(email) return insertRow('consultants', { ...consultant, email, firstName: consultant.firstName.trim(), lastName: consultant.lastName.trim(), }) } export function updateConsultant(id: number, patch: Partial): number { const next: Partial = { ...patch } if (patch.email !== undefined) { const email = normalizeEmail(patch.email) assertUniqueEmail(email, id) next.email = email } if (patch.firstName !== undefined) next.firstName = patch.firstName.trim() if (patch.lastName !== undefined) next.lastName = patch.lastName.trim() return updateRow('consultants', id, next) } export function archiveConsultant(id: number): number { return updateRow('consultants', id, { archivedAt: new Date().toISOString() }) } export function unarchiveConsultant(id: number): number { return updateRow('consultants', id, { archivedAt: null as unknown as undefined }) } /** * Import in eine gewählte Zielgruppe. Abgleich nur über E-Mail. * moveIfDifferentGroup: bekannte Personen mit anderer Gruppe in die Zielgruppe verschieben. */ export function importConsultantsToGroup( groupId: number, rows: ConsultantImportRow[], moveIfDifferentGroup = true, ): ConsultantImportResult { const group = getGroup(groupId) if (!group) throw new Error('Zielgruppe nicht gefunden.') const result: ConsultantImportResult = { created: 0, updated: 0, moved: 0, skipped: 0, errors: [] } const seenInFile = new Set() for (let i = 0; i < rows.length; i++) { const row = rows[i]! const line = i + 1 const email = normalizeEmail(row.email ?? '') const firstName = (row.firstName ?? '').trim() const lastName = (row.lastName ?? '').trim() if (!email) { result.errors.push(`Zeile ${line}: E-Mail fehlt`) result.skipped++ continue } if (!firstName || !lastName) { result.errors.push(`Zeile ${line}: Name unvollständig (${email})`) result.skipped++ continue } if (seenInFile.has(email)) { result.errors.push(`Zeile ${line}: doppelte E-Mail in Datei (${email})`) result.skipped++ continue } seenInFile.add(email) const existing = findConsultantByEmail(email) if (!existing?.id) { insertRow('consultants', { groupId, firstName, lastName, email }) result.created++ continue } const patch: Partial = { firstName, lastName } if (existing.groupId !== groupId) { if (moveIfDifferentGroup) { patch.groupId = groupId result.moved++ } } updateRow('consultants', existing.id, patch) result.updated++ } return result } /** Feedback + personenbezogene Meeting-Daten weg; Assignment bleibt, Person wird entkoppelt. */ export function hardDeleteConsultant(id: number): HardDeleteConsultantResult { const person = getConsultant(id) if (!person) throw new Error('Person nicht gefunden.') const feedbacks = getAllRows('assignmentFeedbacks').filter(f => f.instituteeId === id) const feedbackIds = feedbacks.map(f => f.id!).filter(Boolean) if (feedbackIds.length) { deleteRowsWhereIn('feedbackCategoryRatings', 'assignmentFeedbackId', feedbackIds) deleteRowsWhereIn('feedbackDimensionTexts', 'assignmentFeedbackId', feedbackIds) deleteRowsWhereIn('feedbackCategoryTexts', 'assignmentFeedbackId', feedbackIds) } deleteRowsWhereEquals('assignmentFeedbacks', 'instituteeId', id) deleteRowsWhereEquals('feedbackDrafts', 'instituteeId', id) const entries = getAllRows('conversationEntries').filter(e => e.instituteeId === id) const entryIds = entries.map(e => e.id!).filter(Boolean) if (entryIds.length) { deleteRowsWhereIn('conversationSkillScores', 'conversationEntryId', entryIds) } deleteRowsWhereEquals('conversationEntries', 'instituteeId', id) deleteRowsWhereEquals('assessments', 'instituteeId', id) deleteRowsWhereEquals('meetingCategoryRatings', 'instituteeId', id) deleteRowsWhereEquals('checklistResponses', 'instituteeId', id) const emptiedAssignmentIds: number[] = [] let assignmentsTouched = 0 const assignments = getAllRows('assignments') for (const a of assignments) { if (!a.id || !a.instituteeIds.includes(id)) continue assignmentsTouched++ const instituteeIds = a.instituteeIds.filter(x => x !== id) const instituteeEnrollments = (a.instituteeEnrollments ?? []).filter(e => e.instituteeId !== id) updateRow('assignments', a.id, { instituteeIds, instituteeEnrollments }) if (instituteeIds.length === 0) emptiedAssignmentIds.push(a.id) } deleteRowById('consultants', id) return { emptiedAssignmentIds, feedbacksDeleted: feedbacks.length, assignmentsTouched, } } /** @deprecated — nutze hardDeleteConsultant; bleibt als Alias für alte Client-Aufrufe. */ export function deleteConsultant(id: number): void { hardDeleteConsultant(id) } export function getConsultantDetail(id: number): ConsultantDetail | undefined { const consultant = getConsultant(id) if (!consultant) return undefined const group = getGroup(consultant.groupId) const types = getAllRows<{ id?: number; name: string }>('assignmentTypes') const typeMap = new Map(types.map(t => [t.id!, t.name])) const categories = getAllRows('feedbackCategories') const catMap = new Map(categories.map(c => [c.id!, c.name])) const allFeedbacks = getAllRows('assignmentFeedbacks') const allRatings = getAllRows('feedbackCategoryRatings') const assignments = getAllRows('assignments') .filter(a => a.instituteeIds.includes(id)) .map(a => { const fb = allFeedbacks.find(f => f.assignmentId === a.id && f.instituteeId === id) const ratings = fb?.id ? allRatings .filter(r => r.assignmentFeedbackId === fb.id) .map(r => ({ categoryId: r.feedbackCategoryId, categoryName: catMap.get(r.feedbackCategoryId) ?? `Kategorie ${r.feedbackCategoryId}`, rating: (r.rating ?? null) as RatingScore | null, })) : [] return { assignmentId: a.id!, title: a.title, assignmentNumber: a.assignmentNumber, client: a.client, status: a.status, deletedAt: a.deletedAt, archivedAt: a.archivedAt, plannedStart: a.plannedStart, plannedEnd: a.plannedEnd, createdAt: a.createdAt, typeName: typeMap.get(a.assignmentTypeId) ?? '—', feedbackStatus: (!fb ? 'none' : fb.status === 'final' ? 'final' : 'draft') as 'none' | 'draft' | 'final', categoryRatings: ratings, } }) .sort((a, b) => { const ae = a.plannedEnd ?? a.createdAt const be = b.plannedEnd ?? b.createdAt return be.localeCompare(ae) }) return { consultant, group, assignments } }