import { useCallback, useMemo, useRef, useState } from "react"; import { useListContext, useStore } from "react-admin"; import request from "@/utils/request"; import { DEFAULT_PRINT_PREVIEW_FETCH_PAGE_SIZE, downloadBlobFile, resolveReportMeta, resolveVisibleColumns, } from "./listReportUtils"; const getPreferencePath = (preferenceKey, field) => `preferences.${preferenceKey || "__listReport__"}.${field}`; export const useListReportActionParams = (reportConfig, options = {}) => { const { ids: explicitIds } = options; const { filter, filterValues, resource, selectedIds, sort, total } = useListContext(); const [storedColumns] = useStore( getPreferencePath(reportConfig?.preferenceKey, "columns"), [] ); const [availableColumns] = useStore( getPreferencePath(reportConfig?.preferenceKey, "availableColumns"), [] ); const [storedOmit] = useStore( getPreferencePath(reportConfig?.preferenceKey, "omit"), [] ); const visibleColumns = useMemo( () => resolveVisibleColumns({ columnDefs: reportConfig?.columns, storedColumns, availableColumns, storedOmit, omit: reportConfig?.omit, }), [availableColumns, reportConfig?.columns, reportConfig?.omit, storedColumns, storedOmit] ); return { visibleColumns, params: { filter, filterValues, ids: explicitIds ?? selectedIds, resource: reportConfig?.resource || resource, sort, total, }, }; }; export const useListReportOutput = ({ reportConfig, buildRequestPayload, notify, }) => { const [exportLoading, setExportLoading] = useState(false); const [previewOpen, setPreviewOpen] = useState(false); const [previewRows, setPreviewRows] = useState([]); const [previewColumns, setPreviewColumns] = useState([]); const [previewMeta, setPreviewMeta] = useState(null); const [previewDataset, setPreviewDataset] = useState({ loadedPages: 0, transportPages: 0, pageSize: reportConfig?.previewPageSize || DEFAULT_PRINT_PREVIEW_FETCH_PAGE_SIZE, total: 0, fullyLoaded: false, }); const [previewLoading, setPreviewLoading] = useState(false); const [previewPrefetching, setPreviewPrefetching] = useState(false); const requestIdRef = useRef(0); const cacheRef = useRef(new Map()); const previewPageSize = reportConfig?.previewPageSize || DEFAULT_PRINT_PREVIEW_FETCH_PAGE_SIZE; const buildPayload = useCallback( params => { const payload = typeof buildRequestPayload === "function" ? buildRequestPayload(params) : {}; if (payload.reportMeta) { return payload; } return { ...payload, reportMeta: resolveReportMeta(reportConfig, params), }; }, [buildRequestPayload, reportConfig] ); const resetPreviewCache = useCallback(() => { cacheRef.current = new Map(); setPreviewRows([]); setPreviewColumns([]); setPreviewMeta(null); setPreviewDataset({ loadedPages: 0, transportPages: 0, pageSize: previewPageSize, total: 0, fullyLoaded: false, }); }, [previewPageSize]); const fetchPreviewPage = useCallback( async ({ payload, current }) => { const { data: { code, data, msg } } = await request.post( `/${reportConfig.resource}/print/query`, { ...payload, current, pageSize: previewPageSize, } ); if (code !== 200) { throw new Error(msg || "Print preview query failed"); } return data || {}; }, [previewPageSize, reportConfig.resource] ); const syncPreviewCache = useCallback( ({ totalPages, totalRows }) => { const nextRows = []; for (let pageIndex = 1; pageIndex <= totalPages; pageIndex += 1) { const pageRows = cacheRef.current.get(pageIndex); if (Array.isArray(pageRows)) { nextRows.push(...pageRows); } } setPreviewRows(nextRows); setPreviewDataset({ loadedPages: cacheRef.current.size, transportPages: totalPages, pageSize: previewPageSize, total: totalRows, fullyLoaded: cacheRef.current.size >= totalPages, }); }, [previewPageSize] ); const applyPreviewPage = useCallback( ({ data, payload, openPreview = false }) => { const current = Number(data.current) || 1; const totalPages = Number(data.pages) || 0; const totalRows = Number(data.total) || 0; setPreviewColumns(payload.columns || []); setPreviewMeta(payload.reportMeta || null); cacheRef.current.set(current, Array.isArray(data.records) ? data.records : []); syncPreviewCache({ totalPages, totalRows }); if (openPreview) { setPreviewOpen(true); } }, [syncPreviewCache] ); const prefetchPreviewPages = useCallback( async ({ payload, totalPages, totalRows, requestId }) => { if (totalPages <= 1) { setPreviewPrefetching(false); return; } setPreviewPrefetching(true); const pendingPages = Array.from({ length: totalPages - 1 }, (_, index) => index + 2); const workerCount = Math.min(3, pendingPages.length); let hasErrored = false; const worker = async () => { while (pendingPages.length > 0 && requestIdRef.current === requestId) { const nextPage = pendingPages.shift(); if (!nextPage) { return; } try { const data = await fetchPreviewPage({ payload, current: nextPage }); if (requestIdRef.current !== requestId) { return; } cacheRef.current.set(nextPage, Array.isArray(data.records) ? data.records : []); syncPreviewCache({ totalPages, totalRows }); } catch (error) { if (!hasErrored && requestIdRef.current === requestId) { hasErrored = true; console.error(error); notify(error.message || "ra.notification.http_error", { type: "error" }); } return; } } }; await Promise.all(Array.from({ length: workerCount }, () => worker())); if (requestIdRef.current === requestId) { setPreviewPrefetching(false); } }, [fetchPreviewPage, notify, syncPreviewCache] ); const openPrintPreview = useCallback( async params => { if (!reportConfig?.enablePrint) { return; } setPreviewLoading(true); try { requestIdRef.current += 1; const requestId = requestIdRef.current; resetPreviewCache(); const payload = buildPayload(params); const data = await fetchPreviewPage({ payload, current: 1 }); applyPreviewPage({ data, payload, openPreview: true }); prefetchPreviewPages({ payload, totalPages: Number(data.pages) || 0, totalRows: Number(data.total) || 0, requestId, }); } catch (error) { console.error(error); notify(error.message || "ra.notification.http_error", { type: "error" }); } finally { setPreviewLoading(false); } }, [ applyPreviewPage, buildPayload, fetchPreviewPage, notify, prefetchPreviewPages, reportConfig?.enablePrint, resetPreviewCache, ] ); const closePrintPreview = useCallback(() => { requestIdRef.current += 1; setPreviewPrefetching(false); setPreviewOpen(false); }, []); const exportReport = useCallback( async params => { setExportLoading(true); try { const payload = buildPayload(params); const response = await request.post(`/${reportConfig.resource}/export`, payload, { responseType: "blob", }); downloadBlobFile( response, reportConfig.exportFileName || `${reportConfig.resource}.xlsx` ); } catch (error) { console.error(error); notify("ra.notification.http_error", { type: "error" }); } finally { setExportLoading(false); } }, [buildPayload, notify, reportConfig.exportFileName, reportConfig.resource] ); return { exportLoading, previewOpen, previewRows, previewColumns, previewMeta, previewDataset, previewLoading, previewPrefetching, openPrintPreview, closePrintPreview, exportReport, }; };