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

314 lines
No EOL
10 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 { 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<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])
const [formData, setFormData] = useState(row)
const refForm = useRef<FormRef>(null)
const cardElementRef = useRef<HTMLDivElement | null>(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 (
<div
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
}}
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}`}
>
<div
className={`flex items-center pt-2 px-2 gap-2 ${isSubForm ? 'justify-between' : 'justify-between'}`}
>
<div className="flex items-center gap-2">
{selectionMode !== 'none' && onSelectionChange && (
<div
onClick={(e) => {
e.stopPropagation()
onSelectionChange(!isSelected)
}}
>
<Checkbox
checked={isSelected}
onChange={(checked: boolean) => {
onSelectionChange(checked)
}}
className="cursor-pointer"
aria-label="Select card"
/>
</div>
)}
{!isSubForm && (
<h3>{translate('::' + gridDto?.gridOptions.title)}</h3>
)}
</div>
{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">
{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>
)}
</div>
</div>
)
})
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
)
})