Initial React project
This commit is contained in:
4250
src/App.css
Normal file
4250
src/App.css
Normal file
File diff suppressed because it is too large
Load Diff
246
src/App.tsx
Normal file
246
src/App.tsx
Normal 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 på 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
1
src/assets/react.svg
Normal 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 |
18
src/environments/environment.ts
Normal file
18
src/environments/environment.ts
Normal 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
28
src/index.css
Normal 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
10
src/main.tsx
Normal 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
27
src/mvvm/README.md
Normal 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, []);
|
||||
```
|
||||
51
src/mvvm/core/async-state.ts
Normal file
51
src/mvvm/core/async-state.ts
Normal 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)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/mvvm/core/http-client.ts
Normal file
120
src/mvvm/core/http-client.ts
Normal 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
3
src/mvvm/core/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './http-client';
|
||||
export * from './async-state';
|
||||
|
||||
4
src/mvvm/index.ts
Normal file
4
src/mvvm/index.ts
Normal 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';
|
||||
4
src/mvvm/models/PaymentIntentResponse.ts
Normal file
4
src/mvvm/models/PaymentIntentResponse.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
interface PaymentIntentResponse {
|
||||
success: boolean;
|
||||
paymentIntent: any; // Du kan også specificere det mere hvis ønsket
|
||||
}
|
||||
29
src/mvvm/models/ai-generated-cv-description.interface.ts
Normal file
29
src/mvvm/models/ai-generated-cv-description.interface.ts
Normal 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;
|
||||
}
|
||||
16
src/mvvm/models/all-language.interface.ts
Normal file
16
src/mvvm/models/all-language.interface.ts
Normal 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?: [];
|
||||
}
|
||||
11
src/mvvm/models/application-examination.interface.ts
Normal file
11
src/mvvm/models/application-examination.interface.ts
Normal 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;
|
||||
}
|
||||
25
src/mvvm/models/applied-job.interface.ts
Normal file
25
src/mvvm/models/applied-job.interface.ts
Normal 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;
|
||||
}
|
||||
6
src/mvvm/models/auth.interface.ts
Normal file
6
src/mvvm/models/auth.interface.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface AuthInterface{
|
||||
id: string;
|
||||
token: string;
|
||||
email: string;
|
||||
runOutDate: string;
|
||||
}
|
||||
18
src/mvvm/models/candidate-application.interface.ts
Normal file
18
src/mvvm/models/candidate-application.interface.ts
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface CandidateRetentionResponseInterface {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
accountClosed: boolean;
|
||||
closedAt: Date;
|
||||
retentionDays: number;
|
||||
}
|
||||
9
src/mvvm/models/candidate-subscription-gift.interface.ts
Normal file
9
src/mvvm/models/candidate-subscription-gift.interface.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface CandidateSubscriptionGiftInterface {
|
||||
id: number;
|
||||
endUserSubscriptionProductName: string;
|
||||
freeDays: number;
|
||||
isActivated: boolean;
|
||||
activatedAt?: string;
|
||||
expiresAt?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
188
src/mvvm/models/candidate.interface.ts
Normal file
188
src/mvvm/models/candidate.interface.ts
Normal 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;
|
||||
}
|
||||
|
||||
19
src/mvvm/models/chat-message-thread.interface.ts
Normal file
19
src/mvvm/models/chat-message-thread.interface.ts
Normal 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;
|
||||
}
|
||||
8
src/mvvm/models/chat-message.interface.ts
Normal file
8
src/mvvm/models/chat-message.interface.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface ChatMessageInterface{
|
||||
id?: string | undefined;
|
||||
threadId: string | undefined;
|
||||
timeSent: Date;
|
||||
fromCandidate: boolean;
|
||||
text: string;
|
||||
seen?: Date | undefined;
|
||||
}
|
||||
4
src/mvvm/models/cv-language.interface.ts
Normal file
4
src/mvvm/models/cv-language.interface.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface CvLanguageInterface {
|
||||
shortCode: string;
|
||||
name: string;
|
||||
}
|
||||
26
src/mvvm/models/cv-suggestion.interface.ts
Normal file
26
src/mvvm/models/cv-suggestion.interface.ts
Normal 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;
|
||||
}
|
||||
104
src/mvvm/models/cv-upload-data.interface.ts
Normal file
104
src/mvvm/models/cv-upload-data.interface.ts
Normal 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}]}"
|
||||
6
src/mvvm/models/driver-license-group.interface.ts
Normal file
6
src/mvvm/models/driver-license-group.interface.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type {DriverLicenseTypeInterface} from "./driver-license-type.interface";
|
||||
|
||||
export interface DriverLicenseGroupInterface{
|
||||
name: string;
|
||||
driverLicenses: DriverLicenseTypeInterface[];
|
||||
}
|
||||
7
src/mvvm/models/driver-license-type.interface.ts
Normal file
7
src/mvvm/models/driver-license-type.interface.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface DriverLicenseTypeInterface{
|
||||
code: string;
|
||||
id: string;
|
||||
name: string;
|
||||
priority: number;
|
||||
special: boolean;
|
||||
}
|
||||
5
src/mvvm/models/education-search.interface.ts
Normal file
5
src/mvvm/models/education-search.interface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface EducationSearchInterface{
|
||||
name: string;
|
||||
disced15: number;
|
||||
levelName: string;
|
||||
}
|
||||
4
src/mvvm/models/error-response.interface.ts
Normal file
4
src/mvvm/models/error-response.interface.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface ErrorResponseInterface{
|
||||
error: string;
|
||||
error_code: number;
|
||||
}
|
||||
5
src/mvvm/models/esco.interface.ts
Normal file
5
src/mvvm/models/esco.interface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface EscoInterface{
|
||||
id: number;
|
||||
preferedLabelDa: string;
|
||||
preferedLabelEu: string;
|
||||
}
|
||||
29
src/mvvm/models/filter-job-search.interface.ts
Normal file
29
src/mvvm/models/filter-job-search.interface.ts
Normal 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;
|
||||
}
|
||||
10
src/mvvm/models/generated-job-application.ts
Normal file
10
src/mvvm/models/generated-job-application.ts
Normal 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
52
src/mvvm/models/index.ts
Normal 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';
|
||||
8
src/mvvm/models/job-agent-filter.interface.ts
Normal file
8
src/mvvm/models/job-agent-filter.interface.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface JobAgentFilterInterface{
|
||||
id: number;
|
||||
escoId: number;
|
||||
escoName: string;
|
||||
isCalculated: boolean;
|
||||
visible: boolean;
|
||||
discoAms08: number;
|
||||
}
|
||||
12
src/mvvm/models/job-application.interface.ts
Normal file
12
src/mvvm/models/job-application.interface.ts
Normal 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;
|
||||
}
|
||||
65
src/mvvm/models/job-detail.interface.ts
Normal file
65
src/mvvm/models/job-detail.interface.ts
Normal 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;
|
||||
}
|
||||
25
src/mvvm/models/job-posting-overview.interface.ts
Normal file
25
src/mvvm/models/job-posting-overview.interface.ts
Normal 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;
|
||||
}
|
||||
9
src/mvvm/models/job-search-container.ts
Normal file
9
src/mvvm/models/job-search-container.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type {JobInterface} from "./job.interface";
|
||||
|
||||
export interface JobSearchContainer{
|
||||
level: number;
|
||||
nextOffset: number;
|
||||
nextLevel: number;
|
||||
numberOfEscos: number;
|
||||
searchList: JobInterface[];
|
||||
}
|
||||
22
src/mvvm/models/job-search-filter.interface.ts
Normal file
22
src/mvvm/models/job-search-filter.interface.ts
Normal 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[];
|
||||
}
|
||||
39
src/mvvm/models/job.interface.ts
Normal file
39
src/mvvm/models/job.interface.ts
Normal 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;
|
||||
}
|
||||
65
src/mvvm/models/jobnet-job-detail.interface.ts
Normal file
65
src/mvvm/models/jobnet-job-detail.interface.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
4
src/mvvm/models/level.interface.ts
Normal file
4
src/mvvm/models/level.interface.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface LevelInterface{
|
||||
id: number;
|
||||
text: string;
|
||||
}
|
||||
3
src/mvvm/models/login.interface.ts
Normal file
3
src/mvvm/models/login.interface.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface LoginInterface{
|
||||
|
||||
}
|
||||
24
src/mvvm/models/notification-setting.interface.ts
Normal file
24
src/mvvm/models/notification-setting.interface.ts
Normal 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[];
|
||||
}
|
||||
|
||||
|
||||
16
src/mvvm/models/notification.interface.ts
Normal file
16
src/mvvm/models/notification.interface.ts
Normal 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;
|
||||
}
|
||||
19
src/mvvm/models/occupation-categorization.interface.ts
Normal file
19
src/mvvm/models/occupation-categorization.interface.ts
Normal 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;
|
||||
}
|
||||
23
src/mvvm/models/occupation.interface.ts
Normal file
23
src/mvvm/models/occupation.interface.ts
Normal 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;
|
||||
}
|
||||
12
src/mvvm/models/payment-overview.interface.ts
Normal file
12
src/mvvm/models/payment-overview.interface.ts
Normal 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;
|
||||
}
|
||||
12
src/mvvm/models/post/save-candidate.interface.ts
Normal file
12
src/mvvm/models/post/save-candidate.interface.ts
Normal 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;
|
||||
}
|
||||
11
src/mvvm/models/post/save-education.interface.ts
Normal file
11
src/mvvm/models/post/save-education.interface.ts
Normal 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;
|
||||
}
|
||||
4
src/mvvm/models/post/upload-cv.interface.ts
Normal file
4
src/mvvm/models/post/upload-cv.interface.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface UploadCvInterface{
|
||||
base_64_cv_file: string;
|
||||
cv_file_type: string;
|
||||
}
|
||||
4
src/mvvm/models/predefined-user-input.interface.ts
Normal file
4
src/mvvm/models/predefined-user-input.interface.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface PredefinedUserInputInterface{
|
||||
id: string;
|
||||
predefinedUserInput: string;
|
||||
}
|
||||
5
src/mvvm/models/qualification-search.interface.ts
Normal file
5
src/mvvm/models/qualification-search.interface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface QualificationSearchInterface{
|
||||
id: string;
|
||||
name: string;
|
||||
type: number;
|
||||
}
|
||||
27
src/mvvm/models/saved-job.interface.ts
Normal file
27
src/mvvm/models/saved-job.interface.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
4
src/mvvm/models/school.interface.ts
Normal file
4
src/mvvm/models/school.interface.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface SchoolInterface{
|
||||
instNumber: number;
|
||||
name: string;
|
||||
}
|
||||
9
src/mvvm/models/search-job.interface.ts
Normal file
9
src/mvvm/models/search-job.interface.ts
Normal 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;
|
||||
}
|
||||
5
src/mvvm/models/searched-certification.interface.ts
Normal file
5
src/mvvm/models/searched-certification.interface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface SearchedCertificationInterface{
|
||||
id: string;
|
||||
name: number;
|
||||
type: string;
|
||||
}
|
||||
4
src/mvvm/models/select-language.interface.ts
Normal file
4
src/mvvm/models/select-language.interface.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface SelectLanguageInterface{
|
||||
name: string;
|
||||
shortName: string;
|
||||
}
|
||||
4
src/mvvm/models/simulation-personality.interface.ts
Normal file
4
src/mvvm/models/simulation-personality.interface.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface SimulationPersonalityInterface {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
14
src/mvvm/models/subscription-product.interface.ts
Normal file
14
src/mvvm/models/subscription-product.interface.ts
Normal 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;
|
||||
}
|
||||
4
src/mvvm/models/translation.interface.ts
Normal file
4
src/mvvm/models/translation.interface.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface TranslationInterface {
|
||||
shortCode: string;
|
||||
text: string;
|
||||
}
|
||||
7
src/mvvm/models/zip-v2.interface.ts
Normal file
7
src/mvvm/models/zip-v2.interface.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface ZipV2Interface{
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
cityName: string;
|
||||
zipCode: number;
|
||||
url: string;
|
||||
}
|
||||
8
src/mvvm/models/zip.interface.ts
Normal file
8
src/mvvm/models/zip.interface.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface ZipInterface{
|
||||
href: string;
|
||||
nr: string;
|
||||
navn: string;
|
||||
visueltcenter: number[];
|
||||
}
|
||||
|
||||
|
||||
85
src/mvvm/services/ai-handler.service.ts
Normal file
85
src/mvvm/services/ai-handler.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
476
src/mvvm/services/audio.service.ts
Normal file
476
src/mvvm/services/audio.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
26
src/mvvm/services/auth.service.ts
Normal file
26
src/mvvm/services/auth.service.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
11
src/mvvm/services/candidate-education.service.ts
Normal file
11
src/mvvm/services/candidate-education.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
11
src/mvvm/services/candidate-experience.service.ts
Normal file
11
src/mvvm/services/candidate-experience.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
23
src/mvvm/services/candidate-search-filter.service.ts
Normal file
23
src/mvvm/services/candidate-search-filter.service.ts
Normal 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, {})
|
||||
}
|
||||
}
|
||||
20
src/mvvm/services/candidate-subscription-gift.service.ts
Normal file
20
src/mvvm/services/candidate-subscription-gift.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
309
src/mvvm/services/candidate.service.ts
Normal file
309
src/mvvm/services/candidate.service.ts
Normal 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 plugin’et 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();
|
||||
53
src/mvvm/services/certification.service.ts
Normal file
53
src/mvvm/services/certification.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
28
src/mvvm/services/chat-messages.service.ts
Normal file
28
src/mvvm/services/chat-messages.service.ts
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
96
src/mvvm/services/cv-upload.service.ts
Normal file
96
src/mvvm/services/cv-upload.service.ts
Normal 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();
|
||||
35
src/mvvm/services/cv.service.ts
Normal file
35
src/mvvm/services/cv.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
102
src/mvvm/services/driver-license.service.ts
Normal file
102
src/mvvm/services/driver-license.service.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
89
src/mvvm/services/education.service.ts
Normal file
89
src/mvvm/services/education.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
22
src/mvvm/services/esco.service.ts
Normal file
22
src/mvvm/services/esco.service.ts
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
18
src/mvvm/services/fireMessagingService.service.ts
Normal file
18
src/mvvm/services/fireMessagingService.service.ts
Normal 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();
|
||||
|
||||
33
src/mvvm/services/google-tag-manager.service.ts
Normal file
33
src/mvvm/services/google-tag-manager.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
36
src/mvvm/services/index.ts
Normal file
36
src/mvvm/services/index.ts
Normal 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';
|
||||
14
src/mvvm/services/institution.service.ts
Normal file
14
src/mvvm/services/institution.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
12
src/mvvm/services/ios-mat-select-fix.service.ts
Normal file
12
src/mvvm/services/ios-mat-select-fix.service.ts
Normal 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();
|
||||
|
||||
20
src/mvvm/services/job-agent.service.ts
Normal file
20
src/mvvm/services/job-agent.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
145
src/mvvm/services/job.service.ts
Normal file
145
src/mvvm/services/job.service.ts
Normal 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();
|
||||
55
src/mvvm/services/language.service.ts
Normal file
55
src/mvvm/services/language.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
73
src/mvvm/services/level.service.ts
Normal file
73
src/mvvm/services/level.service.ts
Normal 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'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
112
src/mvvm/services/local-storage.service.ts
Normal file
112
src/mvvm/services/local-storage.service.ts
Normal 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();
|
||||
11
src/mvvm/services/message.service.ts
Normal file
11
src/mvvm/services/message.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
73
src/mvvm/services/notification.service.ts
Normal file
73
src/mvvm/services/notification.service.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
15
src/mvvm/services/occupation.service.ts
Normal file
15
src/mvvm/services/occupation.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
25
src/mvvm/services/payment.service.ts
Normal file
25
src/mvvm/services/payment.service.ts
Normal 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();
|
||||
25
src/mvvm/services/permissions.service.ts
Normal file
25
src/mvvm/services/permissions.service.ts
Normal 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();
|
||||
|
||||
17
src/mvvm/services/places.service.ts
Normal file
17
src/mvvm/services/places.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
61
src/mvvm/services/qualification.service.ts
Normal file
61
src/mvvm/services/qualification.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
56
src/mvvm/services/simulation.service.ts
Normal file
56
src/mvvm/services/simulation.service.ts
Normal 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();
|
||||
247
src/mvvm/services/sse.service.ts
Normal file
247
src/mvvm/services/sse.service.ts
Normal 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();
|
||||
37
src/mvvm/services/subscription.service.ts
Normal file
37
src/mvvm/services/subscription.service.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
13
src/mvvm/services/toaster.service.ts
Normal file
13
src/mvvm/services/toaster.service.ts
Normal 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
Reference in New Issue
Block a user