import { useEffect, useState } from 'react' import { Plus, Trash2, X } from 'lucide-react' import { api } from '../lib/api' import type { Account, Folder, MatchType, NewRule, Rule, RuleAction, RuleCondition } from '../types' const inputClass = 'rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-[13px] text-zinc-800 outline-none transition focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-100 dark:focus:ring-indigo-500/20' function Field({ label, children }: { label: string; children: React.ReactNode }) { return ( {label} {children} ) } function conditionsSummary(conditions: RuleCondition[], matchType: MatchType) { const parts = conditions.map((c) => `${c.field === 'subject' ? 'Betreff' : 'Absender'} enthält „${c.contains}“`) const joiner = matchType === 'all' ? ' UND ' : ' ODER ' return parts.join(joiner) } function actionLabel(r: Rule, accountFolders: Record) { switch (r.action) { case 'move_to': { const folders = r.account_id ? accountFolders[r.account_id] ?? [] : [] const folder = folders.find((f) => f.name === r.action_value) return `verschieben nach „${folder?.display_name ?? r.action_value}“` } case 'mark_read': return 'als gelesen markieren' case 'flag': return 'markieren' case 'delete': return 'löschen' } } const emptyCondition = (): RuleCondition => ({ field: 'from', contains: '' }) export function RulesModal({ open, onClose, accounts, accountFolders, onToast, }: { open: boolean onClose: () => void accounts: Account[] accountFolders: Record onToast: (msg: string) => void }) { const [rules, setRules] = useState([]) const [loading, setLoading] = useState(false) const [accountId, setAccountId] = useState('') const [conditions, setConditions] = useState([emptyCondition()]) const [matchType, setMatchType] = useState('all') const [action, setAction] = useState('move_to') const [actionValue, setActionValue] = useState('') const [saving, setSaving] = useState(false) const ruleAccounts = accounts.filter((a) => a.protocol === 'imap' || a.protocol === 'pop3') const targetFolders = accountId ? accountFolders[accountId] ?? [] : [] const validConditions = conditions.filter((c) => c.contains.trim()) const isPop3 = accounts.find((a) => a.id === accountId)?.protocol === 'pop3' function refresh() { setLoading(true) api .listRules() .then(setRules) .catch((e) => onToast(`Regeln konnten nicht geladen werden: ${e}`)) .finally(() => setLoading(false)) } useEffect(() => { if (open) refresh() // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]) if (!open) return null function updateCondition(index: number, patch: Partial) { setConditions((prev) => prev.map((c, i) => (i === index ? { ...c, ...patch } : c))) } function addCondition() { setConditions((prev) => [...prev, emptyCondition()]) } function removeCondition(index: number) { setConditions((prev) => (prev.length > 1 ? prev.filter((_, i) => i !== index) : prev)) } async function handleAdd(e: React.FormEvent) { e.preventDefault() if (validConditions.length === 0) return if (action === 'move_to' && !actionValue) return setSaving(true) try { const newRule: NewRule = { account_id: accountId || null, conditions: validConditions.map((c) => ({ field: c.field, contains: c.contains.trim() })), match_type: matchType, action, action_value: action === 'move_to' ? actionValue : undefined, } await api.createRule(newRule) setConditions([emptyCondition()]) setMatchType('all') setActionValue('') refresh() onToast('Regel erstellt') } catch (err) { onToast(`Fehlgeschlagen: ${err}`) } finally { setSaving(false) } } async function handleToggle(r: Rule) { setRules((prev) => prev.map((x) => (x.id === r.id ? { ...x, enabled: !x.enabled } : x))) try { await api.setRuleEnabled(r.id, !r.enabled) } catch (err) { onToast(String(err)) } } async function handleDelete(r: Rule) { setRules((prev) => prev.filter((x) => x.id !== r.id)) try { await api.deleteRule(r.id) } catch (err) { onToast(String(err)) } } return ( Regeln verwalten Neue Regel { setAccountId(e.target.value) const nowPop3 = accounts.find((a) => a.id === e.target.value)?.protocol === 'pop3' if (nowPop3 && action === 'move_to') setAction('mark_read') }} > Alle Konten {ruleAccounts.map((a) => ( {a.label} {a.protocol === 'pop3' ? ' (POP3)' : ''} ))} Bedingungen {conditions.length > 1 && ( setMatchType('all')} className={`rounded-md px-2 py-0.5 transition ${matchType === 'all' ? 'bg-white font-medium text-zinc-800 shadow-sm dark:bg-zinc-700 dark:text-zinc-100' : 'text-zinc-500'}`} > Alle (UND) setMatchType('any')} className={`rounded-md px-2 py-0.5 transition ${matchType === 'any' ? 'bg-white font-medium text-zinc-800 shadow-sm dark:bg-zinc-700 dark:text-zinc-100' : 'text-zinc-500'}`} > Eine (ODER) )} {conditions.map((c, i) => ( updateCondition(i, { field: e.target.value })} > Absender Betreff updateCondition(i, { contains: e.target.value })} placeholder="z. B. newsletter@ oder Rechnung" /> removeCondition(i)} disabled={conditions.length === 1} className="shrink-0 rounded-lg p-1.5 text-zinc-400 transition hover:bg-red-50 hover:text-red-600 disabled:opacity-30 dark:hover:bg-red-950/40" > ))} Bedingung hinzufügen setAction(e.target.value as RuleAction)}> {!isPop3 && In Ordner verschieben} Als gelesen markieren Markieren Löschen {isPop3 && ( POP3 hat keine Server-Ordner, „Verschieben“ ist daher nicht verfügbar. )} {action === 'move_to' && ( setActionValue(e.target.value)} disabled={!accountId} > {accountId ? 'Ordner wählen…' : 'Erst Konto wählen'} {targetFolders.map((f) => ( {f.display_name} ))} )} {saving ? 'Speichere…' : 'Regel hinzufügen'} Aktive Regeln {loading ? ( Lade… ) : rules.length === 0 ? ( Noch keine Regeln angelegt. ) : ( {rules.map((r) => ( handleToggle(r)} className="rounded border-zinc-300" /> {conditionsSummary(r.conditions, r.match_type)} {' → '} {actionLabel(r, accountFolders)} {r.account_id && ( {' '} ({accounts.find((a) => a.id === r.account_id)?.label ?? '?'}) )} handleDelete(r)} className="shrink-0 rounded-full p-1 text-zinc-400 transition hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-950/40" > ))} )} ) }
Neue Regel
POP3 hat keine Server-Ordner, „Verschieben“ ist daher nicht verfügbar.
Aktive Regeln
Lade…
Noch keine Regeln angelegt.