erp-platform/ui/src/views/list/Pivot.tsx

506 lines
17 KiB
TypeScript
Raw Normal View History

2025-05-06 06:45:49 +00:00
import Container from '@/components/shared/Container'
import { DX_CLASSNAMES } from '@/constants/app.constant'
import { GridDto, ListFormCustomizationTypeEnum } from '@/proxy/form/models'
2025-05-06 06:45:49 +00:00
import {
getListFormCustomization,
postListFormCustomization,
} from '@/services/list-form-customization.service'
2025-05-06 06:45:49 +00:00
import { useLocalization } from '@/utils/hooks/useLocalization'
import Chart, {
ChartRef,
CommonSeriesSettings,
Size,
Tooltip,
} from 'devextreme-react/chart'
2025-05-06 06:45:49 +00:00
import PivotGrid, {
Export,
2025-05-06 06:45:49 +00:00
FieldChooser,
FieldPanel,
HeaderFilter,
LoadPanel,
PivotGridRef,
2025-05-06 06:45:49 +00:00
PivotGridTypes,
Scrolling,
Search,
StateStoring,
2025-05-06 06:45:49 +00:00
} from 'devextreme-react/pivot-grid'
import CustomStore from 'devextreme/data/custom_store'
import PivotGridDataSource, { Field } from 'devextreme/ui/pivot_grid/data_source'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2025-05-06 06:45:49 +00:00
import { Helmet } from 'react-helmet'
import { GridColumnData } from './GridColumnData'
import {
addCss,
addJs,
controlStyleCondition,
pivotFieldConvertDataType,
setGridPanelColor,
} from './Utils'
import { useFilters } from './useFilters'
2025-09-25 14:40:16 +00:00
import WidgetGroup from '@/components/common/WidgetGroup'
import { Button, Notification, toast } from '@/components/ui'
import { FaCog, FaTimes, FaUndo } from 'react-icons/fa'
import { usePermission } from '@/utils/hooks/usePermission'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { usePWA } from '@/utils/hooks/usePWA'
2025-11-08 20:22:50 +00:00
import { layoutTypes } from '../admin/listForm/edit/types'
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
import { useListFormColumns } from './useListFormColumns'
import { useStoreState } from '@/store'
2025-05-06 06:45:49 +00:00
interface PivotProps {
2025-05-06 06:45:49 +00:00
listFormCode: string
searchParams?: URLSearchParams
isSubForm?: boolean
level?: number
refreshData?: () => Promise<void>
2025-09-22 14:08:42 +00:00
gridDto?: GridDto
2025-09-29 08:33:51 +00:00
refreshGridDto: () => Promise<void>
2025-05-06 06:45:49 +00:00
}
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk
const Pivot = (props: PivotProps) => {
2025-09-22 14:08:42 +00:00
const { listFormCode, searchParams, isSubForm, level, gridDto } = props
2025-05-06 06:45:49 +00:00
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const isPwaMode = usePWA()
2025-05-06 06:45:49 +00:00
const gridRef = useRef<PivotGridRef>()
const chartRef = useRef<ChartRef>(null)
2025-05-06 06:45:49 +00:00
const refListFormCode = useRef('')
const config = useStoreState((state) => state.abpConfig.config)
2025-05-06 06:45:49 +00:00
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
const [columnData, setColumnData] = useState<GridColumnData[]>()
const { filterToolbarData, ...filterData } = useFilters({
gridDto,
gridRef,
listFormCode,
})
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
2025-05-06 06:45:49 +00:00
const { getBandedColumns } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
2025-10-16 22:15:34 +00:00
gridRef,
2025-05-06 06:45:49 +00:00
})
const onCellPrepared = useCallback((e: any) => {
2025-05-06 06:45:49 +00:00
const columnFormats = gridDto?.columnFormats
if (!columnFormats) {
return
}
// satir, hucre yada header vb. kisimlara conditional style uygulamak icin
for (let indxCol = 0; indxCol < columnFormats.length; indxCol++) {
const colFormat = columnFormats[indxCol]
for (let indxStyl = 0; indxStyl < colFormat.columnStylingDto.length; indxStyl++) {
const colStyle = colFormat.columnStylingDto[indxStyl] // uygulanacak style
if (e.rowType == colStyle.rowType) {
// header, filter, data, group, summaries ..her birisine style uygulanabilir
// style bütün satıra uygulansın olarak seçili ise yada sadece ilgili field üzerinde ise
if (colStyle.useRow || e.column?.dataField == colFormat.fieldName) {
if (
!colStyle.conditionValue ||
controlStyleCondition(e.data, colFormat.fieldName, colStyle)
) {
// css sınıf ismi var ise uygula
if (colStyle.cssClassName) {
e.cellElement.addClass(colStyle.cssClassName)
}
// css inline style var ise uygula
if (colStyle.cssStyles) {
e.cellElement.attr('style', e.cellElement.attr('style') + ';' + colStyle.cssStyles)
}
}
}
}
}
}
}, [gridDto])
2025-05-06 06:45:49 +00:00
const clearPivotFilters = useCallback(() => {
const grid = gridRef.current?.instance()
if (!grid) return
const ds = grid.getDataSource()
if (ds) {
const fields = ds.fields()
fields.forEach((f: any) => {
f.filterValues = undefined
f.filterType = undefined
})
ds.reload()
}
}, [])
const moveAllFieldsToFilterArea = useCallback(() => {
const grid = gridRef.current?.instance()
if (!grid) return
const ds = grid.getDataSource()
if (!ds) return
const fields = ds.fields()
fields.forEach((field: any) => {
field.area = 'filter' // tüm alanları filtre alanına taşı
field.areaIndex = undefined
})
ds.fields(fields)
ds.reload() // PivotGridi yeniden yükle
grid.repaint() // UI güncelle
}, [])
const resetPivotGridState = useCallback(async () => {
const grid = gridRef.current?.instance()
if (grid) {
// kullaniciya ait kayitli grid state i sil customizationData boşalt silinsin.
await postListFormCustomization({
listFormCode: listFormCode,
customizationType: ListFormCustomizationTypeEnum.GridState,
filterName: `pivot-${gridRef.current?.instance()?.option('stateStoring')?.storageKey ?? ''}`,
customizationData: '',
})
await props.refreshGridDto()
clearPivotFilters()
moveAllFieldsToFilterArea()
}
}, [listFormCode, props, clearPivotFilters, moveAllFieldsToFilterArea])
const onExporting = useCallback(async (e: PivotGridTypes.ExportingEvent) => {
e.cancel = true
const pivot = gridRef?.current?.instance()
if (!pivot) return
try {
// PivotGrid sadece Excel export destekliyor
const [{ Workbook }, { saveAs }, { exportPivotGrid }] = await Promise.all([
import('exceljs'),
import('file-saver'),
import('devextreme/excel_exporter'),
])
const workbook = new Workbook()
const worksheet = workbook.addWorksheet(`${listFormCode}_pivot`)
await exportPivotGrid({
component: pivot as any,
worksheet,
})
const buffer = await workbook.xlsx.writeBuffer()
saveAs(
new Blob([buffer], { type: 'application/octet-stream' }),
`${listFormCode}_pivot_export.xlsx`,
)
} catch (err) {
console.error('Pivot 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-end' },
)
}
}, [listFormCode, translate])
// StateStoring fonksiyonlarını ref'e kaydet
2025-05-06 06:45:49 +00:00
const customSaveState = useCallback(
(state: any) => {
return postListFormCustomization({
2025-05-06 06:45:49 +00:00
listFormCode: listFormCode,
customizationType: ListFormCustomizationTypeEnum.GridState,
filterName: `pivot-${gridRef.current?.instance()?.option('stateStoring')?.storageKey ?? ''}`,
2025-05-06 06:45:49 +00:00
customizationData: JSON.stringify(state),
}).then(() => {
setGridPanelColor(statedGridPanelColor)
})
},
2025-05-06 06:45:49 +00:00
[listFormCode],
)
const customLoadState = useCallback(
() => {
return getListFormCustomization(
2025-05-06 06:45:49 +00:00
listFormCode,
ListFormCustomizationTypeEnum.GridState,
`pivot-${gridRef.current?.instance()?.option('stateStoring')?.storageKey ?? ''}`,
2025-05-06 06:45:49 +00:00
).then((response: any) => {
setGridPanelColor(statedGridPanelColor)
if (response.data?.length > 0) {
return JSON.parse(response.data[0].customizationData)
}
})
},
2025-05-06 06:45:49 +00:00
[listFormCode],
)
const customSaveStateRef = useRef(customSaveState)
const customLoadStateRef = useRef(customLoadState)
2025-05-06 06:45:49 +00:00
useEffect(() => {
customSaveStateRef.current = customSaveState
customLoadStateRef.current = customLoadState
}, [customSaveState, customLoadState])
useEffect(() => {
refListFormCode.current = listFormCode
2025-05-06 06:45:49 +00:00
}, [listFormCode])
useEffect(() => {
if (!gridDto) {
return
}
// Set js and css
const grdOpt = gridDto.gridOptions
if (grdOpt.customJsSources.length) {
for (const js of grdOpt.customJsSources) {
addJs(js)
}
}
if (grdOpt.customStyleSources.length) {
for (const css of grdOpt.customStyleSources) {
addCss(css)
}
}
}, [gridDto])
// Kolonları memoize et
const memoizedColumns = useMemo(() => {
if (!gridDto || !config) return undefined
2025-05-06 06:45:49 +00:00
const cols = getBandedColumns()
return cols?.filter((a) => a.colData?.pivotSettingsDto.isPivot)
}, [gridDto, config])
// DataSource'u memoize et
const memoizedDataSource = useMemo(() => {
if (!gridDto) return undefined
2025-05-06 06:45:49 +00:00
const cols = getBandedColumns()
return createSelectDataSource(
2025-05-06 06:45:49 +00:00
gridDto.gridOptions,
listFormCode,
searchParams,
2025-11-08 20:22:50 +00:00
layoutTypes.pivot,
cols,
2025-05-06 06:45:49 +00:00
)
}, [gridDto, listFormCode, createSelectDataSource])
2025-05-06 06:45:49 +00:00
useEffect(() => {
if (memoizedColumns) {
setColumnData(memoizedColumns)
}
}, [memoizedColumns])
2025-05-06 06:45:49 +00:00
useEffect(() => {
if (memoizedDataSource) {
setGridDataSource(memoizedDataSource)
}
}, [memoizedDataSource])
// Pivot dataSource ve fields'i set et + StateStoring ayarla
useEffect(() => {
if (!columnData || !gridDataSource || !gridRef?.current || !gridDto) {
2025-05-06 06:45:49 +00:00
return
}
const instance = gridRef?.current?.instance()
if (!instance) return
// 1. StateStoring'i ÖNCE ayarla
const stateStoring: any = {
enabled: gridDto?.gridOptions.stateStoringDto?.enabled,
type: gridDto?.gridOptions.stateStoringDto?.type,
savingTimeout: gridDto?.gridOptions.stateStoringDto?.savingTimeout,
storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
}
if (
gridDto?.gridOptions.stateStoringDto?.enabled &&
gridDto?.gridOptions.stateStoringDto?.type === 'custom'
) {
stateStoring.customSave = (state: any) => customSaveStateRef.current(state)
stateStoring.customLoad = () => customLoadStateRef.current()
}
instance.option('stateStoring', stateStoring)
// 2. Default fields'i hazırla
const defaultFields: any = columnData?.map((b) => {
2025-05-06 06:45:49 +00:00
return {
dataField: b.dataField,
caption: b.caption,
dataType: pivotFieldConvertDataType(b.dataType),
area: b.colData?.pivotSettingsDto.area,
format: b.colData?.pivotSettingsDto.format,
summaryType: b.colData?.pivotSettingsDto.summaryType,
groupInterval: b.colData?.pivotSettingsDto.groupInterval,
sortOrder: b.colData?.pivotSettingsDto.sortOrder,
expanded: b.colData?.pivotSettingsDto.expanded,
wordWrapEnabled: b.colData?.pivotSettingsDto.wordWrapEnabled,
visible: true,
width: b.width,
} as Field
})
// 3. DataSource'u ayarla - fields olmadan
2025-05-06 06:45:49 +00:00
const dataSource: PivotGridTypes.Properties['dataSource'] = {
remoteOperations: true,
store: gridDataSource,
fields: defaultFields,
2025-05-06 06:45:49 +00:00
}
instance.option('dataSource', dataSource)
// 4. DataSource set edildikten SONRA state'i yükle - state fields'i override edecek
setTimeout(() => {
const ds = instance.getDataSource()
if (ds && typeof ds.state === 'function') {
ds.state(null) // Bu saved state'teki fields'i kullanacak
}
}, 50)
}, [columnData, gridDataSource, gridDto])
// Chart binding - sadece bir kez
useEffect(() => {
if (!gridRef?.current || !chartRef?.current) return
const pivotInstance = gridRef?.current?.instance()
const chartInstance = chartRef?.current?.instance()
if (pivotInstance && chartInstance) {
pivotInstance.bindChart(chartInstance, {
dataFieldsDisplayMode: 'splitPanes',
alternateDataFields: false,
})
2025-05-06 06:45:49 +00:00
}
}, [gridDto])
2025-05-06 06:45:49 +00:00
return (
2025-09-25 14:40:16 +00:00
<>
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
<Container className={DX_CLASSNAMES}>
{!isSubForm && (
<Helmet
2025-11-03 11:25:05 +00:00
titleTemplate="%s | Erp Platform"
2025-09-25 14:40:16 +00:00
title={translate('::' + gridDto?.gridOptions.title)}
2025-11-03 11:25:05 +00:00
defaultTitle="Erp Platform"
2025-09-25 14:40:16 +00:00
></Helmet>
)}
{gridDto && columnData && (
<div className="p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700 ">
<div className="flex justify-end items-center">
<div className="relative pb-1 flex gap-1 border-b-1">
2025-12-02 21:07:31 +00:00
<Button
size="xs"
variant={'default'}
2025-12-02 21:07:31 +00:00
className="text-sm flex items-center gap-1"
onClick={clearPivotFilters}
title={translate('::ListForms.ListForm.RemoveFilter')}
>
<FaTimes className="w-3 h-3" /> {translate('::ListForms.ListForm.RemoveFilter')}
</Button>
<Button
size="xs"
variant={'default'}
2025-12-02 21:07:31 +00:00
className="text-sm flex items-center gap-1"
onClick={resetPivotGridState}
title={translate('::ListForms.ListForm.ResetGridState')}
>
<FaUndo className="w-3 h-3" /> {translate('::ListForms.ListForm.ResetGridState')}
</Button>
{checkPermission(gridDto?.gridOptions.permissionDto.u) && (
<Button
size="xs"
variant={'default'}
className="text-sm"
onClick={() => {
window.open(
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(
':listFormCode',
listFormCode,
),
isPwaMode ? '_self' : '_blank',
)
}}
title={translate('::ListForms.ListForm.Manage')}
>
2025-09-29 08:33:51 +00:00
<FaCog className="w-3 h-3" />
</Button>
)}
</div>
</div>
2025-09-25 14:40:16 +00:00
{gridDto.gridOptions.pivotOptionDto.showChart && (
<Chart ref={chartRef as any}>
<Size height={gridDto.gridOptions.pivotOptionDto.chartHeight} />
<Tooltip enabled={true}></Tooltip>
<CommonSeriesSettings
type={gridDto.gridOptions.pivotOptionDto.chartCommonSeriesType}
/>
</Chart>
)}
2025-05-06 06:45:49 +00:00
<div className="dx-datagrid-header-panel h-1"></div>
2025-09-25 14:40:16 +00:00
<PivotGrid
ref={gridRef as any}
id={'Pivot-' + listFormCode}
allowFiltering={gridDto.gridOptions.filterRowDto.visible}
allowSorting={gridDto.gridOptions.sortMode !== 'none'}
allowSortingBySummary={gridDto.gridOptions.sortMode !== 'none'}
height={gridDto.gridOptions.height || '100%'}
width={gridDto.gridOptions.width || '100%'}
showBorders={gridDto.gridOptions.columnOptionDto?.showBorders}
rtlEnabled={gridDto.gridOptions.columnOptionDto?.rtlEnabled}
hoverStateEnabled={gridDto.gridOptions.columnOptionDto?.hoverStateEnabled}
onCellPrepared={onCellPrepared}
onExporting={onExporting}
2025-05-06 06:45:49 +00:00
>
<Export enabled={gridDto.gridOptions.exportDto?.enabled} />
2025-09-25 14:40:16 +00:00
<HeaderFilter
allowSelectAll={gridDto.gridOptions.selectionDto.allowSelectAll}
width={gridDto.gridOptions.headerFilterDto.width}
height={gridDto.gridOptions.headerFilterDto.height}
>
<Search
enabled={gridDto.gridOptions.headerFilterDto.allowSearch}
timeout={gridDto.gridOptions.headerFilterDto.searchTimeout}
></Search>
</HeaderFilter>
<FieldPanel
allowFieldDragging={gridDto.gridOptions.pivotOptionDto.allowFieldDragging}
visible={gridDto.gridOptions.pivotOptionDto.showFieldPanel}
showDataFields={gridDto.gridOptions.pivotOptionDto.showDataFields}
showColumnFields={gridDto.gridOptions.pivotOptionDto.showColumnFields}
showRowFields={gridDto.gridOptions.pivotOptionDto.showRowFields}
showFilterFields={gridDto.gridOptions.pivotOptionDto.showFilterFields}
/>
<FieldChooser
enabled={gridDto.gridOptions.pivotOptionDto.columnChooserEnabled}
2025-09-25 14:40:16 +00:00
height={500}
/>
<LoadPanel
enabled={gridDto.gridOptions.pagerOptionDto?.loadPanelEnabled as boolean | undefined}
text={gridDto.gridOptions.pagerOptionDto?.loadPanelText}
/>
2025-09-25 14:40:16 +00:00
<Scrolling mode={gridDto.gridOptions.pagerOptionDto.scrollingMode} />
</PivotGrid>
</div>
2025-09-25 14:40:16 +00:00
)}
</Container>
</>
2025-05-06 06:45:49 +00:00
)
}
export default Pivot