import { apiRequest, getToken } from './api'; /* * Zugriff auf den portierten Projekt Scanner. Alle Endpunkte liegen unter * /api/scanner und haengen an derselben CRM-Anmeldung wie der Rest der App. * Die Feldnamen sind bewusst identisch zur Scanner-Datenbank (camelCase aus * Sequelize) - so bleibt der Umstieg vom alten Frontend nachvollziehbar. */ export interface ScannerAgent { id: number; name: string; email: string | null; /** Kommagetrennte Suchbegriffe */ keywords: string; /** Kommagetrennte Ausschlussbegriffe */ excludeKeywords: string | null; cvFilePath: string | null; isActive: boolean; createdAt?: string; updatedAt?: string; } export type PortalStatus = 'connected' | 'disconnected' | 'expired' | 'error'; export interface ScannerPortal { id: number; name: string; username: string | null; hasPassword: boolean; hasSession: boolean; connectionStatus: PortalStatus; lastLoginAttempt: string | null; updatedAt?: string; } export interface ScannerProject { id: number; portal: string; portalProjectId: string; title: string; description: string | null; company: string | null; location: string | null; rate: string | null; url: string; publishedAt: string | null; /** * Das Project-Modell benennt die Spalte selbst um (`createdAt: 'created_at'`), * deshalb kommt sie als snake_case aus der API. */ created_at?: string; createdAt?: string; } export type MatchStatus = 'new' | 'reviewed' | 'applied' | 'rejected'; export interface ScannerMatch { id: number; agentId: number; projectId: number; matchPercentage: number; pros: string[]; cons: string[]; status: MatchStatus; notificationSent: boolean; createdAt: string; updatedAt?: string; /** Aus dem Join - Sequelize haengt die Modellnamen gross an. */ Agent?: Pick | null; Project?: ScannerProject | null; } /** Alle Werte liegen in der DB als Text - auch Zahlen und Schalter. */ export interface ScannerSettings { scanning_active: string; search_interval: string; openai_api_key: string; match_threshold: string; display_threshold: string; sound_notifications_active: string; email_notifications_active: string; smtp_host: string; smtp_port: string; smtp_user: string; smtp_pass: string; smtp_from: string; notification_email_recipient: string; } export const SCANNER_SETTINGS_DEFAULTS: ScannerSettings = { scanning_active: 'true', search_interval: '60', openai_api_key: '', match_threshold: '70', display_threshold: '50', sound_notifications_active: 'true', email_notifications_active: 'false', smtp_host: '', smtp_port: '587', smtp_user: '', smtp_pass: '', smtp_from: 'scanner@perinco.de', notification_email_recipient: '', }; /** Sentinel des Backends fuer gesetzte Geheimnisse. */ export const SECRET_MASK = '********'; /** Betriebszustand dieser Scanner-Instanz. */ export interface ScannerStatus { /** Ein Suchlauf laeuft gerade (Scraping + Bewertung). */ scanRunning: boolean; /** Hintergrund-Scheduler dieser Instanz aktiv (SCANNER_SCHEDULER). */ schedulerEnabled: boolean; /** Hauptschalter aus den Einstellungen. */ scanningActive: boolean; /** Name der Scanner-Datenbank - beim Parallelbetrieb hilfreich. */ database: string; } export interface AgentInput { name: string; email: string; keywords: string; excludeKeywords: string; isActive: boolean; /** Neues CV-PDF; fehlt es, bleibt das gespeicherte erhalten. */ cv?: File | null; } function agentFormData(input: AgentInput): FormData { const form = new FormData(); form.append('name', input.name); form.append('email', input.email); form.append('keywords', input.keywords); form.append('excludeKeywords', input.excludeKeywords); form.append('isActive', String(input.isActive)); if (input.cv) form.append('cv', input.cv); return form; } export const scannerApi = { // ---------- Agenten ---------- agents: () => apiRequest('/scanner/agents'), createAgent: (input: AgentInput) => apiRequest('/scanner/agents', { method: 'POST', body: agentFormData(input) }), updateAgent: (id: number, input: AgentInput) => apiRequest(`/scanner/agents/${id}`, { method: 'PUT', body: agentFormData(input) }), removeAgent: (id: number) => apiRequest<{ message: string }>(`/scanner/agents/${id}`, { method: 'DELETE' }), // ---------- Portale ---------- portals: () => apiRequest('/scanner/portals'), savePortalCredentials: (input: { name: string; username: string; password: string }) => apiRequest<{ message: string }>('/scanner/portals/credentials', { method: 'POST', body: JSON.stringify(input), }), syncPortalSession: (name: string, sessionState: unknown) => apiRequest<{ message: string; status: PortalStatus }>('/scanner/portals/sync-session', { method: 'POST', body: JSON.stringify({ name, sessionState }), }), testPortal: (id: number) => apiRequest<{ status: PortalStatus; message: string }>( `/scanner/portals/test-connection/${id}`, { method: 'POST' } ), // ---------- Matches ---------- matches: (params: { status?: MatchStatus; agentId?: number; limit?: number } = {}) => { const query = new URLSearchParams(); if (params.status) query.set('status', params.status); if (params.agentId) query.set('agentId', String(params.agentId)); if (params.limit) query.set('limit', String(params.limit)); const suffix = query.toString() ? `?${query}` : ''; return apiRequest(`/scanner/matches${suffix}`); }, match: (id: number) => apiRequest(`/scanner/matches/${id}`), setMatchStatus: (id: number, status: MatchStatus) => apiRequest(`/scanner/matches/${id}`, { method: 'PUT', body: JSON.stringify({ status }), }), removeMatch: (id: number) => apiRequest<{ message: string }>(`/scanner/matches/${id}`, { method: 'DELETE' }), // ---------- Einstellungen ---------- settings: () => apiRequest>('/scanner/settings'), saveSettings: (input: Partial) => apiRequest<{ message: string }>('/scanner/settings', { method: 'POST', body: JSON.stringify(input), }), // ---------- Scan ---------- scan: () => apiRequest<{ message: string }>('/scanner/scan', { method: 'POST' }), status: () => apiRequest('/scanner/status'), }; /** * CV-PDF anzeigen. Ein einfacher Link kaeme ohne Bearer-Token beim Backend an, * deshalb wird die Datei geholt und als Blob in einem neuen Tab geoeffnet. */ export async function openAgentCv(agentId: number): Promise { const token = getToken(); const response = await fetch(`/api/scanner/agents/${agentId}/cv`, { headers: token ? { Authorization: `Bearer ${token}` } : undefined, }); if (!response.ok) throw new Error('CV konnte nicht geladen werden.'); const blob = await response.blob(); const url = URL.createObjectURL(blob); window.open(url, '_blank', 'noopener'); // Der Tab haelt den Blob; nach einer Minute darf der Speicher zurueck. setTimeout(() => URL.revokeObjectURL(url), 60_000); }