GanttView düzenlemeleri

This commit is contained in:
Sedat ÖZTÜRK 2026-07-29 10:57:49 +03:00
parent 55793e3db0
commit 9988cd11b0
10 changed files with 335 additions and 65 deletions

View file

@ -76,6 +76,10 @@ public class GanttOptionDto
/// </summary>
public bool AllowTaskUpdating { get; set; } = false;
/// <summary>
/// Task kaynaklarının güncellenmesine izin verilsin mi
/// </summary>
public bool AllowTaskResourceUpdating { get; set; } = false;
/// <summary>
/// Task silmeye izin verilsin mi
/// </summary>
public bool AllowTaskDeleting { get; set; } = false;
@ -94,6 +98,11 @@ public class GanttOptionDto
/// </summary>
public bool AllowResourceAdding { get; set; } = false;
/// <summary>
/// Kaynak güncellemeye izin verilsin mi
/// </summary>
public bool AllowResourceUpdating { get; set; } = false;
/// <summary>
/// Kaynaklar için kullanılan field adı (örn: "resources")
/// </summary>

View file

@ -5562,6 +5562,12 @@
"en": "Allow Task Updating",
"tr": "Görev Güncelleme İzni"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.AllowTaskResourceUpdating",
"en": "Allow Task Resource Updating",
"tr": "Görev Kaynaklarını Güncelleme İzni"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.AllowDependencyAdding",
@ -5580,6 +5586,12 @@
"en": "Allow Resource Adding",
"tr": "Kaynak Ekleme İzni"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.AllowResourceUpdating",
"en": "Allow Resource Updating",
"tr": "Kaynak Güncelleme İzni"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.AllowResourceDeleting",

View file

