Initial React project

This commit is contained in:
Johan
2026-02-14 10:46:50 +01:00
commit 6c1f178ba9
5884 changed files with 1701440 additions and 0 deletions

27
src/mvvm/README.md Normal file
View File

@@ -0,0 +1,27 @@
# MVVM Data Layer
This folder contains the Angular-to-React data-layer migration in native TypeScript async style.
## Structure
- `core/`: shared primitives (`HttpClient`, async state helpers)
- `models/`: domain interfaces/types migrated from Angular
- `services/`: API/data services migrated from Angular
- `index.ts`: top-level barrel exports
## Usage
```ts
import { Services } from '../mvvm';
const authService = new Services.AuthService();
const login = await authService.login(email, password);
```
or use provided service singletons where available, e.g.:
```ts
import { jobService } from './services/job.service';
const jobs = await jobService.getJobsV2(0, 0, 20, []);
```

View File

@@ -0,0 +1,51 @@
export type Observer<T> = (value: T) => void
export interface Observable<T> {
subscribe: (observer: Observer<T>) => { unsubscribe: () => void }
}
export class Subject<T> {
protected observers = new Set<Observer<T>>()
next(value: T): void {
this.observers.forEach((observer) => observer(value))
}
asObservable(): Observable<T> {
return {
subscribe: (observer: Observer<T>) => {
this.observers.add(observer)
return {
unsubscribe: () => this.observers.delete(observer),
}
},
}
}
}
export class BehaviorSubject<T> extends Subject<T> {
private currentValue: T
constructor(currentValue: T) {
super()
this.currentValue = currentValue
}
override next(value: T): void {
this.currentValue = value
super.next(value)
}
get value(): T {
return this.currentValue
}
override asObservable(): Observable<T> {
return {
subscribe: (observer: Observer<T>) => {
observer(this.currentValue)
return super.asObservable().subscribe(observer)
},
}
}
}

View File

@@ -0,0 +1,120 @@
export class HttpParams {
private readonly searchParams: URLSearchParams
constructor(searchParams?: URLSearchParams) {
this.searchParams = searchParams ? new URLSearchParams(searchParams) : new URLSearchParams()
}
append(key: string, value: string | number | boolean): HttpParams {
const next = new URLSearchParams(this.searchParams)
next.append(key, String(value))
return new HttpParams(next)
}
set(key: string, value: string | number | boolean): HttpParams {
const next = new URLSearchParams(this.searchParams)
next.set(key, String(value))
return new HttpParams(next)
}
toString(): string {
return this.searchParams.toString()
}
}
type QueryParams = HttpParams | Record<string, string | number | boolean | Array<string | number | boolean>>
function withQuery(url: string, params?: QueryParams): string {
if (!params) return url
const target = new URL(url)
if (params instanceof HttpParams) {
const query = params.toString()
if (query) {
const source = new URLSearchParams(query)
source.forEach((value, key) => target.searchParams.append(key, value))
}
return target.toString()
}
Object.entries(params).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((item) => target.searchParams.append(key, String(item)))
return
}
target.searchParams.set(key, String(value))
})
return target.toString()
}
async function parseResponse<T>(response: Response, responseType: 'json' | 'blob' = 'json'): Promise<T> {
if (!response.ok) {
const text = await response.text()
throw new Error(text || `HTTP ${response.status}`)
}
if (response.status === 204) {
return undefined as T
}
if (responseType === 'blob') {
return (await response.blob()) as T
}
return (await response.json()) as T
}
function withAuth(headers?: Record<string, string>): Record<string, string> {
const token = typeof window === 'undefined' ? null : window.localStorage.getItem('token')
return {
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(headers ?? {}),
}
}
export class HttpClient {
async get<T>(
url: string,
options?: { params?: QueryParams; headers?: Record<string, string>; responseType?: 'json' | 'blob' },
): Promise<T> {
const response = await fetch(withQuery(url, options?.params), {
method: 'GET',
headers: withAuth(options?.headers),
})
return parseResponse<T>(response, options?.responseType)
}
async post<T>(url: string, body?: unknown, options?: { params?: QueryParams; headers?: Record<string, string> }): Promise<T> {
const response = await fetch(withQuery(url, options?.params), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...withAuth(options?.headers),
},
body: body === undefined ? undefined : JSON.stringify(body),
})
return parseResponse<T>(response)
}
async put<T>(url: string, body?: unknown, options?: { params?: QueryParams; headers?: Record<string, string> }): Promise<T> {
const response = await fetch(withQuery(url, options?.params), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...withAuth(options?.headers),
},
body: body === undefined ? undefined : JSON.stringify(body),
})
return parseResponse<T>(response)
}
async delete<T>(url: string, options?: { params?: QueryParams; headers?: Record<string, string> }): Promise<T> {
const response = await fetch(withQuery(url, options?.params), {
method: 'DELETE',
headers: withAuth(options?.headers),
})
return parseResponse<T>(response)
}
}
export const httpClient = new HttpClient()

3
src/mvvm/core/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './http-client';
export * from './async-state';

4
src/mvvm/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * as Models from './models';
export * as Services from './services';
export * as Core from './core';
export * as ViewModels from './viewmodels';

View File

@@ -0,0 +1,4 @@
interface PaymentIntentResponse {
success: boolean;
paymentIntent: any; // Du kan også specificere det mere hvis ønsket
}

View File

@@ -0,0 +1,29 @@
export interface AiGeneratedCVDescription {
id: number;
content: GeneratedCvDescriptionContent[];
processing: boolean;
done: boolean;
state: number;
subState: number;
}
export interface GeneratedCvDescriptionContent{
language: string;
flavorText: string;
educations: GeneratedCvEducation[];
experiences: GeneratedCvExperience[];
}
export interface GeneratedCvEducation{
educationId: string;
description: string;
name: string;
textBefore: string;
}
export interface GeneratedCvExperience{
experienceId: string;
description: string;
name: string;
textBefore: string;
}

View File

@@ -0,0 +1,16 @@
export interface AllLanguageInterface {
allLanguages: PlainLanguageInterface[];
priorityLanguages: PlainLanguageInterface[];
}
export interface PlainLanguageInterface{
id: string;
isO639: string;
name: string;
ownName: string;
priority: number;
candidateLanguages?: [] | null;
jobLanguages?: [] | null;
jobnetLanguage?: [];
}

View File

@@ -0,0 +1,11 @@
export interface ApplicationExamination {
id: string;
aiGeneratedJobApplicationId: number;
gpt52: number | null;
gpt52Reason: string | null;
gemini30: number | null;
gemini30Reason: string | null;
processing: boolean;
done: boolean;
createdTs: string | null;
}

View File

@@ -0,0 +1,25 @@
export interface AppliedJobInterface {
address: string;
applicationDeadline: Date;
candidateDistance: number;
companyLogoImage: string;
companyName: string;
descriptionIntro: string;
fromJobnet: boolean;
id: string;
jobTypeDescription: string;
jobTypes: number[];
occupationName: string;
paid: boolean;
period: string;
string: string;
salary: number;
stage: number;
time: string;
title: string;
// unknown type
workTimes: string;
logoUrl: string;
isActive: boolean;
applyJobBefore: Date;
}

View File

@@ -0,0 +1,6 @@
export interface AuthInterface{
id: string;
token: string;
email: string;
runOutDate: string;
}

View File

@@ -0,0 +1,18 @@
import type {JobPostingInterface} from "./job.interface";
export interface CandidateApplicationInterface{
id: string;
jobPostingId: string;
candidateId: string;
applicationDate: Date;
jobPosting: JobPostingInterface;
motivatedTitle: string;
motivatedBody: string;
motivatedAttachment: string;
applicationSeen: boolean;
applicationRejected: boolean;
rejectionSeen: boolean;
applicationAccepted: boolean;
acceptSeen: boolean;
}

View File

@@ -0,0 +1,7 @@
export interface CandidateRetentionResponseInterface {
id: string;
createdAt: string;
accountClosed: boolean;
closedAt: Date;
retentionDays: number;
}

View File

@@ -0,0 +1,9 @@
export interface CandidateSubscriptionGiftInterface {
id: number;
endUserSubscriptionProductName: string;
freeDays: number;
isActivated: boolean;
activatedAt?: string;
expiresAt?: string;
createdAt: string;
}

View File

@@ -0,0 +1,188 @@
import type {OccupationInterface} from "./occupation.interface";
import type {SchoolInterface} from "./school.interface";
import type {EducationSearchInterface} from "./education-search.interface";
import type {TranslationInterface} from "./translation.interface";
export interface CandidateInterface{
accountClosed: boolean;
address: LatLngInterface | undefined;
birthday: Date | string;
// unknown
certificates: CertificationInterface[];
// unknown, but guessing
closedAt: Date;
// unknown
courses: [];
createdAt: Date;
cvTipsEnabled: true;
driversLicenses: DriversLicenseInterface[];
// unknown
educations: EducationInterface[];
email: string;
experiences: ExperienceInterface[];
firstName: string;
gender: string;
hasCertificates: boolean;
hasDriversLicenses: boolean;
hasEducations: boolean;
hasExperiences: boolean;
hasLanguages: boolean;
hasSkills: boolean;
hasStreetAddress: boolean;
id: string;
image: string;
isActive: boolean;
isAnonymized: boolean;
// unknown
jobApplications: [];
// unknown
languages: LanguageInterface[];
lastLogin: Date;
lastName: string;
name: string;
password: string;
personalDescription: string;
personalDescriptions: TranslationInterface[];
// unknown but guessing
phoneNumber: string;
setToInactiveAutomatically: boolean;
signupStage: number;
// unknown
skills: SkillInterface[];
useOwnCar: boolean;
yearsOld: string;
newImage: boolean;
imageUrl: string;
generatedCv: boolean;
isGeneratingCv: boolean;
cvs: CvInterface[];
}
export interface CvInterface{
language: string;
generatedCv: boolean;
isGeneratingCv: boolean;
}
export interface SkillInterface{
levelName?: string;
level: number;
id?: string;
editMode?: boolean;
qualification: QualificationInterface;
}
export interface QualificationInterface{
id: string;
reviewed?: boolean;
// unknown
dateAdded?: object;
candidateCourses?: object;
candidateCertificates?: object;
candidateSkills?: object;
jobCertificates?: object;
jobSkills?: object;
occupations?: object;
jobnetSkills?: [];
jobnetCertificate?: [];
type: number;
name: string;
}
export interface EducationInterface{
editMode?: boolean;
comments: string;
education: EducationSearchInterface;
educationDisced15: number;
fromDate: Date;
id: string;
institution: SchoolInterface;
institutionNumber: number;
isCurrent: boolean;
toDate: Date;
translationComments?: TranslationInterface[];
}
export interface LanguageInterface{
id: string;
level: number;
levelName: string;
language: CandidateLanguageInterface;
editMode?: boolean;
}
export interface CandidateLanguageInterface{
// unknown, guess
candidateLanguage: string;
id: string;
isO639: string;
// unknown
jobLanguages: [];
// unknown
jobnetLanguage: []
name: string;
ownName: string;
priority: number,
}
export interface LatLngInterface{
latitude: number;
longitude: number;
additionalCityName?: string;
awsId: string;
awsUrl: string;
houseNum: string;
road: string;
zip: string;
zipName: string;
}
export interface DriversLicenseInterface{
driversLicenseId: string;
id: string;
level: number;
levelName: string;
driversLicense: DriverLicenseCandidateInterface;
editMode: boolean;
}
export interface DriverLicenseCandidateInterface{
// unknown guessing
candidateDriversLicense: string;
code: string;
id: string;
// unknown guessing
jobDriversLicense: string;
jobnetDriversLicense: string;
name: string;
priority: number;
special: boolean;
}
export interface ExperienceInterface{
// unknown but guessing
comments: string;
translationComments?: TranslationInterface[];
companyName: string;
fromDate: Date;
id?: string;
isCurrent: boolean;
toDate: Date;
occupation: OccupationInterface;
editMode?: boolean;
}
export interface CertificationInterface{
levelName: string;
origin: string;
validTo: Date;
level: number;
id: string;
qualification: QualificationInterface;
editMode?: boolean;
}

View File

@@ -0,0 +1,19 @@
import type {ChatMessageInterface} from "./chat-message.interface";
import type {JobPostingOverviewInterface} from "./job-posting-overview.interface";
export interface ChatMessageThreadInterface{
id: string;
companyLogo: string;
companyLogoUrl: string;
companyName: string;
candidateFirstName: string;
candidateLastName: string;
candidateImage: string;
allMessages: ChatMessageInterface[];
latestMessage: ChatMessageInterface;
title: string;
messagesLoaded: boolean;
jobPostingId: string;
jobPosting: JobPostingOverviewInterface;
isFromSupport: boolean;
}

View File

@@ -0,0 +1,8 @@
export interface ChatMessageInterface{
id?: string | undefined;
threadId: string | undefined;
timeSent: Date;
fromCandidate: boolean;
text: string;
seen?: Date | undefined;
}

View File

@@ -0,0 +1,4 @@
export interface CvLanguageInterface {
shortCode: string;
name: string;
}

View File

@@ -0,0 +1,26 @@
export interface CvSuggestionInterface{
escoId: number;
escoName: string;
jobImprovementSuggestion: JobImprovementSuggestion;
improvements: ImprovementInterface[] | undefined;
}
export interface JobImprovementSuggestion{
certificates: ImprovementInterface[];
educations: ImprovementInterface[];
qualifications: ImprovementInterface[];
driversLicenses: ImprovementInterface[];
languages: ImprovementInterface[];
}
export interface ImprovementInterface{
name: string;
description: string;
type: string;
jobChanceIncrease: number;
estimatedDurationMonths: number | null | undefined;
improvementType: string;
escoId: number;
shortName: string;
}

View File

