erp-platform/ui/src/views/list/CardView.tsx
2026-01-19 02:40:24 +03:00

1472 lines
51 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<void>
gridDto?: GridDto
refreshGridDto?: () => Promise<void>
}
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)'
// Lookup cache (module scope)
const __lookupCache = new Map<string, Promise<any[]>>()
const cachedLoader = (key: string, loader: () => Promise<any[]>) => {
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<CardViewRef<any, any>>(null)
const refListFormCode = useRef('')
const widgetGroupRef = useRef<HTMLDivElement>(null)
const [columnData, setColumnData] = useState<GridColumnData[]>()
const [formData, setFormData] = useState<any>()
const [mode, setMode] = useState<RowMode>('view')
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
const [gridDto, setGridDto] = useState<GridDto>()
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
const [cardsPerRow, setCardsPerRow] = useState<number>(0)
const [pageSize, setPageSize] = useState<number>(20)
const [lookupItemsCache, setLookupItemsCache] = useState<Map<string, any[]>>(new Map())
const defaultSearchParams = useRef<string | null>(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<any, any>) {
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<any>) {
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<any>) {
e.data = setFormEditingExtraItemValues(e.data)
}
function onCardUpdating(e: CardUpdatingEvent<any, any>) {
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<any, any>) {
setMode('edit')
setIsPopupFullScreen(gridDto?.gridOptions.editingOptionDto?.popup?.fullScreen ?? false)
}
function onDataErrorOccurred(e: EventInfo<any> & DataErrorOccurredInfo) {
toast.push(
<Notification type="danger" duration={5000}>
{(e.error as any)?.message || 'An error occurred'}
</Notification>,
{
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<Map<string, any>>(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 <Column key={col.dataField} {...columnProps} />
})
}
// 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 (
<>
<div ref={widgetGroupRef}>
<WidgetGroup widgetGroups={gridDto?.widgets ?? []} />
</div>
{gridDto?.gridOptions.extraFilterDto && gridDto?.gridOptions.extraFilterDto.length > 0 && (
<GridExtraFilterToolbar
filters={gridDto.gridOptions.extraFilterDto}
extraFilters={extraFilters}
setExtraFilters={setExtraFilters}
/>
)}
<Container className={DX_CLASSNAMES}>
{!isSubForm && (
<Helmet
titleTemplate="%s | Erp Platform"
title={translate('::' + gridDto?.gridOptions.title)}
defaultTitle="Erp Platform"
/>
)}
{!gridDto && (
<div className="p-4">
<Loading loading>Loading CardView configuration...</Loading>
</div>
)}
{gridDto && !cardViewDataSource && (
<div className="p-4">
<Loading loading>Loading data source...</Loading>
</div>
)}
{gridDto && columnsWithLookup && Array.isArray(columnsWithLookup) && columnsWithLookup.length > 0 && cardViewDataSource && (
<div className="p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700">
<CardViewDx
ref={cardViewRef as any}
key={`CardView-${listFormCode}`}
id={'CardView-' + listFormCode}
dataSource={cardViewDataSource}
keyExpr={gridDto.gridOptions.keyFieldName}
cardsPerRow={cardsPerRow || 'auto'}
wordWrapEnabled={true}
hoverStateEnabled={gridDto.gridOptions.columnOptionDto?.hoverStateEnabled}
height={
gridDto.gridOptions.height > 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<Promise<{ key: string; items: any[] }>> = []
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<string, any[]>()
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 */}
<Toolbar items={toolbarItems} />
{/* Selection */}
<Selection
mode={gridDto.gridOptions.selectionDto?.mode}
allowSelectAll={gridDto.gridOptions.selectionDto?.allowSelectAll}
selectAllMode={gridDto.gridOptions.selectionDto?.selectAllMode}
showCheckBoxesMode={gridDto.gridOptions.selectionDto?.showCheckBoxesMode}
/>
{/* Sorting */}
<Sorting mode={gridDto.gridOptions.sortMode} />
{/* Header Filter */}
<HeaderFilter
visible={gridDto.gridOptions.headerFilterDto?.visible}
allowSearch={gridDto.gridOptions.headerFilterDto?.allowSearch}
height={gridDto.gridOptions.headerFilterDto?.height}
searchTimeout={gridDto.gridOptions.headerFilterDto?.searchTimeout}
width={gridDto.gridOptions.headerFilterDto?.width}
/>
{/* Filter Panel */}
<FilterPanel
visible={gridDto.gridOptions.filterPanelDto?.visible}
filterEnabled={gridDto.gridOptions.filterPanelDto?.filterEnabled}
/>
{/* Search Panel */}
<SearchPanel
visible={gridDto.gridOptions.searchPanelDto?.visible}
width={gridDto.gridOptions.searchPanelDto?.width}
/>
{/* Column Chooser */}
<ColumnChooser enabled={true} mode="select" />
{/* Paging */}
<Paging enabled={pagingConfig.enabled} defaultPageSize={pagingConfig.pageSize} />
{/* Pager */}
<Pager
visible={pagerConfig.visible}
showPageSizeSelector={pagerConfig.showPageSizeSelector}
showInfo={pagerConfig.showInfo}
showNavigationButtons={pagerConfig.showNavigationButtons}
allowedPageSizes={pagerConfig.allowedPageSizes}
displayMode={pagerConfig.displayMode as any}
infoText={pagerConfig.infoText}
/>
{/* Editing */}
<Editing
allowUpdating={
gridDto.gridOptions.editingOptionDto?.allowUpdating &&
checkPermission(gridDto.gridOptions.permissionDto?.u)
}
allowAdding={
gridDto.gridOptions.editingOptionDto?.allowAdding &&
checkPermission(gridDto.gridOptions.permissionDto?.c)
}
allowDeleting={
gridDto.gridOptions.editingOptionDto?.allowDeleting &&
checkPermission(gridDto.gridOptions.permissionDto?.d)
}
confirmDelete={gridDto.gridOptions.editingOptionDto?.confirmDelete}
popup={{
animation: {},
title:
(mode === 'new' ? '✚ ' : '🖊️ ') +
translate('::' + gridDto.gridOptions.editingOptionDto?.popup?.title),
showTitle: gridDto.gridOptions.editingOptionDto?.popup?.showTitle,
width: isPopupFullScreen
? '100%'
: gridDto.gridOptions.editingOptionDto?.popup?.width || 600,
height: isPopupFullScreen
? '100%'
: gridDto.gridOptions.editingOptionDto?.popup?.height || 'auto',
fullScreen: isPopupFullScreen,
hideOnOutsideClick:
gridDto.gridOptions.editingOptionDto?.popup?.hideOnOutsideClick,
resizeEnabled: gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled,
toolbarItems: [
{
widget: 'dxButton',
toolbar: 'bottom',
location: 'after',
options: {
text: translate('::Save'),
type: 'default',
onClick: () => {
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()}
</CardViewDx>
</div>
)}
</Container>
</>
)
}
export default CardView