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

313 lines
10 KiB
TypeScript
Raw Normal View History

2025-10-22 14:58:27 +00:00
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"
2026-01-05 19:30:23 +00:00
import { useMemo, useRef, useState, forwardRef, memo, useEffect } from "react"
2025-10-22 14:58:27 +00:00
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"
2025-11-30 16:56:36 +00:00
import { Checkbox } from "@/components/ui"
import classNames from "classnames"
2025-10-22 14:58:27 +00:00
2025-11-30 16:56:36 +00:00
interface CardItemProps {
2025-10-22 14:58:27 +00:00
isSubForm?: boolean
row: any
gridDto: GridDto
listFormCode: string
dataSource: CustomStore
refreshData: () => void
getCachedLookupDataSource: (editorOptions: any, colData: any, row?: any) => any
2025-11-30 16:56:36 +00:00
// 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<HTMLDivElement, CardItemProps>((
{
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])
2025-10-22 14:58:27 +00:00
const [formData, setFormData] = useState(row)
const refForm = useRef<FormRef>(null)
2026-01-05 19:30:23 +00:00
const cardElementRef = useRef<HTMLDivElement | null>(null)
2025-10-22 14:58:27 +00:00
const navigate = useNavigate()
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const isPwaMode = usePWA()
2026-01-05 19:30:23 +00:00
// 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()
}
}, [])
2025-10-22 14:58:27 +00:00
const keyField = gridDto.gridOptions.keyFieldName
const rowId = row[keyField!]
2026-01-05 19:30:23 +00:00
// Form Items - memoized to prevent recalculation on every render
2025-10-22 14:58:27 +00:00
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,
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])
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 (
2025-11-30 16:56:36 +00:00
<div
2026-01-05 19:30:23 +00:00
ref={(element) => {
// 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
}}
2025-11-30 16:56:36 +00:00
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}`}
>
2025-10-22 14:58:27 +00:00
<div
2025-11-30 16:56:36 +00:00
className={`flex items-center pt-2 px-2 gap-2 ${isSubForm ? 'justify-between' : 'justify-between'}`}
2025-10-22 14:58:27 +00:00
>
2025-11-30 16:56:36 +00:00
<div className="flex items-center gap-2">
{selectionMode !== 'none' && onSelectionChange && (
2025-11-30 17:15:04 +00:00
<div
onClick={(e) => {
e.stopPropagation()
onSelectionChange(!isSelected)
}}
>
2025-11-30 16:56:36 +00:00
<Checkbox
checked={isSelected}
onChange={(checked: boolean) => {
onSelectionChange(checked)
}}
className="cursor-pointer"
aria-label="Select card"
/>
</div>
)}
{!isSubForm && (
<h3>{translate('::' + gridDto?.gridOptions.title)}</h3>
)}
</div>
2025-10-22 14:58:27 +00:00
{permissionResults && (
<FormButtons
isSubForm={true}
mode="view"
listFormCode={listFormCode}
id={rowId}
gridDto={gridDto}
commandColumnData={{ buttons: [] }}
dataSource={dataSource}
permissions={permissionResults}
handleSubmit={() => ({})}
refreshData={refreshData}
getSelectedRowKeys={() => [rowId]}
getSelectedRowsData={() => [row]}
getFilter={() => null}
onActionEdit={onActionEdit}
onActionNew={onActionNew}
/>
)}
</div>
<div className="flex-grow">
2026-01-05 19:30:23 +00:00
{shouldRenderForm ? (
<FormDevExpress
listFormCode={listFormCode}
mode="view"
refForm={refForm}
formData={formData}
formItems={formItems}
setFormData={setFormData}
/>
) : (
<div className="p-4">
<div className="animate-pulse space-y-2">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2"></div>
</div>
</div>
)}
2025-10-22 14:58:27 +00:00
</div>
</div>
)
2025-11-30 16:56:36 +00:00
})
CardItem.displayName = 'CardItem'
2025-10-22 14:58:27 +00:00
2026-01-05 19:30:23 +00:00
// 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
)
})