Performans çalışmaları ve DevExpres License

This commit is contained in:
Sedat ÖZTÜRK 2025-08-18 17:55:51 +03:00
parent 0d04a30767
commit 7c6d4857df
8 changed files with 659 additions and 293 deletions

3
ui/.gitignore vendored
View file

@ -23,4 +23,5 @@ build
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.vite-cache/ .vite-cache/
src/devextreme-license.ts

View file

@ -82,7 +82,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "index.html", "url": "index.html",
"revision": "0.p87ro290qlo" "revision": "0.20gg38gpeso"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

670
ui/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -65,7 +65,7 @@
"@types/babel__standalone": "^7.1.9", "@types/babel__standalone": "^7.1.9",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"@types/node": "^18.15.5", "@types/node": "^20.19.11",
"@types/react": "^18.3.18", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5", "@types/react-dom": "^18.3.5",
"@types/react-helmet": "^6.1.9", "@types/react-helmet": "^6.1.9",
@ -95,8 +95,8 @@
"prettier": "^3.1.1", "prettier": "^3.1.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^4.9.3", "typescript": "^4.9.3",
"vite": "^5.4.11", "vite": "^7.1.2",
"vite-plugin-pwa": "^0.21.1" "vite-plugin-pwa": "^1.0.2"
}, },
"volta": { "volta": {
"node": "22.12.0", "node": "22.12.0",

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect, useMemo, useCallback } from 'react'
import { useParams, useNavigate } from 'react-router-dom' import { useParams, useNavigate } from 'react-router-dom'
import { Button } from '../ui/Button' import { Button } from '../ui/Button'
import { import {
@ -9,8 +9,6 @@ import {
FaSearchPlus, FaSearchPlus,
FaSearchMinus, FaSearchMinus,
} from 'react-icons/fa' } from 'react-icons/fa'
import html2canvas from 'html2canvas'
import jsPDF from 'jspdf'
import { ReportGeneratedDto, ReportTemplateDto } from '@/proxy/reports/models' import { ReportGeneratedDto, ReportTemplateDto } from '@/proxy/reports/models'
import { useReports } from '@/utils/hooks/useReports' import { useReports } from '@/utils/hooks/useReports'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
@ -25,9 +23,62 @@ export const ReportViewer: React.FC = () => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const { translate } = useLocalization() const { translate } = useLocalization()
const { getReportById, getTemplateById } = useReports() 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(() => {
// Hoverda ı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 // Asenkron veri yükleme
useEffect(() => { useEffect(() => {
const loadReportData = async () => { const loadReportData = async () => {
@ -77,49 +128,6 @@ export const ReportViewer: React.FC = () => {
setZoomLevel((prev) => Math.max(prev - 25, 50)) // Minimum %50 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 // Loading durumu
if (isLoading) { if (isLoading) {
return ( return (
@ -205,22 +213,30 @@ export const ReportViewer: React.FC = () => {
}, 100) }, 100)
} }
// DEĞİŞTİR: handleDownloadPdf
const handleDownloadPdf = async () => { const handleDownloadPdf = async () => {
const pages = splitContentIntoPages(report.generatedContent) // 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 { try {
const pdf = new jsPDF({ const pdf = new jsPDFCtor({ orientation: 'portrait', unit: 'mm', format: 'a4' })
orientation: 'portrait',
unit: 'mm',
format: 'a4',
})
for (let i = 0; i < pages.length; i++) { for (let i = 0; i < pages.length; i++) {
const elementId = i === 0 ? 'report-content' : `report-content-page-${i + 1}` const elementId = i === 0 ? 'report-content' : `report-content-page-${i + 1}`
const element = document.getElementById(elementId) const element = document.getElementById(elementId)
if (!element) continue if (!element) continue
// Yakalama öncesi zoomu 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, { const canvas = await html2canvas(element, {
scale: 2, scale: 2,
useCORS: true, useCORS: true,
@ -228,19 +244,17 @@ export const ReportViewer: React.FC = () => {
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
}) })
if (container) container.style.transform = prevTransform ?? ''
const imgData = canvas.toDataURL('image/png') const imgData = canvas.toDataURL('image/png')
const imgWidth = 210 // A4 width in mm const imgWidth = 210
const imgHeight = 297 // A4 height in mm const imgHeight = 297
if (i > 0) pdf.addPage()
if (i > 0) {
pdf.addPage()
}
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight) pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight)
} }
pdf.save( pdf.save(
`${report.templateName}_${new Date(report.generatedAt).toLocaleDateString('tr-TR')}.pdf`, `${report!.templateName}_${new Date(report!.generatedAt).toLocaleDateString('tr-TR')}.pdf`,
) )
} catch (error) { } catch (error) {
console.error('PDF oluşturma hatası:', error) console.error('PDF oluşturma hatası:', error)
@ -290,6 +304,7 @@ export const ReportViewer: React.FC = () => {
</Button> </Button>
<div className="w-px h-6 bg-gray-300 mx-2"></div> <div className="w-px h-6 bg-gray-300 mx-2"></div>
<Button <Button
onMouseEnter={preloadPdfLibs} // ← opsiyonel prefetch
onClick={handleDownloadPdf} 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" 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"
> >
@ -313,7 +328,7 @@ export const ReportViewer: React.FC = () => {
transformOrigin: 'top center', transformOrigin: 'top center',
}} }}
> >
{splitContentIntoPages(report.generatedContent).map((pageContent, index) => ( {memoizedPages.map((pageContent, index) => (
<div <div
key={index} key={index}
id={index === 0 ? 'report-content' : `report-content-page-${index + 1}`} id={index === 0 ? 'report-content' : `report-content-page-${index + 1}`}
@ -483,8 +498,7 @@ export const ReportViewer: React.FC = () => {
{/* Sayfa Footer - Sayfa Numarası */} {/* Sayfa Footer - Sayfa Numarası */}
<div className="page-footer"> <div className="page-footer">
{translate('::App.Reports.ReportViewer.Page')} {index + 1} /{' '} {translate('::App.Reports.ReportViewer.Page')} {index + 1} / {memoizedPages.length}
{splitContentIntoPages(report.generatedContent).length}
</div> </div>
</div> </div>
))} ))}

View file

@ -5,6 +5,11 @@ import './index.css'
import { UiEvalService } from './services/UiEvalService' import { UiEvalService } from './services/UiEvalService'
import 'devextreme-react/text-area' import 'devextreme-react/text-area'
import 'devextreme-react/html-editor' import 'devextreme-react/html-editor'
import config from 'devextreme/core/config'
import { licenseKey } from './devextreme-license'
// Lisansı uygulama başlamadan önce kaydediyoruz
config({ licenseKey })
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />) ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)

View file

@ -63,11 +63,6 @@ import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent' import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { useFilters } from './useFilters' import { useFilters } from './useFilters'
import { useToolbar } from './useToolbar' import { useToolbar } from './useToolbar'
import { Workbook } from 'exceljs'
import saveAs from 'file-saver'
import { jsPDF } from 'jspdf'
import { exportDataGrid as exportDataPdf } from 'devextreme/pdf_exporter'
import { exportDataGrid as exportDataExcel } from 'devextreme/excel_exporter'
import { ImportDashboard } from '@/components/importManager/ImportDashboard' import { ImportDashboard } from '@/components/importManager/ImportDashboard'
interface GridProps { interface GridProps {
@ -94,6 +89,14 @@ const Grid = (props: GridProps) => {
const [formData, setFormData] = useState<any>() const [formData, setFormData] = useState<any>()
const [mode, setMode] = useState<RowMode>('view') const [mode, setMode] = useState<RowMode>('view')
const preloadExportLibs = () => {
import('exceljs')
import('file-saver')
import('devextreme/excel_exporter')
import('jspdf')
import('devextreme/pdf_exporter')
}
const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({ const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({
gridDto, gridDto,
listFormCode, listFormCode,
@ -431,45 +434,71 @@ const Grid = (props: GridProps) => {
gridRef.current.instance.option('stateStoring', stateStoring) gridRef.current.instance.option('stateStoring', stateStoring)
}, [columnData]) }, [columnData])
const onExporting = (e: DataGridTypes.ExportingEvent) => { const onExporting = async (e: DataGridTypes.ExportingEvent) => {
if (e.format == 'xlsx') { // DevExtremein varsayılan export davranışını iptal ediyoruz; kendi akışımızı çalıştıracağız
const workbook = new Workbook() e.cancel = true
const worksheet = workbook.addWorksheet(`${listFormCode}_sheet`)
exportDataExcel({ const grid = gridRef?.current?.instance
component: gridRef?.current?.instance, if (!grid) return
worksheet,
autoFilterEnabled: true, try {
}).then(() => { if (e.format === 'xlsx' || e.format === 'csv') {
workbook.xlsx.writeBuffer().then((buffer) => { // exceljs + file-saver + devextreme excel exporter => ihtiyaç anında yükle
const [{ Workbook }, { saveAs }, { exportDataGrid: exportDataExcel }] = await Promise.all([
import('exceljs'),
import('file-saver'),
import('devextreme/excel_exporter'),
])
const workbook = new Workbook()
const worksheet = workbook.addWorksheet(`${listFormCode}_sheet`)
await exportDataExcel({
component: grid,
worksheet,
autoFilterEnabled: true,
})
if (e.format === 'xlsx') {
const buffer = await workbook.xlsx.writeBuffer()
saveAs( saveAs(
new Blob([buffer], { type: 'application/octet-stream' }), new Blob([buffer], { type: 'application/octet-stream' }),
`${listFormCode}_export.xlsx`, `${listFormCode}_export.xlsx`,
) )
}) } else {
}) const buffer = await workbook.csv.writeBuffer()
} else if (e.format == 'pdf') {
const doc = new jsPDF()
exportDataPdf({
jsPDFDocument: doc,
component: gridRef?.current?.instance,
indent: 5,
}).then(() => {
doc.save(`${listFormCode}_export.pdf`)
})
} else if (e.format == 'csv') {
const workbook = new Workbook()
const worksheet = workbook.addWorksheet(`${listFormCode}_sheet`)
exportDataExcel({
component: gridRef?.current?.instance,
worksheet: worksheet,
}).then(function () {
workbook.csv.writeBuffer().then(function (buffer) {
saveAs( saveAs(
new Blob([buffer], { type: 'application/octet-stream' }), new Blob([buffer], { type: 'application/octet-stream' }),
`${listFormCode}_export.csv`, `${listFormCode}_export.csv`,
) )
}
} else if (e.format === 'pdf') {
// jspdf + devextreme pdf exporter => ihtiyaç anında yükle
const [jspdfMod, { exportDataGrid: exportDataPdf }] = await Promise.all([
import('jspdf'),
import('devextreme/pdf_exporter'),
])
// jsPDF bazı paketlemelerde default, bazılarında named export olarak gelir
const JsPDFCtor = (jspdfMod as any).default ?? (jspdfMod as any).jsPDF
const doc = new JsPDFCtor({})
await exportDataPdf({
jsPDFDocument: doc,
component: grid,
indent: 5,
}) })
})
doc.save(`${listFormCode}_export.pdf`)
}
} catch (err) {
console.error('Export error:', err)
toast.push(
<Notification type="danger" duration={2500}>
{translate('::App.Common.ExportError') ?? 'Dışa aktarma sırasında hata oluştu.'}
</Notification>,
{ placement: 'top-center' },
)
} }
} }

View file

@ -77,6 +77,29 @@ export default defineConfig(async ({ mode }) => {
build: { build: {
outDir: 'dist', outDir: 'dist',
sourcemap: false, sourcemap: false,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.match(/node_modules[\\/]react/)) return 'vendor-react'
if (id.match(/node_modules[\\/]react-dom/)) return 'vendor-reactdom'
if (id.match(/node_modules[\\/]devextreme/)) return 'vendor-devextreme'
if (id.match(/node_modules[\\/]@devexpress/)) return 'vendor-devexpress'
if (id.match(/node_modules[\\/]devextreme-react/)) return 'vendor-devextreme-react'
if (id.match(/node_modules[\\/]axios/)) return 'vendor-axios'
if (id.match(/node_modules[\\/]formik/)) return 'vendor-formik'
if (id.match(/node_modules[\\/]jspdf/)) return 'vendor-jspdf'
if (id.match(/node_modules[\\/]exceljs/)) return 'vendor-exceljs'
if (id.match(/node_modules[\\/]html2canvas/)) return 'vendor-html2canvas'
if (id.match(/node_modules[\\/]@?react-router/)) return 'vendor-reactrouter'
// Büyük modüller için özel chunk
if (id.match(/src[\\/]codeParser/)) return 'chunk-codeParser'
if (id.match(/src[\\/]views[\\/]list[\\/]Utils/)) return 'chunk-list-utils'
return 'vendor'
}
},
},
},
}, },
preview: { preview: {