erp-platform/ui/src/views/list/CardView.tsx

1473 lines
51 KiB
TypeScript
Raw Normal View History

2026-01-18 23:40:24 +00:00
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