2026-02-14 15:26:42 +01:00
|
|
|
'use client'
|
|
|
|
|
|
|
|
|
|
import { useState, useCallback } 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,
|
|
|
|
|
addAllPageFooters,
|
|
|
|
|
savePdf,
|
|
|
|
|
} from '@/lib/pdf-generator'
|
|
|
|
|
|
|
|
|
|
interface PdfReportProps {
|
|
|
|
|
stageId: string
|
|
|
|
|
sections: string[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function PdfReportGenerator({ stageId, sections }: PdfReportProps) {
|
|
|
|
|
const [generating, setGenerating] = useState(false)
|
|
|
|
|
|
|
|
|
|
const { refetch } = trpc.export.getReportData.useQuery(
|
|
|
|
|
{ stageId, 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 = String(data.roundName || 'Report')
|
|
|
|
|
const pName = 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
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Rankings
|
|
|
|
|
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
|
|
|
|
|
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. Footers
|
|
|
|
|
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])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Button variant="outline" 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>
|
|
|
|
|
)
|
|
|
|
|
}
|