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>
177 lines
4.7 KiB
TypeScript
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>
|
|
);
|
|
}
|