Gantt Layout

This commit is contained in:
Sedat ÖZTÜRK 2025-12-01 17:45:23 +03:00
parent f18818d16a
commit 3e588fb98b
10 changed files with 378 additions and 11 deletions

View file

@ -5,8 +5,9 @@ public class LayoutDto
public bool Grid { get; set; } = true;
public bool Card { get; set; } = true;
public bool Pivot { get; set; } = true;
public bool Tree { get; set; } = true;
public bool Chart { get; set; } = true;
public bool Tree { get; set; } = true;
public bool Gantt { get; set; } = true;
public string DefaultLayout { get; set; } = "grid";
public int CardLayoutColumn { get; set; } = 4;
}

View file

@ -3592,8 +3592,8 @@
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.TabTree",
"en": "Tree",
"tr": "Ağaç"
"en": "Tree & Gantt",
"tr": "Ağaç & Gantt"
},
{
"resourceName": "Platform",
@ -3799,6 +3799,12 @@
"en": "Full Height",
"tr": "Tam Yükseklik"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.DetailsLayoutDto.GanttLayout",
"en": "Gantt Layout",
"tr": "Gantt Düzeni"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.DetailsLayoutDto.GridLayout",

View file

@ -1496,11 +1496,11 @@ public class ListFormSeeder_Hr : IDataSeedContributor, ITransientDependency
DisplayExpr = "name",
ValueExpr = "key",
LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] {
new () { Key= "Active", Name= "Active"},
new () { Key= "Inactive", Name= "Inactive" },
new () { Key= "OnLeave", Name= "OnLeave" },
new () { Key= "Suspended", Name= "Suspended" },
new () { Key= "Terminated", Name= "Terminated" },
new () { Key= "Aktif", Name= "Aktif"},
new () { Key= "Pasif", Name= "Pasif" },
new () { Key= "İzinli", Name= "İzinli" },
new () { Key= "Askıda", Name= "Askıda" },
new () { Key= "Sonlandırıldı", Name= "Sonlandırıldı" },
}),
}),
PermissionJson = DefaultFieldPermissionJson(listForm.Name),

View file

@ -68,6 +68,8 @@ public static class SeederDefaults
Card = true,
Pivot = true,
Chart = true,
Tree = true,
Gantt = true,
DefaultLayout = "grid",
CardLayoutColumn = 4
});

View file

@ -7,5 +7,6 @@ public static class ListFormTypeEnum
public const string Chart = "Chart";
public const string Pivot = "Pivot";
public const string Tree = "Tree";
public const string Gantt = "Gantt";
}

View file

@ -820,6 +820,7 @@ export interface LayoutDto {
pivot: boolean
tree: boolean
chart: boolean
gantt: boolean
defaultLayout: ListViewLayoutType
cardLayoutColumn: number
}

View file

