diff --git a/api/src/Erp.Platform.DbMigrator/Seeds/ListFormSeeder_Maintenance.cs b/api/src/Erp.Platform.DbMigrator/Seeds/ListFormSeeder_Maintenance.cs index be018ee3..e6314bbf 100644 --- a/api/src/Erp.Platform.DbMigrator/Seeds/ListFormSeeder_Maintenance.cs +++ b/api/src/Erp.Platform.DbMigrator/Seeds/ListFormSeeder_Maintenance.cs @@ -2135,7 +2135,7 @@ public class ListFormSeeder_Maintenance : IDataSeedContributor, ITransientDepend new EditingFormItemDto { Order = 4, DataField = "Priority", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox }, new EditingFormItemDto { Order = 5, DataField = "Status", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox }, new EditingFormItemDto { Order = 6, DataField = "EmployeeId", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox }, - new EditingFormItemDto { Order = 7, DataField = "Subject", ColSpan = 2, EditorType2 = EditorTypes.dxTextArea }, + new EditingFormItemDto { Order = 7, DataField = "Subject", ColSpan = 2, IsRequired = true, EditorType2 = EditorTypes.dxTextArea }, new EditingFormItemDto { Order = 8, DataField = "ScheduledStart", ColSpan = 1, EditorType2 = EditorTypes.dxDateBox, EditorOptions=EditorOptionValues.DateTimeFormat }, new EditingFormItemDto { Order = 9, DataField = "ScheduledEnd", ColSpan = 1, EditorType2 = EditorTypes.dxDateBox, EditorOptions=EditorOptionValues.DateTimeFormat }, new EditingFormItemDto { Order = 10, DataField = "ActualStart", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxDateBox, EditorOptions=EditorOptionValues.DateTimeFormat }, @@ -2292,6 +2292,7 @@ public class ListFormSeeder_Maintenance : IDataSeedContributor, ITransientDepend IsActive = true, IsDeleted = false, AllowSearch = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, ColumnCustomizationJson = DefaultColumnCustomizationJson, PermissionJson = DefaultFieldPermissionJson(listForm.Name), PivotSettingsJson = DefaultPivotSettingsJson @@ -2804,7 +2805,7 @@ public class ListFormSeeder_Maintenance : IDataSeedContributor, ITransientDepend EditingFormJson = JsonSerializer.Serialize(new List() { new() { Order=1, ColCount=2, ColSpan=1, ItemType="group", Items= [ - new EditingFormItemDto { Order = 1, DataField = "Subject", ColSpan = 2, EditorType2 = EditorTypes.dxTextArea }, + new EditingFormItemDto { Order = 1, DataField = "Subject", ColSpan = 2, IsRequired = true, EditorType2 = EditorTypes.dxTextArea }, new EditingFormItemDto { Order = 2, DataField = "WorkcenterId", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox }, new EditingFormItemDto { Order = 3, DataField = "WorkorderTypeId", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox }, new EditingFormItemDto { Order = 4, DataField = "Priority", ColSpan = 1, IsRequired = true, EditorType2 = EditorTypes.dxSelectBox }, @@ -2813,6 +2814,7 @@ public class ListFormSeeder_Maintenance : IDataSeedContributor, ITransientDepend new EditingFormItemDto { Order = 7, DataField = "ScheduledStart", ColSpan = 1, EditorType2 = EditorTypes.dxDateBox, EditorOptions=EditorOptionValues.DateTimeFormat }, new EditingFormItemDto { Order = 8, DataField = "ScheduledEnd", ColSpan = 1, EditorType2 = EditorTypes.dxDateBox, EditorOptions=EditorOptionValues.DateTimeFormat }, new EditingFormItemDto { Order = 9, DataField = "EstimatedCost", ColSpan = 1, EditorType2 = EditorTypes.dxNumberBox, EditorOptions=EditorOptionValues.NumberStandartFormat }, + new EditingFormItemDto { Order = 10, DataField = "Description", ColSpan = 2, EditorType2 = EditorTypes.dxTextArea }, ]} }), FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { @@ -2851,6 +2853,7 @@ public class ListFormSeeder_Maintenance : IDataSeedContributor, ITransientDepend IsActive = true, IsDeleted = false, AllowSearch = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, ColumnCustomizationJson = DefaultColumnCustomizationJson, PermissionJson = DefaultFieldPermissionJson(listForm.Name), PivotSettingsJson = DefaultPivotSettingsJson @@ -2998,6 +3001,21 @@ public class ListFormSeeder_Maintenance : IDataSeedContributor, ITransientDepend PermissionJson = DefaultFieldPermissionJson(listForm.Name), PivotSettingsJson = DefaultPivotSettingsJson }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Description", + Width = 300, + ListOrderNo = 11, + Visible = true, + IsActive = true, + IsDeleted = false, + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, ]); #endregion } diff --git a/api/src/Erp.Platform.Domain/Queries/QueryManager.cs b/api/src/Erp.Platform.Domain/Queries/QueryManager.cs index 135f9ad0..013e8b6b 100644 --- a/api/src/Erp.Platform.Domain/Queries/QueryManager.cs +++ b/api/src/Erp.Platform.Domain/Queries/QueryManager.cs @@ -149,9 +149,12 @@ public class QueryManager : PlatformDomainService, IQueryManager }; var updateFields = parameters.Select(a => $"\"{a.Key}\" = @{a.Key}").ToList(); var val = QueryHelper.GetFormattedValue(listForm.KeyFieldDbSourceType, keys); - parameters.Add( - listForm.KeyFieldName, - val.GetType().IsArray ? val : new object[] { val }); + if (!parameters.ContainsKey(listForm.KeyFieldName)) + { + parameters.Add( + listForm.KeyFieldName, + val.GetType().IsArray ? val : new object[] { val }); + } sql = $"UPDATE \"{listForm.SelectCommand}\" SET {string.Join(',', updateFields)} WHERE {where}"; } else if (op == OperationEnum.Delete) diff --git a/ui/src/views/list/Grid.tsx b/ui/src/views/list/Grid.tsx index 6c0deeb4..70f3be2e 100644 --- a/ui/src/views/list/Grid.tsx +++ b/ui/src/views/list/Grid.tsx @@ -55,6 +55,7 @@ import { addJs, autoNumber, controlStyleCondition, + extractSearchParamsFields, GridExtraFilterState, setFormEditingExtraItemValues, setGridPanelColor, @@ -159,18 +160,6 @@ const Grid = (props: GridProps) => { gridRef, }) - function extractSearchParamsFields(filter: any): [string, string, any][] { - if (!Array.isArray(filter)) return [] - - // [field, op, val] formu mu? - if (filter.length === 3 && typeof filter[0] === 'string') { - return [filter as [string, string, any]] - } - - // içinde başka filter array’leri olabilir - return filter.flatMap((f) => extractSearchParamsFields(f)) - } - async function getSelectedRowKeys() { const grd = gridRef.current?.instance() if (!grd) { diff --git a/ui/src/views/list/SchedulerView.tsx b/ui/src/views/list/SchedulerView.tsx index 923b0b94..5d712257 100644 --- a/ui/src/views/list/SchedulerView.tsx +++ b/ui/src/views/list/SchedulerView.tsx @@ -1,6 +1,6 @@ import Container from '@/components/shared/Container' import { DX_CLASSNAMES } from '@/constants/app.constant' -import { GridDto } from '@/proxy/form/models' +import { GridDto, PlatformEditorTypes, UiLookupDataSourceTypeEnum } from '@/proxy/form/models' import { useLocalization } from '@/utils/hooks/useLocalization' import Scheduler, { Editing, @@ -22,6 +22,10 @@ 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 { useListFormColumns } from './useListFormColumns' +import { EditingFormItemDto } from '@/proxy/form/models' +import useResponsive from '@/utils/hooks/useResponsive' +import { SimpleItemWithColData } from '../form/types' interface SchedulerViewProps { listFormCode: string @@ -41,11 +45,13 @@ const SchedulerView = (props: SchedulerViewProps) => { const refListFormCode = useRef('') const widgetGroupRef = useRef(null) const { checkPermission } = usePermission() + const { smaller } = useResponsive() 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(() => { @@ -77,6 +83,12 @@ const SchedulerView = (props: SchedulerViewProps) => { }, [listFormCode]) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: schedulerRef }) + const { getBandedColumns } = useListFormColumns({ + gridDto, + listFormCode, + isSubForm, + gridRef: schedulerRef, + }) useEffect(() => { if (!gridDto) { @@ -157,9 +169,350 @@ const SchedulerView = (props: SchedulerViewProps) => { setCurrentView(value) }, []) - const onAppointmentFormOpening = useCallback((e: any) => { - // Özelleştirme yapılabilir - }, []) + const onAppointmentFormOpening = useCallback( + (e: any) => { + if (!gridDto) return + + // Popup ayarlarını her açılışta yeniden yapılandır + e.popup.option('title', translate('::' + gridDto.gridOptions.editingOptionDto?.popup?.title)) + e.popup.option('showTitle', gridDto.gridOptions.editingOptionDto?.popup?.showTitle) + e.popup.option('width', gridDto.gridOptions.editingOptionDto?.popup?.width || 800) + e.popup.option('height', gridDto.gridOptions.editingOptionDto?.popup?.height || 600) + e.popup.option('resizeEnabled', gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled) + e.popup.option('fullScreen', isPopupFullScreen) + + // Toolbar butonlarını ekle + const 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 {0 + // 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') + } + + // Button icon ve hint'i güncelle + const button = e.popup._$element.find('.dx-toolbar-after .dx-button').last() + if (button.length) { + const buttonInstance = button.dxButton('instance') + if (buttonInstance) { + buttonInstance.option('icon', newFullScreenState ? 'collapse' : 'fullscreen') + buttonInstance.option('hint', newFullScreenState ? translate('::Normal Boyut') : translate('::Tam Ekran')) + } + } + }, + }, + }, + ] + + e.popup.option('toolbarItems', toolbarItems) + + // EditingFormDto'dan form items oluştur + const formItems: any[] = [] + + if (gridDto.gridOptions.editingFormDto?.length > 0) { + const sortedFormDto = gridDto.gridOptions.editingFormDto + .slice() + .sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) + + sortedFormDto.forEach((group: any) => { + const groupItems = (group.items || []).map((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) + + // Label oluştur + const label: any = { + text: listFormField?.captionName + ? translate('::' + listFormField.captionName) + : i.dataField, + } + + // EditorType belirleme - Grid'deki gibi + 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 + + // EditorType'ı dxSelectBox olarak ayarla + if (!editorType || editorType === 'dxTextBox') { + editorType = 'dxSelectBox' + } + + 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) + } + } + } + } + + // Validation rules + const validationRules: any[] = [] + if (i.isRequired) { + validationRules.push({ type: 'required' }) + } + + return { + label, + dataField: i.dataField, + editorType, + colSpan: i.colSpan, + editorOptions, + validationRules: validationRules.length > 0 ? validationRules : undefined, + } + }) + + if (group.itemType === 'group') { + const groupItem: any = { + itemType: 'group', + colCount: group.colCount || 1, + items: groupItems, + } + // Caption null değilse ekle + if (group.caption) { + groupItem.caption = translate('::' + group.caption) + } + formItems.push(groupItem) + } else if (group.itemType === 'tabbed') { + formItems.push({ + itemType: 'tabbed', + tabs: (group.tabs || []).map((tab: any) => ({ + title: translate('::' + tab.title), + colCount: tab.colCount || 2, + items: (tab.items || []).map((i: EditingFormItemDto) => { + // Tab içindeki itemlar için de aynı mapping + 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, + ) + + const label: any = { + text: listFormField?.captionName + ? translate('::' + listFormField.captionName) + : i.dataField, + } + + // EditorType belirleme - Grid'deki gibi + let editorType: any = i.editorType2 || i.editorType + if (i.editorType2 === PlatformEditorTypes.dxGridBox) { + editorType = 'dxDropDownBox' + } else if (i.editorType2) { + editorType = i.editorType2 + } + + if (listFormField?.lookupDto) { + const lookup = listFormField.lookupDto + + // EditorType'ı dxSelectBox olarak ayarla + if (!editorType || editorType === 'dxTextBox') { + editorType = 'dxSelectBox' + } + + 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 validationRules: any[] = [] + if (i.isRequired) { + validationRules.push({ type: 'required' }) + } + + return { + label, + dataField: i.dataField, + editorType, + colSpan: i.colSpan, + editorOptions, + validationRules: validationRules.length > 0 ? validationRules : undefined, + } + }), + })), + }) + } else { + // No group, add items directly + formItems.push(...groupItems) + } + }) + } + + // Form'a items'ı set et ve colCount'u ayarla + e.form.option('showValidationSummary', false) + e.form.option('items', formItems) + }, + [gridDto, translate, isPopupFullScreen, listFormCode], + ) return ( <> diff --git a/ui/src/views/list/Utils.ts b/ui/src/views/list/Utils.ts index 5902cc0e..51bcdf82 100644 --- a/ui/src/views/list/Utils.ts +++ b/ui/src/views/list/Utils.ts @@ -10,6 +10,18 @@ export interface GridExtraFilterState { value: string } +export function extractSearchParamsFields(filter: any): [string, string, any][] { + if (!Array.isArray(filter)) return [] + + // [field, op, val] formu mu? + if (filter.length === 3 && typeof filter[0] === 'string') { + return [filter as [string, string, any]] + } + + // içinde başka filter array’leri olabilir + return filter.flatMap((f) => extractSearchParamsFields(f)) +} + // sayfaya dinamik olarak css style ekler export function addCss(css: string) { if (css.startsWith('http') || css.startsWith('/')) {