@@ -0,0 +1,104 @@
export interface CvUploadDataInterface {
cv_upload_data_id: number;
creation_state?: number;
response_data?: AiResponseDataInterface;
end_user_experience_done?: boolean;
end_user_education_done?: boolean;
end_user_certificate_done?: boolean;
end_user_language_done?: boolean;
end_user_qualification_done?: boolean;
end_user_drivers_license_done?: boolean;
end_user_profile_text_done?: boolean;
}
export interface AiResponseDataInterface {
languages: CvUploadLanguageInterface[];
educations: CvUploadEducationInterface[];
work_experience: CvUploadWorkExperienceInterface[];
certificates: CvUploadCertificateInterface[];
qualifications: CvUploadQualificationInterface[];
driver_licenses: CvUploadDriversLicenseInterface[];
profile_text: string;
}
export interface CvUploadDriversLicenseInterface{
name: string;
driver_license_id: string | null;
readyToSave: boolean;
editMode: boolean;
levelId: number;
levelName: string;
}
export interface CvUploadLanguageInterface{
name: string;
language_id: string;
levelId: number;
readyToSave: boolean;
editMode: boolean;
levelName: string;
}
export interface CvUploadCertificateInterface {
name: string;
certificate_id: string | null;
readyToSave: boolean;
editMode: boolean;
newCertificate: boolean;
}
export interface CvUploadQualificationInterface{
name: string;
level: number | null;
levelName: string;
qualification_id: string | null;
readyToSave: boolean;
editMode: boolean;
newQualification: boolean;
}
export interface CvUploadWorkExperienceInterface{
company: string;
esco_id: number;
job_title: string;
start_year: number | string | null;
start_month: number | string | null;
end_year: number | string | null;
end_month: number | string | null;
readyToSave: boolean;
editMode: boolean;
fromDate: Date | null;
toDate: Date | null;
currentlyWorkingHere: boolean;
description: string;
esco_title_da: string;
esco_title_en: string;
}
export interface CvUploadEducationInterface{
education_name: string;
degree: string;
school: string | null;
start_year: number | string | null;
start_month: number | string | null;
end_year: number | string | null;
end_month: number | string | null;
fromDate: Date | null;
toDate: Date | null;
institution_number: number | null;
education_disced_15: number | null;
currentlyStudyingHere: boolean;
readyToSave: boolean;
editMode: boolean;
newInstitution: boolean;
newEducation: boolean;
description: string;
}
// "{""languages"": [{""name"": ""Dansk"", ""language_id"": ""fb095c8c-8b8e-4069-9a95-c7594a1fb580""}, {""name"": ""Engelsk"", ""language_id"": ""0bbae99f-0737-466c-84be-c916a9720680""}], " +
// """educations"": [{""degree"": ""Funktionsprogrammering (ECTS kursus)"", ""school"": null, ""end_year"": 2005, ""end_month"": null, ""start_year"": 2005, ""start_month"": null, ""institution_number"": null}, {""degree"": ""Datamatiker"", ""school"": ""Erhvervsakademiet Nordsjælland i Hillerød"", ""end_year"": 2004, ""end_month"": null, ""start_year"": 2004, ""start_month"": null, ""institution_number"": 10001743}, {""degree"": ""HHX"", ""school"": ""Hillerød Handelsskole"", ""end_year"": 2001, ""end_month"": null, ""start_year"": 2001, ""start_month"": null, ""institution_number"": 10002593}, {""degree"": ""10. Klasse folkeskole"", ""school"": ""Engholmsskolen i Allerød"", ""end_year"": 1998, ""end_month"": null, ""start_year"": 1998, ""start_month"": null, ""institution_number"": 201005}, {""degree"": ""9. Klasse folkeskole"", ""school"": ""Lillerød Skole i Allerød"", ""end_year"": 1997, ""end_month"": null, ""start_year"": 1997, ""start_month"": null, ""institution_number"": 217010}], " +
// """certificates"": [{""name"": ""Java Standard Edition 5 Programmer Certified"", ""certificate_id"": ""None""}], ""profile_text"": ""Selvstændig full stack udvikler som altid leverer en høj kvalitet. Jeg går meget højt op i god kodestruktur, følger best practice, undgår teknisk gæld, samt har fokus på unit tests. Disse punkter gør også, at det altid vil være nemt for andre at læse min kode efterfølgende."", " +
// """qualifications"": [{""name"": ""Java"", ""qualification_id"": ""0dfc0252-dac3-4ae5-be71-36e81e9393bb""}, {""name"": ""Spring"", ""qualification_id"": ""333dca70-4d75-4290-b2a2-5df01e626efa""}, {""name"": ""Hibernate"", ""qualification_id"": ""884ea5dd-3187-40e9-9b2c-f5322db3dbd7""}, {""name"": ""Play"", ""qualification_id"": ""95439c7f-d0bf-44e0-a897-4958933e1f1a""}, {""name"": ""Maven"", ""qualification_id"": ""145f4b1c-7f1b-4fd9-9ff4-4ebcdb155c7a""}, {""name"": ""Python"", ""qualification_id"": ""9d59b296-1696-4d5e-b1f5-19d4518fd2a5""}, {""name"": ""Flask"", ""qualification_id"": ""4f564926-cd9f-437e-982f-5a65d7fd7cad""}, {""name"": ""Ming"", ""qualification_id"": ""631ca0a5-58e5-43b4-acc0-eb25b572cf5a""}, {""name"": ""Redis to go"", ""qualification_id"": ""fd798ee2-3672-4886-b155-7735bced3b0d""}, {""name"": ""C#"", ""qualification_id"": ""6f3e1d75-01cf-4894-81ce-211226d8352a""}, {""name"": "".Net Core"", ""qualification_id"": ""6246b651-2fe3-4890-84c7-ef5d26a99451""}, {""name"": "".Net Framework"", ""qualification_id"": ""0f1a42fa-e238-470d-9b60-38046ce7f0bd""}, {""name"": ""Angular"", ""qualification_id"": ""1a244aa6-f556-455a-bbcf-7c23cb392bcf""}, {""name"": ""AngularJS"", ""qualification_id"": ""8f4b2542-5553-4517-94a2-a6f1794f54ec""}, {""name"": ""Typescript"", ""qualification_id"": ""502c581b-3ec2-49eb-a255-b9636f349492""}, {""name"": ""React"", ""qualification_id"": ""b6210d44-d637-435d-a518-8dd8c39c221d""}, {""name"": ""NodeJS"", ""qualification_id"": ""d65b3584-cbd3-4a45-bca6-661d3194a8d7""}, {""name"": ""ColdFusion"", ""qualification_id"": ""884ea5dd-3187-40e9-9b2c-f5322db3dbd7""}, {""name"": ""HTML"", ""qualification_id"": ""f14d0e57-ea7d-4531-86a9-c264fe5b3d70""}, {""name"": ""CSS"", ""qualification_id"": ""8f269b6f-1d43-480e-940b-fc217afc2612""}, {""name"": ""SASS"", ""qualification_id"": ""9c4ed5c0-043d-47d5-89e1-3da2b938fca9""}, {""name"": ""MongoDB"", ""qualification_id"": ""f2104081-690b-45cc-9c46-4054c5b08152""}, {""name"": ""PostgreSQL"", ""qualification_id"": ""7a4a74e3-f031-4168-b921-5c0845f2b8d9""}, {""name"": ""MsSQL"", ""qualification_id"": ""ee9ad56a-76a7-41c4-ac6f-82b9947225a9""}, {""name"": ""MySQL"", ""qualification_id"": ""fc6a0e0f-103c-46d1-adbf-178b88fa0705""}, {""name"": ""REST API"", ""qualification_id"": ""120447e4-11e9-4acb-9816-562f12e7345a""}, {""name"": ""JSON"", ""qualification_id"": ""f2ed110b-fc85-446f-95ae-dd20152f3548""}, {""name"": ""XML"", ""qualification_id"": ""81beb6d0-7907-4514-8f53-92d2e6ba0f3f""}, {""name"": ""Heroku"", ""qualification_id"": ""95439c7f-d0bf-44e0-a897-4958933e1f1a""}, {""name"": ""Amazon Web Services"", ""qualification_id"": ""2f6b0d66-4bcb-4b75-b35f-ac4604a6427f""}], " +
// """driver_licenses"": [], " +
// """work_experience"": [{" +
// """company"": ""Comby"", ""esco_id"": ""292"", ""end_year"": 2022, ""end_month"": null, ""job_title"": ""Konsulent Software udvikler"", ""start_year"": 2022, ""start_month"": null}, {""company"": ""Nemlig.com"", ""esco_id"": ""292"", ""end_year"": 2022, ""end_month"": null, ""job_title"": ""Konsulent Software udvikler"", ""start_year"": 2022, ""start_month"": null}, {""company"": ""AncoTrans"", ""esco_id"": ""292"", ""end_year"": 2022, ""end_month"": null, ""job_title"": ""Konsulent Software udvikler"", ""start_year"": 2021, ""start_month"": null}, {""company"": ""Comby"", ""esco_id"": ""292"", ""end_year"": 2021, ""end_month"": null, ""job_title"": ""Konsulent Software udvikler"", ""start_year"": 2021, ""start_month"": null}, {""company"": ""ALD"", ""esco_id"": ""292"", ""end_year"": 2020, ""end_month"": null, ""job_title"": ""Konsulent - Software udvikler"", ""start_year"": 2020, ""start_month"": null}, {""company"": ""Simplyture"", ""esco_id"": ""1383"", ""end_year"": 2020, ""end_month"": null, ""job_title"": ""Software udvikler"", ""start_year"": 2019, ""start_month"": null}, {""company"": ""Parkicar"", ""esco_id"": ""1383"", ""end_year"": ""Present"", ""end_month"": ""Present"", ""job_title"": ""Software udvikler"", ""start_year"": 2019, ""start_month"": null}, {""company"": ""This Is Universe"", ""esco_id"": ""1383"", ""end_year"": 2019, ""end_month"": null, ""job_title"": ""Software udvikler"", ""start_year"": 2018, ""start_month"": null}, {""company"": ""NS Media"", ""esco_id"": ""875"", ""end_year"": ""Present"", ""end_month"": ""Present"", ""job_title"": ""Konsulent med løs kontrakt Software udvikler"", ""start_year"": 2018, ""start_month"": null}, {""company"": ""NS Media"", ""esco_id"": ""1383"", ""end_year"": 2018, ""end_month"": null, ""job_title"": ""Software udvikler"", ""start_year"": 2014, ""start_month"": null}, {""company"": ""Unwire"", ""esco_id"": ""1383"", ""end_year"": 2013, ""end_month"": null, ""job_title"": ""Software udvikler"", ""start_year"": 2008, ""start_month"": null}, {""company"": ""Info-Connect"", ""esco_id"": ""368"", ""end_year"": 2008, ""end_month"": null, ""job_title"": ""Webudvikler"", ""start_year"": 2006, ""start_month"": null}]}"

View File

@@ -0,0 +1,6 @@
import type {DriverLicenseTypeInterface} from "./driver-license-type.interface";
export interface DriverLicenseGroupInterface{
name: string;
driverLicenses: DriverLicenseTypeInterface[];
}

View File

@@ -0,0 +1,7 @@
export interface DriverLicenseTypeInterface{
code: string;
id: string;
name: string;
priority: number;
special: boolean;
}

View File

@@ -0,0 +1,5 @@
export interface EducationSearchInterface{
name: string;
disced15: number;
levelName: string;
}

View File

@@ -0,0 +1,4 @@
export interface ErrorResponseInterface{
error: string;
error_code: number;
}

View File

@@ -0,0 +1,5 @@
export interface EscoInterface{
id: number;
preferedLabelDa: string;
preferedLabelEu: string;
}

View File

@@ -0,0 +1,29 @@
export interface FilterJobSearchInterface{
workArea: FilterWorkAreaInterface[];
workTime: FilterWorkTimeInterface;
workType: FilterWorkTypeInterface;
workPlacement: number | null;
partTimeHours: number | null;
distanceCenterName: string;
latitude?: number;
longitude?: number;
}
export interface FilterWorkAreaInterface{
}
export interface FilterWorkTimeInterface{
day: boolean;
evening: boolean;
night: boolean;
weekend: boolean;
}
export interface FilterWorkTypeInterface{
permanent: boolean;
partTime: boolean;
temporary: boolean;
substitute: boolean;
freelance: boolean;
}

View File

@@ -0,0 +1,10 @@
export interface GeneratedJobApplication {
id: number;
language: string;
userInput: string;
jobApplication: string;
processing: boolean;
done: boolean;
rating?: number;
ratingText?: string;
}

52
src/mvvm/models/index.ts Normal file
View File

@@ -0,0 +1,52 @@
export * from './PaymentIntentResponse';
export * from './ai-generated-cv-description.interface';
export * from './all-language.interface';
export * from './application-examination.interface';
export * from './applied-job.interface';
export * from './auth.interface';
export * from './candidate-application.interface';
export * from './candidate-retention-response.interface';
export * from './candidate-subscription-gift.interface';
export * from './candidate.interface';
export * from './chat-message-thread.interface';
export * from './chat-message.interface';
export * from './cv-language.interface';
export * from './cv-suggestion.interface';
export * from './cv-upload-data.interface';
export * from './driver-license-group.interface';
export * from './driver-license-type.interface';
export * from './education-search.interface';
export * from './error-response.interface';
export * from './esco.interface';
export * from './filter-job-search.interface';
export * from './generated-job-application';
export * from './job-agent-filter.interface';
export * from './job-application.interface';
export * from './job-detail.interface';
export * from './job-posting-overview.interface';
export * from './job-search-container';
export * from './job-search-filter.interface';
export * from './job.interface';
export * from './jobnet-job-detail.interface';
export * from './level.interface';
export * from './login.interface';
export * from './notification-setting.interface';
export * from './notification.interface';
export * from './occupation-categorization.interface';
export * from './occupation.interface';
export * from './payment-overview.interface';
export * from './predefined-user-input.interface';
export * from './qualification-search.interface';
export * from './saved-job.interface';
export * from './school.interface';
export * from './search-job.interface';
export * from './searched-certification.interface';
export * from './select-language.interface';
export * from './simulation-personality.interface';
export * from './subscription-product.interface';
export * from './translation.interface';
export * from './zip-v2.interface';
export * from './zip.interface';
export * from './post/save-candidate.interface';
export * from './post/save-education.interface';
export * from './post/upload-cv.interface';

View File

@@ -0,0 +1,8 @@
export interface JobAgentFilterInterface{
id: number;
escoId: number;
escoName: string;
isCalculated: boolean;
visible: boolean;
discoAms08: number;
}

View File

@@ -0,0 +1,12 @@
import type {JobPostingOverviewInterface} from "./job-posting-overview.interface";
export interface JobApplicationInterface{
id: string;
jobPostingId: string;
candidateId: string;
applicationDate: Date;
motivatedTitle: string;
motivatedBody: string;
motivatedAttachment: string;
jobPosting: JobPostingOverviewInterface | undefined | null;
}

View File

@@ -0,0 +1,65 @@
export interface JobDetailInterface{
id: string;
position: string;
title: string;
descriptionIntro: string;
jobTypes: number[];
workTimes: number[];
companyName: string;
descriptionCompany: string;
descriptionPosition: string;
website: string;
descriptionCandidate: string;
experiences: JobDetailExperienceInterface[];
educations: JobDetailEducationInterface[];
skills: JobDetailSkillInterface[];
certificates: JobDetailCertificateInterface[];
languages: JobDetailLanguageInterface[];
driversLicenses: JobDetailDriversLicenseInterface[];
salary: number;
salaryType: number;
descriptionOffer: string;
fromDate: Date;
toDate: Date;
fromTime: string;
toTime: string;
applicationDeadline: Date;
interviewsFrom: Date;
interviewsTo: Date;
numberOfPositions: number;
appliedJob?: boolean;
appliedJobId?: string;
}
export interface JobDetailExperienceInterface{
id: string;
occupationId: string;
occupationName: string;
numberOfYears: number;
latest: object;
}
export interface JobDetailEducationInterface{
name: string;
ended: string | number;
}
export interface JobDetailSkillInterface{
name: string;
requiredLevel: number | string;
}
export interface JobDetailCertificateInterface{
name: string;
requiredLevel: string | number;
}
export interface JobDetailLanguageInterface{
name: string;
requiredLevel: number | string;
}
export interface JobDetailDriversLicenseInterface{
name: string;
requiredExperience: string | number;
}

View File

@@ -0,0 +1,25 @@
import type {JobApplicationInterface} from "./job-application.interface";
export interface JobPostingOverviewInterface{
saved: boolean;
applied: boolean;
display: boolean;
id: string;
title: string;
jobTypes: Array<number>;
workTimes: Array<number>;
descriptionIntro: string;
application: JobApplicationInterface;
applicationDeadline: Date;
occupationName: string;
companyName: string;
companyLogoImage: string;
candidateDistance: number;
postal: string;
fromJobnet: boolean;
stage: number;
address: string;
period: string;
salary: number;
time: string;
}

View File

@@ -0,0 +1,9 @@
import type {JobInterface} from "./job.interface";
export interface JobSearchContainer{
level: number;
nextOffset: number;
nextLevel: number;
numberOfEscos: number;
searchList: JobInterface[];
}

View File

@@ -0,0 +1,22 @@
export interface JobSearchFilterInterface{
candidateSearchFilter: {
workTimeDay: boolean,
workTimeEvening: boolean,
workTimeNight: boolean,
workTimeWeekend: boolean,
workTypePermanent: boolean,
workTypeFreelance: boolean,
workTypePartTime: boolean,
workTypeSubstitute: boolean,
workTypeTemporary: boolean,
workDistance: number | null,
partTimeHours: number | null,
terms?: string[],
distanceCenterName?: string,
longitude?: number | null,
latitude?: number | null
defaultCenterName?: string;
defaultDistance?: number;
};
escoIds: number[];
}

View File

@@ -0,0 +1,39 @@
export interface JobInterface {
certificateScore: number;
distanceScore: number;
driversLicenseScore: number;
educationScore: number;
experienceCategoryScore: number;
experienceScore: number;
fromJobnet: boolean;
id: string;
jobPosting: JobPostingInterface;
languageScore: number;
matchMedal: number;
skillScore: number
total: number;
}
export interface JobPostingInterface{
address: string;
applicationDeadline: Date;
candidateDistance: number;
companyLogoImage: string;
companyName: string;
descriptionIntro: string;
fromJobnet: boolean;
id: string;
jobTypeDescription: string;
jobTypes: number[];
occupationName: string;
paid: boolean;
period: string;
postal: string;
salary: number;
stage: number;
time: string;
title: string;
saved: boolean;
workTimes: number[];
logoUrl: string;
}

View File

@@ -0,0 +1,65 @@
import type {OccupationInterface} from "./occupation.interface";
export interface JobnetJobDetailInterface {
workTimes: number[];
jobTypes: number[];
occupation: OccupationInterface;
// unknown type
driversLicenses: any[];
id: string;
jobnetId: number;
title: string;
description: string;
datePosted: Date;
lastModifiedDate: Date;
numberOfPositions: number;
applicationDeadline: Date;
fromDate: Date;
toDate: Date;
// unknown type
fromTime: string;
// unknown type
toTime: string;
workTimesDefined: boolean;
employmentDate: Date;
startAsSoonAsPossible: boolean;
useOwnCar: boolean;
latitude: number;
longitude: number;
awsUrl: string;
awsId: string;
houseNum: string;
zip: string;
zipName: string;
// unknown type
road: string;
additionalCityName: string;
countryCode: string;
applicationProcessDescription: string;
applicationUrl: string;
applicationPhoneNumber: string;
applicationEmail: string;
hidePhoneNumbers: boolean;
isAnonymousEmployer: boolean;
hiringCompanyCvr: string;
hiringCompanyName: string;
hiringCompanyContact: string;
hiringCompanyEmail: string;
hiringCompanyPrimaryPhone: string;
hiringCompanySecondaryPhone: string;
hiringCompanyUrl: string;
contactName: string;
contactPrimaryPhone: string;
contactSecondaryPhone: string;
contactTitle: string;
contactEmail: string;
isFullTime: boolean;
occupationId: string;
status: number;
partTimeMinHours: number;
partTimeMaxHours: number;
applied: boolean;
logoUrl: string;
}

View File

@@ -0,0 +1,4 @@
export interface LevelInterface{
id: number;
text: string;
}

View File

@@ -0,0 +1,3 @@
export interface LoginInterface{
}

View File

@@ -0,0 +1,24 @@
export interface NotificationSettingInterface{
id?: number | null;
jobAgentName: string | null;
workTimeDay: boolean;
workTimeEvening: boolean;
workTimeNight: boolean;
workTimeWeekend: boolean;
workTypePermanent: boolean;
workTypeFreelance: boolean;
workTypePartTime: boolean;
workTypeSubstitute: boolean;
workTypeTemporary: boolean;
workDistance: number | null;
distanceCenterName?: string | null;
latitude?: number | null;
longitude?: number | null;
partTimeHours: number | null;
notifyOnPush: boolean;
notifyOnSms: boolean;
searchText: string | null;
escoIds: number[];
}

View File

@@ -0,0 +1,16 @@
export interface NotificationInterface{
id: number;
jobnetPostingId: string;
jobPostingId: string;
notificationDate: Date;
seenByUser: boolean;
escoTitle: string;
companyName: string;
jobTitle: string;
zip: string;
city: string;
distance: number;
jobIsActive: boolean;
saved: boolean;
logoUrl: string;
}

View File

@@ -0,0 +1,19 @@
import type {OccupationInterface} from "./occupation.interface";
export interface OccupationCategorizationInterface {
areaCode: number;
areaName: string;
subAreas: SubAreaInterface[];
expanded: boolean;
activated: boolean;
someIsActive: boolean;
}
export interface SubAreaInterface{
subAreaCode: number;
subAreaName: string;
occupations: OccupationInterface[];
expanded: boolean;
activated: boolean;
someIsActive: boolean;
}

