erp-platform/ui/src/components/reports/ReportViewer.tsx

476 lines
16 KiB
TypeScript
Raw Normal View History

2025-08-15 08:07:51 +00:00
import React, { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { Button } from '../ui/Button'
import { ArrowLeft, Calendar, FileText, Download, ZoomIn, ZoomOut } from 'lucide-react'
import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'
import { GeneratedReportDto, ReportTemplateDto } from '@/proxy/reports/models'
import { useReports } from '@/utils/hooks/useReports'
export const ReportViewer: React.FC = () => {
2025-08-15 09:19:20 +00:00
const { id } = useParams<{ id: string }>()
2025-08-15 08:07:51 +00:00
const navigate = useNavigate()
const [zoomLevel, setZoomLevel] = useState(100)
const [report, setReport] = useState<GeneratedReportDto | null>(null)
const [template, setTemplate] = useState<ReportTemplateDto | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const { getReportById, getTemplateById } = useReports()
// Asenkron veri yükleme
useEffect(() => {
const loadReportData = async () => {
2025-08-15 09:19:20 +00:00
if (!id) {
2025-08-15 08:07:51 +00:00
setError("Rapor ID'si bulunamadı")
setIsLoading(false)
return
}
try {
setIsLoading(true)
setError(null)
// Raporu yükle
2025-08-15 09:19:20 +00:00
const reportData = await getReportById(id)
2025-08-15 08:07:51 +00:00
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()
2025-08-15 09:19:20 +00:00
}, [id, getReportById, getTemplateById])
2025-08-15 08:07:51 +00:00
// Zoom fonksiyonları
const handleZoomIn = () => {
setZoomLevel((prev) => Math.min(prev + 25, 200)) // Maksimum %200
}
const handleZoomOut = () => {
setZoomLevel((prev) => Math.max(prev - 25, 50)) // Minimum %50
}
// İç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]
}
// 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">Rapor yükleniyor...</h1>
<p className="text-gray-600">Lütfen bekleyin</p>
</div>
</div>
)
}
// Error durumu
2025-08-15 09:19:20 +00:00
if (error || !id) {
2025-08-15 08:07:51 +00:00
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 || 'Rapor bulunamadı'}</h1>
<Button onClick={() => navigate('/')}>
<ArrowLeft className="h-4 w-4 mr-2" />
Ana Sayfaya Dön
</Button>
</div>
</div>
)
}
2025-08-15 09:19:20 +00:00
if (!id) {
2025-08-15 08:07:51 +00:00
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">Rapor bulunamadı</h1>
<Button onClick={() => navigate('/')}>
<ArrowLeft 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">Rapor bulunamadı</h1>
<p className="text-gray-600 mb-6">
Aradığınız rapor mevcut değil veya silinmiş olabilir.
</p>
<Button onClick={() => navigate('/')}>
<ArrowLeft className="h-4 w-4 mr-2" />
Ana Sayfaya Dön
</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)
}
const handleDownloadPdf = async () => {
const pages = splitContentIntoPages(report.generatedContent)
try {
const pdf = new jsPDF({
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
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
})
const imgData = canvas.toDataURL('image/png')
const imgWidth = 210 // A4 width in mm
const imgHeight = 297 // A4 height in mm
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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center space-x-4">
<Button variant="solid" onClick={() => navigate('/')}>
<ArrowLeft className="h-4 w-4 mr-2" />
Geri Dön
</Button>
<div>
<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">
<Calendar 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">
<FileText className="h-4 w-4 mr-1" />
{template.category}
</span>
)}
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<Button variant="solid" onClick={handleZoomOut} disabled={zoomLevel <= 50} size="sm">
<ZoomOut 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">
<ZoomIn className="h-4 w-4" />
</Button>
<div className="w-px h-6 bg-gray-300 mx-2"></div>
<Button variant="solid" onClick={handleDownloadPdf}>
<Download className="h-4 w-4 mr-2" />
PDF İndir
</Button>
<Button onClick={handlePrint}>Yazdır</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',
}}
>
{splitContentIntoPages(report.generatedContent).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">
Sayfa {index + 1} / {splitContentIntoPages(report.generatedContent).length}
</div>
</div>
))}
</div>
</div>
</div>
)
}