/* eslint-disable @typescript-eslint/no-explicit-any */ import Container from '@/components/shared/Container' import { Dialog, Notification, toast } from '@/components/ui' import { DX_CLASSNAMES } from '@/constants/app.constant' import { DbTypeEnum, EditingFormItemDto, GridDto, ListFormCustomizationTypeEnum, PlatformEditorTypes } from '@/proxy/form/models' import { getListFormCustomization, postListFormCustomization, } from '@/services/list-form-customization.service' import { useListFormColumns } from '@/shared/useListFormColumns' import { useListFormCustomDataSource } from '@/shared/useListFormCustomDataSource' import { useLocalization } from '@/utils/hooks/useLocalization' import useResponsive from '@/utils/hooks/useResponsive' import { captionize } from 'devextreme/core/utils/inflector' import { Template } from 'devextreme-react/core/template' import TreeListDx, { ColumnChooser, ColumnFixing, Editing, FilterPanel, FilterRow, HeaderFilter, IStateStoringProps, LoadPanel, Pager, Paging, RemoteOperations, Scrolling, SearchPanel, Selection, Sorting, Toolbar, TreeListTypes, } from 'devextreme-react/tree-list' import { Item } from 'devextreme-react/toolbar' import CustomStore from 'devextreme/data/custom_store' import { useCallback, useEffect, useRef, useState } from 'react' import { Helmet } from 'react-helmet' import { RowMode, SimpleItemWithColData } from '../form/types' import { GridColumnData } from './GridColumnData' import GridFilterDialogs from './GridFilterDialogs' import { addCss, addJs, controlStyleCondition, GridExtraFilterState, setFormEditingExtraItemValues, setGridPanelColor, } from './Utils' import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent' import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent' import { useFilters } from './useFilters' import { useToolbar } from './useToolbar' import WidgetGroup from '@/components/ui/Widget/WidgetGroup' import { GridExtraFilterToolbar } from './GridExtraFilterToolbar' import { getList } from '@/services/form.service' interface TreeProps { listFormCode: string searchParams?: URLSearchParams isSubForm?: boolean level?: number refreshData?: () => Promise gridDto?: GridDto } const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' const Tree = (props: TreeProps) => { const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props const { translate } = useLocalization() const { smaller } = useResponsive() const gridRef = useRef() const refListFormCode = useRef('') const [treeListDataSource, setTreeListDataSource] = useState>() const [columnData, setColumnData] = useState() const [formData, setFormData] = useState() const [mode, setMode] = useState('view') const [extraFilters, setExtraFilters] = useState([]) const [gridDto, setGridDto] = useState() const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const [expandedRowKeys, setExpandedRowKeys] = useState([]) const preloadExportLibs = () => { import('exceljs') import('file-saver') import('devextreme/excel_exporter') import('jspdf') import('devextreme/pdf_exporter') } type EditorOptionsWithButtons = { buttons?: any[] } & Record const defaultSearchParams = useRef(null) useEffect(() => { const initializeTreeList = async () => { const response = await getList({ listFormCode }) setGridDto(response.data) } if (extGridDto === undefined) { initializeTreeList() } else { setGridDto(extGridDto) } }, [listFormCode, extGridDto]) useEffect(() => { if (!defaultSearchParams.current) { defaultSearchParams.current = searchParams?.get('filter') ?? null } }, [searchParams]) const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({ gridDto, listFormCode, getSelectedRowKeys, getSelectedRowsData, refreshData, expandAll, collapseAll, getFilter, isTree: true }) const { filterToolbarData, ...filterData } = useFilters({ gridDto, gridRef, listFormCode, }) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef }) const { getBandedColumns } = useListFormColumns({ gridDto, listFormCode, isSubForm, gridRef, }) function extractSearchParamsFields(filter: any): [string, string, any][] { if (!Array.isArray(filter)) return [] if (filter.length === 3 && typeof filter[0] === 'string') { return [filter as [string, string, any]] } return filter.flatMap((f) => extractSearchParamsFields(f)) } async function getSelectedRowKeys() { const tree = gridRef.current?.instance if (!tree) { return [] } return await tree.getSelectedRowKeys() } function getSelectedRowsData() { const tree = gridRef.current?.instance if (!tree) { return [] } return tree.getSelectedRowsData() } function expandAll() { const tree = gridRef.current?.instance if (!tree) return tree.forEachNode((node: any) => { if (node.hasChildren) { tree.expandRow(node.key) } }) } function collapseAll() { const tree = gridRef.current?.instance if (!tree) return tree.forEachNode((node: any) => { if (node.hasChildren) { tree.collapseRow(node.key) } }) } function refreshData() { gridRef.current?.instance.refresh() } function getFilter() { const tree = gridRef.current?.instance if (!tree) { return } return tree.getCombinedFilter() } function onSelectionChanged(data: any) { const treeOpt = gridDto?.gridOptions const tree = gridRef.current?.instance if (!treeOpt || !tree) { return } if (treeOpt.editingOptionDto?.allowDeleting) { try { const opt = tree.option('toolbar') if (opt && opt.items && Array.isArray(opt.items)) { const deleteSelectedRecordsIndex = opt.items .map((e: any) => e.name) .indexOf('deleteSelectedRecords') if (deleteSelectedRecordsIndex >= 0) { tree.option( `toolbar.items[${deleteSelectedRecordsIndex}].options.visible`, data.selectedRowsData.length > 1, ) } } } catch (error) { console.error('Error updating toolbar items:', error) } } if (data.selectedRowsData.length) { setFormData(data.selectedRowsData[0]) } } function onCellPrepared(e: any) { const columnFormats = gridDto?.columnFormats if (!columnFormats) { return } 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] if (e.rowType == colStyle.rowType) { if (colStyle.useRow || e.column.dataField == colFormat.fieldName) { if ( !colStyle.conditionValue || controlStyleCondition(e.data, colFormat.fieldName, colStyle) ) { if (colStyle.cssClassName) { e.cellElement.addClass(colStyle.cssClassName) } if (colStyle.cssStyles) { e.cellElement.attr('style', e.cellElement.attr('style') + ';' + colStyle.cssStyles) } } } } } } } function onInitNewRow(e: any) { if (!gridDto?.columnFormats) { return } setMode('new') setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) for (const colFormat of gridDto?.columnFormats) { if (!colFormat.fieldName) { continue } if (colFormat.defaultValue != null) { e.data[colFormat.fieldName] = colFormat.defaultValue } if (extraFilters.some((f) => f.fieldName === colFormat.fieldName)) { continue } if (!searchParams) { continue } const rawFilter = searchParams?.get('filter') if (rawFilter) { const parsed = JSON.parse(rawFilter) const filters = extractSearchParamsFields(parsed) const hasFilter = filters.some(([field, op, val]) => field === colFormat.fieldName) if (hasFilter) { const fieldMatch = filters.find(([field, op, val]) => field === colFormat.fieldName) if (fieldMatch) { const [, , val] = fieldMatch const dType = colFormat.dataType as any switch (dType) { case 'date': case 'datetime': e.data[colFormat.fieldName] = new Date(val) break case 'number': e.data[colFormat.fieldName] = Number(val) break case 'boolean': e.data[colFormat.fieldName] = val === true || val === 'true' break case 'object': try { e.data[colFormat.fieldName] = JSON.parse(val) } catch {} break default: e.data[colFormat.fieldName] = val break } } } } } } function onRowInserting(e: TreeListTypes.RowInsertingEvent) { e.data = setFormEditingExtraItemValues(e.data) } function onRowUpdating(e: TreeListTypes.RowUpdatingEvent) { if (gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate) { if (Object.keys(e.newData).some((a) => a.includes(':'))) { Object.keys(e.oldData).forEach((col) => { if (col.includes(':')) { e.newData[col] = e.newData[col] ?? e.oldData[col] } }) } e.newData = setFormEditingExtraItemValues(e.newData) } else { let newData = { ...e.oldData, ...e.newData } newData = setFormEditingExtraItemValues(newData) Object.keys(newData).forEach((key) => { if (key.includes(':')) { delete newData[key] } }) e.newData = newData } if (gridDto?.gridOptions.keyFieldName) { delete e.newData[gridDto?.gridOptions.keyFieldName] } } function onEditingStart(e: TreeListTypes.EditingStartEvent) { setMode('edit') setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) const columns = e.component.option('columns') as GridColumnData[] columns?.forEach((col) => { if (!col.dataField?.includes(':')) { return } const field = col.dataField.split(':') if (!e.data[field[0]]) { return } const json = JSON.parse(e.data[field[0]]) e.data[col.dataField] = json[field[1]] }) } function onDataErrorOccurred(e: TreeListTypes.DataErrorOccurredEvent) { toast.push( {e.error?.message} , { placement: 'top-end', }, ) } const customSaveState = useCallback( (state: any) => postListFormCustomization({ listFormCode: listFormCode, customizationType: ListFormCustomizationTypeEnum.GridState, filterName: `tree-${gridRef.current?.instance.option('stateStoring')?.storageKey ?? ''}`, customizationData: JSON.stringify(state), }).then(() => { setGridPanelColor(statedGridPanelColor) }), [listFormCode], ) const customLoadState = useCallback( () => getListFormCustomization( listFormCode, ListFormCustomizationTypeEnum.GridState, `tree-${gridRef.current?.instance.option('stateStoring')?.storageKey ?? ''}`, ).then((response: any) => { setGridPanelColor(statedGridPanelColor) if (response.data?.length > 0) { return JSON.parse(response.data[0].customizationData) } }), [listFormCode], ) useEffect(() => { if (gridRef?.current) { gridRef.current.instance.option('columns', undefined) gridRef.current.instance.option('dataSource', undefined) gridRef.current.instance.state(null) } if (refListFormCode.current !== listFormCode) { setExtraFilters([]) } }, [listFormCode]) useEffect(() => { if (!gridDto) { return } const treeOpt = gridDto.gridOptions if (treeOpt.customJsSources.length) { for (const js of treeOpt.customJsSources) { addJs(js) } } if (treeOpt.customStyleSources.length) { for (const css of treeOpt.customStyleSources) { addCss(css) } } if (gridDto?.gridOptions.extraFilterDto) { setExtraFilters( gridDto.gridOptions.extraFilterDto.map((f) => ({ fieldName: f.fieldName, operator: f.operator, controlType: f.controlType, value: f.defaultValue ?? '', })), ) } if (gridDto.gridOptions.editingOptionDto?.popup) { setIsPopupFullScreen(gridDto.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) } // Set initial expanded row keys if (gridDto.gridOptions.treeOptionDto?.expandedRowKeys) { setExpandedRowKeys(gridDto.gridOptions.treeOptionDto.expandedRowKeys) } else if (gridDto.gridOptions.treeOptionDto?.autoExpandAll) { setExpandedRowKeys([]) } }, [gridDto]) useEffect(() => { if (!gridDto) return const cols = getBandedColumns() setColumnData(cols) const dataSource = createSelectDataSource(gridDto.gridOptions, listFormCode, searchParams, cols) setTreeListDataSource(dataSource) }, [gridDto, searchParams]) useEffect(() => { const activeFilters = extraFilters.filter((f) => f.value) let base: any = null if (defaultSearchParams.current) { base = JSON.parse(defaultSearchParams.current) } const baseTriplets = extractSearchParamsFields(base) const extraTriplets = activeFilters.map( (f) => [f.fieldName, f.operator, f.value] as [string, string, any], ) const merged = [...baseTriplets, ...extraTriplets].reduce( (acc, cur) => { const [field, op] = cur const idx = acc.findIndex((a) => a[0] === field && a[1] === op) if (idx >= 0) { acc[idx] = cur } else { acc.push(cur) } return acc }, [] as [string, string, any][], ) let filter: any = null if (merged.length === 1) { filter = merged[0] } else if (merged.length > 1) { filter = merged.reduce((acc, f, idx) => { if (idx === 0) return f return [acc, 'and', f] }, null as any) } if (filter) { searchParams?.set('filter', JSON.stringify(filter)) } else { searchParams?.delete('filter') } gridRef.current?.instance.refresh() }, [extraFilters]) useEffect(() => { refListFormCode.current = listFormCode if (!gridRef?.current) { return } gridRef.current.instance.option('columns', columnData as any) gridRef.current.instance.option('dataSource', treeListDataSource) const stateStoring: IStateStoringProps = { 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 = customSaveState stateStoring.customLoad = customLoadState } gridRef.current.instance.option('stateStoring', stateStoring) }, [columnData]) return ( <> {!isSubForm && ( )} {gridDto && columnData && (
{ setExpandedRowKeys((prev) => [...prev, e.key]) }, onRowCollapsing: (e: any) => { setExpandedRowKeys((prev) => prev.filter((k) => k !== e.key)) }, })} autoExpandAll={gridDto.gridOptions.treeOptionDto?.autoExpandAll || false} allowColumnResizing={gridDto.gridOptions.columnOptionDto?.allowColumnResizing} allowColumnReordering={gridDto.gridOptions.columnOptionDto?.allowColumnReordering} showBorders={gridDto.gridOptions.columnOptionDto?.showBorders} showRowLines={gridDto.gridOptions.columnOptionDto?.showRowLines} showColumnLines={gridDto.gridOptions.columnOptionDto?.showColumnLines} columnResizingMode={gridDto.gridOptions.columnOptionDto?.columnResizingMode} columnAutoWidth={gridDto.gridOptions.columnOptionDto?.columnAutoWidth} rtlEnabled={gridDto.gridOptions.columnOptionDto?.rtlEnabled} rowAlternationEnabled={gridDto.gridOptions.columnOptionDto?.rowAlternationEnabled} hoverStateEnabled={gridDto.gridOptions.columnOptionDto?.hoverStateEnabled} columnHidingEnabled={gridDto.gridOptions.columnOptionDto?.columnHidingEnabled} focusedRowEnabled={gridDto.gridOptions.columnOptionDto?.focusedRowEnabled} showColumnHeaders={gridDto.gridOptions.columnOptionDto?.showColumnHeaders} filterSyncEnabled={true} onSelectionChanged={onSelectionChanged} onInitNewRow={onInitNewRow} onCellPrepared={onCellPrepared} onRowInserting={onRowInserting} onRowUpdating={onRowUpdating} onEditingStart={onEditingStart} onDataErrorOccurred={onDataErrorOccurred} onEditCanceled={() => { setMode('view') setIsPopupFullScreen(false) }} onSaved={() => { setMode('view') setIsPopupFullScreen(false) }} onRowInserted={() => { props.refreshData?.() }} onRowUpdated={() => { props.refreshData?.() }} onRowRemoved={() => { props.refreshData?.() }} onContentReady={(e) => { // Restore expanded keys after data refresh (only if autoExpandAll is false) if ( !gridDto.gridOptions.treeOptionDto?.autoExpandAll && expandedRowKeys.length > 0 ) { e.component.option('expandedRowKeys', expandedRowKeys) } }} > setIsPopupFullScreen(!isPopupFullScreen), }, }, ], }} form={{ colCount: 1, onFieldDataChanged: (e) => { if (e.dataField) { const formItem = gridDto.gridOptions.editingFormDto .flatMap((group) => group.items || []) .find((i) => i.dataField === e.dataField) if (formItem?.editorScript) { try { eval(formItem.editorScript) } catch (err) { console.error('Script exec error', err) } } } }, items: gridDto.gridOptions.editingFormDto?.length > 0 ? (() => { const sortedFormDto = gridDto.gridOptions.editingFormDto .slice() .sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) // Tabbed item'ları grupla const tabbedItems = sortedFormDto.filter( (e: any) => e.itemType === 'tabbed', ) const result: any[] = [] // Helper function: item mapper const mapFormItem = (i: EditingFormItemDto) => { let editorOptions: EditorOptionsWithButtons = {} try { editorOptions = i.editorOptions && JSON.parse(i.editorOptions) if (editorOptions?.buttons) { editorOptions.buttons = (editorOptions?.buttons || []).map( (btn: any) => { if ( btn?.options?.onClick && typeof btn.options.onClick === 'string' ) { btn.options.onClick = eval(`(${btn.options.onClick})`) } return btn }, ) } const rawFilter = searchParams?.get('filter') if (rawFilter) { const parsed = JSON.parse(rawFilter) const filters = extractSearchParamsFields(parsed) const hasFilter = filters.some( ([field, op, val]) => field === i.dataField, ) if (hasFilter) { const existsInExtra = extraFilters.some( (f) => f.fieldName === i.dataField && !!f.value, ) if (!existsInExtra) { editorOptions = { ...editorOptions, readOnly: true, } } } } } catch {} const fieldName = i.dataField.split(':')[0] const listFormField = gridDto.columnFormats.find( (x: any) => x.fieldName === fieldName, ) if (listFormField?.sourceDbType === DbTypeEnum.Date) { editorOptions = { ...{ type: 'date', dateSerializationFormat: 'yyyy-MM-dd', displayFormat: 'shortDate', }, ...editorOptions, } } else if ( listFormField?.sourceDbType === DbTypeEnum.DateTime || listFormField?.sourceDbType === DbTypeEnum.DateTime2 || listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset ) { editorOptions = { ...{ type: 'datetime', dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ssxxx', displayFormat: 'shortDateShortTime', }, ...editorOptions, } } const item: SimpleItemWithColData = { canRead: listFormField?.canRead ?? false, canUpdate: listFormField?.canUpdate ?? false, canCreate: listFormField?.canCreate ?? false, canExport: listFormField?.canExport ?? false, dataField: i.dataField, name: i.dataField, editorType2: i.editorType2, editorType: i.editorType2 == PlatformEditorTypes.dxGridBox ? 'dxDropDownBox' : i.editorType2, colSpan: i.colSpan, isRequired: i.isRequired, editorOptions, editorScript: i.editorScript, } if (i.dataField.indexOf(':') >= 0) { item.label = { text: captionize(i.dataField.split(':')[1]) } } if ( (mode == 'edit' && !item.canUpdate) || (mode == 'new' && !item.canCreate) ) { item.editorOptions = { ...item.editorOptions, readOnly: true, } } return item } sortedFormDto.forEach((e: any) => { if (e.itemType !== 'tabbed') { result.push({ itemType: e.itemType, colCount: e.colCount, colSpan: e.colSpan, caption: e.caption, // Group'larda caption olmalı items: e.items ?.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) .map(mapFormItem) .filter((a: any) => { if (mode === 'view') { return a.canRead } else if (mode === 'new') { return a.canCreate || a.canRead } else if (mode === 'edit') { return a.canUpdate || a.canRead } else { return false } }), }) } else if (tabbedItems.length > 0 && e === tabbedItems[0]) { // Tabbed için caption OLMAMALI - sadece tabs array içinde title kullan result.push({ itemType: 'tabbed', colCount: 1, colSpan: 1, // caption kullanma! Tabs içindeki title'lar yeterli tabs: tabbedItems.map((tabbedItem: any) => { // Backend'den gelen colCount ve colSpan değerlerini kullan const effectiveColCount = tabbedItem.colCount || 1 return { title: tabbedItem.caption, // Her tab'ın title'ı colCount: effectiveColCount, items: tabbedItem.items ?.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) .map(mapFormItem) .filter((a: any) => { if (mode === 'view') { return a.canRead } else if (mode === 'new') { return a.canCreate || a.canRead } else if (mode === 'edit') { return a.canUpdate || a.canRead } else { return false } }), } }), }) } }) return result })() : undefined, }} >