View File

@@ -0,0 +1,23 @@
export interface OccupationInterface{
id: number;
name: string;
discoAms08: number;
activated?:boolean;
occupationCategorizationCode?: number;
subAreaCode?: number;
aliasId?: number;
// unknown
candidateExperiences?: object;
// unknown
category?: object;
// unknown
dateAdded?: object;
// unknown
jobExperiences?: object;
primary?: boolean;
reviewed?: boolean;
userContent?: boolean;
}

View File

@@ -0,0 +1,12 @@
export interface PaymentOverview {
generateApplication: boolean;
careerAgent: boolean;
downloadCv: boolean;
jobInterviewSimulation: boolean;
productType: string;
renewDate: Date;
activeToDate: Date;
productTypeName: string;
isUsingGift?: boolean;
paymentType?: string;
}

View File

@@ -0,0 +1,12 @@
export interface SaveCandidateInterface{
email: string;
password: string;
zip: number;
zipName: string;
awsUrl: string;
latitude: number;
longitude: number;
firstName: string;
lastName: string;
subscribe: boolean;
}

View File

@@ -0,0 +1,11 @@
import type {EducationSearchInterface} from "../education-search.interface";
import type {SchoolInterface} from "../school.interface";
export interface SaveEducationInterface{
comments: string;
education: EducationSearchInterface;
institution?: SchoolInterface;
fromDate: Date;
toDate: Date;
isCurrent: boolean;
}

View File

@@ -0,0 +1,4 @@
export interface UploadCvInterface{
base_64_cv_file: string;
cv_file_type: string;
}

View File

@@ -0,0 +1,4 @@
export interface PredefinedUserInputInterface{
id: string;
predefinedUserInput: string;
}

View File

@@ -0,0 +1,5 @@
export interface QualificationSearchInterface{
id: string;
name: string;
type: number;
}

View File

@@ -0,0 +1,27 @@
export interface SavedJobInterface{
address: string;
applicationDeadline: Date;
candidateDistance: number;
companyLogoImage: string;
companyName: string;
descriptionIntro: string;
fromJobnet: boolean;
id: string;
jobTypeDescription: string;
jobTypes: number[];
occupationName: string;
paid: boolean;
period: string;
string: string;
salary: number;
stage: number;
time: string;
title: string;
// unknown type
workTimes: string;
logoUrl: string;
isActive: boolean;
applyJobBefore: Date;
}

View File

@@ -0,0 +1,4 @@
export interface SchoolInterface{
instNumber: number;
name: string;
}

View File

@@ -0,0 +1,9 @@
import type { OccupationInterface } from "./occupation.interface";
export interface SearchJobInterface{
maxDistance?: number | undefined | null;
occupations: OccupationInterface[];
workTimes: number[];
workTypes: number[];
term?: string;
}

View File

@@ -0,0 +1,5 @@
export interface SearchedCertificationInterface{
id: string;
name: number;
type: string;
}

View File

@@ -0,0 +1,4 @@
export interface SelectLanguageInterface{
name: string;
shortName: string;
}

View File

@@ -0,0 +1,4 @@
export interface SimulationPersonalityInterface {
id: number;
name: string;
}

View File

@@ -0,0 +1,14 @@
export interface SubscriptionProductInterface{
premium_30: ProductInterface;
premium_90: ProductInterface;
premium_365?: ProductInterface;
premium_plus_30: ProductInterface;
premium_plus_90: ProductInterface;
premium_plus_365?: ProductInterface;
}
export interface ProductInterface{
productId: number;
price: number;
extraCode?: boolean;
}

View File

@@ -0,0 +1,4 @@
export interface TranslationInterface {
shortCode: string;
text: string;
}

View File

@@ -0,0 +1,7 @@
export interface ZipV2Interface{
latitude: number;
longitude: number;
cityName: string;
zipCode: number;
url: string;
}

View File

@@ -0,0 +1,8 @@
export interface ZipInterface{
href: string;
nr: string;
navn: string;
visueltcenter: number[];
}

View File

@@ -0,0 +1,85 @@
import { HttpClient, httpClient } from '../core/http-client';
import {environment} from "../../environments/environment";
import type {GeneratedJobApplication} from "../models/generated-job-application";
import type {PredefinedUserInputInterface} from "../models/predefined-user-input.interface";
import type {AiGeneratedCVDescription} from "../models/ai-generated-cv-description.interface";
import type {ApplicationExamination} from "../models/application-examination.interface";
export class AiHandlerService {
constructor(private http: HttpClient = httpClient) {}
generateApplication(isStar: boolean, jobId: string, language: string, userInput: string): Promise<{ai_generated_job_application_id: number}>{
let url = environment.backendApiV2 + '1.0.0/client/ai_handler/create_my_application';
let payload = {
isStar: isStar,
jobId: jobId,
language: language,
userInput: userInput
};
return this.http.post<{ai_generated_job_application_id: number}>(url, payload);
}
listGeneratedJobApplications(jobId: string): Promise<GeneratedJobApplication[]>{
let url = environment.backendApiV2 + '1.0.0/client/ai_handler/list_created_job_application/' + jobId;
return this.http.get<GeneratedJobApplication[]>(url);
}
listPredefinedUserInput(): Promise<PredefinedUserInputInterface[]>{
let url = environment.backendApiV2 + '1.0.0/client/ai_handler/list_predefined_user_input';
return this.http.get<PredefinedUserInputInterface[]>(url);
}
getJobApplication(aiGeneratedJobApplicationId: number): Promise<GeneratedJobApplication>{
let url = environment.backendApiV2 + '1.0.0/client/ai_handler/' + aiGeneratedJobApplicationId;
return this.http.get<GeneratedJobApplication>(url);
}
updateMyCvDescriptions(language: string): Promise<{ai_generated_updated_cv_description_id: number}>{
let url = environment.backendApiV2 + '1.0.0/client/ai_handler/update_my_cv_descriptions';
let payload= {
language: language
};
return this.http.post<{ai_generated_updated_cv_description_id: number}>(url, payload);
}
updateStatesOnMyCvDescription(aiGeneratedUpdatedCvDescriptionId: number, state: number, subState: number): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/ai_handler/update_states_on_my_cv_descriptions/' + aiGeneratedUpdatedCvDescriptionId;
let payload = {
state: state,
subState: subState
}
return this.http.put(url, payload);
}
getMyCvDescriptions(): Promise<AiGeneratedCVDescription>{
let url = environment.backendApiV2 + '1.0.0/client/ai_handler/get_my_cv_descriptions';
return this.http.get<AiGeneratedCVDescription>(url);
}
submitJobApplicationRating(applicationId: number, rating: number, comment: string): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/ai_handler/job_application_rating';
let payload = {
ai_generated_job_application_id: applicationId,
rating: rating,
rating_text: comment
};
return this.http.post(url, payload);
}
startApplicationExamination(aiGeneratedJobApplicationId: number): Promise<{ai_generated_job_application_examination_id: string}>{
const url = environment.backendApiV2 + '1.0.0/client/ai_handler/create_application_examination';
const payload = { ai_generated_job_application_id: aiGeneratedJobApplicationId };
return this.http.post<{ai_generated_job_application_examination_id: string}>(url, payload);
}
getApplicationExamination(examinationId: string): Promise<ApplicationExamination>{
const url = environment.backendApiV2 + '1.0.0/client/ai_handler/application_examination/' + encodeURIComponent(examinationId);
return this.http.get<ApplicationExamination>(url);
}
getApplicationExaminationByJobApplicationId(aiGeneratedJobApplicationId: number): Promise<ApplicationExamination>{
const url = environment.backendApiV2 + '1.0.0/client/ai_handler/application_examination_by_job_application/' + aiGeneratedJobApplicationId;
return this.http.get<ApplicationExamination>(url);
}
}

View File

@@ -0,0 +1,476 @@
import { Observable, Subject } from '../core/async-state';
/**
* Service for handling audio recording and encoding.
*
* Handles:
* - Microphone access and recording (Web and Android)
* - Audio encoding to PCM 16-bit 16kHz mono format
* - Streaming audio chunks
*/
export class AudioService {
private mediaRecorder?: MediaRecorder;
private audioContext?: AudioContext;
private mediaStream?: MediaStream;
private audioChunksSubject = new Subject<Blob>();
private isRecording = false;
// Audio level monitoring
private analyserNode?: AnalyserNode;
private audioLevelSubject = new Subject<number>();
public audioLevel$ = this.audioLevelSubject.asObservable();
private audioLevelInterval?: number;
// Audio playback queue
private audioQueue: ArrayBuffer[] = [];
private isProcessingQueue = false;
private isPlaying = false;
private currentSource: AudioBufferSourceNode | null = null;
private scheduledSources: AudioBufferSourceNode[] = [];
// Observable for playback completion
private playbackCompleteSubject = new Subject<void>();
public playbackComplete$ = this.playbackCompleteSubject.asObservable();
/**
* Request microphone access and initialize audio context.
* Works on both Web and Android (via Capacitor).
*/
async requestMicrophoneAccess(): Promise<boolean> {
try {
// Check if we're on native platform
const isNative = false;
const platform = 'web';
console.log(`[AUDIO] Requesting microphone access on platform: ${platform} (native: ${isNative})`);
// Check if getUserMedia is available
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
const errorMsg = isNative
? 'Mikrofon adgang er ikke tilgængelig på denne enhed. Tjek app-indstillinger.'
: 'Mikrofon adgang er ikke tilgængelig i din browser.';
console.error('[AUDIO]', errorMsg);
throw new Error(errorMsg);
}
// Request microphone access
// On Android, this will trigger the system permission dialog if not already granted
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
this.mediaStream = stream;
// Initialize AudioContext for processing
this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
// Set up audio level monitoring
this.setupAudioLevelMonitoring(stream);
console.log('[AUDIO] Microphone access granted successfully');
return true;
} catch (error: any) {
console.error('[AUDIO] Error requesting microphone access:', error);
// Provide user-friendly error messages
const isNative = false;
let errorMessage = 'Kunne ikke få adgang til mikrofon.';
if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
errorMessage = isNative
? 'Mikrofon adgang blev nægtet. Gå til app-indstillinger og giv adgang til mikrofon.'
: 'Mikrofon adgang blev nægtet. Tjek browser-indstillinger.';
} else if (error.name === 'NotFoundError' || error.name === 'DevicesNotFoundError') {
errorMessage = 'Ingen mikrofon fundet på denne enhed.';
} else if (error.name === 'NotReadableError' || error.name === 'TrackStartError') {
errorMessage = 'Mikrofon er optaget af en anden app. Luk andre apps og prøv igen.';
} else if (error.name === 'OverconstrainedError' || error.name === 'ConstraintNotSatisfiedError') {
errorMessage = 'Mikrofon understøtter ikke de krævede indstillinger.';
} else if (error.message) {
errorMessage = error.message;
}
console.error('[AUDIO]', errorMessage);
return false;
}
}
/**
* Set up audio level monitoring using AnalyserNode.
*/
private setupAudioLevelMonitoring(stream: MediaStream): void {
if (!this.audioContext) {
return;
}
try {
this.analyserNode = this.audioContext.createAnalyser();
this.analyserNode.fftSize = 256;
this.analyserNode.smoothingTimeConstant = 0.8;
const source = this.audioContext.createMediaStreamSource(stream);
source.connect(this.analyserNode);
this.startAudioLevelMonitoring();
} catch (error) {
console.error('Error setting up audio level monitoring:', error);
}
}
/**
* Start monitoring audio levels and emit updates.
*/
private startAudioLevelMonitoring(): void {
if (!this.analyserNode) {
return;
}
if (this.audioLevelInterval) {
clearInterval(this.audioLevelInterval);
}
const bufferLength = this.analyserNode.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
this.audioLevelInterval = window.setInterval(() => {
if (!this.analyserNode) {
return;
}
this.analyserNode.getByteFrequencyData(dataArray);
let sum = 0;
for (let i = 0; i < bufferLength; i++) {
sum += dataArray[i];
}
const average = sum / bufferLength;
const normalizedLevel = Math.min(100, (average / 255) * 100);
this.audioLevelSubject.next(normalizedLevel);
}, 100);
}
/**
* Stop audio level monitoring.
*/
private stopAudioLevelMonitoring(): void {
if (this.audioLevelInterval) {
clearInterval(this.audioLevelInterval);
this.audioLevelInterval = undefined;
}
this.audioLevelSubject.next(0);
}
/**
* Start recording audio.
*/
async startRecording(
onChunk: (audioBase64: string) => void,
chunkIntervalMs: number = 100
): Promise<void> {
if (this.isRecording) {
throw new Error('Already recording');
}
if (!this.mediaStream) {
const granted = await this.requestMicrophoneAccess();
if (!granted) {
throw new Error('Microphone access denied');
}
}
try {
const options: MediaRecorderOptions = {
mimeType: 'audio/webm;codecs=opus',
};
if (!MediaRecorder.isTypeSupported(options.mimeType!)) {
options.mimeType = 'audio/webm';
}
this.mediaRecorder = new MediaRecorder(this.mediaStream!, options);
this.isRecording = true;
this.mediaRecorder.ondataavailable = async (event: BlobEvent) => {
if (event.data && event.data.size > 0) {
try {
const pcmData = await this.convertToPCM(event.data);
const base64 = await this.blobToBase64(pcmData);
onChunk(base64);
} catch (error) {
console.error('Error processing audio chunk:', error);
}
}
};
this.mediaRecorder.start(chunkIntervalMs);
} catch (error) {
this.isRecording = false;
console.error('Error starting recording:', error);
throw error;
}
}
/**
* Stop recording.
*/
stopRecording(): void {
if (this.mediaRecorder && this.isRecording) {
this.mediaRecorder.stop();
this.isRecording = false;
}
}
/**
* Stop and cleanup all resources.
*/
cleanup(): void {
this.stopRecording();
this.stopAudioLevelMonitoring();
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop());
this.mediaStream = undefined;
}
if (this.audioContext) {
this.audioContext.close();
this.audioContext = undefined;
}
}
/**
* Convert audio blob to PCM 16-bit 16kHz mono format.
*/
private async convertToPCM(audioBlob: Blob): Promise<Blob> {
if (!this.audioContext) {
throw new Error('AudioContext not initialized');
}
try {
const arrayBuffer = await audioBlob.arrayBuffer();
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
const targetSampleRate = 16000;
const sourceSampleRate = audioBuffer.sampleRate;
const ratio = sourceSampleRate / targetSampleRate;
const length = Math.floor(audioBuffer.length / ratio);
const leftChannel = audioBuffer.getChannelData(0);
const rightChannel = audioBuffer.numberOfChannels > 1
? audioBuffer.getChannelData(1)
: leftChannel;
const monoData = new Float32Array(length);
for (let i = 0; i < length; i++) {
const sourceIndex = Math.floor(i * ratio);
monoData[i] = (leftChannel[sourceIndex] + rightChannel[sourceIndex]) / 2;
}
const pcmData = new Int16Array(length);
for (let i = 0; i < length; i++) {
const sample = Math.max(-1, Math.min(1, monoData[i]));
pcmData[i] = sample < 0
? Math.round(sample * 32768)
: Math.round(sample * 32767);
}
return new Blob([pcmData.buffer], { type: 'audio/pcm' });
} catch (error) {
console.error('Error converting to PCM:', error);
return audioBlob;
}
}
/**
* Convert blob to base64 string.
*/
private async blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const base64 = (reader.result as string).split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
/**
* Play audio from base64 PCM data.
*/
async playAudio(audioBase64: string): Promise<void> {
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
}
if (this.audioContext.state === 'suspended') {
await this.audioContext.resume();
}
try {
const binaryString = atob(audioBase64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
this.audioQueue.push(bytes.buffer);
if (!this.isProcessingQueue) {
this.processAudioQueue();
}
} catch (error) {
console.error('Error queuing audio:', error);
throw error;
}
}
/**
* Process audio queue continuously.
*/
private async processAudioQueue(): Promise<void> {
if (this.isProcessingQueue) {
return;
}
this.isProcessingQueue = true;
this.isPlaying = true;
let nextStartTime = this.audioContext!.currentTime;
let noChunksTimeout: any = null;
while (true) {
while (this.audioQueue.length > 0) {
const audioData = this.audioQueue.shift();
if (!audioData || audioData.byteLength === 0) {
continue;
}
if (noChunksTimeout) {
clearTimeout(noChunksTimeout);
noChunksTimeout = null;
}
try {
let audioBuffer: AudioBuffer;
try {
audioBuffer = await this.audioContext!.decodeAudioData(audioData.slice(0));
} catch (decodeError) {
const int16Array = new Int16Array(audioData);
const float32Array = new Float32Array(int16Array.length);
for (let i = 0; i < int16Array.length; i++) {
const normalized = int16Array[i] / 32768.0;
float32Array[i] = Math.max(-1, Math.min(1, normalized));
}
const sampleRate = 16000;
audioBuffer = this.audioContext!.createBuffer(1, float32Array.length, sampleRate);
audioBuffer.copyToChannel(float32Array, 0);
}
const startTime = Math.max(nextStartTime, this.audioContext!.currentTime);
const source = this.audioContext!.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.audioContext!.destination);
this.currentSource = source;
this.scheduledSources.push(source);
const duration = audioBuffer.duration;
nextStartTime = startTime + duration;
source.onended = () => {
const index = this.scheduledSources.indexOf(source);
if (index > -1) {
this.scheduledSources.splice(index, 1);
}
};
source.start(startTime);
source.addEventListener('error', (error) => {
console.error('Audio playback error:', error);
const index = this.scheduledSources.indexOf(source);
if (index > -1) {
this.scheduledSources.splice(index, 1);
}
});
} catch (error) {
console.error('Error playing audio chunk:', error);
}
}
if (!noChunksTimeout && this.audioQueue.length === 0) {
noChunksTimeout = setTimeout(() => {
const checkCompletion = async () => {
const maxWaitTime = 30000;
let waitCount = 0;
while (this.scheduledSources.length > 0 || this.audioQueue.length > 0) {
if (this.audioQueue.length > 0) {
noChunksTimeout = null;
return;
}
if (Date.now() - Date.now() > maxWaitTime) {
break;
}
await new Promise(resolve => setTimeout(resolve, 50));
waitCount++;
}
if (this.audioQueue.length === 0 && this.scheduledSources.length === 0) {
await new Promise(resolve => setTimeout(resolve, 200));
if (this.audioQueue.length === 0) {
this.isPlaying = false;
this.isProcessingQueue = false;
this.currentSource = null;
this.scheduledSources = [];
this.playbackCompleteSubject.next();
} else {
noChunksTimeout = null;
}
} else {
noChunksTimeout = null;
}
};
checkCompletion();
}, 5000);
}
await new Promise(resolve => setTimeout(resolve, 10));
if (this.audioQueue.length === 0 && this.scheduledSources.length === 0 && !this.isProcessingQueue) {
break;
}
}
if (noChunksTimeout) {
clearTimeout(noChunksTimeout);
}
}
getRecordingState(): boolean {
return this.isRecording;
}
getMediaStream(): MediaStream | undefined {
return this.mediaStream;
}
}

