sozsoft-platform/ui/src/views/list/GanttView.tsx

492 lines
18 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import Container from '@/components/shared/Container'
import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant'
2026-07-29 07:57:49 +00:00
import { GridDto, ListFormCustomizationTypeEnum } from '@/proxy/form/models'
2026-02-24 20:44:16 +00:00
import { useLocalization } from '@/utils/hooks/useLocalization'
import Gantt, {
Column,
ContextMenu,
Editing,
FilterRow,
2026-07-13 08:39:13 +00:00
type GanttRef,
2026-02-24 20:44:16 +00:00
HeaderFilter,
Item,
Sorting,
Tasks,
Toolbar,
Validation,
} from 'devextreme-react/gantt'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
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'
2026-07-29 07:57:49 +00:00
import type { ContentReadyEvent, GanttScaleType } from 'devextreme/ui/gantt'
2026-02-24 20:44:16 +00:00
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'
2026-07-29 07:57:49 +00:00
import { useListFormStateStoring } from './useListFormStateStoring'
import TreeList from 'devextreme/ui/tree_list'
import { postListFormCustomization } from '@/services/list-form-customization.service'
import { useFilters } from './useFilters'
import {
useListFormCustomSources,
useListFormGridDto,
useWidgetGroupHeight,
} from './shared/hooks'
2026-07-29 07:57:49 +00:00
import GridFilterDialogs from './GridFilterDialogs'
type GanttInstance = ReturnType<GanttRef['instance']>
2026-08-05 20:51:43 +00:00
type GanttTreeListInstance = {
element: () => HTMLElement
getVisibleColumns: () => Array<Record<string, any>>
state: {
(): Record<string, any>
(value: Record<string, any> | null): void
}
}
2026-07-29 07:57:49 +00:00
const getGanttTreeList = (gantt: GanttInstance) => {
const element = (gantt.element() as HTMLElement).querySelector('.dx-treelist')
2026-08-05 20:51:43 +00:00
return element
? (TreeList.getInstance(element as HTMLElement) as unknown as GanttTreeListInstance)
: undefined
2026-07-29 07:57:49 +00:00
}
2026-02-24 20:44:16 +00:00
interface GanttViewProps {
listFormCode: string
searchParams?: URLSearchParams
isSubForm?: boolean
level?: number
refreshData?: () => Promise<void>
gridDto?: GridDto
}
const GanttView = (props: GanttViewProps) => {
2026-07-06 09:44:26 +00:00
const { listFormCode, searchParams, isSubForm, gridDto: extGridDto } = props
2026-02-24 20:44:16 +00:00
const { translate } = useLocalization()
const isPwaMode = usePWA()
const gridRef = useRef<GanttRef>()
const widgetGroupRef = useRef<HTMLDivElement>(null)
const { checkPermission } = usePermission()
const [ganttDataSource, setGanttDataSource] = useState<CustomStore<any, any>>()
const [columnData, setColumnData] = useState<GridColumnData[]>()
const gridDto = useListFormGridDto(listFormCode, extGridDto)
2026-02-24 20:44:16 +00:00
const [scaleType, setScaleType] = useState<GanttScaleType>('weeks')
2026-07-29 07:57:49 +00:00
const [taskListWidth, setTaskListWidth] = useState(500)
2026-02-24 20:44:16 +00:00
const layout = layoutTypes.gantt || 'gantt'
2026-07-29 07:57:49 +00:00
const pendingStateRef = useRef<Record<string, any>>()
const loadedStateKeyRef = useRef('')
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
useListFormCustomSources(gridDto)
2026-07-29 07:57:49 +00:00
const { customSaveState, customLoadState, storageKey } = useListFormStateStoring({
listFormCode,
storageKey: gridDto?.gridOptions.stateStoringDto?.storageKey,
filterPrefix: 'gantt',
})
const ganttFilterRef = useRef<any>({
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 ?? ''),
)
2026-02-24 20:44:16 +00:00
useEffect(() => {
setScaleType(gridDto?.gridOptions.ganttOptionDto?.scaleType || 'weeks')
}, [gridDto])
2026-02-24 20:44:16 +00:00
// listFormCode değişiminde eski veri kaynağını temizle.
2026-02-24 20:44:16 +00:00
useEffect(() => {
gridRef.current?.instance()?.option('dataSource', undefined)
2026-02-24 20:44:16 +00:00
}, [listFormCode])
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
const { getBandedColumns } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
gridRef,
})
useEffect(() => {
if (!gridDto) return
const cols = getBandedColumns()
setColumnData(cols)
const dataSource = createSelectDataSource(
gridDto.gridOptions,
listFormCode,
searchParams,
layout,
undefined,
)
setGanttDataSource(dataSource)
2026-08-05 20:51:43 +00:00
}, [createSelectDataSource, getBandedColumns, gridDto, layout, listFormCode, searchParams])
2026-02-24 20:44:16 +00:00
const settingButtonClick = useCallback(() => {
window.open(
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(':listFormCode', listFormCode),
isPwaMode ? '_self' : '_blank',
)
2026-08-05 20:51:43 +00:00
}, [isPwaMode, listFormCode])
2026-02-24 20:44:16 +00:00
const getSettingButtonOptions = useCallback(
() => ({
icon: 'preferences',
2026-07-29 07:57:49 +00:00
text: translate('::ListForms.ListForm.Manage'),
hint: translate('::ListForms.ListForm.Manage'),
2026-02-24 20:44:16 +00:00
stylingMode: 'icon',
onClick: () => {
settingButtonClick()
},
}),
2026-07-29 07:57:49 +00:00
[settingButtonClick, translate],
2026-02-24 20:44:16 +00:00
)
const getRefreshButtonOptions = useCallback(
() => ({
icon: 'refresh',
text: translate('::ListForms.ListForm.Refresh'),
stylingMode: 'icon',
onClick: () => {
gridRef.current?.instance()?.refresh()
},
}),
2026-08-05 20:51:43 +00:00
[translate],
2026-02-24 20:44:16 +00:00
)
2026-07-29 07:57:49 +00:00
const getCurrentState = useCallback((gantt: GanttInstance) => {
const treeList = getGanttTreeList(gantt)
const state = (treeList?.state() ?? {}) as Record<string, any>
const visibleColumns = (treeList?.getVisibleColumns() ?? []) as Array<Record<string, any>>
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<string, any>) => {
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(
2026-08-05 20:51:43 +00:00
taskListElement?.getBoundingClientRect().width ?? gantt.option('taskListWidth') ?? 500,
2026-07-29 07:57:49 +00:00
),
}
}, [])
const applyState = useCallback((gantt: GanttInstance, state: Record<string, any>) => {
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,
])
2026-02-24 20:44:16 +00:00
return (
<>
2026-06-18 14:03:22 +00:00
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
2026-02-24 20:44:16 +00:00
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
</div>
<Container className={DX_CLASSNAMES}>
{!isSubForm && (
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + gridDto?.gridOptions.title)}
defaultTitle={APP_NAME}
>
<link rel="stylesheet" href="/css/gantt/dx-gantt.min.css" />
</Helmet>
)}
{!gridDto && (
<div className="p-4">
<Loading loading>Loading gantt configuration...</Loading>
</div>
)}
{gridDto && !ganttDataSource && (
<div className="p-4">
<Loading loading>Loading data source...</Loading>
</div>
)}
{gridDto && columnData && ganttDataSource && (
<>
<div className="p-1">
<Gantt
ref={gridRef as any}
key={`Gantt-${listFormCode}-${ganttDataSource ? 'loaded' : 'loading'}`}
id={'Gantt-' + listFormCode}
2026-07-30 14:37:15 +00:00
className="list-view-toolbar-host"
2026-07-29 07:57:49 +00:00
taskListWidth={taskListWidth}
2026-02-24 20:44:16 +00:00
scaleType={scaleType}
rootValue={
gridDto.gridOptions.ganttOptionDto?.rootValue === '' ||
gridDto.gridOptions.ganttOptionDto?.rootValue === undefined
? null
: gridDto.gridOptions.ganttOptionDto?.rootValue
}
height={
gridDto.gridOptions.height > 0
? gridDto.gridOptions.height
: gridDto.gridOptions.fullHeight
? `calc(100vh - ${170 + widgetGroupHeight}px)`
: undefined
}
2026-07-29 07:57:49 +00:00
onContentReady={handleGanttContentReady}
2026-02-24 20:44:16 +00:00
>
<Tasks
dataSource={ganttDataSource}
keyExpr={gridDto.gridOptions.ganttOptionDto?.keyExpr}
parentIdExpr={gridDto.gridOptions.ganttOptionDto?.parentIdExpr}
titleExpr={gridDto.gridOptions.ganttOptionDto?.titleExpr}
startExpr={gridDto.gridOptions.ganttOptionDto?.startExpr}
endExpr={gridDto.gridOptions.ganttOptionDto?.endExpr}
progressExpr={gridDto.gridOptions.ganttOptionDto?.progressExpr}
/>
<Toolbar>
2026-06-10 07:53:32 +00:00
<Item name="undo" locateInMenu="auto" />
<Item name="redo" locateInMenu="auto" />
<Item name="separator" locateInMenu="auto" />
<Item name="collapseAll" locateInMenu="auto" />
<Item name="expandAll" locateInMenu="auto" />
{gridDto.gridOptions.ganttOptionDto?.allowTaskAdding && (
<Item name="addTask" locateInMenu="auto" />
)}
2026-02-24 20:44:16 +00:00
{gridDto.gridOptions.ganttOptionDto?.allowTaskDeleting && (
2026-06-10 07:53:32 +00:00
<Item name="deleteTask" locateInMenu="auto" />
2026-02-24 20:44:16 +00:00
)}
2026-06-10 07:53:32 +00:00
<Item name="separator" locateInMenu="auto" />
<Item name="zoomIn" locateInMenu="auto" />
<Item name="zoomOut" locateInMenu="auto" />
<Item name="separator" locateInMenu="auto" />
2026-02-24 20:44:16 +00:00
<Item
location="after"
widget="dxSelectBox"
2026-06-10 07:53:32 +00:00
locateInMenu="auto"
2026-02-24 20:44:16 +00:00
options={{
width: 150,
items: [
{ value: 'auto', text: translate('::Auto') },
{ value: 'minutes', text: translate('::Minutes') },
{ value: 'hours', text: translate('::Hours') },
{ value: 'days', text: translate('::Days') },
{ value: 'weeks', text: translate('::Weeks') },
{ value: 'months', text: translate('::Months') },
{ value: 'quarters', text: translate('::Quarters') },
{ value: 'years', text: translate('::Years') },
],
displayExpr: 'text',
valueExpr: 'value',
value: scaleType,
onValueChanged: (e: any) => setScaleType(e.value),
}}
/>
2026-06-10 07:53:32 +00:00
<Item
location="after"
widget="dxButton"
locateInMenu="auto"
showText="always"
options={getRefreshButtonOptions()}
/>
2026-07-29 07:57:49 +00:00
{ganttFilterToolbarData.map((item) => (
<Item
key={item.name}
name={item.name}
location="after"
widget="dxButton"
locateInMenu="never"
showText="always"
options={item.options}
/>
))}
{gridDto.gridOptions.stateStoringDto?.enabled && (
<Item
location="after"
widget="dxButton"
locateInMenu="auto"
showText="always"
options={{
icon: 'save',
text: translate('::ListForms.ListForm.SaveGridState'),
hint: translate('::ListForms.ListForm.SaveGridState'),
onClick: saveGanttState,
}}
/>
)}
{gridDto.gridOptions.stateStoringDto?.enabled && (
<Item
location="after"
widget="dxButton"
locateInMenu="auto"
showText="always"
options={{
icon: 'revert',
text: translate('::ListForms.ListForm.ResetGridState'),
hint: translate('::ListForms.ListForm.ResetGridState'),
onClick: resetGanttState,
}}
/>
)}
{checkPermission('App.Listforms.Listform.Update') && (
2026-06-10 07:53:32 +00:00
<Item
location="after"
widget="dxButton"
locateInMenu="auto"
showText="always"
options={getSettingButtonOptions()}
/>
2026-02-24 20:44:16 +00:00
)}
</Toolbar>
2026-07-29 07:57:49 +00:00
2026-02-24 20:44:16 +00:00
<Editing
enabled={gridDto.gridOptions.ganttOptionDto?.allowEditing}
allowTaskAdding={gridDto.gridOptions.ganttOptionDto?.allowTaskAdding}
allowTaskUpdating={gridDto.gridOptions.ganttOptionDto?.allowTaskUpdating}
allowTaskDeleting={gridDto.gridOptions.ganttOptionDto?.allowTaskDeleting}
2026-07-29 07:57:49 +00:00
allowTaskResourceUpdating={
gridDto.gridOptions.ganttOptionDto?.allowTaskResourceUpdating
}
2026-02-24 20:44:16 +00:00
allowDependencyAdding={gridDto.gridOptions.ganttOptionDto?.allowDependencyAdding}
allowDependencyDeleting={
gridDto.gridOptions.ganttOptionDto?.allowDependencyDeleting
}
allowResourceAdding={gridDto.gridOptions.ganttOptionDto?.allowResourceAdding}
allowResourceDeleting={gridDto.gridOptions.ganttOptionDto?.allowResourceDeleting}
2026-07-29 07:57:49 +00:00
allowResourceUpdating={gridDto.gridOptions.ganttOptionDto?.allowResourceUpdating}
2026-02-24 20:44:16 +00:00
/>
2026-08-05 20:51:43 +00:00
2026-02-24 20:44:16 +00:00
<FilterRow visible={gridDto.gridOptions.filterRowDto?.visible}></FilterRow>
<HeaderFilter visible={gridDto.gridOptions.headerFilterDto.visible}></HeaderFilter>
<Sorting mode={gridDto.gridOptions?.sortMode}></Sorting>
<Validation autoUpdateParentTasks={true} />
<ContextMenu enabled={false} />
{columnData
.filter((col) => col.type != 'buttons')
.map((col: any) => (
<Column key={col.dataField} {...col} />
))}
</Gantt>
2026-07-29 07:57:49 +00:00
<GridFilterDialogs
gridRef={ganttFilterRef}
listFormCode={listFormCode}
{...filterData}
/>
2026-02-24 20:44:16 +00:00
</div>
</>
)}
</Container>
</>
)
}
export default GanttView