210 lines
8.0 KiB
JavaScript
210 lines
8.0 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { Eye, EyeOff } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { api } from '@/api';
|
|
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';
|
|
|
|
export default function EmailNotifCard() {
|
|
const DEFAULTS = {
|
|
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: '',
|
|
};
|
|
|
|
const [cfg, setCfg] = useState(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 }))
|
|
.catch(err => setLoadError(err.message || 'Failed to load email settings'))
|
|
.finally(() => setLoading(false));
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const set = (k, v) => setCfg(p => ({ ...p, [k]: v }));
|
|
|
|
const handleSave = async () => {
|
|
setSaving(true);
|
|
try {
|
|
await api.saveNotifAdmin(cfg);
|
|
toast.success('Email settings saved.');
|
|
} catch (err) {
|
|
toast.error(err.message || '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(err.message || '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>
|
|
|
|
<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="BillTracker" />
|
|
</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>
|
|
);
|
|
}
|