Files
goodgo-platform/apps/web/components/valuation/export-pdf-button.tsx
Ho Ngoc Hai 8da488711b feat(analytics): AVM v2 batch valuation, comparison, history + frontend upgrade
Add batch valuation (POST /analytics/valuation/batch, max 50 properties),
valuation comparison (POST /analytics/valuation/compare, 2-5 properties),
and history endpoint (GET /analytics/valuation/history/:propertyId) with
confidence explanation helper. Frontend: enhanced valuation form with project
autocomplete and deep analysis toggle, results with confidence badges and
price range visualization, comparables table, history chart, market context
card, and PDF export.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-16 05:08:05 +07:00

177 lines
4.7 KiB
TypeScript

'use client';
import { Download, Loader2 } from 'lucide-react';
import { useCallback, useState } from 'react';
import { Button } from '@/components/ui/button';
interface ExportPdfButtonProps {
/** CSS selector for the DOM element to capture */
targetSelector: string;
/** Filename without extension */
filename?: string;
}
export function ExportPdfButton({
targetSelector,
filename = 'dinh-gia-bat-dong-san',
}: ExportPdfButtonProps) {
const [isExporting, setIsExporting] = useState(false);
const handleExport = useCallback(async () => {
setIsExporting(true);
try {
const element = document.querySelector(targetSelector);
if (!element) {
console.error('Export target not found:', targetSelector);
return;
}
// Dynamic imports for client-only PDF libraries
const [html2canvasModule, jsPDFModule] = await Promise.all([
import('html2canvas'),
import('jspdf'),
]);
const html2canvas = html2canvasModule.default;
const { jsPDF } = jsPDFModule;
const canvas = await html2canvas(element as HTMLElement, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
});
const imgData = canvas.toDataURL('image/png');
const imgWidth = canvas.width;
const imgHeight = canvas.height;
// A4 dimensions in mm
const pdfWidth = 210;
const pdfMargin = 10;
const contentWidth = pdfWidth - 2 * pdfMargin;
const contentHeight = (imgHeight / imgWidth) * contentWidth;
const pdf = new jsPDF({
orientation: contentHeight > 297 - 2 * pdfMargin ? 'portrait' : 'portrait',
unit: 'mm',
format: 'a4',
});
// Add header
pdf.setFontSize(16);
pdf.setTextColor(34, 139, 34); // Green
pdf.text('GoodGo — Báo cáo định giá', pdfMargin, pdfMargin + 5);
pdf.setFontSize(10);
pdf.setTextColor(128, 128, 128);
pdf.text(
`Ngày: ${new Date().toLocaleDateString('vi-VN')}`,
pdfMargin,
pdfMargin + 12,
);
const headerHeight = 20;
const availableHeight = 297 - 2 * pdfMargin - headerHeight;
if (contentHeight <= availableHeight) {
// Fits on one page
pdf.addImage(
imgData,
'PNG',
pdfMargin,
pdfMargin + headerHeight,
contentWidth,
contentHeight,
);
} else {
// Multi-page: split the image
let yOffset = 0;
let isFirstPage = true;
while (yOffset < contentHeight) {
if (!isFirstPage) {
pdf.addPage();
}
const pageContentHeight = isFirstPage
? availableHeight
: 297 - 2 * pdfMargin;
const pageTopMargin = isFirstPage
? pdfMargin + headerHeight
: pdfMargin;
// Calculate source rectangle in image coordinates
const srcY = (yOffset / contentHeight) * imgHeight;
const srcHeight = (pageContentHeight / contentHeight) * imgHeight;
// Create a temporary canvas for this page slice
const pageCanvas = document.createElement('canvas');
pageCanvas.width = imgWidth;
pageCanvas.height = Math.min(srcHeight, imgHeight - srcY);
const ctx = pageCanvas.getContext('2d');
if (ctx) {
ctx.drawImage(
canvas,
0,
srcY,
imgWidth,
pageCanvas.height,
0,
0,
imgWidth,
pageCanvas.height,
);
}
const pageImgData = pageCanvas.toDataURL('image/png');
const sliceHeight =
(pageCanvas.height / imgWidth) * contentWidth;
pdf.addImage(
pageImgData,
'PNG',
pdfMargin,
pageTopMargin,
contentWidth,
sliceHeight,
);
yOffset += pageContentHeight;
isFirstPage = false;
}
}
// Footer on last page
pdf.setFontSize(8);
pdf.setTextColor(180, 180, 180);
pdf.text(
'Được tạo bởi GoodGo AI Valuation — goodgo.vn',
pdfMargin,
297 - pdfMargin,
);
pdf.save(`${filename}.pdf`);
} catch (error) {
console.error('PDF export failed:', error);
} finally {
setIsExporting(false);
}
}, [targetSelector, filename]);
return (
<Button
variant="outline"
size="sm"
onClick={handleExport}
disabled={isExporting}
>
{isExporting ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Download className="mr-2 h-4 w-4" />
)}
{isExporting ? 'Đang xuất...' : 'Xuất PDF'}
</Button>
);
}