erp-platform/ui/src/views/list/Card.tsx
Sedat ÖZTÜRK 6e3f58ce9d Grid ve Form komponentlerine Script özelliği
FormView, FormNew, Grid Popup için Script özelliği eklendi. Ayrıca itemlara buton eklenebiliyor.
Sadece textbox olan inputlara ekleniyor. Diğer komponenler için render özelliği kullanılması gerekiyor.
2025-09-24 20:46:03 +03:00

513 lines
17 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { EditingFormItemDto, GridDto, PlatformEditorTypes } from '@/proxy/form/models'
import { captionize } from 'devextreme/core/utils/inflector'
import { useListFormCustomDataSource } from '@/shared/useListFormCustomDataSource'
import { Button, Pagination, Select } from '@/components/ui'
import classNames from 'classnames'
import { FaSearch } from 'react-icons/fa'
import FormDevExpress from '../form/FormDevExpress'
import { GroupItem } from 'devextreme/ui/form'
import { Form as FormDx } from 'devextreme-react/form'
import FormButtons from '../form/FormButtons'
import { Link, useNavigate } from 'react-router-dom'
import { ROUTES_ENUM } from '@/routes/route.constant'
import CustomStore from 'devextreme/data/custom_store'
import { PermissionResults, SimpleItemWithColData } from '../form/types'
import { usePermission } from '@/utils/hooks/usePermission'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useLookupDataSource } from '../form/useLookupDataSource'
import { Container, Loading } from '@/components/shared'
import WidgetGroup from '@/components/common/WidgetGroup'
import { GridExtraFilterState } from './Utils'
import { useStoreActions, useStoreState } from '@/store/store'
const CardItem = ({
isSubForm,
row,
gridDto,
listFormCode,
dataSource,
refreshData,
getCachedLookupDataSource,
}: {
isSubForm?: boolean
row: any
gridDto: GridDto
listFormCode: string
dataSource: CustomStore
refreshData: () => void
getCachedLookupDataSource: (editorOptions: any, colData: any) => any
}) => {
const [formData, setFormData] = useState(row)
const refForm = useRef<FormDx>(null)
const navigate = useNavigate()
const { checkPermission } = usePermission()
const { translate } = useLocalization()
const keyField = gridDto.gridOptions.keyFieldName
const rowId = row[keyField!]
// Form Items
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,
script: i.script,
}
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 (
<div className="bg-white dark:bg-neutral-800 border dark:border-neutral-700 flex flex-col">
<div
className={`flex items-center pt-2 px-2 ${isSubForm ? 'justify-end' : 'justify-between'}`}
>
{!isSubForm && <h3>{translate('::' + gridDto?.gridOptions.title)}</h3>}
{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">
<FormDevExpress
mode="view"
refForm={refForm}
formData={formData}
formItems={formItems}
setFormData={setFormData}
listFormCode={listFormCode}
/>
</div>
</div>
)
}
interface CardProps {
listFormCode: string
searchParams?: URLSearchParams
isSubForm?: boolean
level?: number
refreshData?: () => Promise<void>
gridDto?: GridDto
}
type Option = {
value: number
label: string
}
const Card = (props: CardProps) => {
const { listFormCode, searchParams, gridDto } = props
const { createSelectDataSource } = useListFormCustomDataSource({})
const [data, setData] = useState<any[]>([])
const [totalCount, setTotalCount] = useState(0)
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [pageSizeOptions, setPageSizeOptions] = useState<Option[]>([])
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
const { getLookupDataSource } = useLookupDataSource({ listFormCode })
const [layoutCount, setLayoutCount] = useState(4)
const [searchText, setSearchText] = useState('')
const [prevValue, setPrevValue] = useState('')
const [loading, setLoading] = useState(false)
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
const { states } = useStoreState((state) => state.base.lists)
const { setStates } = useStoreActions((a) => a.base.lists)
// props.searchParams varsa onunla başlat
const [urlSearchParams, setUrlSearchParams] = useState<URLSearchParams>(
searchParams ? new URLSearchParams(searchParams) : new URLSearchParams(),
)
const lookupCache = useRef<Map<string, any>>(new Map())
const getCachedLookupDataSource = useCallback(
(editorOptions: any, colData: any) => {
const key = colData?.fieldName
if (!key) return null
if (!lookupCache.current.has(key)) {
lookupCache.current.set(key, getLookupDataSource(editorOptions, colData))
}
return lookupCache.current.get(key)
},
[getLookupDataSource],
)
const onPageSizeSelect = ({ value }: Option) => {
setPageSize(value)
setCurrentPage(1)
}
const onPageChange = (page: number) => {
setCurrentPage(page)
}
const onFilter = useCallback(
(value?: string) => {
const text = value !== undefined ? value.trim() : searchText.trim()
if (!gridDto?.columnFormats) return
const newParams = new URLSearchParams(urlSearchParams.toString())
if (!text) {
newParams.delete('filter')
setUrlSearchParams(newParams)
return
}
const merged = gridDto.columnFormats
.filter(
(col) =>
col.dataType === 'string' &&
col.visible &&
col.width &&
col.allowSearch &&
col.width > 0,
)
.map((col) => [col.fieldName, 'contains', text])
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, 'or', f]
}, null as any)
}
if (filter) {
newParams.set('filter', JSON.stringify(filter))
} else {
newParams.delete('filter')
}
setUrlSearchParams(newParams)
},
[gridDto, urlSearchParams, searchText],
)
useEffect(() => {
if (gridDto) {
const dataSource = createSelectDataSource(gridDto.gridOptions, listFormCode, urlSearchParams)
setGridDataSource(dataSource)
//listFormStates
const listFormStates = states.find((a) => a.listFormCode === listFormCode)
if (listFormStates) {
setLayoutCount(listFormStates.cardLayoutColumn || 4)
} else {
setLayoutCount(gridDto.gridOptions.layoutDto.cardLayoutColumn || 4)
}
}
}, [gridDto, listFormCode, urlSearchParams, createSelectDataSource])
const loadData = useCallback(() => {
if (!gridDataSource) return
setLoading(true)
const loadOptions = {
skip: (currentPage - 1) * pageSize,
take: pageSize,
requireTotalCount: true,
}
gridDataSource
.load(loadOptions)
.then((res: any) => {
setData(res.data)
setTotalCount(res.totalCount || 0)
setLoading(false)
})
.catch(() => {
setLoading(false)
})
}, [gridDataSource, currentPage, pageSize])
useEffect(() => {
if (gridDataSource) {
loadData()
}
}, [gridDataSource, loadData])
useEffect(() => {
if (!gridDto) return
const pagerOptions = gridDto.gridOptions.pagerOptionDto
const allowedSizes = pagerOptions?.allowedPageSizes
?.split(',')
.map((s) => Number(s.trim()))
.filter((n) => !isNaN(n) && n > 0) || [10, 20, 50, 100]
setPageSizeOptions(allowedSizes.map((size) => ({ value: size, label: `${size} page` })))
}, [gridDto, listFormCode, searchParams])
useEffect(() => {
if (data.length > 0) {
window.scrollTo({ top: 0, behavior: 'smooth' })
if (data.length < 6) {
setLayoutCount(data.length)
}
}
}, [data])
if (!gridDto) return null
return (
<>
<WidgetGroup widgetGroups={gridDto.widgets || []} />
<Container>
<div className="p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700 ">
<div className="flex justify-end items-center">
<div className="relative py-1 flex gap-1 border-b-1">
<FaSearch className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-sm" />
<input
type="text"
placeholder="Search..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onFilter(e.currentTarget.value)
setPrevValue(e.currentTarget.value.trim()) // Enter ile tetiklenirse güncelle
}
}}
onBlur={(e) => {
const newValue = e.currentTarget.value.trim()
// 1. Değer değişmemişse => hiçbir şey yapma
if (newValue === prevValue) return
// 2. Yeni değer boş, ama eskiden değer vardı => filtre temizle
// 3. Yeni değer dolu ve eskisinden farklı => filtre uygula
onFilter(newValue)
setPrevValue(newValue)
}}
className="p-1 pl-6 pr-2 border border-1 outline-none text-xs text-gray-700 dark:text-gray-200 placeholder-gray-400 rounded"
/>
<Button
size="xs"
variant={layoutCount === 1 ? 'solid' : 'default'}
className="text-sm"
onClick={() => {
setLayoutCount(1)
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 1 })
}}
title="1 Sütunda Göster"
>
1
</Button>
<Button
size="xs"
variant={layoutCount === 2 ? 'solid' : 'default'}
className="text-sm"
onClick={() => {
setLayoutCount(2)
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 2 })
}}
title="2 Sütunda Göster"
>
2
</Button>
<Button
size="xs"
variant={layoutCount === 3 ? 'solid' : 'default'}
className="text-sm"
onClick={() => {
setLayoutCount(3)
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 3 })
}}
title="3 Sütunda Göster"
>
3
</Button>
<Button
size="xs"
variant={layoutCount === 4 ? 'solid' : 'default'}
className="text-sm"
onClick={() => {
setLayoutCount(4)
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 4 })
}}
title="4 Sütunda Göster"
>
4
</Button>
<Button
size="xs"
variant={layoutCount === 5 ? 'solid' : 'default'}
className="text-sm"
onClick={() => {
setLayoutCount(5)
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 5 })
}}
title="5 Sütunda Göster"
>
5
</Button>
</div>
</div>
{loading ? (
<Loading loading={true} />
) : data.length === 0 ? (
<div className="text-center py-12">
<FaSearch className="mx-auto h-10 w-10 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-gray-200">
Uygun kayıt bulunamadı
</h3>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Farklı filtreler deneyin veya yeni bir kayıt ekleyin.
</p>
</div>
) : (
<div className={`bg-transparent grid grid-cols-1 lg:grid-cols-${layoutCount} gap-4`}>
{gridDataSource &&
data.map((row, idx) => {
const keyField = gridDto.gridOptions.keyFieldName
const rowId = row[keyField!]
return (
<CardItem
isSubForm={true}
key={rowId || idx}
row={row}
gridDto={gridDto}
listFormCode={listFormCode}
dataSource={gridDataSource}
refreshData={loadData}
getCachedLookupDataSource={getCachedLookupDataSource}
/>
)
})}
</div>
)}
{gridDto.gridOptions.pagerOptionDto?.visible && totalCount > pageSize && (
<div className={classNames('flex items-center justify-between border-t-1 gap-4 mt-2')}>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-600 dark:text-gray-300">
Toplam {totalCount} kayıt
</span>
<Select
size="xs"
menuPlacement="top"
value={pageSizeOptions.find((o) => o.value === pageSize)}
options={pageSizeOptions}
onChange={(selected) => onPageSizeSelect(selected as Option)}
/>
</div>
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={onPageChange}
/>
</div>
)}
</div>
</Container>
</>
)
}
export default Card