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"}
+
+
)}