erp-platform/ui/src/views/list/SchedulerView.tsx

669 lines
24 KiB
TypeScript
Raw Normal View History

2025-12-02 18:15:09 +00:00
import Container from '@/components/shared/Container'
import { DX_CLASSNAMES } from '@/constants/app.constant'
import { GridDto, PlatformEditorTypes, UiLookupDataSourceTypeEnum } from '@/proxy/form/models'
2025-12-02 18:15:09 +00:00
import { useLocalization } from '@/utils/hooks/useLocalization'
2025-12-02 20:20:47 +00:00
import Scheduler, {
Editing,
Item,
Resource,
SchedulerRef,
Toolbar,
View,
} from 'devextreme-react/scheduler'
2025-12-02 18:15:09 +00:00
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'
2025-12-02 20:20:47 +00:00
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'
2025-12-02 18:15:09 +00:00
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)
2025-12-02 20:20:47 +00:00
const { checkPermission } = usePermission()
const { smaller } = useResponsive()
2025-12-02 18:15:09 +00:00
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)
2025-12-02 18:15:09 +00:00
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,
})
2025-12-02 18:15:09 +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
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])
2025-12-02 20:20:47 +00:00
const handleRefresh = useCallback(() => {
const instance = schedulerRef.current?.instance()
if (instance) {
const dataSource = instance.getDataSource()
if (dataSource) {
dataSource.reload()
}
}
}, [])
2025-12-02 18:15:09 +00:00
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) => {
const groupItems = (group.items || []).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)
// Label oluştur
const label: any = {
text: listFormField?.captionName
? translate('::' + listFormField.captionName)
: i.dataField,
}
// 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' })
}
return {
label,
dataField: i.dataField,
editorType,
colSpan: i.colSpan,
editorOptions,
validationRules: validationRules.length > 0 ? validationRules : undefined,
}
})
if (group.itemType === 'group') {
const groupItem: any = {
itemType: 'group',
colCount: group.colCount || 1,
items: groupItems,
}
// Caption null değilse ekle
if (group.caption) {
groupItem.caption = translate('::' + group.caption)
}
formItems.push(groupItem)
} else if (group.itemType === 'tabbed') {
formItems.push({
itemType: 'tabbed',
tabs: (group.tabs || []).map((tab: any) => ({
title: translate('::' + tab.title),
colCount: tab.colCount || 2,
items: (tab.items || []).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,
)
const label: any = {
text: listFormField?.captionName
? translate('::' + listFormField.captionName)
: i.dataField,
}
// 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' })
}
return {
label,
dataField: i.dataField,
editorType,
colSpan: i.colSpan,
editorOptions,
validationRules: validationRules.length > 0 ? validationRules : undefined,
}
}),
})),
})
} else {
// No group, add items directly
formItems.push(...groupItems)
}
})
}
// Form'a items'ı set et ve colCount'u ayarla
e.form.option('showValidationSummary', false)
e.form.option('items', formItems)
},
[gridDto, translate, isPopupFullScreen, listFormCode],
)
2025-12-02 18:15:09 +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-02 20:20:47 +00:00
></Helmet>
2025-12-02 18:15:09 +00:00
)}
{!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}
2025-12-03 21:01:00 +00:00
onAppointmentAdding={() => {
props.refreshData?.()
}}
onAppointmentUpdating={() => {
props.refreshData?.()
}}
onAppointmentDeleting={() => {
props.refreshData?.()
}}
2025-12-02 18:15:09 +00:00
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}
>
2025-12-02 20:20:47 +00:00
<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>
2025-12-02 18:15:09 +00:00
<Editing
allowAdding={gridDto.gridOptions.schedulerOptionDto?.allowAdding ?? false}
allowUpdating={gridDto.gridOptions.schedulerOptionDto?.allowUpdating ?? false}
allowDeleting={gridDto.gridOptions.schedulerOptionDto?.allowDeleting ?? false}
2025-12-02 18:15:09 +00:00
allowResizing={gridDto.gridOptions.schedulerOptionDto?.allowResizing ?? false}
allowDragging={gridDto.gridOptions.schedulerOptionDto?.allowDragging ?? false}
/>
2025-12-02 19:58:31 +00:00
<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')} />
2025-12-02 18:15:09 +00:00
<View
type="timelineDay"
2025-12-02 19:58:31 +00:00
name={translate('::ListForms.SchedulerOptions.TimelineDay')}
2025-12-02 18:15:09 +00:00
maxAppointmentsPerCell="unlimited"
/>
<View
type="timelineWeek"
2025-12-02 19:58:31 +00:00
name={translate('::ListForms.SchedulerOptions.TimelineWeek')}
2025-12-02 18:15:09 +00:00
maxAppointmentsPerCell="unlimited"
/>
<View
type="timelineMonth"
2025-12-02 19:58:31 +00:00
name={translate('::ListForms.SchedulerOptions.TimelineMonth')}
2025-12-02 18:15:09 +00:00
maxAppointmentsPerCell="unlimited"
/>
2025-12-02 19:58:31 +00:00
<View type="agenda" name={translate('::ListForms.SchedulerOptions.Agenda')} />
2025-12-02 18:15:09 +00:00
{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