@ -107,7 +107,7 @@ function FormTabDetails(
name="listFormType"
placeholder={translate('::ListForms.ListFormEdit.ListFormType')}
>
{({ field, form }: FieldProps<SelectBox>) => (
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
@ -245,7 +245,7 @@ function FormTabDetails(
'::ListForms.ListFormEdit.DetailsLayoutDto.DefaultLayout',
)}
>
{({ field, form }: FieldProps<SelectBox>) => (
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
@ -282,6 +282,20 @@ function FormTabDetails(
)}
<div className="flex gap-2">
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.GanttLayout')}
invalid={errors.layoutDto?.gantt && touched.layoutDto?.gantt}
errorMessage={errors.layoutDto?.gantt}
>
<Field
className="w-20"
autoComplete="off"
name="layoutDto.gantt"
placeholder={translate('::ListForms.ListFormEdit.DetailsLayoutDto.GanttLayout')}
component={Checkbox}
/>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsLayoutDto.GridLayout')}
invalid={errors.layoutDto?.grid && touched.layoutDto?.grid}

View file

@ -1,6 +1,6 @@
export type ChartOperation = '' | 'select' | 'insert' | 'update' | 'delete'
export type ChartDialogType = '' | 'pane' | 'serie' | 'annotation' | 'axis'
export type ListViewLayoutType = 'grid' | 'card' | 'pivot' | 'tree' | 'chart'
export type ListViewLayoutType = 'grid' | 'card' | 'pivot' | 'tree' | 'chart' | 'gantt'
export const layoutTypes = {
grid: 'Grid',
@ -8,4 +8,5 @@ export const layoutTypes = {
pivot: 'Pivot',
tree: 'Tree',
chart: 'Chart',
gantt: 'Gantt',
}

View file

@ -0,0 +1,317 @@
import Container from '@/components/shared/Container'
import { Dialog, Notification, toast } from '@/components/ui'
import { DX_CLASSNAMES } from '@/constants/app.constant'
import { GridDto } from '@/proxy/form/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import useResponsive from '@/utils/hooks/useResponsive'
import Gantt, {
Column,
Dependencies,
Editing,
GanttRef,
GanttTypes,
ResourceAssignments,
Resources,
Tasks,
Validation,
} from 'devextreme-react/gantt'
import { Button } from '@/components/ui'
import CustomStore from 'devextreme/data/custom_store'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { getList } from '@/services/form.service'
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
import { useToolbar } from './useToolbar'
import { useFilters } from './useFilters'
import { addCss, addJs } from './Utils'
import { layoutTypes } from '../admin/listForm/edit/types'
import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
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()
const { smaller } = useResponsive()
const ganttRef = useRef<GanttRef>()
const refListFormCode = useRef('')
const widgetGroupRef = useRef<HTMLDivElement>(null)
const [ganttDataSource, setGanttDataSource] = useState<CustomStore<any, any>>()
const [gridDto, setGridDto] = useState<GridDto>()
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
useEffect(() => {
const initializeGantt = async () => {
const response = await getList({ listFormCode })
setGridDto(response.data)
}
if (extGridDto === undefined) {
initializeGantt()
} else {
setGridDto(extGridDto)
}
}, [listFormCode, extGridDto])
const layout = layoutTypes.gantt || 'gantt'
const { toolbarData, toolbarModalData, setToolbarModalData } = useToolbar({
gridDto,
listFormCode,
getSelectedRowKeys: () => Promise.resolve([]),
getSelectedRowsData: () => [],
refreshData,
getFilter: () => undefined,
layout,
})
const { filterToolbarData, ...filterData } = useFilters({
gridDto,
gridRef: ganttRef as any,
listFormCode,
})
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: ganttRef as any })
function refreshData() {
ganttRef.current?.instance()?.refresh()
}
function onTaskInserted() {
props.refreshData?.()
}
function onTaskUpdated() {
props.refreshData?.()
}
function onTaskDeleted() {
props.refreshData?.()
}
useEffect(() => {
if (ganttRef?.current) {
const instance = ganttRef?.current?.instance()
if (instance) {
instance.option('dataSource', undefined)
}
}
if (refListFormCode.current !== listFormCode) {
// Reset state if needed
}
}, [listFormCode])
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 dataSource = createSelectDataSource(
gridDto.gridOptions,
listFormCode,
searchParams,
layout,
undefined,
)
setGanttDataSource(dataSource)
}, [gridDto, 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])
// Gantt için sütunları oluştur
const getGanttColumns = useCallback(() => {
if (!gridDto?.columnFormats) return []
return gridDto.columnFormats
.filter((col) => col.canRead && col.isActive && col.visible)
.map((col) => {
const column: any = {
dataField: col.fieldName,
caption: col.captionName ? translate('::' + col.captionName) : col.fieldName,
width: col.width > 0 ? col.width : undefined,
alignment: col.alignment,
format: col.format,
}
return column
})
}, [gridDto, translate])
const ganttColumns = getGanttColumns()
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"
></Helmet>
)}
{!gridDto && <div className="p-4">Loading gantt configuration...</div>}
{gridDto && !ganttDataSource && <div className="p-4">Loading data source...</div>}
{gridDto && ganttDataSource && (
<>
{/* Custom Toolbar */}
{(toolbarData.length > 0 || filterToolbarData.length > 0) && (
<div className="flex flex-wrap gap-2 p-2 border-b bg-white">
{toolbarData.map((item: any) => {
if (item.widget === 'dxButton' && item.options) {
return (
<Button
key={item.name}
size="sm"
variant={item.options.type || 'solid'}
icon={item.options.icon}
onClick={item.options.onClick}
disabled={item.options.disabled}
>
{item.options.text}
</Button>
)
}
return null
})}
{filterToolbarData.map((item: any) => {
if (item.widget === 'dxButton' && item.options) {
return (
<Button
key={item.name}
size="sm"
variant={item.options.type || 'solid'}
icon={item.options.icon}
onClick={item.options.onClick}
disabled={item.options.disabled}
>
{item.options.text}
</Button>
)
}
return null
})}
</div>
)}
<div className="p-1">
<Gantt
ref={ganttRef as any}
key={`Gantt-${listFormCode}-${ganttDataSource ? 'loaded' : 'loading'}`}
id={'Gantt-' + listFormCode}
taskListWidth={500}
scaleType="weeks"
height={
gridDto.gridOptions.height > 0
? gridDto.gridOptions.height
: gridDto.gridOptions.fullHeight
? `calc(100vh - ${170 + widgetGroupHeight}px)`
: 700
}
showResources={true}
showDependencies={true}
onTaskInserted={onTaskInserted}
onTaskUpdated={onTaskUpdated}
onTaskDeleted={onTaskDeleted}
>
<Tasks dataSource={ganttDataSource} />
<Dependencies dataSource={[]} />
<Resources dataSource={[]} />
<ResourceAssignments dataSource={[]} />
<Editing
enabled={
gridDto.gridOptions.editingOptionDto?.allowAdding ||
gridDto.gridOptions.editingOptionDto?.allowUpdating ||
gridDto.gridOptions.editingOptionDto?.allowDeleting
}
allowTaskAdding={gridDto.gridOptions.editingOptionDto?.allowAdding}
allowTaskUpdating={gridDto.gridOptions.editingOptionDto?.allowUpdating}
allowTaskDeleting={gridDto.gridOptions.editingOptionDto?.allowDeleting}
allowDependencyAdding={false}
allowDependencyDeleting={false}
allowResourceAdding={false}
allowResourceDeleting={false}
/>
<Validation autoUpdateParentTasks={true} />
{ganttColumns.map((col: any) => (
<Column
key={col.dataField}
dataField={col.dataField}
caption={col.caption}
width={col.width}
/>
))}
</Gantt>
</div>
</>
)}
<Dialog
isOpen={toolbarModalData?.open || false}
onClose={() => setToolbarModalData(undefined)}
onRequestClose={() => setToolbarModalData(undefined)}
>
{toolbarModalData?.content}
</Dialog>
</Container>
</>
)
}
export default GanttView

