Initial React project

This commit is contained in:
Johan
2026-03-04 16:57:05 +01:00
parent 20370144fb
commit 689c6e9e15
17 changed files with 3448 additions and 27 deletions

View File

@@ -0,0 +1,135 @@
import { JobsPageViewModel, type JobsListItem } from './JobsPageViewModel';
import { SimulationService } from '../services/simulation.service';
import type { SimulationPersonalityInterface } from '../models/simulation-personality.interface';
export interface SimulatorInterviewItem {
id: string;
title: string;
companyName: string;
dateLabel: string;
completed: boolean;
durationMinutes: number | null;
personality: string;
}
interface RecordLike {
[key: string]: unknown;
}
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 {
return typeof value === 'number' ? value : null;
}
function parseBoolean(value: unknown): boolean | null {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'string') {
const normalized = value.toLowerCase();
if (normalized === 'completed' || normalized === 'done' || normalized === 'true') {
return true;
}
if (normalized === 'incomplete' || normalized === 'pending' || normalized === 'false') {
return false;
}
}
return null;
}
function formatDate(value: string): string {
if (!value) {
return '';
}
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return '';
}
return new Intl.DateTimeFormat('da-DK', {
day: '2-digit',
month: 'short',
year: 'numeric',
}).format(parsed);
}
function mapInterview(item: unknown, index: number): SimulatorInterviewItem | null {
const source = asRecord(item);
if (!source) {
return null;
}
const id = asString(source.id) || asString(source.interview_id) || `interview-${index}`;
const title = asString(source.job_name) || asString(source.job_title) || asString(source.title) || 'Interview';
const companyName = asString(source.company_name) || asString(source.companyName) || 'Ukendt virksomhed';
const dateRaw = asString(source.interview_date)
|| asString(source.created_at)
|| asString(source.updated_at)
|| asString(source.date);
const completed = parseBoolean(source.is_completed) ?? parseBoolean(source.completed) ?? parseBoolean(source.status) ?? true;
const durationMinutes = asNumber(source.duration_minutes) ?? asNumber(source.duration) ?? asNumber(source.length_minutes);
const personality = asString(source.personality_name)
|| asString(source.simulation_personality_name)
|| asString(source.personality)
|| 'Professionel';
return {
id,
title,
companyName,
dateLabel: formatDate(dateRaw),
completed,
durationMinutes,
personality,
};
}
export class SimulatorViewModel {
constructor(
private readonly jobsViewModel: JobsPageViewModel = new JobsPageViewModel(),
private readonly simulationService: SimulationService = new SimulationService(),
) {}
async getCandidateProfile(): Promise<{ imageUrl?: string; name: string }> {
return this.jobsViewModel.getCandidateProfile();
}
async getJobs(): Promise<JobsListItem[]> {
try {
return await this.jobsViewModel.getTabItems('jobs');
} catch {
return [];
}
}
async getPersonalities(): Promise<SimulationPersonalityInterface[]> {
try {
const list = await this.simulationService.listSimulationPersonalities();
return Array.isArray(list) ? list : [];
} catch {
return [];
}
}
async getInterviews(limit: number = 12): Promise<SimulatorInterviewItem[]> {
try {
const payload = await this.simulationService.listInterviews(limit, 0);
const root = asRecord(payload);
const list = Array.isArray(root?.interviews) ? root.interviews : (Array.isArray(payload) ? payload : []);
return list
.map((item, index) => mapInterview(item, index))
.filter((item): item is SimulatorInterviewItem => Boolean(item));
} catch {
return [];
}
}
}