Files
Arbejd.com-react/src/presentation/ai-jobagent/pages/AiJobAgentPage.tsx
2026-02-14 10:46:50 +01:00

910 lines
37 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { Sidebar } from '../../layout/components/Sidebar';
import { Topbar } from '../../layout/components/Topbar';
import type { NotificationInterface } from '../../../mvvm/models/notification.interface';
import type { NotificationSettingInterface } from '../../../mvvm/models/notification-setting.interface';
import type { OccupationCategorizationInterface, SubAreaInterface } from '../../../mvvm/models/occupation-categorization.interface';
import type { OccupationInterface } from '../../../mvvm/models/occupation.interface';
import { AiJobAgentViewModel } from '../../../mvvm/viewmodels/AiJobAgentViewModel';
interface AiJobAgentPageProps {
onLogout: () => Promise<void>;
onNavigate: (key: 'dashboard' | 'cv' | 'jobs' | 'beskeder' | 'ai-jobagent' | 'ai-agent' | 'simulator' | 'abonnement') => void;
onOpenJob: (jobId: string, fromJobnet: boolean) => void;
}
type OccupationTree = OccupationCategorizationInterface[];
const PAGE_LIMIT = 20;
function createEmptySetting(): NotificationSettingInterface {
return {
id: null,
jobAgentName: '',
workTimeDay: false,
workTimeEvening: false,
workTimeNight: false,
workTimeWeekend: false,
workTypePermanent: false,
workTypeFreelance: false,
workTypePartTime: false,
workTypeSubstitute: false,
workTypeTemporary: false,
workDistance: 50,
distanceCenterName: '',
latitude: null,
longitude: null,
partTimeHours: null,
notifyOnPush: false,
notifyOnSms: false,
searchText: '',
escoIds: [],
};
}
function normalizeSetting(setting: NotificationSettingInterface): NotificationSettingInterface {
return {
...createEmptySetting(),
...setting,
id: setting.id ?? null,
jobAgentName: setting.jobAgentName ?? '',
distanceCenterName: setting.distanceCenterName ?? '',
searchText: setting.searchText ?? '',
escoIds: Array.isArray(setting.escoIds) ? setting.escoIds : [],
workDistance: typeof setting.workDistance === 'number' ? setting.workDistance : 50,
};
}
function mapTree(source: OccupationCategorizationInterface[]): OccupationTree {
return source.map((area) => ({
...area,
expanded: Boolean(area.expanded),
activated: Boolean(area.activated),
someIsActive: Boolean(area.someIsActive),
subAreas: area.subAreas.map((subArea) => ({
...subArea,
expanded: Boolean(subArea.expanded),
activated: Boolean(subArea.activated),
someIsActive: Boolean(subArea.someIsActive),
occupations: subArea.occupations.map((occupation) => ({
...occupation,
activated: Boolean(occupation.activated),
})),
})),
}));
}
function applySelectedEscos(tree: OccupationTree, escoIds: number[]): OccupationTree {
const selected = new Set(escoIds);
return tree.map((area) => {
const subAreas = area.subAreas.map((subArea) => {
const occupations = subArea.occupations.map((occupation) => ({
...occupation,
activated: selected.has(occupation.id),
}));
const activeCount = occupations.filter((occ) => occ.activated).length;
return {
...subArea,
occupations,
activated: activeCount > 0 && activeCount === occupations.length,
someIsActive: activeCount > 0 && activeCount < occupations.length,
};
});
const allActive = subAreas.length > 0 && subAreas.every((item) => item.activated);
const someActive = subAreas.some((item) => item.activated || item.someIsActive);
return {
...area,
subAreas,
activated: allActive,
someIsActive: someActive && !allActive,
};
});
}
function collectSelectedEscos(tree: OccupationTree): number[] {
const ids: number[] = [];
for (const area of tree) {
for (const subArea of area.subAreas) {
for (const occupation of subArea.occupations) {
if (occupation.activated) {
ids.push(occupation.id);
}
}
}
}
return ids;
}
function formatDate(value: Date | string): string {
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) {
return '';
}
return date.toLocaleDateString('da-DK', {
day: '2-digit',
month: 'short',
year: 'numeric',
});
}
function companyInitial(value: string | null | undefined): string {
if (!value?.trim()) {
return 'Ar';
}
return value.trim().slice(0, 1).toUpperCase();
}
function hasAnyActiveJobAgent(settings: NotificationSettingInterface[]): boolean {
return settings.some((setting) => (setting.escoIds?.length ?? 0) > 0);
}
export function AiJobAgentPage({ onLogout, onNavigate, onOpenJob }: AiJobAgentPageProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => window.localStorage.getItem('arbejd.sidebar.collapsed') === '1');
const [showSettings, setShowSettings] = useState(false);
const [showWorkAreas, setShowWorkAreas] = useState(false);
const [isLoadingSettings, setIsLoadingSettings] = useState(true);
const [isLoadingNotifications, setIsLoadingNotifications] = useState(true);
const [isLoadingMoreNotifications, setIsLoadingMoreNotifications] = useState(false);
const [isSavingSetting, setIsSavingSetting] = useState(false);
const [isSearchingPlaces, setIsSearchingPlaces] = useState(false);
const [hasMoreNotifications, setHasMoreNotifications] = useState(true);
const [error, setError] = useState<string | null>(null);
const [notificationSettings, setNotificationSettings] = useState<NotificationSettingInterface[]>([]);
const [notifications, setNotifications] = useState<NotificationInterface[]>([]);
const [selectedJobAgentId, setSelectedJobAgentId] = useState<number | 'new'>('new');
const [editingSetting, setEditingSetting] = useState<NotificationSettingInterface>(createEmptySetting());
const [occupationTree, setOccupationTree] = useState<OccupationTree>([]);
const [searchOccupationWord, setSearchOccupationWord] = useState('');
const [placeSuggestions, setPlaceSuggestions] = useState<Array<{ place_id?: string; description?: string }>>([]);
const viewModel = useMemo(() => new AiJobAgentViewModel(), []);
useEffect(() => {
let active = true;
async function bootstrap() {
setError(null);
setIsLoadingSettings(true);
setIsLoadingNotifications(true);
try {
const [settings, rawTree, firstNotifications] = await Promise.all([
viewModel.getNotificationSettings(),
viewModel.getOccupationTree(),
viewModel.getNotifications(0, PAGE_LIMIT),
]);
if (!active) {
return;
}
const normalizedSettings = settings.map(normalizeSetting);
const mappedTree = mapTree(rawTree);
const initialSetting = normalizedSettings[0] ?? createEmptySetting();
const initialId = normalizedSettings[0]?.id ?? 'new';
setNotificationSettings(normalizedSettings);
setSelectedJobAgentId(initialId);
setEditingSetting(initialSetting);
setOccupationTree(applySelectedEscos(mappedTree, initialSetting.escoIds));
setNotifications(firstNotifications);
setHasMoreNotifications(firstNotifications.length === PAGE_LIMIT);
} catch (loadError) {
if (!active) {
return;
}
setError(loadError instanceof Error ? loadError.message : 'Kunne ikke indlæse AI JobAgent.');
} finally {
if (active) {
setIsLoadingSettings(false);
setIsLoadingNotifications(false);
}
}
}
void bootstrap();
return () => {
active = false;
};
}, [viewModel]);
useEffect(() => {
const query = editingSetting.distanceCenterName?.trim() ?? '';
if (!showSettings || query.length < 3) {
setPlaceSuggestions([]);
return;
}
const timeout = window.setTimeout(() => {
setIsSearchingPlaces(true);
void viewModel
.searchPlaces(query)
.then((suggestions) => {
setPlaceSuggestions(suggestions);
})
.catch(() => {
setPlaceSuggestions([]);
})
.finally(() => setIsSearchingPlaces(false));
}, 350);
return () => window.clearTimeout(timeout);
}, [editingSetting.distanceCenterName, showSettings, viewModel]);
const filteredOccupations = useMemo(() => {
const query = searchOccupationWord.trim().toLowerCase();
if (!query) {
return [];
}
const list: Array<{ areaCode: number; subAreaCode: number; occupation: OccupationInterface }> = [];
for (const area of occupationTree) {
for (const subArea of area.subAreas) {
for (const occupation of subArea.occupations) {
if (occupation.name.toLowerCase().includes(query)) {
list.push({ areaCode: area.areaCode, subAreaCode: subArea.subAreaCode, occupation });
}
}
}
}
return list.slice(0, 40);
}, [occupationTree, searchOccupationWord]);
const selectedEscoCount = useMemo(() => collectSelectedEscos(occupationTree).length, [occupationTree]);
const unseenCount = useMemo(
() => notifications.filter((notification) => !notification.seenByUser).length,
[notifications],
);
function selectJobAgent(value: number | 'new') {
setSelectedJobAgentId(value);
setShowWorkAreas(false);
setSearchOccupationWord('');
if (value === 'new') {
const empty = createEmptySetting();
setEditingSetting(empty);
setOccupationTree((prev) => applySelectedEscos(prev, []));
return;
}
const setting = notificationSettings.find((item) => item.id === value);
const normalized = normalizeSetting(setting ?? createEmptySetting());
setEditingSetting(normalized);
setOccupationTree((prev) => applySelectedEscos(prev, normalized.escoIds));
}
function updateTreeByArea(areaCode: number, active: boolean) {
setOccupationTree((prev) =>
prev.map((area) => {
if (area.areaCode !== areaCode) {
return area;
}
const subAreas = area.subAreas.map((subArea) => ({
...subArea,
activated: active,
someIsActive: false,
occupations: subArea.occupations.map((occupation) => ({
...occupation,
activated: active,
})),
}));
return {
...area,
subAreas,
activated: active,
someIsActive: false,
};
}),
);
}
function updateTreeBySubArea(areaCode: number, subAreaCode: number, active: boolean) {
setOccupationTree((prev) =>
prev.map((area) => {
if (area.areaCode !== areaCode) {
return area;
}
const subAreas = area.subAreas.map((subArea) => {
if (subArea.subAreaCode !== subAreaCode) {
return subArea;
}
return {
...subArea,
activated: active,
someIsActive: false,
occupations: subArea.occupations.map((occupation) => ({
...occupation,
activated: active,
})),
};
});
const allActive = subAreas.length > 0 && subAreas.every((item) => item.activated);
const someActive = subAreas.some((item) => item.activated || item.someIsActive);
return {
...area,
subAreas,
activated: allActive,
someIsActive: someActive && !allActive,
};
}),
);
}
function updateTreeByOccupation(areaCode: number, subAreaCode: number, occupationId: number) {
setOccupationTree((prev) =>
prev.map((area) => {
if (area.areaCode !== areaCode) {
return area;
}
const subAreas = area.subAreas.map((subArea) => {
if (subArea.subAreaCode !== subAreaCode) {
return subArea;
}
const occupations = subArea.occupations.map((occupation) =>
occupation.id === occupationId
? { ...occupation, activated: !occupation.activated }
: occupation,
);
const activeCount = occupations.filter((occupation) => occupation.activated).length;
return {
...subArea,
occupations,
activated: activeCount > 0 && activeCount === occupations.length,
someIsActive: activeCount > 0 && activeCount < occupations.length,
};
});
const allActive = subAreas.length > 0 && subAreas.every((item) => item.activated);
const someActive = subAreas.some((item) => item.activated || item.someIsActive);
return {
...area,
subAreas,
activated: allActive,
someIsActive: someActive && !allActive,
};
}),
);
}
async function saveSetting() {
if (!editingSetting.jobAgentName?.trim()) {
return;
}
setIsSavingSetting(true);
setError(null);
try {
const payload: NotificationSettingInterface = {
...editingSetting,
jobAgentName: editingSetting.jobAgentName.trim(),
searchText: editingSetting.searchText?.trim() || null,
distanceCenterName: editingSetting.distanceCenterName?.trim() || null,
escoIds: collectSelectedEscos(occupationTree),
};
await viewModel.saveNotificationSetting(selectedJobAgentId, payload);
const settings = (await viewModel.getNotificationSettings()).map(normalizeSetting);
setNotificationSettings(settings);
if (selectedJobAgentId === 'new') {
const created = settings.find((item) => item.jobAgentName === payload.jobAgentName) ?? settings[0];
if (created?.id != null) {
setSelectedJobAgentId(created.id);
setEditingSetting(created);
setOccupationTree((prev) => applySelectedEscos(prev, created.escoIds));
}
} else {
const updated = settings.find((item) => item.id === selectedJobAgentId);
if (updated) {
setEditingSetting(updated);
setOccupationTree((prev) => applySelectedEscos(prev, updated.escoIds));
}
}
setShowSettings(false);
setShowWorkAreas(false);
} catch (saveError) {
setError(saveError instanceof Error ? saveError.message : 'Kunne ikke gemme jobagent indstillinger.');
} finally {
setIsSavingSetting(false);
}
}
async function deleteCurrentSetting() {
if (selectedJobAgentId === 'new') {
return;
}
setIsSavingSetting(true);
setError(null);
try {
await viewModel.deleteNotificationSetting(selectedJobAgentId);
const settings = (await viewModel.getNotificationSettings()).map(normalizeSetting);
setNotificationSettings(settings);
const next = settings[0] ?? createEmptySetting();
setSelectedJobAgentId(settings[0]?.id ?? 'new');
setEditingSetting(next);
setOccupationTree((prev) => applySelectedEscos(prev, next.escoIds));
} catch (deleteError) {
setError(deleteError instanceof Error ? deleteError.message : 'Kunne ikke slette jobagent.');
} finally {
setIsSavingSetting(false);
}
}
async function selectPlaceSuggestion(placeId: string) {
const details = await viewModel.getPlaceDetails(placeId);
if (!details) {
return;
}
setEditingSetting((prev) => ({
...prev,
distanceCenterName: details.address,
latitude: details.latitude,
longitude: details.longitude,
}));
setPlaceSuggestions([]);
}
async function openNotificationJob(notification: NotificationInterface) {
if (!notification.seenByUser) {
void viewModel.markNotificationSeen(notification.id);
setNotifications((prev) =>
prev.map((item) => (item.id === notification.id ? { ...item, seenByUser: true } : item)),
);
}
const fromJobnet = Boolean(notification.jobnetPostingId);
const jobId = fromJobnet ? notification.jobnetPostingId : notification.jobPostingId;
if (jobId) {
onOpenJob(jobId, fromJobnet);
}
}
async function toggleNotificationBookmark(notification: NotificationInterface) {
const save = !notification.saved;
try {
await viewModel.toggleNotificationBookmark(notification, save);
setNotifications((prev) =>
prev.map((item) => (item.id === notification.id ? { ...item, saved: save } : item)),
);
} catch {
setError('Kunne ikke opdatere gemt status på notifikation.');
}
}
async function loadMoreNotifications() {
if (!hasMoreNotifications || isLoadingMoreNotifications) {
return;
}
setIsLoadingMoreNotifications(true);
setError(null);
try {
const next = await viewModel.getNotifications(notifications.length, PAGE_LIMIT);
setNotifications((prev) => [...prev, ...next]);
setHasMoreNotifications(next.length === PAGE_LIMIT);
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : 'Kunne ikke indlæse flere notifikationer.');
} finally {
setIsLoadingMoreNotifications(false);
}
}
return (
<section className="dashboard-layout">
<Sidebar
collapsed={sidebarCollapsed}
activeKey="ai-jobagent"
onToggle={() => setSidebarCollapsed((prev) => { const next = !prev; window.localStorage.setItem('arbejd.sidebar.collapsed', next ? '1' : '0'); return next; })}
onSelect={(key) => {
if (key === 'dashboard' || key === 'cv' || key === 'jobs' || key === 'beskeder' || key === 'ai-jobagent' || key === 'ai-agent' || key === 'simulator' || key === 'abonnement') {
onNavigate(key);
}
}}
/>
<main className="dashboard-main">
<Topbar title="AI JobAgent" userName="Anders Jensen" planLabel="Jobseeker Pro" onLogout={onLogout} />
<div className="dashboard-scroll">
{error ? <p className="status error">{error}</p> : null}
<article className="glass-panel dash-card jobagent-hero-card">
<div className="jobagent-hero-top">
<div>
<span className="jobagent-kicker">AI JobAgent</span>
<h3>Automatisk jobmatch med dit CV</h3>
<p>
Jobagenten følger nye opslag og fremhæver relevante job baseret dine valgte områder,
arbejdstype og afstand.
</p>
</div>
<button
type="button"
className="primary-btn"
onClick={() => setShowSettings((prev) => !prev)}
disabled={isLoadingSettings}
>
{showSettings ? 'Luk indstillinger' : 'Åbn indstillinger'}
</button>
</div>
{!isLoadingSettings ? (
<div className="jobagent-hero-metrics">
<div className="jobagent-metric-pill">
<span>Jobagenter</span>
<strong>{notificationSettings.length}</strong>
</div>
<div className="jobagent-metric-pill">
<span>Aktive filtre</span>
<strong>{selectedEscoCount}</strong>
</div>
<div className="jobagent-metric-pill">
<span>Nye notifikationer</span>
<strong>{unseenCount}</strong>
</div>
<p className="jobagent-summary-note">
{hasAnyActiveJobAgent(notificationSettings)
? 'Mindst én jobagent er aktiv.'
: 'Vælg stillingstyper og områder for at aktivere en jobagent.'}
</p>
</div>
) : (
<p className="jobagent-summary-note">Indlæser jobagenter...</p>
)}
</article>
<section className="jobagent-layout-grid">
{showSettings ? (
<article className="glass-panel dash-card jobagent-settings-card">
<div className="jobagent-settings-top">
<div>
<span className="jobagent-kicker">Opsaetning</span>
<h4>Jobagent indstillinger</h4>
</div>
<div className="jobagent-settings-actions">
{selectedJobAgentId !== 'new' ? (
<button type="button" className="secondary-btn danger" onClick={() => void deleteCurrentSetting()} disabled={isSavingSetting}>
Slet
</button>
) : null}
<button type="button" className="primary-btn" onClick={() => void saveSetting()} disabled={isSavingSetting || !editingSetting.jobAgentName?.trim()}>
{isSavingSetting ? 'Gemmer...' : 'Gem'}
</button>
</div>
</div>
<div className="jobagent-settings-grid">
<label className="jobagent-field">
<span>Vælg jobagent</span>
<select
className="field-input"
value={selectedJobAgentId === 'new' ? 'new' : String(selectedJobAgentId)}
onChange={(event) => {
const value = event.target.value;
selectJobAgent(value === 'new' ? 'new' : Number(value));
}}
>
{notificationSettings.map((setting) => (
<option key={setting.id ?? Math.random()} value={String(setting.id)}>
{setting.jobAgentName?.trim() || 'Uden navn'}
</option>
))}
<option value="new">Opret ny jobagent</option>
</select>
</label>
<label className="jobagent-field">
<span>Jobagent navn</span>
<input
className="field-input"
value={editingSetting.jobAgentName ?? ''}
onChange={(event) => setEditingSetting((prev) => ({ ...prev, jobAgentName: event.target.value }))}
placeholder="Fx. Min jobagent"
/>
</label>
<label className="jobagent-field">
<span>Søgetekst</span>
<input
className="field-input"
value={editingSetting.searchText ?? ''}
onChange={(event) => setEditingSetting((prev) => ({ ...prev, searchText: event.target.value }))}
placeholder="Fritekst til søgning"
/>
</label>
<div className="jobagent-field">
<span>Arbejdsområder</span>
<button type="button" className="secondary-btn" onClick={() => setShowWorkAreas((prev) => !prev)}>
{showWorkAreas ? 'Luk arbejdsområder' : 'Åbn arbejdsområder'}
</button>
</div>
<div className="jobagent-field">
<span>Arbejdstype</span>
<div className="jobagent-toggle-row">
<button
type="button"
className={editingSetting.workTypePermanent ? 'tab-btn active' : 'tab-btn'}
onClick={() => setEditingSetting((prev) => ({ ...prev, workTypePermanent: !prev.workTypePermanent }))}
>
Fast
</button>
<button
type="button"
className={editingSetting.workTypePartTime ? 'tab-btn active' : 'tab-btn'}
onClick={() => setEditingSetting((prev) => ({ ...prev, workTypePartTime: !prev.workTypePartTime }))}
>
Deltid
</button>
</div>
{editingSetting.workTypePartTime ? (
<input
type="number"
min={0}
max={37}
className="field-input"
value={editingSetting.partTimeHours ?? ''}
onChange={(event) =>
setEditingSetting((prev) => ({
...prev,
partTimeHours: event.target.value ? Number(event.target.value) : null,
}))
}
placeholder="Timer pr. uge"
/>
) : null}
</div>
<div className="jobagent-field jobagent-location-field">
<span>Center for afstand</span>
<input
className="field-input"
value={editingSetting.distanceCenterName ?? ''}
onChange={(event) =>
setEditingSetting((prev) => ({
...prev,
distanceCenterName: event.target.value,
latitude: null,
longitude: null,
}))
}
placeholder="Søg adresse"
/>
{isSearchingPlaces ? <small>Søger adresser...</small> : null}
{placeSuggestions.length > 0 ? (
<div className="jobagent-place-suggestions glass-panel">
{placeSuggestions.map((suggestion) => (
<button
key={`${suggestion.place_id}-${suggestion.description}`}
type="button"
onClick={() => suggestion.place_id && void selectPlaceSuggestion(suggestion.place_id)}
>
{suggestion.description}
</button>
))}
</div>
) : null}
</div>
<div className="jobagent-field jobagent-distance-field">
<span>Arbejdsafstand: {editingSetting.workDistance ?? 50} km</span>
<input
type="range"
min={0}
max={500}
value={editingSetting.workDistance ?? 50}
onChange={(event) => setEditingSetting((prev) => ({ ...prev, workDistance: Number(event.target.value) }))}
/>
</div>
</div>
{showWorkAreas ? (
<section className="jobagent-workareas-card">
<div className="jobagent-workareas-top">
<h5>Arbejdsområder</h5>
<span>{selectedEscoCount} valgt</span>
</div>
<div className="jobagent-workareas">
<input
className="field-input"
value={searchOccupationWord}
onChange={(event) => setSearchOccupationWord(event.target.value)}
placeholder="Søg arbejdsområde"
/>
{searchOccupationWord.trim() && filteredOccupations.length > 0 ? (
<div className="jobagent-workareas-search-list">
{filteredOccupations.map((entry) => (
<label key={`${entry.areaCode}-${entry.subAreaCode}-${entry.occupation.id}`} className="jobagent-checkline">
<input
type="checkbox"
checked={Boolean(entry.occupation.activated)}
onChange={() => updateTreeByOccupation(entry.areaCode, entry.subAreaCode, entry.occupation.id)}
/>
<span>{entry.occupation.name}</span>
</label>
))}
</div>
) : (
<div className="jobagent-workareas-tree">
{occupationTree.map((area) => (
<div key={area.areaCode} className="jobagent-workarea-node">
<div className="jobagent-workarea-row">
<button type="button" className="jobagent-expand-btn" onClick={() =>
setOccupationTree((prev) => prev.map((item) => item.areaCode === area.areaCode ? { ...item, expanded: !item.expanded } : item))
}>
{area.expanded ? '▾' : '▸'}
</button>
<label className="jobagent-checkline">
<input
type="checkbox"
checked={area.activated || area.someIsActive}
onChange={() => updateTreeByArea(area.areaCode, !(area.activated || area.someIsActive))}
/>
<span>{area.areaName}</span>
</label>
</div>
{area.expanded ? (
<div className="jobagent-workarea-children">
{area.subAreas.map((subArea: SubAreaInterface) => (
<div key={`${area.areaCode}-${subArea.subAreaCode}`} className="jobagent-workarea-node">
<div className="jobagent-workarea-row">
<button type="button" className="jobagent-expand-btn" onClick={() =>
setOccupationTree((prev) =>
prev.map((item) =>
item.areaCode === area.areaCode
? {
...item,
subAreas: item.subAreas.map((child) =>
child.subAreaCode === subArea.subAreaCode ? { ...child, expanded: !child.expanded } : child,
),
}
: item,
),
)
}>
{subArea.expanded ? '▾' : '▸'}
</button>
<label className="jobagent-checkline">
<input
type="checkbox"
checked={subArea.activated || subArea.someIsActive}
onChange={() => updateTreeBySubArea(area.areaCode, subArea.subAreaCode, !(subArea.activated || subArea.someIsActive))}
/>
<span>{subArea.subAreaName}</span>
</label>
</div>
{subArea.expanded ? (
<div className="jobagent-workarea-children">
{subArea.occupations.map((occupation) => (
<label key={occupation.id} className="jobagent-checkline">
<input
type="checkbox"
checked={Boolean(occupation.activated)}
onChange={() => updateTreeByOccupation(area.areaCode, subArea.subAreaCode, occupation.id)}
/>
<span>{occupation.name}</span>
</label>
))}
</div>
) : null}
</div>
))}
</div>
) : null}
</div>
))}
</div>
)}
</div>
</section>
) : null}
</article>
) : null}
<article className="glass-panel dash-card jobagent-notifications-card">
<div className="jobagent-notifications-top">
<div>
<span className="jobagent-kicker">Live feed</span>
<h4>Notifikationer</h4>
</div>
<span className="jobagent-notification-count">{notifications.length}</span>
</div>
{isLoadingNotifications ? <p>Indlæser notifikationer...</p> : null}
{!isLoadingNotifications && notifications.length === 0 ? (
<p>Ingen notifikationer endnu.</p>
) : null}
<div className="jobagent-notification-list">
<div className="jobs-results-grid jobagent-results-grid">
{notifications.map((notification) => (
<article
key={notification.id}
className={notification.seenByUser ? 'glass-panel jobs-result-card jobagent-result-card' : 'glass-panel jobs-result-card jobagent-result-card unseen'}
role="button"
tabIndex={0}
onClick={() => void openNotificationJob(notification)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
void openNotificationJob(notification);
}
}}
>
<div className="jobs-result-top">
<div className="jobs-result-brand">
{notification.logoUrl ? (
<img className="jobs-result-logo-img" src={notification.logoUrl} alt={notification.companyName || 'Virksomhed'} />
) : (
<div className="jobs-result-logo">{companyInitial(notification.companyName)}</div>
)}
<div>
<p className="jobs-result-company">{notification.companyName || 'Ukendt virksomhed'}</p>
<p className="jobs-result-address">
{notification.city ? `${notification.city} ${notification.zip || ''}` : 'Ukendt lokation'}
</p>
</div>
</div>
<button
type="button"
className={notification.saved ? 'jobs-bookmark-icon active' : 'jobs-bookmark-icon'}
onMouseDown={(event) => event.stopPropagation()}
onClickCapture={(event) => event.stopPropagation()}
onClick={() => void toggleNotificationBookmark(notification)}
aria-label={notification.saved ? 'Fjern gemt job' : 'Gem job'}
title={notification.saved ? 'Fjern gemt' : 'Gem job'}
>
{notification.saved ? '★' : '☆'}
</button>
</div>
<h5 className="jobs-result-title">{notification.jobTitle || 'Jobagent match'}</h5>
<p className="jobs-result-occupation">{notification.escoTitle || 'AI JobAgent forslag'}</p>
<p className="jobs-result-description">
{notification.seenByUser ? 'Match fra din jobagent baseret på dine valgte filtre.' : 'Nyt match fra din jobagent.'}
</p>
<div className="jobs-result-tags">
<span className="chip">{formatDate(notification.notificationDate)}</span>
{notification.distance ? <span className="chip">{Number(notification.distance).toFixed(1)} km</span> : null}
<span className="chip">{notification.seenByUser ? 'Set' : 'Ny'}</span>
</div>
<div className="jobs-result-footer">
<button
type="button"
className="primary-btn jobs-card-primary-btn"
onClick={(event) => {
event.stopPropagation();
void openNotificationJob(notification);
}}
>
Åbn job
</button>
</div>
</article>
))}
</div>
</div>
{hasMoreNotifications ? (
<button type="button" className="secondary-btn jobagent-load-more" onClick={() => void loadMoreNotifications()} disabled={isLoadingMoreNotifications}>
{isLoadingMoreNotifications ? 'Indlæser...' : 'Indlæs flere'}
</button>
) : null}
</article>
</section>
</div>
</main>
</section>
);
}