509 lines
18 KiB
TypeScript
509 lines
18 KiB
TypeScript
import React, { useState, useEffect, useMemo, useCallback } from 'react'
|
||
import { useParams, useNavigate } from 'react-router-dom'
|
||
import { Button } from '../ui/Button'
|
||
import {
|
||
FaArrowLeft,
|
||
FaCalendarAlt,
|
||
FaFileAlt,
|
||
FaDownload,
|
||
FaSearchPlus,
|
||
FaSearchMinus,
|
||
} from 'react-icons/fa'
|
||
import { ReportGeneratedDto, ReportTemplateDto } from '@/proxy/reports/models'
|
||
import { useReports } from '@/utils/hooks/useReports'
|
||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||
|
||
export const ReportViewer: React.FC = () => {
|
||
const { id } = useParams<{ id: string }>()
|
||
const navigate = useNavigate()
|
||
const [zoomLevel, setZoomLevel] = useState(100)
|
||
const [report, setReport] = useState<ReportGeneratedDto | null>(null)
|
||
const [template, setTemplate] = useState<ReportTemplateDto | null>(null)
|
||
const [isLoading, setIsLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const { translate } = useLocalization()
|
||
const { getReportById, getTemplateById } = useReports()
|
||
|
||
// İçeriği sayfalara bölen fonksiyon
|
||
const splitContentIntoPages = (content: string) => {
|
||
// Basit olarak içeriği paragraf ve tablo bazında bölelim
|
||
const tempDiv = document.createElement('div')
|
||
tempDiv.innerHTML = content
|
||
|
||
const elements = Array.from(tempDiv.children)
|
||
const pages: string[] = []
|
||
let currentPage = ''
|
||
let currentPageHeight = 0
|
||
const maxPageHeight = 257 // 297mm - 40mm padding (top+bottom)
|
||
|
||
elements.forEach((element) => {
|
||
const elementHtml = element.outerHTML
|
||
// Basit yükseklik tahmini (gerçek uygulamada daha karmaşık olabilir)
|
||
let estimatedHeight = 20 // Default height
|
||
|
||
if (element.tagName === 'TABLE') {
|
||
const rows = element.querySelectorAll('tr')
|
||
estimatedHeight = rows.length * 25 // Her satır için 25mm
|
||
} else if (element.tagName.startsWith('H')) {
|
||
estimatedHeight = 15
|
||
} else if (element.tagName === 'P') {
|
||
estimatedHeight = 10
|
||
}
|
||
|
||
if (currentPageHeight + estimatedHeight > maxPageHeight && currentPage) {
|
||
pages.push(currentPage)
|
||
currentPage = elementHtml
|
||
currentPageHeight = estimatedHeight
|
||
} else {
|
||
currentPage += elementHtml
|
||
currentPageHeight += estimatedHeight
|
||
}
|
||
})
|
||
|
||
if (currentPage) {
|
||
pages.push(currentPage)
|
||
}
|
||
|
||
return pages.length > 0 ? pages : [content]
|
||
}
|
||
|
||
const preloadPdfLibs = useCallback(() => {
|
||
// Hover’da ısıtma için (opsiyonel)
|
||
import('jspdf')
|
||
import('html2canvas')
|
||
}, [])
|
||
|
||
// YENİ: memoize edilmiş sayfalar
|
||
const memoizedPages = useMemo(() => {
|
||
return report ? splitContentIntoPages(report.generatedContent) : []
|
||
}, [report])
|
||
|
||
// Asenkron veri yükleme
|
||
useEffect(() => {
|
||
const loadReportData = async () => {
|
||
if (!id) {
|
||
setError("Rapor ID'si bulunamadı")
|
||
setIsLoading(false)
|
||
return
|
||
}
|
||
|
||
try {
|
||
setIsLoading(true)
|
||
setError(null)
|
||
|
||
// Raporu yükle
|
||
const reportData = await getReportById(id)
|
||
|
||
if (!reportData) {
|
||
setError('Rapor bulunamadı')
|
||
setIsLoading(false)
|
||
return
|
||
}
|
||
|
||
setReport(reportData)
|
||
|
||
// Şablonu yükle
|
||
if (reportData.templateId) {
|
||
const templateData = await getTemplateById(reportData.templateId)
|
||
setTemplate(templateData || null)
|
||
}
|
||
} catch (err) {
|
||
console.error('Error loading report data:', err)
|
||
setError('Rapor yüklenirken bir hata oluştu')
|
||
} finally {
|
||
setIsLoading(false)
|
||
}
|
||
}
|
||
|
||
loadReportData()
|
||
}, [id, getReportById, getTemplateById])
|
||
|
||
// Zoom fonksiyonları
|
||
const handleZoomIn = () => {
|
||
setZoomLevel((prev) => Math.min(prev + 25, 200)) // Maksimum %200
|
||
}
|
||
|
||
const handleZoomOut = () => {
|
||
setZoomLevel((prev) => Math.max(prev - 25, 50)) // Minimum %50
|
||
}
|
||
|
||
// Loading durumu
|
||
if (isLoading) {
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||
<div className="text-center">
|
||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||
<h1 className="text-xl font-semibold text-gray-900 mb-2">
|
||
{translate('::App.Reports.ReportViewer.LoadingTitle')}
|
||
</h1>
|
||
<p className="text-gray-600">{translate('::App.Reports.ReportViewer.LoadingSubtitle')}</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Error durumu
|
||
if (error || !id) {
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||
<div className="text-center">
|
||
<h1 className="text-2xl font-bold text-gray-900 mb-4">
|
||
{error || translate('::App.Reports.ReportViewer.ErrorNotFound')}
|
||
</h1>
|
||
<Button onClick={() => navigate(ROUTES_ENUM.protected.dashboard)}>
|
||
<FaArrowLeft className="h-4 w-4 mr-2" />
|
||
Ana Sayfaya Dön
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (!id) {
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||
<div className="text-center">
|
||
<h1 className="text-2xl font-bold text-gray-900 mb-4">
|
||
{translate('::App.Reports.ReportViewer.ErrorNotFound')}
|
||
</h1>
|
||
<Button onClick={() => navigate(ROUTES_ENUM.protected.dashboard)}>
|
||
<FaArrowLeft className="h-4 w-4 mr-2" />
|
||
Ana Sayfaya Dön
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Report yüklenmemiş ise
|
||
if (!report) {
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||
<div className="text-center">
|
||
<h1 className="text-2xl font-bold text-gray-900 mb-4">
|
||
{translate('::App.Reports.ReportViewer.ErrorNotFound')}
|
||
</h1>
|
||
<p className="text-gray-600 mb-6">
|
||
{translate('::App.Reports.ReportViewer.ErrorNotFoundDescription')}
|
||
</p>
|
||
<Button onClick={() => navigate(ROUTES_ENUM.protected.dashboard)}>
|
||
<FaArrowLeft className="h-4 w-4 mr-2" />
|
||
{translate('::App.Reports.ReportViewer.BackToDashboard')}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const handlePrint = () => {
|
||
// Yazdırma sırasında zoom seviyesini geçici olarak %100'e ayarla
|
||
const currentZoom = zoomLevel
|
||
setZoomLevel(100)
|
||
|
||
// DOM'un güncellenmesi için kısa bir gecikme
|
||
setTimeout(() => {
|
||
window.print()
|
||
|
||
// Yazdırma işlemi tamamlandıktan sonra orijinal zoom seviyesine geri dön
|
||
// Print dialog kapandıktan sonra zoom'u eski haline getir
|
||
setTimeout(() => {
|
||
setZoomLevel(currentZoom)
|
||
}, 100)
|
||
}, 100)
|
||
}
|
||
|
||
// DEĞİŞTİR: handleDownloadPdf
|
||
const handleDownloadPdf = async () => {
|
||
// Ağır kütüphaneleri ihtiyaç anında indir
|
||
const [jspdfMod, h2cMod] = await Promise.all([import('jspdf'), import('html2canvas')])
|
||
|
||
// jsPDF bazı dağıtımlarda default, bazılarında { jsPDF } olarak gelir
|
||
const jsPDFCtor = (jspdfMod as any).default ?? (jspdfMod as any).jsPDF
|
||
const html2canvas = (h2cMod as any).default ?? (h2cMod as any)
|
||
|
||
const pages = memoizedPages // aşağıdaki 2. adımda tanımlayacağız
|
||
|
||
try {
|
||
const pdf = new jsPDFCtor({ orientation: 'portrait', unit: 'mm', format: 'a4' })
|
||
|
||
for (let i = 0; i < pages.length; i++) {
|
||
const elementId = i === 0 ? 'report-content' : `report-content-page-${i + 1}`
|
||
const element = document.getElementById(elementId)
|
||
if (!element) continue
|
||
|
||
// Yakalama öncesi zoom’u etkisizleştir (transform varsa kalite düşmesin)
|
||
const container = element.parentElement as HTMLElement | null
|
||
const prevTransform = container?.style.transform
|
||
if (container) container.style.transform = 'none'
|
||
|
||
const canvas = await html2canvas(element, {
|
||
scale: 2,
|
||
useCORS: true,
|
||
allowTaint: true,
|
||
backgroundColor: '#ffffff',
|
||
})
|
||
|
||
if (container) container.style.transform = prevTransform ?? ''
|
||
|
||
const imgData = canvas.toDataURL('image/png')
|
||
const imgWidth = 210
|
||
const imgHeight = 297
|
||
if (i > 0) pdf.addPage()
|
||
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight)
|
||
}
|
||
|
||
pdf.save(
|
||
`${report!.templateName}_${new Date(report!.generatedAt).toLocaleDateString('tr-TR')}.pdf`,
|
||
)
|
||
} catch (error) {
|
||
console.error('PDF oluşturma hatası:', error)
|
||
alert('PDF oluşturulurken bir hata oluştu.')
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50">
|
||
{/* Header - Print edilmeyecek */}
|
||
<div className="print:hidden bg-white shadow-sm border-b border-gray-200 sticky top-0 z-10">
|
||
<div className="w-full px-4">
|
||
<div className="flex items-center justify-between h-16">
|
||
<div className="flex items-center space-x-4 flex-1">
|
||
<div className="flex-1">
|
||
<h1 className="text-lg font-semibold text-gray-900">{report.templateName}</h1>
|
||
<div className="flex items-center space-x-4 text-sm text-gray-500">
|
||
<span className="flex items-center">
|
||
<FaCalendarAlt className="h-4 w-4 mr-1" />
|
||
{new Date(report.generatedAt).toLocaleDateString('tr-TR', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
})}
|
||
</span>
|
||
{template && (
|
||
<span className="flex items-center">
|
||
<FaFileAlt className="h-4 w-4 mr-1" />
|
||
{template.categoryName}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center space-x-2 flex-shrink-0">
|
||
<Button variant="solid" onClick={handleZoomOut} disabled={zoomLevel <= 50} size="sm">
|
||
<FaSearchMinus className="h-4 w-4" />
|
||
</Button>
|
||
<span className="text-sm text-gray-600 px-2 min-w-[4rem] text-center">
|
||
{zoomLevel}%
|
||
</span>
|
||
<Button variant="solid" onClick={handleZoomIn} disabled={zoomLevel >= 200} size="sm">
|
||
<FaSearchPlus className="h-4 w-4" />
|
||
</Button>
|
||
<div className="w-px h-6 bg-gray-300 mx-2"></div>
|
||
<Button
|
||
onMouseEnter={preloadPdfLibs} // ← opsiyonel prefetch
|
||
onClick={handleDownloadPdf}
|
||
className="bg-white-600 hover:bg-white-700 font-medium px-2 sm:px-3 py-1.5 rounded text-xs flex items-center gap-1"
|
||
>
|
||
<FaDownload className="h-4 w-4 mr-2" />
|
||
{translate('::App.Reports.ReportViewer.DownloadPDF')}
|
||
</Button>
|
||
<Button variant="solid" onClick={handlePrint}>
|
||
{translate('::App.Reports.ReportViewer.Print')}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Rapor İçeriği */}
|
||
<div className="flex justify-center bg-gray-200 py-8 print:bg-white print:py-0">
|
||
<div
|
||
className="space-y-8 print:space-y-0 transition-transform duration-300 ease-in-out"
|
||
style={{
|
||
transform: `scale(${zoomLevel / 100})`,
|
||
transformOrigin: 'top center',
|
||
}}
|
||
>
|
||
{memoizedPages.map((pageContent, index) => (
|
||
<div
|
||
key={index}
|
||
id={index === 0 ? 'report-content' : `report-content-page-${index + 1}`}
|
||
className="bg-white shadow-lg print:shadow-none page-container"
|
||
style={{
|
||
width: '210mm',
|
||
minHeight: '297mm',
|
||
padding: '0',
|
||
boxSizing: 'border-box',
|
||
position: 'relative',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
}}
|
||
>
|
||
{index === 0 && (
|
||
<style>
|
||
{`
|
||
/* Sayfa kırılması için CSS */
|
||
.page-container {
|
||
page-break-after: always;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.page-container:last-child {
|
||
page-break-after: auto;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
/* Header ve Footer stilleri */
|
||
.page-header {
|
||
height: 15mm;
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: right;
|
||
background-color: transparent;
|
||
color: #666;
|
||
font-size: 10px;
|
||
padding: 0 20mm;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.page-footer {
|
||
height: 15mm;
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
background-color: transparent;
|
||
color: #666;
|
||
font-size: 10px;
|
||
padding: 0 20mm;
|
||
box-sizing: border-box;
|
||
margin-top: auto;
|
||
}
|
||
|
||
.page-content-area {
|
||
flex: 1;
|
||
padding: 0 20mm;
|
||
min-height: 267mm; /* 297mm - 15mm header - 15mm footer */
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* İçerik için sayfa kırılması kuralları */
|
||
.page-container h1, .page-container h2, .page-container h3 {
|
||
page-break-after: avoid;
|
||
}
|
||
|
||
.page-container table {
|
||
page-break-inside: avoid;
|
||
border-collapse: collapse;
|
||
width: 100%;
|
||
margin: 0;
|
||
}
|
||
|
||
.page-container table td,
|
||
.page-container table th {
|
||
border: 1px solid #ddd;
|
||
text-align: left;
|
||
vertical-align: top;
|
||
}
|
||
|
||
/* Print specific styles */
|
||
@media print {
|
||
body {
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
}
|
||
|
||
.page-container {
|
||
width: 210mm !important;
|
||
min-height: 297mm !important;
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
box-shadow: none !important;
|
||
page-break-after: always !important;
|
||
}
|
||
|
||
.page-container:last-child {
|
||
page-break-after: auto !important;
|
||
}
|
||
|
||
.page-header {
|
||
height: 15mm !important;
|
||
color: #666 !important;
|
||
-webkit-print-color-adjust: exact !important;
|
||
}
|
||
|
||
.page-footer {
|
||
height: 15mm !important;
|
||
color: #666 !important;
|
||
-webkit-print-color-adjust: exact !important;
|
||
}
|
||
|
||
.page-content-area {
|
||
padding: 0 20mm !important;
|
||
min-height: 267mm !important;
|
||
}
|
||
|
||
.print\\:hidden {
|
||
display: none !important;
|
||
}
|
||
|
||
/* Table print styles */
|
||
.page-container table {
|
||
border-collapse: collapse !important;
|
||
page-break-inside: avoid !important;
|
||
}
|
||
|
||
.page-container table th,
|
||
.page-container table td {
|
||
border: 1px solid #000 !important;
|
||
page-break-inside: avoid !important;
|
||
}
|
||
|
||
.page-container table th {
|
||
background-color: #f0f0f0 !important;
|
||
-webkit-print-color-adjust: exact !important;
|
||
color-adjust: exact !important;
|
||
}
|
||
|
||
.page-container table tr:nth-child(even) {
|
||
background-color: #f5f5f5 !important;
|
||
-webkit-print-color-adjust: exact !important;
|
||
color-adjust: exact !important;
|
||
}
|
||
}
|
||
`}
|
||
</style>
|
||
)}
|
||
|
||
{/* Sayfa Header - Tarih ve Saat */}
|
||
<div className="page-header">
|
||
{new Date().toLocaleDateString('tr-TR', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
})}
|
||
</div>
|
||
|
||
{/* Sayfa İçeriği */}
|
||
<div className="page-content-area">
|
||
<div dangerouslySetInnerHTML={{ __html: pageContent }} />
|
||
</div>
|
||
|
||
{/* Sayfa Footer - Sayfa Numarası */}
|
||
<div className="page-footer">
|
||
{translate('::App.Reports.ReportViewer.Page')} {index + 1} / {memoizedPages.length}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|