feat(report): page shell with two-column layout and PDF download action
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { pdf } from '@react-pdf/renderer';
|
||||
import type { MuseumRecord } from '../../types';
|
||||
import { DEFAULT_CONFIG, computeReportData } from './reportHelpers';
|
||||
import type { ReportConfig } from './reportHelpers';
|
||||
import { ReportDocument } from './ReportDocument';
|
||||
import ReportForm from './ReportForm';
|
||||
import ReportPreview from './ReportPreview';
|
||||
import { getUniqueMuseums, getUniqueChannels } from '../../services/dataService';
|
||||
|
||||
interface Props {
|
||||
data: MuseumRecord[];
|
||||
}
|
||||
|
||||
export default function ReportPage({ data }: Props) {
|
||||
const [config, setConfig] = useState<ReportConfig>(DEFAULT_CONFIG);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
|
||||
const allMuseums = getUniqueMuseums(data);
|
||||
const allChannels = getUniqueChannels(data);
|
||||
|
||||
const patch = useCallback((p: Partial<ReportConfig>) => setConfig(c => ({ ...c, ...p })), []);
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setGenerating(true);
|
||||
try {
|
||||
const reportData = computeReportData(data, config);
|
||||
const blob = await pdf(<ReportDocument data={reportData} />).toBlob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
const slug = (config.clientName || 'report').toLowerCase().replace(/\s+/g, '-');
|
||||
a.href = url;
|
||||
a.download = `hihala-${slug}-${config.startDate.slice(0, 7)}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.error('PDF generation failed:', err);
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="report-page">
|
||||
<div className="report-header">
|
||||
<h1 className="report-title">Report Builder</h1>
|
||||
<p className="report-sub">Configure and download a client-ready PDF report.</p>
|
||||
</div>
|
||||
|
||||
<div className="report-body">
|
||||
<div className="report-form-col">
|
||||
<ReportForm config={config} onChange={patch} allMuseums={allMuseums} allChannels={allChannels} />
|
||||
</div>
|
||||
<div className="report-preview-col">
|
||||
<div className="report-preview-sticky">
|
||||
<ReportPreview config={config} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="report-footer-bar">
|
||||
<button
|
||||
type="button"
|
||||
className="report-generate-btn"
|
||||
onClick={handleGenerate}
|
||||
disabled={generating}
|
||||
>
|
||||
{generating ? (
|
||||
<>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="report-spin" aria-hidden="true">
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
||||
</svg>
|
||||
Generating…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
Download PDF
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user