import { EditingFormItemDto, GridDto, PlatformEditorTypes } from "@/proxy/form/models" import { useLocalization } from "@/utils/hooks/useLocalization" import { usePermission } from "@/utils/hooks/usePermission" import { usePWA } from "@/utils/hooks/usePWA" import CustomStore from "devextreme/data/custom_store" import { useMemo, useRef, useState, forwardRef, memo, useEffect } from "react" import { useNavigate } from "react-router-dom" import { GroupItem } from 'devextreme/ui/form' import { PermissionResults, SimpleItemWithColData } from "../form/types" import { captionize } from 'devextreme/core/utils/inflector' import { ROUTES_ENUM } from "@/routes/route.constant" import FormButtons from "../form/FormButtons" import FormDevExpress from "../form/FormDevExpress" import { FormRef } from "devextreme-react/cjs/form" import { Checkbox } from "@/components/ui" import classNames from "classnames" interface CardItemProps { isSubForm?: boolean row: any gridDto: GridDto listFormCode: string dataSource: CustomStore refreshData: () => void getCachedLookupDataSource: (editorOptions: any, colData: any, row?: any) => any // Selection and interaction props isSelected?: boolean isFocused?: boolean isHovered?: boolean onSelectionChange?: (checked: boolean) => void onClick?: (e: React.MouseEvent) => void onDoubleClick?: () => void onMouseEnter?: () => void onMouseLeave?: () => void tabIndex?: number } const CardItem = forwardRef(( { isSubForm, row, gridDto, listFormCode, dataSource, refreshData, getCachedLookupDataSource, isSelected = false, isFocused = false, isHovered = false, onSelectionChange, onClick, onDoubleClick, onMouseEnter, onMouseLeave, tabIndex = -1, }, ref ) => { // Selection mode'u gridDto'dan al const selectionMode = useMemo(() => { if (!gridDto?.gridOptions?.selectionDto?.mode) return 'none' const mode = gridDto.gridOptions.selectionDto.mode.toLowerCase() if (mode === 'single') return 'single' if (mode === 'multiple') return 'multiple' return 'none' }, [gridDto]) const [formData, setFormData] = useState(row) const refForm = useRef(null) const cardElementRef = useRef(null) const navigate = useNavigate() const { translate } = useLocalization() const { checkPermission } = usePermission() const isPwaMode = usePWA() // Lazy load form with Intersection Observer - sadece görünür kartları render et const [shouldRenderForm, setShouldRenderForm] = useState(false) useEffect(() => { const element = cardElementRef.current if (!element) return // Intersection Observer ile görünürlük kontrolü const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { // Kart görünür hale geldiğinde form'u render et setShouldRenderForm(true) // Bir kez render edildikten sonra observer'ı kaldır observer.disconnect() } }) }, { rootMargin: '50px', // 50px önceden yüklemeye başla threshold: 0.01, // %1 görünür olduğunda tetikle } ) observer.observe(element) return () => { observer.disconnect() } }, []) const keyField = gridDto.gridOptions.keyFieldName const rowId = row[keyField!] // Form Items - memoized to prevent recalculation on every render const formItems: GroupItem[] = useMemo(() => { if (!gridDto) return [] return gridDto.gridOptions.editingFormDto ?.sort((a, b) => (a.order >= b.order ? 1 : -1)) .map((e) => { return { itemType: e.itemType, colCount: e.colCount, colSpan: e.colSpan, caption: e.caption, items: e.items ?.sort((a, b) => (a.order >= b.order ? 1 : -1)) .map((i: EditingFormItemDto) => { let editorOptions = {} const colData = gridDto.columnFormats.find((x) => x.fieldName === i.dataField) try { editorOptions = i.editorOptions && JSON.parse(i.editorOptions) } catch {} const item: SimpleItemWithColData = { canRead: colData?.canRead ?? false, canUpdate: colData?.canUpdate ?? false, canCreate: colData?.canCreate ?? false, canExport: colData?.canExport ?? false, dataField: i.dataField, name: i.dataField, label: { text: colData?.captionName ? translate('::' + colData?.captionName) : i.dataField }, editorType2: i.editorType2, editorType: i.editorType2 == PlatformEditorTypes.dxGridBox ? 'dxDropDownBox' : i.editorType2, colSpan: i.colSpan, isRequired: i.isRequired, editorOptions: { ...editorOptions, ...(colData?.lookupDto?.dataSourceType ? { dataSource: getCachedLookupDataSource(colData?.editorOptions, colData), valueExpr: colData?.lookupDto?.valueExpr?.toLowerCase(), displayExpr: colData?.lookupDto?.displayExpr?.toLowerCase(), } : {}), }, colData, tagBoxOptions: i.tagBoxOptions, gridBoxOptions: i.gridBoxOptions, editorScript: i.editorScript, } if (i.dataField.indexOf(':') >= 0) { item.label = { text: captionize(i.dataField.split(':')[1]) } } item.editorOptions = { ...item.editorOptions, readOnly: true, } return item }), } as GroupItem }) }, [gridDto, getCachedLookupDataSource, translate]) const permissionResults: PermissionResults = { c: gridDto?.gridOptions.editingOptionDto.allowAdding === true && checkPermission(gridDto?.gridOptions.permissionDto.c), r: checkPermission(gridDto?.gridOptions.permissionDto.r), u: gridDto?.gridOptions.editingOptionDto.allowUpdating === true && checkPermission(gridDto?.gridOptions.permissionDto.u), d: gridDto?.gridOptions.editingOptionDto.allowDeleting === true && checkPermission(gridDto?.gridOptions.permissionDto.d), e: checkPermission(gridDto?.gridOptions.permissionDto.e), i: checkPermission(gridDto?.gridOptions.permissionDto.i), } const onActionEdit = () => { navigate( ROUTES_ENUM.protected.admin.formEdit .replace(':listFormCode', listFormCode) .replace(':id', rowId!), ) } const onActionNew = () => { navigate(ROUTES_ENUM.protected.admin.formNew.replace(':listFormCode', listFormCode)) } return (
{ // Forward ref to parent if (typeof ref === 'function') { ref(element) } else if (ref) { ref.current = element } // Also save to local ref for Intersection Observer cardElementRef.current = element }} tabIndex={tabIndex} onClick={onClick} onDoubleClick={onDoubleClick} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} className={classNames( "bg-white dark:bg-neutral-800 border flex flex-col transition-all duration-200 outline-none", { "border-blue-500 shadow-lg ring-2 ring-blue-300": isSelected, "border-blue-400 shadow-md": isFocused && !isSelected, "border-gray-300 dark:border-neutral-600": !isSelected && !isFocused, "shadow-md transform scale-[1.02]": isHovered, "cursor-pointer": selectionMode !== 'none', } )} role="article" aria-selected={isSelected} aria-label={`Card ${rowId}`} >
{selectionMode !== 'none' && onSelectionChange && (
{ e.stopPropagation() onSelectionChange(!isSelected) }} > { onSelectionChange(checked) }} className="cursor-pointer" aria-label="Select card" />
)} {!isSubForm && (

{translate('::' + gridDto?.gridOptions.title)}

)}
{permissionResults && ( ({})} refreshData={refreshData} getSelectedRowKeys={() => [rowId]} getSelectedRowsData={() => [row]} getFilter={() => null} onActionEdit={onActionEdit} onActionNew={onActionNew} /> )}
{shouldRenderForm ? ( ) : (
)}
) }) CardItem.displayName = 'CardItem' // Memoize to prevent unnecessary re-renders when parent re-renders export default memo(CardItem, (prevProps, nextProps) => { // Custom comparison to prevent re-render when not needed return ( prevProps.row === nextProps.row && prevProps.isSelected === nextProps.isSelected && prevProps.isFocused === nextProps.isFocused && prevProps.isHovered === nextProps.isHovered && prevProps.gridDto === nextProps.gridDto && prevProps.listFormCode === nextProps.listFormCode ) })