Initial React project

This commit is contained in:
Johan
2026-03-03 00:56:54 +01:00
parent 6c1f178ba9
commit 20370144fb
4012 changed files with 287867 additions and 9843 deletions

View File

@@ -20,6 +20,11 @@ export interface AiAgentInitialData {
escos: EscoInterface[];
}
interface UserProfilePreview {
imageUrl?: string;
name: string;
}
export class AiAgentViewModel {
constructor(
private candidateService: CandidateService = new CandidateService(),
@@ -44,6 +49,17 @@ export class AiAgentViewModel {
};
}
async getCandidateProfile(): Promise<UserProfilePreview> {
try {
const candidate = await this.candidateService.getCandidate();
const name = candidate.firstName?.trim() || candidate.name?.trim() || 'Lasse';
const imageUrl = candidate.imageUrl || candidate.image || undefined;
return { name, imageUrl };
} catch {
return { name: 'Lasse' };
}
}
async addEscoToFilter(escoId: number): Promise<void> {
await this.jobAgentService.addEscoToJobAgent(escoId);
}

View File

@@ -1,4 +1,5 @@
import { CandidateSearchFilterService } from '../services/candidate-search-filter.service';
import { CandidateService } from '../services/candidate.service';
import { JobService } from '../services/job.service';
import { PlacesService } from '../services/places.service';
import type { AppliedJobInterface } from '../models/applied-job.interface';
@@ -54,6 +55,16 @@ export interface PlaceSelection {
longitude: number | null;
}
export interface JobsSearchQuery {
desiredTitles?: string[];
searchText?: string;
}
interface UserProfilePreview {
imageUrl?: string;
name: string;
}
const DEFAULT_FILTER: JobsFilterDraft = {
escoIds: [],
workTypePermanent: false,
@@ -118,6 +129,31 @@ function toNumber(source: Record<string, unknown> | null, key: string): number |
return typeof value === 'number' ? value : null;
}
function normalizeTerm(value: string): string {
return value.trim();
}
function buildTerms(query?: JobsSearchQuery): string[] {
if (!query) {
return [];
}
const terms = new Set<string>();
const searchText = query.searchText?.trim();
if (searchText && searchText.length > 0) {
terms.add(searchText);
}
for (const item of query.desiredTitles ?? []) {
const normalized = normalizeTerm(item);
if (normalized.length > 0) {
terms.add(normalized);
}
}
return Array.from(terms);
}
function postingToListItem(posting: JobPostingInterface, matchPercent?: number): JobsListItem {
return {
id: normalizeText(posting.id),
@@ -169,8 +205,20 @@ export class JobsPageViewModel {
private jobService: JobService = new JobService(),
private filterService: CandidateSearchFilterService = new CandidateSearchFilterService(),
private placesService: PlacesService = new PlacesService(),
private candidateService: CandidateService = new CandidateService(),
) {}
async getCandidateProfile(): Promise<UserProfilePreview> {
try {
const candidate = await this.candidateService.getCandidate();
const name = candidate.firstName?.trim() || candidate.name?.trim() || 'Lasse';
const imageUrl = candidate.imageUrl || candidate.image || undefined;
return { name, imageUrl };
} catch {
return { name: 'Lasse' };
}
}
async getOccupationOptions(): Promise<OccupationOption[]> {
const categorizations: OccupationCategorizationInterface[] = await this.jobService.getOccupationCategorizations();
const options: OccupationOption[] = [];
@@ -300,7 +348,13 @@ export class JobsPageViewModel {
return appliedArray.map((job) => savedOrAppliedToListItem(job as AppliedJobInterface));
}
return this.getJobsFeedItems(searchTerm);
return this.getJobsFeedItems(searchTerm ? [searchTerm] : undefined);
}
async applyFiltersAndGetJobs(filter: JobsFilterDraft, query?: JobsSearchQuery): Promise<JobsListItem[]> {
await this.saveFilter(filter);
const terms = buildTerms(query);
return this.getJobsFeedItems(terms);
}
async toggleBookmark(item: Pick<JobsListItem, 'id' | 'fromJobnet'>, save: boolean): Promise<void> {
@@ -308,7 +362,7 @@ export class JobsPageViewModel {
await this.jobService.bookmarkJobV2(item.id, save, jobType);
}
private async getJobsFeedItems(searchTerm?: string): Promise<JobsListItem[]> {
private async getJobsFeedItems(preferredTerms?: string[]): Promise<JobsListItem[]> {
const limit = 20;
let level = 10;
let offset = 0;
@@ -317,10 +371,12 @@ export class JobsPageViewModel {
const seenIds = new Set<string>();
const collected: JobsListItem[] = [];
const trimmedSearch = searchTerm?.trim() ?? '';
let searchWords: string[] = [];
if (trimmedSearch.length > 0) {
searchWords = [trimmedSearch];
const terms = (preferredTerms ?? [])
.map((value) => value.trim())
.filter((value) => value.length > 0);
if (terms.length > 0) {
searchWords = terms;
} else {
try {
const words = await this.jobService.getSearchWords();

View File

@@ -1,5 +1,6 @@
import type { ChatMessageInterface } from '../models/chat-message.interface';
import type { ChatMessageThreadInterface } from '../models/chat-message-thread.interface';
import { CandidateService } from '../services/candidate.service';
import { ChatMessagesService } from '../services/chat-messages.service';
import { MessageService } from '../services/message.service';
@@ -8,6 +9,11 @@ export interface MessageThreadItem extends ChatMessageThreadInterface {
latestMessage: ChatMessageInterface;
}
interface UserProfilePreview {
imageUrl?: string;
name: string;
}
function toMillis(value?: Date | string): number {
if (!value) {
return 0;
@@ -35,8 +41,20 @@ export class MessagesViewModel {
constructor(
private readonly chatMessagesService: ChatMessagesService = new ChatMessagesService(),
private readonly messageService: MessageService = new MessageService(),
private readonly candidateService: CandidateService = new CandidateService(),
) {}
async getCandidateProfile(): Promise<UserProfilePreview> {
try {
const candidate = await this.candidateService.getCandidate();
const name = candidate.firstName?.trim() || candidate.name?.trim() || 'Lasse';
const imageUrl = candidate.imageUrl || candidate.image || undefined;
return { name, imageUrl };
} catch {
return { name: 'Lasse' };
}
}
async getThreads(): Promise<MessageThreadItem[]> {
const threads = await this.chatMessagesService.getChatMessages();