erp-platform/ui/src/views/list/SchedulerView.tsx
2025-12-04 01:45:18 +03:00

684 lines
25 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, PlatformEditorTypes, UiLookupDataSourceTypeEnum } from '@/proxy/form/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Scheduler, {
Editing,
Item,
Resource,
SchedulerRef,
Toolbar,
View,
} from 'devextreme-react/scheduler'
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'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { usePWA } from '@/utils/hooks/usePWA'
import CustomStore from 'devextreme/data/custom_store'
import { Loading } from '@/components/shared'
import { usePermission } from '@/utils/hooks/usePermission'
import { useListFormColumns } from './useListFormColumns'
import { EditingFormItemDto } from '@/proxy/form/models'
import useResponsive from '@/utils/hooks/useResponsive'
import { SimpleItemWithColData } from '../form/types'
interface SchedulerViewProps {
listFormCode: string
searchParams?: URLSearchParams
isSubForm?: boolean
level?: number
refreshData?: () => Promise<void>
gridDto?: GridDto
}
const SchedulerView = (props: SchedulerViewProps) => {
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization()
const isPwaMode = usePWA()
const schedulerRef = useRef<SchedulerRef>()
const refListFormCode = useRef('')
const widgetGroupRef = useRef<HTMLDivElement>(null)
const { checkPermission } = usePermission()
const { smaller } = useResponsive()
const [schedulerDataSource, setSchedulerDataSource] = useState<CustomStore<any, any>>()
const [gridDto, setGridDto] = useState<GridDto>()
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
const [currentView, setCurrentView] = useState<string>('week')
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
const layout = layoutTypes.scheduler || 'scheduler'
useEffect(() => {
const initializeScheduler = async () => {
const response = await getList({ listFormCode })
setGridDto(response.data)
}
if (extGridDto === undefined) {
initializeScheduler()
} else {
setGridDto(extGridDto)
}
setCurrentView(extGridDto?.gridOptions.schedulerOptionDto?.defaultView || 'week')
}, [listFormCode, extGridDto])
useEffect(() => {
if (schedulerRef?.current) {
const instance = schedulerRef?.current?.instance()
if (instance) {
instance.option('dataSource', undefined)
}
}
if (refListFormCode.current !== listFormCode) {
// Reset state if needed
}
}, [listFormCode])
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: schedulerRef })
const { getBandedColumns } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
gridRef: schedulerRef,
})
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,
)
setSchedulerDataSource(dataSource)
}, [gridDto, searchParams, createSelectDataSource])
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])
const settingButtonClick = useCallback(() => {
window.open(
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(':listFormCode', listFormCode),
isPwaMode ? '_self' : '_blank',
)
}, [listFormCode, isPwaMode])
const handleRefresh = useCallback(() => {
const instance = schedulerRef.current?.instance()
if (instance) {
const dataSource = instance.getDataSource()
if (dataSource) {
dataSource.reload()
}
}
}, [])
const onCurrentViewChange = useCallback((value: string) => {
setCurrentView(value)
}, [])
const onAppointmentFormOpening = useCallback(
(e: any) => {
if (!gridDto) return
// Popup ayarlarını her açılışta yeniden yapılandır
e.popup.option('title', translate('::' + gridDto.gridOptions.editingOptionDto?.popup?.title))
e.popup.option('showTitle', gridDto.gridOptions.editingOptionDto?.popup?.showTitle)
e.popup.option('width', gridDto.gridOptions.editingOptionDto?.popup?.width || 800)
e.popup.option('height', gridDto.gridOptions.editingOptionDto?.popup?.height || 600)
e.popup.option('resizeEnabled', gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled)
e.popup.option('fullScreen', isPopupFullScreen)
// Toolbar butonlarını ekle
const toolbarItems = [
{
widget: 'dxButton',
toolbar: 'bottom',
location: 'after',
options: {
text: translate('::Save'),
type: 'default',
onClick: async () => {
const formInstance = e.form
const validationResult = formInstance.validate()
if (validationResult.isValid) {
// Form verilerini al
const formData = formInstance.option('formData')
try {0
// Scheduler instance'ını al
const scheduler = schedulerRef.current?.instance()
if (e.appointmentData && scheduler) {
// Yeni appointment mı yoksa güncelleme mi kontrol et
if (
e.appointmentData.id ||
e.appointmentData[gridDto.gridOptions.keyFieldName || 'id']
) {
// Güncelleme
await scheduler.updateAppointment(e.appointmentData, formData)
} else {
// Yeni ekleme
await scheduler.addAppointment(formData)
}
// Popup'ı kapat
e.popup.hide()
// RefreshData varsa çağır
if (props.refreshData) {
await props.refreshData()
}
}
} catch (error) {
console.error('Appointment save error:', error)
}
}
},
},
},
{
widget: 'dxButton',
toolbar: 'bottom',
location: 'after',
options: {
text: translate('::Cancel'),
onClick: () => {
e.cancel = true
e.popup.hide()
},
},
},
{
widget: 'dxButton',
toolbar: 'top',
location: 'after',
options: {
icon: 'fullscreen',
hint: translate('::Tam Ekran'),
stylingMode: 'text',
onClick: () => {
// Popup'tan mevcut fullScreen durumunu al
const currentFullScreen = e.popup.option('fullScreen')
const newFullScreenState = !currentFullScreen
// State'i güncelle
setIsPopupFullScreen(newFullScreenState)
// Popup'ı güncelle
e.popup.option('fullScreen', newFullScreenState)
// Width ve height'i da güncelle
if (newFullScreenState) {
e.popup.option('width', '100%')
e.popup.option('height', '100%')
} else {
e.popup.option('width', gridDto.gridOptions.editingOptionDto?.popup?.width || 600)
e.popup.option('height', gridDto.gridOptions.editingOptionDto?.popup?.height || 'auto')
}
// Button icon ve hint'i güncelle
const button = e.popup._$element.find('.dx-toolbar-after .dx-button').last()
if (button.length) {
const buttonInstance = button.dxButton('instance')
if (buttonInstance) {
buttonInstance.option('icon', newFullScreenState ? 'collapse' : 'fullscreen')
buttonInstance.option('hint', newFullScreenState ? translate('::Normal Boyut') : translate('::Tam Ekran'))
}
}
},
},
},
]
e.popup.option('toolbarItems', toolbarItems)
// EditingFormDto'dan form items oluştur
const formItems: any[] = []
if (gridDto.gridOptions.editingFormDto?.length > 0) {
const sortedFormDto = gridDto.gridOptions.editingFormDto
.slice()
.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1))
sortedFormDto.forEach((group: any) => {
// Items'ları da order'a göre sırala
const sortedItems = (group.items || [])
.slice()
.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1))
const groupItems = sortedItems.map((i: EditingFormItemDto) => {
let editorOptions: any = {}
try {
if (i.editorOptions) {
editorOptions = JSON.parse(i.editorOptions)
}
} catch {}
const fieldName = i.dataField.split(':')[0]
const listFormField = gridDto.columnFormats.find((x: any) => x.fieldName === fieldName)
// EditorType belirleme - Grid'deki gibi
let editorType: any = i.editorType2 || i.editorType
if (i.editorType2 === PlatformEditorTypes.dxGridBox) {
editorType = 'dxDropDownBox'
} else if (i.editorType2) {
editorType = i.editorType2
}
// Lookup DataSource oluştur
if (listFormField?.lookupDto) {
const lookup = listFormField.lookupDto
// EditorType'ı dxSelectBox olarak ayarla
if (!editorType || editorType === 'dxTextBox') {
editorType = 'dxSelectBox'
}
if (lookup.dataSourceType === UiLookupDataSourceTypeEnum.Query) {
editorOptions.dataSource = new CustomStore({
key: 'key',
loadMode: 'raw',
load: async () => {
try {
const { dynamicFetch } = await import('@/services/form.service')
const response = await dynamicFetch('list-form-select/lookup', 'POST', null, {
listFormCode,
listFormFieldName: fieldName,
filters: [],
})
return (response.data ?? []).map((a: any) => ({
key: a.Key,
name: a.Name,
group: a.Group,
}))
} catch (error) {
console.error('Lookup load error:', error)
return []
}
},
})
editorOptions.valueExpr = 'key'
editorOptions.displayExpr = 'name'
} else if (lookup.dataSourceType === UiLookupDataSourceTypeEnum.StaticData) {
if (lookup.lookupQuery) {
try {
const staticData = JSON.parse(lookup.lookupQuery)
editorOptions.dataSource = staticData
editorOptions.valueExpr = lookup.valueExpr || 'key'
editorOptions.displayExpr = lookup.displayExpr || 'name'
} catch (error) {
console.error('Static data parse error:', error)
}
}
}
}
// Validation rules
const validationRules: any[] = []
if (i.isRequired) {
validationRules.push({ type: 'required' })
}
const item: any = {
dataField: i.dataField,
name: i.dataField,
editorType,
colSpan: i.colSpan,
editorOptions,
validationRules: validationRules.length > 0 ? validationRules : undefined,
}
// Label sadece caption varsa ekle
if (listFormField?.captionName) {
item.label = { text: translate('::' + listFormField.captionName) }
}
return item
})
if (group.itemType === 'group') {
// Grup kullanmadan direkt items'ları ekle - form'un colCount'u geçerli olsun
formItems.push(...groupItems)
} else if (group.itemType === 'tabbed') {
formItems.push({
itemType: 'tabbed',
tabs: (group.tabs || []).map((tab: any) => {
// Tab items'larını da order'a göre sırala
const sortedTabItems = (tab.items || [])
.slice()
.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1))
return {
title: translate('::' + tab.title),
colCount: tab.colCount || 2,
items: sortedTabItems.map((i: EditingFormItemDto) => {
// Tab içindeki itemlar için de aynı mapping
let editorOptions: any = {}
try {
if (i.editorOptions) {
editorOptions = JSON.parse(i.editorOptions)
}
} catch {}
const fieldName = i.dataField.split(':')[0]
const listFormField = gridDto.columnFormats.find(
(x: any) => x.fieldName === fieldName,
)
// EditorType belirleme - Grid'deki gibi
let editorType: any = i.editorType2 || i.editorType
if (i.editorType2 === PlatformEditorTypes.dxGridBox) {
editorType = 'dxDropDownBox'
} else if (i.editorType2) {
editorType = i.editorType2
}
if (listFormField?.lookupDto) {
const lookup = listFormField.lookupDto
// EditorType'ı dxSelectBox olarak ayarla
if (!editorType || editorType === 'dxTextBox') {
editorType = 'dxSelectBox'
}
if (lookup.dataSourceType === UiLookupDataSourceTypeEnum.Query) {
editorOptions.dataSource = new CustomStore({
key: 'key',
loadMode: 'raw',
load: async () => {
try {
const { dynamicFetch } = await import('@/services/form.service')
const response = await dynamicFetch(
'list-form-select/lookup',
'POST',
null,
{
listFormCode,
listFormFieldName: fieldName,
filters: [],
},
)
return (response.data ?? []).map((a: any) => ({
key: a.Key,
name: a.Name,
group: a.Group,
}))
} catch (error) {
console.error('Lookup load error:', error)
return []
}
},
})
editorOptions.valueExpr = 'key'
editorOptions.displayExpr = 'name'
} else if (lookup.dataSourceType === UiLookupDataSourceTypeEnum.StaticData) {
if (lookup.lookupQuery) {
try {
const staticData = JSON.parse(lookup.lookupQuery)
editorOptions.dataSource = staticData
editorOptions.valueExpr = lookup.valueExpr || 'key'
editorOptions.displayExpr = lookup.displayExpr || 'name'
} catch (error) {
console.error('Static data parse error:', error)
}
}
}
}
const validationRules: any[] = []
if (i.isRequired) {
validationRules.push({ type: 'required' })
}
const item: any = {
dataField: i.dataField,
name: i.dataField,
editorType,
colSpan: i.colSpan,
editorOptions,
validationRules: validationRules.length > 0 ? validationRules : undefined,
}
// Label sadece caption varsa ekle
if (listFormField?.captionName) {
item.label = { text: translate('::' + listFormField.captionName) }
}
return item
}),
}
}),
})
} else {
// No group, add items directly
formItems.push(...groupItems)
}
})
}
// Form'u tamamen yeniden yapılandır
const formConfig = {
colCount: gridDto.gridOptions.editingFormDto?.[0]?.colCount || 2,
showValidationSummary: false,
items: formItems,
}
// Form'u tamamen yeniden baştan yapılandır
e.form.option('colCount', formConfig.colCount)
e.form.option('showValidationSummary', formConfig.showValidationSummary)
e.form.option('items', formConfig.items)
// Form'u repaint et
e.form.repaint()
},
[gridDto, translate, isPopupFullScreen, listFormCode],
)
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 loading>Loading scheduler configuration...</Loading>
</div>
)}
{gridDto && !schedulerDataSource && (
<div className="p-4">
<Loading loading>Loading data source...</Loading>
</div>
)}
{gridDto && schedulerDataSource && (
<>
<div className="p-1">
<Scheduler
ref={schedulerRef as any}
key={`Scheduler-${listFormCode}-${schedulerDataSource ? 'loaded' : 'loading'}`}
id={'Scheduler-' + listFormCode}
dataSource={schedulerDataSource}
textExpr={gridDto.gridOptions.schedulerOptionDto?.textExpr || 'text'}
startDateExpr={gridDto.gridOptions.schedulerOptionDto?.startDateExpr || 'startDate'}
endDateExpr={gridDto.gridOptions.schedulerOptionDto?.endDateExpr || 'endDate'}
allDayExpr={gridDto.gridOptions.schedulerOptionDto?.allDayExpr}
recurrenceRuleExpr={gridDto.gridOptions.schedulerOptionDto?.recurrenceRuleExpr}
recurrenceExceptionExpr={
gridDto.gridOptions.schedulerOptionDto?.recurrenceExceptionExpr
}
startDayHour={gridDto.gridOptions.schedulerOptionDto?.startDayHour || 8}
endDayHour={gridDto.gridOptions.schedulerOptionDto?.endDayHour || 18}
currentView={currentView}
onCurrentViewChange={onCurrentViewChange}
onAppointmentFormOpening={onAppointmentFormOpening}
onAppointmentAdding={() => {
props.refreshData?.()
}}
onAppointmentUpdating={() => {
props.refreshData?.()
}}
onAppointmentDeleting={() => {
props.refreshData?.()
}}
height={
gridDto.gridOptions.height > 0
? gridDto.gridOptions.height
: gridDto.gridOptions.fullHeight
? `calc(100vh - ${170 + widgetGroupHeight}px)`
: undefined
}
showAllDayPanel={gridDto.gridOptions.schedulerOptionDto?.showAllDayPanel ?? true}
crossScrollingEnabled={
gridDto.gridOptions.schedulerOptionDto?.crossScrollingEnabled ?? false
}
cellDuration={gridDto.gridOptions.schedulerOptionDto?.cellDuration || 30}
firstDayOfWeek={
(gridDto.gridOptions.schedulerOptionDto?.firstDayOfWeek as
| 0
| 1
| 2
| 3
| 4
| 5
| 6) || 1
}
adaptivityEnabled={true}
>
<Toolbar>
<Item name="dateNavigator" />
<Item name="today" />
<Item name="viewSwitcher" />
<Item
location="after"
widget="dxButton"
options={{
icon: 'refresh',
hint: translate('::ListForms.ListForm.Refresh'),
onClick: handleRefresh,
}}
/>
{checkPermission(gridDto?.gridOptions.permissionDto.u) && (
<Item
location="after"
widget="dxButton"
options={{
icon: 'preferences',
hint: 'Settings',
onClick: settingButtonClick,
}}
/>
)}
</Toolbar>
<Editing
allowAdding={gridDto.gridOptions.schedulerOptionDto?.allowAdding ?? false}
allowUpdating={gridDto.gridOptions.schedulerOptionDto?.allowUpdating ?? false}
allowDeleting={gridDto.gridOptions.schedulerOptionDto?.allowDeleting ?? false}
allowResizing={gridDto.gridOptions.schedulerOptionDto?.allowResizing ?? false}
allowDragging={gridDto.gridOptions.schedulerOptionDto?.allowDragging ?? false}
/>
<View type="day" name={translate('::ListForms.SchedulerOptions.Day')} />
<View type="week" name={translate('::ListForms.SchedulerOptions.Week')} />
<View type="workWeek" name={translate('::ListForms.SchedulerOptions.WorkWeek')} />
<View type="month" name={translate('::ListForms.SchedulerOptions.Month')} />
<View
type="timelineDay"
name={translate('::ListForms.SchedulerOptions.TimelineDay')}
maxAppointmentsPerCell="unlimited"
/>
<View
type="timelineWeek"
name={translate('::ListForms.SchedulerOptions.TimelineWeek')}
maxAppointmentsPerCell="unlimited"
/>
<View
type="timelineMonth"
name={translate('::ListForms.SchedulerOptions.TimelineMonth')}
maxAppointmentsPerCell="unlimited"
/>
<View type="agenda" name={translate('::ListForms.SchedulerOptions.Agenda')} />
{gridDto.gridOptions.schedulerOptionDto?.resources?.map((resource, index) => (
<Resource
key={index}
fieldExpr={resource.fieldExpr}
dataSource={resource.dataSource}
label={resource.label}
useColorAsDefault={resource.useColorAsDefault}
/>
))}
</Scheduler>
</div>
</>
)}
</Container>
</>
)
}
export default SchedulerView