sozsoft-platform/ui/src/views/list/Pivot.tsx
Sedat Öztürk 429227df1d Initial
2026-02-24 23:44:16 +03:00

592 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Container from '@/components/shared/Container'
import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant'
import { GridDto, ListFormCustomizationTypeEnum } from '@/proxy/form/models'
import {
getListFormCustomization,
postListFormCustomization,
} from '@/services/list-form-customization.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Chart, { ChartRef, CommonSeriesSettings, Size, Tooltip } from 'devextreme-react/chart'
import PivotGrid, {
Export,
FieldChooser,
FieldPanel,
HeaderFilter,
LoadPanel,
PivotGridRef,
PivotGridTypes,
Scrolling,
Search,
} from 'devextreme-react/pivot-grid'
import CustomStore from 'devextreme/data/custom_store'
import { Field } from 'devextreme/ui/pivot_grid/data_source'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { GridColumnData } from './GridColumnData'
import {
addCss,
addJs,
controlStyleCondition,
pivotFieldConvertDataType,
setGridPanelColor,
} from './Utils'
import { useFilters } from './useFilters'
import WidgetGroup from '@/components/common/WidgetGroup'
import { Button, Dropdown, Notification, toast } from '@/components/ui'
import { FaChevronDown, FaCog, FaEllipsisV, FaSave, 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'
import { layoutTypes } from '../admin/listForm/edit/types'
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
import { useListFormColumns } from './useListFormColumns'
import { useStoreState } from '@/store'
import Checkbox from '@/components/ui/Checkbox'
interface PivotProps {
listFormCode: string
searchParams?: URLSearchParams
isSubForm?: boolean
level?: number
refreshData?: () => Promise<void>
gridDto?: GridDto
refreshGridDto: () => Promise<void>
}
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk
const Pivot = (props: PivotProps) => {
const { listFormCode, searchParams, isSubForm, level, gridDto } = props
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const isPwaMode = usePWA()
const gridRef = useRef<PivotGridRef>()
const chartRef = useRef<ChartRef>(null)
const refListFormCode = useRef('')
const config = useStoreState((state) => state.abpConfig.config)
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
const [columnData, setColumnData] = useState<GridColumnData[]>()
const [showChart, setShowChart] = useState(false)
// StateStoring için storageKey'i memoize et
const storageKey = useMemo(() => {
return gridDto?.gridOptions.stateStoringDto?.storageKey ?? ''
}, [gridDto?.gridOptions.stateStoringDto?.storageKey])
const { filterToolbarData, ...filterData } = useFilters({
gridDto,
gridRef,
listFormCode,
})
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
const { getBandedColumns } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
gridRef,
})
const onCellPrepared = useCallback(
(e: any) => {
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],
)
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) {
// State'i veritabanından sil
await postListFormCustomization({
listFormCode: listFormCode,
customizationType: ListFormCustomizationTypeEnum.GridState,
filterName: `pivot-${storageKey}`,
customizationData: '',
})
// DataSource'u resetle
const ds = grid.getDataSource()
if (ds && typeof ds.state === 'function') {
ds.state(null) // State'i temizle
}
// Filtreleri temizle
clearPivotFilters()
setGridPanelColor('transparent')
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.ListForm.GridStateReset')}
</Notification>,
{ placement: 'top-end' },
)
}
}, [listFormCode, storageKey, clearPivotFilters, translate])
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
const customSaveState = useCallback(
(state: any) => {
return postListFormCustomization({
listFormCode: listFormCode,
customizationType: ListFormCustomizationTypeEnum.GridState,
filterName: `pivot-${storageKey}`,
customizationData: JSON.stringify(state),
}).then(() => {
setGridPanelColor(statedGridPanelColor)
})
},
[listFormCode, storageKey],
)
const customLoadState = useCallback(() => {
return getListFormCustomization(
listFormCode,
ListFormCustomizationTypeEnum.GridState,
`pivot-${storageKey}`,
).then((response: any) => {
if (response.data?.length > 0) {
setGridPanelColor(statedGridPanelColor)
return JSON.parse(response.data[0].customizationData)
}
return null
})
}, [listFormCode, storageKey])
useEffect(() => {
refListFormCode.current = listFormCode
}, [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)
}
}
// Set initial chart visibility
setShowChart(gridDto.gridOptions.pivotOptionDto.showChart ?? false)
}, [gridDto])
// Kolonları memoize et
const memoizedColumns = useMemo(() => {
if (!gridDto || !config) return undefined
const cols = getBandedColumns()
return cols?.filter((a) => a.colData?.pivotSettingsDto.isPivot)
}, [gridDto, config])
// DataSource'u memoize et
const memoizedDataSource = useMemo(() => {
if (!gridDto) return undefined
const cols = getBandedColumns()
return createSelectDataSource(
gridDto.gridOptions,
listFormCode,
searchParams,
layoutTypes.pivot,
cols,
)
}, [gridDto, listFormCode, createSelectDataSource])
useEffect(() => {
if (memoizedColumns) {
setColumnData(memoizedColumns)
}
}, [memoizedColumns])
useEffect(() => {
if (memoizedDataSource) {
setGridDataSource(memoizedDataSource)
}
}, [memoizedDataSource])
// StateStoring'i devre dışı bırak - manuel kaydetme kullanılacak
useEffect(() => {
if (!gridDto || !gridRef?.current) return
const instance = gridRef?.current?.instance()
if (instance) {
instance.option('stateStoring', {
enabled: false,
})
}
}, [gridDto])
// Pivot dataSource ve fields'i set et
useEffect(() => {
if (!columnData || !gridDataSource || !gridRef?.current || !gridDto) {
return
}
const instance = gridRef?.current?.instance()
if (!instance) return
// Default fields'i hazırla
const defaultFields: any = columnData?.map((b) => {
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
})
// DataSource'u ayarla
const dataSource: PivotGridTypes.Properties['dataSource'] = {
remoteOperations: true,
store: gridDataSource,
fields: defaultFields,
}
instance.option('dataSource', dataSource)
}, [columnData, gridDataSource, gridDto])
// Component mount olduğunda state'i bir kez yükle
useEffect(() => {
if (!gridDto || !gridRef?.current || !columnData || !gridDataSource) return
const instance = gridRef?.current?.instance()
if (instance) {
customLoadState()
.then((state) => {
if (state) {
const ds = instance.getDataSource()
if (ds && typeof ds.state === 'function') {
ds.state(state)
}
}
})
.catch((err) => {
console.error('Pivot state load error:', err)
})
}
}, [gridDto, columnData, gridDataSource, customLoadState])
// Chart binding - showChart değiştiğinde de çalışmalı
useEffect(() => {
if (!showChart || !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,
})
}
}, [showChart, gridDto])
return (
<>
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
<Container className={DX_CLASSNAMES}>
{!isSubForm && (
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + gridDto?.gridOptions.title)}
defaultTitle={APP_NAME}
></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 flex gap-1">
<label className="flex items-center gap-1 text-xs cursor-pointer">
<span>{translate('::ListForms.ListFormEdit.ShowChart')}</span>
<Checkbox checked={showChart} onChange={(checked) => setShowChart(checked)} />
</label>
<div className="dx-menu">
<Dropdown
placement="bottom-end"
renderTitle={
<Button
size="xs"
variant={'default'}
className="text-sm flex items-center gap-1 border-0"
title="Menu"
>
<i className="dx-icon dx-icon-overflow"></i>
<span>{translate('::ListForms.ListForm.GridMenu')}</span>
<FaChevronDown className="w-2 h-2" />
</Button>
}
>
<Dropdown.Item
eventKey="removeFilter"
onClick={clearPivotFilters}
className="px-2"
>
<span className="flex items-center gap-2">
<FaTimes className="w-3 h-3" />
<span>{translate('::ListForms.ListForm.RemoveFilter')}</span>
</span>
</Dropdown.Item>
<Dropdown.Item
eventKey="saveGridState"
onClick={() => {
const instance = gridRef.current?.instance()
if (instance) {
const ds = instance.getDataSource()
if (ds && typeof ds.state === 'function') {
const currentState = ds.state()
customSaveState(currentState)
.then(() => {
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.ListForm.GridStateSaved')}
</Notification>,
{ placement: 'top-end' },
)
})
.catch(() => {
toast.push(
<Notification type="danger" duration={2500}>
{translate('::ListForms.ListForm.GridStateSaveError')}
</Notification>,
{ placement: 'top-end' },
)
})
}
}
}}
className="px-2"
>
<span className="flex items-center gap-2">
<FaSave className="w-3 h-3" />
<span>{translate('::ListForms.ListForm.SaveGridState')}</span>
</span>
</Dropdown.Item>
<Dropdown.Item
eventKey="resetGridState"
onClick={resetPivotGridState}
className="px-2"
>
<span className="flex items-center gap-2">
<FaUndo className="w-3 h-3" />
<span>{translate('::ListForms.ListForm.ResetGridState')}</span>
</span>
</Dropdown.Item>
{checkPermission(gridDto?.gridOptions.permissionDto.u) && (
<Dropdown.Item
eventKey="formManager"
onClick={() => {
window.open(
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(
':listFormCode',
listFormCode,
),
isPwaMode ? '_self' : '_blank',
)
}}
className="px-2"
>
<span className="flex items-center gap-2">
<FaCog className="w-3 h-3" />
<span>{translate('::ListForms.ListForm.Manage')}</span>
</span>
</Dropdown.Item>
)}
</Dropdown>
</div>
</div>
</div>
{showChart && (
<Chart ref={chartRef as any}>
<Size height={gridDto.gridOptions.pivotOptionDto.chartHeight} />
<Tooltip enabled={true}></Tooltip>
<CommonSeriesSettings
type={gridDto.gridOptions.pivotOptionDto.chartCommonSeriesType}
/>
</Chart>
)}
<div className="dx-datagrid-header-panel h-1"></div>
<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}
>
<Export enabled={gridDto.gridOptions.exportDto?.enabled} />
<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}
height={500}
/>
<LoadPanel
enabled={
gridDto.gridOptions.pagerOptionDto?.loadPanelEnabled as boolean | undefined
}
text={gridDto.gridOptions.pagerOptionDto?.loadPanelText}
/>
<Scrolling mode={gridDto.gridOptions.pagerOptionDto.scrollingMode} />
</PivotGrid>
</div>
)}
</Container>
</>
)
}
export default Pivot