import type { LoaderFunctionArgs } from "react-router"; import { Link, useLoaderData, useNavigation, useFetcher } from "react-router"; import { authenticate } from "../shopify.server"; import db from "../db.server"; interface RecentOrder { id: string; // gid name: string; createdAt: string; totalPrice: string; currency: string; hasInvoice: boolean; invoiceNumber?: string; invoiceSent?: boolean; invoiceCancelled?: boolean; pdfUrl?: string; } const RECENT_ORDERS_QUERY = `#graphql query RecentOrders($first: Int!) { orders(first: $first, sortKey: CREATED_AT, reverse: true) { nodes { id name createdAt displayFinancialStatus totalPriceSet { shopMoney { amount currencyCode } } } } } `; export const loader = async ({ request }: LoaderFunctionArgs) => { const { admin, session } = await authenticate.admin(request); const url = new URL(request.url); const filter = url.searchParams.get("filter") ?? "all"; // Recent orders from Shopify (first 25). let orders: RecentOrder[] = []; try { const res = await admin.graphql(RECENT_ORDERS_QUERY, { variables: { first: 25 } }); const json = (await res.json()) as { data?: { orders?: { nodes?: Array<{ id: string; name: string; createdAt: string; totalPriceSet?: { shopMoney: { amount: string; currencyCode: string } }; }>; }; }; }; const nodes = json.data?.orders?.nodes ?? []; const orderIds = nodes.map((n) => n.id); // Look up which of these orders already have a non-cancelled invoice. const invoices = await db.invoice.findMany({ where: { shopDomain: session.shop, orderId: { in: orderIds }, kind: "invoice", }, orderBy: [{ version: "desc" }, { createdAt: "desc" }], }); const latestByOrder = new Map(); for (const inv of invoices) { if (!latestByOrder.has(inv.orderId)) latestByOrder.set(inv.orderId, inv); } orders = nodes.map((n) => { const inv = latestByOrder.get(n.id); return { id: n.id, name: n.name, createdAt: n.createdAt, totalPrice: n.totalPriceSet?.shopMoney.amount ?? "", currency: n.totalPriceSet?.shopMoney.currencyCode ?? "EUR", hasInvoice: !!inv && !inv.cancelledAt, invoiceNumber: inv?.invoiceNumber, invoiceSent: !!inv?.sentAt, invoiceCancelled: !!inv?.cancelledAt, pdfUrl: inv?.pdfUrl, }; }); } catch (err) { console.warn("Failed to load recent orders:", err); } if (filter === "missing") orders = orders.filter((o) => !o.hasInvoice); if (filter === "with") orders = orders.filter((o) => o.hasInvoice); return { orders, filter }; }; export default function InvoicesPage() { const { orders, filter } = useLoaderData(); const navigation = useNavigation(); const isLoading = navigation.state !== "idle"; return ( Generate or view the latest invoice for each order. For sent invoices, use Cancel & reissue to issue a Stornorechnung followed by a fresh invoice. All Missing invoice Has invoice {isLoading ? ( Loading… ) : orders.length === 0 ? ( No orders match the current filter. ) : ( {orders.map((o) => ( ))} )} Use the buttons next to each order to generate one at a time. (A true multi-select bulk run will be added once the admin block extension is installed.) Tip: filter by Missing invoice to see orders that still need one. ); } function OrderRow({ order }: { order: RecentOrder }) { const numericId = order.id.replace(/^.*\//, ""); const fetcher = useFetcher<{ ok: boolean; error?: string; invoiceNumber?: string }>(); const isBusy = fetcher.state !== "idle"; return ( {order.name} {order.totalPrice} {order.currency} {order.hasInvoice ? ( ✓ {order.invoiceNumber} {order.invoiceCancelled ? " (cancelled)" : order.invoiceSent ? " (sent)" : ""} {order.pdfUrl ? ( <> {" — "} PDF ) : null} ) : ( — no invoice yet )} {order.hasInvoice ? order.invoiceSent ? "Cancel & reissue" : "Regenerate" : "Generate"} {order.hasInvoice && order.invoiceSent && ( )} {fetcher.data?.error && ( {fetcher.data.error} )} ); }