import Container from '@/components/shared/Container' import { Notification, toast } from '@/components/ui' import { DX_CLASSNAMES } from '@/constants/app.constant' import { DbTypeEnum, GridDto, ListFormCustomizationTypeEnum, PlatformEditorTypes, UiLookupDataSourceTypeEnum, } from '@/proxy/form/models' import { getListFormCustomization, postListFormCustomization, } from '@/services/list-form-customization.service' import { useLocalization } from '@/utils/hooks/useLocalization' import { usePermission } from '@/utils/hooks/usePermission' import { usePWA } from '@/utils/hooks/usePWA' import CardViewDx, { CardViewRef, Column, ColumnChooser, Editing, HeaderFilter, SearchPanel, Sorting, Paging, Pager, Toolbar, ToolbarItem, Selection, FilterPanel, } from 'devextreme-react/card-view' import type { CardInsertingEvent, CardUpdatingEvent, EditingStartEvent, InitNewCardEvent, SelectionChangedEvent, } from 'devextreme/ui/card_view' import type { DataErrorOccurredInfo } from 'devextreme/common/grids' import type { EventInfo } from 'devextreme/events' import { EditingFormItemDto } from '@/proxy/form/models' import { captionize } from 'devextreme/core/utils/inflector' import CustomStore from 'devextreme/data/custom_store' import DataSource from 'devextreme/data/data_source' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Helmet } from 'react-helmet' import { RowMode } from '../form/types' import { GridColumnData } from './GridColumnData' import { addCss, addJs, autoNumber, extractSearchParamsFields, GridExtraFilterState, setFormEditingExtraItemValues, setGridPanelColor, } from './Utils' import WidgetGroup from '@/components/ui/Widget/WidgetGroup' import { GridExtraFilterToolbar } from './GridExtraFilterToolbar' import { getList } from '@/services/form.service' import { layoutTypes } from '../admin/listForm/edit/types' import { useListFormCustomDataSource } from './useListFormCustomDataSource' import { useListFormColumns } from './useListFormColumns' import { Loading } from '@/components/shared' import { useStoreState } from '@/store' import { ROUTES_ENUM } from '@/routes/route.constant' import { dynamicFetch } from '@/services/form.service' interface CardViewProps { listFormCode: string searchParams?: URLSearchParams isSubForm?: boolean level?: number refreshData?: () => Promise gridDto?: GridDto refreshGridDto?: () => Promise } const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // Lookup cache (module scope) const __lookupCache = new Map>() const cachedLoader = (key: string, loader: () => Promise) => { if (__lookupCache.has(key)) return __lookupCache.get(key)! const p = Promise.resolve() .then(() => loader()) .then((res) => res ?? []) .catch((err) => { __lookupCache.delete(key) throw err }) __lookupCache.set(key, p) return p } const CardView = (props: CardViewProps) => { const { listFormCode, searchParams, isSubForm, gridDto: extGridDto, refreshGridDto } = props const { translate } = useLocalization() const { checkPermission } = usePermission() const isPwaMode = usePWA() const config = useStoreState((state) => state.abpConfig.config) const cardViewRef = useRef>(null) const refListFormCode = useRef('') const widgetGroupRef = useRef(null) const [columnData, setColumnData] = useState() const [formData, setFormData] = useState() const [mode, setMode] = useState('view') const [extraFilters, setExtraFilters] = useState([]) const [gridDto, setGridDto] = useState() const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const [widgetGroupHeight, setWidgetGroupHeight] = useState(0) const [cardsPerRow, setCardsPerRow] = useState(0) const [pageSize, setPageSize] = useState(20) const [lookupItemsCache, setLookupItemsCache] = useState>(new Map()) const defaultSearchParams = useRef(null) useEffect(() => { const initializeCardView = async () => { const response = await getList({ listFormCode }) setGridDto(response.data) } if (extGridDto === undefined) { initializeCardView() } else { setGridDto(extGridDto) } }, [listFormCode, extGridDto]) useEffect(() => { if (!defaultSearchParams.current) { defaultSearchParams.current = searchParams?.get('filter') ?? null } }, [searchParams]) // Clear lookup cache when listFormCode changes useEffect(() => { __lookupCache.clear() }, [listFormCode]) const layout = layoutTypes.cardView const { createSelectDataSource } = useListFormCustomDataSource({ gridRef: cardViewRef as any }) const { getBandedColumns } = useListFormColumns({ gridDto, listFormCode, isSubForm, gridRef: cardViewRef as any, }) // Lookup data source helpers const createLookupStaticDataSource = useCallback( (load: () => any, filter: any = null, key: any = 'static', sort: any = 'name') => new DataSource({ store: new CustomStore({ key, loadMode: 'raw', load: async () => { return cachedLoader(`static:${key}`, () => Promise.resolve(load())) }, }), paginate: false, sort, filter, }), [], ) const createLookupQueryDataSource = useCallback( (listFormCode?: string, listFormFieldName?: string, filters?: any[]) => { return new DataSource({ store: new CustomStore({ loadMode: 'raw', load: async () => { try { const cacheKey = `query:${listFormCode}:${listFormFieldName}:${JSON.stringify(filters ?? null)}` return cachedLoader(cacheKey, async () => { const response = await dynamicFetch('list-form-select/lookup', 'POST', null, { listFormCode, listFormFieldName, filters, }) return (response.data ?? []).map((a: any) => ({ key: a.Key, name: a.Name, group: a.Group, ...a, })) }) } catch (error: any) { return [] } }, }), paginate: false, }) }, [], ) const createLookupApiDataSource = useCallback( (listFormCode?: string, lookupQuery?: string, filters?: any[], keyName?: string) => { return new DataSource({ store: new CustomStore({ key: keyName, loadMode: 'raw', load: async () => { if (!lookupQuery) return [] const [method, url, body, keySelector, nameSelector, groupSelector] = lookupQuery.split(';') let resolvedBody = body if (filters?.length) { for (let i = 0; i < filters.length; i++) { resolvedBody = resolvedBody.replace( new RegExp(`@param${i}`, 'g'), String(filters[i]), ) } } try { const cacheKey = `api:${lookupQuery}:${JSON.stringify(filters ?? null)}` return cachedLoader(cacheKey, async () => { const response = await dynamicFetch(url, method, null, resolvedBody) let { data } = response if (!data) return [] if (!Array.isArray(data)) data = [data] return data.map((a: any) => ({ key: eval(keySelector), name: eval(nameSelector), group: eval(groupSelector), ...a, })) }) } catch { return [] } }, }), paginate: false, }) }, [], ) const lookupDataSource = useCallback( (options: any, colData: any) => { const { lookupDto } = colData const filters: any[] = [] if (lookupDto.cascadeParentFields) { if (lookupDto.dataSourceType === UiLookupDataSourceTypeEnum.StaticData) { filters.push([ lookupDto?.cascadeRelationField, lookupDto?.cascadeFilterOperator, options?.data?.[lookupDto?.cascadeParentField], ]) } else { const data = options?.data ?? options for (const cascadeParentField of lookupDto.cascadeParentFields.split(',')) { filters.push(data?.[cascadeParentField]) } } } if (lookupDto.dataSourceType === UiLookupDataSourceTypeEnum.StaticData) { return createLookupStaticDataSource( () => JSON.parse(lookupDto?.lookupQuery), filters.length ? filters : null, `static:${listFormCode}:${colData.fieldName}`, ) } else if (lookupDto.dataSourceType === UiLookupDataSourceTypeEnum.Query) { return createLookupQueryDataSource(listFormCode, colData.fieldName, filters) } else if (lookupDto.dataSourceType === UiLookupDataSourceTypeEnum.WebService) { return createLookupApiDataSource( listFormCode, lookupDto?.lookupQuery, filters, colData.lookupDto?.valueExpr?.toLowerCase(), ) } return { store: [] } }, [ listFormCode, createLookupStaticDataSource, createLookupQueryDataSource, createLookupApiDataSource, ], ) function refreshData() { // Cache'i temizle if (typeof (window as any).__clearCardViewCache === 'function') { ;(window as any).__clearCardViewCache() } const instance = cardViewRef.current?.instance() if (instance) { instance.getDataSource()?.reload() } } // CardView specific events function onSelectionChanged(data: SelectionChangedEvent) { const grdOpt = gridDto?.gridOptions const cardView = cardViewRef.current?.instance() if (!grdOpt || !cardView) { return } if (data.selectedCardsData?.length) { setFormData(data.selectedCardsData[0]) } } function onInitNewCard(e: InitNewCardEvent) { if (!gridDto?.columnFormats) { return } setMode('new') setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) for (const colFormat of gridDto?.columnFormats) { if (!colFormat.fieldName) { continue } // Grid'den gelen columnFormat'ları kullanarak default değerleri set et if (colFormat.defaultValue != null) { const defaultValStr = String(colFormat.defaultValue) if (defaultValStr === '@AUTONUMBER') { e.data[colFormat.fieldName] = autoNumber() } else { e.data[colFormat.fieldName] = colFormat.defaultValue } } // ExtraFilters içerisinde ilgili Field varsa, default değerleri set etme if (extraFilters.some((f) => f.fieldName === colFormat.fieldName)) { continue } // URL'den veya Component Prop'dan gelen parametreleri set et const defaultValue = searchParams?.get(colFormat.fieldName) if (defaultValue) { e.data[colFormat.fieldName] = defaultValue } } } function onCardInserting(e: CardInsertingEvent) { e.data = setFormEditingExtraItemValues(e.data) } function onCardUpdating(e: CardUpdatingEvent) { if (gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate) { e.newData = { ...e.newData, [gridDto?.gridOptions.keyFieldName!]: e.oldData[gridDto?.gridOptions.keyFieldName!], } } else { e.newData = { ...e.oldData, ...e.newData, [gridDto?.gridOptions.keyFieldName!]: e.oldData[gridDto?.gridOptions.keyFieldName!], } } if (gridDto?.gridOptions.keyFieldName) { e.newData = setFormEditingExtraItemValues(e.newData) } } function onEditingStart(e: EditingStartEvent) { setMode('edit') setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false) } function onDataErrorOccurred(e: EventInfo & DataErrorOccurredInfo) { toast.push( {(e.error as any)?.message || 'An error occurred'} , { placement: 'top-end', }, ) } const customSaveState = useCallback( (state: any) => { return postListFormCustomization({ listFormCode: listFormCode, customizationType: ListFormCustomizationTypeEnum.GridState, filterName: `cardview-state`, customizationData: JSON.stringify(state), }).then(() => { setGridPanelColor(statedGridPanelColor) }) }, [listFormCode], ) const customLoadState = useCallback(() => { return getListFormCustomization( listFormCode, ListFormCustomizationTypeEnum.GridState, `cardview-state`, ).then((response: any) => { setGridPanelColor(statedGridPanelColor) if (response.data?.length > 0) { return JSON.parse(response.data[0].customizationData) } }) }, [listFormCode]) useEffect(() => { if (cardViewRef?.current) { const instance = cardViewRef?.current?.instance() if (instance) { instance.option('dataSource', undefined) } } if (refListFormCode.current !== listFormCode) { setColumnData(undefined) } }, [listFormCode]) 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) } } if (gridDto?.gridOptions.extraFilterDto) { const extras = gridDto.gridOptions.extraFilterDto.map((f) => ({ fieldName: f.fieldName, caption: f.caption, operator: f.operator || '=', value: f.defaultValue || '', controlType: f.controlType, })) // Sadece ilk yüklemede extraFilters'ı set et, her gridDto değişiminde değil setExtraFilters((prev) => { if (prev.length === 0) return extras return prev }) } if (gridDto?.gridOptions.editingOptionDto?.popup) { setIsPopupFullScreen(gridDto.gridOptions.editingOptionDto.popup.fullScreen) } // cardsPerRow başlangıç değeri - localStorage'dan oku const storageKey = `cardview-cardsPerRow-${listFormCode}` const savedCardsPerRow = localStorage.getItem(storageKey) if (savedCardsPerRow !== null) { setCardsPerRow(parseInt(savedCardsPerRow, 10)) } else if (gridDto?.gridOptions.layoutDto?.cardLayoutColumn) { setCardsPerRow(gridDto.gridOptions.layoutDto.cardLayoutColumn) } // pageSize başlangıç değeri if (gridDto?.gridOptions.pageSize) { setPageSize(gridDto.gridOptions.pageSize) } }, [gridDto, listFormCode]) // Lookup DataSource'ları bir kere oluştur ve cache'le const lookupDataSourcesRef = useRef>(new Map()) // gridDto değiştiğinde lookup cache'i temizle useEffect(() => { lookupDataSourcesRef.current.clear() }, [gridDto, listFormCode]) // Build columns with lookup support - sadece gridDto değiştiğinde const columnsWithLookup = useMemo(() => { if (!gridDto || !config) return [] const cols = getBandedColumns() if (!cols) return [] return cols.map((col) => { const colData = gridDto.columnFormats.find((c) => c.fieldName === col.dataField) if (!colData) return col // Type assertion for extended column properties const extCol = col as any // Form items için editorType ve editorOptions ayarla const gridFormItem = gridDto.gridOptions.editingFormDto ?.flatMap((f) => f.items) .find((i) => i?.dataField === col.dataField) // Lookup desteği ekle - dataSourceType > 0 olmalı (0 = None) if (colData.lookupDto?.dataSourceType && colData.lookupDto.dataSourceType > 0) { // Cache'den al veya oluştur const cacheKey = `${colData.fieldName}` let lookupDs = lookupDataSourcesRef.current.get(cacheKey) if (!lookupDs) { lookupDs = lookupDataSource(null, colData) lookupDataSourcesRef.current.set(cacheKey, lookupDs) } // NOT: Lookup için CardView'da editorType ve editorOptions KULLANMA // DevExpress bunları görünce otomatik lookup resolution yapıp çift görünmeye sebep oluyor // Sadece fieldValueRender ile lookup gösterimi yapılacak // Edit popup'ta form item için lookup ayarları yapılacak // Lookup olduğunu işaretle (renderColumns'da kullanılacak) ;(col as any).isLookup = true ;(col as any).lookupInfo = { valueExpr: colData.lookupDto?.valueExpr?.toLowerCase() || 'key', displayExpr: colData.lookupDto?.displayExpr?.toLowerCase() || 'name', } } // Form item ayarları if (gridFormItem) { const editorOptions: any = {} // Parse editorOptions from JSON if (gridFormItem.editorOptions) { try { Object.assign(editorOptions, JSON.parse(gridFormItem.editorOptions)) } catch {} } // EditorType belirleme (SchedulerView pattern'i) let editorType: any = gridFormItem.editorType2 || gridFormItem.editorType if (gridFormItem.editorType2 === PlatformEditorTypes.dxGridBox) { editorType = 'dxDropDownBox' } else if (gridFormItem.editorType2 === PlatformEditorTypes.dxTagBox) { editorType = 'dxTagBox' } else if (gridFormItem.editorType2) { editorType = gridFormItem.editorType2 } // Lookup için dataSource ve valueExpr/displayExpr ekle - dataSourceType > 0 olmalı if (colData.lookupDto?.dataSourceType && colData.lookupDto.dataSourceType > 0) { // Cache'den al const cacheKey = `${colData.fieldName}` const lookupDs = lookupDataSourcesRef.current.get(cacheKey) if (lookupDs) { editorOptions.dataSource = lookupDs editorOptions.valueExpr = colData.lookupDto?.valueExpr?.toLowerCase() || 'key' editorOptions.displayExpr = colData.lookupDto?.displayExpr?.toLowerCase() || 'name' editorOptions.searchEnabled = true editorOptions.showClearButton = true } // Lookup varsa SelectBox kullan (eğer başka bir tip belirtilmediyse) if (!editorType || editorType === 'dxTextBox') { editorType = 'dxSelectBox' } } // Date/DateTime alanları için özel ayarlar if (colData.sourceDbType === DbTypeEnum.Date) { editorType = 'dxDateBox' editorOptions.type = 'date' editorOptions.dateSerializationFormat = 'yyyy-MM-dd' editorOptions.displayFormat = 'shortDate' } else if ( colData.sourceDbType === DbTypeEnum.DateTime || colData.sourceDbType === DbTypeEnum.DateTime2 || colData.sourceDbType === DbTypeEnum.DateTimeOffset ) { editorType = 'dxDateBox' editorOptions.type = 'datetime' editorOptions.dateSerializationFormat = 'yyyy-MM-ddTHH:mm:ss' editorOptions.displayFormat = 'shortDateShortTime' } col.formItem = { ...col.formItem, colSpan: gridFormItem.colSpan, } if (editorType) { extCol.editorType = editorType } col.editorOptions = { ...col.editorOptions, ...editorOptions, } } return col }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [gridDto, config]) // getBandedColumns ve lookupDataSource çıkarıldı - sonsuz döngüyü önlemek için // DataSource oluştur - sadece gridDto ve listFormCode değiştiğinde (useMemo ile cache'le) const cardViewDataSource = useMemo(() => { if (!gridDto) return null const cols = getBandedColumns() if (!cols || cols.length === 0) return null const baseStore = createSelectDataSource( gridDto.gridOptions, listFormCode, searchParams, layoutTypes.cardView, cols, ) // CardView için sadece 1 select çağrısı yapacak wrapper let cachedData: any[] | null = null let cachedTotalCount: number = 0 let isLoading = false const keyExpr = gridDto.gridOptions.keyFieldName const clearCache = () => { cachedData = null cachedTotalCount = 0 isLoading = false } // Cache temizleme fonksiyonunu dışarıya expose et ;(window as any).__clearCardViewCache = clearCache const optimizedStore: any = new CustomStore({ key: keyExpr, load: async (loadOptions: any) => { // Zaten yüklenmişse cache'den dön if (cachedData !== null && !isLoading) { return { data: cachedData, totalCount: cachedTotalCount, } } // Yükleme devam ediyorsa bekle if (isLoading) { await new Promise((resolve) => setTimeout(resolve, 100)) if (cachedData !== null) { return { data: cachedData, totalCount: cachedTotalCount, } } } isLoading = true try { const result = await baseStore.load(loadOptions) cachedData = result?.data || [] cachedTotalCount = result?.totalCount || 0 return result } finally { isLoading = false } }, byKey: async (key: any) => { // Cache'de ara if (cachedData && keyExpr) { const item = cachedData.find((row: any) => row?.[keyExpr] === key) if (item) return item } // Bulamazsa server'a git return baseStore.byKey(key) }, insert: async (values: any) => { const result = await baseStore.insert(values) clearCache() return result }, update: async (key: any, values: any) => { const result = await baseStore.update(key, values) clearCache() return result }, remove: async (key: any) => { const result = await baseStore.remove(key) clearCache() return result }, }) // DataSource içine sar return new DataSource({ store: optimizedStore, reshapeOnPush: true, paginate: true, pageSize: pageSize, }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [gridDto, listFormCode]) // extraFilters değişikliğini izlemek için ref const extraFiltersInitialized = useRef(false) useEffect(() => { // İlk yüklemede reload yapma, sadece kullanıcı filtre değiştirdiğinde if (!extraFiltersInitialized.current) { extraFiltersInitialized.current = true return } const instance = cardViewRef.current?.instance() if (!instance) return const activeFilters = extraFilters.filter((f) => f.value) let base: any = null if (defaultSearchParams.current) { base = JSON.parse(defaultSearchParams.current) } const baseTriplets = extractSearchParamsFields(base) const extraTriplets = activeFilters.map( (f) => [f.fieldName, f.operator, f.value] as [string, string, any], ) const merged = [...baseTriplets, ...extraTriplets].reduce( (acc, cur) => { const idx = acc.findIndex((a) => a[0] === cur[0] && a[1] === cur[1]) if (idx >= 0) { acc[idx] = cur } else { acc.push(cur) } return acc }, [] as [string, string, any][], ) let filter: any = null if (merged.length === 1) { filter = merged[0] } else if (merged.length > 1) { filter = merged.reduce((acc, f, idx) => { if (idx === 0) return f return [acc, 'and', f] }, null as any) } if (filter) { instance?.option('filterValue', filter) } else { instance?.option('filterValue', undefined) } instance?.getDataSource()?.reload() }, [extraFilters]) 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 customizeLookupText = useCallback( (fieldName: string) => { return (cellInfo: any) => { const value = cellInfo.value if (!value) return '' const cacheKey = fieldName const items = lookupItemsCache.get(cacheKey) if (!items || items.length === 0) { return value } try { const colData = gridDto?.columnFormats.find((c) => c.fieldName === fieldName) const valueExpr = colData?.lookupDto?.valueExpr?.toLowerCase() || 'key' const displayExpr = colData?.lookupDto?.displayExpr?.toLowerCase() || 'name' const item = items.find((i: any) => i[valueExpr] === value) return item ? item[displayExpr] : value } catch { return value } } }, [gridDto, lookupItemsCache], ) // CardView column render from GridColumnData const renderColumns = () => { if (!columnsWithLookup || !Array.isArray(columnsWithLookup) || columnsWithLookup.length === 0) { return null } return columnsWithLookup .filter((col) => col.type !== 'buttons' && col.visible !== false) .map((col) => { const extCol = col as any const colData = gridDto?.columnFormats.find((c) => c.fieldName === col.dataField) // Column props const columnProps: any = { dataField: col.dataField, caption: col.caption ? translate('::' + col.caption) : captionize(col.dataField || ''), dataType: col.dataType, visible: col.visible, allowSorting: col.allowSorting, allowFiltering: col.allowFiltering, allowHeaderFiltering: col.allowHeaderFiltering, sortOrder: col.sortOrder, sortIndex: col.sortIndex, format: col.format, alignment: col.alignment, formItem: col.formItem, } // Lookup varsa customizeText kullan - fieldValueRender çift görünmeye sebep oluyor if (extCol.isLookup) { columnProps.customizeText = customizeLookupText(col.dataField!) } else { // Lookup değilse editorType ve editorOptions ekle if (extCol.editorType) { columnProps.editorType = extCol.editorType } if (col.editorOptions) { columnProps.editorOptions = col.editorOptions } // fieldValueRender - farklı veri tipleri için özel renderlar (lookup değilse) if (colData?.sourceDbType === DbTypeEnum.Date || col.dataType === 'date') { columnProps.customizeText = (cellInfo: any) => { if (!cellInfo.value) return '—' return new Date(cellInfo.value).toLocaleDateString() } } else if ( colData?.sourceDbType === DbTypeEnum.DateTime || colData?.sourceDbType === DbTypeEnum.DateTime2 || colData?.sourceDbType === DbTypeEnum.DateTimeOffset || col.dataType === 'datetime' ) { columnProps.customizeText = (cellInfo: any) => { if (!cellInfo.value) return '—' return new Date(cellInfo.value).toLocaleString() } } } // Boolean için customizeText kullan (fieldValueRender çift checkbox oluşturuyor) if (colData?.sourceDbType === DbTypeEnum.Boolean || col.dataType === 'boolean') { columnProps.customizeText = (cellInfo: any) => { return cellInfo.value ? translate('::App.Listforms.ImportManager.Yes') : translate('::App.Listforms.ImportManager.No') } } return }) } // Kolon sayısı değiştiğinde const onCardsPerRowChanged = useCallback( (value: number) => { setCardsPerRow(value) // localStorage'a kaydet const storageKey = `cardview-cardsPerRow-${listFormCode}` localStorage.setItem(storageKey, value.toString()) }, [listFormCode], ) // Page size değiştiğinde const onPageSizeChanged = useCallback((newPageSize: number) => { setPageSize(newPageSize) const instance = cardViewRef.current?.instance() if (instance) { instance.pageSize(newPageSize) } }, []) // Toolbar items const toolbarItems = useMemo(() => { if (!gridDto) return [] const items: any[] = [ { name: 'addCardButton' }, { name: 'searchPanel' }, { location: 'after', widget: 'dxButtonGroup', options: { items: [ { text: 'Auto', value: 0, hint: 'Otomatik' }, { text: '1', value: 1, hint: '1 Kolon' }, { text: '2', value: 2, hint: '2 Kolon' }, { text: '3', value: 3, hint: '3 Kolon' }, { text: '4', value: 4, hint: '4 Kolon' }, { text: '5', value: 5, hint: '5 Kolon' }, ], keyExpr: 'value', selectedItemKeys: [cardsPerRow || 0], selectionMode: 'single', stylingMode: 'outlined', onItemClick: (e: any) => { if (e.itemData) { onCardsPerRowChanged(e.itemData.value) } }, }, }, { location: 'after', widget: 'dxButton', options: { icon: 'refresh', hint: translate('::Refresh'), stylingMode: 'text', onClick: () => refreshData(), }, }, ] // Column Chooser butonu için permission kontrolü if (checkPermission(gridDto?.gridOptions.permissionDto?.u)) { items.push({ location: 'after', widget: 'dxButton', options: { icon: 'columnchooser', hint: translate('::ColumnChooser'), stylingMode: 'text', onClick: () => { const instance = cardViewRef.current?.instance() instance?.showColumnChooser() }, }, }) } // Settings butonu için permission kontrolü if (checkPermission(gridDto?.gridOptions.permissionDto?.u)) { items.push({ location: 'after', widget: 'dxButton', options: { icon: 'preferences', hint: translate('::ListForms.ListForm.Manage'), stylingMode: 'text', onClick: settingButtonClick, }, }) } return items }, [ gridDto, cardsPerRow ]) // Paging ayarları const pagingConfig = useMemo( () => ({ enabled: gridDto?.gridOptions.pagerOptionDto?.visible !== false, pageSize: pageSize, }), [gridDto?.gridOptions.pagerOptionDto?.visible, pageSize], ) // Pager ayarları const pagerConfig = useMemo(() => { const allowedSizes = gridDto?.gridOptions.pagerOptionDto?.allowedPageSizes ?.split(',') .map((s) => Number(s.trim())) .filter((n) => !isNaN(n) && n > 0) || [10, 20, 50, 100] return { visible: gridDto?.gridOptions.pagerOptionDto?.visible !== false, showPageSizeSelector: gridDto?.gridOptions.pagerOptionDto?.showPageSizeSelector !== false, showInfo: gridDto?.gridOptions.pagerOptionDto?.showInfo !== false, showNavigationButtons: gridDto?.gridOptions.pagerOptionDto?.showNavigationButtons !== false, allowedPageSizes: allowedSizes, displayMode: gridDto?.gridOptions.pagerOptionDto?.displayMode || 'full', infoText: gridDto?.gridOptions.pagerOptionDto?.infoText, } }, [gridDto?.gridOptions.pagerOptionDto]) return ( <>
{gridDto?.gridOptions.extraFilterDto && gridDto?.gridOptions.extraFilterDto.length > 0 && ( )} {!isSubForm && ( )} {!gridDto && (
Loading CardView configuration...
)} {gridDto && !cardViewDataSource && (
Loading data source...
)} {gridDto && columnsWithLookup && Array.isArray(columnsWithLookup) && columnsWithLookup.length > 0 && cardViewDataSource && (
0 ? gridDto.gridOptions.height : gridDto.gridOptions.fullHeight ? `calc(100vh - ${170 + widgetGroupHeight}px)` : undefined } remoteOperations={false} onSelectionChanged={onSelectionChanged as any} onInitNewCard={onInitNewCard as any} onCardInserting={onCardInserting as any} onCardUpdating={onCardUpdating as any} onEditingStart={onEditingStart as any} onDataErrorOccurred={onDataErrorOccurred as any} onEditCanceled={() => { setMode('view') setIsPopupFullScreen(false) }} onCardInserted={() => { setMode('view') setIsPopupFullScreen(false) // Küçük bir gecikme ile reload - server transaction commit bekle setTimeout(() => { refreshData() props.refreshData?.() }, 100) }} onCardUpdated={() => { setMode('view') setIsPopupFullScreen(false) // Küçük bir gecikme ile reload - server transaction commit bekle setTimeout(() => { refreshData() props.refreshData?.() }, 100) }} onCardRemoved={() => { // Küçük bir gecikme ile reload - server transaction commit bekle setTimeout(() => { refreshData() props.refreshData?.() }, 100) }} onOptionChanged={(e: any) => { if (e.name === 'paging.pageSize' && e.value !== pageSize) { setPageSize(e.value) } }} onContentReady={() => { // Lookup DataSource'ları yükle ve state'e cache'le (sadece ilk yüklemede) if (lookupItemsCache.size === 0 && lookupDataSourcesRef.current.size > 0) { const lookupPromises: Array> = [] lookupDataSourcesRef.current.forEach((ds, key) => { if (ds && typeof ds.load === 'function') { lookupPromises.push( ds.load().then(() => ({ key, items: ds.items() || [], })), ) } }) if (lookupPromises.length > 0) { Promise.all(lookupPromises) .then((results) => { const newCache = new Map() results.forEach(({ key, items }) => { newCache.set(key, items) }) setLookupItemsCache(newCache) const instance = cardViewRef.current?.instance() if (instance) { instance.repaint() } }) .catch(() => { // Hata durumunda sessizce devam et }) } } }} > {/* Toolbar */} {/* Selection */} {/* Sorting */} {/* Header Filter */} {/* Filter Panel */} {/* Search Panel */} {/* Column Chooser */} {/* Paging */} {/* Pager */} {/* Editing */} { const cardView = cardViewRef.current?.instance() if (cardView) { // Form validasyonu yap const editForm = (cardView as any).getController?.('validating')?.validate?.() // Eğer validate fonksiyonu yoksa direkt kaydet if (!editForm) { cardView.saveEditData() return } // Validasyon hatası varsa kaydetme if (editForm && !editForm.brokenRules?.length) { cardView.saveEditData() } } }, }, }, { widget: 'dxButton', toolbar: 'bottom', location: 'after', options: { text: translate('::Cancel'), onClick: () => { const cardView = cardViewRef.current?.instance() cardView?.cancelEditData() }, }, }, { widget: 'dxButton', toolbar: 'top', location: 'after', options: { icon: isPopupFullScreen ? 'collapse' : 'fullscreen', hint: isPopupFullScreen ? translate('::Normal Boyut') : translate('::Tam Ekran'), stylingMode: 'text', onClick: () => setIsPopupFullScreen(!isPopupFullScreen), }, }, ], }} form={{ colCount: gridDto.gridOptions.editingFormDto?.[0]?.colCount || 2, showValidationSummary: true, items: gridDto.gridOptions.editingFormDto?.length > 0 ? (() => { const sortedFormDto = gridDto.gridOptions.editingFormDto .slice() .sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) const tabbedItems = sortedFormDto.filter( (e: any) => e.itemType === 'tabbed', ) const result: any[] = [] 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' && mode === '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 && listFormField.lookupDto.dataSourceType > 0 ) { const cacheKey = `${listFormField.fieldName}` let lookupDs = lookupDataSourcesRef.current.get(cacheKey) if (!lookupDs) { lookupDs = lookupDataSource(null, listFormField) lookupDataSourcesRef.current.set(cacheKey, lookupDs) } editorOptions.dataSource = lookupDs editorOptions.valueExpr = listFormField.lookupDto.valueExpr?.toLowerCase() || 'key' editorOptions.displayExpr = listFormField.lookupDto.displayExpr?.toLowerCase() || 'name' editorOptions.searchEnabled = true editorOptions.showClearButton = true if (!editorType || editorType === 'dxTextBox') { editorType = 'dxSelectBox' } } const item: any = { dataField: i.dataField, editorType: editorType, colSpan: i.colSpan, isRequired: i.isRequired, editorOptions, } // Required field için validasyon kuralı ekle if (i.isRequired) { item.validationRules = [ { type: 'required', message: `${i.dataField.split(':')[1] || i.dataField} zorunludur`, }, ] } if (i.dataField.indexOf(':') >= 0) { item.label = { text: captionize(i.dataField.split(':')[1]) } } return item } sortedFormDto.forEach((e: any) => { if (e.itemType !== 'tabbed') { result.push({ itemType: e.itemType, colCount: e.colCount || 1, colSpan: e.colSpan || 1, caption: e.caption, items: e.items ?.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) .map(mapFormItem), }) } else if (tabbedItems.length > 0 && e === tabbedItems[0]) { result.push({ itemType: 'tabbed', colCount: 1, colSpan: 1, tabs: tabbedItems.map((tabbedItem: any) => ({ title: tabbedItem.caption, colCount: tabbedItem.colCount || 1, items: tabbedItem.items ?.sort((a: any, b: any) => (a.order >= b.order ? 1 : -1)) .map(mapFormItem), })), }) } }) return result })() : undefined, }} /> {/* Columns */} {renderColumns()}
)}
) } export default CardView