sozsoft-platform/ui/src/views/list/GanttView.tsx
2026-08-13 11:41:48 +03:00

487 lines
18 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 { 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 PageTitle from '@/components/shared/PageTitle'
import { useStylesheet } from '@/utils/hooks/useStylesheet'
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'
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 {
useListFormCustomSources,
useListFormGridDto,
useWidgetGroupHeight,
} from './shared/hooks'
import GridFilterDialogs from './GridFilterDialogs'
type GanttInstance = ReturnType<GanttRef['instance']>
type GanttTreeListInstance = {
element: () => HTMLElement
getVisibleColumns: () => Array<Record<string, any>>
state: {
(): Record<string, any>
(value: Record<string, any> | 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<void>
gridDto?: GridDto
}
const GanttView = (props: GanttViewProps) => {
const { listFormCode, searchParams, isSubForm, gridDto: extGridDto } = props
const { translate } = useLocalization()
const isPwaMode = usePWA()
// Eskiden Helmet'in <link> çocuğuydu; koşul aynen korunuyor.
useStylesheet(isSubForm ? null : '/css/gantt/dx-gantt.min.css', 'dx-gantt-css')
const gridRef = useRef<GanttRef | undefined>(undefined)
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)
const [scaleType, setScaleType] = useState<GanttScaleType>('weeks')
const [taskListWidth, setTaskListWidth] = useState(500)
const layout = layoutTypes.gantt || 'gantt'
const pendingStateRef = useRef<Record<string, any> | undefined>(undefined)
const loadedStateKeyRef = useRef('')
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
useListFormCustomSources(gridDto)
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 ?? ''),
)
useEffect(() => {
setScaleType(gridDto?.gridOptions.ganttOptionDto?.scaleType || 'weeks')
}, [gridDto])
// listFormCode değişiminde eski veri kaynağını temizle.
useEffect(() => {
gridRef.current?.instance()?.option('dataSource', undefined)
}, [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)
}, [createSelectDataSource, getBandedColumns, gridDto, layout, listFormCode, searchParams])
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<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(
taskListElement?.getBoundingClientRect().width ?? gantt.option('taskListWidth') ?? 500,
),
}
}, [])
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,
])
return (
<>
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
</div>
<Container className={DX_CLASSNAMES}>
{!isSubForm && <PageTitle title={translate('::' + gridDto?.gridOptions.title)} />}
{!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}
className="list-view-toolbar-host"
taskListWidth={taskListWidth}
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
}
onContentReady={handleGanttContentReady}
>
<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>
<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" />
)}
{gridDto.gridOptions.ganttOptionDto?.allowTaskDeleting && (
<Item name="deleteTask" locateInMenu="auto" />
)}
<Item name="separator" locateInMenu="auto" />
<Item name="zoomIn" locateInMenu="auto" />
<Item name="zoomOut" locateInMenu="auto" />
<Item name="separator" locateInMenu="auto" />
<Item
location="after"
widget="dxSelectBox"
locateInMenu="auto"
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),
}}
/>
<Item
location="after"
widget="dxButton"
locateInMenu="auto"
showText="always"
options={getRefreshButtonOptions()}
/>
{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') && (
<Item
location="after"
widget="dxButton"
locateInMenu="auto"
showText="always"
options={getSettingButtonOptions()}
/>
)}
</Toolbar>
<Editing
enabled={gridDto.gridOptions.ganttOptionDto?.allowEditing}
allowTaskAdding={gridDto.gridOptions.ganttOptionDto?.allowTaskAdding}
allowTaskUpdating={gridDto.gridOptions.ganttOptionDto?.allowTaskUpdating}
allowTaskDeleting={gridDto.gridOptions.ganttOptionDto?.allowTaskDeleting}
allowTaskResourceUpdating={
gridDto.gridOptions.ganttOptionDto?.allowTaskResourceUpdating
}
allowDependencyAdding={gridDto.gridOptions.ganttOptionDto?.allowDependencyAdding}
allowDependencyDeleting={
gridDto.gridOptions.ganttOptionDto?.allowDependencyDeleting
}
allowResourceAdding={gridDto.gridOptions.ganttOptionDto?.allowResourceAdding}
allowResourceDeleting={gridDto.gridOptions.ganttOptionDto?.allowResourceDeleting}
allowResourceUpdating={gridDto.gridOptions.ganttOptionDto?.allowResourceUpdating}
/>
<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>
<GridFilterDialogs
gridRef={ganttFilterRef}
listFormCode={listFormCode}
{...filterData}
/>
</div>
</>
)}
</Container>
</>
)
}
export default GanttView