View File

@@ -0,0 +1,26 @@
import { HttpClient, httpClient } from '../core/http-client';
import type { LoginInterface } from "../models/login.interface";
import {environment} from "../../environments/environment";
export class AuthService{
private http: HttpClient
constructor(http: HttpClient = httpClient) {
this.http = http
}
login(username: string, password: string): Promise<LoginInterface>{
let url = environment.backendApi + 'api/1.1.0/candidate/login';
let payload = {
email: username,
password: password
};
return this.http.post(url, payload);
}
forgotPassword(username: string): Promise<any>{
let url = environment.backendApi + 'api/1.1.0/candidate/resetPassword/'+ username.toLowerCase();
return this.http.get(url);
}
}

View File

@@ -0,0 +1,11 @@
import { HttpClient, httpClient } from '../core/http-client';
import {environment} from "../../environments/environment";
export class CandidateEducationService {
constructor(private http: HttpClient = httpClient) {}
updateEducationDescription(experienceId: string, payload: {language: string, text: string}[]){
let url = environment.backendApiV2 + '1.0.0/client/candidate_education/update_education_description/' + experienceId;
return this.http.put(url, payload);
}
}

View File

@@ -0,0 +1,11 @@
import { HttpClient, httpClient } from '../core/http-client';
import {environment} from "../../environments/environment";
export class CandidateExperienceService {
constructor(private http: HttpClient = httpClient) {}
updateExperienceDescription(experienceId: string, payload: {language: string, text: string}[]){
let url = environment.backendApiV2 + '1.0.0/client/candidate_experience/update_experience_description/' + experienceId;
return this.http.put(url, payload);
}
}

View File

@@ -0,0 +1,23 @@
import {environment} from "../../environments/environment";
import { HttpClient, HttpParams, httpClient } from '../core/http-client';
import type {JobSearchFilterInterface} from "../models/job-search-filter.interface";
export class CandidateSearchFilterService {
constructor(private http: HttpClient = httpClient) {}
saveJobFilter(payload: JobSearchFilterInterface): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/candidate_search_filter';
return this.http.post(url, payload);
}
getJobFilter(): Promise<JobSearchFilterInterface>{
let url = environment.backendApiV2 + '1.0.0/client/candidate_search_filter';
return this.http.get<JobSearchFilterInterface>(url);
}
resetJobFilter(): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/candidate_search_filter/reset_job_filter';
return this.http.put(url, {})
}
}

View File

@@ -0,0 +1,20 @@
import { HttpClient, httpClient } from '../core/http-client';
import { environment } from '../../environments/environment';
import { CandidateSubscriptionGiftInterface } from '../models/candidate-subscription-gift.interface';
export class CandidateSubscriptionGiftService {
constructor(private http: HttpClient = httpClient) {}
listSubscriptionGift(): Promise<CandidateSubscriptionGiftInterface[]> {
let url = environment.backendApiV2 + 'client/candidate_subscription_gift/v1';
return this.http.get<CandidateSubscriptionGiftInterface[]>(url);
}
activateSubscriptionGift(giftId: number): Promise<any> {
let url = environment.backendApiV2 + 'client/candidate_subscription_gift/v1';
let payload = {
id: giftId
};
return this.http.post(url, payload);
}
}

View File

@@ -0,0 +1,309 @@
import { HttpClient, httpClient } from '../core/http-client';
import {environment} from "../../environments/environment";
import type {CandidateRetentionResponseInterface} from "../models/candidate-retention-response.interface";
import {
CandidateInterface, CertificationInterface, DriversLicenseInterface,
EducationInterface,
ExperienceInterface, LanguageInterface,
LatLngInterface, SkillInterface
} from "../models/candidate.interface";
import type {CvSuggestionInterface} from "../models/cv-suggestion.interface";
import type {JobAgentFilterInterface} from "../models/job-agent-filter.interface";
import type {SaveCandidateInterface} from "../models/post/save-candidate.interface";
import {
LocalStorageService,
localStorageService as localStorageServiceSingleton,
} from "./local-storage.service";
export class CandidateService {
constructor(
private http: HttpClient = httpClient,
private localStorageService: LocalStorageService = localStorageServiceSingleton
) {
}
// OLD API
closeAccount(candidateEmail: string): Promise<any>{
let url = environment.backendApi + 'api/1.1.0/candidate/' + candidateEmail.toLowerCase();
return this.http.delete(url);
}
accountRetention(guid: string): Promise<CandidateRetentionResponseInterface>{
let url = environment.backendApi + 'api/1.1.0/candidate/'+guid+'/account-retention/';
return this.http.get<CandidateRetentionResponseInterface>(url);
}
getCandidatesQualifications(): Promise<SkillInterface[]>{
let url = environment.backendApi + 'api/1.1.0/candidate/skill/';
return this.http.get<SkillInterface[]>(url);
}
getCandidatesCertifications(): Promise<CertificationInterface[]>{
let url = environment.backendApi + 'api/1.1.0/candidate/certificate/';
return this.http.get<CertificationInterface[]>(url);
}
getCandidatesLanguages(): Promise<LanguageInterface[]>{
let url = environment.backendApi + 'api/1.1.0/candidate/language/';
return this.http.get<LanguageInterface[]>(url);
}
getCandidatesDriverLicenses(): Promise<DriversLicenseInterface[]>{
let url = environment.backendApi + 'api/1.1.0/candidate/driversLicense/';
return this.http.get<DriversLicenseInterface[]>(url);
}
changePassword(key: string, password: string): Promise<any>{
let url = environment.backendApi + 'api/1.1.0/candidate/resetPassword';
let payload = {
key: key,
password: password
}
return this.http.post(url, payload);
}
// new API
getCandidatesEducations(): Promise<EducationInterface[]>{
let url = environment.backendApiV2 + '1.0.0/client/candidate_education';
return this.http.get<EducationInterface[]>(url);
}
updateExperience(experience: ExperienceInterface, language: string): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/candidate_experience/' + experience.id;
let payload = {
comments: experience.comments,
company_name: experience.companyName,
is_current: experience.isCurrent,
from_date: experience.fromDate,
to_date: experience.toDate,
language: language
};
return this.http.put(url, payload);
}
createAccountV2(payload: SaveCandidateInterface): Promise<CandidateInterface>{
let url = environment.backendApiV2 + '1.0.0/client/candidate';
return this.http.post<CandidateInterface>(url, payload);
}
// updateCandidate(candidate: CandidateInterface): Promise<CandidateInterface> {
//
// const url: string = environment.backendApiV2 + '1.0.0/client/candidate/' + candidate.id;
//
// // Clone & sanitize candidate object
// let updatedCandidate = { ...candidate };
//
// if (candidate.birthday) {
// candidate.birthday = new Date(candidate.birthday);
// }
//
// if (candidate.birthday instanceof Date) {
// updatedCandidate.birthday = candidate.birthday.toISOString(); // Convert to ISO 8601 format
// }
//
// var token = this.localStorageService.getAuthToken();
// // Build the request
// const options = {
// method: 'PUT', // 👈 HER! Vi specificerer metoden direkte
// url: url,
// headers: {
// 'Content-Type': 'application/json',
// 'Authorization': 'Bearer ' + token
// },
// data: updatedCandidate,
// params: {} // <-- vigtig linje i community pluginet for at undgå null-pointer!
// };
//
// // Return an observable wrapping the Capacitor HTTP call
// return from(
// Http.request(options).then(response => {
// // Hvis dit backend returnerer JSON af typen CandidateInterface
// return response.data as CandidateInterface;
// })
// );
// }
updateCandidate(candidate: CandidateInterface, language: string):Promise<CandidateInterface>{
// let url = environment.backendApi + 'api/1.1.0/candidate/'+candidate.id;
let url: string = environment.backendApiV2 + '1.0.0/client/candidate/v2/'+candidate.id;
// Ensure `birthday` is in ISO format if it's a Date object
let updatedCandidate = { ...candidate };
if(candidate.birthday){
candidate.birthday = new Date(candidate.birthday);
}
if (candidate.birthday instanceof Date) {
updatedCandidate.birthday = candidate.birthday.toISOString(); // Convert to ISO 8601 format
}
let payload = {
language: language,
candidate: updatedCandidate
}
return this.http.put<CandidateInterface>(url, payload);
}
// updateCandidate(candidate: CandidateInterface):Promise<CandidateInterface>{
// // let url = environment.backendApi + 'api/1.1.0/candidate/'+candidate.id;
// let url: string = environment.backendApiV2 + '1.0.0/client/candidate/'+candidate.id;
//
// // Ensure `birthday` is in ISO format if it's a Date object
// let updatedCandidate = { ...candidate };
//
// if(candidate.birthday){
// candidate.birthday = new Date(candidate.birthday);
// }
//
// if (candidate.birthday instanceof Date) {
// updatedCandidate.birthday = candidate.birthday.toISOString(); // Convert to ISO 8601 format
// }
//
// return this.http.put<CandidateInterface>(url, candidate);
// }
getCandidate(): Promise<CandidateInterface>{
// let url = environment.backendApi + 'api/1.1.0/candidate/'+candidateEmail.toLowerCase() +'/light';
let url = environment.backendApiV2 + '1.0.0/client/candidate';
return this.http.get<CandidateInterface>(url);
}
getCvSuggestion(): Promise<CvSuggestionInterface[]>{
// let url = environment.backendApi + 'api/1.1.0/jobnetrecom/' + candidateId;
let url: string = environment.backendApiV2 + '1.0.0/client/suggestion';
return this.http.get<CvSuggestionInterface[]>(url);
}
saveExperience(experience: ExperienceInterface, language: string): Promise<any>{
//let url = environment.backendApi + 'api/1.1.0/candidate/experience/';
let url = environment.backendApiV2 + '1.0.0/client/candidate_experience';
let payload = {
comments: experience.comments,
company_name: experience.companyName,
is_current: experience.isCurrent,
esco_id: experience.occupation.id,
from_date: experience.fromDate,
to_date: experience.toDate,
language: language
};
return this.http.post(url, payload);
}
removeExperience(experienceId: string): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/experience/' + experienceId;
let url = environment.backendApiV2 + '1.0.0/client/candidate_experience/' + experienceId;
return this.http.delete(url);
}
getCandidatesExperiences(): Promise<ExperienceInterface[]>{
// let url = environment.backendApi + 'api/1.1.0/candidate/experience/';
let url = environment.backendApiV2 + '1.0.0/client/candidate_experience';
return this.http.get<ExperienceInterface[]>(url);
}
getJobAgentFilters(): Promise<JobAgentFilterInterface[]>{
let url = environment.backendApiV2 + '1.0.0/client/job_agent_filter';
return this.http.get<JobAgentFilterInterface[]>(url);
}
updateJobAgentFilter(occupation: JobAgentFilterInterface): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/job_agent_filter/' + occupation.id;
let payload = occupation;
return this.http.put(url, payload);
}
removeJobAgentFilter(jobAgentFilterId: number): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/job_agent_filter/' + jobAgentFilterId;
return this.http.delete(url);
}
updateCandidateJobSeeker(candidate: CandidateInterface): Promise<any>{
let url: string = environment.backendApiV2 + '1.0.0/client/candidate/update_job_seeker/'+candidate.id;
return this.http.put(url, candidate);
}
//
// createAccount(email: string, password: string): Promise<CandidateInterface>{
// let url = environment.backendApi + 'api/1.1.0/candidate/';
// let payload = {
// email: email,
// password: password
// };
// return this.http.post<CandidateInterface>(url, payload);
// }
//
// updateCandidateWithSubscription(candidateInterface: CandidateInterface): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/'+candidateInterface.id+'/withSubscription';
// let payload = candidateInterface;
// return this.http.put(url, payload);
// }
//
// endSignupFlow(): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/updateSignupStage/4';
// let payload = {};
// return this.http.put(url, payload);
// }
//
// updateCandidateAddress(address: LatLngInterface): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/address/';
// let payload = address;
// return this.http.put(url, payload);
// }
//
// updateCandidatePoint(candidateId: string):Promise<any>{
// let url: string = environment.backendApiV2 + '1.0.0/client/candidate/update_candidates_point/'+candidateId;
// return this.http.put<any>(url, {});
// }
updateCvUploadProfileText(candidate_id: string, description: string, language: string): Promise<any>{
let url: string = environment.backendApiV2 + '1.0.0/client/candidate/update_cv_upload_profile/' + candidate_id;
let payload = {
description: description,
language: language
};
return this.http.put<any>(url, payload);
}
saveExperienceV2(experience: {
fromDate: Date | null;
isCurrent: boolean;
comments: string;
companyName: string;
toDate: Date | null;
escoId: number
}, language: string): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/candidate_experience';
let payload = {
comments: experience.comments,
company_name: experience.companyName,
is_current: experience.isCurrent,
esco_id: experience.escoId,
from_date: experience.fromDate,
to_date: experience.toDate,
language: language
};
return this.http.post(url, payload);
}
updateProfileText(profileTexts: {language: string, text: string}[]): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/candidate/update_profile_text';
return this.http.put(url, profileTexts);
}
}
export const candidateService = new CandidateService();

View File

@@ -0,0 +1,53 @@
import { HttpClient, httpClient } from '../core/http-client';
import {environment} from "../../environments/environment";
import type {CertificationInterface} from "../models/candidate.interface";
import type {SearchedCertificationInterface} from "../models/searched-certification.interface";
export class CertificationService {
constructor(private http: HttpClient = httpClient) {}
searchForCertification(searchText: string): Promise<SearchedCertificationInterface[]>{
let encodedSearchText = encodeURIComponent(searchText);
let url = environment.backendApiV2 + `1.0.0/client/certificate?search_text=${encodedSearchText}`;
return this.http.get<SearchedCertificationInterface[]>(url);
}
saveCertification(certification_id: string): Promise<any>{
/*
let url = environment.backendApi + 'api/1.1.0/candidate/certificate/';
// TODO at some point change qualification in the backend to certification
let payload = {
'level': level,
'qualification': certification
}
return this.http.post(url, payload);
*/
let url = environment.backendApiV2 + '1.0.0/client/candidate_certificate';
let payload = {
'qualification_id': certification_id
};
return this.http.post(url, payload);
}
updateCertification(certification: CertificationInterface): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/certificate/' + certification.id;
let url = environment.backendApiV2 + '1.0.0/client/candidate_certificate/' + certification.id;
let payload = certification;
return this.http.put(url, payload);
}
removeCertification(certificationId: string): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/certificate/' + certificationId;
let url = environment.backendApiV2 + '1.0.0/client/candidate_certificate/' + certificationId;
return this.http.delete(url);
}
addUnknownCertificate(certificateName: string): Promise<{'certificate_id': string}>{
let url = environment.backendApiV2 + '1.0.0/client/certificate';
let payload = {
'certificate_name': certificateName
};
return this.http.post<{'certificate_id': string}>(url, payload);
}
}

View File

@@ -0,0 +1,28 @@
import { HttpClient, httpClient } from '../core/http-client';
import {environment} from "../../environments/environment";
import type {ChatMessageThreadInterface} from "../models/chat-message-thread.interface";
import type {ChatMessageInterface} from "../models/chat-message.interface";
import type {JobPostingInterface} from "../models/job.interface";
export class ChatMessagesService {
chatMessageThreads: ChatMessageThreadInterface[] = [];
constructor(private http: HttpClient = httpClient) {}
getChatMessages():Promise<ChatMessageThreadInterface[]>{
let url = environment.backendApi + 'api/1.1.0/chatMessages/forCandidate/';
return this.http.get<ChatMessageThreadInterface[]>(url);
}
sendMessage(chatMessage: ChatMessageInterface): Promise<ChatMessageInterface>{
let url = environment.backendApi + 'api/1.1.0/chatMessages/'+chatMessage.threadId+'/fromCandidate';
return this.http.post<ChatMessageInterface>(url, chatMessage);
}
markThreadRead(messageId: string): Promise<any>{
let url = environment.backendApi + 'api/1.1.0/chatMessages/markRead/'+messageId;
return this.http.get(url);
}
}

View File

