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

538 lines
18 KiB
TypeScript
Raw Normal View History

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2025-09-21 20:05:13 +00:00
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'
2025-09-29 08:33:51 +00:00
import { FaCog, FaSearch } from 'react-icons/fa'
import FormDevExpress from '../form/FormDevExpress'
2025-09-21 20:05:13 +00:00
import { GroupItem } from 'devextreme/ui/form'
import { Form as FormDx } from 'devextreme-react/form'
import FormButtons from '../form/FormButtons'
2025-09-29 08:33:51 +00:00
import { useNavigate } from 'react-router-dom'
import { ROUTES_ENUM } from '@/routes/route.constant'
import CustomStore from 'devextreme/data/custom_store'
2025-09-21 20:05:13 +00:00
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'
2025-09-29 08:33:51 +00:00
import { usePWA } from '@/utils/hooks/usePWA'
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 { translate } = useLocalization()
2025-09-29 08:33:51 +00:00
const { checkPermission } = usePermission()
const isPwaMode = usePWA()
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) => {
2025-09-21 20:05:13 +00:00
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))
2025-09-21 20:05:13 +00:00
.map((i: EditingFormItemDto) => {
let editorOptions = {}
const colData = gridDto.columnFormats.find((x) => x.fieldName === i.dataField)
2025-09-21 20:05:13 +00:00
try {
editorOptions = i.editorOptions && JSON.parse(i.editorOptions)
} catch {}
2025-09-21 20:05:13 +00:00
const item: SimpleItemWithColData = {
canRead: colData?.canRead ?? false,
canUpdate: colData?.canUpdate ?? false,
canCreate: colData?.canCreate ?? false,
canExport: colData?.canExport ?? false,
2025-09-21 20:05:13 +00:00
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,
2025-09-21 20:05:13 +00:00
tagBoxOptions: i.tagBoxOptions,
gridBoxOptions: i.gridBoxOptions,
script: i.script,
2025-09-21 20:05:13 +00:00
}
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}
2025-09-21 20:05:13 +00:00
listFormCode={listFormCode}
/>
</div>
</div>
)
}
2025-09-23 19:52:08 +00:00
interface CardProps {
listFormCode: string
searchParams?: URLSearchParams
isSubForm?: boolean
level?: number
refreshData?: () => Promise<void>
gridDto?: GridDto
}
type Option = {
value: number
label: string
}
2025-09-22 14:08:42 +00:00
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>>()
2025-09-22 14:08:42 +00:00
const { getLookupDataSource } = useLookupDataSource({ listFormCode })
const [layoutCount, setLayoutCount] = useState(4)
2025-09-22 14:08:42 +00:00
const [searchText, setSearchText] = useState('')
const [prevValue, setPrevValue] = useState('')
2025-09-22 14:08:42 +00:00
const [loading, setLoading] = useState(false)
2025-09-29 08:33:51 +00:00
const { checkPermission } = usePermission()
const isPwaMode = usePWA()
2025-09-22 14:08:42 +00:00
const [extraFilters, setExtraFilters] = useState<GridExtraFilterState[]>([])
const { states } = useStoreState((state) => state.admin.lists)
const { setStates } = useStoreActions((a) => a.admin.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(
2025-09-22 14:08:42 +00:00
(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)
2025-09-22 14:08:42 +00:00
},
[gridDto, urlSearchParams, searchText],
)
useEffect(() => {
if (gridDto) {
2025-09-22 14:08:42 +00:00
const dataSource = createSelectDataSource(gridDto.gridOptions, listFormCode, urlSearchParams)
setGridDataSource(dataSource)
2025-09-22 14:08:42 +00:00
2025-09-23 19:52:08 +00:00
//listFormStates
const listFormStates = states.find((a) => a.listFormCode === listFormCode)
if (listFormStates) {
setLayoutCount(listFormStates.cardLayoutColumn || 4)
} else {
setLayoutCount(gridDto.gridOptions.layoutDto.cardLayoutColumn || 4)
}
}
2025-09-22 14:08:42 +00:00
}, [gridDto, listFormCode, urlSearchParams, createSelectDataSource])
const loadData = useCallback(() => {
if (!gridDataSource) return
2025-09-22 14:08:42 +00:00
setLoading(true)
const loadOptions = {
skip: (currentPage - 1) * pageSize,
take: pageSize,
requireTotalCount: true,
}
2025-09-22 14:08:42 +00:00
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' })
2025-09-22 14:08:42 +00:00
2025-09-23 19:52:08 +00:00
if (data.length < 6) {
2025-09-22 14:08:42 +00:00
setLayoutCount(data.length)
}
}
}, [data])
if (!gridDto) return null
return (
<>
<WidgetGroup widgetGroups={gridDto.widgets || []} />
<Container>
2025-09-29 08:33:51 +00:00
<div className="bg-white dark:bg-neutral-800 dark:border-neutral-700 ">
2025-09-22 14:08:42 +00:00
<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
2025-09-22 14:08:42 +00:00
}
}}
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)
2025-09-22 14:08:42 +00:00
}}
2025-09-23 19:52:08 +00:00
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"
2025-09-22 14:08:42 +00:00
/>
2025-09-22 14:08:42 +00:00
<Button
size="xs"
variant={layoutCount === 1 ? 'solid' : 'default'}
className="text-sm"
onClick={() => {
setLayoutCount(1)
setStates({ listFormCode, layout: 'card', cardLayoutColumn: 1 })
}}
2025-09-22 14:08:42 +00:00
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 })
}}
2025-09-22 14:08:42 +00:00
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 })
}}
2025-09-22 14:08:42 +00:00
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 })
}}
2025-09-22 14:08:42 +00:00
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 })
}}
2025-09-22 14:08:42 +00:00
title="5 Sütunda Göster"
>
5
</Button>
2025-09-29 08:33:51 +00:00
{checkPermission(gridDto?.gridOptions.permissionDto.u) && (
<Button
size="xs"
variant={'default'}
className="text-sm"
onClick={() => {
window.open(
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(
':listFormCode',
listFormCode,
),
isPwaMode ? '_self' : '_blank',
)
}}
title="Form Manager"
>
<FaCog className="w-3 h-3" />
</Button>
)}
2025-09-22 14:08:42 +00:00
</div>
</div>
2025-09-22 14:08:42 +00:00
{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)}
/>
2025-09-22 14:08:42 +00:00
</div>
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={onPageChange}
/>
</div>
2025-09-22 14:08:42 +00:00
)}
</div>
</Container>
</>
)
}
export default Card