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 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() const widgetGroupRef = useRef(null) const { checkPermission } = usePermission() const [schedulerDataSource, setSchedulerDataSource] = useState>() const gridDto = useListFormGridDto(listFormCode, extGridDto) const [currentView, setCurrentView] = useState('week') const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const [userAvatarUrls, setUserAvatarUrls] = useState>({}) 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>( (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) => { const textExpr = gridDto?.gridOptions.schedulerOptionDto?.textExpr || 'text' return String(appointmentData?.[textExpr] ?? '') }, [gridDto], ) const getAppointmentTimeText = useCallback( (appointmentData: Record) => { 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) => { 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 (
{previewDetails?.userName && (
{previewDetails.userName}
)}
{translate('::App.Listform.ListformField.Text')}
{previewDetails?.description && (
{translate('::App.Platform.Description')}
{previewDetails.description}
)}
{getAppointmentTimeText(appointmentData)}
) }, [getAppointmentHtml, getAppointmentPreviewDetails, getAppointmentTimeText], ) const renderAppointmentTooltip = useCallback( (data: any) => { const appointmentData = data.targetedAppointmentData || data.appointmentData || {} const previewDetails = getAppointmentPreviewDetails(appointmentData) return (
{previewDetails?.userName && (
{translate('::App.Listform.ListformField.UserId')}
{previewDetails.userName}
)}
{translate('::App.Listform.ListformField.Text')}
{previewDetails?.description && (
{translate('::App.Platform.Description')}
{previewDetails.description}
)}
{getAppointmentTimeText(appointmentData)}
{gridDto?.gridOptions.schedulerOptionDto?.allowDeleting && ( )}
) }, [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 const defaultValues: Record = {} const sequenceDefaults: Promise[] = [] 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 ( <>
0 ? 'mt-2' : ''}>
{!isSubForm && ( )} {!gridDto && (
Loading scheduler configuration...
)} {gridDto && !schedulerDataSource && (
Loading data source...
)} {gridDto && schedulerDataSource && ( <>
{ 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} > {checkPermission('App.Listforms.Listform.Update') && ( )} {gridDto.gridOptions.schedulerOptionDto?.resources?.map((resource, index) => ( ))}
)}
) } export default SchedulerView