BillTracker/client/components/admin/EmailNotifCard.tsx

310 lines
9.9 KiB
TypeScript
Raw Normal View History

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>
);
}