@ -289,7 +289,8 @@ div.dialog-after-open > div.dialog-content.maximized {
}
.dx-datagrid-header-panel > .dx-toolbar .dx-toolbar-after .dx-button-text,
.dx-treelist-header-panel > .dx-toolbar .dx-toolbar-after .dx-button-text {
.dx-treelist-header-panel > .dx-toolbar .dx-toolbar-after .dx-button-text,
.dx-gantt-toolbar-wrapper .dx-button-text {
text-transform: none !important;
}

View file

@ -420,10 +420,12 @@ export interface GanttOptionDto {
allowEditing?: boolean
allowTaskAdding?: boolean
allowTaskUpdating?: boolean
allowTaskResourceUpdating?: boolean
allowTaskDeleting?: boolean
allowDependencyAdding?: boolean
allowDependencyDeleting?: boolean
allowResourceAdding?: boolean
allowResourceUpdating?: boolean
allowResourceDeleting?: boolean
}

View file

@ -361,21 +361,6 @@ function FormTabGantt(props: FormEditProps) {
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.AllowTaskAdding')}
invalid={
errors.ganttOptionDto?.allowTaskAdding &&
touched.ganttOptionDto?.allowTaskAdding
}
errorMessage={errors.ganttOptionDto?.allowTaskAdding}
>
<Field
name="ganttOptionDto.allowTaskAdding"
placeholder={translate('::ListForms.ListFormEdit.AllowTaskAdding')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.AllowTaskUpdating')}
invalid={
@ -391,22 +376,6 @@ function FormTabGantt(props: FormEditProps) {
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.AllowTaskDeleting')}
invalid={
errors.ganttOptionDto?.allowTaskDeleting &&
touched.ganttOptionDto?.allowTaskDeleting
}
errorMessage={errors.ganttOptionDto?.allowTaskDeleting}
>
<Field
name="ganttOptionDto.allowTaskDeleting"
placeholder={translate('::ListForms.ListFormEdit.AllowTaskDeleting')}
component={Checkbox}
/>
</FormItem>
</div>
<div>
<FormItem
label={translate('::ListForms.ListFormEdit.AllowDependencyAdding')}
invalid={
@ -422,21 +391,6 @@ function FormTabGantt(props: FormEditProps) {
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.AllowDependencyDeleting')}
invalid={
errors.ganttOptionDto?.allowDependencyDeleting &&
touched.ganttOptionDto?.allowDependencyDeleting
}
errorMessage={errors.ganttOptionDto?.allowDependencyDeleting}
>
<Field
name="ganttOptionDto.allowDependencyDeleting"
placeholder={translate('::ListForms.ListFormEdit.AllowDependencyDeleting')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.AllowResourceAdding')}
invalid={
@ -467,11 +421,96 @@ function FormTabGantt(props: FormEditProps) {
/>
</FormItem>
</div>
<div>
<FormItem
label={translate('::ListForms.ListFormEdit.AllowTaskAdding')}
invalid={
errors.ganttOptionDto?.allowTaskAdding &&
touched.ganttOptionDto?.allowTaskAdding
}
errorMessage={errors.ganttOptionDto?.allowTaskAdding}
>
<Field
name="ganttOptionDto.allowTaskAdding"
placeholder={translate('::ListForms.ListFormEdit.AllowTaskAdding')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.AllowTaskDeleting')}
invalid={
errors.ganttOptionDto?.allowTaskDeleting &&
touched.ganttOptionDto?.allowTaskDeleting
}
errorMessage={errors.ganttOptionDto?.allowTaskDeleting}
>
<Field
name="ganttOptionDto.allowTaskDeleting"
placeholder={translate('::ListForms.ListFormEdit.AllowTaskDeleting')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.AllowDependencyDeleting')}
invalid={
errors.ganttOptionDto?.allowDependencyDeleting &&
touched.ganttOptionDto?.allowDependencyDeleting
}
errorMessage={errors.ganttOptionDto?.allowDependencyDeleting}
>
<Field
name="ganttOptionDto.allowDependencyDeleting"
placeholder={translate('::ListForms.ListFormEdit.AllowDependencyDeleting')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.AllowResourceUpdating')}
invalid={
errors.ganttOptionDto?.allowResourceUpdating &&
touched.ganttOptionDto?.allowResourceUpdating
}
errorMessage={errors.ganttOptionDto?.allowResourceUpdating}
>
<Field
name="ganttOptionDto.allowResourceUpdating"
placeholder={translate('::ListForms.ListFormEdit.AllowResourceUpdating')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.AllowTaskResourceUpdating')}
invalid={
errors.ganttOptionDto?.allowTaskResourceUpdating &&
touched.ganttOptionDto?.allowTaskResourceUpdating
}
errorMessage={errors.ganttOptionDto?.allowTaskResourceUpdating}
>
<Field
name="ganttOptionDto.allowTaskResourceUpdating"
placeholder={translate(
'::ListForms.ListFormEdit.AllowTaskResourceUpdating',
)}
component={Checkbox}
/>
</FormItem>
</div>
</div>
</Card>
</div>
<Button block size="sm" variant="solid" loading={isSubmitting} type="submit" className="my-2">
<Button
block
size="sm"
variant="solid"
loading={isSubmitting}
type="submit"
className="my-2"
>
{isSubmitting ? translate('::Saving') : translate('::Save')}
</Button>
</FormContainer>

View file

@ -93,10 +93,12 @@ const initialValues: ListFormWizardDto = {
allowEditing: false,
allowTaskAdding: false,
allowTaskUpdating: false,
allowTaskResourceUpdating: false,
allowTaskDeleting: false,
allowDependencyAdding: false,
allowDependencyDeleting: false,
allowResourceAdding: false,
allowResourceUpdating: false,
allowResourceDeleting: false,
},
schedulerOptionDto: {

View file

@ -41,7 +41,9 @@ const WizardStep9 = ({ selectCommandColumns, translate, onBack, onNext }: Wizard
['allowDependencyAdding', '::ListForms.ListFormEdit.AllowDependencyAdding'],
['allowDependencyDeleting', '::ListForms.ListFormEdit.AllowDependencyDeleting'],
['allowResourceAdding', '::ListForms.ListFormEdit.AllowResourceAdding'],
['allowResourceUpdating', '::ListForms.ListFormEdit.AllowResourceUpdating'],
['allowResourceDeleting', '::ListForms.ListFormEdit.AllowResourceDeleting'],
['allowTaskResourceUpdating', '::ListForms.ListFormEdit.AllowTaskResourceUpdating'],
] as const
return (

View file

@ -382,15 +382,6 @@ const GANTT_COLUMNS: ColumnDefinition[] = [
defaultValue: 'NEWID()',
description: 'Gantt task primary key',
},
{
id: '__GanttParentId',
columnName: 'ParentId',
dataType: 'uniqueidentifier',
maxLength: '',
isNullable: true,
defaultValue: '',
description: 'Parent Gantt task ID',
},
{
id: '__GanttTitle',
columnName: 'Title',
@ -400,6 +391,15 @@ const GANTT_COLUMNS: ColumnDefinition[] = [
defaultValue: '',
description: 'Gantt task title',
},
{
id: '__GanttParentId',
columnName: 'ParentId',
dataType: 'uniqueidentifier',
maxLength: '',
isNullable: true,
defaultValue: '',
description: 'Parent Gantt task ID',
},
{
id: '__GanttStartDate',
columnName: 'StartDate',

View file

@ -1,6 +1,6 @@
import Container from '@/components/shared/Container'
import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant'
import { GridDto } from '@/proxy/form/models'
import { GridDto, ListFormCustomizationTypeEnum } from '@/proxy/form/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Gantt, {
Column,
@ -24,12 +24,24 @@ 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 { GanttScaleType } from 'devextreme/ui/gantt'
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<GanttRef['instance']>
const getGanttTreeList = (gantt: GanttInstance) => {
const element = (gantt.element() as HTMLElement).querySelector('.dx-treelist')
return element ? TreeList.getInstance(element as HTMLElement) : undefined
}
interface GanttViewProps {
listFormCode: string
@ -55,7 +67,30 @@ const GanttView = (props: GanttViewProps) => {
const [gridDto, setGridDto] = useState<GridDto>()
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
const [scaleType, setScaleType] = useState<GanttScaleType>('weeks')
const [taskListWidth, setTaskListWidth] = useState(500)
const layout = layoutTypes.gantt || 'gantt'
const pendingStateRef = useRef<Record<string, any>>()
const loadedStateKeyRef = useRef('')
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(() => {
const initializeGantt = async () => {
@ -92,7 +127,6 @@ const GanttView = (props: GanttViewProps) => {
isSubForm,
gridRef,
})
useEffect(() => {
if (!gridDto) {
return
@ -164,12 +198,14 @@ const GanttView = (props: GanttViewProps) => {
const getSettingButtonOptions = useCallback(
() => ({
icon: 'preferences',
text: translate('::ListForms.ListForm.Manage'),
hint: translate('::ListForms.ListForm.Manage'),
stylingMode: 'icon',
onClick: () => {
settingButtonClick()
},
}),
[settingButtonClick],
[settingButtonClick, translate],
)
const getRefreshButtonOptions = useCallback(
@ -184,6 +220,124 @@ const GanttView = (props: GanttViewProps) => {
[settingButtonClick],
)
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'),
),
}
}, [])
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' : ''}>
@ -217,7 +371,7 @@ const GanttView = (props: GanttViewProps) => {
ref={gridRef as any}
key={`Gantt-${listFormCode}-${ganttDataSource ? 'loaded' : 'loading'}`}
id={'Gantt-' + listFormCode}
taskListWidth={500}
taskListWidth={taskListWidth}
scaleType={scaleType}
rootValue={
gridDto.gridOptions.ganttOptionDto?.rootValue === '' ||
@ -232,6 +386,7 @@ const GanttView = (props: GanttViewProps) => {
? `calc(100vh - ${170 + widgetGroupHeight}px)`
: undefined
}
onContentReady={handleGanttContentReady}
>
<Tasks
dataSource={ganttDataSource}
@ -288,6 +443,45 @@ const GanttView = (props: GanttViewProps) => {
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"
@ -298,20 +492,24 @@ const GanttView = (props: GanttViewProps) => {
/>
)}
</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>
@ -324,6 +522,11 @@ const GanttView = (props: GanttViewProps) => {
<Column key={col.dataField} {...col} />
))}
</Gantt>
<GridFilterDialogs
gridRef={ganttFilterRef}
listFormCode={listFormCode}
{...filterData}
/>
</div>
</>
)}

View file

@ -20,7 +20,7 @@ type PendingSave = {
type UseListFormStateStoringParams = {
listFormCode: string
storageKey?: string | null
filterPrefix: 'list' | 'tree' | 'pivot' | 'todo'
filterPrefix: 'list' | 'tree' | 'pivot' | 'todo' | 'gantt'
skipSaveRef?: MutableRefObject<boolean>
showSaveToast?: boolean
saveDebounceMs?: number