@@ -0,0 +1,96 @@
import {environment} from "../../environments/environment";
import { HttpClient, httpClient } from '../core/http-client';
import type {UploadCvInterface} from "../models/post/upload-cv.interface";
import type {CvUploadDataInterface} from "../models/cv-upload-data.interface";
import type {CandidateInterface} from "../models/candidate.interface";
import {
LocalStorageService,
localStorageService as localStorageServiceSingleton,
} from "./local-storage.service";
export class CvUploadService {
constructor(
private http: HttpClient = httpClient,
private localStorageService: LocalStorageService = localStorageServiceSingleton
) {
}
// Updated version of upload to accommodate upload on the IOS app.
uploadCv(uploadCv: UploadCvInterface, token: string): Promise<{'cv_upload_data_id': number}>{
const url: string = environment.backendApiV2 + '1.0.0/client/cv_upload';
return this.http.post<{'cv_upload_data_id': number}>(url, uploadCv, {
headers: {
Authorization: `Bearer ${token}`,
},
});
}
// uploadCv(uploadCv: UploadCvInterface): Promise<{'cv_upload_data_id': number}>{
// let url = environment.backendApiV2 + '1.0.0/client/cv_upload';
// return this.http.post<{'cv_upload_data_id': number}>(url, uploadCv);
// }
getCvUploadData(): Promise<CvUploadDataInterface>{
let url = environment.backendApiV2 + '1.0.0/client/cv_upload/get_upload_cv';
return this.http.get<CvUploadDataInterface>(url);
}
setProfileTextToDone(cvUploadCvId: number): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/cv_upload/profile_text_is_done/'+cvUploadCvId;
let payload = {
}
return this.http.put(url, payload);
}
setExperienceToDone(cvUploadCvId: number): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/cv_upload/experience_is_done/'+cvUploadCvId;
let payload = {
}
return this.http.put(url, payload);
}
setEducationToDone(cvUploadCvId: number): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/cv_upload/education_is_done/'+cvUploadCvId;
let payload = {
}
return this.http.put(url, payload);
}
setQualificationToDone(cvUploadCvId: number): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/cv_upload/qualification_is_done/'+cvUploadCvId;
let payload = {
}
return this.http.put(url, payload);
}
setCertificateToDone(cvUploadCvId: number): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/cv_upload/certificate_is_done/'+cvUploadCvId;
let payload = {
}
return this.http.put(url, payload);
}
setLanguageToDone(cvUploadCvId: number): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/cv_upload/language_is_done/'+cvUploadCvId;
let payload = {
}
return this.http.put(url, payload);
}
setDriversLicenseToDone(cvUploadCvId: number): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/cv_upload/drivers_license_is_done/'+cvUploadCvId;
let payload = {
}
return this.http.put(url, payload);
}
setCvUploadToDone(cvUploadCvId: number): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/cv_upload/cv_is_done/'+cvUploadCvId;
let payload = {
}
return this.http.put(url, payload);
}
}
export const cvUploadService = new CvUploadService();

View File

@@ -0,0 +1,35 @@
import {environment} from "../../environments/environment";
import { HttpClient, httpClient } from '../core/http-client';
import type {DriverLicenseTypeInterface} from "../models/driver-license-type.interface";
import type {DriverLicenseGroupInterface} from "../models/driver-license-group.interface";
export class CvService {
constructor(private http: HttpClient = httpClient) {}
sendMyCvToEmail(): Promise<any>{
let url = environment.backendApi + 'api/1.1.0/candidate/sendPdf';
let payload = {
};
return this.http.post(url, payload);
}
getMyCv(): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/pdf';
let url = environment.backendApiV2 + '1.0.0/client/candidate/pdf';
return this.http.get(url, { responseType: 'blob' });
}
getMyCvV2(language: string): Promise<{url: string}>{
let url = environment.backendApiV2 + '1.0.0/client/candidate/pdf_url?language='+language;
return this.http.get<{url: string}>(url);
}
generateCv(language: string): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/candidate/generateCv';
let payload = {
language: language
}
return this.http.post(url, payload);
}
}

View File

@@ -0,0 +1,102 @@
import {environment} from "../../environments/environment";
import { HttpClient, httpClient } from '../core/http-client';
import type {DriverLicenseTypeInterface} from "../models/driver-license-type.interface";
import type {DriverLicenseGroupInterface} from "../models/driver-license-group.interface";
export class DriverLicenseService {
constructor(private http: HttpClient = httpClient) {}
getAllDriverLicenses(): Promise<DriverLicenseTypeInterface[]>{
let url = environment.backendApi + 'api/1.1.0/driversLicenses';
return this.http.get<DriverLicenseTypeInterface[]>(url);
}
// TODO this need a fix
updateDriverLicense(driverLicenseId: string, level: number): Promise<any>{
/*
{
driversLicense: {
candidateDriversLicense
:
null
code
:
"C1"
id
:
"a3cf994a-317e-47b3-a2c9-2142b5029ce3"
jobDriversLicense
:
null
jobnetDriversLicense
:
null
name
:
"C1 - Lille lastbil"
priority
:
8
special
:
false
}
driversLicenseId
:
"a3cf994a-317e-47b3-a2c9-2142b5029ce3"
id
:
"03807078-bd86-445d-94ef-8cdb39c41390"
level
:
3
levelName
:
"Let øvet"
}
* */
let url = environment.backendApi + 'api/1.1.0/candidate/driversLicense/'+ driverLicenseId;
let payload = {
};
return this.http.put(url, payload);
}
removeLanguage(driverLicenseId: string): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/driversLicense/'+driverLicenseId;
let url = environment.backendApiV2 + '1.0.0/client/candidate_drivers_license/'+driverLicenseId;
return this.http.delete(url);
}
saveDriverLicense(driverLicense: DriverLicenseTypeInterface, level: number): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/driversLicense/';
// let payload = {
// level: level,
// driversLicense: driverLicense
// };
let url = environment.backendApiV2 + '1.0.0/client/candidate_drivers_license';
let payload = {
level: level,
drivers_license_id: driverLicense.id
}
return this.http.post(url, payload);
}
saveDriverLicenseV2(level: number, drivers_license_id: string): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/candidate_drivers_license';
let payload = {
level: level,
drivers_license_id: drivers_license_id
}
return this.http.post(url, payload);
}
}

View File

@@ -0,0 +1,89 @@
import {environment} from "../../environments/environment";
import { HttpClient, httpClient } from '../core/http-client';
import type {EducationSearchInterface} from "../models/education-search.interface";
import type {SchoolInterface} from "../models/school.interface";
import type {SaveEducationInterface} from "../models/post/save-education.interface";
import type {QualificationInterface} from "../models/candidate.interface";
export class EducationService {
constructor(private http: HttpClient = httpClient) {}
searchForEducations(searchWord: string): Promise<EducationSearchInterface[]>{
let url = environment.backendApi + 'api/1.1.0/educations/search/' + searchWord;
return this.http.get<EducationSearchInterface[]>(url);
}
searchForSchools(searchWord: string): Promise<SchoolInterface[]>{
let url = environment.backendApi + 'api/1.1.0/institutions/search/' + searchWord;
return this.http.get<SchoolInterface[]>(url);
}
saveEducation(saveEducation: SaveEducationInterface, language: string): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/education/';
// return this.http.post(url, saveEducation);
let url = environment.backendApiV2 + '1.0.0/client/candidate_education';
let payload = {
comments: saveEducation.comments,
education_disced_15: saveEducation.education.disced15,
from_date: saveEducation.fromDate,
to_date: saveEducation.toDate,
institution_number: saveEducation.institution?.instNumber,
is_current: saveEducation.isCurrent,
language: language
};
return this.http.post(url, payload);
}
removeEducation(educationId: string): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/education/'+ educationId;
let url = environment.backendApiV2 + '1.0.0/client/candidate_education/'+ educationId;
return this.http.delete(url);
}
updateEducation(educationId: string, saveEducation: SaveEducationInterface, language: string): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/candidate_education/' + educationId;
let payload = {
comments: saveEducation.comments,
from_date: saveEducation.fromDate,
to_date: saveEducation.toDate,
is_current: saveEducation.isCurrent,
language: language
};
return this.http.put(url, payload);
}
getEducationOccupationSuggestions(disco: number): Promise<EducationSearchInterface[]>{
let url = environment.backendApi + 'api/1.1.0/educations/suggestions/occupation/' + disco;
return this.http.get<EducationSearchInterface[]>(url);
}
saveUnknownEducation(educatioName: string): Promise<{disced15: number}>{
let url = environment.backendApiV2 + '1.0.0/client/education/save_unknown_education';
let payload = {
'education_name': educatioName
}
return this.http.post<{disced15: number}>(url, payload);
}
saveEducationV2(education: {
comments: string;
institution_number: number | null;
from_date: Date | null;
to_date: Date | null;
education_disced_15: number | null;
is_current: boolean
}, language: string): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/candidate_education';
let payload = {
comments: education.comments,
education_disced_15: education.education_disced_15,
from_date: education.from_date,
to_date: education.to_date,
institution_number: education.institution_number,
is_current: education.is_current,
language: language
}
return this.http.post(url, payload);
}
}

View File

@@ -0,0 +1,22 @@
import { HttpClient, httpClient } from '../core/http-client';
import {environment} from "../../environments/environment";
import type {EscoInterface} from "../models/esco.interface";
export class EscoService {
constructor(private http: HttpClient = httpClient) {}
listEscoByParent(concept_uri: string | null): Promise<EscoInterface[]>{
let url = environment.backendApiV2 + '1.0.0/client/esco';
if(concept_uri){
url += '/'+concept_uri;
}
return this.http.get<EscoInterface[]>(url);
}
listAllEscos(){
let url = environment.backendApiV2 + '1.0.0/client/esco';
return this.http.get<EscoInterface[]>(url);
}
}

View File

@@ -0,0 +1,18 @@
export class FireMessagingServiceService {
async requestPermission(): Promise<string | null> {
if (typeof window === 'undefined' || !('Notification' in window)) {
return null;
}
const permission = await Notification.requestPermission();
return permission === 'granted' ? 'granted' : null;
}
listenForMessages(handler: (payload: unknown) => void): () => void {
const listener = (event: Event) => handler(event);
window.addEventListener('message', listener);
return () => window.removeEventListener('message', listener);
}
}
export const fireMessagingService = new FireMessagingServiceService();

View File

@@ -0,0 +1,33 @@
interface IEventData {
category?: string;
action?: string;
label?: string;
value?: number;
nonInteraction?: boolean;
data?: object;
}
export interface IDataLayerObject {
event: string;
eventData?: IEventData;
}
export class GoogleTagManagerService {
public triggerCustomEvent(obj: IDataLayerObject): void {
this.pushToDataLayer(obj);
}
private pushToDataLayer(obj: IDataLayerObject): void {
try {
// @ts-ignore
window.dataLayer = window.dataLayer || [];
// @ts-ignore
window.dataLayer.push(obj);
} catch (error) {
// console.log('GoogleTagManagerService: pushToDataLayer()', error.message);
}
}
}

View File

@@ -0,0 +1,36 @@
export * from './ai-handler.service';
export * from './audio.service';
export * from './auth.service';
export * from './candidate-education.service';
export * from './candidate-experience.service';
export * from './candidate-search-filter.service';
export * from './candidate-subscription-gift.service';
export * from './candidate.service';
export * from './certification.service';
export * from './chat-messages.service';
export * from './cv-upload.service';
export * from './cv.service';
export * from './driver-license.service';
export * from './education.service';
export * from './esco.service';
export * from './fireMessagingService.service';
export * from './google-tag-manager.service';
export * from './institution.service';
export * from './ios-mat-select-fix.service';
export * from './job-agent.service';
export * from './job.service';
export * from './language.service';
export * from './level.service';
export * from './local-storage.service';
export * from './message.service';
export * from './notification.service';
export * from './occupation.service';
export * from './payment.service';
export * from './permissions.service';
export * from './places.service';
export * from './qualification.service';
export * from './simulation.service';
export * from './sse.service';
export * from './subscription.service';
export * from './toaster.service';
export * from './zip.service';

View File

@@ -0,0 +1,14 @@
import {environment} from "../../environments/environment";
import { HttpClient, httpClient } from '../core/http-client';
export class InstitutionService {
constructor(private http: HttpClient = httpClient) {}
saveUnknownInstitution(institutionName: string): Promise<{inst_number: number}>{
let url = environment.backendApiV2 + '1.0.0/client/institution/save_unknown_institution';
let payload = {
'institution_name': institutionName
}
return this.http.post<{inst_number: number}>(url, payload);
}
}

View File

@@ -0,0 +1,12 @@
export class IosMatSelectFixService {
initializeFix(): void {
// No-op in React data-layer migration.
}
fixMatSelectOverlay(): void {
// No-op in React data-layer migration.
}
}
export const iosMatSelectFixService = new IosMatSelectFixService();

View File

@@ -0,0 +1,20 @@
import type {JobInterface, JobPostingInterface} from "../models/job.interface";
import {environment} from "../../environments/environment";
import { HttpClient, HttpParams, httpClient } from '../core/http-client';
export class JobAgentService {
constructor(private http: HttpClient = httpClient) {}
addEscoToJobAgent(escoId: number){
let url = environment.backendApiV2 + '1.0.0/client/job_agent_filter';
let payload = {
'esco_id': escoId
};
return this.http.post(url, payload);
}
removeJobAgentFilter(jobAgentFilterId: number): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/job_agent_filter/' + jobAgentFilterId;
return this.http.delete(url);
}
}

View File

@@ -0,0 +1,145 @@
import type { JobInterface, JobPostingInterface } from "../models/job.interface";
import {environment} from "../../environments/environment";
import { HttpClient, HttpParams, httpClient } from '../core/http-client';
import type { SearchJobInterface } from "../models/search-job.interface";
import type { SavedJobInterface } from "../models/saved-job.interface";
import type { JobnetJobDetailInterface } from "../models/jobnet-job-detail.interface";
import type { OccupationCategorizationInterface } from "../models/occupation-categorization.interface";
import type { JobDetailInterface } from "../models/job-detail.interface";
import type { CandidateApplicationInterface } from "../models/candidate-application.interface";
import type { JobSearchContainer } from "../models/job-search-container";
import type { AppliedJobInterface } from "../models/applied-job.interface";
export class JobService {
private http: HttpClient
constructor(http: HttpClient = httpClient) {
this.http = http
}
getSearchWords(): Promise<string[]> {
let url = environment.backendApiV2 + '1.0.0/client/candidate_search/list_search_words';
return this.http.get<string[]>(url);
}
getJobs(payload: SearchJobInterface, offset: number, limit: number): Promise<JobInterface[]>{
let url = environment.backendApi + 'api/1.1.0/jobs/all/'+offset+'/'+limit;
return this.http.post<JobInterface[]>(url, payload);
}
getJobsV2(searchLevel: number, offset: number, limit: number, searchWords: string[]): Promise<JobSearchContainer>{
let url = environment.backendApiV2 + '1.0.0/client/candidate_search/v2/' +searchLevel +'/'+offset+'/' + limit;
let params = new HttpParams();
if(searchWords.length > 0){
searchWords.forEach(item => {
params = params.append('terms', item);
});
}
return this.http.get<JobSearchContainer>(url, { params });
}
getSavedJobs(offset: number, limit: number): Promise<SavedJobInterface[]>{
void offset;
void limit;
let url = environment.backendApi + 'api/1.1.0/candidate/jobs/saved';
return this.http.get<SavedJobInterface[]>(url);
}
getSavedJobsV2(offset: number, limit: number): Promise<SavedJobInterface[]>{
let url = environment.backendApiV2 + '1.0.0/client/job/bookmarked_jobs/'+ offset + '/' + limit;
return this.http.get<SavedJobInterface[]>(url);
}
getAppliedJobs(offset: number, limit: number): Promise<CandidateApplicationInterface[]>{
void offset;
void limit;
let url = environment.backendApi + 'api/1.1.0/applications/candidate/';
return this.http.get<CandidateApplicationInterface[]>(url);
}
getAppliedJobsV2(offset: number, limit: number): Promise<AppliedJobInterface[]>{
let url = environment.backendApiV2 + '1.0.0/client/job/applied_jobs/'+ offset + '/' + limit;
return this.http.get<AppliedJobInterface[]>(url);
}
payWithStripe(paymentMethodId: string, amountInOere: number): Promise<any> {
const url = environment.backendApiV2 + '1.0.0/client/payment/create-payment-intent';
const payload = {
paymentMethodId: paymentMethodId,
amount: amountInOere
};
return this.http.post(url, payload);
}
// TODO REMOVE
bookmarkJob(jobId: string): Promise<any> {
let url = environment.backendApi + 'api/1.1.0/candidate/jobs/'+jobId+'/jobnet/updateSavedStatus';
let payload = {
saved: true
};
return this.http.post(url, payload);
}
// TODO REMOVE
unbookmarkJob(jobId: string): Promise<any>{
let url = environment.backendApi + 'api/1.1.0/candidate/jobs/'+jobId+'/jobnet/updateSavedStatus';
let payload = {
saved: false
};
return this.http.post(url, payload);
}
bookmarkJobV2(jobId: string, save: boolean, jobType: string): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/job/bookmark/'+jobId;
let payload = {
'job_type': jobType,
'save': save
}
return this.http.post(url, payload);
}
getJobNetJobDetail(jobId: string): Promise<JobnetJobDetailInterface>{
// let url = environment.backendApi + 'api/1.1.0/jobs/jobnet/'+jobId;
let url = environment.backendApiV2 + '1.0.0/client/job/detail/star/'+jobId;
return this.http.get<JobnetJobDetailInterface>(url);
}
getJobDetail(jobId: string): Promise<JobDetailInterface>{
let url = environment.backendApi + 'api/1.1.0/jobs/'+ jobId;
return this.http.get<JobDetailInterface>(url);
}
getOccupationCategorizations(): Promise<OccupationCategorizationInterface[]>{
// let url = environment.backendApi + 'api/1.1.0/occupations/categorization';
let url = environment.backendApiV2 + '1.0.0/client/ds_tree';
return this.http.get<OccupationCategorizationInterface[]>(url);
}
getJobSummary(jobId: string): Promise<JobPostingInterface>{
let url = environment.backendApi + 'api/1.1.0/jobs/'+jobId+'/summary';
return this.http.get<JobPostingInterface>(url);
}
applyJob(jobPostingId: string): Promise<CandidateApplicationInterface>{
let url = environment.backendApi + 'api/1.1.0/applications/job/'+ jobPostingId;
let payload = {
jobPostingId: jobPostingId
};
return this.http.post<CandidateApplicationInterface>(url, payload);
}
undoApplyJob(jobPostingId: string): Promise<any>{
let url = environment.backendApi + 'api/1.1.0/applications/'+ jobPostingId;
return this.http.delete(url);
}
toggleApplyJobnetjob(jobId: string, apply: boolean): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/job/detail/star/apply/'+jobId;
let payload = {
apply: apply
}
return this.http.put(url, payload);
}
}
export const jobService = new JobService();

