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 Inbound Shipments dashboard (issue #605). Pages through the * full filtered/sorted result set via `fetchAllPages` and serializes the * dashboard's visible columns. */ const InboundShipmentsCsvQuery = graphql(` query InboundShipmentsCsv( $first: Int $after: String $order: [InboundShipmentOrderInput] $query: InboundShipmentQueryInput ) { inboundShipments(first: $first, after: $after, order: $order, query: $query) { pageInfo { hasNextPage endCursor } edges { node { id docNumber status effectiveDate lines(first: 1) { total } } } } } `); // Open Qty is computed server-side by a sibling resolver (not a real column), so // it isn't part of the cursor query. We fetch it for the full result set after // paging, in id-batches. const OpenQuantitiesQuery = graphql(` query InboundShipmentCsvOpenQuantities($ids: [String!]!) { inboundShipmentOpenQuantities(ids: $ids) { items { id openQuantity } } } `); // Batch size for the open-quantities lookup. Kept separate from the cursor // PAGE_SIZE (1000): this resolver computes a value per id server-side, so it's // heavier per row than a plain page read and is tuned independently. const OPEN_QTY_BATCH_SIZE = 500; type CsvVariables = VariablesOf; type CsvResult = ResultOf; type InboundShipmentNode = NonNullable< NonNullable["edges"] >[number]["node"]; type InboundShipmentCsvRow = InboundShipmentNode & { openQuantity?: string }; // Header labels match the GR dashboard columns. const columns: CsvColumn[] = [ { header: "Shipment Number", value: (r) => r.docNumber }, { header: "Status", value: (r) => r.status }, { header: "Receipt Date", value: (r) => toDateInputValue(r.effectiveDate) }, { header: "Lines", value: (r) => r.lines.total }, { header: "Open Qty", value: (r) => r.openQuantity }, ]; export function useExportInboundShipmentsCsv() { const client = useClient(); const toast = useToast(); const [isExporting, setIsExporting] = useState(false); async function fetchOpenQuantities(ids: string[]): Promise> { const map: Record = {}; for (let i = 0; i < ids.length; i += OPEN_QTY_BATCH_SIZE) { const chunk = ids.slice(i, i + OPEN_QTY_BATCH_SIZE); const result = await client .query(OpenQuantitiesQuery, { ids: chunk }, { requestPolicy: "network-only" }) .toPromise(); if (result.error) throw result.error; for (const item of result.data?.inboundShipmentOpenQuantities.items ?? []) { map[item.id] = item.openQuantity; } } return map; } async function exportCsv(order: CsvVariables["order"], query: CsvVariables["query"]) { if (isExporting) return; setIsExporting(true); try { const nodes = await fetchAllPages( client, InboundShipmentsCsvQuery, { order, query }, (data) => data.inboundShipments, ); if (nodes.length === 0) { toast.error("No inbound shipments to export"); return; } const openByGr = await fetchOpenQuantities(nodes.map((n) => n.id)); const rows: InboundShipmentCsvRow[] = nodes.map((n) => ({ ...n, openQuantity: openByGr[n.id], })); downloadCsv(toCsv(rows, columns), `inbound-shipments-${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 }; }