2025-12-01 14:45:23 +00:00
|
|
|
import Container from '@/components/shared/Container'
|
|
|
|
|
import { DX_CLASSNAMES } from '@/constants/app.constant'
|
|
|
|
|
import { GridDto } from '@/proxy/form/models'
|
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
import Gantt, {
|
|
|
|
|
Column,
|
2025-12-02 09:28:54 +00:00
|
|
|
ContextMenu,
|
2025-12-01 14:45:23 +00:00
|
|
|
Editing,
|
2025-12-02 09:28:54 +00:00
|
|
|
FilterRow,
|
2025-12-01 14:45:23 +00:00
|
|
|
GanttRef,
|
2025-12-02 09:28:54 +00:00
|
|
|
HeaderFilter,
|
2025-12-01 22:05:05 +00:00
|
|
|
Item,
|
2025-12-02 09:28:54 +00:00
|
|
|
Sorting,
|
2025-12-01 14:45:23 +00:00
|
|
|
Tasks,
|
2025-12-01 22:05:05 +00:00
|
|
|
Toolbar,
|
2025-12-01 14:45:23 +00:00
|
|
|
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'
|
2025-12-02 09:28:54 +00:00
|
|
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
|
|
|
import { usePWA } from '@/utils/hooks/usePWA'
|
|
|
|
|
import { 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'
|
2025-12-02 20:20:47 +00:00
|
|
|
import { usePermission } from '@/utils/hooks/usePermission'
|
2025-12-01 14:45:23 +00:00
|
|
|
|
|
|
|
|
interface GanttViewProps {
|
|
|
|
|
listFormCode: string
|
|
|
|
|
searchParams?: URLSearchParams
|
|
|
|
|
isSubForm?: boolean
|
|
|
|
|
level?: number
|
|
|
|
|
refreshData?: () => Promise<void>
|
|
|
|
|
gridDto?: GridDto
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const GanttView = (props: GanttViewProps) => {
|
|
|
|
|
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
|
|
|
|
|
const { translate } = useLocalization()
|
2025-12-02 09:28:54 +00:00
|
|
|
const isPwaMode = usePWA()
|
2025-12-01 14:45:23 +00:00
|
|
|
|
2025-12-02 09:28:54 +00:00
|
|
|
const gridRef = useRef<GanttRef>()
|
2025-12-01 14:45:23 +00:00
|
|
|
const refListFormCode = useRef('')
|
|
|
|
|
const widgetGroupRef = useRef<HTMLDivElement>(null)
|
2025-12-02 20:20:47 +00:00
|
|
|
const { checkPermission } = usePermission()
|
2025-12-01 14:45:23 +00:00
|
|
|
|
2025-12-02 09:28:54 +00:00
|
|
|
const [ganttDataSource, setGanttDataSource] = useState<CustomStore<any, any>>()
|
|
|
|
|
const [columnData, setColumnData] = useState<GridColumnData[]>()
|
2025-12-01 14:45:23 +00:00
|
|
|
const [gridDto, setGridDto] = useState<GridDto>()
|
|
|
|
|
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
|
2025-12-02 09:28:54 +00:00
|
|
|
const [scaleType, setScaleType] = useState<GanttScaleType>('weeks')
|
|
|
|
|
const layout = layoutTypes.gantt || 'gantt'
|
2025-12-01 14:45:23 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const initializeGantt = async () => {
|
|
|
|
|
const response = await getList({ listFormCode })
|
|
|
|
|
setGridDto(response.data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (extGridDto === undefined) {
|
|
|
|
|
initializeGantt()
|
|
|
|
|
} else {
|
|
|
|
|
setGridDto(extGridDto)
|
|
|
|
|
}
|
2025-12-02 10:20:22 +00:00
|
|
|
|
|
|
|
|
setScaleType(extGridDto?.gridOptions.ganttOptionDto?.scaleType || 'weeks')
|
2025-12-01 14:45:23 +00:00
|
|
|
}, [listFormCode, extGridDto])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-12-02 09:28:54 +00:00
|
|
|
if (gridRef?.current) {
|
|
|
|
|
const instance = gridRef?.current?.instance()
|
2025-12-01 14:45:23 +00:00
|
|
|
if (instance) {
|
|
|
|
|
instance.option('dataSource', undefined)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (refListFormCode.current !== listFormCode) {
|
|
|
|
|
// Reset state if needed
|
|
|
|
|
}
|
|
|
|
|
}, [listFormCode])
|
|
|
|
|
|
2025-12-02 09:28:54 +00:00
|
|
|
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
|
|
|
|
|
const { getBandedColumns } = useListFormColumns({
|
|
|
|
|
gridDto,
|
|
|
|
|
listFormCode,
|
|
|
|
|
isSubForm,
|
|
|
|
|
gridRef,
|
|
|
|
|
})
|
|
|
|
|
|
2025-12-01 14:45:23 +00:00
|
|
|
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
|
|
|
|
|
|
2025-12-02 09:28:54 +00:00
|
|
|
const cols = getBandedColumns()
|
|
|
|
|
setColumnData(cols)
|
|
|
|
|
|
|
|
|
|
const dataSource = createSelectDataSource(
|
2025-12-01 14:45:23 +00:00
|
|
|
gridDto.gridOptions,
|
|
|
|
|
listFormCode,
|
|
|
|
|
searchParams,
|
|
|
|
|
layout,
|
|
|
|
|
undefined,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
setGanttDataSource(dataSource)
|
2025-12-01 22:05:05 +00:00
|
|
|
}, [gridDto, searchParams, createSelectDataSource])
|
2025-12-01 14:45:23 +00:00
|
|
|
|
|
|
|
|
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])
|
|
|
|
|
|
2025-12-02 09:28:54 +00:00
|
|
|
const settingButtonClick = useCallback(() => {
|
|
|
|
|
window.open(
|
|
|
|
|
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(':listFormCode', listFormCode),
|
|
|
|
|
isPwaMode ? '_self' : '_blank',
|
|
|
|
|
)
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const getSettingButtonOptions = useCallback(
|
|
|
|
|
() => ({
|
|
|
|
|
icon: 'preferences',
|
|
|
|
|
stylingMode: 'icon',
|
|
|
|
|
onClick: () => {
|
|
|
|
|
settingButtonClick()
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
[settingButtonClick],
|
|
|
|
|
)
|
2025-12-01 14:45:23 +00:00
|
|
|
|
2025-12-02 09:28:54 +00:00
|
|
|
const getRefreshButtonOptions = useCallback(
|
|
|
|
|
() => ({
|
|
|
|
|
icon: 'refresh',
|
|
|
|
|
text: translate('::ListForms.ListForm.Refresh'),
|
|
|
|
|
stylingMode: 'icon',
|
|
|
|
|
onClick: () => {
|
|
|
|
|
gridRef.current?.instance()?.refresh()
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
[settingButtonClick],
|
|
|
|
|
)
|
2025-12-01 14:45:23 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div ref={widgetGroupRef}>
|
|
|
|
|
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Container className={DX_CLASSNAMES}>
|
|
|
|
|
{!isSubForm && (
|
|
|
|
|
<Helmet
|
|
|
|
|
titleTemplate="%s | Erp Platform"
|
|
|
|
|
title={translate('::' + gridDto?.gridOptions.title)}
|
|
|
|
|
defaultTitle="Erp Platform"
|
2025-12-01 22:05:05 +00:00
|
|
|
>
|
|
|
|
|
<link rel="stylesheet" href="/css/gantt/dx-gantt.min.css" />
|
|
|
|
|
</Helmet>
|
2025-12-01 14:45:23 +00:00
|
|
|
)}
|
2025-12-02 09:28:54 +00:00
|
|
|
{!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 && (
|
2025-12-01 14:45:23 +00:00
|
|
|
<>
|
|
|
|
|
<div className="p-1">
|
|
|
|
|
<Gantt
|
2025-12-02 09:28:54 +00:00
|
|
|
ref={gridRef as any}
|
2025-12-01 14:45:23 +00:00
|
|
|
key={`Gantt-${listFormCode}-${ganttDataSource ? 'loaded' : 'loading'}`}
|
|
|
|
|
id={'Gantt-' + listFormCode}
|
|
|
|
|
taskListWidth={500}
|
2025-12-02 09:28:54 +00:00
|
|
|
scaleType={scaleType}
|
2025-12-01 22:05:05 +00:00
|
|
|
rootValue={
|
2025-12-02 09:56:11 +00:00
|
|
|
gridDto.gridOptions.ganttOptionDto?.rootValue === '' ||
|
|
|
|
|
gridDto.gridOptions.ganttOptionDto?.rootValue === undefined
|
2025-12-01 22:05:05 +00:00
|
|
|
? null
|
2025-12-02 09:56:11 +00:00
|
|
|
: gridDto.gridOptions.ganttOptionDto?.rootValue
|
2025-12-01 22:05:05 +00:00
|
|
|
}
|
2025-12-01 14:45:23 +00:00
|
|
|
height={
|
|
|
|
|
gridDto.gridOptions.height > 0
|
|
|
|
|
? gridDto.gridOptions.height
|
|
|
|
|
: gridDto.gridOptions.fullHeight
|
|
|
|
|
? `calc(100vh - ${170 + widgetGroupHeight}px)`
|
2025-12-02 09:28:54 +00:00
|
|
|
: undefined
|
2025-12-01 14:45:23 +00:00
|
|
|
}
|
|
|
|
|
>
|
2025-12-01 22:05:05 +00:00
|
|
|
<Tasks
|
|
|
|
|
dataSource={ganttDataSource}
|
2025-12-02 09:56:11 +00:00
|
|
|
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}
|
2025-12-01 22:05:05 +00:00
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Toolbar>
|
2025-12-02 20:20:47 +00:00
|
|
|
<Item name="undo" />
|
|
|
|
|
<Item name="redo" />
|
|
|
|
|
<Item name="separator" />
|
2025-12-01 22:05:05 +00:00
|
|
|
<Item name="collapseAll" />
|
|
|
|
|
<Item name="expandAll" />
|
2025-12-02 20:20:47 +00:00
|
|
|
{gridDto.gridOptions.editingOptionDto?.allowAdding && <Item name="addTask" />}
|
|
|
|
|
{gridDto.gridOptions.editingOptionDto?.allowDeleting && (
|
|
|
|
|
<Item name="deleteTask" />
|
|
|
|
|
)}
|
2025-12-01 22:05:05 +00:00
|
|
|
<Item name="separator" />
|
|
|
|
|
<Item name="zoomIn" />
|
|
|
|
|
<Item name="zoomOut" />
|
2025-12-02 09:28:54 +00:00
|
|
|
<Item name="separator" />
|
|
|
|
|
<Item
|
|
|
|
|
location="after"
|
|
|
|
|
widget="dxSelectBox"
|
|
|
|
|
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" options={getRefreshButtonOptions()} />
|
2025-12-02 20:20:47 +00:00
|
|
|
{checkPermission(gridDto?.gridOptions.permissionDto.u) && (
|
|
|
|
|
<Item location="after" widget="dxButton" options={getSettingButtonOptions()} />
|
|
|
|
|
)}
|
2025-12-01 22:05:05 +00:00
|
|
|
</Toolbar>
|
2025-12-01 14:45:23 +00:00
|
|
|
|
2025-12-02 19:58:31 +00:00
|
|
|
<Editing
|
|
|
|
|
enabled={gridDto.gridOptions.editingOptionDto?.allowUpdating}
|
|
|
|
|
allowTaskAdding={gridDto.gridOptions.editingOptionDto?.allowAdding}
|
|
|
|
|
allowTaskUpdating={gridDto.gridOptions.editingOptionDto?.allowUpdating}
|
|
|
|
|
allowTaskDeleting={gridDto.gridOptions.editingOptionDto?.allowDeleting}
|
|
|
|
|
allowDependencyAdding={false}
|
|
|
|
|
allowDependencyDeleting={false}
|
|
|
|
|
allowResourceAdding={false}
|
|
|
|
|
allowResourceDeleting={false}
|
|
|
|
|
/>
|
2025-12-02 20:20:47 +00:00
|
|
|
|
2025-12-02 09:28:54 +00:00
|
|
|
<FilterRow visible={gridDto.gridOptions.filterRowDto?.visible}></FilterRow>
|
|
|
|
|
<HeaderFilter visible={gridDto.gridOptions.headerFilterDto.visible}></HeaderFilter>
|
|
|
|
|
<Sorting mode={gridDto.gridOptions?.sortMode}></Sorting>
|
2025-12-01 14:45:23 +00:00
|
|
|
<Validation autoUpdateParentTasks={true} />
|
2025-12-02 09:28:54 +00:00
|
|
|
<ContextMenu enabled={false} />
|
2025-12-01 14:45:23 +00:00
|
|
|
|
2025-12-02 09:28:54 +00:00
|
|
|
{columnData
|
|
|
|
|
.filter((col) => col.type != 'buttons')
|
|
|
|
|
.map((col: any) => (
|
|
|
|
|
<Column key={col.dataField} {...col} />
|
|
|
|
|
))}
|
2025-12-01 14:45:23 +00:00
|
|
|
</Gantt>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Container>
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default GanttView
|