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

676 lines
24 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import Container from '@/components/shared/Container'
import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant'
import {
DbTypeEnum,
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, autoNumber } 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 { EditingFormItemDto } from '@/proxy/form/models'
import { RowMode, SimpleItemWithColData } from '../form/types'
import { captionize } from 'devextreme/core/utils/inflector'
import { AppointmentFormOpeningEvent } from 'devextreme/ui/scheduler'
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 [mode, setMode] = useState<RowMode>('view')
const schedulerRef = useRef<SchedulerRef>()
const refListFormCode = useRef('')
const widgetGroupRef = useRef<HTMLDivElement>(null)
const { checkPermission } = usePermission()
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 })
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: AppointmentFormOpeningEvent) => {
if (!gridDto) return
// Yeni appointment mı yoksa düzenleme mi kontrol et
const isNewAppointment =
!e.appointmentData ||
(!e.appointmentData.id && !e.appointmentData[gridDto.gridOptions.keyFieldName || 'id'])
const currentMode = isNewAppointment ? 'new' : 'edit'
setMode(currentMode)
// Popup ayarlarını her açılışta yeniden yapılandır
e.popup.option(
'title',
(currentMode === 'new' ? '✚ ' : '🖊️ ') +
translate('::' + gridDto.gridOptions.editingOptionDto?.popup?.title),
)
e.popup.option('showTitle', gridDto.gridOptions.editingOptionDto?.popup?.showTitle)
e.popup.option('width', gridDto.gridOptions.editingOptionDto?.popup?.width)
e.popup.option('height', gridDto.gridOptions.editingOptionDto?.popup?.height)
e.popup.option('resizeEnabled', gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled)
e.popup.option('fullScreen', isPopupFullScreen)
// Toolbar butonlarını ekle
e.popup.option('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 {
// 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',
)
}
},
},
},
])
// EditingFormDto'dan form items oluştur
const result: any[] = []
if (gridDto.gridOptions.editingFormDto?.length > 0) {
const sortedFormDto = gridDto.gridOptions.editingFormDto
.slice()
.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1))
// Tüm tabbed items'ları topla (Grid'deki gibi)
const tabbedItems = sortedFormDto.filter((e: any) => e.itemType === 'tabbed')
// Ortak item mapper fonksiyonu - hem group hem tab için kullanılır
const mapFormItem = (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)
if (listFormField?.sourceDbType === DbTypeEnum.Date) {
editorOptions = {
...{
type: 'date',
dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate',
},
...editorOptions,
}
} else if (
listFormField?.sourceDbType === DbTypeEnum.DateTime ||
listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
) {
editorOptions = {
...{
type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime',
},
...editorOptions,
}
}
// Set defaultValue for @AUTONUMBER fields
if (
typeof listFormField?.defaultValue === 'string' &&
listFormField?.defaultValue === '@AUTONUMBER' &&
currentMode === 'new'
) {
editorOptions = {
...editorOptions,
value: autoNumber(),
}
}
// EditorType belirleme
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
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 item: SimpleItemWithColData = {
canRead: listFormField?.canRead ?? false,
canUpdate: listFormField?.canUpdate ?? false,
canCreate: listFormField?.canCreate ?? false,
canExport: listFormField?.canExport ?? false,
2026-05-13 11:31:15 +00:00
allowEditing: listFormField?.allowEditing ?? true,
allowAdding: listFormField?.allowAdding ?? true,
2026-02-24 20:44:16 +00:00
dataField: i.dataField,
name: i.dataField,
editorType2: i.editorType2,
editorType:
2026-05-13 11:31:15 +00:00
i.editorType2 == PlatformEditorTypes.dxGridBox
? 'dxDropDownBox'
: i.editorType2 == PlatformEditorTypes.dxImageUpload
? undefined
: i.editorType2,
2026-02-24 20:44:16 +00:00
colSpan: i.colSpan,
editorOptions,
editorScript: i.editorScript,
}
if (i.dataField.indexOf(':') >= 0) {
item.label = { text: captionize(i.dataField.split(':')[1]) }
}
2026-05-13 11:31:15 +00:00
if (
(currentMode == 'edit' && !item.canUpdate) ||
(currentMode == 'new' && !item.canCreate)
) {
2026-02-24 20:44:16 +00:00
item.editorOptions = {
...item.editorOptions,
readOnly: true,
}
}
return item
}
sortedFormDto.forEach((e: any) => {
// Items'ları da order'a göre sırala
const sortedItems = (e.items || [])
.slice()
.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1))
const groupItems = sortedItems.map(mapFormItem)
if (e.itemType !== 'tabbed') {
// Grup kullanmadan direkt items'ları ekle - form'un colCount'u geçerli olsun
result.push(...groupItems)
} else if (e.itemType === 'tabbed' && tabbedItems.length > 0 && e === tabbedItems[0]) {
// Tabbed için caption OLMAMALI - sadece tabs array içinde title kullan
result.push({
itemType: 'tabbed',
// colCount tabbed item için OLMAMALI - sadece colSpan
colSpan: 1,
// caption kullanma! Tabs içindeki title'lar yeterli
tabs: tabbedItems.map((tabbedItem: any) => {
// Backend'den gelen colCount ve colSpan değerlerini kullan
const effectiveColCount = tabbedItem.colCount || 2
return {
title: tabbedItem.caption, // Her tab'ın title'ı
colCount: effectiveColCount,
items: tabbedItem.items
?.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1))
.map(mapFormItem)
.filter((a: any) => {
if (currentMode === 'new') {
return a.canCreate || a.canRead
} else if (currentMode === 'edit') {
return a.canUpdate || a.canRead
}
return false
}),
}
}),
})
}
})
}
// Form'u tamamen yeniden yapılandır
const hasTabbedItems = result.some((item: any) => item.itemType === 'tabbed')
// Form'u tamamen yeniden baştan yapılandır
e.form.option(
'colCount',
hasTabbedItems ? 1 : gridDto.gridOptions.editingFormDto?.[0]?.colCount || 2,
)
e.form.option(
'colCount',
hasTabbedItems ? 1 : gridDto.gridOptions.editingFormDto?.[0]?.colCount || 2,
)
e.form.option('showValidationSummary', false)
e.form.option('items', result)
// Yeni versiyonlarda bu şekilde Style değiştirme işlemi yapmak zorunda olmayabiliriz.
// Yani geçici olarak eklenmiştir.
// Tabbed varsa belirli class'lara sahip elementi bul ve flex-direction'ı değiştir
if (hasTabbedItems) {
setTimeout(() => {
2026-05-13 11:31:15 +00:00
const targetElement = document.querySelector(
'.dx-item-content.dx-box-item-content.dx-box-flex.dx-box.dx-widget.dx-collection',
)
2026-02-24 20:44:16 +00:00
if (targetElement) {
const htmlElement = targetElement as HTMLElement
2026-05-13 11:31:15 +00:00
2026-02-24 20:44:16 +00:00
// Mevcut style'ı al ve flex-direction: row'u column'a çevir
const currentStyle = htmlElement.getAttribute('style') || ''
2026-05-13 11:31:15 +00:00
const updatedStyle = currentStyle.replace(
/flex-direction:\s*row/gi,
'flex-direction: column',
)
2026-02-24 20:44:16 +00:00
htmlElement.setAttribute('style', updatedStyle)
2026-05-13 11:31:15 +00:00
}
2026-02-24 20:44:16 +00:00
}, 100)
}
},
[gridDto, translate, isPopupFullScreen, listFormCode],
)
return (
<>
<div ref={widgetGroupRef}>
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
</div>
<Container className={DX_CLASSNAMES}>
{!isSubForm && (
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + gridDto?.gridOptions.title)}
defaultTitle={APP_NAME}
></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}
2026-05-13 11:31:15 +00:00
dateSerializationFormat="yyyy-MM-ddTHH:mm:ss"
2026-02-24 20:44:16 +00:00
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?.allowEditing ?? false}
allowDeleting={gridDto.gridOptions.schedulerOptionDto?.allowDeleting ?? false}
allowResizing={gridDto.gridOptions.schedulerOptionDto?.allowResizing ?? false}
allowDragging={gridDto.gridOptions.schedulerOptionDto?.allowDragging ?? false}
/>
2026-03-29 08:59:07 +00:00
<View type="day" name={translate('::App.Listform.ListformField.Day')} />
2026-02-24 20:44:16 +00:00
<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