c9be7c9bbf
SmtpSettingsPanel now owns its form state, save/test mutations, and feedback state internally. Props reduced from 17 to 2 (initialSettings + onSettingsSaved callback). Removes 7 useState declarations, 2 mutation definitions, and 1 handler from the parent. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
202 lines
6.6 KiB
TypeScript
202 lines
6.6 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { trpc } from "~/lib/trpc/client.js";
|
|
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
|
import {
|
|
CHECKBOX_ROW_CLASS,
|
|
INPUT_CLASS,
|
|
LABEL_CLASS,
|
|
PANEL_CLASS,
|
|
PRIMARY_BUTTON_CLASS,
|
|
SECONDARY_BUTTON_CLASS,
|
|
RuntimeSecretCard,
|
|
type RuntimeSecrets,
|
|
type SaveResult,
|
|
} from "./shared.js";
|
|
|
|
type SmtpSettingsPanelProps = {
|
|
initialSettings: {
|
|
smtpHost: string | null;
|
|
smtpPort: number | null;
|
|
smtpUser: string | null;
|
|
smtpFrom: string | null;
|
|
smtpTls: boolean | null;
|
|
runtimeSecrets: { smtpPassword: RuntimeSecrets["smtpPassword"] };
|
|
};
|
|
onSettingsSaved: () => void;
|
|
};
|
|
|
|
export function SmtpSettingsPanel({ initialSettings, onSettingsSaved }: SmtpSettingsPanelProps) {
|
|
const [smtpHost, setSmtpHost] = useState("");
|
|
const [smtpPort, setSmtpPort] = useState(587);
|
|
const [smtpUser, setSmtpUser] = useState("");
|
|
const [smtpFrom, setSmtpFrom] = useState("");
|
|
const [smtpTls, setSmtpTls] = useState(true);
|
|
const [saved, setSaved] = useState(false);
|
|
const [testResult, setTestResult] = useState<SaveResult | null>(null);
|
|
|
|
useEffect(() => {
|
|
setSmtpHost(initialSettings.smtpHost ?? "");
|
|
setSmtpPort(initialSettings.smtpPort ?? 587);
|
|
setSmtpUser(initialSettings.smtpUser ?? "");
|
|
setSmtpFrom(initialSettings.smtpFrom ?? "");
|
|
setSmtpTls(initialSettings.smtpTls ?? true);
|
|
}, [initialSettings]);
|
|
|
|
const saveMutation = trpc.settings.updateSystemSettings.useMutation({
|
|
onSuccess: () => {
|
|
setSaved(true);
|
|
setTestResult(null);
|
|
onSettingsSaved();
|
|
setTimeout(() => setSaved(false), 3000);
|
|
},
|
|
});
|
|
|
|
const testMutation = trpc.settings.testSmtpConnection.useMutation({
|
|
onSuccess: (data) => setTestResult(data),
|
|
onError: (error) => setTestResult({ ok: false, error: error.message }),
|
|
});
|
|
|
|
function handleSave() {
|
|
saveMutation.mutate({
|
|
smtpHost: smtpHost || undefined,
|
|
smtpPort,
|
|
smtpUser: smtpUser || undefined,
|
|
smtpFrom: smtpFrom || undefined,
|
|
smtpTls,
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div className={PANEL_CLASS}>
|
|
<div>
|
|
<h2 className="flex items-center text-base font-semibold text-gray-900 dark:text-gray-100">
|
|
Email Notifications (SMTP){" "}
|
|
<InfoTooltip content="Configure SMTP to send email notifications for vacation approvals and rejections." />
|
|
</h2>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
Used to send email notifications when vacation requests are approved or rejected.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
<div>
|
|
<label className={LABEL_CLASS}>
|
|
<span className="flex items-center">
|
|
SMTP Host{" "}
|
|
<InfoTooltip content="The SMTP server hostname, for example smtp.gmail.com or smtp.office365.com." />
|
|
</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
className={INPUT_CLASS}
|
|
value={smtpHost}
|
|
onChange={(event) => setSmtpHost(event.target.value)}
|
|
placeholder="smtp.example.com"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className={LABEL_CLASS}>
|
|
<span className="flex items-center">
|
|
SMTP Port{" "}
|
|
<InfoTooltip content="Common ports: 587 for STARTTLS, 465 for SSL/TLS, 25 for unencrypted." />
|
|
</span>
|
|
</label>
|
|
<input
|
|
type="number"
|
|
className={INPUT_CLASS}
|
|
value={smtpPort}
|
|
onChange={(event) => setSmtpPort(parseInt(event.target.value, 10))}
|
|
min={1}
|
|
max={65535}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className={LABEL_CLASS}>
|
|
<span className="flex items-center">
|
|
SMTP Username <InfoTooltip content="Authentication username for the SMTP server." />
|
|
</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
className={INPUT_CLASS}
|
|
value={smtpUser}
|
|
onChange={(event) => setSmtpUser(event.target.value)}
|
|
placeholder="user@example.com"
|
|
autoComplete="off"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className={LABEL_CLASS}>
|
|
<span className="flex items-center">
|
|
From Address{" "}
|
|
<InfoTooltip content="The sender email address shown in notification emails." />
|
|
</span>
|
|
</label>
|
|
<input
|
|
type="email"
|
|
className={INPUT_CLASS}
|
|
value={smtpFrom}
|
|
onChange={(event) => setSmtpFrom(event.target.value)}
|
|
placeholder="noreply@capakraken.app"
|
|
/>
|
|
</div>
|
|
<div className={`${CHECKBOX_ROW_CLASS} pt-0 md:mt-[1.65rem]`}>
|
|
<input
|
|
type="checkbox"
|
|
id="smtpTls"
|
|
checked={smtpTls}
|
|
onChange={(event) => setSmtpTls(event.target.checked)}
|
|
className="rounded border-gray-300 text-brand-600"
|
|
/>
|
|
<label
|
|
htmlFor="smtpTls"
|
|
className="cursor-pointer text-sm text-gray-700 dark:text-gray-300"
|
|
>
|
|
Use TLS
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<RuntimeSecretCard
|
|
title="SMTP Password"
|
|
description="SMTP credentials are provisioned outside the application and injected at runtime."
|
|
secret={initialSettings.runtimeSecrets.smtpPassword}
|
|
optionalNote="Provision SMTP_PASSWORD in the deployment target used by the API service."
|
|
/>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={handleSave}
|
|
disabled={saveMutation.isPending}
|
|
className={PRIMARY_BUTTON_CLASS}
|
|
>
|
|
{saveMutation.isPending ? "Saving\u2026" : "Save SMTP Settings"}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => testMutation.mutate()}
|
|
disabled={testMutation.isPending}
|
|
className={SECONDARY_BUTTON_CLASS}
|
|
>
|
|
{testMutation.isPending ? "Testing\u2026" : "Test Connection"}
|
|
</button>
|
|
{saved ? (
|
|
<span className="text-sm font-medium text-green-600 dark:text-green-400">Saved!</span>
|
|
) : null}
|
|
{testResult ? (
|
|
<span
|
|
className={`text-sm font-medium ${
|
|
testResult.ok
|
|
? "text-green-600 dark:text-green-400"
|
|
: "text-red-500 dark:text-red-400"
|
|
}`}
|
|
>
|
|
{testResult.ok ? "\u2713 Connection successful" : `\u2717 ${testResult.error}`}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|