import { SimulationService } from '../services/simulation.service'; interface RecordLike { [key: string]: unknown; } export interface SimulatorEvaluationSelectionContext { companyName?: string; dateLabel?: string; title?: string; } export interface EvaluationImprovementItem { id: string; title: string; behavior: string; effect: string; nextStep: string; } export interface SimulatorEvaluationData { companyName: string; dateLabel: string; evaluationLabel: string; interviewerEvaluationBody: string; interviewerEvaluationLead: string; interviewerEvaluationTitle: string; interviewerScore: number; interviewTitle: string; recommendations: string[]; selfScore: number; strengths: string[]; suggestions: EvaluationImprovementItem[]; } function asRecord(value: unknown): RecordLike | null { return typeof value === 'object' && value !== null ? (value as RecordLike) : null; } function asString(value: unknown): string { return typeof value === 'string' ? value : ''; } function asNumber(value: unknown): number | null { if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string') { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } return null; } function asStringArray(value: unknown): string[] { if (!Array.isArray(value)) { return []; } return value.map((item) => asString(item).trim()).filter(Boolean); } function pickString(root: RecordLike | null, keys: string[]): string { if (!root) { return ''; } for (const key of keys) { const value = asString(root[key]).trim(); if (value) { return value; } } return ''; } function pickNumber(root: RecordLike | null, keys: string[]): number | null { if (!root) { return null; } for (const key of keys) { const value = asNumber(root[key]); if (value !== null) { return value; } } return null; } function formatDateLabel(value: string): string { if (!value) { return ''; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return new Intl.DateTimeFormat('da-DK', { day: '2-digit', month: 'long', year: 'numeric', }).format(date); } function normalizeScore(value: number | null, fallback: number): number { if (value === null) { return fallback; } return Math.max(0, Math.min(10, Math.round(value))); } const FALLBACK_SUGGESTIONS: EvaluationImprovementItem[] = [ { id: '1', title: 'Mere struktur og korthed i dine svar', behavior: 'Du gav nogle svar, der blev lange og gentagende i stedet for fokuserede pa kernen.', effect: 'Det kan gore det svaerere for intervieweren hurtigt at vurdere din erfaring og beslutninger.', nextStep: 'Svar i 3 trin: pointe, konkret eksempel, resultat. Sigt efter 30-60 sekunder pr. svar.', }, { id: '2', title: 'Flere konkrete eksempler pa performance i service', behavior: 'Du fortalte om ansvar, men gav fa konkrete scenarier med handling og resultat.', effect: 'Uden konkrete cases bliver niveau og paalidelighed svaerere at validere.', nextStep: 'Forbered 2-3 STAR-historier med tydelig situation, handling og maelbart resultat.', }, { id: '3', title: 'Gor din motivation mere maelrettet virksomheden', behavior: 'Motivationen var positiv, men ikke altid koblet direkte til virksomhedens drift og behov.', effect: 'Du kan fremsta generelt motiveret i stedet for specifikt relevant for rollen.', nextStep: 'Naevn 2-3 konkrete grunde til, at netop deres setup matcher din erfaring.', }, { id: '4', title: 'Konkretiser kvalitet og sikkerhed i praksis', behavior: 'Du naevnte standarder og certificeringer, men beskrev fa daglige rutiner.', effect: 'Det reducerer tydeligheden omkring, hvordan du arbejder sikkert i travle perioder.', nextStep: 'Beskriv faste rutiner for kontrol, logning og hurtig korrektion under pres.', }, ]; const FALLBACK_STRENGTHS = [ 'Du viser relevant erfaring for rollen og kommunikerer ro under pres.', 'Du arbejder struktureret med kvalitet og timing i service.', 'Du har en moden team-tilgang med fokus pa samarbejde.', 'Du kobler dine svar til konkrete arbejdsrutiner og drift.', 'Du fremstar stabil og ansvarlig i hektiske situationer.', ]; const FALLBACK_RECOMMENDATIONS = [ 'Forbered 3 korte STAR-historier med maelbare resultater.', 'Lav en 60-sekunders pitch af din profil og vaerdiskabelse.', 'Research virksomheden i 10-15 minutter inden samtalen.', 'Forbered 4-5 konkrete spoergsmaal om rolle og forventninger.', 'Afslut svar med resultat, sa din effekt bliver tydelig.', ]; const FALLBACK_DATA: SimulatorEvaluationData = { companyName: 'Ukendt virksomhed', dateLabel: 'Nyligt', evaluationLabel: 'Interview Feedback', interviewerEvaluationTitle: 'Interviewer evaluering', interviewerEvaluationLead: 'Du er godt med. Din praestation viste styrker der matcher stillingen.', interviewerEvaluationBody: 'Du viste relevante kompetencer og en stabil tilgang under pres. Du kan staerke dit indtryk yderligere ved at svare mere struktureret og bruge flere konkrete resultateksempler.', interviewTitle: 'Stilling', interviewerScore: 8, selfScore: 5, strengths: FALLBACK_STRENGTHS, recommendations: FALLBACK_RECOMMENDATIONS, suggestions: FALLBACK_SUGGESTIONS, }; function mapSuggestion(item: unknown, index: number): EvaluationImprovementItem | null { const source = asRecord(item); if (!source) { return null; } const title = pickString(source, ['title', 'heading', 'name', 'subject']); const behavior = pickString(source, ['behavior', 'adfaerd', 'observation', 'issue']); const effect = pickString(source, ['effect', 'impact', 'consequence']); const nextStep = pickString(source, ['next_step', 'nextStep', 'recommendation', 'suggestion']); if (!title && !behavior && !effect && !nextStep) { return null; } return { id: asString(source.id) || String(index + 1), title: title || `Forbedringspunkt ${index + 1}`, behavior: behavior || 'Ingen detaljer tilgaengelige.', effect: effect || 'Ingen detaljer tilgaengelige.', nextStep: nextStep || 'Ingen detaljer tilgaengelige.', }; } function extractSuggestions(root: RecordLike | null): EvaluationImprovementItem[] { const candidates = [ root?.suggestions, root?.improvements, root?.improvement_points, root?.feedback_points, asRecord(root?.evaluation)?.suggestions, asRecord(root?.evaluation)?.improvements, ]; for (const candidate of candidates) { if (!Array.isArray(candidate)) { continue; } const mapped = candidate .map((item, index) => mapSuggestion(item, index)) .filter((item): item is EvaluationImprovementItem => Boolean(item)); if (mapped.length > 0) { return mapped; } } return FALLBACK_SUGGESTIONS; } function extractStringList(root: RecordLike | null, keys: string[], fallback: string[]): string[] { for (const key of keys) { const values = asStringArray(root?.[key]); if (values.length > 0) { return values; } } const evaluation = asRecord(root?.evaluation); for (const key of keys) { const values = asStringArray(evaluation?.[key]); if (values.length > 0) { return values; } } return fallback; } export class SimulatorEvaluationViewModel { constructor(private readonly simulationService: SimulationService = new SimulationService()) {} async getEvaluation(interviewId: string, context?: SimulatorEvaluationSelectionContext): Promise { try { const payload = await this.simulationService.getInterviewEvaluation(interviewId); const root = asRecord(payload); const evaluation = asRecord(root?.evaluation); const companyName = pickString(root, ['company_name', 'companyName']) || pickString(evaluation, ['company_name', 'companyName']) || context?.companyName || FALLBACK_DATA.companyName; const interviewTitle = pickString(root, ['job_title', 'job_name', 'title']) || pickString(evaluation, ['job_title', 'job_name', 'title']) || context?.title || FALLBACK_DATA.interviewTitle; const rawDate = pickString(root, ['interview_date', 'created_at', 'date']) || pickString(evaluation, ['interview_date', 'created_at', 'date']); const dateLabel = formatDateLabel(rawDate) || context?.dateLabel || FALLBACK_DATA.dateLabel; const interviewerEvaluationLead = pickString(root, ['interviewer_evaluation_lead', 'lead']) || pickString(evaluation, ['interviewer_evaluation_lead', 'lead']) || FALLBACK_DATA.interviewerEvaluationLead; const interviewerEvaluationBody = pickString(root, ['interviewer_evaluation', 'summary', 'feedback']) || pickString(evaluation, ['interviewer_evaluation', 'summary', 'feedback']) || FALLBACK_DATA.interviewerEvaluationBody; const interviewerEvaluationTitle = pickString(root, ['evaluation_title', 'interviewer_title']) || pickString(evaluation, ['evaluation_title', 'interviewer_title']) || FALLBACK_DATA.interviewerEvaluationTitle; const evaluationLabel = pickString(root, ['label', 'evaluation_label']) || pickString(evaluation, ['label', 'evaluation_label']) || FALLBACK_DATA.evaluationLabel; const selfScore = normalizeScore( pickNumber(root, ['candidate_score', 'self_score', 'self_rating']) ?? pickNumber(evaluation, ['candidate_score', 'self_score', 'self_rating']), FALLBACK_DATA.selfScore, ); const interviewerScore = normalizeScore( pickNumber(root, ['interviewer_score', 'score', 'interviewer_rating']) ?? pickNumber(evaluation, ['interviewer_score', 'score', 'interviewer_rating']), FALLBACK_DATA.interviewerScore, ); return { companyName, dateLabel, evaluationLabel, interviewerEvaluationBody, interviewerEvaluationLead, interviewerEvaluationTitle, interviewerScore, interviewTitle, recommendations: extractStringList(root, ['recommendations', 'constructive_recommendations', 'next_steps'], FALLBACK_RECOMMENDATIONS), selfScore, strengths: extractStringList(root, ['strengths', 'highlights'], FALLBACK_STRENGTHS), suggestions: extractSuggestions(root), }; } catch { return { ...FALLBACK_DATA, companyName: context?.companyName || FALLBACK_DATA.companyName, interviewTitle: context?.title || FALLBACK_DATA.interviewTitle, dateLabel: context?.dateLabel || FALLBACK_DATA.dateLabel, }; } } async submitRating(interviewId: string, rating: number, comment: string): Promise { await this.simulationService.submitEvaluationRating(interviewId, rating, comment); } }