View File

@@ -0,0 +1,55 @@
import { HttpClient, httpClient } from '../core/http-client';
import {environment} from "../../environments/environment";
import type {AllLanguageInterface, PlainLanguageInterface} from "../models/all-language.interface";
export class LanguageService {
constructor(private http: HttpClient = httpClient) {}
getAllLanguages(): Promise<AllLanguageInterface>{
let url = environment.backendApi + 'api/1.1.0/languages';
return this.http.get<AllLanguageInterface>(url);
}
saveLanguage(level: number, language: PlainLanguageInterface): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/language/';
let url = environment.backendApiV2 + '1.0.0/client/candidate_language';
let payload = {
language_id: language.id,
level: level
};
return this.http.post(url, payload);
}
updateLanguage(id: string, level: number, language: PlainLanguageInterface): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/language/' + id;
let url = environment.backendApiV2 + '1.0.0/client/candidate_language/' + id;
// let payload = {
// id: id,
// language: language,
// level: level
// };
let payload = {
level: level
};
return this.http.put(url, payload);
}
removeLanguage(languageId: string): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/language/' + languageId;
let url = environment.backendApiV2 + '1.0.0/client/candidate_language/' + languageId;
return this.http.delete(url);
}
saveLanguageV2(level: number, languageId: string): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/language/';
let url = environment.backendApiV2 + '1.0.0/client/candidate_language';
let payload = {
language_id: languageId,
level: level
};
return this.http.post(url, payload);
}
}

View File

@@ -0,0 +1,73 @@
import type {LevelInterface} from "../models/level.interface";
export class LevelService {
constructor() {
}
getLanguageLevels(): LevelInterface[]{
return [
{
id: 1,
text: 'Begynder'
},
{
id: 2,
text: 'Let øvet'
},
{
id: 3,
text: 'Øvet'
},
{
id: 4,
text: 'Rutineret'
},
{
id: 5,
text: 'Flydende'
},
];
}
getQualificationLevels(): LevelInterface[]{
return [
{
id: 1,
text: 'Begynder'
},
{
id: 2,
text: 'Let øvet'
},
{
id: 3,
text: 'Øvet'
},
{
id: 4,
text: 'Rutineret'
}
];
}
getDriverLicenseLevels(): LevelInterface[]{
return [
{
id: 1,
text: 'Begynder'
},
{
id: 2,
text: 'Let øvet'
},
{
id: 3,
text: 'Øvet'
},
{
id: 4,
text: 'Rutineret'
}
];
}
}

View File

@@ -0,0 +1,112 @@
import type {AuthInterface} from "../models/auth.interface";
import {BehaviorSubject} from "../core/async-state";
export class LocalStorageService {
authId: string = 'id';
authToken: string = 'token';
authEmail: string = 'email';
runOutDate: string = 'runOutDate';
private authTokenSubject = new BehaviorSubject<string | null>(null);
constructor() {
}
async loadAuthTokenFromStorage() {
const value = window.localStorage.getItem(this.authToken);
this.authTokenSubject.next(value);
}
async setAuthData(auth: AuthInterface, rememberMe: boolean){
await this.setPreference(this.authId, auth.id);
await this.setPreference(this.authToken, auth.token);
await this.setPreference(this.authEmail, auth.email);
if(rememberMe){
await this.setPreference(this.runOutDate, 'forever')
}
else{
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(now.getDate() + 1);
const dateString = tomorrow.toISOString();
await this.setPreference(this.runOutDate, dateString);
}
}
async setPreference(key: string, value: string){
window.localStorage.setItem(key, value);
}
async getCandidateId(): Promise<string | null>{
return window.localStorage.getItem(this.authId);
}
async getCandidateEmail(): Promise<string | null>{
return window.localStorage.getItem(this.authEmail);
}
async getAuthToken(): Promise<string | null>{
return window.localStorage.getItem(this.authToken);
}
getAuthTokenNoneAsync(): string | null {
// console.log('getAuthTokenNoneAsync', this.authTokenSubject);
return this.authTokenSubject.value;
}
async getRunOutDate(): Promise<string | null>{
return window.localStorage.getItem(this.runOutDate);
}
async clearCredentials(){
window.localStorage.removeItem(this.authId);
window.localStorage.removeItem(this.authToken);
window.localStorage.removeItem(this.runOutDate);
}
async getAuth(): Promise<AuthInterface | null> {
let id = await this.getCandidateId();
let token = await this.getAuthToken();
let email = await this.getCandidateEmail();
let runOutDate = await this.getRunOutDate();
if(id && token && email && runOutDate) {
let auth: AuthInterface = {
id: id,
token: token,
email: email,
runOutDate: runOutDate
};
return auth;
}
return null;
}
getAuthTokenWithoutWindow(): string {
const token = this.getAuthTokenNoneAsync();
if (token) {
return token;
}
// Fallback: try to get from storage synchronously if possible
// Note: This is a limitation - we can't do async operations here
// The token should be loaded via loadAuthTokenFromStorage() before use
return '';
}
getAuthWithoutWindow(): AuthInterface | null {
const token = this.getAuthTokenNoneAsync();
if (!token) {
return null;
}
return {
id: '',
token: token,
email: '',
runOutDate: 'forever'
};
}
}
export const localStorageService = new LocalStorageService();

View File

@@ -0,0 +1,11 @@
import { HttpClient, httpClient } from '../core/http-client';
import {environment} from "../../environments/environment";
export class MessageService {
constructor(private http: HttpClient = httpClient) {}
getUnreadMessages(): Promise<{unreadCount: number}>{
let url = environment.backendApi + 'api/1.1.0/chatMessages/getUnreadCount';
return this.http.get<{unreadCount: number}>(url);
}
}

View File

@@ -0,0 +1,73 @@
import { HttpClient, httpClient } from '../core/http-client';
import {environment} from "../../environments/environment";
import type {CandidateRetentionResponseInterface} from "../models/candidate-retention-response.interface";
import type {CandidateInterface, ExperienceInterface} from "../models/candidate.interface";
import type {CvSuggestionInterface} from "../models/cv-suggestion.interface";
//import exp from "node:constants";
import type {NotificationInterface} from "../models/notification.interface";
import type {JobInterface} from "../models/job.interface";
import type {NotificationSettingInterface} from "../models/notification-setting.interface";
export class NotificationService {
constructor(private http: HttpClient = httpClient) {}
// getNewNotifications(): Promise<NotificationInterface[]>{
// let url = environment.backendApi + 'api/1.1.0/candidate/notification/jobRelated/new';
// return this.http.get<NotificationInterface[]>(url);
// }
// getOldNotifications(): Promise<NotificationInterface[]>{
// let url = environment.backendApi + 'api/1.1.0/candidate/notification/jobRelated/old';
// return this.http.get<NotificationInterface[]>(url);
// }
//
// readNotification(notificationId: string): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/notification/'+notificationId+'/read';
// let payload = {};
// return this.http.put(url, payload);
// }
//
// getJobFromIds(jobsIds: string[]): Promise<JobInterface[]>{
// let url = environment.backendApi + 'api/1.1.0/jobs/getFromIds';
// let payload = jobsIds;
// return this.http.post<JobInterface[]>(url, payload);
// }
// NEW API (2.0.0 multiple job agents)
getNotificationSetting(): Promise<NotificationSettingInterface[]>{
const url = environment.backendApiV2 + '2.0.0/client/notification/settings';
return this.http.get<NotificationSettingInterface[]>(url);
}
createNotificationSetting(notificationSetting: NotificationSettingInterface): Promise<any> {
const url = environment.backendApiV2 + '2.0.0/client/notification/settings';
return this.http.post(url, notificationSetting);
}
updateNotificationSetting(id: number, notificationSetting: NotificationSettingInterface): Promise<any> {
const url = environment.backendApiV2 + '2.0.0/client/notification/settings/' + id;
return this.http.put(url, notificationSetting);
}
deleteNotificationSetting(id: number): Promise<void> {
const url = environment.backendApiV2 + '2.0.0/client/notification/settings/' + id;
return this.http.delete<void>(url);
}
getNewNotificationCount(): Promise<{unseenNotifications: number}> {
let url = environment.backendApiV2 + '1.0.0/client/notification/unseen_notifications';
return this.http.get<{ unseenNotifications: number }>(url);
}
getNotifications(offset: number, limit: number): Promise<NotificationInterface[]>{
let url = environment.backendApiV2 + '1.0.0/client/notification/' + offset + '/' + limit;
return this.http.get<NotificationInterface[]>(url);
}
notificationSeenByUser(notificationId: number): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/notification/seen_by_user/'+notificationId;
let payload = {
}
return this.http.put(url, payload)
}
}

View File

@@ -0,0 +1,15 @@
import { HttpClient, httpClient } from '../core/http-client';
import {environment} from "../../environments/environment";
import type {OccupationInterface} from "../models/occupation.interface";
export class OccupationService {
constructor(private http: HttpClient = httpClient) {}
addUnknownOccupation(occupation: string): Promise<OccupationInterface>{
let url = environment.backendApi + 'api/1.1.0/occupations/fromCandidate';
let payload = {
name: occupation
};
return this.http.post<OccupationInterface>(url, payload);
}
}

View File

@@ -0,0 +1,25 @@
import {environment} from "../../environments/environment";
import { HttpClient, httpClient } from '../core/http-client';
export class PaymentService {
constructor(private http: HttpClient = httpClient) {}
goToStripeCheckout(productId: number): Promise<any> {
let payload = {
productId: productId,
clientType: 'app'
}
return this.http.post<any>(environment.backendApiV2 + 'client/stripe/1.0.0/create_customer_session', payload)
}
goToStripeChangeCardCheckout(): Promise<any>{
let url = environment.backendApiV2 + 'client/stripe/1.0.0/create_update_card_session'
let payload = {
clientType: 'web'
};
return this.http.post<any>(url, payload)
}
}
export const paymentService = new PaymentService();

View File

@@ -0,0 +1,25 @@
import { localStorageService, LocalStorageService } from './local-storage.service';
export class PermissionsService {
constructor(private storage: LocalStorageService = localStorageService) {}
async canActivate(): Promise<boolean> {
const auth = await this.storage.getAuth();
if (!auth) {
return false;
}
if (auth.runOutDate === 'forever') {
return true;
}
const storedDate = new Date(auth.runOutDate);
const now = new Date();
if (storedDate > now) {
return true;
}
await this.storage.clearCredentials();
return false;
}
}
export const permissionsService = new PermissionsService();

View File

@@ -0,0 +1,17 @@
import { HttpClient, httpClient } from '../core/http-client';
import { environment } from '../../environments/environment';
export class PlacesService {
constructor(private http: HttpClient = httpClient) {}
searchPlaces(input: string): Promise<any> {
const url = environment.backendApiV2 + 'client/google_maps/1.0.0/get_places?query=' + encodeURIComponent(input);
return this.http.get(url);
}
getPlaceDetails(placeId: string): Promise<any> {
const url = environment.backendApiV2 + 'client/google_maps/1.0.0/get_place_details?place_id=' + encodeURIComponent(placeId);
return this.http.get(url);
}
}

View File

@@ -0,0 +1,61 @@
import {environment} from "../../environments/environment";
import { HttpClient, httpClient } from '../core/http-client';
import type {QualificationSearchInterface} from "../models/qualification-search.interface";
import type {QualificationInterface, SkillInterface} from "../models/candidate.interface";
export class QualificationService{
constructor(private http: HttpClient = httpClient) {}
searchForQualification(searchWord: string): Promise<QualificationSearchInterface[]>{
let url = environment.backendApi + 'api/1.1.0/qualifications/type/3/search/' + searchWord;
return this.http.get<QualificationSearchInterface[]>(url);
}
updateQualification(qualificationId: string, level: number): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/candidate_qualification/'+qualificationId;
let payload = {
level: level
}
return this.http.put(url, payload);
}
saveQualification(skill: SkillInterface): Promise<any> {
// let url = environment.backendApi + 'api/1.1.0/candidate/skill/';
// let payload = skill;
let url = environment.backendApiV2 + '1.0.0/client/candidate_qualification';
let payload = {
level: skill.level,
qualification_id: skill.qualification.id
};
return this.http.post(url, payload);
}
removeQualification(skillId: string): Promise<any>{
// let url = environment.backendApi + 'api/1.1.0/candidate/skill/'+skillId;
let url = environment.backendApiV2 + '1.0.0/client/candidate_qualification/'+skillId;
return this.http.delete(url);
}
getQualificationsOccupationSuggestions(disco: number, type: number): Promise<QualificationInterface[]>{
let url = environment.backendApi + 'api/1.1.0/qualifications/type/'+type+'/suggestions/occupation/' + disco;
return this.http.get<QualificationInterface[]>(url);
}
addUnknownQualification(unknownQualification: string): Promise<QualificationSearchInterface>{
let url = environment.backendApi + 'api/1.1.0/qualifications/type/3/fromCandidate';
let payload = {
name: unknownQualification,
type: 3
};
return this.http.post<QualificationSearchInterface>(url, payload);
}
saveQualificationV2(qualification: {level: number, qualification_id: string}): Promise<any>{
let url = environment.backendApiV2 + '1.0.0/client/candidate_qualification';
let payload = {
level: qualification.level,
qualification_id: qualification.qualification_id
};
return this.http.post(url, payload);
}
}

View File

@@ -0,0 +1,56 @@
import { HttpClient, HttpParams, httpClient } from '../core/http-client';
import {environment} from "../../environments/environment";
import type {SimulationPersonalityInterface} from "../models/simulation-personality.interface";
import {
LocalStorageService,
localStorageService as localStorageServiceSingleton,
} from "./local-storage.service";
export class SimulationService {
constructor(
private http: HttpClient = httpClient,
private localStorage: LocalStorageService = localStorageServiceSingleton,
) {
}
listSimulationPersonalities(): Promise<SimulationPersonalityInterface[]> {
let url = environment.backendApiV2 + 'client/simulation/1.0.0/list_simulation_personalities';
let params = new HttpParams().set('language', 'da');
return this.http.get<SimulationPersonalityInterface[]>(url, { params });
}
getInterviewEvaluation(interviewId: string): Promise<any> {
const url = environment.backendApiV2 + `client/simulation/1.0.0/interview_evaluation/${interviewId}`;
return this.http.get<any>(url);
}
listInterviews(limit: number = 20, offset: number = 0): Promise<any> {
const url = environment.backendApiV2 + `client/job_simulator/1.0.0/list_interviews`;
const params = new HttpParams()
.set('limit', limit.toString())
.set('offset', offset.toString());
return this.http.get<any>(url, { params });
}
submitEvaluationRating(interviewId: string, rating: number, ratingText: string): Promise<any> {
const url = environment.backendApiV2 + 'client/simulation/1.0.0/evaluation_rating';
const payload = {
interview_id: interviewId,
rating: rating,
rating_text: ratingText
};
return this.http.post(url, payload);
}
submitInterviewRating(interviewId: string, rating: number, ratingText: string): Promise<any> {
const url = environment.backendApiV2 + 'client/simulation/1.0.0/interview_rating';
const payload = {
interview_id: interviewId,
rating: rating,
rating_text: ratingText
};
return this.http.post(url, payload);
}
}
export const simulationService = new SimulationService();

View File

