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 {
|
2026-07-17 08:59:51 +00:00
|
|
|
|
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'
|
2026-08-06 22:54:50 +00:00
|
|
|
|
import { getNextSequenceValue } from '@/services/form.service'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
2026-08-06 22:54:50 +00:00
|
|
|
|
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'
|
2026-07-31 19:39:52 +00:00
|
|
|
|
import { useListFormColumns } from './useListFormColumns'
|
2026-08-06 22:54:50 +00:00
|
|
|
|
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()
|
2026-08-06 22:54:50 +00:00
|
|
|
|
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>>()
|
2026-08-06 22:54:50 +00:00
|
|
|
|
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'
|
|
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
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(() => {
|
2026-08-06 22:54:50 +00:00
|
|
|
|
setCurrentView(gridDto?.gridOptions.schedulerOptionDto?.defaultView || 'week')
|
|
|
|
|
|
}, [gridDto])
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
2026-08-06 22:54:50 +00:00
|
|
|
|
// listFormCode değişiminde eski veri kaynağını temizle.
|
2026-02-24 20:44:16 +00:00
|
|
|
|
useEffect(() => {
|
2026-08-06 22:54:50 +00:00
|
|
|
|
schedulerRef.current?.instance()?.option('dataSource', undefined)
|
2026-02-24 20:44:16 +00:00
|
|
|
|
}, [listFormCode])
|
|
|
|
|
|
|
|
|
|
|
|
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: schedulerRef })
|
2026-07-31 19:39:52 +00:00
|
|
|
|
const { getBandedColumns } = useListFormColumns({
|
|
|
|
|
|
gridDto,
|
|
|
|
|
|
listFormCode,
|
|
|
|
|
|
isSubForm,
|
|
|
|
|
|
gridRef: schedulerRef,
|
|
|
|
|
|
})
|
2026-08-06 22:54:50 +00:00
|
|
|
|
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,
|
2026-08-06 22:54:50 +00:00
|
|
|
|
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
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
])
|
|
|
|
|
|
|
2026-07-17 08:59:51 +00:00
|
|
|
|
// Grup düzenini EditingFormDto'dan, alanları ColumnFormatDto'dan oluştur.
|
2026-02-24 20:44:16 +00:00
|
|
|
|
const result: any[] = []
|
|
|
|
|
|
|
2026-07-17 08:59:51 +00:00
|
|
|
|
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
|
2026-07-17 08:59:51 +00:00
|
|
|
|
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
|
|
|
|
|
2026-07-17 08:59:51 +00:00
|
|
|
|
const listFormField = i
|
2026-07-31 19:39:52 +00:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 19:39:52 +00:00
|
|
|
|
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,
|
2026-07-17 08:59:51 +00:00
|
|
|
|
dataField: i.fieldName,
|
|
|
|
|
|
name: i.fieldName,
|
2026-02-24 20:44:16 +00:00
|
|
|
|
editorType2: i.editorType2,
|
2026-08-06 22:54:50 +00:00
|
|
|
|
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) {
|
2026-07-17 08:59:51 +00:00
|
|
|
|
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
|
2026-07-17 08:59:51 +00:00
|
|
|
|
?.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
|
|
|
|
},
|
2026-07-31 19:39:52 +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
|
2026-08-06 22:54:50 +00:00
|
|
|
|
? 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,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-06-19 14:15:46 +00:00
|
|
|
|
{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
|