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

929 lines
35 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
import Container from '@/components/shared/Container'
2026-07-11 20:35:32 +00:00
import '@/utils/registerDevExtremeEditors'
2026-07-30 19:38:27 +00:00
import { APP_NAME, AVATAR_URL, DX_CLASSNAMES } from '@/constants/app.constant'
2026-02-24 20:44:16 +00:00
import {
ColumnFormatDto,
2026-02-24 20:44:16 +00:00
DbTypeEnum,
2026-07-30 19:38:27 +00:00
FieldCustomValueTypeEnum,
2026-02-24 20:44:16 +00:00
GridDto,
} from '@/proxy/form/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Scheduler, {
Editing,
Item,
Resource,
2026-07-13 08:39:13 +00:00
type SchedulerRef,
2026-02-24 20:44:16 +00:00
Toolbar,
View,
} from 'devextreme-react/scheduler'
2026-07-30 19:38:27 +00:00
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2026-02-24 20:44:16 +00:00
import { Helmet } from 'react-helmet'
import { getNextSequenceValue } from '@/services/form.service'
2026-02-24 20:44:16 +00:00
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
import { autoNumber, getEditingFormGroups } from './Utils'
2026-02-24 20:44:16 +00:00
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'
2026-07-30 19:38:27 +00:00
import { SimpleItemWithColData } from '../form/types'
2026-02-24 20:44:16 +00:00
import { captionize } from 'devextreme/core/utils/inflector'
2026-07-29 10:04:07 +00:00
import { formatDate } from 'devextreme/localization'
2026-07-13 08:39:13 +00:00
import type { AppointmentFormOpeningEvent } from 'devextreme/ui/scheduler'
2026-07-30 19:38:27 +00:00
import type { ValidationRule } from 'devextreme/common'
import { FaAlignLeft, FaClock, FaHeading } from 'react-icons/fa'
import { Avatar } from '@/components/ui'
import { getUsers } from '@/services/identity.service'
import { useListFormColumns } from './useListFormColumns'
import {
getEditPopupHeight,
getEditPopupMaxHeight,
getMobilePopupPosition,
shouldUseMobileEditPopup,
} from './shared/editPopup'
import { getValueByField, resolveEditorType } from './shared/editingForm'
import { flattenGridColumns } from './shared/columns'
import {
useListFormCustomSources,
useListFormGridDto,
useWidgetGroupHeight,
} from './shared/hooks'
2026-02-24 20:44:16 +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 useMobileEditPopup = shouldUseMobileEditPopup()
2026-02-24 20:44:16 +00:00
const schedulerRef = useRef<SchedulerRef>()
const widgetGroupRef = useRef<HTMLDivElement>(null)
const { checkPermission } = usePermission()
const [schedulerDataSource, setSchedulerDataSource] = useState<CustomStore<any, any>>()
const gridDto = useListFormGridDto(listFormCode, extGridDto)
2026-02-24 20:44:16 +00:00
const [currentView, setCurrentView] = useState<string>('week')
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
2026-07-30 19:38:27 +00:00
const [userAvatarUrls, setUserAvatarUrls] = useState<Record<string, string>>({})
2026-02-24 20:44:16 +00:00
const layout = layoutTypes.scheduler || 'scheduler'
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
useListFormCustomSources(gridDto)
2026-07-30 19:38:27 +00:00
useEffect(() => {
let cancelled = false
getUsers(0, 1000)
.then((response) => {
if (cancelled) {
return
}
const avatarUrls = (response.data?.items ?? []).reduce<Record<string, string>>(
(result, user) => {
if (user.userName) {
result[user.userName.toLowerCase()] = AVATAR_URL(user.id, user.tenantId)
}
return result
},
{},
)
setUserAvatarUrls(avatarUrls)
})
.catch((error) => console.error('Scheduler user avatars load error:', error))
return () => {
cancelled = true
}
}, [])
2026-02-24 20:44:16 +00:00
useEffect(() => {
setCurrentView(gridDto?.gridOptions.schedulerOptionDto?.defaultView || 'week')
}, [gridDto])
2026-02-24 20:44:16 +00:00
// listFormCode değişiminde eski veri kaynağını temizle.
2026-02-24 20:44:16 +00:00
useEffect(() => {
schedulerRef.current?.instance()?.option('dataSource', undefined)
2026-02-24 20:44:16 +00:00
}, [listFormCode])
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: schedulerRef })
const { getBandedColumns } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
gridRef: schedulerRef,
})
const listFormColumns = useMemo(() => flattenGridColumns(getBandedColumns()), [getBandedColumns])
2026-02-24 20:44:16 +00:00
useEffect(() => {
if (!gridDto) return
const dataSource = createSelectDataSource(
gridDto.gridOptions,
listFormCode,
searchParams,
layout,
undefined,
)
setSchedulerDataSource(dataSource)
}, [gridDto, searchParams, createSelectDataSource])
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)
}, [])
2026-07-29 10:04:07 +00:00
const getAppointmentHtml = useCallback(
(appointmentData: Record<string, any>) => {
const textExpr = gridDto?.gridOptions.schedulerOptionDto?.textExpr || 'text'
return String(appointmentData?.[textExpr] ?? '')
},
[gridDto],
)
const getAppointmentTimeText = useCallback(
(appointmentData: Record<string, any>) => {
const schedulerOptions = gridDto?.gridOptions.schedulerOptionDto
const startDate = appointmentData?.[schedulerOptions?.startDateExpr || 'startDate']
const endDate = appointmentData?.[schedulerOptions?.endDateExpr || 'endDate']
if (!startDate || !endDate) {
return ''
}
return `${formatDate(new Date(startDate), 'shortTime')} - ${formatDate(
new Date(endDate),
'shortTime',
)}`
},
[gridDto],
)
2026-07-30 19:38:27 +00:00
const getAppointmentPreviewDetails = useCallback(
(appointmentData: Record<string, any>) => {
const schedulerOptions = gridDto?.gridOptions.schedulerOptionDto
const userNameField =
schedulerOptions?.userNameExpr ??
gridDto?.columnFormats?.find((column) => column.fieldName?.toLowerCase() === 'username')
?.fieldName
const descriptionField =
schedulerOptions?.descriptionExpr ??
gridDto?.columnFormats?.find((column) => column.fieldName?.toLowerCase() === 'description')
?.fieldName
const userName = getValueByField(appointmentData, userNameField)
const description = getValueByField(appointmentData, descriptionField)
if (
(userName === null || userName === undefined) &&
(description === null || description === undefined)
) {
return undefined
}
return {
userName: userName === null || userName === undefined ? undefined : String(userName),
avatarUrl:
userName === null || userName === undefined
? undefined
: userAvatarUrls[String(userName).toLowerCase()],
description:
description === null || description === undefined ? undefined : String(description),
}
},
[gridDto, userAvatarUrls],
)
2026-07-29 10:04:07 +00:00
const renderAppointment = useCallback(
(data: any) => {
const appointmentData = data.targetedAppointmentData || data.appointmentData || {}
2026-07-30 19:38:27 +00:00
const previewDetails = getAppointmentPreviewDetails(appointmentData)
2026-07-29 10:04:07 +00:00
return (
2026-07-30 19:38:27 +00:00
<div className="flex h-full min-w-0 flex-col gap-1 overflow-hidden p-1">
{previewDetails?.userName && (
<div
className="flex min-w-0 items-center gap-1 self-start rounded-full bg-black/20 px-2 py-0.5 text-[10px] font-semibold"
title={`User Name: ${previewDetails.userName}`}
>
<Avatar
alt={previewDetails.userName}
className="shrink-0"
shape="circle"
size={18}
src={previewDetails.avatarUrl ?? '/img/others/default-profile.png'}
/>
<span className="truncate">{previewDetails.userName}</span>
</div>
)}
<div className="min-w-0">
<div className="flex items-center gap-1 text-[9px] font-semibold uppercase opacity-70">
<FaHeading className="shrink-0" />
2026-07-31 06:04:50 +00:00
<span>{translate('::App.Listform.ListformField.Text')}</span>
2026-07-30 19:38:27 +00:00
</div>
<div
className="dx-scheduler-appointment-title scheduler-appointment-text line-clamp-2 whitespace-normal font-semibold leading-tight"
dangerouslySetInnerHTML={{ __html: getAppointmentHtml(appointmentData) }}
/>
</div>
{previewDetails?.description && (
<div className="min-w-0">
<div className="flex items-center gap-1 text-[9px] font-semibold uppercase opacity-70">
<FaAlignLeft className="shrink-0" />
2026-07-31 06:04:50 +00:00
<span>{translate('::App.Platform.Description')}</span>
2026-07-30 19:38:27 +00:00
</div>
<div
className="line-clamp-2 whitespace-normal text-[11px] leading-tight"
title={previewDetails.description}
>
{previewDetails.description}
</div>
</div>
)}
<div className="dx-scheduler-appointment-content-date mt-auto flex items-center gap-1 border-t border-black/10 pt-1 text-[10px] font-medium">
<FaClock className="shrink-0" />
<span>{getAppointmentTimeText(appointmentData)}</span>
2026-07-29 10:04:07 +00:00
</div>
2026-07-30 19:38:27 +00:00
</div>
2026-07-29 10:04:07 +00:00
)
},
2026-07-30 19:38:27 +00:00
[getAppointmentHtml, getAppointmentPreviewDetails, getAppointmentTimeText],
2026-07-29 10:04:07 +00:00
)
const renderAppointmentTooltip = useCallback(
(data: any) => {
const appointmentData = data.targetedAppointmentData || data.appointmentData || {}
2026-07-30 19:38:27 +00:00
const previewDetails = getAppointmentPreviewDetails(appointmentData)
2026-07-29 10:04:07 +00:00
return (
2026-07-30 19:38:27 +00:00
<div className="scheduler-appointment-preview flex w-96 max-w-[calc(100vw-2rem)] items-start gap-3 p-4 text-gray-900 dark:text-gray-100">
<div className="min-w-0 flex-1 space-y-3 text-left">
{previewDetails?.userName && (
<div className="flex min-w-0 items-center gap-2">
<Avatar
alt={previewDetails.userName}
className="shrink-0 ring-2 ring-blue-100 dark:ring-blue-900/50"
shape="circle"
size={40}
src={previewDetails.avatarUrl ?? '/img/others/default-profile.png'}
/>
<div className="min-w-0">
<div className="text-[10px] font-semibold uppercase text-gray-500 dark:text-gray-400">
2026-07-31 06:04:50 +00:00
{translate('::App.Listform.ListformField.UserId')}
2026-07-30 19:38:27 +00:00
</div>
<div className="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
{previewDetails.userName}
</div>
</div>
</div>
)}
<div className="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
<div className="mb-1 flex items-center gap-1.5 text-[10px] font-semibold uppercase text-gray-500 dark:text-gray-400">
<FaHeading />
2026-07-31 06:04:50 +00:00
<span>{translate('::App.Listform.ListformField.Text')}</span>
2026-07-30 19:38:27 +00:00
</div>
<div
className="scheduler-appointment-preview-text text-base font-semibold leading-snug text-gray-900 dark:text-gray-100"
dangerouslySetInnerHTML={{ __html: getAppointmentHtml(appointmentData) }}
/>
</div>
{previewDetails?.description && (
<div className="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800">
<div className="mb-1 flex items-center gap-1.5 text-[10px] font-semibold uppercase text-gray-500 dark:text-gray-400">
<FaAlignLeft />
2026-07-31 06:04:50 +00:00
<span>{translate('::App.Platform.Description')}</span>
2026-07-30 19:38:27 +00:00
</div>
<div className="max-h-32 overflow-y-auto whitespace-pre-wrap pr-1 text-sm leading-relaxed text-gray-700 dark:text-gray-300">
{previewDetails.description}
</div>
</div>
)}
<div className="flex items-center gap-2 border-t border-gray-200 pt-2 text-xs font-medium text-gray-600 dark:border-gray-700 dark:text-gray-300">
<FaClock className="text-blue-600 dark:text-blue-400" />
<span>{getAppointmentTimeText(appointmentData)}</span>
</div>
2026-07-29 10:04:07 +00:00
</div>
{gridDto?.gridOptions.schedulerOptionDto?.allowDeleting && (
<button
type="button"
className="shrink-0 self-start dx-button dx-button-mode-text dx-widget dx-button-has-icon"
onClick={(event) => {
event.stopPropagation()
schedulerRef.current?.instance().deleteAppointment(data.appointmentData)
}}
>
<span className="dx-button-content">
<i className="dx-icon dx-icon-trash" />
</span>
</button>
)}
</div>
)
},
2026-07-30 19:38:27 +00:00
[getAppointmentHtml, getAppointmentPreviewDetails, getAppointmentTimeText, gridDto],
2026-07-29 10:04:07 +00:00
)
2026-02-24 20:44:16 +00:00
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'
2026-07-30 19:38:27 +00:00
if (isNewAppointment) {
const appointmentData = (e.appointmentData ?? {}) as Record<string, any>
const defaultValues: Record<string, any> = {}
const sequenceDefaults: Promise<void>[] = []
for (const colFormat of gridDto.columnFormats ?? []) {
const fieldName = colFormat.fieldName
if (
!fieldName ||
colFormat.defaultValue === null ||
colFormat.defaultValue === undefined
) {
continue
}
if (
typeof colFormat.defaultValue === 'string' &&
colFormat.defaultValue === '@AUTONUMBER'
) {
defaultValues[fieldName] = autoNumber()
} else if (colFormat.defaultValueType === FieldCustomValueTypeEnum.Sequence) {
sequenceDefaults.push(
getNextSequenceValue(String(colFormat.defaultValue))
.then((response) => {
const value = response.data
appointmentData[fieldName] = value
e.form.updateData(fieldName, value)
})
.catch((error) => {
console.error('Sequence default value alınamadı:', {
fieldName,
defaultValue: colFormat.defaultValue,
error,
})
}),
)
} else {
defaultValues[fieldName] = colFormat.defaultValue
}
}
Object.assign(appointmentData, defaultValues)
e.form.option('formData', {
...(e.form.option('formData') ?? {}),
...appointmentData,
})
void Promise.all(sequenceDefaults)
}
const popupOptions = gridDto.gridOptions.editingOptionDto?.popup
const popupFullScreen =
useMobileEditPopup || isPopupFullScreen || (popupOptions?.fullScreen ?? false)
// Popup ayarlarını Grid ile aynı ölçü ve cihaz kurallarıyla yapılandır.
e.popup.option({
animation: {},
deferRendering: true,
dragEnabled: popupOptions?.dragEnabled,
fullScreen: popupFullScreen,
height: getEditPopupHeight(useMobileEditPopup, popupFullScreen),
hideOnOutsideClick: popupOptions?.hideOnOutsideClick,
maxHeight: getEditPopupMaxHeight(useMobileEditPopup, popupFullScreen, popupOptions?.height),
maxWidth: useMobileEditPopup ? '100%' : popupOptions?.width,
position: useMobileEditPopup ? getMobilePopupPosition() : popupOptions?.position,
2026-07-30 19:38:27 +00:00
resizeEnabled: popupOptions?.resizeEnabled,
restorePosition: popupOptions?.restorePosition,
showTitle: popupOptions?.showTitle,
title: (currentMode === 'new' ? '✚ ' : '🖊️ ') + translate('::' + popupOptions?.title),
width: useMobileEditPopup ? '100%' : popupOptions?.width,
wrapperAttr: useMobileEditPopup
? { class: 'dx-scheduler-appointment-popup mobile-edit-popup' }
: undefined,
})
2026-02-24 20:44:16 +00:00
// 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: {
2026-07-30 19:38:27 +00:00
icon: popupFullScreen ? 'collapse' : 'fullscreen',
hint: popupFullScreen ? translate('::Normal Boyut') : translate('::Tam Ekran'),
2026-02-24 20:44:16 +00:00
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)
2026-07-30 19:38:27 +00:00
e.popup.option({
height: getEditPopupHeight(useMobileEditPopup, newFullScreenState),
maxHeight: getEditPopupMaxHeight(
useMobileEditPopup,
newFullScreenState,
popupOptions?.height,
),
maxWidth: useMobileEditPopup ? '100%' : popupOptions?.width,
width: useMobileEditPopup ? '100%' : popupOptions?.width,
})
2026-02-24 20:44:16 +00:00
},
},
},
])
// Grup düzenini EditingFormDto'dan, alanları ColumnFormatDto'dan oluştur.
2026-02-24 20:44:16 +00:00
const result: any[] = []
if (getEditingFormGroups(gridDto)?.length > 0) {
const sortedFormDto = getEditingFormGroups(gridDto)
2026-02-24 20:44:16 +00:00
.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: ColumnFormatDto) => {
2026-02-24 20:44:16 +00:00
let editorOptions: any = {}
try {
if (i.editorOptions) {
editorOptions = JSON.parse(i.editorOptions)
}
2026-07-16 08:19:17 +00:00
} catch (err) {
console.log(err)
}
2026-02-24 20:44:16 +00:00
const listFormField = i
const listFormColumn = listFormColumns.find(
(column) => column.dataField?.toLowerCase() === i.fieldName?.toLowerCase(),
)
2026-02-24 20:44:16 +00:00
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,
}
}
const lookup = listFormColumn?.lookup
if (lookup) {
editorOptions.dataSource =
typeof lookup.dataSource === 'function'
? lookup.dataSource({ data: e.appointmentData ?? {} })
: lookup.dataSource
editorOptions.valueExpr = lookup.valueExpr
editorOptions.displayExpr = lookup.displayExpr
2026-02-24 20:44:16 +00:00
}
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,
dataField: i.fieldName,
name: i.fieldName,
2026-02-24 20:44:16 +00:00
editorType2: i.editorType2,
editorType: resolveEditorType(i.editorType2),
2026-02-24 20:44:16 +00:00
colSpan: i.colSpan,
editorOptions,
editorScript: i.editorScript,
2026-07-30 19:38:27 +00:00
isRequired:
listFormField.validationRuleDto?.some((rule) => rule.type === 'required') ?? false,
validationRules: (listFormField.validationRuleDto ?? []) as ValidationRule[],
2026-02-24 20:44:16 +00:00
}
2026-07-29 10:04:07 +00:00
if (i.captionName) {
item.label = { text: translate('::' + i.captionName) }
} else if (i.fieldName?.indexOf(':') >= 0) {
item.label = { text: captionize(i.fieldName.split(':')[1]) }
2026-02-24 20:44:16 +00:00
}
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
}
2026-07-29 10:04:07 +00:00
const isVisibleFormItem = (item: SimpleItemWithColData) => {
if (currentMode === 'new') {
return item.canCreate && item.allowAdding
}
if (currentMode === 'edit') {
return item.canUpdate && item.allowEditing
}
return false
}
2026-02-24 20:44:16 +00:00
sortedFormDto.forEach((e: any) => {
// Items'ları da order'a göre sırala
const sortedItems = (e.items || [])
.slice()
2026-07-29 10:04:07 +00:00
.sort((a: any, b: any) => ((a.editOrderNo ?? 0) >= (b.editOrderNo ?? 0) ? 1 : -1))
2026-02-24 20:44:16 +00:00
2026-07-29 10:04:07 +00:00
const groupItems = sortedItems.map(mapFormItem).filter(isVisibleFormItem)
2026-02-24 20:44:16 +00:00
if (e.itemType !== 'tabbed') {
2026-07-30 19:38:27 +00:00
result.push({
itemType: e.itemType,
colCount: e.colCount || 1,
colSpan: e.colSpan || 1,
caption: e.caption,
items: groupItems,
})
2026-02-24 20:44:16 +00:00
} else if (e.itemType === 'tabbed' && tabbedItems.length > 0 && e === tabbedItems[0]) {
result.push({
itemType: 'tabbed',
2026-07-30 19:38:27 +00:00
colCount: 1,
2026-02-24 20:44:16 +00:00
colSpan: 1,
tabs: tabbedItems.map((tabbedItem: any) => {
2026-07-30 19:38:27 +00:00
const effectiveColCount = tabbedItem.colCount || 1
2026-02-24 20:44:16 +00:00
return {
2026-07-30 19:38:27 +00:00
title: tabbedItem.caption,
2026-02-24 20:44:16 +00:00
colCount: effectiveColCount,
items: tabbedItem.items
?.sort((a: any, b: any) =>
(a.editOrderNo ?? 0) >= (b.editOrderNo ?? 0) ? 1 : -1,
)
2026-02-24 20:44:16 +00:00
.map(mapFormItem)
2026-07-29 10:04:07 +00:00
.filter(isVisibleFormItem),
2026-02-24 20:44:16 +00:00
}
}),
})
}
})
}
2026-07-30 19:38:27 +00:00
e.form.option('colCount', 1)
2026-02-24 20:44:16 +00:00
e.form.option('showValidationSummary', false)
e.form.option('items', result)
2026-07-30 19:38:27 +00:00
},
[gridDto, translate, isPopupFullScreen, listFormColumns, useMobileEditPopup],
2026-07-30 19:38:27 +00:00
)
2026-02-24 20:44:16 +00:00
2026-07-30 19:38:27 +00:00
const configuredEditPopup = gridDto?.gridOptions.editingOptionDto?.popup
const schedulerPopupFullScreen =
useMobileEditPopup || isPopupFullScreen || (configuredEditPopup?.fullScreen ?? false)
const schedulerPopupWidth = useMobileEditPopup ? '100%' : configuredEditPopup?.width
const schedulerEditingPopup = useMemo(
() =>
configuredEditPopup
? {
animation: {},
deferRendering: true,
dragEnabled: configuredEditPopup.dragEnabled,
fullScreen: schedulerPopupFullScreen,
height: getEditPopupHeight(useMobileEditPopup, schedulerPopupFullScreen),
hideOnOutsideClick: configuredEditPopup.hideOnOutsideClick,
maxHeight: getEditPopupMaxHeight(
useMobileEditPopup,
schedulerPopupFullScreen,
configuredEditPopup.height,
),
maxWidth: schedulerPopupWidth,
position: useMobileEditPopup
? getMobilePopupPosition()
2026-07-30 19:38:27 +00:00
: configuredEditPopup.position,
resizeEnabled: configuredEditPopup.resizeEnabled,
restorePosition: configuredEditPopup.restorePosition,
width: schedulerPopupWidth,
wrapperAttr: useMobileEditPopup
? { class: 'dx-scheduler-appointment-popup mobile-edit-popup' }
: undefined,
onShowing: (event: any) => {
event.component.option({
fullScreen: schedulerPopupFullScreen,
height: getEditPopupHeight(useMobileEditPopup, schedulerPopupFullScreen),
maxHeight: getEditPopupMaxHeight(
useMobileEditPopup,
schedulerPopupFullScreen,
configuredEditPopup.height,
),
maxWidth: schedulerPopupWidth,
width: schedulerPopupWidth,
})
},
2026-05-13 11:31:15 +00:00
}
2026-07-30 19:38:27 +00:00
: undefined,
[configuredEditPopup, schedulerPopupFullScreen, schedulerPopupWidth, useMobileEditPopup],
2026-02-24 20:44:16 +00:00
)
2026-07-30 19:38:27 +00:00
const schedulerDescriptionExpr =
gridDto?.gridOptions.schedulerOptionDto?.descriptionExpr ??
gridDto?.columnFormats?.find((column) => column.fieldName?.toLowerCase() === 'description')
?.fieldName
2026-02-24 20:44:16 +00:00
return (
<>
2026-06-18 14:03:22 +00:00
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
2026-02-24 20:44:16 +00:00
<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}
2026-07-30 14:37:15 +00:00
className="list-view-toolbar-host"
2026-02-24 20:44:16 +00:00
dataSource={schedulerDataSource}
2026-05-13 11:31:15 +00:00
dateSerializationFormat="yyyy-MM-ddTHH:mm:ss"
2026-07-30 19:38:27 +00:00
descriptionExpr={schedulerDescriptionExpr}
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}
2026-07-29 10:04:07 +00:00
endDayHour={gridDto.gridOptions.schedulerOptionDto?.endDayHour || 20}
2026-02-24 20:44:16 +00:00
currentView={currentView}
onCurrentViewChange={onCurrentViewChange}
onAppointmentFormOpening={onAppointmentFormOpening}
2026-07-29 10:04:07 +00:00
appointmentRender={renderAppointment}
appointmentTooltipRender={renderAppointmentTooltip}
2026-02-24 20:44:16 +00:00
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
}
2026-07-29 10:04:07 +00:00
adaptivityEnabled={false}
2026-02-24 20:44:16 +00:00
>
<Toolbar>
<Item name="dateNavigator" />
<Item name="today" />
<Item name="viewSwitcher" />
<Item
2026-07-29 10:04:07 +00:00
cssClass="scheduler-toolbar-action"
2026-02-24 20:44:16 +00:00
location="after"
widget="dxButton"
2026-07-29 10:04:07 +00:00
locateInMenu="auto"
showText="always"
2026-02-24 20:44:16 +00:00
options={{
icon: 'refresh',
2026-07-29 10:04:07 +00:00
text: translate('::ListForms.ListForm.Refresh'),
2026-02-24 20:44:16 +00:00
hint: translate('::ListForms.ListForm.Refresh'),
onClick: handleRefresh,
}}
/>
{checkPermission('App.Listforms.Listform.Update') && (
2026-02-24 20:44:16 +00:00
<Item
2026-07-29 10:04:07 +00:00
cssClass="scheduler-toolbar-action"
2026-02-24 20:44:16 +00:00
location="after"
widget="dxButton"
2026-07-29 10:04:07 +00:00
locateInMenu="auto"
showText="always"
2026-02-24 20:44:16 +00:00
options={{
icon: 'preferences',
2026-07-29 10:04:07 +00:00
text: translate('::ListForms.ListForm.Manage'),
hint: translate('::ListForms.ListForm.Manage'),
2026-02-24 20:44:16 +00:00
onClick: settingButtonClick,
}}
/>
)}
</Toolbar>
<Editing
allowAdding={gridDto.gridOptions.schedulerOptionDto?.allowAdding ?? false}
allowDeleting={gridDto.gridOptions.schedulerOptionDto?.allowDeleting ?? false}
allowDragging={gridDto.gridOptions.schedulerOptionDto?.allowDragging ?? false}
2026-07-30 19:38:27 +00:00
allowResizing={gridDto.gridOptions.schedulerOptionDto?.allowResizing ?? false}
allowUpdating={gridDto.gridOptions.schedulerOptionDto?.allowEditing ?? false}
form={{ colCount: 1 }}
popup={schedulerEditingPopup}
2026-02-24 20:44:16 +00:00
/>
2026-07-29 10:04:07 +00:00
<View
type="day"
name={translate('::App.Listform.ListformField.Day')}
maxAppointmentsPerCell="unlimited"
/>
<View
type="week"
name={translate('::ListForms.SchedulerOptions.Week')}
maxAppointmentsPerCell="unlimited"
/>
<View
type="workWeek"
name={translate('::ListForms.SchedulerOptions.WorkWeek')}
maxAppointmentsPerCell="unlimited"
/>
2026-02-24 20:44:16 +00:00
<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