310 lines
9.9 KiB
TypeScript
310 lines
9.9 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Eye, EyeOff } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { api } from '@/api';
|
|
import { errMessage } from '@/lib/utils';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
|
import {
|
|
Select,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
SelectContent,
|
|
SelectItem,
|
|
} from '@/components/ui/select';
|
|
import { SectionHeading, FieldRow, Toggle } from './adminShared';
|
|
|
|
interface EmailConfig {
|
|
enabled: boolean;
|
|
sender_name: string;
|
|
sender_address: string;
|
|
smtp_host: string;
|
|
smtp_port: string;
|
|
smtp_encryption: string;
|
|
smtp_self_signed: boolean;
|
|
smtp_username: string;
|
|
smtp_password: string;
|
|
allow_user_config: boolean;
|
|
global_recipient: string;
|
|
reminder_hour: string;
|
|
}
|
|
|
|
const DEFAULTS: EmailConfig = {
|
|
enabled: false,
|
|
sender_name: '',
|
|
sender_address: '',
|
|
smtp_host: '',
|
|
smtp_port: '587',
|
|
smtp_encryption: 'starttls',
|
|
smtp_self_signed: false,
|
|
smtp_username: '',
|
|
smtp_password: '',
|
|
allow_user_config: false,
|
|
global_recipient: '',
|
|
reminder_hour: '6',
|
|
};
|
|
|
|
// "6:00 AM" … "11:00 PM" for the hour select.
|
|
const HOUR_OPTIONS = Array.from({ length: 24 }, (_, h) => ({
|
|
value: String(h),
|
|
label: `${h % 12 || 12}:00 ${h < 12 ? 'AM' : 'PM'}`,
|
|
}));
|
|
|
|
export default function EmailNotifCard() {
|
|
const [cfg, setCfg] = useState<EmailConfig>(DEFAULTS);
|
|
const [loading, setLoading] = useState(true);
|
|
const [loadError, setLoadError] = useState('');
|
|
const [saving, setSaving] = useState(false);
|
|
const [showPw, setShowPw] = useState(false);
|
|
const [testEmail, setTestEmail] = useState('');
|
|
const [testing, setTesting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
api
|
|
.notifAdmin()
|
|
.then((d) => setCfg({ ...DEFAULTS, ...(d as Partial<EmailConfig>) }))
|
|
.catch((err) => setLoadError(errMessage(err, 'Failed to load email settings')))
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
const set = (k: keyof EmailConfig, v: string | boolean) =>
|
|
setCfg((p) => ({ ...p, [k]: v }) as EmailConfig);
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true);
|
|
try {
|
|
await api.saveNotifAdmin(cfg);
|
|
toast.success('Email settings saved.');
|
|
} catch (err) {
|
|
toast.error(errMessage(err, 'Failed to save.'));
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleTest = async () => {
|
|
if (!testEmail) {
|
|
toast.error('Enter a recipient email.');
|
|
return;
|
|
}
|
|
setTesting(true);
|
|
try {
|
|
await api.testEmail({ to: testEmail });
|
|
toast.success('Test email sent.');
|
|
} catch (err) {
|
|
toast.error(errMessage(err, 'Failed to send test email.'));
|
|
} finally {
|
|
setTesting(false);
|
|
}
|
|
};
|
|
|
|
if (loading)
|
|
return (
|
|
<Card>
|
|
<CardContent className="py-8 text-center text-muted-foreground text-sm">
|
|
Loading…
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
if (loadError)
|
|
return (
|
|
<Card>
|
|
<CardContent className="py-8 text-center text-sm text-destructive">{loadError}</CardContent>
|
|
</Card>
|
|
);
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader className="pb-4">
|
|
<CardTitle>Email Notifications</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-5">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium">Enable email notifications</p>
|
|
<p className="text-xs text-muted-foreground">Configure SMTP to send bill reminders</p>
|
|
</div>
|
|
<Toggle
|
|
checked={cfg.enabled}
|
|
onChange={(v) => set('enabled', v)}
|
|
label="Enable email notifications"
|
|
/>
|
|
</div>
|
|
|
|
<FieldRow label="Reminder send time">
|
|
<div className="flex flex-col items-end gap-1">
|
|
<Select value={cfg.reminder_hour} onValueChange={(v) => set('reminder_hour', v)}>
|
|
<SelectTrigger className="w-40">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{HOUR_OPTIONS.map((o) => (
|
|
<SelectItem key={o.value} value={o.value}>
|
|
{o.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<span className="text-[11px] text-muted-foreground">
|
|
Daily reminder emails — applies account-wide.
|
|
</span>
|
|
</div>
|
|
</FieldRow>
|
|
|
|
<div className="border-t border-border" />
|
|
|
|
<div className="space-y-4">
|
|
<SectionHeading>Sender</SectionHeading>
|
|
<FieldRow label="Sender name">
|
|
<Input
|
|
value={cfg.sender_name}
|
|
onChange={(e) => set('sender_name', e.target.value)}
|
|
placeholder="Bill Tracker"
|
|
/>
|
|
</FieldRow>
|
|
<FieldRow label="Sender address">
|
|
<Input
|
|
value={cfg.sender_address}
|
|
onChange={(e) => set('sender_address', e.target.value)}
|
|
placeholder="no-reply@example.com"
|
|
type="email"
|
|
/>
|
|
</FieldRow>
|
|
</div>
|
|
|
|
<div className="border-t border-border" />
|
|
|
|
<div className="space-y-4">
|
|
<SectionHeading>SMTP Server</SectionHeading>
|
|
<FieldRow label="SMTP host">
|
|
<Input
|
|
value={cfg.smtp_host}
|
|
onChange={(e) => set('smtp_host', e.target.value)}
|
|
placeholder="smtp.example.com"
|
|
/>
|
|
</FieldRow>
|
|
<FieldRow label="Port">
|
|
<Input
|
|
value={cfg.smtp_port}
|
|
onChange={(e) => set('smtp_port', e.target.value)}
|
|
placeholder="587"
|
|
type="number"
|
|
className="w-28"
|
|
/>
|
|
</FieldRow>
|
|
<FieldRow label="Encryption">
|
|
<Select value={cfg.smtp_encryption} onValueChange={(v) => set('smtp_encryption', v)}>
|
|
<SelectTrigger className="w-40">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="starttls">STARTTLS</SelectItem>
|
|
<SelectItem value="ssl">SSL/TLS</SelectItem>
|
|
<SelectItem value="none">None</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</FieldRow>
|
|
<FieldRow label="Allow self-signed cert">
|
|
<div className="flex items-center h-9">
|
|
<input
|
|
type="checkbox"
|
|
id="self-signed"
|
|
checked={cfg.smtp_self_signed}
|
|
onChange={(e) => set('smtp_self_signed', e.target.checked)}
|
|
className="h-4 w-4 rounded border-input bg-input accent-primary"
|
|
/>
|
|
<label htmlFor="self-signed" className="ml-2 text-sm text-muted-foreground">
|
|
Accept self-signed certificates
|
|
</label>
|
|
</div>
|
|
</FieldRow>
|
|
<FieldRow label="SMTP username">
|
|
<Input
|
|
value={cfg.smtp_username}
|
|
onChange={(e) => set('smtp_username', e.target.value)}
|
|
placeholder="user@example.com"
|
|
/>
|
|
</FieldRow>
|
|
<FieldRow label="SMTP password">
|
|
<div className="relative">
|
|
<Input
|
|
type={showPw ? 'text' : 'password'}
|
|
value={cfg.smtp_password}
|
|
onChange={(e) => set('smtp_password', e.target.value)}
|
|
placeholder="••••••••"
|
|
className="pr-9"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPw((p) => !p)}
|
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
{showPw ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
</button>
|
|
</div>
|
|
</FieldRow>
|
|
</div>
|
|
|
|
<div className="border-t border-border" />
|
|
|
|
<div className="space-y-4">
|
|
<SectionHeading>User Access</SectionHeading>
|
|
<FieldRow label="Allow user config">
|
|
<div className="flex items-center h-9">
|
|
<input
|
|
type="checkbox"
|
|
id="allow-user"
|
|
checked={cfg.allow_user_config}
|
|
onChange={(e) => set('allow_user_config', e.target.checked)}
|
|
className="h-4 w-4 rounded border-input bg-input accent-primary"
|
|
/>
|
|
<label htmlFor="allow-user" className="ml-2 text-sm text-muted-foreground">
|
|
Let users configure their own notification preferences
|
|
</label>
|
|
</div>
|
|
</FieldRow>
|
|
<FieldRow label="Global recipient">
|
|
<Input
|
|
value={cfg.global_recipient}
|
|
onChange={(e) => set('global_recipient', e.target.value)}
|
|
placeholder="recipient@example.com"
|
|
type="email"
|
|
/>
|
|
</FieldRow>
|
|
</div>
|
|
|
|
<div className="border-t border-border" />
|
|
|
|
<div className="space-y-4">
|
|
<SectionHeading>Test Email</SectionHeading>
|
|
<FieldRow label="Send test to">
|
|
<div className="flex gap-2">
|
|
<Input
|
|
value={testEmail}
|
|
onChange={(e) => setTestEmail(e.target.value)}
|
|
placeholder="you@example.com"
|
|
type="email"
|
|
/>
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleTest}
|
|
disabled={testing}
|
|
className="shrink-0"
|
|
>
|
|
{testing ? 'Sending…' : 'Send Test Email'}
|
|
</Button>
|
|
</div>
|
|
</FieldRow>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-2">
|
|
<Button onClick={handleSave} disabled={saving}>
|
|
{saving ? 'Saving…' : 'Save Settings'}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|