import { useState, useEffect, useRef, useCallback } from 'react' import { PdfExportOptions } from './PdfExportOptions' export type JobPhase = "idle" | "creating_job" | "polling_job" | "success" | "error" | "cancelling" export interface UsePdfExportJobOptions { /** URL to POST to for creating the job */ jobCreationUrl: string /** Base API URL for job polling */ apiUrl: string /** PDF export options */ options: PdfExportOptions /** Optional HTML content to include in request */ html?: string /** Returns extra fields for the job creation request body. Called when the job starts, so it * captures state at that moment. */ getExtraBody?: () => { [key: string]: any } } export interface UsePdfExportJobResult { /** Current phase of the export job */ phase: JobPhase /** Current job ID, if available */ jobId: string | null /** Status message for display */ statusMessage: string | null /** Full job details from polling */ jobDetails: any | null /** Start the job creation and polling process */ startJob: () => Promise /** Cancel the current job */ cancelJob: () => Promise /** Reset the job state to idle */ reset: () => void } /** * Hook for managing PDF export job creation and polling. * Handles all the async job logic including creation, polling, and cancellation. */ export function usePdfExportJob(hookOptions: UsePdfExportJobOptions): UsePdfExportJobResult { const { jobCreationUrl, apiUrl, options, html, getExtraBody } = hookOptions const [phase, setPhase] = useState("idle") const [jobId, setJobId] = useState(null) const [statusMessage, setStatusMessage] = useState(null) const [jobDetails, setJobDetails] = useState(null) const pollingIntervalIdRef = useRef | null>(null) const reset = useCallback(() => { if (pollingIntervalIdRef.current) { clearInterval(pollingIntervalIdRef.current) pollingIntervalIdRef.current = null } setPhase("idle") setJobId(null) setStatusMessage(null) setJobDetails(null) }, []) const startJob = useCallback(async () => { if (phase === "creating_job") return // Prevent double creation setPhase("creating_job") setStatusMessage(T`Generating PDF...`) setJobId(null) setJobDetails(null) try { // Include HTML in body if provided, with PDF options in pdfOptions field const body = { ...getExtraBody?.(), html, pdfOptions: options } const response = await fetch(jobCreationUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(body) }) if (!response.ok) { const errorData = await response.json().catch(() => ({})) throw new Error(errorData.error || errorData.message || `Failed to create job (status ${response.status})`) } const result = await response.json() if (result.jobId) { setJobId(result.jobId) setPhase("polling_job") } else { throw new Error(T`Failed to get job ID from server.`) } } catch (err: any) { setPhase("error") setStatusMessage(err.message || T`An unexpected error occurred while generating the PDF.`) } }, [jobCreationUrl, options, html, getExtraBody, phase]) const cancelJob = useCallback(async () => { if ((phase === "polling_job" || phase === "creating_job") && jobId) { setPhase("cancelling") setStatusMessage(T`Cancelling PDF generation...`) try { await fetch(`${apiUrl}jobs/${jobId}/cancel`, { method: 'POST' }) } catch (err) { console.error("Error attempting to cancel job on backend:", err) } } // Stop polling immediately if (pollingIntervalIdRef.current) { clearInterval(pollingIntervalIdRef.current) pollingIntervalIdRef.current = null } if (phase === "polling_job" || phase === "creating_job" || phase === "cancelling") { setPhase("idle") } }, [phase, jobId, apiUrl]) // Polling effect useEffect(() => { if (phase !== "polling_job" || !jobId) { if (pollingIntervalIdRef.current) { clearInterval(pollingIntervalIdRef.current) pollingIntervalIdRef.current = null } return } // Clear previous interval if any if (pollingIntervalIdRef.current) { clearInterval(pollingIntervalIdRef.current) } pollingIntervalIdRef.current = setInterval(async () => { try { const response = await fetch(`${apiUrl}jobs/${jobId}`) if (!response.ok) { if (phase === "polling_job") { setStatusMessage(T`Error getting status (status ${response.status}). Retrying...`) } return // Keep polling } const job = await response.json() setJobDetails(job) if (job.data && job.data.message) { setStatusMessage(job.data.message) } else { // Fallback status message based on job status switch (job.status) { case "working": setStatusMessage(T`Generating PDF...`); break case "success": setStatusMessage(T`PDF ready for download.`); break case "error": setStatusMessage(job.data?.message || T`PDF generation failed.`); break case "cancelled": setStatusMessage(T`PDF generation cancelled.`); break default: setStatusMessage(T`Job status: ${job.status}`); break } } if (job.status === "success") { setPhase("success") if (pollingIntervalIdRef.current) clearInterval(pollingIntervalIdRef.current) pollingIntervalIdRef.current = null } else if (job.status === "error" || job.status === "cancelled") { setPhase("error") if (pollingIntervalIdRef.current) clearInterval(pollingIntervalIdRef.current) pollingIntervalIdRef.current = null } else if (job.status === "cancelling") { setStatusMessage(job.data?.message || T`Print is being cancelled...`) } } catch (err) { if (phase === "polling_job") { setStatusMessage(T`Network error while checking job status. Retrying...`) } } }, 3000) return () => { if (pollingIntervalIdRef.current) { clearInterval(pollingIntervalIdRef.current) pollingIntervalIdRef.current = null } } }, [phase, jobId, apiUrl]) // Cleanup on unmount useEffect(() => { return () => { if (pollingIntervalIdRef.current) { clearInterval(pollingIntervalIdRef.current) pollingIntervalIdRef.current = null } } }, []) return { phase, jobId, statusMessage, jobDetails, startJob, cancelJob, reset } }