erp-platform/ui/src/views/list/Card.tsx
2025-10-22 17:58:27 +03:00

360 lines
12 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, useState } from 'react'
import { GridDto } from '@/proxy/form/models'
import { useListFormCustomDataSource } from '@/shared/useListFormCustomDataSource'
import { Button, Pagination, Select } from '@/components/ui'
import classNames from 'classnames'
import { FaCog, FaSearch } from 'react-icons/fa'
import { ROUTES_ENUM } from '@/routes/route.constant'
import CustomStore from 'devextreme/data/custom_store'
import { usePermission } from '@/utils/hooks/usePermission'
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'
import { usePWA } from '@/utils/hooks/usePWA'
import CardItem from './CardItem'
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 { checkPermission } = usePermission()
const isPwaMode = usePWA()
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 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="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>
{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>
)}
</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={getLookupDataSource}
/>
)
})}
</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