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

4250
src/App.css Normal file

File diff suppressed because it is too large Load Diff

246
src/App.tsx Normal file
View File

@@ -0,0 +1,246 @@
import './App.css';
import { useEffect } from 'react';
import { ForgotPasswordPage } from './presentation/auth/pages/ForgotPasswordPage';
import { LoginPage } from './presentation/auth/pages/LoginPage';
import { RegisterPage } from './presentation/auth/pages/RegisterPage';
import { DashboardPage } from './presentation/dashboard/pages/DashboardPage';
import { CvPage } from './presentation/cv/pages/CvPage';
import { JobsPage } from './presentation/jobs/pages/JobsPage';
import { JobDetailPage } from './presentation/jobs/pages/JobDetailPage';
import { BeskederPage } from './presentation/messages/pages/BeskederPage';
import { AiAgentPage } from './presentation/ai-agent/pages/AiAgentPage';
import { AiJobAgentPage } from './presentation/ai-jobagent/pages/AiJobAgentPage';
import { JobSimulatorPage } from './presentation/simulator/pages/JobSimulatorPage';
import { SubscriptionPage } from './presentation/subscription/pages/SubscriptionPage';
import { ThemeToggle } from './presentation/layout/components/ThemeToggle';
import { useAuthViewModel } from './presentation/auth/hooks/useAuthViewModel';
import { useBrowserRoute } from './presentation/router/useBrowserRoute';
import { localStorageService } from './mvvm/services/local-storage.service';
function App() {
const { path, navigate } = useBrowserRoute();
const { isLoading, result, login, register, forgotPassword } = useAuthViewModel();
const isJobDetail = path.startsWith('/jobs/');
const jobDetailMatch = isJobDetail ? path.match(/^\/jobs\/([^/]+)\/(jobnet|arbejd)$/) : null;
const jobIdFromPath = jobDetailMatch ? decodeURIComponent(jobDetailMatch[1]) : '';
const fromJobnetFromPath = jobDetailMatch ? jobDetailMatch[2] === 'jobnet' : false;
const mode =
path === '/register'
? 'register'
: path === '/forgot-password'
? 'forgot'
: path === '/dashboard'
? 'dashboard'
: path === '/cv'
? 'cv'
: path === '/jobs'
? 'jobs'
: path === '/beskeder'
? 'beskeder'
: path === '/ai-jobagent'
? 'ai-jobagent'
: path === '/ai-agent'
? 'ai-agent'
: path === '/simulator'
? 'simulator'
: path === '/abonnement'
? 'abonnement'
: isJobDetail
? 'job-detail'
: 'login';
useEffect(() => {
const token = window.localStorage.getItem('token');
const isAuthPage = path === '/login' || path === '/register' || path === '/forgot-password' || path === '/';
if ((path === '/dashboard' || path === '/cv' || path === '/jobs' || path === '/beskeder' || path === '/ai-jobagent' || path === '/ai-agent' || path === '/simulator' || path === '/abonnement' || isJobDetail) && !token) {
navigate('/login', true);
return;
}
if (isAuthPage && token) {
navigate('/dashboard', true);
}
}, [path, navigate, isJobDetail]);
async function logout() {
await localStorageService.clearCredentials();
navigate('/login', true);
}
function navigateFromSidebar(key: 'dashboard' | 'cv' | 'jobs' | 'beskeder' | 'ai-jobagent' | 'ai-agent' | 'simulator' | 'abonnement') {
if (key === 'dashboard') {
navigate('/dashboard');
return;
}
if (key === 'cv') {
navigate('/cv');
return;
}
if (key === 'jobs') {
navigate('/jobs');
return;
}
if (key === 'ai-jobagent') {
navigate('/ai-jobagent');
return;
}
if (key === 'ai-agent') {
navigate('/ai-agent');
return;
}
if (key === 'simulator') {
navigate('/simulator');
return;
}
if (key === 'abonnement') {
navigate('/abonnement');
return;
}
navigate('/beskeder');
}
const isAppLayoutMode = mode === 'dashboard' || mode === 'cv' || mode === 'jobs' || mode === 'job-detail' || mode === 'beskeder' || mode === 'ai-jobagent' || mode === 'ai-agent' || mode === 'simulator' || mode === 'abonnement';
return (
<main className={isAppLayoutMode ? 'auth-root dashboard-mode' : 'auth-root'}>
<div className="orb orb-1" />
<div className="orb orb-2" />
<div className="orb orb-3" />
{mode === 'dashboard' ? (
<DashboardPage
onLogout={logout}
onNavigate={navigateFromSidebar}
onOpenJob={(jobId, fromJobnet) =>
navigate(`/jobs/${encodeURIComponent(jobId)}/${fromJobnet ? 'jobnet' : 'arbejd'}`)
}
/>
) : mode === 'cv' ? (
<CvPage onLogout={logout} onNavigate={navigateFromSidebar} />
) : mode === 'beskeder' ? (
<BeskederPage onLogout={logout} onNavigate={navigateFromSidebar} />
) : mode === 'ai-jobagent' ? (
<AiJobAgentPage
onLogout={logout}
onNavigate={navigateFromSidebar}
onOpenJob={(jobId, fromJobnet) =>
navigate(`/jobs/${encodeURIComponent(jobId)}/${fromJobnet ? 'jobnet' : 'arbejd'}`)
}
/>
) : mode === 'ai-agent' ? (
<AiAgentPage onLogout={logout} onNavigate={navigateFromSidebar} activeNavKey="ai-agent" />
) : mode === 'simulator' ? (
<JobSimulatorPage onLogout={logout} onNavigate={navigateFromSidebar} />
) : mode === 'abonnement' ? (
<SubscriptionPage onLogout={logout} onNavigate={navigateFromSidebar} />
) : mode === 'job-detail' && jobDetailMatch ? (
<JobDetailPage
jobId={jobIdFromPath}
fromJobnet={fromJobnetFromPath}
onLogout={logout}
onNavigate={navigateFromSidebar}
/>
) : mode === 'jobs' ? (
<JobsPage
onLogout={logout}
onNavigate={navigateFromSidebar}
onOpenJob={(jobId, fromJobnet) =>
navigate(`/jobs/${encodeURIComponent(jobId)}/${fromJobnet ? 'jobnet' : 'arbejd'}`)
}
/>
) : (
<section className="auth-shell glass-panel">
<aside className="brand-panel">
<div className="brand-chip">
<span>Ar</span>
</div>
<h1>Arbejd.com</h1>
<p>
AI-assisteret jobsøgning med glasdesign og fokus flow.
</p>
<ul className="brand-list">
<li>Log ind</li>
<li>Opret konto</li>
<li>Glemt kodeord</li>
</ul>
</aside>
<section className="form-panel glass-panel">
<div className="auth-theme-row">
<ThemeToggle />
</div>
<div className="mode-tabs">
<button
className={mode === 'login' ? 'tab-btn active' : 'tab-btn'}
onClick={() => navigate('/login')}
type="button"
>
Log ind
</button>
<button
className={mode === 'register' ? 'tab-btn active' : 'tab-btn'}
onClick={() => navigate('/register')}
type="button"
>
Opret konto
</button>
<button
className={mode === 'forgot' ? 'tab-btn active' : 'tab-btn'}
onClick={() => navigate('/forgot-password')}
type="button"
>
Glemt kode
</button>
</div>
{mode === 'login' && (
<LoginPage
isLoading={isLoading}
onSubmit={async (email, password, rememberMe) => {
const response = await login(email, password, rememberMe);
if (response.ok) {
navigate('/dashboard');
}
}}
/>
)}
{mode === 'register' && (
<RegisterPage
isLoading={isLoading}
onSubmit={async (payload) => {
const response = await register(payload);
if (response.ok) {
navigate('/login');
}
}}
/>
)}
{mode === 'forgot' && (
<ForgotPasswordPage
isLoading={isLoading}
onSubmit={async (email) => {
const response = await forgotPassword(email);
if (response.ok) {
navigate('/login');
}
}}
/>
)}
{result && (
<p className={result.ok ? 'status success' : 'status error'}>
{result.message}
</p>
)}
</section>
</section>
)}
</main>
);
}
export default App;

1
src/assets/react.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,18 @@
export const environment = {
production: false,
backendApi: import.meta.env.VITE_BACKEND_API ?? 'https://api.arbejd.com/',
backendApiV2: import.meta.env.VITE_BACKEND_API_V2 ?? 'https://api2.arbejd.com/api/',
backendSSE: import.meta.env.VITE_BACKEND_SSE ?? 'https://sse.arbejd.com/api/',
pageUrl: import.meta.env.VITE_PAGE_URL ?? 'http://localhost:5173/',
stripeKey: import.meta.env.VITE_STRIPE_KEY ?? '',
googleMapsApiKey: import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? '',
firebase: {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY ?? '',
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN ?? '',
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID ?? '',
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET ?? '',
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID ?? '',
appId: import.meta.env.VITE_FIREBASE_APP_ID ?? '',
},
} as const

28
src/index.css Normal file
View File

@@ -0,0 +1,28 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap');
:root {
font-family: 'Inter', sans-serif;
line-height: 1.5;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html,
body,
#root {
height: 100%;
}
body {
margin: 0;
min-width: 320px;
color: #475569;
overflow: hidden;
}
button {
font-family: inherit;
}

10
src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

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();

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