360 lines
12 KiB
TypeScript
360 lines
12 KiB
TypeScript
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
|