import { useState } from "react"; import { useClient } from "urql"; import { graphql, type ResultOf, type VariablesOf } from "@/graphql"; import { useToast } from "@/hooks/use-toast"; import { fetchAllPages } from "@/lib/fetch-all-pages"; import { toDateInputValue } from "@/lib/format-date"; import { csvTimestamp, downloadCsv, toCsv, type CsvColumn } from "@/lib/to-csv"; /** * CSV export for the Purchase Orders dashboard (issue #605). * * The dashboard query is cursor-paginated (25/page); `fetchAllPages` walks every * page (1000/page, the Tailor cap) so the export covers the full filtered/sorted * result set. If PO volume grows past what this can fetch in a reasonable time, * this is the seam to swap for a backend bulk-export resolver. */ const PurchaseOrdersCsvQuery = graphql(` query PurchaseOrdersCsv( $first: Int $after: String $order: [PurchaseOrderOrderInput] $query: PurchaseOrderQueryInput ) { purchaseOrders(first: $first, after: $after, order: $order, query: $query) { pageInfo { hasNextPage endCursor } edges { node { docNumber supplierSnapshotName orderDate orderStatus receiptStatus billingStatus externalSupplierOrderReference createdAt receivingSite { name } lines(first: 1000) { edges { node { quantity unitPrice } } } } } } } `); type CsvVariables = VariablesOf; type CsvResult = ResultOf; type PurchaseOrderCsvRow = NonNullable< NonNullable["edges"] >[number]["node"]; const lineTotal = (row: PurchaseOrderCsvRow): number => row.lines.edges.reduce( (sum, edge) => sum + Number(edge.node.quantity ?? 0) * Number(edge.node.unitPrice ?? 0), 0, ); // Columns follow the order issue #605 specifies, with header labels matching the // PO UI (detail page / create form). Total Amount + Created Date are the "other // fields visible in the dashboard" the issue also asks for. const columns: CsvColumn[] = [ { header: "Document Number", value: (r) => r.docNumber }, { header: "Supplier Name", value: (r) => r.supplierSnapshotName }, { header: "Receiving Site", value: (r) => r.receivingSite?.name }, { header: "Order Date", value: (r) => toDateInputValue(r.orderDate) }, { header: "Status", value: (r) => r.orderStatus }, { header: "Receipt Status", value: (r) => r.receiptStatus }, { header: "Billing Status", value: (r) => r.billingStatus }, { header: "External Reference", value: (r) => r.externalSupplierOrderReference }, { header: "Total Amount", value: (r) => lineTotal(r).toFixed(2) }, { header: "Created Date", value: (r) => toDateInputValue(r.createdAt) }, ]; export function useExportPoCsv() { const client = useClient(); const toast = useToast(); const [isExporting, setIsExporting] = useState(false); /** * Export all rows matching the current dashboard filter/sort to a CSV. * Pass the same `order` and `query` the dashboard query uses so the export * respects the active filter + sort state. */ async function exportCsv(order: CsvVariables["order"], query: CsvVariables["query"]) { if (isExporting) return; setIsExporting(true); try { const rows = await fetchAllPages( client, PurchaseOrdersCsvQuery, { order, query }, (data) => data.purchaseOrders, ); if (rows.length === 0) { toast.error("No purchase orders to export"); return; } downloadCsv(toCsv(rows, columns), `purchase-orders-${csvTimestamp()}.csv`); toast.success(`${rows.length.toLocaleString()} rows exported`); } catch (err) { console.error("CSV export failed:", err); toast.error("Failed to export CSV"); } finally { setIsExporting(false); } } return { exportCsv, isExporting }; }