import Container from '@/components/shared/Container' import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant' import { GridDto, ListFormCustomizationTypeEnum } from '@/proxy/form/models' import { useLocalization } from '@/utils/hooks/useLocalization' import Gantt, { Column, ContextMenu, Editing, FilterRow, type GanttRef, HeaderFilter, Item, Sorting, Tasks, Toolbar, Validation, } from 'devextreme-react/gantt' import { useCallback, useEffect, useRef, useState } from 'react' import { Helmet } from 'react-helmet' import { getList } from '@/services/form.service' import { useListFormCustomDataSource } from './useListFormCustomDataSource' import { addCss, addJs } from './Utils' import { layoutTypes } from '../admin/listForm/edit/types' import WidgetGroup from '@/components/ui/Widget/WidgetGroup' import { ROUTES_ENUM } from '@/routes/route.constant' import { usePWA } from '@/utils/hooks/usePWA' import type { ContentReadyEvent, GanttScaleType } from 'devextreme/ui/gantt' import { useListFormColumns } from './useListFormColumns' import CustomStore from 'devextreme/data/custom_store' import { GridColumnData } from './GridColumnData' import { Loading } from '@/components/shared' import { usePermission } from '@/utils/hooks/usePermission' import { useListFormStateStoring } from './useListFormStateStoring' import TreeList from 'devextreme/ui/tree_list' import { postListFormCustomization } from '@/services/list-form-customization.service' import { useFilters } from './useFilters' import GridFilterDialogs from './GridFilterDialogs' type GanttInstance = ReturnType type GanttTreeListInstance = { element: () => HTMLElement getVisibleColumns: () => Array> state: { (): Record (value: Record | null): void } } const getGanttTreeList = (gantt: GanttInstance) => { const element = (gantt.element() as HTMLElement).querySelector('.dx-treelist') return element ? (TreeList.getInstance(element as HTMLElement) as unknown as GanttTreeListInstance) : undefined } interface GanttViewProps { listFormCode: string searchParams?: URLSearchParams isSubForm?: boolean level?: number refreshData?: () => Promise gridDto?: GridDto } const GanttView = (props: GanttViewProps) => { const { listFormCode, searchParams, isSubForm, gridDto: extGridDto } = props const { translate } = useLocalization() const isPwaMode = usePWA() const gridRef = useRef() const refListFormCode = useRef('') const widgetGroupRef = useRef(null) const { checkPermission } = usePermission() const [ganttDataSource, setGanttDataSource] = useState>() const [columnData, setColumnData] = useState() const [gridDto, setGridDto] = useState() const [widgetGroupHeight, setWidgetGroupHeight] = useState(0) const [scaleType, setScaleType] = useState('weeks') const [taskListWidth, setTaskListWidth] = useState(500) const layout = layoutTypes.gantt || 'gantt' const pendingStateRef = useRef>() const loadedStateKeyRef = useRef('') const { customSaveState, customLoadState, storageKey } = useListFormStateStoring({ listFormCode, storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey, filterPrefix: 'gantt', }) const ganttFilterRef = useRef({ instance: () => { const gantt = gridRef.current?.instance() return gantt ? getGanttTreeList(gantt) : undefined }, }) const { filterToolbarData, ...filterData } = useFilters({ gridDto, gridRef: ganttFilterRef, listFormCode, }) const ganttFilterToolbarData = filterToolbarData.filter((item) => ['saveActiveFilter', 'deleteFilter', 'clearFilter'].includes(item.name ?? ''), ) useEffect(() => { const initializeGantt = async () => { const response = await getList({ listFormCode }) setGridDto(response.data) } if (extGridDto === undefined) { initializeGantt() } else { setGridDto(extGridDto) } setScaleType(extGridDto?.gridOptions.ganttOptionDto?.scaleType || 'weeks') }, [listFormCode, extGridDto]) useEffect(() => { if (gridRef?.current) { const instance = gridRef?.current?.instance() if (instance) { instance.option('dataSource', undefined) } } if (refListFormCode.current !== listFormCode) { // Reset state if needed } }, [listFormCode]) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef }) const { getBandedColumns } = useListFormColumns({ gridDto, listFormCode, isSubForm, gridRef, }) 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]) useEffect(() => { if (!gridDto) return const cols = getBandedColumns() setColumnData(cols) const dataSource = createSelectDataSource( gridDto.gridOptions, listFormCode, searchParams, layout, undefined, ) setGanttDataSource(dataSource) }, [createSelectDataSource, getBandedColumns, gridDto, layout, listFormCode, searchParams]) useEffect(() => { refListFormCode.current = listFormCode }, [listFormCode]) // WidgetGroup yüksekliğini hesapla useEffect(() => { const calculateWidgetHeight = () => { if (widgetGroupRef.current) { const height = widgetGroupRef.current.offsetHeight setWidgetGroupHeight(height) } } calculateWidgetHeight() const resizeObserver = new ResizeObserver(calculateWidgetHeight) if (widgetGroupRef.current) { resizeObserver.observe(widgetGroupRef.current) } return () => { resizeObserver.disconnect() } }, [gridDto?.widgets]) const settingButtonClick = useCallback(() => { window.open( ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(':listFormCode', listFormCode), isPwaMode ? '_self' : '_blank', ) }, [isPwaMode, listFormCode]) const getSettingButtonOptions = useCallback( () => ({ icon: 'preferences', text: translate('::ListForms.ListForm.Manage'), hint: translate('::ListForms.ListForm.Manage'), stylingMode: 'icon', onClick: () => { settingButtonClick() }, }), [settingButtonClick, translate], ) const getRefreshButtonOptions = useCallback( () => ({ icon: 'refresh', text: translate('::ListForms.ListForm.Refresh'), stylingMode: 'icon', onClick: () => { gridRef.current?.instance()?.refresh() }, }), [translate], ) const getCurrentState = useCallback((gantt: GanttInstance) => { const treeList = getGanttTreeList(gantt) const state = (treeList?.state() ?? {}) as Record const visibleColumns = (treeList?.getVisibleColumns() ?? []) as Array> const headerCells = Array.from( (treeList?.element() as HTMLElement | undefined)?.querySelectorAll('.dx-header-row > td') ?? [], ) const widths = new Map( visibleColumns.map((column, index) => [ column.dataField, headerCells[index]?.getBoundingClientRect().width, ]), ) const taskListElement = (gantt.element() as HTMLElement).querySelector( '.dx-gantt-treelist-wrapper', ) return { ...state, columns: state.columns?.map((column: Record) => { const width = widths.get(column.dataField) return width ? { ...column, width: Math.round(width) } : column }), scaleType: gantt.option('scaleType'), showDependencies: gantt.option('showDependencies'), showResources: gantt.option('showResources'), taskListWidth: Math.round( taskListElement?.getBoundingClientRect().width ?? gantt.option('taskListWidth') ?? 500, ), } }, []) const applyState = useCallback((gantt: GanttInstance, state: Record) => { const treeList = getGanttTreeList(gantt) if (!treeList) return const { scaleType: savedScaleType, showDependencies, showResources, taskListWidth: savedTaskListWidth, treeListState, ...treeState } = state treeList.state(treeListState ?? treeState) if (savedScaleType) setScaleType(savedScaleType) if (savedTaskListWidth) setTaskListWidth(savedTaskListWidth) gantt.option({ ...(showDependencies !== undefined ? { showDependencies } : {}), ...(showResources !== undefined ? { showResources } : {}), ...(savedTaskListWidth ? { taskListWidth: savedTaskListWidth } : {}), }) }, []) const handleGanttContentReady = useCallback( (event: ContentReadyEvent) => { if (!pendingStateRef.current) return const state = pendingStateRef.current pendingStateRef.current = undefined requestAnimationFrame(() => requestAnimationFrame(() => applyState(event.component, state))) }, [applyState], ) const saveGanttState = useCallback(() => { const gantt = gridRef.current?.instance() if (gantt) customSaveState(getCurrentState(gantt)).catch(() => undefined) }, [customSaveState, getCurrentState]) const resetGanttState = useCallback(async () => { const gantt = gridRef.current?.instance() if (!gantt || !storageKey) return await postListFormCustomization({ listFormCode, customizationType: ListFormCustomizationTypeEnum.GridState, filterName: `gantt-${storageKey}`, customizationData: '', }) getGanttTreeList(gantt)?.state(null) setTaskListWidth(500) }, [listFormCode, storageKey]) useEffect(() => { if ( !gridDto?.gridOptions.stateStoringDto?.enabled || !ganttDataSource || !columnData || !storageKey ) return const key = `${listFormCode}-${storageKey}` if (loadedStateKeyRef.current === key) return loadedStateKeyRef.current = key customLoadState() .then((state) => { pendingStateRef.current = state ?? undefined const gantt = gridRef.current?.instance() if (gantt && state) handleGanttContentReady({ component: gantt } as ContentReadyEvent) }) .catch(() => { loadedStateKeyRef.current = '' }) }, [ columnData, customLoadState, ganttDataSource, gridDto, handleGanttContentReady, listFormCode, storageKey, ]) return ( <>
0 ? 'mt-2' : ''}>
{!isSubForm && ( )} {!gridDto && (
Loading gantt configuration...
)} {gridDto && !ganttDataSource && (
Loading data source...
)} {gridDto && columnData && ganttDataSource && ( <>
0 ? gridDto.gridOptions.height : gridDto.gridOptions.fullHeight ? `calc(100vh - ${170 + widgetGroupHeight}px)` : undefined } onContentReady={handleGanttContentReady} > {gridDto.gridOptions.ganttOptionDto?.allowTaskAdding && ( )} {gridDto.gridOptions.ganttOptionDto?.allowTaskDeleting && ( )} setScaleType(e.value), }} /> {ganttFilterToolbarData.map((item) => ( ))} {gridDto.gridOptions.stateStoringDto?.enabled && ( )} {gridDto.gridOptions.stateStoringDto?.enabled && ( )} {checkPermission('App.Listforms.Listform.Update') && ( )} {columnData .filter((col) => col.type != 'buttons') .map((col: any) => ( ))}
)}
) } export default GanttView