Files
pn-new-crm/src/components/admin/email/smtp-test-send-card.tsx
Matt 221ae5784e chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
2026-05-23 00:52:59 +02:00

122 lines
4.5 KiB
TypeScript

'use client';
import { useState } from 'react';
import { Send, CheckCircle2, AlertCircle } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { apiFetch } from '@/lib/api/client';
/**
* SMTP connectivity test card. Distinct from the branding-page "Send a
* test" affordance:
* - This one isolates SMTP - plaintext + minimal HTML, no logo, no
* branded shell - so the failure mode is pure transport.
* - The branding-preview send exercises the full rendering pipeline.
*
* Surfaces the SMTP error inline (under the input) instead of toasting,
* because the whole point is to read it. ENOTFOUND, EAUTH, connection
* refused, etc. land here as a useful diagnostic.
*/
export function SmtpTestSendCard() {
const [recipient, setRecipient] = useState('');
const [sending, setSending] = useState(false);
const [result, setResult] = useState<
| { kind: 'ok'; recipient: string; messageId: string | null }
| { kind: 'err'; message: string }
| null
>(null);
async function send() {
if (!recipient) return;
setSending(true);
setResult(null);
try {
const res = await apiFetch<{
data: { sent: boolean; recipient: string; messageId: string | null };
}>('/api/v1/admin/email/test-send', {
method: 'POST',
body: { recipient },
});
setResult({
kind: 'ok',
recipient: res.data.recipient,
messageId: res.data.messageId,
});
toast.success(`Test email accepted by SMTP for ${res.data.recipient}`);
} catch (err) {
const message = err instanceof Error ? err.message : 'SMTP send failed';
setResult({ kind: 'err', message });
} finally {
setSending(false);
}
}
return (
<Card>
<CardHeader>
<CardTitle>SMTP test send</CardTitle>
<CardDescription>
Fires a minimal plaintext email through the configured SMTP host. Use this to confirm
credentials reach the wire before a real notification flow depends on them. Errors land
inline below so you can read the SMTP response (auth failure, connection refused, etc.).
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="space-y-2">
<Label htmlFor="smtp-test-recipient">Recipient</Label>
<div className="flex flex-wrap items-center gap-2">
<Input
id="smtp-test-recipient"
type="email"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="you@example.com"
className="flex-1 min-w-[240px]"
/>
<Button onClick={send} disabled={sending || !recipient}>
<Send className="mr-1.5 h-4 w-4" aria-hidden />
{sending ? 'Sending…' : 'Send test'}
</Button>
</div>
</div>
{result?.kind === 'ok' && (
<div
role="status"
className="flex items-start gap-2 rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-900"
>
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" aria-hidden />
<div>
<div className="font-medium">Accepted by SMTP for {result.recipient}.</div>
{result.messageId ? (
<div className="text-xs text-emerald-800/80">
Message-ID: <span className="font-mono">{result.messageId}</span>
</div>
) : null}
<div className="text-xs text-emerald-800/80">
Acceptance doesn&apos;t guarantee inbox delivery. Check the recipient&apos;s mailbox
(and spam folder) to confirm.
</div>
</div>
</div>
)}
{result?.kind === 'err' && (
<div
role="alert"
className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm text-destructive"
>
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" aria-hidden />
<div>
<div className="font-medium">SMTP send failed</div>
<div className="font-mono text-xs break-all">{result.message}</div>
</div>
</div>
)}
</CardContent>
</Card>
);
}