MOPC-App/src/components/shared/export-pdf-button.tsx

192 lines
5.8 KiB
TypeScript

'use client'
import { useState, useCallback, type RefObject } from 'react'
import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button'
import { FileDown, Loader2 } from 'lucide-react'
import { toast } from 'sonner'
import {
createReportDocument,
addCoverPage,
addPageBreak,
addHeader,
addSectionTitle,
addStatCards,
addTable,
addChartImage,
addAllPageFooters,
savePdf,
} from '@/lib/pdf-generator'
interface ExportPdfButtonProps {
roundId: string
roundName?: string
programName?: string
chartRefs?: Record<string, RefObject<HTMLDivElement | null>>
variant?: 'default' | 'outline' | 'secondary' | 'ghost'
size?: 'default' | 'sm' | 'lg' | 'icon'
}
export function ExportPdfButton({
roundId,
roundName,
programName,
chartRefs,
variant = 'outline',
size = 'sm',
}: ExportPdfButtonProps) {
const [generating, setGenerating] = useState(false)
const { refetch } = trpc.export.getReportData.useQuery(
{ roundId, sections: [] },
{ enabled: false }
)
const handleGenerate = useCallback(async () => {
setGenerating(true)
toast.info('Generating PDF report...')
try {
const result = await refetch()
if (!result.data) {
toast.error('Failed to fetch report data')
return
}
const data = result.data as Record<string, unknown>
const rName = roundName || String(data.roundName || 'Report')
const pName = programName || String(data.programName || '')
// 1. Create document
const doc = await createReportDocument()
// 2. Cover page
await addCoverPage(doc, {
title: 'Round Report',
subtitle: `${pName} ${data.programYear ? `(${data.programYear})` : ''}`.trim(),
roundName: rName,
programName: pName,
})
// 3. Summary section
const summary = data.summary as Record<string, unknown> | undefined
if (summary) {
addPageBreak(doc)
await addHeader(doc, rName)
let y = addSectionTitle(doc, 'Summary', 28)
y = addStatCards(doc, [
{ label: 'Projects', value: String(summary.projectCount ?? 0) },
{ label: 'Evaluations', value: String(summary.evaluationCount ?? 0) },
{
label: 'Avg Score',
value: summary.averageScore != null
? Number(summary.averageScore).toFixed(1)
: '--',
},
{
label: 'Completion',
value: summary.completionRate != null
? `${Number(summary.completionRate).toFixed(0)}%`
: '--',
},
], y)
// Capture chart images if refs provided
if (chartRefs) {
for (const [, ref] of Object.entries(chartRefs)) {
if (ref.current) {
try {
y = await addChartImage(doc, ref.current, y, { maxHeight: 90 })
} catch {
// Skip chart if capture fails
}
}
}
}
}
// 4. Rankings section
const rankings = data.rankings as Array<Record<string, unknown>> | undefined
if (rankings && rankings.length > 0) {
addPageBreak(doc)
await addHeader(doc, rName)
let y = addSectionTitle(doc, 'Project Rankings', 28)
const headers = ['#', 'Project', 'Team', 'Avg Score', 'Evaluations', 'Yes %']
const rows = rankings.map((r, i) => [
i + 1,
String(r.title ?? ''),
String(r.teamName ?? ''),
r.averageScore != null ? Number(r.averageScore).toFixed(2) : '-',
String(r.evaluationCount ?? 0),
r.yesPercentage != null ? `${Number(r.yesPercentage).toFixed(0)}%` : '-',
])
y = addTable(doc, headers, rows, y)
}
// 5. Juror stats section
const jurorStats = data.jurorStats as Array<Record<string, unknown>> | undefined
if (jurorStats && jurorStats.length > 0) {
addPageBreak(doc)
await addHeader(doc, rName)
let y = addSectionTitle(doc, 'Juror Statistics', 28)
const headers = ['Juror', 'Assigned', 'Completed', 'Completion %', 'Avg Score']
const rows = jurorStats.map((j) => [
String(j.name ?? ''),
String(j.assigned ?? 0),
String(j.completed ?? 0),
`${Number(j.completionRate ?? 0).toFixed(0)}%`,
j.averageScore != null ? Number(j.averageScore).toFixed(2) : '-',
])
y = addTable(doc, headers, rows, y)
}
// 6. Criteria breakdown
const criteriaBreakdown = data.criteriaBreakdown as Array<Record<string, unknown>> | undefined
if (criteriaBreakdown && criteriaBreakdown.length > 0) {
addPageBreak(doc)
await addHeader(doc, rName)
let y = addSectionTitle(doc, 'Criteria Breakdown', 28)
const headers = ['Criterion', 'Avg Score', 'Responses']
const rows = criteriaBreakdown.map((c) => [
String(c.label ?? ''),
c.averageScore != null ? Number(c.averageScore).toFixed(2) : '-',
String(c.count ?? 0),
])
y = addTable(doc, headers, rows, y)
}
// 7. Footer on all pages
addAllPageFooters(doc)
// 8. Save
const dateStr = new Date().toISOString().split('T')[0]
savePdf(doc, `MOPC-Report-${rName.replace(/\s+/g, '-')}-${dateStr}.pdf`)
toast.success('PDF report downloaded successfully')
} catch (err) {
console.error('PDF generation error:', err)
toast.error('Failed to generate PDF report')
} finally {
setGenerating(false)
}
}, [refetch, roundName, programName, chartRefs])
return (
<Button variant={variant} size={size} onClick={handleGenerate} disabled={generating}>
{generating ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<FileDown className="mr-2 h-4 w-4" />
)}
{generating ? 'Generating...' : 'Export PDF Report'}
</Button>
)
}