"use client"; import { useState, useEffect, useCallback } from "react"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { getRoutingRules, createRoutingRule, updateRoutingRule, deleteRoutingRule, dryRunRoutingRule, getTenants, type RoutingRule, type RoutingRuleInput, type RoutingMatchType, type RoutingDryRunResult, type Tenant, } from "@/lib/api"; const MATCH_LABELS: Record = { from_domain: "Absender-Domain", to_domain: "Empfänger-Domain", from_addr: "Absender-Adresse", to_addr: "Empfänger-Adresse", }; const MATCH_PLACEHOLDER: Record = { from_domain: "z.B. kunde.de oder *.kunde.de", to_domain: "z.B. firma.de", from_addr: "z.B. buchhaltung@kunde.de", to_addr: "z.B. archiv@firma.de", }; interface EditState { id: number | null; // null = create tenant_id: string; // "" = noch nicht gewählt (nur superadmin relevant) match_type: RoutingMatchType; pattern: string; priority: string; } const EMPTY_EDIT: EditState = { id: null, tenant_id: "", match_type: "from_domain", pattern: "", priority: "0", }; function formatDate(value: string | null): string { if (!value) return "–"; const d = new Date(value); if (isNaN(d.getTime())) return value; return d.toLocaleString("de-DE", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }); } export function RoutingRulesTab({ isSuperAdmin }: { isSuperAdmin: boolean }) { const [rules, setRules] = useState([]); const [tenants, setTenants] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [edit, setEdit] = useState(null); const [saving, setSaving] = useState(false); const [formError, setFormError] = useState(""); // Dry-Run innerhalb des Anlege-/Bearbeiten-Dialogs const [dryRunning, setDryRunning] = useState(false); const [dryRunResult, setDryRunResult] = useState(null); const [dryRunError, setDryRunError] = useState(""); const [deleteRule, setDeleteRule] = useState(null); const [deleting, setDeleting] = useState(false); const load = useCallback(() => { setLoading(true); setError(""); const loaders: [Promise, Promise] = [ getRoutingRules(), isSuperAdmin ? getTenants() : Promise.resolve([]), ]; Promise.all(loaders) .then(([rs, ts]) => { setRules(rs); setTenants(ts); }) .catch(() => setError("Routing-Regeln konnten nicht geladen werden")) .finally(() => setLoading(false)); }, [isSuperAdmin]); useEffect(() => { load(); }, [load]); const tenantName = (id: number): React.ReactNode => { const t = tenants.find((x) => x.id === id); return t ? t.name : `#${id}`; }; const resetDryRun = () => { setDryRunResult(null); setDryRunError(""); }; const openCreate = () => { setEdit({ ...EMPTY_EDIT }); setFormError(""); resetDryRun(); }; const openEdit = (r: RoutingRule) => { setEdit({ id: r.id, tenant_id: String(r.tenant_id), match_type: r.match_type, pattern: r.pattern, priority: String(r.priority), }); setFormError(""); resetDryRun(); }; const buildInput = (): RoutingRuleInput | null => { if (!edit) return null; const pattern = edit.pattern.trim(); if (!pattern) { setFormError("Muster darf nicht leer sein"); return null; } const priority = parseInt(edit.priority, 10); if (isNaN(priority)) { setFormError("Priorität muss eine Zahl sein"); return null; } const input: RoutingRuleInput = { match_type: edit.match_type, pattern, priority, }; if (isSuperAdmin) { if (edit.tenant_id === "") { setFormError("Bitte einen Mandanten auswählen"); return null; } input.tenant_id = parseInt(edit.tenant_id, 10); } return input; }; const handleSave = async () => { if (!edit) return; setFormError(""); const input = buildInput(); if (!input) return; setSaving(true); try { if (edit.id === null) { await createRoutingRule(input); } else { await updateRoutingRule(edit.id, input); } setEdit(null); load(); } catch (e: unknown) { setFormError(e instanceof Error ? e.message : "Speichern fehlgeschlagen"); } finally { setSaving(false); } }; const handleDryRun = async () => { if (!edit) return; setDryRunError(""); setDryRunResult(null); const pattern = edit.pattern.trim(); if (!pattern) { setDryRunError("Muster darf nicht leer sein"); return; } setDryRunning(true); try { const res = await dryRunRoutingRule({ match_type: edit.match_type, pattern, }); setDryRunResult(res); } catch (e: unknown) { setDryRunError(e instanceof Error ? e.message : "Dry-Run fehlgeschlagen"); } finally { setDryRunning(false); } }; const handleDelete = async () => { if (!deleteRule) return; setDeleting(true); try { await deleteRoutingRule(deleteRule.id); setDeleteRule(null); load(); } catch (e: unknown) { setError(e instanceof Error ? e.message : "Löschen fehlgeschlagen"); setDeleteRule(null); } finally { setDeleting(false); } }; return (
Routing-Regeln Ordnen eingehende Mails automatisch einem Mandanten zu — nach Absender-/Empfänger-Domain oder -Adresse. Wildcard-Domains via{" "} *.kunde.de.
Priorität: Höhere Zahl = höhere Priorität und gewinnt, wenn mehrere Regeln zutreffen (bei Gleichstand die zuerst angelegte Regel). Regeln greifen nur beim Import neuer Mails —{" "} bereits archivierte Mails werden nicht rückwirkend umgeroutet. {error &&

{error}

} {loading ? (
) : rules.length === 0 ? (

Noch keine Routing-Regeln definiert.

) : (
Prio Typ Muster {isSuperAdmin && Mandant} Angelegt {rules.map((r) => ( {r.priority} {MATCH_LABELS[r.match_type] ?? r.match_type} {r.pattern} {isSuperAdmin && {tenantName(r.tenant_id)}} {formatDate(r.created_at)}
))}
)}
{/* Create / Edit Dialog */} { if (!o) { setEdit(null); resetDryRun(); } }} > {edit?.id === null ? "Regel hinzufügen" : "Regel bearbeiten"} Legt fest, welche eingehenden Mails automatisch welchem Mandanten zugeordnet werden. {edit && (
{ setEdit({ ...edit, pattern: e.target.value }); resetDryRun(); }} />
{isSuperAdmin && (
)}
setEdit({ ...edit, priority: e.target.value })} />

Höhere Zahl = höhere Priorität. Bei mehreren Treffern gewinnt die Regel mit der höchsten Priorität.

{/* Dry-Run */}

Test (Dry-Run)

Zeigt, wie viele bereits archivierte Mails dieses Muster treffen würde (nur zur Vorschau).

{dryRunError && (

{dryRunError}

)} {dryRunResult && (

{dryRunResult.match_count} Treffer{" "} (Stichprobe max. {dryRunResult.sample_limit})

{dryRunResult.sample && dryRunResult.sample.length > 0 ? (
Von An Betreff Datum {dryRunResult.sample.map((s) => ( {s.mail_from} {s.mail_to} {s.subject} {formatDate(s.received_at)} ))}
) : (

Keine passenden Mails im Archiv gefunden.

)}
)}
{formError &&

{formError}

}
)}
{/* Delete Dialog */} { if (!o) setDeleteRule(null); }} > Regel löschen Die Regel wird gelöscht. Bereits archivierte Mails bleiben ihrem bisherigen Mandanten zugeordnet. {deleteRule && (

{deleteRule.pattern} ( {MATCH_LABELS[deleteRule.match_type]})

)}
); }