Gantt Layout
This commit is contained in:
parent
f18818d16a
commit
3e588fb98b
10 changed files with 378 additions and 11 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ public static class SeederDefaults
|
|||
Card = true,
|
||||
Pivot = true,
|
||||
Chart = true,
|
||||
Tree = true,
|
||||
Gantt = true,
|
||||
DefaultLayout = "grid",
|
||||
CardLayoutColumn = 4
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -820,6 +820,7 @@ export interface LayoutDto {
|
|||
pivot: boolean
|
||||
tree: boolean
|
||||
chart: boolean
|
||||
gantt: boolean
|
||||
defaultLayout: ListViewLayoutType
|
||||
cardLayoutColumn: number
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
|
|
|||
317
ui/src/views/list/GanttView.tsx
Normal file
317
ui/src/views/list/GanttView.tsx
Normal 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
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue