323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
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<SimulatorEvaluationData> {
|
|
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<void> {
|
|
await this.simulationService.submitEvaluationRating(interviewId, rating, comment);
|
|
}
|
|
}
|