View file

@ -15,6 +15,8 @@ import { useCurrentMenuIcon } from '@/utils/hooks/useCurrentMenuIcon'
import { ListViewLayoutType } from '../admin/listForm/edit/types'
import Chart from './Chart'
import Card from './Card'
import { FaChartGantt } from 'react-icons/fa6'
import GanttView from './GanttView'
const List = () => {
const params = useParams()
@ -93,6 +95,21 @@ const List = () => {
)}
<div className="flex gap-1">
{gridDto?.gridOptions?.layoutDto.gantt &&
gridDto?.gridOptions?.treeOptionDto?.parentIdExpr && (
<Button
size="xs"
variant={viewMode === 'gantt' ? 'solid' : 'default'}
onClick={() => {
setViewMode('gantt')
setStates({ listFormCode, layout: 'gantt' })
}}
title="Gantt Görünümü"
>
<FaChartGantt className="w-4 h-4" />
</Button>
)}
{gridDto?.gridOptions?.layoutDto.tree &&
gridDto?.gridOptions?.treeOptionDto?.parentIdExpr && (
<Button
@ -174,6 +191,13 @@ const List = () => {
gridDto={gridDto}
refreshGridDto={refreshGridDto}
/>
) : viewMode === 'gantt' ? (
<GanttView
listFormCode={listFormCode}
searchParams={searchParams}
isSubForm={false}
gridDto={gridDto}
/>
) : viewMode === 'tree' ? (
<Tree
listFormCode={listFormCode}