@@ -0,0 +1,247 @@
import { Observable, Subject, BehaviorSubject } from '../core/async-state';
import { environment } from '../../environments/environment';
import { LocalStorageService, localStorageService } from './local-storage.service';
/**
* Service for managing Server-Sent Events (SSE) connections to realtime interview backend.
*
* Handles:
* - SSE connection to Flask backend
* - Sending voice audio to backend
* - Receiving audio chunks and text updates from backend
* - Connection state management
*/
export class SSEService {
private currentEventSource: EventSource | null = null;
private currentInterviewId: string | null = null;
private connected$ = new BehaviorSubject<boolean>(false);
private audioChunk$ = new Subject<{ audio: string; format: string }>();
private textUpdate$ = new Subject<any>();
private error$ = new Subject<string>();
private joined$ = new Subject<{ interview_id: string; status: string }>();
private timeUpdate$ = new Subject<{ elapsed_time_minutes: number; remaining_time_minutes: number }>();
private interviewEnded$ = new Subject<{ interview_id: string; message: string }>();
private phaseUpdate$ = new Subject<string>(); // Track interview phase
constructor(private localStorage: LocalStorageService = localStorageService) {}
/**
* Connect to SSE endpoint for an interview.
*
* @param interviewId Interview session ID
* @returns Promise that resolves when connection is established
*/
connect(interviewId: string): Promise<void> {
return new Promise((resolve, reject) => {
// Close existing connection if any (even if same interview_id - we want a fresh connection)
if (this.currentEventSource) {
console.log('[SSE] Closing existing connection before creating new one');
this.disconnect();
}
this.currentInterviewId = interviewId;
const token = this.localStorage.getAuthTokenWithoutWindow();
// Connect to dedicated SSE dyno (sse.arbejd.com in production, separate from WEB dyno)
const sseUrl = `${environment.backendSSE}client/simulation_realtime/3.0.0/sse/${interviewId}?token=${encodeURIComponent(token)}`;
console.log('[SSE] Connecting to SSE dyno:', sseUrl);
const eventSource = new EventSource(sseUrl);
this.currentEventSource = eventSource;
// Handle connection established
eventSource.addEventListener('connected', (event: any) => {
console.log('[SSE] Connected:', event.data);
this.connected$.next(true);
try {
const data = JSON.parse(event.data);
this.joined$.next({
interview_id: data.interview_id,
status: data.status
});
resolve();
} catch (e) {
console.error('[SSE] Error parsing connected event:', e);
reject(e);
}
});
// Handle audio chunks
eventSource.addEventListener('audio_chunk', (event: any) => {
try {
const data = JSON.parse(event.data);
this.audioChunk$.next({
audio: data.audio,
format: data.format || 'pcm16'
});
} catch (e) {
console.error('[SSE] Error parsing audio_chunk event:', e);
}
});
// Handle text updates
eventSource.addEventListener('text_update', (event: any) => {
try {
const data = JSON.parse(event.data);
this.textUpdate$.next(data);
// Emit phase update if present
if (data.current_phase) {
this.phaseUpdate$.next(data.current_phase);
}
} catch (e) {
console.error('[SSE] Error parsing text_update event:', e);
}
});
// Handle audio complete
eventSource.addEventListener('audio_complete', (event: any) => {
try {
const data = JSON.parse(event.data);
if (data.elapsed_time_minutes !== undefined && data.remaining_time_minutes !== undefined) {
this.timeUpdate$.next({
elapsed_time_minutes: data.elapsed_time_minutes,
remaining_time_minutes: data.remaining_time_minutes
});
}
// Emit phase update if present
if (data.current_phase) {
this.phaseUpdate$.next(data.current_phase);
}
// When done=true, signal that all chunks have been sent
// The interview component will handle closing the connection when all chunks are received and played
if (data.done) {
console.log('[SSE] Audio complete with done marker - all chunks sent (connection will be closed when audio finishes playing)');
// Signal completion via audio chunk with format='complete'
// This lets the interview component know all chunks are sent and can close connection when playback finishes
this.audioChunk$.next({ audio: '', format: 'complete' });
}
} catch (e) {
console.error('[SSE] Error parsing audio_complete event:', e);
}
});
// Handle errors
eventSource.addEventListener('error', (event: any) => {
try {
const data = JSON.parse(event.data);
this.error$.next(data.message || 'Unknown error');
} catch (e) {
this.error$.next('Connection error');
}
console.error('[SSE] Error event:', event);
this.disconnect();
reject(new Error('SSE connection error'));
});
// Handle timeout
eventSource.addEventListener('timeout', (event: any) => {
console.warn('[SSE] Connection timeout');
this.disconnect();
});
// Handle generic error (connection failed)
eventSource.onerror = (error) => {
console.error('[SSE] EventSource error:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.warn('[SSE] Connection closed - frontend should reconnect automatically');
this.connected$.next(false);
// Don't reject here - let component handle reconnection
// This allows graceful reconnection without breaking the promise chain
} else if (eventSource.readyState === EventSource.CONNECTING) {
console.log('[SSE] Connection lost, attempting to reconnect...');
// EventSource will automatically try to reconnect
}
};
});
}
/**
* Disconnect from SSE endpoint.
*/
disconnect(): void {
if (this.currentEventSource) {
console.log('[SSE] Disconnecting');
this.currentEventSource.close();
this.currentEventSource = null;
this.currentInterviewId = null;
this.connected$.next(false);
}
}
/**
* Upload voice audio to backend.
*
* @param interviewId Interview session ID
* @param audioBase64 Base64 encoded audio file
* @param mimeType MIME type (e.g., 'audio/webm;codecs=opus')
* @param durationMs Duration in milliseconds (optional)
* @returns Promise that resolves when upload is acknowledged
*/
async uploadVoice(
interviewId: string,
audioBase64: string,
mimeType: string = 'audio/webm;codecs=opus',
durationMs?: number
): Promise<any> {
const token = this.localStorage.getAuthTokenWithoutWindow();
const url = `${environment.backendApiV2}client/simulation_realtime/3.0.0/voice`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
interview_id: interviewId,
audio_base64: audioBase64,
mime_type: mimeType,
duration_ms: durationMs
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to upload voice');
}
return await response.json();
}
// Observables for components to subscribe to
get connected(): Observable<boolean> {
return this.connected$.asObservable();
}
get audioChunk(): Observable<{ audio: string; format: string }> {
return this.audioChunk$.asObservable();
}
get textUpdate(): Observable<any> {
return this.textUpdate$.asObservable();
}
get error(): Observable<string> {
return this.error$.asObservable();
}
get joined(): Observable<{ interview_id: string; status: string }> {
return this.joined$.asObservable();
}
get timeUpdate(): Observable<{ elapsed_time_minutes: number; remaining_time_minutes: number }> {
return this.timeUpdate$.asObservable();
}
get interviewEnded(): Observable<{ interview_id: string; message: string }> {
return this.interviewEnded$.asObservable();
}
get phaseUpdate(): Observable<string> {
return this.phaseUpdate$.asObservable();
}
}
export const sseService = new SSEService();

View File

@@ -0,0 +1,37 @@
import {environment} from "../../environments/environment";
import { HttpClient, httpClient } from '../core/http-client';
import type {QualificationSearchInterface} from "../models/qualification-search.interface";
import type {QualificationInterface, SkillInterface} from "../models/candidate.interface";
import type {ZipInterface} from "../models/zip.interface";
import type {ZipV2Interface} from "../models/zip-v2.interface";
import type {PaymentOverview} from "../models/payment-overview.interface";
import type {SubscriptionProductInterface} from "../models/subscription-product.interface";
export class SubscriptionService {
constructor(private http: HttpClient = httpClient) {}
getPaymentOverview(): Promise<PaymentOverview>{
let url = environment.backendApiV2 + 'client/end_user_subscription/1.0.0/payment_overview';
return this.http.get<PaymentOverview>(url);
}
getSubscriptionProducts():Promise<SubscriptionProductInterface>{
let url = environment.backendApiV2 + 'client/end_user_subscription/1.0.0/get_products';
return this.http.get<SubscriptionProductInterface>(url);
}
deactivateAutoRenew(): Promise<any>{
let url = environment.backendApiV2 + 'client/end_user_subscription/1.0.0/deactivate_auto_renew';
return this.http.put(url, {});
}
activateAutoRenew(): Promise<any>{
let url = environment.backendApiV2 + 'client/end_user_subscription/1.0.0/activate_auto_renew';
return this.http.put(url, {});
}
redeemCode(code: string): Promise<any>{
let url = environment.backendApiV2 + 'client/candidate_redeem_code/v1';
return this.http.post(url, { code: code });
}
}

View File

@@ -0,0 +1,13 @@
export class ToasterService {
async show(
message: string,
type: 'success' | 'error' | 'warning' | 'info' = 'info',
): Promise<void> {
const level = type === 'error' ? 'error' : type === 'warning' ? 'warn' : 'log';
// Minimal platform-neutral implementation for now.
console[level](`[${type}] ${message}`);
}
}
export const toasterService = new ToasterService();

View File

@@ -0,0 +1,21 @@
import {environment} from "../../environments/environment";
import { HttpClient, httpClient } from '../core/http-client';
import type {QualificationSearchInterface} from "../models/qualification-search.interface";
import type {QualificationInterface, SkillInterface} from "../models/candidate.interface";
import type {ZipInterface} from "../models/zip.interface";
import type {ZipV2Interface} from "../models/zip-v2.interface";
export class ZipService {
constructor(private http: HttpClient = httpClient) {}
searchByZipCodeV2(zipCode: number): Promise<ZipV2Interface>{
let url = environment.backendApiV2 + '1.0.0/client/position/'+zipCode+'';
return this.http.get<ZipV2Interface>(url);
}
searchByZipCode(zipCode: number): Promise<ZipInterface[]>{
let url = environment.backendApi + 'api/1.1.0/address/postal?q='+zipCode+'*';
return this.http.get<ZipInterface[]>(url);
}
}

View File

@@ -0,0 +1,125 @@
import type { CvSuggestionInterface, ImprovementInterface } from '../models/cv-suggestion.interface';
import type { EscoInterface } from '../models/esco.interface';
import type { JobAgentFilterInterface } from '../models/job-agent-filter.interface';
import type { PaymentOverview } from '../models/payment-overview.interface';
import { CandidateService } from '../services/candidate.service';
import { EscoService } from '../services/esco.service';
import { JobAgentService } from '../services/job-agent.service';
import { SubscriptionService } from '../services/subscription.service';
export type ImprovementType = 'education' | 'language' | 'driversLicense' | 'qualification' | 'certificate';
export interface SuggestionImprovement extends ImprovementInterface {
improvementType: ImprovementType;
}
export interface AiAgentInitialData {
paymentOverview: PaymentOverview | null;
jobAgentFilters: JobAgentFilterInterface[];
cvSuggestions: CvSuggestionInterface[];
escos: EscoInterface[];
}
export class AiAgentViewModel {
constructor(
private candidateService: CandidateService = new CandidateService(),
private subscriptionService: SubscriptionService = new SubscriptionService(),
private jobAgentService: JobAgentService = new JobAgentService(),
private escoService: EscoService = new EscoService(),
) {}
async loadInitialData(): Promise<AiAgentInitialData> {
const [paymentResult, filtersResult, suggestionsResult, escosResult] = await Promise.allSettled([
this.subscriptionService.getPaymentOverview(),
this.candidateService.getJobAgentFilters(),
this.candidateService.getCvSuggestion(),
this.escoService.listAllEscos(),
]);
return {
paymentOverview: paymentResult.status === 'fulfilled' ? paymentResult.value : null,
jobAgentFilters: filtersResult.status === 'fulfilled' ? filtersResult.value : [],
cvSuggestions: suggestionsResult.status === 'fulfilled' ? this.withImprovements(suggestionsResult.value) : [],
escos: escosResult.status === 'fulfilled' ? escosResult.value : [],
};
}
async addEscoToFilter(escoId: number): Promise<void> {
await this.jobAgentService.addEscoToJobAgent(escoId);
}
async removeFilter(filterId: number): Promise<void> {
await this.jobAgentService.removeJobAgentFilter(filterId);
}
async setFilterVisibility(filter: JobAgentFilterInterface, visible: boolean): Promise<void> {
await this.candidateService.updateJobAgentFilter({
...filter,
visible,
});
}
getEscoSuggestions(query: string, allEscos: EscoInterface[], activeFilters: JobAgentFilterInterface[]): EscoInterface[] {
const trimmed = query.trim().toLowerCase();
if (!trimmed) {
return [];
}
const activeEscoIds = new Set(activeFilters.map((filter) => filter.escoId));
return allEscos
.filter((esco) => !activeEscoIds.has(esco.id))
.filter((esco) => esco.preferedLabelDa.toLowerCase().includes(trimmed))
.slice(0, 10);
}
getSuggestionText(chanceIncrease: number): string {
if (chanceIncrease >= 0 && chanceIncrease < 50) {
return 'Et godt første skridt mod flere relevante job.';
}
if (chanceIncrease >= 50 && chanceIncrease < 100) {
return 'Kan styrke dine chancer i ansøgningsbunken.';
}
if (chanceIncrease >= 100 && chanceIncrease < 150) {
return 'Ofte efterspurgt og forbedrer dine jobmuligheder markant.';
}
if (chanceIncrease >= 150 && chanceIncrease < 200) {
return 'Et klart plus som gør dig mere attraktiv for arbejdsgivere.';
}
if (chanceIncrease >= 200) {
return 'En afgørende faktor der åbner langt flere jobmuligheder.';
}
return 'Forbedrer din profil til kommende jobmatch.';
}
private withImprovements(suggestions: CvSuggestionInterface[]): CvSuggestionInterface[] {
return suggestions.map((suggestion) => {
const improvements: SuggestionImprovement[] = [];
const mappings: Array<{ items: ImprovementInterface[]; type: ImprovementType }> = [
{ items: suggestion.jobImprovementSuggestion.educations ?? [], type: 'education' },
{ items: suggestion.jobImprovementSuggestion.languages ?? [], type: 'language' },
{ items: suggestion.jobImprovementSuggestion.driversLicenses ?? [], type: 'driversLicense' },
{ items: suggestion.jobImprovementSuggestion.qualifications ?? [], type: 'qualification' },
{ items: suggestion.jobImprovementSuggestion.certificates ?? [], type: 'certificate' },
];
for (const mapping of mappings) {
for (const item of mapping.items) {
improvements.push({
...item,
improvementType: mapping.type,
});
}
}
improvements.sort((a, b) => b.jobChanceIncrease - a.jobChanceIncrease);
return {
...suggestion,
improvements,
};
});
}
}
export const aiAgentViewModel = new AiAgentViewModel();

View File

@@ -0,0 +1,89 @@
import type { NotificationInterface } from '../models/notification.interface';
import type { NotificationSettingInterface } from '../models/notification-setting.interface';
import type { OccupationCategorizationInterface } from '../models/occupation-categorization.interface';
import { JobService } from '../services/job.service';
import { NotificationService } from '../services/notification.service';
import { PlacesService } from '../services/places.service';
export class AiJobAgentViewModel {
constructor(
private notificationService: NotificationService = new NotificationService(),
private jobService: JobService = new JobService(),
private placesService: PlacesService = new PlacesService(),
) {}
async getNotificationSettings(): Promise<NotificationSettingInterface[]> {
const settings = await this.notificationService.getNotificationSetting();
return Array.isArray(settings) ? settings : [];
}
async getOccupationTree(): Promise<OccupationCategorizationInterface[]> {
const tree = await this.jobService.getOccupationCategorizations();
return Array.isArray(tree) ? tree : [];
}
async getNotifications(offset: number, limit: number): Promise<NotificationInterface[]> {
const notifications = await this.notificationService.getNotifications(offset, limit);
return Array.isArray(notifications) ? notifications : [];
}
async saveNotificationSetting(selectedId: number | 'new', setting: NotificationSettingInterface): Promise<void> {
if (selectedId === 'new') {
const payload = { ...setting };
delete (payload as Partial<NotificationSettingInterface>).id;
await this.notificationService.createNotificationSetting(payload);
return;
}
await this.notificationService.updateNotificationSetting(selectedId, setting);
}
async deleteNotificationSetting(id: number): Promise<void> {
await this.notificationService.deleteNotificationSetting(id);
}
async markNotificationSeen(id: number): Promise<void> {
await this.notificationService.notificationSeenByUser(id);
}
async toggleNotificationBookmark(notification: NotificationInterface, save: boolean): Promise<void> {
const hasJobnet = Boolean(notification.jobnetPostingId);
const jobId = hasJobnet ? notification.jobnetPostingId : notification.jobPostingId;
if (!jobId) {
return;
}
await this.jobService.bookmarkJobV2(jobId, save, hasJobnet ? 'star' : 'arbejd.com');
}
async searchPlaces(query: string): Promise<Array<{ place_id?: string; description?: string }>> {
const response = (await this.placesService.searchPlaces(query)) as {
predictions?: Array<{ place_id?: string; description?: string }>;
};
return response.predictions ?? [];
}
async getPlaceDetails(placeId: string): Promise<{
address: string;
latitude: number | null;
longitude: number | null;
} | null> {
const response = (await this.placesService.getPlaceDetails(placeId)) as {
result?: {
formatted_address?: string;
geometry?: { location?: { lat?: number; lng?: number } };
};
};
const result = response.result;
if (!result?.formatted_address) {
return null;
}
return {
address: result.formatted_address,
latitude: typeof result.geometry?.location?.lat === 'number' ? result.geometry.location.lat : null,
longitude: typeof result.geometry?.location?.lng === 'number' ? result.geometry.location.lng : null,
};
}
}
export const aiJobAgentViewModel = new AiJobAgentViewModel();

View File

@@ -0,0 +1,102 @@
import { AuthService } from '../services/auth.service';
import { CandidateService } from '../services/candidate.service';
import { localStorageService } from '../services/local-storage.service';
import type { SaveCandidateInterface } from '../models/post/save-candidate.interface';
import type { AuthInterface } from '../models/auth.interface';
export interface RegisterInput {
firstName: string;
lastName: string;
email: string;
password: string;
zip: string;
zipName: string;
subscribe: boolean;
}
export interface AuthActionResult {
ok: boolean;
message: string;
}
function getString(record: Record<string, unknown>, key: string): string | null {
const value = record[key];
return typeof value === 'string' ? value : null;
}
export class AuthViewModel {
private authService: AuthService;
private candidateService: CandidateService;
constructor(
authService: AuthService = new AuthService(),
candidateService: CandidateService = new CandidateService(),
) {
this.authService = authService;
this.candidateService = candidateService;
}
async login(email: string, password: string, rememberMe: boolean): Promise<AuthActionResult> {
const response = await this.authService.login(email, password);
const record = (response ?? {}) as Record<string, unknown>;
const token = getString(record, 'token');
if (!token) {
return {
ok: false,
message: 'Login fejlede: token mangler i svaret fra serveren.',
};
}
const authData: AuthInterface = {
id: getString(record, 'id') ?? '',
token,
email: getString(record, 'email') ?? email,
runOutDate: 'forever',
};
await localStorageService.setAuthData(authData, rememberMe);
return {
ok: true,
message: 'Du er nu logget ind.',
};
}
async forgotPassword(email: string): Promise<AuthActionResult> {
await this.authService.forgotPassword(email);
return {
ok: true,
message: 'Anmodning om nulstilling af kodeord er sendt.',
};
}
async register(input: RegisterInput): Promise<AuthActionResult> {
const zip = Number.parseInt(input.zip, 10);
if (Number.isNaN(zip)) {
return {
ok: false,
message: 'Postnummer skal være et tal.',
};
}
const payload: SaveCandidateInterface = {
email: input.email,
password: input.password,
zip,
zipName: input.zipName,
awsUrl: '',
latitude: 0,
longitude: 0,
firstName: input.firstName,
lastName: input.lastName,
subscribe: input.subscribe,
};
await this.candidateService.createAccountV2(payload);
return {
ok: true,
message: 'Kontoen er oprettet.',
};
}
}

