ReportViewer ve ReportDesigner bundle çıkarıldı

This commit is contained in:
Sedat Öztürk 2026-02-04 20:36:18 +03:00
parent ff3344b3ae
commit b070702378
8 changed files with 1474 additions and 1271 deletions

View file

@ -4667,12 +4667,12 @@ public class ListFormSeeder_SupplyChain : IDataSeedContributor, ITransientDepend
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.PurchaseOrder)),
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 500, true, true, true, true, false, true),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 800, 400, true, true, true, true, false, true),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>()
{
new() {
Order=1, ColCount=1, ColSpan=1, ItemType="group", Items =[
Order=1, ColCount=2, ColSpan=1, ItemType="group", Items =[
new EditingFormItemDto { Order = 1, DataField="OrderNumber", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxTextBox, EditorOptions = EditorOptionValues.Disabled },
new EditingFormItemDto { Order = 2, DataField="SupplierId", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox, EditorOptions=EditorOptionValues.ShowClearButton },
new EditingFormItemDto { Order = 3, DataField="OrderDate", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxDateBox, EditorOptions = EditorOptionValues.DateFormat },

2224
ui/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -37,10 +37,9 @@
"axios": "^1.7.9",
"classnames": "^2.5.1",
"dayjs": "^1.11.13",
"devexpress-reporting-react": "^25.1.7",
"devextreme": "^25.1.7",
"devextreme-dist": "^25.1.7",
"devextreme-react": "^25.1.7",
"devexpress-reporting-react": "^25.2.3",
"devextreme": "^25.2.3",
"devextreme-react": "^25.2.3",
"easy-peasy": "^6.0.5",
"emoji-picker-react": "^4.14.1",
"exceljs": "^4.4.0",

View file

@ -1,6 +1,19 @@
{
"commit": "16dca4a0",
"commit": "270e50e0",
"releases": [
{
"version": "1.0.40",
"buildDate": "2026-01-17",
"commit": "52c93ccbaf1be1c8365097e97276f48988864de4",
"changeLog": [
"- Grid üzerinden Dil desteği güncellemeleri yapıldı.",
"- Nodejs 24 versiyonuna geçildi.",
"- Grid ve Popup içerisinde Numeric formatlar geliştirildi.",
"- File Management geliştirildi.",
"- XtraReport viewer ve desing komponentleri geliştirildi.",
"- Devexpress Licenceı artırıldı."
]
},
{
"version": "1.0.37",
"buildDate": "2025-12-07",
@ -192,7 +205,7 @@
{
"version": "1.0.14",
"buildDate": "2025-09-22",
"commit": "51208b86937484d68b699120d74872067b1c7ef6",
"commit": "1c4ab4f8232b4cd2a39fa66f8101664840113ce5",
"changeLog": [
"Yeni versiyon çıktı uyarı gelecek şekilde düzenlendi.",
"Sağ alt kısımda mesaj çıkacak ve yenile butonu ile uygulama yeni versiyona geçecektir."

View file

@ -1,98 +1,134 @@
import React, { Suspense, startTransition, useCallback, useEffect, useMemo, useState } from 'react'
import { useParams, useSearchParams } from 'react-router-dom'
import { useEffect, useState } from 'react'
import Container from '@/components/shared/Container'
import Grid from './Grid'
import { FaChartArea, FaList, FaSitemap, FaTable, FaTh, FaCalendarAlt, FaIdCard, FaProjectDiagram } from 'react-icons/fa'
import { useStoreActions, useStoreState } from '@/store/store'
import classNames from 'classnames'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { GridDto } from '@/proxy/form/models'
import Container from '@/components/shared/Container'
import { Badge, Button } from '@/components/ui'
import Pivot from './Pivot'
import Tree from './Tree'
import { getList } from '@/services/form.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
import { useStoreActions, useStoreState } from '@/store/store'
import { GridDto } from '@/proxy/form/models'
import { getList } from '@/services/form.service'
import { ListViewLayoutType } from '../admin/listForm/edit/types'
import Chart from './Chart'
import GanttView from './GanttView'
import SchedulerView from './SchedulerView'
const List = () => {
const params = useParams()
const { translate } = useLocalization()
import {
FaChartArea,
FaList,
FaSitemap,
FaTable,
FaCalendarAlt,
FaProjectDiagram,
} from 'react-icons/fa'
/* =======================
🔥 LAZY VIEW IMPORTS
======================= */
const Grid = React.lazy(() => import('./Grid'))
const Pivot = React.lazy(() => import('./Pivot'))
const Tree = React.lazy(() => import('./Tree'))
const Chart = React.lazy(() => import('./Chart'))
const GanttView = React.lazy(() => import('./GanttView'))
const SchedulerView = React.lazy(() => import('./SchedulerView'))
const List: React.FC = () => {
const { listFormCode = '' } = useParams()
const [searchParams] = useSearchParams()
const listFormCode = params?.listFormCode ?? ''
const mode = useStoreState((state) => state.theme.mode)
const [viewMode, setViewMode] = useState<ListViewLayoutType>()
const [gridDto, setGridDto] = useState<GridDto>()
const { translate } = useLocalization()
const mode = useStoreState((state) => state.theme.mode)
const MenuIcon = useCurrentMenuIcon('w-5 h-5')
const { states } = useStoreState((state) => state.admin.lists)
const { setStates } = useStoreActions((a) => a.admin.lists)
// 🔹 Tekrar çağırılabilir metod
const refreshGridDto = async () => {
const [gridDto, setGridDto] = useState<GridDto | null>(null)
const [viewMode, setViewMode] = useState<ListViewLayoutType | undefined>()
/* =======================
🔹 GRID DTO FETCH
======================= */
const refreshGridDto = useCallback(async () => {
if (!listFormCode) return
try {
const response = await getList({ listFormCode })
setGridDto(response.data)
} catch (error) {
console.error('GridDto refresh error:', error)
} catch (err) {
console.error('GridDto load error:', err)
}
}
useEffect(() => {
refreshGridDto()
}, [listFormCode])
useEffect(() => {
const listFormStates = states.find((a) => a.listFormCode === listFormCode)
refreshGridDto()
}, [refreshGridDto])
if (listFormStates) {
setViewMode(listFormStates.layout)
return
}
/* =======================
🔹 INITIAL VIEW MODE
======================= */
useEffect(() => {
if (!gridDto) return
if (gridDto) {
setViewMode(gridDto?.gridOptions?.layoutDto.defaultLayout)
return
}
}, [gridDto])
const savedLayout = states.find((s) => s.listFormCode === listFormCode)?.layout
if (!listFormCode) {
return null
}
const defaultLayout = gridDto.gridOptions?.layoutDto?.defaultLayout
if (!gridDto) {
setViewMode(savedLayout ?? defaultLayout)
}, [gridDto, states, listFormCode])
/* =======================
🔹 VIEW SWITCH HANDLER
======================= */
const setLayout = useCallback(
(layout: ListViewLayoutType) => {
startTransition(() => {
setViewMode(layout)
})
setStates({ listFormCode, layout })
},
[listFormCode, setStates],
)
/* =======================
🔹 PRELOAD MAP (HOVER)
======================= */
const preload = useMemo(
() => ({
grid: () => import('./Grid'),
pivot: () => import('./Pivot'),
tree: () => import('./Tree'),
chart: () => import('./Chart'),
gantt: () => import('./GanttView'),
scheduler: () => import('./SchedulerView'),
}),
[],
)
if (!listFormCode || !gridDto || !viewMode) {
return null
}
return (
<Container>
{/* =======================
HEADER
======================= */}
<div
className={classNames('flex items-center border-solid gap-1 pb-1', {
'border-gray-100': mode === 'light',
'border-neutral-700': mode === 'dark',
})}
>
<div className="flex items-center gap-2">
{MenuIcon}
<h4 className="text-slate-700 text-sm font-medium leading-none">
{translate('::' + gridDto?.gridOptions?.title) || ''}
</h4>
<Badge content={viewMode} />
</div>
{gridDto?.gridOptions?.description === gridDto?.gridOptions?.title ? (
<p className="mr-auto"></p>
) : (
<p className="text-slate-500 text-xs mr-auto ml-2 leading-none">
{translate('::' + gridDto?.gridOptions?.description)}
</p>
className={classNames(
'flex items-center gap-2 pb-1 border-b',
mode === 'light' ? 'border-gray-200' : 'border-neutral-700',
)}
>
{MenuIcon}
<h4 className="text-sm font-medium">{translate('::' + gridDto.gridOptions.title)}</h4>
<Badge content={viewMode} />
<p className="ml-2 text-xs text-slate-500 mr-auto">
{translate('::' + gridDto.gridOptions.description)}
</p>
{/* =======================
VIEW BUTTONS
======================= */}
<div className="flex gap-1">
{gridDto?.gridOptions?.layoutDto.scheduler &&
gridDto?.gridOptions?.schedulerOptionDto?.textExpr &&
@ -100,13 +136,10 @@ const List = () => {
<Button
size="xs"
variant={viewMode === 'scheduler' ? 'solid' : 'default'}
onClick={() => {
setViewMode('scheduler')
setStates({ listFormCode, layout: 'scheduler' })
}}
title="Scheduler Görünümü"
onClick={() => setLayout('scheduler')}
onMouseEnter={() => preload.scheduler()}
>
<FaCalendarAlt className="w-4 h-4" />
<FaCalendarAlt />
</Button>
)}
@ -116,13 +149,10 @@ const List = () => {
<Button
size="xs"
variant={viewMode === 'gantt' ? 'solid' : 'default'}
onClick={() => {
setViewMode('gantt')
setStates({ listFormCode, layout: 'gantt' })
}}
title="Gantt Görünümü"
onClick={() => setLayout('gantt')}
onMouseEnter={() => preload.gantt()}
>
<FaProjectDiagram className="w-4 h-4" />
<FaProjectDiagram />
</Button>
)}
@ -131,61 +161,60 @@ const List = () => {
<Button
size="xs"
variant={viewMode === 'tree' ? 'solid' : 'default'}
onClick={() => {
setViewMode('tree')
setStates({ listFormCode, layout: 'tree' })
}}
title="TreeList Görünümü"
onClick={() => setLayout('tree')}
onMouseEnter={() => preload.tree()}
>
<FaSitemap className="w-4 h-4" />
<FaSitemap />
</Button>
)}
{gridDto?.gridOptions?.layoutDto.grid && (
<Button
size="xs"
variant={viewMode === 'grid' ? 'solid' : 'default'}
onClick={() => {
setViewMode('grid')
setStates({ listFormCode, layout: 'grid' })
}}
title="Grid Görünümü"
onClick={() => setLayout('grid')}
onMouseEnter={() => preload.grid()}
>
<FaList className="w-4 h-4" />
<FaList />
</Button>
)}
{gridDto?.gridOptions?.layoutDto.pivot && (
{gridDto.gridOptions.layoutDto.pivot && (
<Button
size="xs"
variant={viewMode === 'pivot' ? 'solid' : 'default'}
onClick={() => {
setViewMode('pivot')
setStates({ listFormCode, layout: 'pivot' })
}}
title="Pivot Görünümü"
onClick={() => setLayout('pivot')}
onMouseEnter={() => preload.pivot()}
>
<FaTable className="w-4 h-4" />
<FaTable />
</Button>
)}
{gridDto?.gridOptions?.layoutDto.chart && (
{gridDto.gridOptions.layoutDto.chart && (
<Button
size="xs"
variant={viewMode === 'chart' ? 'solid' : 'default'}
onClick={() => {
setViewMode('chart')
setStates({ listFormCode, layout: 'chart' })
}}
title="Grafik Görünümü"
onClick={() => setLayout('chart')}
onMouseEnter={() => preload.chart()}
>
<FaChartArea className="w-4 h-4" />
<FaChartArea />
</Button>
)}
</div>
</div>
{viewMode === 'pivot' ? (
{/* =======================
VIEW RENDER
======================= */}
<Suspense fallback={<div className="p-4 text-sm">Yükleniyor...</div>}>
{viewMode === 'grid' && (
<Grid
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
gridDto={gridDto}
/>
)}
{viewMode === 'pivot' && (
<Pivot
listFormCode={listFormCode}
searchParams={searchParams}
@ -193,35 +222,36 @@ const List = () => {
gridDto={gridDto}
refreshGridDto={refreshGridDto}
/>
) : viewMode === 'scheduler' ? (
<SchedulerView
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
gridDto={gridDto}
/>
) : viewMode === 'gantt' ? (
<GanttView
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
gridDto={gridDto}
/>
) : viewMode === 'tree' ? (
)}
{viewMode === 'tree' && (
<Tree
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
gridDto={gridDto}
/>
) : viewMode === 'grid' ? (
<Grid
)}
{viewMode === 'scheduler' && (
<SchedulerView
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
gridDto={gridDto}
/>
) : viewMode === 'chart' ? (
)}
{viewMode === 'gantt' && (
<GanttView
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
gridDto={gridDto}
/>
)}
{viewMode === 'chart' && (
<Chart
id={gridDto?.gridOptions.id!}
listFormCode={listFormCode}
@ -230,7 +260,8 @@ const List = () => {
gridDto={gridDto}
refreshGridDto={refreshGridDto}
/>
) : null}
)}
</Suspense>
</Container>
)
}

View file

@ -1,37 +1,58 @@
import React from 'react'
import React, { lazy, Suspense, useEffect } from 'react'
import { Container } from '@/components/shared'
import { Helmet } from 'react-helmet'
import { useLocalization } from '@/utils/hooks/useLocalization'
import ReportDesigner, { RequestOptions } from 'devexpress-reporting-react/dx-report-designer'
import '@devexpress/analytics-core/dist/css/dx-analytics.common.css'
import '@devexpress/analytics-core/dist/css/dx-analytics.light.css'
import '@devexpress/analytics-core/dist/css/dx-querybuilder.css'
import 'devexpress-reporting/dist/css/dx-webdocumentviewer.css'
import 'devexpress-reporting/dist/css/dx-reportdesigner.css'
import { useParams } from 'react-router-dom'
import { RequestOptions } from 'devexpress-reporting-react/dx-report-designer'
const ReportDesigner = lazy(() =>
import('devexpress-reporting-react/dx-report-designer')
)
const loadDesignerCss = () => {
const styles = [
new URL('@devexpress/analytics-core/dist/css/dx-analytics.common.css', import.meta.url).href,
new URL('@devexpress/analytics-core/dist/css/dx-analytics.light.css', import.meta.url).href,
new URL('@devexpress/analytics-core/dist/css/dx-querybuilder.css', import.meta.url).href,
new URL('devexpress-reporting/dist/css/dx-webdocumentviewer.css', import.meta.url).href,
new URL('devexpress-reporting/dist/css/dx-reportdesigner.css', import.meta.url).href,
]
styles.forEach((href) => {
if (document.querySelector(`link[href="${href}"]`)) return
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = href
document.head.appendChild(link)
})
}
const DevexpressReportDesigner: React.FC = () => {
const { translate } = useLocalization()
const { id } = useParams<{ id: string }>()
if (!id) {
return null
}
useEffect(() => {
loadDesignerCss()
}, [])
if (!id) return null
return (
<Container>
<Helmet
titleTemplate="%s | Erp Platform"
title={translate('::' + 'App.Reports')}
title={translate('::App.Reports')}
defaultTitle="Erp Platform"
></Helmet>
/>
<Suspense fallback={<div>Rapor tasarımcısı yükleniyor...</div>}>
<ReportDesigner reportUrl={id}>
<RequestOptions
host={`${import.meta.env.VITE_API_URL}/`}
getDesignerModelAction="DXXRD/GetDesignerModel"
/>
</ReportDesigner>
</Suspense>
</Container>
)
}

View file

@ -1,42 +1,63 @@
import React, { useMemo } from 'react'
import React, { useMemo, lazy, Suspense, useEffect } from 'react'
import { Container } from '@/components/shared'
import { Helmet } from 'react-helmet'
import ReportViewer, { RequestOptions } from 'devexpress-reporting-react/dx-report-viewer'
import { useLocalization } from '@/utils/hooks/useLocalization'
import 'devextreme/dist/css/dx.light.css'
import '@devexpress/analytics-core/dist/css/dx-analytics.common.css'
import '@devexpress/analytics-core/dist/css/dx-analytics.light.css'
import 'devexpress-reporting/dist/css/dx-webdocumentviewer.css'
import { useParams, useLocation } from 'react-router-dom'
import { RequestOptions } from 'devexpress-reporting-react/dx-report-viewer'
// Lazy load
const ReportViewer = lazy(() =>
import('devexpress-reporting-react/dx-report-viewer')
)
const loadViewerCss = () => {
const styles = [
new URL('@devexpress/analytics-core/dist/css/dx-analytics.common.css', import.meta.url).href,
new URL('@devexpress/analytics-core/dist/css/dx-analytics.light.css', import.meta.url).href,
new URL('devexpress-reporting/dist/css/dx-webdocumentviewer.css', import.meta.url).href,
]
styles.forEach((href) => {
if (document.querySelector(`link[href="${href}"]`)) return
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = href
document.head.appendChild(link)
})
}
const DevexpressReportViewer: React.FC = () => {
const { translate } = useLocalization()
const { id } = useParams<{ id: string }>()
const location = useLocation()
// Query string parametrelerini reportUrl'e ekle
useEffect(() => {
loadViewerCss()
}, [])
const reportUrlWithParams = useMemo(() => {
if (location.search) {
return `${id}${location.search}`
}
return id
if (!id) return ''
return location.search ? `${id}${location.search}` : id
}, [id, location.search])
if (!id) {
return null
}
if (!id) return null
return (
<Container>
<Helmet
titleTemplate="%s | Erp Platform"
title={translate('::' + 'App.Reports')}
title={translate('::App.Reports')}
defaultTitle="Erp Platform"
></Helmet>
/>
<Suspense fallback={<div>Rapor yükleniyor...</div>}>
<ReportViewer reportUrl={reportUrlWithParams}>
<RequestOptions host={`${import.meta.env.VITE_API_URL}/`} invokeAction="DXXRDV" />
<RequestOptions
host={`${import.meta.env.VITE_API_URL}/`}
invokeAction="DXXRDV"
/>
</ReportViewer>
</Suspense>
</Container>
)
}

View file

@ -37,7 +37,7 @@ export default defineConfig(async ({ mode }) => {
globPatterns: ['**/*.{js,css,html,wasm}'],
// Büyük asset'leri de cache'leyebil
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
// EN KRİTİK: yeni SW beklemeden kontrolü alsın
clientsClaim: true,
@ -139,6 +139,7 @@ export default defineConfig(async ({ mode }) => {
build: {
outDir: 'dist',
chunkSizeWarningLimit: 2000,
sourcemap: false,
emptyOutDir: true,
rollupOptions: {
@ -154,6 +155,41 @@ export default defineConfig(async ({ mode }) => {
}
return 'assets/[name]-[hash][extname]'
},
manualChunks(id) {
// 🔴 DEVEXTREME (esm + non-esm + react wrapper)
if (
id.includes('node_modules/devextreme') ||
id.includes('node_modules/devextreme-react')
) {
return 'dx-framework'
}
// 🔴 REPORTING
if (id.includes('devexpress-reporting') || id.includes('@devexpress')) {
return 'dx-reporting'
}
// 🟣 EXPORT TOOLS
if (
id.includes('exceljs') ||
id.includes('xlsx') ||
id.includes('jspdf') ||
id.includes('html2canvas') ||
id.includes('file-saver')
) {
return 'export-tools'
}
// 🟡 EDITOR
if (id.includes('monaco-editor') || id.includes('codemirror')) {
return 'editor'
}
// ⚪ KALAN VENDOR
if (id.includes('node_modules')) {
return 'vendor'
}
},
},
},
},