import React, { useEffect, useState } from 'react'; import { scannerApi, type ScannerAgent, type ScannerAgentInput, type ScannerOverview, type ScannerSourceId, } from '../../lib/api'; import { useAuth } from '../../lib/auth'; import { useNavigation } from '../../lib/navigation'; import { useToast } from '../Toast'; import { ConfirmDialog, Modal } from '../ui/Modal'; import { Field, PrimaryButton, SecondaryButton, inputClass } from '../ui/form'; import { AlarmBell, SOURCE_LABELS, SOURCE_ORDER, TermInput } from './scannerUi'; /* * Scanner -> Suchagenten. * * Ein Agent = Schlagwörter + Börsen + Ausschlussbegriffe. Jedes Schlagwort * wird in jeder gewählten Börse einzeln gesucht; eine Anzeige, die einen * Ausschlussbegriff enthält, wird gar nicht erst gespeichert. */ const EMPTY: ScannerAgentInput = { name: '', keywords: [], sources: ['ba', 'stepstone', 'karriere'], excludes: [], alarm_keywords: [], is_active: true, }; export const ScannerAgents: React.FC = () => { const { toast } = useToast(); const { isAdmin } = useAuth(); const { goToSettings } = useNavigation(); const [agents, setAgents] = useState(null); const [sources, setSources] = useState([]); const [editing, setEditing] = useState<{ id: number | null; value: ScannerAgentInput } | null>(null); const [removing, setRemoving] = useState(null); const [busy, setBusy] = useState(false); const load = async () => { try { const [{ agents: list }, overview] = await Promise.all([scannerApi.agents(), scannerApi.overview()]); setAgents(list); setSources(overview.sources); } catch (err) { toast(err instanceof Error ? err.message : 'Laden fehlgeschlagen.', 'error'); } }; useEffect(() => { void load(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const ready = (id: ScannerSourceId) => sources.find((item) => item.id === id)?.ready ?? true; const save = async () => { if (!editing) return; setBusy(true); try { if (editing.id) await scannerApi.updateAgent(editing.id, editing.value); else await scannerApi.createAgent(editing.value); toast('Suchagent gespeichert – er sucht ab dem nächsten Lauf mit.', 'success'); setEditing(null); void load(); } catch (err) { toast(err instanceof Error ? err.message : 'Speichern fehlgeschlagen.', 'error'); } finally { setBusy(false); } }; const toggle = async (agent: ScannerAgent) => { try { await scannerApi.updateAgent(agent.id, { ...agent, is_active: !agent.is_active }); void load(); } catch (err) { toast(err instanceof Error ? err.message : 'Ändern fehlgeschlagen.', 'error'); } }; const remove = async () => { if (!removing) return; try { await scannerApi.removeAgent(removing.id); toast('Suchagent gelöscht.', 'success'); setRemoving(null); void load(); } catch (err) { toast(err instanceof Error ? err.message : 'Löschen fehlgeschlagen.', 'error'); } }; const value = editing?.value; const patch = (changes: Partial) => setEditing((current) => (current ? { ...current, value: { ...current.value, ...changes } } : current)); return (

Jeder Agent sucht seine Schlagwörter in den gewählten Börsen. Je mehr Schlagwörter in einer Anzeige vorkommen, desto höher die Quote – ein Treffer im Titel zählt doppelt. Anzeigen mit einem Ausschlussbegriff fallen raus.

setEditing({ id: null, value: EMPTY })}> + Neuer Suchagent
{agents === null ? (

Wird geladen …

) : agents.length === 0 ? (

Noch keine Suchagenten.

) : (
{agents.map((agent) => (

{agent.name}

{agent.is_active ? 'aktiv' : 'pausiert'} · {agent.matches} Treffer

void toggle(agent)}> {agent.is_active ? 'Pausieren' : 'Aktivieren'} setEditing({ id: agent.id, value: { name: agent.name, keywords: agent.keywords, sources: agent.sources, excludes: agent.excludes, alarm_keywords: agent.alarm_keywords, is_active: agent.is_active, }, }) } > Bearbeiten
{agent.keywords.map((term) => ( {term} ))} {agent.alarm_keywords.length > 0 && ( {agent.alarm_keywords.map((term) => ( {term} ))} )} {agent.sources.map((id) => ( {SOURCE_LABELS[id] ?? id} {!ready(id) && ' ⚠'} ))} {agent.excludes.length > 0 && ( {agent.excludes.map((term) => ( {term} ))} )}
))}
)} setEditing(null)} size="md" title={editing?.id ? 'Suchagent bearbeiten' : 'Neuer Suchagent'} footer={ <> setEditing(null)}> Abbrechen void save()} > Speichern } > {value && (
patch({ name: event.target.value })} placeholder="Interim CFO" className={inputClass} /> patch({ keywords })} placeholder="z. B. Interim CFO, Finanzleiter, SAP FI" /> patch({ alarm_keywords })} placeholder="z. B. Interim CFO" tone="alarm" /> patch({ excludes })} placeholder="z. B. Praktikum, Werkstudent, Ausbildung" tone="red" />

Börsen

{SOURCE_ORDER.map((id) => { const info = sources.find((item) => item.id === id); const checked = value.sources.includes(id); return ( ); })}
{sources.some((item) => !item.ready) && (

Adzuna braucht einen kostenlosen API-Schlüssel {isAdmin ? ( <> {' '} –{' '} ) : ( ' (Administrator: Einstellungen → Scanner)' )} . Ohne Schlüssel wird es übersprungen.

)}
)}
void remove()} onCancel={() => setRemoving(null)} />
); }; const Row: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => (

{label}

{children}
);