sozsoft-platform/ui/src/views/list/SchedulerView.tsx
2026-08-07 01:54:50 +03:00

928 lines
35 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 '@/utils/registerDevExtremeEditors'
import { APP_NAME, AVATAR_URL, DX_CLASSNAMES } from '@/constants/app.constant'
import {
ColumnFormatDto,
DbTypeEnum,
FieldCustomValueTypeEnum,
GridDto,
} from '@/proxy/form/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Scheduler, {
Editing,
Item,
Resource,
type SchedulerRef,
Toolbar,
View,
} from 'devextreme-react/scheduler'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { getNextSequenceValue } from '@/services/form.service'
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
import { autoNumber, getEditingFormGroups } 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 { SimpleItemWithColData } from '../form/types'
import { captionize } from 'devextreme/core/utils/inflector'
import { formatDate } from 'devextreme/localization'
import type { AppointmentFormOpeningEvent } from 'devextreme/ui/scheduler'
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'
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()
const schedulerRef = useRef<SchedulerRef>()
const widgetGroupRef = useRef<HTMLDivElement>(null)
const { checkPermission } = usePermission()
const [schedulerDataSource, setSchedulerDataSource] = useState<CustomStore<any, any>>()
const gridDto = useListFormGridDto(listFormCode, extGridDto)
const [currentView, setCurrentView] = useState<string>('week')
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
const [userAvatarUrls, setUserAvatarUrls] = useState<Record<string, string>>({})
const layout = layoutTypes.scheduler || 'scheduler'
const widgetGroupHeight = useWidgetGroupHeight(widgetGroupRef, gridDto?.widgets)
useListFormCustomSources(gridDto)
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
}
}, [])
useEffect(() => {
setCurrentView(gridDto?.gridOptions.schedulerOptionDto?.defaultView || 'week')
}, [gridDto])
// listFormCode değişiminde eski veri kaynağını temizle.
useEffect(() => {
schedulerRef.current?.instance()?.option('dataSource', undefined)
}, [listFormCode])
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: schedulerRef })
const { getBandedColumns } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
gridRef: schedulerRef,
})
const listFormColumns = useMemo(() => flattenGridColumns(getBandedColumns()), [getBandedColumns])
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)
}, [])
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],
)
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],
)
const renderAppointment = useCallback(
(data: any) => {
const appointmentData = data.targetedAppointmentData || data.appointmentData || {}
const previewDetails = getAppointmentPreviewDetails(appointmentData)
return (
<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" />
<span>{translate('::App.Listform.ListformField.Text')}</span>
</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" />
<span>{translate('::App.Platform.Description')}</span>
</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>
</div>
</div>
)
},
[getAppointmentHtml, getAppointmentPreviewDetails, getAppointmentTimeText],
)
const renderAppointmentTooltip = useCallback(
(data: any) => {
const appointmentData = data.targetedAppointmentData || data.appointmentData || {}
const previewDetails = getAppointmentPreviewDetails(appointmentData)
return (
<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">
{translate('::App.Listform.ListformField.UserId')}
</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 />
<span>{translate('::App.Listform.ListformField.Text')}</span>
</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 />
<span>{translate('::App.Platform.Description')}</span>
</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>
</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>
)
},
[getAppointmentHtml, getAppointmentPreviewDetails, getAppointmentTimeText, gridDto],
)
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'
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,
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,
})
// 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: popupFullScreen ? 'collapse' : 'fullscreen',
hint: popupFullScreen ? translate('::Normal Boyut') : 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)
e.popup.option({
height: getEditPopupHeight(useMobileEditPopup, newFullScreenState),
maxHeight: getEditPopupMaxHeight(
useMobileEditPopup,
newFullScreenState,
popupOptions?.height,
),
maxWidth: useMobileEditPopup ? '100%' : popupOptions?.width,
width: useMobileEditPopup ? '100%' : popupOptions?.width,
})
},
},
},
])
// Grup düzenini EditingFormDto'dan, alanları ColumnFormatDto'dan oluştur.
const result: any[] = []
if (getEditingFormGroups(gridDto)?.length > 0) {
const sortedFormDto = getEditingFormGroups(gridDto)
.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) => {
let editorOptions: any = {}
try {
if (i.editorOptions) {
editorOptions = JSON.parse(i.editorOptions)
}
} catch (err) {
console.log(err)
}
const listFormField = i
const listFormColumn = listFormColumns.find(
(column) => column.dataField?.toLowerCase() === i.fieldName?.toLowerCase(),
)
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
}
const item: SimpleItemWithColData = {
canRead: listFormField?.canRead ?? false,
canUpdate: listFormField?.canUpdate ?? false,
canCreate: listFormField?.canCreate ?? false,
canExport: listFormField?.canExport ?? false,
allowEditing: listFormField?.allowEditing ?? true,
allowAdding: listFormField?.allowAdding ?? true,
dataField: i.fieldName,
name: i.fieldName,
editorType2: i.editorType2,
editorType: resolveEditorType(i.editorType2),
colSpan: i.colSpan,
editorOptions,
editorScript: i.editorScript,
isRequired:
listFormField.validationRuleDto?.some((rule) => rule.type === 'required') ?? false,
validationRules: (listFormField.validationRuleDto ?? []) as ValidationRule[],
}
if (i.captionName) {
item.label = { text: translate('::' + i.captionName) }
} else if (i.fieldName?.indexOf(':') >= 0) {
item.label = { text: captionize(i.fieldName.split(':')[1]) }
}
if (
(currentMode == 'edit' && !item.canUpdate) ||
(currentMode == 'new' && !item.canCreate)
) {
item.editorOptions = {
...item.editorOptions,
readOnly: true,
}
}
return item
}
const isVisibleFormItem = (item: SimpleItemWithColData) => {
if (currentMode === 'new') {
return item.canCreate && item.allowAdding
}
if (currentMode === 'edit') {
return item.canUpdate && item.allowEditing
}
return false
}
sortedFormDto.forEach((e: any) => {
// Items'ları da order'a göre sırala
const sortedItems = (e.items || [])
.slice()
.sort((a: any, b: any) => ((a.editOrderNo ?? 0) >= (b.editOrderNo ?? 0) ? 1 : -1))
const groupItems = sortedItems.map(mapFormItem).filter(isVisibleFormItem)
if (e.itemType !== 'tabbed') {
result.push({
itemType: e.itemType,
colCount: e.colCount || 1,
colSpan: e.colSpan || 1,
caption: e.caption,
items: groupItems,
})
} else if (e.itemType === 'tabbed' && tabbedItems.length > 0 && e === tabbedItems[0]) {
result.push({
itemType: 'tabbed',
colCount: 1,
colSpan: 1,
tabs: tabbedItems.map((tabbedItem: any) => {
const effectiveColCount = tabbedItem.colCount || 1
return {
title: tabbedItem.caption,
colCount: effectiveColCount,
items: tabbedItem.items
?.sort((a: any, b: any) =>
(a.editOrderNo ?? 0) >= (b.editOrderNo ?? 0) ? 1 : -1,
)
.map(mapFormItem)
.filter(isVisibleFormItem),
}
}),
})
}
})
}
e.form.option('colCount', 1)
e.form.option('showValidationSummary', false)
e.form.option('items', result)
},
[gridDto, translate, isPopupFullScreen, listFormColumns, useMobileEditPopup],
)
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()
: 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,
})
},
}
: undefined,
[configuredEditPopup, schedulerPopupFullScreen, schedulerPopupWidth, useMobileEditPopup],
)
const schedulerDescriptionExpr =
gridDto?.gridOptions.schedulerOptionDto?.descriptionExpr ??
gridDto?.columnFormats?.find((column) => column.fieldName?.toLowerCase() === 'description')
?.fieldName
return (
<>
<div ref={widgetGroupRef} className={(gridDto?.widgets?.length ?? 0) > 0 ? 'mt-2' : ''}>
<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}
className="list-view-toolbar-host"
dataSource={schedulerDataSource}
dateSerializationFormat="yyyy-MM-ddTHH:mm:ss"
descriptionExpr={schedulerDescriptionExpr}
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 || 20}
currentView={currentView}
onCurrentViewChange={onCurrentViewChange}
onAppointmentFormOpening={onAppointmentFormOpening}
appointmentRender={renderAppointment}
appointmentTooltipRender={renderAppointmentTooltip}
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={false}
>
<Toolbar>
<Item name="dateNavigator" />
<Item name="today" />
<Item name="viewSwitcher" />
<Item
cssClass="scheduler-toolbar-action"
location="after"
widget="dxButton"
locateInMenu="auto"
showText="always"
options={{
icon: 'refresh',
text: translate('::ListForms.ListForm.Refresh'),
hint: translate('::ListForms.ListForm.Refresh'),
onClick: handleRefresh,
}}
/>
{checkPermission('App.Listforms.Listform.Update') && (
<Item
cssClass="scheduler-toolbar-action"
location="after"
widget="dxButton"
locateInMenu="auto"
showText="always"
options={{
icon: 'preferences',
text: translate('::ListForms.ListForm.Manage'),
hint: translate('::ListForms.ListForm.Manage'),
onClick: settingButtonClick,
}}
/>
)}
</Toolbar>
<Editing
allowAdding={gridDto.gridOptions.schedulerOptionDto?.allowAdding ?? false}
allowDeleting={gridDto.gridOptions.schedulerOptionDto?.allowDeleting ?? false}
allowDragging={gridDto.gridOptions.schedulerOptionDto?.allowDragging ?? false}
allowResizing={gridDto.gridOptions.schedulerOptionDto?.allowResizing ?? false}
allowUpdating={gridDto.gridOptions.schedulerOptionDto?.allowEditing ?? false}
form={{ colCount: 1 }}
popup={schedulerEditingPopup}
/>
<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"
/>
<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