View File

@@ -0,0 +1,354 @@
import type { AiGeneratedCVDescription } from '../models/ai-generated-cv-description.interface';
import type { CandidateInterface, CertificationInterface, DriversLicenseInterface, EducationInterface, ExperienceInterface, LanguageInterface, SkillInterface } from '../models/candidate.interface';
import type { CvUploadDataInterface } from '../models/cv-upload-data.interface';
import type { PaymentOverview } from '../models/payment-overview.interface';
import type { PlainLanguageInterface } from '../models/all-language.interface';
import type { SaveEducationInterface } from '../models/post/save-education.interface';
import type { DriverLicenseTypeInterface } from '../models/driver-license-type.interface';
import type { EscoInterface } from '../models/esco.interface';
import type { QualificationSearchInterface } from '../models/qualification-search.interface';
import type { EducationSearchInterface } from '../models/education-search.interface';
import type { SchoolInterface } from '../models/school.interface';
import type { SearchedCertificationInterface } from '../models/searched-certification.interface';
import { AiHandlerService } from '../services/ai-handler.service';
import { CandidateService } from '../services/candidate.service';
import { CertificationService } from '../services/certification.service';
import { CvService } from '../services/cv.service';
import { CvUploadService } from '../services/cv-upload.service';
import { DriverLicenseService } from '../services/driver-license.service';
import { EducationService } from '../services/education.service';
import { LanguageService } from '../services/language.service';
import { LocalStorageService, localStorageService as localStorageServiceSingleton } from '../services/local-storage.service';
import { QualificationService } from '../services/qualification.service';
import { SubscriptionService } from '../services/subscription.service';
import { EscoService } from '../services/esco.service';
import { OccupationService } from '../services/occupation.service';
import { InstitutionService } from '../services/institution.service';
export interface CvPageSnapshot {
candidate: CandidateInterface | null;
experiences: ExperienceInterface[];
educations: EducationInterface[];
skills: SkillInterface[];
certifications: CertificationInterface[];
languages: LanguageInterface[];
driverLicenses: DriversLicenseInterface[];
paymentOverview: PaymentOverview | null;
cvUploadData: CvUploadDataInterface | null;
aiGeneratedCVDescription: AiGeneratedCVDescription | null;
}
export class CvPageViewModel {
private escosCache: EscoInterface[] | null = null;
constructor(
private readonly candidateService: CandidateService = new CandidateService(),
private readonly cvService: CvService = new CvService(),
private readonly cvUploadService: CvUploadService = new CvUploadService(),
private readonly subscriptionService: SubscriptionService = new SubscriptionService(),
private readonly aiHandlerService: AiHandlerService = new AiHandlerService(),
private readonly educationService: EducationService = new EducationService(),
private readonly qualificationService: QualificationService = new QualificationService(),
private readonly certificationService: CertificationService = new CertificationService(),
private readonly languageService: LanguageService = new LanguageService(),
private readonly driverLicenseService: DriverLicenseService = new DriverLicenseService(),
private readonly escoService: EscoService = new EscoService(),
private readonly occupationService: OccupationService = new OccupationService(),
private readonly institutionService: InstitutionService = new InstitutionService(),
private readonly localStorageService: LocalStorageService = localStorageServiceSingleton,
) {}
async getSnapshot(): Promise<CvPageSnapshot> {
const [
candidateRes,
experiencesRes,
educationsRes,
skillsRes,
certificationsRes,
languagesRes,
driverLicensesRes,
paymentRes,
uploadRes,
aiRes,
] = await Promise.allSettled([
this.candidateService.getCandidate(),
this.candidateService.getCandidatesExperiences(),
this.candidateService.getCandidatesEducations(),
this.candidateService.getCandidatesQualifications(),
this.candidateService.getCandidatesCertifications(),
this.candidateService.getCandidatesLanguages(),
this.candidateService.getCandidatesDriverLicenses(),
this.subscriptionService.getPaymentOverview(),
this.cvUploadService.getCvUploadData(),
this.aiHandlerService.getMyCvDescriptions(),
]);
return {
candidate: candidateRes.status === 'fulfilled' ? candidateRes.value : null,
experiences: experiencesRes.status === 'fulfilled' ? experiencesRes.value : [],
educations: educationsRes.status === 'fulfilled' ? educationsRes.value : [],
skills: skillsRes.status === 'fulfilled' ? skillsRes.value : [],
certifications: certificationsRes.status === 'fulfilled' ? certificationsRes.value : [],
languages: languagesRes.status === 'fulfilled' ? languagesRes.value : [],
driverLicenses: driverLicensesRes.status === 'fulfilled' ? driverLicensesRes.value : [],
paymentOverview: paymentRes.status === 'fulfilled' ? paymentRes.value : null,
cvUploadData: uploadRes.status === 'fulfilled' ? uploadRes.value : null,
aiGeneratedCVDescription: aiRes.status === 'fulfilled' && aiRes.value?.id ? aiRes.value : null,
};
}
async setActiveSeeker(candidate: CandidateInterface, isActive: boolean, language: string): Promise<CandidateInterface> {
const payload: CandidateInterface = {
...candidate,
isActive,
};
return this.candidateService.updateCandidate(payload, language);
}
async updateCandidate(candidate: CandidateInterface, language: string): Promise<CandidateInterface> {
return this.candidateService.updateCandidate(candidate, language);
}
async generateCv(language: string): Promise<void> {
await this.cvService.generateCv(language);
}
async getCvDownloadUrl(language: string): Promise<string> {
const response = await this.cvService.getMyCvV2(language);
return response.url;
}
async uploadCv(base64File: string, fileType: 'pdf' | 'docx'): Promise<void> {
const token = await this.localStorageService.getAuthToken();
if (!token) {
throw new Error('No auth token found.');
}
await this.cvUploadService.uploadCv(
{
base_64_cv_file: base64File,
cv_file_type: fileType,
},
token,
);
}
async optimizeCv(language: string): Promise<void> {
await this.aiHandlerService.updateMyCvDescriptions(language);
}
async updateExperience(experience: ExperienceInterface, language: string): Promise<void> {
await this.candidateService.updateExperience(experience, language);
}
async updateEducation(education: EducationInterface, language: string): Promise<void> {
const payload: SaveEducationInterface = {
comments: education.comments,
education: education.education,
institution: education.institution,
fromDate: new Date(education.fromDate),
toDate: new Date(education.toDate),
isCurrent: education.isCurrent,
};
await this.educationService.updateEducation(education.id, payload, language);
}
async updateCertification(certification: CertificationInterface): Promise<void> {
await this.certificationService.updateCertification(certification);
}
async updateLanguage(languageItem: LanguageInterface): Promise<void> {
const language: PlainLanguageInterface = {
id: languageItem.language.id,
isO639: languageItem.language.isO639,
name: languageItem.language.name,
ownName: languageItem.language.ownName,
priority: languageItem.language.priority,
};
await this.languageService.updateLanguage(languageItem.id, languageItem.level, language);
}
async removeExperience(experienceId: string): Promise<void> {
await this.candidateService.removeExperience(experienceId);
}
async removeEducation(educationId: string): Promise<void> {
await this.educationService.removeEducation(educationId);
}
async removeQualification(skillId: string): Promise<void> {
await this.qualificationService.removeQualification(skillId);
}
async removeCertification(certificationId: string): Promise<void> {
await this.certificationService.removeCertification(certificationId);
}
async removeLanguage(languageId: string): Promise<void> {
await this.languageService.removeLanguage(languageId);
}
async removeDriverLicense(driverLicenseId: string): Promise<void> {
await this.driverLicenseService.removeLanguage(driverLicenseId);
}
async getEscoSuggestions(searchWord: string, limit = 25): Promise<EscoInterface[]> {
if (!this.escosCache) {
this.escosCache = await this.escoService.listAllEscos();
}
const trimmed = searchWord.trim().toLowerCase();
if (!trimmed) {
return this.escosCache.slice(0, limit);
}
return this.escosCache
.filter((esco) => esco.preferedLabelDa.toLowerCase().includes(trimmed))
.slice(0, limit);
}
async getLanguageOptions(): Promise<PlainLanguageInterface[]> {
const allLanguages = await this.languageService.getAllLanguages();
return allLanguages.allLanguages ?? [];
}
async getDriverLicenseOptions(): Promise<DriverLicenseTypeInterface[]> {
return this.driverLicenseService.getAllDriverLicenses();
}
async createExperience(
payload: {
companyName: string;
comments: string;
fromDate: Date | null;
toDate: Date | null;
isCurrent: boolean;
escoId?: number | null;
occupationName?: string;
},
language: string,
): Promise<void> {
let escoId = payload.escoId ?? null;
if (!escoId && payload.occupationName?.trim()) {
const createdOccupation = await this.occupationService.addUnknownOccupation(payload.occupationName.trim());
escoId = createdOccupation.id;
}
if (!escoId) {
throw new Error('Vælg eller opret en stilling først.');
}
await this.candidateService.saveExperienceV2(
{
companyName: payload.companyName,
comments: payload.comments,
fromDate: payload.fromDate,
toDate: payload.toDate,
isCurrent: payload.isCurrent,
escoId,
},
language,
);
}
async createEducation(
payload: {
comments: string;
fromDate: Date | null;
toDate: Date | null;
isCurrent: boolean;
educationName?: string;
educationDisced15?: number | null;
institutionName?: string;
institutionNumber: number | null;
},
language: string,
): Promise<void> {
let educationDisced15 = payload.educationDisced15 ?? null;
if (!educationDisced15 && payload.educationName?.trim()) {
const unknownEducation = await this.educationService.saveUnknownEducation(payload.educationName);
educationDisced15 = unknownEducation.disced15;
}
if (!educationDisced15) {
throw new Error('Vælg eller opret en uddannelse først.');
}
let institutionNumber = payload.institutionNumber;
if (!institutionNumber && payload.institutionName?.trim()) {
const savedInstitution = await this.institutionService.saveUnknownInstitution(payload.institutionName.trim());
institutionNumber = savedInstitution.inst_number;
}
await this.educationService.saveEducationV2(
{
comments: payload.comments,
institution_number: institutionNumber,
from_date: payload.fromDate,
to_date: payload.toDate,
education_disced_15: educationDisced15,
is_current: payload.isCurrent,
},
language,
);
}
async createCertification(payload: { certificateId?: string | null; certificateName?: string }): Promise<void> {
let certificateId = payload.certificateId ?? null;
if (!certificateId && payload.certificateName?.trim()) {
const saved = await this.certificationService.addUnknownCertificate(payload.certificateName.trim());
certificateId = saved.certificate_id;
}
if (!certificateId) {
throw new Error('Vælg eller opret et certifikat først.');
}
await this.certificationService.saveCertification(certificateId);
}
async createLanguage(languageId: string, level: number): Promise<void> {
await this.languageService.saveLanguageV2(level, languageId);
}
async getQualificationSuggestions(searchWord: string): Promise<QualificationSearchInterface[]> {
const trimmed = searchWord.trim();
if (!trimmed) {
return [];
}
return this.qualificationService.searchForQualification(trimmed);
}
async createQualification(payload: { qualificationId?: string; qualificationName?: string; level: number }): Promise<void> {
let qualificationId = payload.qualificationId?.trim() || '';
if (!qualificationId && payload.qualificationName?.trim()) {
const added = await this.qualificationService.addUnknownQualification(payload.qualificationName.trim());
qualificationId = added.id;
}
if (!qualificationId) {
throw new Error('Vælg eller opret en kvalifikation først.');
}
await this.qualificationService.saveQualificationV2({ qualification_id: qualificationId, level: payload.level });
}
async getEducationSuggestions(searchWord: string): Promise<EducationSearchInterface[]> {
const trimmed = searchWord.trim();
if (!trimmed) {
return [];
}
return this.educationService.searchForEducations(trimmed);
}
async getSchoolSuggestions(searchWord: string): Promise<SchoolInterface[]> {
const trimmed = searchWord.trim();
if (!trimmed) {
return [];
}
return this.educationService.searchForSchools(trimmed);
}
async getCertificationSuggestions(searchWord: string): Promise<SearchedCertificationInterface[]> {
const trimmed = searchWord.trim();
if (!trimmed) {
return [];
}
return this.certificationService.searchForCertification(trimmed);
}
async createDriverLicense(driversLicenseId: string, level: number): Promise<void> {
await this.driverLicenseService.saveDriverLicenseV2(level, driversLicenseId);
}
}

View File

@@ -0,0 +1,162 @@
import type { CandidateInterface } from '../models/candidate.interface';
import type { NotificationInterface } from '../models/notification.interface';
import type { PaymentOverview } from '../models/payment-overview.interface';
import type { JobPostingInterface } from '../models/job.interface';
import { CandidateService } from '../services/candidate.service';
import { JobService } from '../services/job.service';
import { NotificationService } from '../services/notification.service';
import { SimulationService } from '../services/simulation.service';
import { SubscriptionService } from '../services/subscription.service';
import { MessagesViewModel, type MessageThreadItem } from './MessagesViewModel';
export interface DashboardBestJobItem {
id: string;
title: string;
companyName: string;
address: string;
applicationDeadline: string;
candidateDistance: number | null;
fromJobnet: boolean;
logoUrl: string;
companyLogoImage: string;
}
export interface DashboardEvaluationItem {
id: string;
jobName: string;
companyName: string | null;
interviewDate: string | null;
recommendation: string | null;
isCompleted: boolean;
}
export interface DashboardInitialData {
candidate: CandidateInterface | null;
notifications: NotificationInterface[];
messages: MessageThreadItem[];
bestJobs: DashboardBestJobItem[];
subscription: PaymentOverview | null;
evaluations: DashboardEvaluationItem[];
}
function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : null;
}
function normalizeText(value: unknown): string {
return typeof value === 'string' ? value : '';
}
function toNullableNumber(value: unknown): number | null {
return typeof value === 'number' ? value : null;
}
function toBestJobItem(value: unknown): DashboardBestJobItem | null {
const source = asRecord(value);
if (!source) {
return null;
}
const postingSource = asRecord(source.jobPosting);
const posting = postingSource ?? source;
const id = normalizeText(posting.id);
if (!id) {
return null;
}
return {
id,
title: normalizeText(posting.title),
companyName: normalizeText(posting.companyName),
address: normalizeText(posting.address),
applicationDeadline: normalizeText(posting.applicationDeadline),
candidateDistance: toNullableNumber(posting.candidateDistance),
fromJobnet: Boolean(posting.fromJobnet),
logoUrl: normalizeText(posting.logoUrl),
companyLogoImage: normalizeText(posting.companyLogoImage),
};
}
function toEvaluationItems(value: unknown): DashboardEvaluationItem[] {
const root = asRecord(value);
const interviews = Array.isArray(root?.interviews) ? root.interviews : [];
return interviews
.map((item) => {
const source = asRecord(item);
if (!source) {
return null;
}
const id = normalizeText(source.id);
if (!id) {
return null;
}
return {
id,
jobName: normalizeText(source.job_name) || 'Interview',
companyName: normalizeText(source.company_name) || null,
interviewDate: normalizeText(source.interview_date) || null,
recommendation: normalizeText(source.recommendation) || null,
isCompleted: Boolean(source.is_completed),
} satisfies DashboardEvaluationItem;
})
.filter((item): item is DashboardEvaluationItem => Boolean(item))
.sort((a, b) => {
const aTime = a.interviewDate ? new Date(a.interviewDate).getTime() : 0;
const bTime = b.interviewDate ? new Date(b.interviewDate).getTime() : 0;
return bTime - aTime;
})
.slice(0, 5);
}
function extractSearchList(payload: unknown): unknown[] {
const root = asRecord(payload);
if (!root) {
return [];
}
return Array.isArray(root.searchList) ? root.searchList : [];
}
export class DashboardViewModel {
constructor(
private readonly candidateService: CandidateService = new CandidateService(),
private readonly notificationService: NotificationService = new NotificationService(),
private readonly jobService: JobService = new JobService(),
private readonly subscriptionService: SubscriptionService = new SubscriptionService(),
private readonly simulationService: SimulationService = new SimulationService(),
private readonly messagesViewModel: MessagesViewModel = new MessagesViewModel(),
) {}
async loadInitialData(): Promise<DashboardInitialData> {
const [candidateResult, notificationsResult, messagesResult, bestJobsResult, subscriptionResult, evaluationsResult] =
await Promise.allSettled([
this.candidateService.getCandidate(),
this.notificationService.getNotifications(0, 5),
this.messagesViewModel.getThreads(),
this.loadBestJobs(),
this.subscriptionService.getPaymentOverview(),
this.simulationService.listInterviews(5, 0),
]);
return {
candidate: candidateResult.status === 'fulfilled' ? candidateResult.value : null,
notifications: notificationsResult.status === 'fulfilled' ? notificationsResult.value.slice(0, 5) : [],
messages: messagesResult.status === 'fulfilled' ? messagesResult.value.slice(0, 5) : [],
bestJobs: bestJobsResult.status === 'fulfilled' ? bestJobsResult.value : [],
subscription: subscriptionResult.status === 'fulfilled' ? subscriptionResult.value : null,
evaluations: evaluationsResult.status === 'fulfilled' ? toEvaluationItems(evaluationsResult.value) : [],
};
}
private async loadBestJobs(): Promise<DashboardBestJobItem[]> {
const payload = await this.jobService.getJobsV2(10, 0, 5, []);
const list = extractSearchList(payload);
return list
.map((item) => toBestJobItem(item))
.filter((item): item is DashboardBestJobItem => Boolean(item))
.slice(0, 5);
}
}

Some files were not shown because too many files have changed in this diff Show More