import Container from '@/components/shared/Container' import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant' import { DbTypeEnum, GridDto, PlatformEditorTypes, UiLookupDataSourceTypeEnum, } from '@/proxy/form/models' import { useLocalization } from '@/utils/hooks/useLocalization' import Scheduler, { Editing, Item, Resource, SchedulerRef, Toolbar, View, } from 'devextreme-react/scheduler' import { useCallback, useEffect, useRef, useState } from 'react' import { Helmet } from 'react-helmet' import { getList } from '@/services/form.service' import { useListFormCustomDataSource } from './useListFormCustomDataSource' import { addCss, addJs, autoNumber } from './Utils' import { layoutTypes } from '../admin/listForm/edit/types' import WidgetGroup from '@/components/ui/Widget/WidgetGroup' import { ROUTES_ENUM } from '@/routes/route.constant' import { usePWA } from '@/utils/hooks/usePWA' import CustomStore from 'devextreme/data/custom_store' import { Loading } from '@/components/shared' import { usePermission } from '@/utils/hooks/usePermission' import { EditingFormItemDto } from '@/proxy/form/models' import { RowMode, SimpleItemWithColData } from '../form/types' import { captionize } from 'devextreme/core/utils/inflector' import { AppointmentFormOpeningEvent } from 'devextreme/ui/scheduler' interface SchedulerViewProps { listFormCode: string searchParams?: URLSearchParams isSubForm?: boolean level?: number refreshData?: () => Promise gridDto?: GridDto } const SchedulerView = (props: SchedulerViewProps) => { const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props const { translate } = useLocalization() const isPwaMode = usePWA() const [mode, setMode] = useState('view') const schedulerRef = useRef() const refListFormCode = useRef('') const widgetGroupRef = useRef(null) const { checkPermission } = usePermission() const [schedulerDataSource, setSchedulerDataSource] = useState>() const [gridDto, setGridDto] = useState() const [widgetGroupHeight, setWidgetGroupHeight] = useState(0) const [currentView, setCurrentView] = useState('week') const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const layout = layoutTypes.scheduler || 'scheduler' useEffect(() => { const initializeScheduler = async () => { const response = await getList({ listFormCode }) setGridDto(response.data) } if (extGridDto === undefined) { initializeScheduler() } else { setGridDto(extGridDto) } setCurrentView(extGridDto?.gridOptions.schedulerOptionDto?.defaultView || 'week') }, [listFormCode, extGridDto]) useEffect(() => { if (schedulerRef?.current) { const instance = schedulerRef?.current?.instance() if (instance) { instance.option('dataSource', undefined) } } if (refListFormCode.current !== listFormCode) { // Reset state if needed } }, [listFormCode]) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: schedulerRef }) useEffect(() => { if (!gridDto) { return } // Set js and css const grdOpt = gridDto.gridOptions if (grdOpt.customJsSources.length) { for (const js of grdOpt.customJsSources) { addJs(js) } } if (grdOpt.customStyleSources.length) { for (const css of grdOpt.customStyleSources) { addCss(css) } } }, [gridDto]) useEffect(() => { if (!gridDto) return const dataSource = createSelectDataSource( gridDto.gridOptions, listFormCode, searchParams, layout, undefined, ) setSchedulerDataSource(dataSource) }, [gridDto, searchParams, createSelectDataSource]) useEffect(() => { refListFormCode.current = listFormCode }, [listFormCode]) // WidgetGroup yüksekliğini hesapla useEffect(() => { const calculateWidgetHeight = () => { if (widgetGroupRef.current) { const height = widgetGroupRef.current.offsetHeight setWidgetGroupHeight(height) } } calculateWidgetHeight() const resizeObserver = new ResizeObserver(calculateWidgetHeight) if (widgetGroupRef.current) { resizeObserver.observe(widgetGroupRef.current) } return () => { resizeObserver.disconnect() } }, [gridDto?.widgets]) const settingButtonClick = useCallback(() => { window.open( ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(':listFormCode', listFormCode), isPwaMode ? '_self' : '_blank', ) }, [listFormCode, isPwaMode]) const handleRefresh = useCallback(() => { const instance = schedulerRef.current?.instance() if (instance) { const dataSource = instance.getDataSource() if (dataSource) { dataSource.reload() } } }, []) const onCurrentViewChange = useCallback((value: string) => { setCurrentView(value) }, []) const onAppointmentFormOpening = useCallback( (e: AppointmentFormOpeningEvent) => { if (!gridDto) return // Yeni appointment mı yoksa düzenleme mi kontrol et const isNewAppointment = !e.appointmentData || (!e.appointmentData.id && !e.appointmentData[gridDto.gridOptions.keyFieldName || 'id']) const currentMode = isNewAppointment ? 'new' : 'edit' setMode(currentMode) // Popup ayarlarını her açılışta yeniden yapılandır e.popup.option( 'title', (currentMode === 'new' ? '✚ ' : '🖊️ ') + translate('::' + gridDto.gridOptions.editingOptionDto?.popup?.title), ) e.popup.option('showTitle', gridDto.gridOptions.editingOptionDto?.popup?.showTitle) e.popup.option('width', gridDto.gridOptions.editingOptionDto?.popup?.width) e.popup.option('height', gridDto.gridOptions.editingOptionDto?.popup?.height) e.popup.option('resizeEnabled', gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled) e.popup.option('fullScreen', isPopupFullScreen) // Toolbar butonlarını ekle e.popup.option('toolbarItems', [ { widget: 'dxButton', toolbar: 'bottom', location: 'after', options: { text: translate('::Save'), type: 'default', onClick: async () => { const formInstance = e.form const validationResult = formInstance.validate() if (validationResult.isValid) { // Form verilerini al const formData = formInstance.option('formData') try { // Scheduler instance'ını al const scheduler = schedulerRef.current?.instance() if (e.appointmentData && scheduler) { // Yeni appointment mı yoksa güncelleme mi kontrol et if ( e.appointmentData.id || e.appointmentData[gridDto.gridOptions.keyFieldName || 'id'] ) { // Güncelleme await scheduler.updateAppointment(e.appointmentData, formData) } else { // Yeni ekleme await scheduler.addAppointment(formData) } // Popup'ı kapat e.popup.hide() // RefreshData varsa çağır if (props.refreshData) { await props.refreshData() } } } catch (error) { console.error('Appointment save error:', error) } } }, }, }, { widget: 'dxButton', toolbar: 'bottom', location: 'after', options: { text: translate('::Cancel'), onClick: () => { e.cancel = true e.popup.hide() }, }, }, { widget: 'dxButton', toolbar: 'top', location: 'after', options: { icon: 'fullscreen', hint: translate('::Tam Ekran'), stylingMode: 'text', onClick: () => { // Popup'tan mevcut fullScreen durumunu al const currentFullScreen = e.popup.option('fullScreen') const newFullScreenState = !currentFullScreen // State'i güncelle setIsPopupFullScreen(newFullScreenState) // Popup'ı güncelle e.popup.option('fullScreen', newFullScreenState) // Width ve height'i da güncelle if (newFullScreenState) { e.popup.option('width', '100%') e.popup.option('height', '100%') } else { e.popup.option('width', gridDto.gridOptions.editingOptionDto?.popup?.width || 600) e.popup.option( 'height', gridDto.gridOptions.editingOptionDto?.popup?.height || 'auto', ) } }, }, }, ]) // EditingFormDto'dan form items oluştur const result: any[] = [] if (gridDto.gridOptions.editingFormDto?.length > 0) { const sortedFormDto = gridDto.gridOptions.editingFormDto .slice() .sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) // Tüm tabbed items'ları topla (Grid'deki gibi) const tabbedItems = sortedFormDto.filter((e: any) => e.itemType === 'tabbed') // Ortak item mapper fonksiyonu - hem group hem tab için kullanılır const mapFormItem = (i: EditingFormItemDto) => { let editorOptions: any = {} try { if (i.editorOptions) { editorOptions = JSON.parse(i.editorOptions) } } catch {} const fieldName = i.dataField.split(':')[0] const listFormField = gridDto.columnFormats.find((x: any) => x.fieldName === fieldName) if (listFormField?.sourceDbType === DbTypeEnum.Date) { editorOptions = { ...{ type: 'date', dateSerializationFormat: 'yyyy-MM-dd', displayFormat: 'shortDate', }, ...editorOptions, } } else if ( listFormField?.sourceDbType === DbTypeEnum.DateTime || listFormField?.sourceDbType === DbTypeEnum.DateTime2 || listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset ) { editorOptions = { ...{ type: 'datetime', dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss', displayFormat: 'shortDateShortTime', }, ...editorOptions, } } // Set defaultValue for @AUTONUMBER fields if ( typeof listFormField?.defaultValue === 'string' && listFormField?.defaultValue === '@AUTONUMBER' && currentMode === 'new' ) { editorOptions = { ...editorOptions, value: autoNumber(), } } // EditorType belirleme let editorType: any = i.editorType2 || i.editorType if (i.editorType2 === PlatformEditorTypes.dxGridBox) { editorType = 'dxDropDownBox' } else if (i.editorType2) { editorType = i.editorType2 } // Lookup DataSource oluştur if (listFormField?.lookupDto) { const lookup = listFormField.lookupDto if (lookup.dataSourceType === UiLookupDataSourceTypeEnum.Query) { editorOptions.dataSource = new CustomStore({ key: 'key', loadMode: 'raw', load: async () => { try { const { dynamicFetch } = await import('@/services/form.service') const response = await dynamicFetch('list-form-select/lookup', 'POST', null, { listFormCode, listFormFieldName: fieldName, filters: [], }) return (response.data ?? []).map((a: any) => ({ key: a.Key, name: a.Name, group: a.Group, })) } catch (error) { console.error('Lookup load error:', error) return [] } }, }) editorOptions.valueExpr = 'key' editorOptions.displayExpr = 'name' } else if (lookup.dataSourceType === UiLookupDataSourceTypeEnum.StaticData) { if (lookup.lookupQuery) { try { const staticData = JSON.parse(lookup.lookupQuery) editorOptions.dataSource = staticData editorOptions.valueExpr = lookup.valueExpr || 'key' editorOptions.displayExpr = lookup.displayExpr || 'name' } catch (error) { console.error('Static data parse error:', error) } } } } const item: SimpleItemWithColData = { canRead: listFormField?.canRead ?? false, canUpdate: listFormField?.canUpdate ?? false, canCreate: listFormField?.canCreate ?? false, canExport: listFormField?.canExport ?? false, dataField: i.dataField, name: i.dataField, editorType2: i.editorType2, editorType: i.editorType2 == PlatformEditorTypes.dxGridBox ? 'dxDropDownBox' : i.editorType2, colSpan: i.colSpan, isRequired: i.isRequired, editorOptions, editorScript: i.editorScript, } if (i.dataField.indexOf(':') >= 0) { item.label = { text: captionize(i.dataField.split(':')[1]) } } if ((currentMode == 'edit' && !item.canUpdate) || (currentMode == 'new' && !item.canCreate)) { item.editorOptions = { ...item.editorOptions, readOnly: true, } } return item } sortedFormDto.forEach((e: any) => { // Items'ları da order'a göre sırala const sortedItems = (e.items || []) .slice() .sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) const groupItems = sortedItems.map(mapFormItem) if (e.itemType !== 'tabbed') { // Grup kullanmadan direkt items'ları ekle - form'un colCount'u geçerli olsun result.push(...groupItems) } else if (e.itemType === 'tabbed' && tabbedItems.length > 0 && e === tabbedItems[0]) { // Tabbed için caption OLMAMALI - sadece tabs array içinde title kullan result.push({ itemType: 'tabbed', // colCount tabbed item için OLMAMALI - sadece colSpan colSpan: 1, // caption kullanma! Tabs içindeki title'lar yeterli tabs: tabbedItems.map((tabbedItem: any) => { // Backend'den gelen colCount ve colSpan değerlerini kullan const effectiveColCount = tabbedItem.colCount || 2 return { title: tabbedItem.caption, // Her tab'ın title'ı colCount: effectiveColCount, items: tabbedItem.items ?.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) .map(mapFormItem) .filter((a: any) => { if (currentMode === 'new') { return a.canCreate || a.canRead } else if (currentMode === 'edit') { return a.canUpdate || a.canRead } return false }), } }), }) } }) } // Form'u tamamen yeniden yapılandır const hasTabbedItems = result.some((item: any) => item.itemType === 'tabbed') // Form'u tamamen yeniden baştan yapılandır e.form.option( 'colCount', hasTabbedItems ? 1 : gridDto.gridOptions.editingFormDto?.[0]?.colCount || 2, ) e.form.option( 'colCount', hasTabbedItems ? 1 : gridDto.gridOptions.editingFormDto?.[0]?.colCount || 2, ) e.form.option('showValidationSummary', false) e.form.option('items', result) // Yeni versiyonlarda bu şekilde Style değiştirme işlemi yapmak zorunda olmayabiliriz. // Yani geçici olarak eklenmiştir. // Tabbed varsa belirli class'lara sahip elementi bul ve flex-direction'ı değiştir if (hasTabbedItems) { setTimeout(() => { const targetElement = document.querySelector('.dx-item-content.dx-box-item-content.dx-box-flex.dx-box.dx-widget.dx-collection') if (targetElement) { const htmlElement = targetElement as HTMLElement // Mevcut style'ı al ve flex-direction: row'u column'a çevir const currentStyle = htmlElement.getAttribute('style') || '' const updatedStyle = currentStyle.replace(/flex-direction:\s*row/gi, 'flex-direction: column') htmlElement.setAttribute('style', updatedStyle) } }, 100) } }, [gridDto, translate, isPopupFullScreen, listFormCode], ) return ( <>
{!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={true} > {checkPermission(gridDto?.gridOptions.permissionDto.u) && ( )} {gridDto.gridOptions.schedulerOptionDto?.resources?.map((resource, index) => ( ))}
)}
) } export default SchedulerView