Initial React project
This commit is contained in:
322
src/mvvm/viewmodels/SimulatorEvaluationViewModel.ts
Normal file
322
src/mvvm/viewmodels/SimulatorEvaluationViewModel.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user