From cc159f9b6b2cf4b8a1d249d13030d1b602b7611a Mon Sep 17 00:00:00 2001 From: Gerhard Scheikl Date: Sat, 9 May 2026 17:09:33 +0200 Subject: [PATCH] feat(ui): add Send/Re-send button on invoices page and order block --- app/routes/app.invoices.tsx | 18 +++- .../src/BlockExtension.tsx | 94 ++++++++++++++++--- 2 files changed, 97 insertions(+), 15 deletions(-) diff --git a/app/routes/app.invoices.tsx b/app/routes/app.invoices.tsx index 5901367..23a322a 100644 --- a/app/routes/app.invoices.tsx +++ b/app/routes/app.invoices.tsx @@ -234,13 +234,16 @@ function FilterChip({ function OrderRow({ order }: { order: RecentOrder }) { const fetcher = useFetcher<{ ok: boolean; error?: string; invoiceNumber?: string }>(); + const sendFetcher = useFetcher<{ ok: boolean; error?: string }>(); const isBusy = fetcher.state !== "idle"; + const isSending = sendFetcher.state !== "idle"; const isCancelReissue = order.hasInvoice && order.invoiceSent; const buttonLabel = !order.hasInvoice ? "Generate" : order.invoiceSent ? "Cancel & reissue" : "Regenerate"; + const sendLabel = order.invoiceSent ? "Re-send" : "Send"; return ( @@ -271,6 +274,9 @@ function OrderRow({ order }: { order: RecentOrder }) { {fetcher.data?.error ? ( {fetcher.data.error} ) : null} + {sendFetcher.data?.error ? ( + {sendFetcher.data.error} + ) : null} ) : ( @@ -289,13 +295,23 @@ function OrderRow({ order }: { order: RecentOrder }) { ) : null} {isBusy ? "Working…" : buttonLabel} + + + + {isSending ? "Sending…" : sendLabel} + + diff --git a/extensions/invoice-order-block/src/BlockExtension.tsx b/extensions/invoice-order-block/src/BlockExtension.tsx index 8aac57b..e7a0910 100644 --- a/extensions/invoice-order-block/src/BlockExtension.tsx +++ b/extensions/invoice-order-block/src/BlockExtension.tsx @@ -30,11 +30,16 @@ function Extension() { const [payload, setPayload] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [busy, setBusy] = useState(null); + const [actionError, setActionError] = useState(null); + const [actionInfo, setActionInfo] = useState(null); + const [reloadKey, setReloadKey] = useState(0); useEffect(() => { if (!orderId) return; let cancelled = false; (async () => { + setLoading(true); try { const res = await fetch(`/api/orders/${orderId}/invoice`); if (!res.ok) throw new Error(`HTTP ${res.status}`); @@ -49,7 +54,36 @@ function Extension() { return () => { cancelled = true; }; - }, [orderId]); + }, [orderId, reloadKey]); + + async function trigger(action: "generate" | "send" | "cancel_reissue") { + if (!orderId) return; + setBusy(action); + setActionError(null); + setActionInfo(null); + try { + const body = new URLSearchParams({ action }); + const res = await fetch(`/api/orders/${orderId}/invoice`, { method: "POST", body }); + const txt = await res.text(); + if (!res.ok) throw new Error(`HTTP ${res.status}: ${txt.slice(0, 200)}`); + setActionInfo( + action === "send" + ? "Invoice email sent." + : action === "cancel_reissue" + ? "Cancelled and reissued." + : "Invoice generated.", + ); + setReloadKey((k) => k + 1); + } catch (e: any) { + setActionError(e?.message ?? "Action failed"); + } finally { + setBusy(null); + } + } + + const latest = payload?.latest; + const hasInvoice = !!latest && !latest.cancelledAt; + const sent = !!latest?.sentAt; return ( @@ -57,21 +91,53 @@ function Extension() { Loading… ) : error ? ( {error} - ) : !payload?.latest ? ( - No invoice yet for this order. ) : ( - {payload.latest.invoiceNumber} (v{payload.latest.version}) - Issued {new Date(payload.latest.issuedAt).toLocaleDateString()} - - {payload.latest.sentAt ? "Sent" : "Not sent"} - - {payload.latest.pdfUrl ? ( - View PDF - ) : null} - {payload.history.length > 1 ? ( - {payload.history.length} versions in history - ) : null} + {latest ? ( + + + {latest.invoiceNumber} (v{latest.version}) + + Issued {new Date(latest.issuedAt).toLocaleDateString()} + {sent ? "Sent" : "Not sent"} + {latest.pdfUrl ? ( + + View PDF + + ) : null} + {payload!.history.length > 1 ? ( + {payload!.history.length} versions in history + ) : null} + + ) : ( + No invoice yet for this order. + )} + + {actionError ? {actionError} : null} + {actionInfo ? {actionInfo} : null} + + + {!hasInvoice ? ( + trigger("generate")} disabled={busy !== null}> + {busy === "generate" ? "Generating…" : "Generate"} + + ) : !sent ? ( + trigger("generate")} disabled={busy !== null}> + {busy === "generate" ? "Working…" : "Regenerate"} + + ) : ( + trigger("cancel_reissue")} + disabled={busy !== null} + tone="critical" + > + {busy === "cancel_reissue" ? "Working…" : "Cancel & reissue"} + + )} + trigger("send")} disabled={busy !== null}> + {busy === "send" ? "Sending…" : sent ? "Re-send" : "Send"} + + )}