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
This commit is contained in:
2026-05-23 00:52:59 +02:00
parent 43719b49e9
commit 221ae5784e
749 changed files with 7440 additions and 3118 deletions

View File

@@ -13,8 +13,8 @@ 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.
* - 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,

View File

@@ -0,0 +1,177 @@
'use client';
import { useState } from 'react';
import { useMutation, useQuery } from '@tanstack/react-query';
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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Skeleton } from '@/components/ui/skeleton';
import { apiFetch } from '@/lib/api/client';
import { toastError } from '@/lib/api/toast-error';
interface TemplateMeta {
id: string;
label: string;
description: string;
}
/**
* Per-template test-send card. Lists every transactional template the
* system can emit, lets the admin pick one + a recipient address, and
* fires a realistic preview (subject line tagged `[TEST · …]`) through
* the configured SMTP transport.
*
* Sits next to `<SmtpTestSendCard>` on the Email admin page - the
* SMTP-test verifies transport-layer auth + connectivity, this card
* verifies that each template renders + deliverability of the real
* branded payload.
*/
export function TestTemplateCard() {
const [templateId, setTemplateId] = useState<string>('');
const [recipient, setRecipient] = useState('');
const [lastResult, setLastResult] = useState<
| { kind: 'ok'; label: string; recipient: string; messageId: string | null }
| { kind: 'err'; message: string }
| null
>(null);
// Registry is fetched at runtime so adding a template on the backend
// surfaces in the dropdown without a client-side build.
const { data, isLoading } = useQuery<{ data: TemplateMeta[] }>({
queryKey: ['admin', 'email', 'test-template', 'list'],
queryFn: () => apiFetch('/api/v1/admin/email/test-template'),
staleTime: 5 * 60_000,
});
const templates = data?.data ?? [];
const selected = templates.find((t) => t.id === templateId);
const mutation = useMutation({
mutationFn: () =>
apiFetch<{
data: { templateId: string; recipient: string; subject: string; messageId: string | null };
}>('/api/v1/admin/email/test-template', {
method: 'POST',
body: { templateId, recipient },
}),
onSuccess: (res) => {
const label = selected?.label ?? res.data.templateId;
setLastResult({
kind: 'ok',
label,
recipient: res.data.recipient,
messageId: res.data.messageId,
});
toast.success(`${label} sent to ${res.data.recipient}`);
},
onError: (err) => {
const message = err instanceof Error ? err.message : 'Send failed';
setLastResult({ kind: 'err', message });
toastError(err);
},
});
const canSend = !!templateId && /.+@.+\..+/.test(recipient) && !mutation.isPending;
return (
<Card>
<CardHeader>
<CardTitle>Test each transactional email</CardTitle>
<CardDescription>
Pick a template and fire a realistic preview to a designated address. The subject is
prefixed <span className="font-mono">[TEST · &lt;template&gt;]</span> so it&apos;s
unambiguous in the recipient&apos;s inbox. Uses the port&apos;s real From / SMTP
configuration - the same path the live flow takes.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="test-template-select">Template</Label>
{isLoading ? (
<Skeleton className="h-9 w-full" />
) : (
<Select value={templateId} onValueChange={setTemplateId}>
<SelectTrigger id="test-template-select" className="w-full">
<SelectValue placeholder="Select a template…" />
</SelectTrigger>
<SelectContent>
{templates.map((t) => (
<SelectItem key={t.id} value={t.id}>
{t.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{selected ? (
<p className="text-xs text-muted-foreground">{selected.description}</p>
) : (
<p className="text-xs text-muted-foreground">
Tip: pick a template above to see what production flow fires it.
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="test-template-recipient">Recipient</Label>
<div className="flex flex-wrap items-center gap-2">
<Input
id="test-template-recipient"
type="email"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="you@example.com"
className="flex-1 min-w-[240px]"
/>
<Button onClick={() => mutation.mutate()} disabled={!canSend}>
<Send className="mr-1.5 h-4 w-4" aria-hidden />
{mutation.isPending ? 'Sending…' : 'Send test'}
</Button>
</div>
</div>
{lastResult?.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">
{lastResult.label} sent to {lastResult.recipient}.
</div>
{lastResult.messageId ? (
<div className="text-xs text-emerald-800/80">
Message-ID: <span className="font-mono">{lastResult.messageId}</span>
</div>
) : null}
<div className="text-xs text-emerald-800/80">
Check the recipient&apos;s mailbox (and spam folder) to confirm delivery.
</div>
</div>
</div>
)}
{lastResult?.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">Send failed</div>
<div className="font-mono text-xs break-all">{lastResult.message}</div>
</div>
</div>
)}
</CardContent>
</Card>
);
}