127 lines
4.5 KiB
TypeScript
127 lines
4.5 KiB
TypeScript
// SendEQSLModal — per-QSO eQSL flow: render the default template with the
|
|
// QSO's data, show the exact card, send it as a JPEG e-mail attachment on
|
|
// explicit confirmation (never automatic). On success the backend stamps
|
|
// eqsl_sent and emits "qsl:sent" so the grid refreshes.
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { Loader2, Mail, CheckCircle2 } from 'lucide-react';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
QSLDefaultTemplateID, RenderEQSL, SendEQSL,
|
|
} from '../../../wailsjs/go/main/App';
|
|
import type { RenderModel } from './qslTypes';
|
|
import { loadCardAssets, type CardAssets } from './qslAssets';
|
|
import { CardPreview } from './CardPreview';
|
|
import { rasterizeCard } from './rasterize';
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
qsoId: number | null;
|
|
onClose: () => void;
|
|
onOpenDesigner: () => void;
|
|
}
|
|
|
|
export function SendEQSLModal({ open, qsoId, onClose, onOpenDesigner }: Props) {
|
|
const [model, setModel] = useState<RenderModel | null>(null);
|
|
const [assets, setAssets] = useState<CardAssets | null>(null);
|
|
const [templateId, setTemplateId] = useState(0);
|
|
const [busy, setBusy] = useState(false);
|
|
const [sent, setSent] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const svgEl = useRef<SVGSVGElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open || !qsoId) return;
|
|
setModel(null);
|
|
setAssets(null);
|
|
setSent(false);
|
|
setError('');
|
|
void (async () => {
|
|
try {
|
|
const tid = await QSLDefaultTemplateID();
|
|
setTemplateId(tid);
|
|
if (!tid) return; // no template yet — offer the designer below
|
|
const m = JSON.parse(await RenderEQSL(qsoId, tid)) as RenderModel;
|
|
setModel(m);
|
|
setAssets(await loadCardAssets(m.template, tid));
|
|
} catch (e) {
|
|
setError(String(e));
|
|
}
|
|
})();
|
|
}, [open, qsoId]);
|
|
|
|
async function send() {
|
|
if (!qsoId || !model || !svgEl.current) return;
|
|
setBusy(true);
|
|
setError('');
|
|
try {
|
|
const card = model.template.card;
|
|
const jpeg = await rasterizeCard(svgEl.current, card.w, card.h, 'image/jpeg');
|
|
await SendEQSL(qsoId, templateId, jpeg);
|
|
setSent(true);
|
|
// Auto-close shortly after the success tick (the grid refreshes on the
|
|
// qsl:sent event); the user doesn't need to dismiss it manually.
|
|
window.setTimeout(onClose, 1100);
|
|
} catch (e) {
|
|
setError(String(e));
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
|
<DialogContent className="max-w-[820px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<Mail className="size-5 text-danger" /> Send OpsLog QSL by e-mail
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-3 px-6 py-5">
|
|
{error && (
|
|
<div className="rounded border border-destructive bg-destructive-muted px-3 py-1.5 text-sm text-destructive-muted-foreground">{error}</div>
|
|
)}
|
|
|
|
{open && !templateId && !error && (
|
|
<div className="space-y-3 py-2 text-sm">
|
|
<p>No QSL template yet — design one first (drop a few photos, pick a layout).</p>
|
|
<Button size="sm" onClick={() => { onClose(); onOpenDesigner(); }}>Open the designer</Button>
|
|
</div>
|
|
)}
|
|
|
|
{templateId > 0 && !model && !error && (
|
|
<div className="flex items-center gap-2 py-6 text-sm text-muted-foreground">
|
|
<Loader2 className="size-4 animate-spin" /> Preparing the card…
|
|
</div>
|
|
)}
|
|
|
|
{model && assets && (
|
|
<div className="rounded-md bg-slate-800/60 p-3">
|
|
<CardPreview model={model} assets={assets} width={740}
|
|
svgRef={(el) => { svgEl.current = el; }} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
{sent ? (
|
|
<div className="flex items-center gap-2 text-sm text-success">
|
|
<CheckCircle2 className="size-4" /> OpsLog QSL sent.
|
|
<Button variant="outline" size="sm" onClick={onClose}>Close</Button>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<Button variant="outline" size="sm" onClick={onClose}>Cancel</Button>
|
|
<Button size="sm" disabled={!model || busy} onClick={send}>
|
|
{busy && <Loader2 className="mr-1 size-4 animate-spin" />} Send
|
|
</Button>
|
|
</>
|
|
)}
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|