Files
Calbook/components/admin/settings-panel.tsx

721 lines
30 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
ArrowLeft,
ArrowRight,
Building2,
CalendarCog,
CheckCircle2,
Mail,
Save,
Video,
Zap
} from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
type SettingsMap = Record<string, string>;
type SmtpTestState = {
status: "idle" | "loading" | "success" | "error";
message: string;
};
function extractAddressFromFromHeader(value: string | undefined) {
if (!value) return "";
const match = value.match(/<([^>]+)>/);
if (match?.[1]) return match[1].trim();
return value.trim();
}
function extractNameFromFromHeader(value: string | undefined) {
if (!value) return "";
const match = value.match(/^([^<]+)<[^>]+>$/);
if (!match?.[1]) return "";
return match[1].trim().replace(/^"(.+)"$/, "$1");
}
const JITSI_MODE_OPTIONS = [
{ value: "public", label: "Öffentlich (meet.jit.si)" },
{ value: "custom", label: "Eigene Jitsi-URL" }
] as const;
const SMTP_SETUP_STEPS = ["Server", "Absender", "Prüfen"] as const;
function emptySmtpTestState(): SmtpTestState {
return { status: "idle", message: "Noch nicht getestet." };
}
const TABS = [
{ id: "general", label: "Allgemein", icon: <Building2 className="h-4 w-4" /> },
{ id: "booking", label: "Buchungsregeln", icon: <CalendarCog className="h-4 w-4" /> },
{ id: "jitsi", label: "Jitsi Meet", icon: <Video className="h-4 w-4" /> },
{ id: "smtp", label: "SMTP", icon: <Mail className="h-4 w-4" /> }
] as const;
type TabId = (typeof TABS)[number]["id"];
function SmtpTestBox({ state }: { state: SmtpTestState }) {
return (
<div
className={cn(
"rounded-xl border p-4 text-sm",
state.status === "success" && "border-emerald-200 bg-emerald-50 text-emerald-800",
state.status === "error" && "border-red-200 bg-red-50 text-red-800",
state.status === "loading" && "border-blue-200 bg-blue-50 text-blue-800",
state.status === "idle" && "border-slate-200 bg-white text-slate-600"
)}
>
<div className="flex items-center gap-2 mb-1">
{state.status === "success" ? (
<CheckCircle2 className="h-4 w-4 text-emerald-600" />
) : state.status === "error" ? (
<Zap className="h-4 w-4 text-red-500" />
) : state.status === "loading" ? (
<span className="h-4 w-4 animate-spin rounded-full border-2 border-blue-400 border-t-transparent" />
) : null}
<p className="font-bold">
{state.status === "success"
? "SMTP-Test erfolgreich"
: state.status === "error"
? "SMTP-Test fehlgeschlagen"
: state.status === "loading"
? "SMTP-Test läuft"
: "SMTP-Test"}
</p>
</div>
<p>{state.message}</p>
</div>
);
}
const settingsSchema = z
.object({
company_name: z.string().min(1),
contact_email: z.string().email(),
default_duration_minutes: z.string().min(1),
buffer_minutes: z.string().min(1),
booking_lead_hours: z.string().min(1),
booking_window_days: z.string().min(1),
cancel_limit_hours: z.string().min(1),
reminder_primary_hours: z.string().regex(/^\d+$/, "Bitte eine Zahl eingeben"),
reminder_secondary_hours: z.string().regex(/^\d+$/, "Bitte eine Zahl eingeben"),
jitsi_meeting_mode: z.enum(["public", "custom"]),
jitsi_base_url: z.string().trim().url("Bitte eine gültige Jitsi-URL eingeben"),
jitsi_room_prefix: z
.string()
.trim()
.min(2, "Bitte ein Präfix mit mindestens 2 Zeichen eingeben")
.regex(/^[a-z0-9-]+$/, "Nur Kleinbuchstaben, Zahlen und Bindestriche erlaubt"),
booking_notice_text: z.string().min(1),
smtp_host: z.string().optional().default(""),
smtp_port: z.string().regex(/^\d+$/, "Bitte einen numerischen SMTP-Port eingeben"),
smtp_user: z.string().optional().default(""),
smtp_pass: z.string().optional().default(""),
smtp_from_name: z.string().min(1),
smtp_from: z
.string()
.trim()
.refine(
(value) => value === "" || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
"Bitte eine gültige Absender-E-Mail eingeben"
)
.default("")
})
.superRefine((values, ctx) => {
const first = Number(values.reminder_primary_hours);
const second = Number(values.reminder_secondary_hours);
if (!Number.isFinite(first) || first < 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["reminder_primary_hours"],
message: "Reminder 1 muss mindestens 1 Stunde sein."
});
}
if (!Number.isFinite(second) || second < 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["reminder_secondary_hours"],
message: "Reminder 2 muss mindestens 1 Stunde sein."
});
}
if (Number.isFinite(first) && Number.isFinite(second) && first <= second) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["reminder_primary_hours"],
message: "Reminder 1 muss später liegen als Reminder 2 (z. B. 24 und 1)."
});
}
});
type SettingsFormValues = z.infer<typeof settingsSchema>;
export function SettingsPanel() {
const router = useRouter();
const [settings, setSettings] = useState<SettingsMap | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [activeTab, setActiveTab] = useState<TabId>("general");
const [smtpTestTo, setSmtpTestTo] = useState("");
const [smtpTestLoading, setSmtpTestLoading] = useState(false);
const [smtpStep, setSmtpStep] = useState(0);
const [smtpTestState, setSmtpTestState] = useState<SmtpTestState>(() => emptySmtpTestState());
const settingsForm = useForm<SettingsFormValues>({
resolver: zodResolver(settingsSchema)
});
const jitsiMode = settingsForm.watch("jitsi_meeting_mode");
const smtpHost = settingsForm.watch("smtp_host") ?? "";
const smtpPort = settingsForm.watch("smtp_port") ?? "587";
const smtpUser = settingsForm.watch("smtp_user") ?? "";
const smtpPass = settingsForm.watch("smtp_pass") ?? "";
const smtpFromName = settingsForm.watch("smtp_from_name") ?? "CalBook";
const smtpFrom = settingsForm.watch("smtp_from") ?? "";
const smtpFromPreview = `${smtpFromName || "CalBook"} <${
smtpFrom || smtpUser || "no-reply@calbook.local"
}>`;
async function loadSettings() {
setLoading(true);
try {
const settingsRes = await fetch("/api/admin/einstellungen", { cache: "no-store" });
const settingsData = await settingsRes.json();
setSettings(settingsData.settings ?? {});
setSmtpTestTo(
settingsData.settings?.contact_email ??
extractAddressFromFromHeader(settingsData.settings?.smtp_from) ??
""
);
setSmtpStep(0);
setSmtpTestState(emptySmtpTestState());
settingsForm.reset({
company_name: settingsData.settings?.company_name ?? "CalBook",
contact_email: settingsData.settings?.contact_email ?? "",
default_duration_minutes: settingsData.settings?.default_duration_minutes ?? "60",
buffer_minutes: settingsData.settings?.buffer_minutes ?? "10",
booking_lead_hours: settingsData.settings?.booking_lead_hours ?? "2",
booking_window_days: settingsData.settings?.booking_window_days ?? "60",
cancel_limit_hours: settingsData.settings?.cancel_limit_hours ?? "24",
reminder_primary_hours: settingsData.settings?.reminder_primary_hours ?? "24",
reminder_secondary_hours: settingsData.settings?.reminder_secondary_hours ?? "1",
jitsi_meeting_mode: settingsData.settings?.jitsi_meeting_mode ?? "public",
jitsi_base_url: settingsData.settings?.jitsi_base_url || "https://meet.jit.si",
jitsi_room_prefix: settingsData.settings?.jitsi_room_prefix || "calbook",
booking_notice_text: settingsData.settings?.booking_notice_text ?? "",
smtp_host: settingsData.settings?.smtp_host ?? "",
smtp_port: settingsData.settings?.smtp_port ?? "587",
smtp_user: settingsData.settings?.smtp_user ?? "",
smtp_pass: settingsData.settings?.smtp_pass ?? "",
smtp_from_name:
settingsData.settings?.smtp_from_name ??
extractNameFromFromHeader(settingsData.settings?.smtp_from) ??
settingsData.settings?.company_name ??
"CalBook",
smtp_from: extractAddressFromFromHeader(settingsData.settings?.smtp_from)
});
} catch {
toast.error("Einstellungen konnten nicht geladen werden.");
} finally {
setLoading(false);
}
}
useEffect(() => {
void loadSettings();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setSmtpTestState((prev) => {
if (prev.status === "idle" || prev.status === "loading") return prev;
return { status: "idle", message: "SMTP-Daten wurden geändert. Bitte erneut testen." };
});
}, [smtpHost, smtpPort, smtpUser, smtpPass, smtpFromName, smtpFrom]);
const onSaveSettings = settingsForm.handleSubmit(
async (values) => {
setSaving(true);
try {
const res = await fetch("/api/admin/einstellungen", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ values })
});
if (!res.ok) {
toast.error("Einstellungen konnten nicht gespeichert werden.");
return;
}
toast.success("Einstellungen gespeichert");
await loadSettings();
router.refresh();
} finally {
setSaving(false);
}
},
() => {
toast.error("Bitte prüfe die Eingaben in den Einstellungen.");
}
);
async function sendSmtpTest() {
const values = settingsForm.getValues();
const host = values.smtp_host?.trim() ?? "";
const port = values.smtp_port?.trim() ?? "";
if (!host || !port) {
toast.error("Bitte SMTP-Host und SMTP-Port eintragen.");
setSmtpTestState({ status: "error", message: "SMTP-Host und Port fehlen." });
return;
}
const formIsValid = await settingsForm.trigger(["smtp_host", "smtp_port", "smtp_from_name", "smtp_from"]);
if (!formIsValid) {
toast.error("Bitte prüfe Absendername und Absender-E-Mail.");
return;
}
if (!smtpTestTo.trim()) {
toast.error("Bitte eine Empfänger-E-Mail für den SMTP-Test angeben.");
return;
}
setSmtpTestLoading(true);
setSmtpTestState({
status: "loading",
message: "Testmail wird mit den aktuell eingetragenen SMTP-Daten versendet ..."
});
try {
const res = await fetch("/api/admin/einstellungen/test-smtp", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
to: smtpTestTo.trim(),
smtp: {
host,
port,
user: values.smtp_user?.trim() ?? "",
pass: values.smtp_pass ?? "",
fromName: values.smtp_from_name?.trim() ?? "CalBook",
from: values.smtp_from?.trim() ?? ""
}
})
});
const data = await res.json();
if (!res.ok) {
setSmtpTestState({ status: "error", message: data?.message ?? "SMTP-Test fehlgeschlagen." });
toast.error(data?.message ?? "SMTP-Test fehlgeschlagen.");
return;
}
setSmtpTestState({
status: "success",
message: data?.message ?? "Testmail wurde erfolgreich versendet."
});
toast.success("SMTP-Testmail wurde versendet.");
} catch {
setSmtpTestState({ status: "error", message: "SMTP-Test fehlgeschlagen." });
toast.error("SMTP-Test fehlgeschlagen.");
} finally {
setSmtpTestLoading(false);
}
}
async function goToNextSmtpStep() {
if (smtpStep === 0) {
const values = settingsForm.getValues();
if (!values.smtp_host?.trim() || !values.smtp_port?.trim()) {
toast.error("Bitte SMTP-Host und SMTP-Port eintragen.");
return;
}
}
if (smtpStep === 1) {
const formIsValid = await settingsForm.trigger(["smtp_from_name", "smtp_from"]);
if (!formIsValid) {
toast.error("Bitte prüfe die Absenderdaten.");
return;
}
}
setSmtpStep((prev) => Math.min(prev + 1, SMTP_SETUP_STEPS.length - 1));
}
if (loading || !settings) return null;
return (
<div className="max-w-6xl mx-auto">
<form onSubmit={onSaveSettings}>
<div className="mb-6 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
<div>
<h1 className="text-3xl font-black tracking-tight text-slate-950">Einstellungen</h1>
<p className="mt-1 text-sm font-medium text-slate-500">
Firma, Buchungsregeln und Kommunikation konfigurieren
</p>
</div>
</div>
{/* Tabs */}
<div className="mb-0 flex rounded-t-2xl border border-b-0 border-slate-200 bg-slate-50/80">
{TABS.map((tab) => (
<button
key={tab.id}
type="button"
onClick={() => setActiveTab(tab.id)}
className={cn(
"flex items-center gap-2 px-5 py-3.5 text-sm font-bold transition-all first:rounded-tl-2xl",
activeTab === tab.id
? "border-b-2 border-slate-900 bg-white text-slate-900"
: "text-slate-500 hover:text-slate-700 hover:bg-white/50"
)}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
{/* Tab content */}
<div className="rounded-b-2xl border border-slate-200 bg-white p-6 shadow-sm">
{/* Allgemein */}
{activeTab === "general" && (
<div className="animate-in fade-in slide-in-from-left-2 duration-200 space-y-5">
<div>
<p className="text-xs font-black uppercase tracking-[0.22em] text-slate-400 mb-4">
Firmeninformationen
</p>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="company_name">Firmenname</Label>
<Input id="company_name" {...settingsForm.register("company_name")} />
<p className="text-xs text-slate-400">Erscheint in E-Mails und auf der Buchungsseite.</p>
</div>
<div className="space-y-1.5">
<Label htmlFor="contact_email">Kontakt-E-Mail</Label>
<Input id="contact_email" {...settingsForm.register("contact_email")} />
<p className="text-xs text-slate-400">Wird für SMTP-Test und als Rückfall-Adresse verwendet.</p>
</div>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="booking_notice_text">Hinweistext Buchungsseite</Label>
<Textarea id="booking_notice_text" {...settingsForm.register("booking_notice_text")} />
<p className="text-xs text-slate-400">Optionaler Hinweis, der auf der öffentlichen Buchungsseite angezeigt wird.</p>
</div>
</div>
)}
{/* Buchungsregeln */}
{activeTab === "booking" && (
<div className="animate-in fade-in slide-in-from-left-2 duration-200 space-y-5">
<div>
<p className="text-xs font-black uppercase tracking-[0.22em] text-slate-400 mb-4">
Termin-Parameter
</p>
<div className="grid gap-4 md:grid-cols-3">
<div className="space-y-1.5">
<Label htmlFor="default_duration_minutes">Standard-Dauer (Min.)</Label>
<Input id="default_duration_minutes" {...settingsForm.register("default_duration_minutes")} />
</div>
<div className="space-y-1.5">
<Label htmlFor="buffer_minutes">Puffer (Min.)</Label>
<Input id="buffer_minutes" {...settingsForm.register("buffer_minutes")} />
</div>
<div className="space-y-1.5">
<Label htmlFor="booking_lead_hours">Buchungsvorlauf (Std.)</Label>
<Input id="booking_lead_hours" {...settingsForm.register("booking_lead_hours")} />
</div>
<div className="space-y-1.5">
<Label htmlFor="booking_window_days">Buchungsfenster (Tage)</Label>
<Input id="booking_window_days" {...settingsForm.register("booking_window_days")} />
</div>
<div className="space-y-1.5">
<Label htmlFor="cancel_limit_hours">Storno-Limit (Std.)</Label>
<Input id="cancel_limit_hours" {...settingsForm.register("cancel_limit_hours")} />
</div>
</div>
<div className="mt-3 rounded-xl border border-dashed border-slate-200 bg-slate-50 p-3 text-xs text-slate-500">
Buchbare Wochentage und Uhrzeiten werden pro Personen-Kalender unter{" "}
<strong>Kalender</strong> gepflegt.
</div>
</div>
<div>
<p className="text-xs font-black uppercase tracking-[0.22em] text-slate-400 mb-4">
Erinnerungen
</p>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="reminder_primary_hours">Erste Erinnerung (Std. vorher)</Label>
<Input id="reminder_primary_hours" {...settingsForm.register("reminder_primary_hours")} />
<p className="text-xs text-slate-400">Z. B. 24 für einen Tag vorher.</p>
</div>
<div className="space-y-1.5">
<Label htmlFor="reminder_secondary_hours">Zweite Erinnerung (Std. vorher)</Label>
<Input id="reminder_secondary_hours" {...settingsForm.register("reminder_secondary_hours")} />
<p className="text-xs text-slate-400">Muss kleiner als die erste sein (z. B. 1).</p>
</div>
</div>
<div className="mt-3 rounded-xl border border-dashed border-slate-200 bg-slate-50 p-3 text-xs text-slate-500">
Beide Erinnerungen gelten für Kunde und Kalenderbesitzer, aber nicht für Instant Meetings.
</div>
</div>
</div>
)}
{/* Jitsi Meet */}
{activeTab === "jitsi" && (
<div className="animate-in fade-in slide-in-from-left-2 duration-200 space-y-5">
<p className="text-xs font-black uppercase tracking-[0.22em] text-slate-400 mb-4">
Videokonferenz
</p>
<div className="space-y-1.5">
<Label htmlFor="jitsi_meeting_mode">Jitsi-Modus</Label>
<select
id="jitsi_meeting_mode"
className="h-11 w-full rounded-xl border border-slate-200 bg-slate-50 px-4 text-sm font-medium text-slate-900 transition-all focus:border-indigo-600 focus:outline-none focus:ring-1 focus:ring-indigo-600"
{...settingsForm.register("jitsi_meeting_mode")}
>
{JITSI_MODE_OPTIONS.map((mode) => (
<option key={mode.value} value={mode.value}>
{mode.label}
</option>
))}
</select>
<p className="text-xs text-slate-400">
Öffentlich vermeidet Moderator-Login und ist für Kundentermine am einfachsten.
</p>
</div>
<div className="space-y-1.5">
<Label htmlFor="jitsi_base_url">Jitsi-Basis-URL</Label>
<Input
id="jitsi_base_url"
{...settingsForm.register("jitsi_base_url")}
placeholder="https://meet.jit.si"
disabled={jitsiMode === "public"}
/>
<p className="text-xs text-slate-400">
Beispiel: <code>https://meet.jit.si</code> oder eigene Jitsi-Domain (nur bei „Eigene Jitsi-URL“).
</p>
</div>
<div className="space-y-1.5">
<Label htmlFor="jitsi_room_prefix">Jitsi-Raum-Präfix</Label>
<Input
id="jitsi_room_prefix"
{...settingsForm.register("jitsi_room_prefix")}
placeholder="calbook"
/>
<p className="text-xs text-slate-400">
Wird vor jede Raum-ID gesetzt, z. B. <code>calbook-abc123</code>.
</p>
</div>
</div>
)}
{/* SMTP */}
{activeTab === "smtp" && (
<div className="animate-in fade-in slide-in-from-left-2 duration-200 space-y-5">
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs font-black uppercase tracking-[0.22em] text-slate-400">
SMTP-Assistent
</p>
<p className="mt-1 text-sm font-medium text-slate-500">
Serverdaten eintragen, Absender prüfen und Testmail senden.
</p>
</div>
<div className="rounded-xl border border-slate-200 bg-slate-50 px-4 py-2 text-xs text-slate-600">
Absender: <span className="font-bold text-slate-900">{smtpFromPreview}</span>
</div>
</div>
{/* Step indicators */}
<div className="flex gap-2">
{SMTP_SETUP_STEPS.map((step, index) => (
<button
key={step}
type="button"
onClick={() => setSmtpStep(index)}
className={cn(
"flex-1 rounded-xl border px-4 py-3 text-left text-sm font-bold transition-all",
smtpStep === index
? "border-slate-900 bg-slate-900 text-white shadow-sm"
: smtpStep > index
? "border-emerald-200 bg-emerald-50 text-emerald-800"
: "border-slate-200 bg-white text-slate-500 hover:border-slate-300"
)}
>
<span className="block text-[10px] opacity-70">Schritt {index + 1}</span>
{smtpStep > index ? (
<span className="flex items-center gap-1">
<CheckCircle2 className="h-3.5 w-3.5" /> {step}
</span>
) : (
step
)}
</button>
))}
</div>
{/* Step content */}
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-5">
{smtpStep === 0 && (
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="smtp-host">SMTP-Host*</Label>
<Input id="smtp-host" {...settingsForm.register("smtp_host")} placeholder="mail.example.com" />
</div>
<div className="space-y-1.5">
<Label htmlFor="smtp-port">SMTP-Port*</Label>
<Input id="smtp-port" {...settingsForm.register("smtp_port")} placeholder="587" />
<p className="text-xs text-slate-400">
Port 465 = SSL, sonst STARTTLS falls angeboten.
</p>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtp-user">SMTP-Benutzer</Label>
<Input id="smtp-user" {...settingsForm.register("smtp_user")} autoComplete="username" />
</div>
<div className="space-y-1.5">
<Label htmlFor="smtp-pass">SMTP-Passwort</Label>
<Input
id="smtp-pass"
type="password"
{...settingsForm.register("smtp_pass")}
autoComplete="new-password"
/>
</div>
</div>
)}
{smtpStep === 1 && (
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="smtp-from-name">Absender-Name*</Label>
<Input id="smtp-from-name" {...settingsForm.register("smtp_from_name")} placeholder="CalBook" />
</div>
<div className="space-y-1.5">
<Label htmlFor="smtp-from">Absender-E-Mail</Label>
<Input id="smtp-from" {...settingsForm.register("smtp_from")} placeholder="no-reply@example.com" />
</div>
</div>
<div className="rounded-xl border border-slate-200 bg-white p-4 text-sm">
<p className="font-bold text-slate-900 mb-1">Vorschau</p>
<p className="break-all font-mono text-slate-700">{smtpFromPreview}</p>
<p className="mt-2 text-xs text-slate-400">
Viele SMTP-Server erzwingen als technische Absenderadresse den SMTP-Benutzer.
Der sichtbare Name bleibt trotzdem steuerbar.
</p>
</div>
</div>
)}
{smtpStep === 2 && (
<div className="grid gap-5 lg:grid-cols-[1fr_320px]">
<div className="space-y-4">
<div className="rounded-xl border border-slate-200 bg-white p-4 text-sm">
<p className="mb-2 font-bold text-slate-900">Zusammenfassung</p>
<div className="grid grid-cols-2 gap-2 text-slate-600">
<p>Host:</p><p className="font-medium text-slate-900">{smtpHost || "-"}</p>
<p>Port:</p><p className="font-medium text-slate-900">{smtpPort || "-"}</p>
<p>Benutzer:</p><p className="font-medium text-slate-900">{smtpUser || "ohne Auth"}</p>
</div>
<p className="mt-2 break-all text-xs text-slate-500">
Absender: <span className="font-medium text-slate-700">{smtpFromPreview}</span>
</p>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtp-test-to">SMTP-Testempfänger*</Label>
<div className="flex flex-wrap gap-2">
<Input
id="smtp-test-to"
type="email"
value={smtpTestTo}
onChange={(e) => {
setSmtpTestTo(e.target.value);
setSmtpTestState(emptySmtpTestState());
}}
placeholder="test@example.com"
className="max-w-sm"
/>
<Button
type="button"
variant="secondary"
onClick={() => void sendSmtpTest()}
disabled={smtpTestLoading}
>
{smtpTestLoading ? "Test läuft..." : "Testmail senden"}
</Button>
</div>
<p className="text-xs text-slate-400">
Der Test nutzt die aktuellen Eingaben, auch wenn sie noch nicht gespeichert sind.
</p>
</div>
</div>
<div>
<SmtpTestBox state={smtpTestState} />
<p className="mt-2 text-xs text-slate-400">
Nach erfolgreichem Test speichern, damit die SMTP-Daten aktiv für Buchungen,
Erinnerungen und Instant Meetings genutzt werden.
</p>
</div>
</div>
)}
{/* SMTP navigation */}
<div className="mt-5 flex justify-between border-t border-slate-200 pt-4">
<Button
type="button"
variant="secondary"
onClick={() => setSmtpStep((prev) => Math.max(prev - 1, 0))}
disabled={smtpStep === 0}
>
<ArrowLeft className="mr-1.5 h-4 w-4" />
Zurück
</Button>
{smtpStep < SMTP_SETUP_STEPS.length - 1 ? (
<Button type="button" onClick={() => void goToNextSmtpStep()}>
Weiter
<ArrowRight className="ml-1.5 h-4 w-4" />
</Button>
) : (
<Button
type="button"
variant="secondary"
onClick={() => void sendSmtpTest()}
disabled={smtpTestLoading}
>
{smtpTestLoading ? "Test läuft..." : "Jetzt SMTP prüfen"}
</Button>
)}
</div>
</div>
</div>
)}
</div>
{/* Bottom save button */}
<div className="mt-6 flex justify-end">
<Button type="submit" size="lg" disabled={saving}>
<Save className="mr-2 h-4 w-4" />
{saving ? "Speichert..." : "Einstellungen speichern"}
</Button>
</div>
</form>
</div>
);
}