import React, { useEffect, useState } from 'react'; import { inputClass, labelClass } from '../leads/leadUi'; import { useToast } from '../Toast'; import { SCANNER_SETTINGS_DEFAULTS, SECRET_MASK, scannerApi, type ScannerSettings as ScannerSettingsValues, } from '../../lib/scannerApi'; /* * Alle Einstellungen des Projekt Scanners an einer Stelle. Anzeige- und * Benachrichtigungs-Schwellenwert lassen sich zusaetzlich direkt im * Scanner-Dashboard verstellen - es ist derselbe Wert. */ const Section: React.FC<{ title: string; description?: string; children: React.ReactNode; accent?: string; }> = ({ title, description, children, accent }) => (

{title}

{description &&

{description}

}
{children}
); const Checkbox: React.FC<{ checked: boolean; onChange: (checked: boolean) => void; label: string; }> = ({ checked, onChange, label }) => ( ); export const ScannerSettings: React.FC = () => { const { addToast } = useToast(); const [values, setValues] = useState(SCANNER_SETTINGS_DEFAULTS); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); /** Nur Anzeige - gespeichert werden immer Tage. */ const [periodUnit, setPeriodUnit] = useState<'days' | 'weeks'>('weeks'); const load = async () => { setIsLoading(true); try { const data = await scannerApi.settings(); const merged = { ...SCANNER_SETTINGS_DEFAULTS, ...data }; setValues(merged); setPeriodUnit(Number(merged.applied_check_days) % 7 === 0 ? 'weeks' : 'days'); } catch (err) { addToast( err instanceof Error ? err.message : 'Scanner-Einstellungen nicht erreichbar.', 'error' ); } finally { setIsLoading(false); } }; useEffect(() => { load(); // Einmal beim Oeffnen laden - addToast ist stabil, load braucht keine Deps. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const set = (key: K, value: string) => setValues((prev) => ({ ...prev, [key]: value })); const setFlag = (key: keyof ScannerSettingsValues, checked: boolean) => set(key, checked ? 'true' : 'false'); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); setIsSaving(true); try { await scannerApi.saveSettings(values); addToast('Scanner-Einstellungen gespeichert.', 'success'); // Neu laden, damit Geheimnisse wieder maskiert erscheinen. await load(); } catch (err) { addToast(err instanceof Error ? err.message : 'Speichern fehlgeschlagen.', 'error'); } finally { setIsSaving(false); } }; if (isLoading) { return (
); } const isScanningActive = values.scanning_active === 'true'; const emailActive = values.email_notifications_active === 'true'; const keyStored = values.openai_api_key === SECRET_MASK; const appliedCheckActive = values.applied_check_active === 'true'; const periodDays = Math.max(1, Number.parseInt(values.applied_check_days, 10) || 14); const periodFactor = periodUnit === 'weeks' ? 7 : 1; const periodValue = Math.max(1, Math.round(periodDays / periodFactor)); return (
{isScanningActive ? '● Aktiv' : '○ Pausiert'}
setFlag('scanning_active', checked)} label="Scanner aktiviert" />
set('search_interval', event.target.value)} className={inputClass} />

Wie oft der Hintergrund-Scanner die Jobbörsen durchsucht.

set('display_threshold', event.target.value)} className={inputClass} />

Treffer darunter bleiben im Dashboard ausgeblendet.

set('match_threshold', event.target.value)} className={inputClass} />

Ab diesem Wert gibt es Signalton und E-Mail.

setFlag('applied_check_active', checked)} label="Abgleich aktiviert" />
{ const next = Math.max(1, Number.parseInt(event.target.value, 10) || 1); set('applied_check_days', String(Math.min(365, next * periodFactor))); }} className={`${inputClass} w-28`} />

Verglichen wird mit Akquise-Einträgen und Opportunities der letzten {periodDays}{' '} {periodDays === 1 ? 'Tag' : 'Tage'} (Papierkorb ausgenommen).

set('openai_api_key', event.target.value)} placeholder={ keyStored ? 'Leer lassen, um den gespeicherten Key zu behalten' : 'sk-proj-...' } className={inputClass} />
setFlag('sound_notifications_active', checked)} label="Signalton bei neuen Matches" />
setFlag('email_notifications_active', checked)} label="E-Mail bei neuen Matches versenden" /> {emailActive && (
set('notification_email_recipient', event.target.value)} placeholder="info@perinco.de" className={inputClass} />

Wird verwendet, wenn beim Agenten keine eigene E-Mail hinterlegt ist.

set('smtp_from', event.target.value)} placeholder="scanner@perinco.de" className={inputClass} />
set('smtp_host', event.target.value)} placeholder="smtp.deine-domain.de" className={inputClass} />
set('smtp_port', event.target.value)} placeholder="587" className={inputClass} />
set('smtp_user', event.target.value)} className={inputClass} autoComplete="off" />
set('smtp_pass', event.target.value)} placeholder={ values.smtp_pass === SECRET_MASK ? 'Leer lassen, um das gespeicherte Passwort zu behalten' : 'Passwort' } className={inputClass} />
)}
); };