2025-11-30 16:56:36 +00:00
|
|
|
|
import { useCallback, useEffect, useState, useRef, KeyboardEvent, useMemo } from 'react'
|
2025-10-22 14:58:27 +00:00
|
|
|
|
import { GridDto } from '@/proxy/form/models'
|
2025-11-30 16:56:36 +00:00
|
|
|
|
import { Button, Pagination, Select, Checkbox } from '@/components/ui'
|
2025-09-21 14:42:24 +00:00
|
|
|
|
import classNames from 'classnames'
|
2025-11-30 16:56:36 +00:00
|
|
|
|
import { FaCog, FaSearch, FaSortAmountDown, FaSortAmountUp } from 'react-icons/fa'
|
2025-09-21 15:11:12 +00:00
|
|
|
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
|
|
|
|
import CustomStore from 'devextreme/data/custom_store'
|
|
|
|
|
|
import { usePermission } from '@/utils/hooks/usePermission'
|
2025-09-21 21:32:05 +00:00
|
|
|
|
import { useLookupDataSource } from '../form/useLookupDataSource'
|
2025-09-22 09:11:15 +00:00
|
|
|
|
import { Container, Loading } from '@/components/shared'
|
|
|
|
|
|
import WidgetGroup from '@/components/common/WidgetGroup'
|
|
|
|
|
|
import { GridExtraFilterState } from './Utils'
|
2025-09-23 12:48:54 +00:00
|
|
|
|
import { useStoreActions, useStoreState } from '@/store/store'
|
2025-09-29 08:33:51 +00:00
|
|
|
|
import { usePWA } from '@/utils/hooks/usePWA'
|
2025-10-22 14:58:27 +00:00
|
|
|
|
import CardItem from './CardItem'
|
2025-11-08 20:22:50 +00:00
|
|
|
|
import { layoutTypes } from '../admin/listForm/edit/types'
|
2025-11-09 10:30:15 +00:00
|
|
|
|
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
2025-09-21 14:42:24 +00:00
|
|
|
|
|
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
|
2025-11-07 23:57:04 +00:00
|
|
|
|
const { createSelectDataSource } = useListFormCustomDataSource({} as any)
|
2025-09-21 14:42:24 +00:00
|
|
|
|
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[]>([])
|
2025-09-22 09:11:15 +00:00
|
|
|
|
const [gridDataSource, setGridDataSource] = useState<CustomStore<any, any>>()
|
2025-09-22 14:08:42 +00:00
|
|
|
|
const { getLookupDataSource } = useLookupDataSource({ listFormCode })
|
2025-09-22 09:11:15 +00:00
|
|
|
|
const [layoutCount, setLayoutCount] = useState(4)
|
2025-09-22 14:08:42 +00:00
|
|
|
|
const [searchText, setSearchText] = useState('')
|
2025-09-23 14:05:42 +00:00
|
|
|
|
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[]>([])
|
2025-11-30 16:56:36 +00:00
|
|
|
|
|
|
|
|
|
|
// Selection features - gridDto değiştiğinde güncellenmeli
|
|
|
|
|
|
const [selectedKeys, setSelectedKeys] = useState<Set<any>>(new Set())
|
|
|
|
|
|
const selectionMode: 'none' | 'single' | 'multiple' = 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])
|
|
|
|
|
|
|
|
|
|
|
|
// Sorting features
|
|
|
|
|
|
const [sortColumn, setSortColumn] = useState<string | null>(null)
|
|
|
|
|
|
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')
|
|
|
|
|
|
|
|
|
|
|
|
// Focus and keyboard navigation
|
|
|
|
|
|
const [focusedCardIndex, setFocusedCardIndex] = useState<number>(-1)
|
|
|
|
|
|
const cardRefs = useRef<(HTMLDivElement | null)[]>([])
|
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
|
|
|
|
// Hover state
|
|
|
|
|
|
const [hoveredCardKey, setHoveredCardKey] = useState<any>(null)
|
2025-09-21 14:42:24 +00:00
|
|
|
|
|
2025-10-02 10:08:48 +00:00
|
|
|
|
const { states } = useStoreState((state) => state.admin.lists)
|
|
|
|
|
|
const { setStates } = useStoreActions((a) => a.admin.lists)
|
2025-09-23 12:48:54 +00:00
|
|
|
|
|
2025-09-23 14:05:42 +00:00
|
|
|
|
// props.searchParams varsa onunla başlat
|
2025-11-30 16:56:36 +00:00
|
|
|
|
const [urlSearchParams, setUrlSearchParams] = useState<URLSearchParams>(() => {
|
|
|
|
|
|
const params = searchParams ? new URLSearchParams(searchParams) : new URLSearchParams()
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize sort state from URL params
|
|
|
|
|
|
const sortParam = params.get('sort')
|
|
|
|
|
|
if (sortParam) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const sortArray = JSON.parse(sortParam)
|
|
|
|
|
|
if (sortArray && sortArray.length > 0) {
|
|
|
|
|
|
setSortColumn(sortArray[0].selector)
|
|
|
|
|
|
setSortOrder(sortArray[0].desc ? 'desc' : 'asc')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Sort param parse error:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return params
|
|
|
|
|
|
})
|
2025-09-23 14:05:42 +00:00
|
|
|
|
|
2025-09-21 14:42:24 +00:00
|
|
|
|
const onPageSizeSelect = ({ value }: Option) => {
|
|
|
|
|
|
setPageSize(value)
|
|
|
|
|
|
setCurrentPage(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const onPageChange = (page: number) => {
|
|
|
|
|
|
setCurrentPage(page)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-30 16:56:36 +00:00
|
|
|
|
const toggleSort = (columnName: string) => {
|
|
|
|
|
|
const newParams = new URLSearchParams(urlSearchParams.toString())
|
|
|
|
|
|
|
|
|
|
|
|
if (sortColumn === columnName) {
|
|
|
|
|
|
const newOrder = sortOrder === 'asc' ? 'desc' : 'asc'
|
|
|
|
|
|
setSortOrder(newOrder)
|
|
|
|
|
|
newParams.set('sort', JSON.stringify([{ selector: columnName, desc: newOrder === 'desc' }]))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSortColumn(columnName)
|
|
|
|
|
|
setSortOrder('asc')
|
|
|
|
|
|
newParams.set('sort', JSON.stringify([{ selector: columnName, desc: false }]))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setUrlSearchParams(newParams)
|
|
|
|
|
|
setCurrentPage(1) // Reset to first page when sorting
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-30 17:15:04 +00:00
|
|
|
|
const handleSelectAll = async (checked: boolean) => {
|
2025-11-30 16:56:36 +00:00
|
|
|
|
if (checked) {
|
|
|
|
|
|
const keyField = gridDto?.gridOptions.keyFieldName
|
2025-11-30 17:15:04 +00:00
|
|
|
|
const selectAllMode = gridDto?.gridOptions?.selectionDto?.selectAllMode?.toLowerCase()
|
|
|
|
|
|
|
|
|
|
|
|
if (!keyField) return
|
|
|
|
|
|
|
|
|
|
|
|
if (selectAllMode === 'allpages') {
|
|
|
|
|
|
// Tüm sayfalardan tüm kayıtları al
|
|
|
|
|
|
if (!gridDataSource) return
|
|
|
|
|
|
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const loadOptions = {
|
|
|
|
|
|
skip: 0,
|
|
|
|
|
|
take: totalCount, // Tüm kayıtları al
|
|
|
|
|
|
requireTotalCount: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const res: any = await gridDataSource.load(loadOptions)
|
|
|
|
|
|
const allKeys = new Set(res.data.map((row: any) => row[keyField]))
|
|
|
|
|
|
setSelectedKeys(allKeys)
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Select all pages error:', err)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Sadece mevcut sayfadaki kayıtları seç (page veya default)
|
2025-11-30 16:56:36 +00:00
|
|
|
|
const allKeys = new Set(data.map(row => row[keyField]))
|
|
|
|
|
|
setSelectedKeys(allKeys)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSelectedKeys(new Set())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleCardSelection = (key: any, checked: boolean) => {
|
|
|
|
|
|
const newSelection = new Set(selectedKeys)
|
|
|
|
|
|
if (checked) {
|
|
|
|
|
|
if (selectionMode === 'single') {
|
|
|
|
|
|
newSelection.clear()
|
|
|
|
|
|
}
|
|
|
|
|
|
newSelection.add(key)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newSelection.delete(key)
|
|
|
|
|
|
}
|
|
|
|
|
|
setSelectedKeys(newSelection)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleCardClick = (key: any, index: number, event: React.MouseEvent) => {
|
|
|
|
|
|
setFocusedCardIndex(index)
|
|
|
|
|
|
if (selectionMode !== 'none' && event.ctrlKey) {
|
|
|
|
|
|
handleCardSelection(key, !selectedKeys.has(key))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleCardDoubleClick = (key: any, row: any) => {
|
|
|
|
|
|
// Navigate to edit page on double click
|
|
|
|
|
|
const navigate = window.location
|
|
|
|
|
|
window.open(
|
|
|
|
|
|
ROUTES_ENUM.protected.admin.formEdit
|
|
|
|
|
|
.replace(':listFormCode', listFormCode)
|
|
|
|
|
|
.replace(':id', key),
|
|
|
|
|
|
isPwaMode ? '_self' : '_blank'
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleExport = async (format: 'xlsx' | 'csv' | 'pdf', selectedOnly: boolean = false) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const exportData = selectedOnly
|
|
|
|
|
|
? data.filter(row => {
|
|
|
|
|
|
const keyField = gridDto?.gridOptions.keyFieldName
|
|
|
|
|
|
return keyField && selectedKeys.has(row[keyField])
|
|
|
|
|
|
})
|
|
|
|
|
|
: data
|
|
|
|
|
|
|
|
|
|
|
|
if (exportData.length === 0) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (format === 'xlsx' || format === 'csv') {
|
|
|
|
|
|
const [{ Workbook }, { saveAs }] = await Promise.all([
|
|
|
|
|
|
import('exceljs'),
|
|
|
|
|
|
import('file-saver'),
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const workbook = new Workbook()
|
|
|
|
|
|
const worksheet = workbook.addWorksheet(`${listFormCode}_sheet`)
|
|
|
|
|
|
|
|
|
|
|
|
// Header row - visible columns only
|
|
|
|
|
|
const visibleColumns = gridDto?.columnFormats?.filter(
|
|
|
|
|
|
col => col.visible && col.dataType !== 'buttons'
|
|
|
|
|
|
) || []
|
|
|
|
|
|
|
|
|
|
|
|
const headerRow = visibleColumns.map(col => col.fieldName)
|
|
|
|
|
|
worksheet.addRow(headerRow)
|
|
|
|
|
|
worksheet.getRow(1).font = { bold: true }
|
|
|
|
|
|
|
|
|
|
|
|
// Data rows
|
|
|
|
|
|
exportData.forEach(row => {
|
|
|
|
|
|
const dataRow = visibleColumns.map(col => row[col.fieldName!])
|
|
|
|
|
|
worksheet.addRow(dataRow)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-fit columns
|
|
|
|
|
|
worksheet.columns.forEach((column: any) => {
|
|
|
|
|
|
column.width = 15
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (format === 'xlsx') {
|
|
|
|
|
|
const buffer = await workbook.xlsx.writeBuffer()
|
|
|
|
|
|
saveAs(
|
|
|
|
|
|
new Blob([buffer], { type: 'application/octet-stream' }),
|
|
|
|
|
|
`${listFormCode}_export${selectedOnly ? '_selected' : ''}.xlsx`,
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const buffer = await workbook.csv.writeBuffer()
|
|
|
|
|
|
saveAs(
|
|
|
|
|
|
new Blob([buffer], { type: 'application/octet-stream' }),
|
|
|
|
|
|
`${listFormCode}_export${selectedOnly ? '_selected' : ''}.csv`,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (format === 'pdf') {
|
|
|
|
|
|
const [jspdfMod] = await Promise.all([
|
|
|
|
|
|
import('jspdf'),
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const JsPDFCtor = (jspdfMod as any).default ?? (jspdfMod as any).jsPDF
|
|
|
|
|
|
const doc = new JsPDFCtor({ orientation: 'landscape' })
|
|
|
|
|
|
|
|
|
|
|
|
// Header
|
|
|
|
|
|
const visibleColumns = gridDto?.columnFormats?.filter(
|
|
|
|
|
|
col => col.visible && col.dataType !== 'buttons'
|
|
|
|
|
|
) || []
|
|
|
|
|
|
|
|
|
|
|
|
let yPos = 10
|
|
|
|
|
|
doc.setFontSize(16)
|
|
|
|
|
|
doc.text(gridDto?.gridOptions.title || listFormCode, 10, yPos)
|
|
|
|
|
|
yPos += 10
|
|
|
|
|
|
|
|
|
|
|
|
// Table
|
|
|
|
|
|
doc.setFontSize(10)
|
|
|
|
|
|
const headers = visibleColumns.map(col => col.fieldName)
|
|
|
|
|
|
const rows = exportData.map(row =>
|
|
|
|
|
|
visibleColumns.map(col => String(row[col.fieldName!] || ''))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Simple table rendering
|
|
|
|
|
|
doc.text(headers.join(' | '), 10, yPos)
|
|
|
|
|
|
yPos += 7
|
|
|
|
|
|
|
|
|
|
|
|
rows.forEach(row => {
|
|
|
|
|
|
if (yPos > 190) {
|
|
|
|
|
|
doc.addPage()
|
|
|
|
|
|
yPos = 10
|
|
|
|
|
|
}
|
|
|
|
|
|
doc.text(row.join(' | '), 10, yPos)
|
|
|
|
|
|
yPos += 7
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
doc.save(`${listFormCode}_export${selectedOnly ? '_selected' : ''}.pdf`)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('Export error:', err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
|
if (data.length === 0) return
|
|
|
|
|
|
|
|
|
|
|
|
const keyField = gridDto?.gridOptions.keyFieldName
|
|
|
|
|
|
if (!keyField) return
|
|
|
|
|
|
|
|
|
|
|
|
switch (e.key) {
|
|
|
|
|
|
case 'ArrowRight':
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
if (focusedCardIndex < data.length - 1) {
|
|
|
|
|
|
const newIndex = focusedCardIndex + 1
|
|
|
|
|
|
setFocusedCardIndex(newIndex)
|
|
|
|
|
|
cardRefs.current[newIndex]?.focus()
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
case 'ArrowLeft':
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
if (focusedCardIndex > 0) {
|
|
|
|
|
|
const newIndex = focusedCardIndex - 1
|
|
|
|
|
|
setFocusedCardIndex(newIndex)
|
|
|
|
|
|
cardRefs.current[newIndex]?.focus()
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
case 'ArrowDown':
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
if (focusedCardIndex + layoutCount < data.length) {
|
|
|
|
|
|
const newIndex = focusedCardIndex + layoutCount
|
|
|
|
|
|
setFocusedCardIndex(newIndex)
|
|
|
|
|
|
cardRefs.current[newIndex]?.focus()
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
case 'ArrowUp':
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
if (focusedCardIndex - layoutCount >= 0) {
|
|
|
|
|
|
const newIndex = focusedCardIndex - layoutCount
|
|
|
|
|
|
setFocusedCardIndex(newIndex)
|
|
|
|
|
|
cardRefs.current[newIndex]?.focus()
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
case 'Enter':
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
if (focusedCardIndex >= 0 && focusedCardIndex < data.length) {
|
|
|
|
|
|
const row = data[focusedCardIndex]
|
|
|
|
|
|
handleCardDoubleClick(row[keyField], row)
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
case ' ': // Space key for selection
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
if (selectionMode !== 'none' && focusedCardIndex >= 0) {
|
|
|
|
|
|
const key = data[focusedCardIndex][keyField]
|
|
|
|
|
|
handleCardSelection(key, !selectedKeys.has(key))
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 14:05:42 +00:00
|
|
|
|
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')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 12:48:54 +00:00
|
|
|
|
setUrlSearchParams(newParams)
|
2025-09-22 14:08:42 +00:00
|
|
|
|
},
|
|
|
|
|
|
[gridDto, urlSearchParams, searchText],
|
|
|
|
|
|
)
|
2025-09-21 14:42:24 +00:00
|
|
|
|
|
2025-09-21 15:11:12 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (gridDto) {
|
2025-11-08 20:22:50 +00:00
|
|
|
|
const dataSource = createSelectDataSource(
|
|
|
|
|
|
gridDto.gridOptions,
|
|
|
|
|
|
listFormCode,
|
|
|
|
|
|
urlSearchParams,
|
2025-11-11 11:50:54 +00:00
|
|
|
|
layoutTypes.card
|
2025-11-08 20:22:50 +00:00
|
|
|
|
)
|
2025-09-22 09:11:15 +00:00
|
|
|
|
setGridDataSource(dataSource)
|
2025-09-22 14:08:42 +00:00
|
|
|
|
|
2025-09-23 19:52:08 +00:00
|
|
|
|
//listFormStates
|
2025-09-23 12:48:54 +00:00
|
|
|
|
const listFormStates = states.find((a) => a.listFormCode === listFormCode)
|
|
|
|
|
|
if (listFormStates) {
|
|
|
|
|
|
setLayoutCount(listFormStates.cardLayoutColumn || 4)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setLayoutCount(gridDto.gridOptions.layoutDto.cardLayoutColumn || 4)
|
|
|
|
|
|
}
|
2025-09-21 15:11:12 +00:00
|
|
|
|
}
|
2025-11-30 16:56:36 +00:00
|
|
|
|
}, [gridDto, listFormCode, urlSearchParams, createSelectDataSource, states])
|
2025-09-21 15:11:12 +00:00
|
|
|
|
|
|
|
|
|
|
const loadData = useCallback(() => {
|
2025-09-22 09:11:15 +00:00
|
|
|
|
if (!gridDataSource) return
|
2025-09-22 14:08:42 +00:00
|
|
|
|
setLoading(true)
|
2025-09-21 14:42:24 +00:00
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
})
|
2025-09-22 09:11:15 +00:00
|
|
|
|
}, [gridDataSource, currentPage, pageSize])
|
2025-09-21 14:42:24 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-09-22 09:11:15 +00:00
|
|
|
|
if (gridDataSource) {
|
2025-09-21 15:11:12 +00:00
|
|
|
|
loadData()
|
2025-11-30 17:15:04 +00:00
|
|
|
|
|
|
|
|
|
|
// selectionMode = page ise sayfa değiştiğinde seçimi temizle
|
|
|
|
|
|
const selectAllMode = gridDto?.gridOptions?.selectionDto?.selectAllMode?.toLowerCase()
|
|
|
|
|
|
if (selectAllMode === 'page') {
|
|
|
|
|
|
setSelectedKeys(new Set())
|
|
|
|
|
|
}
|
2025-09-21 15:11:12 +00:00
|
|
|
|
}
|
2025-11-30 17:15:04 +00:00
|
|
|
|
}, [gridDataSource, loadData, currentPage, gridDto])
|
2025-09-21 14:42:24 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!gridDto) return
|
|
|
|
|
|
|
|
|
|
|
|
const pagerOptions = gridDto.gridOptions.pagerOptionDto
|
2025-09-22 09:11:15 +00:00
|
|
|
|
const allowedSizes = pagerOptions?.allowedPageSizes
|
|
|
|
|
|
?.split(',')
|
|
|
|
|
|
.map((s) => Number(s.trim()))
|
|
|
|
|
|
.filter((n) => !isNaN(n) && n > 0) || [10, 20, 50, 100]
|
2025-09-21 14:42:24 +00:00
|
|
|
|
|
|
|
|
|
|
setPageSizeOptions(allowedSizes.map((size) => ({ value: size, label: `${size} page` })))
|
|
|
|
|
|
}, [gridDto, listFormCode, searchParams])
|
|
|
|
|
|
|
2025-09-22 09:11:15 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (data.length > 0) {
|
|
|
|
|
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [data])
|
|
|
|
|
|
|
2025-09-21 14:42:24 +00:00
|
|
|
|
if (!gridDto) return null
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
2025-09-22 09:11:15 +00:00
|
|
|
|
<WidgetGroup widgetGroups={gridDto.widgets || []} />
|
|
|
|
|
|
|
|
|
|
|
|
<Container>
|
2025-11-30 16:56:36 +00:00
|
|
|
|
<div
|
|
|
|
|
|
ref={containerRef}
|
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
|
className="outline-none"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex justify-between items-center mb-2">
|
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
|
{selectionMode === 'multiple' && gridDto?.gridOptions?.selectionDto?.allowSelectAll && (
|
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
|
<Checkbox
|
2025-11-30 17:15:04 +00:00
|
|
|
|
checked={
|
|
|
|
|
|
selectedKeys.size > 0 &&
|
|
|
|
|
|
(gridDto?.gridOptions?.selectionDto?.selectAllMode?.toLowerCase() === 'allpages'
|
|
|
|
|
|
? selectedKeys.size === totalCount
|
|
|
|
|
|
: selectedKeys.size === data.length)
|
|
|
|
|
|
}
|
2025-11-30 16:56:36 +00:00
|
|
|
|
onChange={(checked: boolean) => handleSelectAll(checked)}
|
|
|
|
|
|
className="cursor-pointer"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-sm text-gray-600 dark:text-gray-300">
|
|
|
|
|
|
{selectedKeys.size > 0 ? `${selectedKeys.size} seçili` : 'Tümünü Seç'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{selectionMode !== 'none' && selectedKeys.size > 0 && !gridDto?.gridOptions?.selectionDto?.allowSelectAll && (
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<span className="text-sm text-gray-600 dark:text-gray-300">
|
|
|
|
|
|
{selectedKeys.size} seçili
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-end items-center gap-2">
|
|
|
|
|
|
{/* Export Buttons */}
|
|
|
|
|
|
{gridDto?.gridOptions?.exportDto?.enabled && (
|
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="xs"
|
|
|
|
|
|
variant="default"
|
|
|
|
|
|
onClick={() => handleExport('xlsx', selectionMode !== 'none' && selectedKeys.size > 0)}
|
|
|
|
|
|
title={selectionMode !== 'none' && selectedKeys.size > 0 ? `Seçilenleri Excel'e Aktar (${selectedKeys.size})` : "Tümünü Excel'e Aktar"}
|
|
|
|
|
|
>
|
|
|
|
|
|
📊 {selectionMode !== 'none' && selectedKeys.size > 0 ? `Seçilenleri Excel'e Aktar (${selectedKeys.size})` : `Tümünü Excel'e Aktar`}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Sort Dropdown */}
|
|
|
|
|
|
{gridDto && gridDto.columnFormats && gridDto.columnFormats.length > 0 && (
|
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
|
<select
|
|
|
|
|
|
className="text-xs border rounded px-2 py-1 dark:bg-neutral-700 dark:text-white dark:border-neutral-600"
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
if (e.target.value) {
|
|
|
|
|
|
toggleSort(e.target.value)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// "Sıralama Yok" seçildiğinde temizle
|
|
|
|
|
|
const newParams = new URLSearchParams(urlSearchParams.toString())
|
|
|
|
|
|
newParams.delete('sort')
|
|
|
|
|
|
setUrlSearchParams(newParams)
|
|
|
|
|
|
setSortColumn(null)
|
|
|
|
|
|
setSortOrder('asc')
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
value={sortColumn || ''}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">Sıralama Yok</option>
|
|
|
|
|
|
{gridDto.columnFormats
|
|
|
|
|
|
.filter((col) => col.visible && col.dataType !== 'buttons')
|
|
|
|
|
|
.map((col) => (
|
|
|
|
|
|
<option key={col.fieldName} value={col.fieldName}>
|
|
|
|
|
|
{col.fieldName}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
{sortColumn && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => toggleSort(sortColumn)}
|
|
|
|
|
|
className="text-gray-600 dark:text-gray-300 hover:text-blue-500 p-1"
|
|
|
|
|
|
title="Sıralama yönünü değiştir"
|
|
|
|
|
|
>
|
|
|
|
|
|
{sortOrder === 'asc' ? <FaSortAmountUp className="w-3 h-3" /> : <FaSortAmountDown className="w-3 h-3" />}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="relative py-1 flex gap-1 border-b-1">
|
2025-09-22 14:08:42 +00:00
|
|
|
|
<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') {
|
2025-09-23 14:05:42 +00:00
|
|
|
|
onFilter(e.currentTarget.value)
|
|
|
|
|
|
setPrevValue(e.currentTarget.value.trim()) // Enter ile tetiklenirse güncelle
|
2025-09-22 14:08:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onBlur={(e) => {
|
2025-09-23 14:05:42 +00:00
|
|
|
|
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-24 17:46:03 +00:00
|
|
|
|
|
2025-09-22 14:08:42 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
size="xs"
|
|
|
|
|
|
variant={layoutCount === 1 ? 'solid' : 'default'}
|
|
|
|
|
|
className="text-sm"
|
2025-09-23 12:48:54 +00:00
|
|
|
|
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"
|
2025-09-23 12:48:54 +00:00
|
|
|
|
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"
|
2025-09-23 12:48:54 +00:00
|
|
|
|
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"
|
2025-09-23 12:48:54 +00:00
|
|
|
|
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"
|
2025-09-23 12:48:54 +00:00
|
|
|
|
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-11-30 16:56:36 +00:00
|
|
|
|
</div>
|
2025-09-22 14:08:42 +00:00
|
|
|
|
</div>
|
2025-09-21 14:42:24 +00:00
|
|
|
|
</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>
|
|
|
|
|
|
) : (
|
2025-11-30 17:15:04 +00:00
|
|
|
|
<div
|
|
|
|
|
|
className="bg-transparent grid gap-4"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
gridTemplateColumns: `repeat(${layoutCount}, minmax(0, 1fr))`
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-09-22 14:08:42 +00:00
|
|
|
|
{gridDataSource &&
|
|
|
|
|
|
data.map((row, idx) => {
|
|
|
|
|
|
const keyField = gridDto.gridOptions.keyFieldName
|
|
|
|
|
|
const rowId = row[keyField!]
|
2025-11-30 16:56:36 +00:00
|
|
|
|
const isSelected = selectedKeys.has(rowId)
|
|
|
|
|
|
const isFocused = focusedCardIndex === idx
|
|
|
|
|
|
const isHovered = hoveredCardKey === rowId
|
2025-09-22 14:08:42 +00:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<CardItem
|
2025-11-30 16:56:36 +00:00
|
|
|
|
ref={(el) => (cardRefs.current[idx] = el)}
|
2025-09-22 14:08:42 +00:00
|
|
|
|
isSubForm={true}
|
|
|
|
|
|
key={rowId || idx}
|
|
|
|
|
|
row={row}
|
|
|
|
|
|
gridDto={gridDto}
|
|
|
|
|
|
listFormCode={listFormCode}
|
|
|
|
|
|
dataSource={gridDataSource}
|
|
|
|
|
|
refreshData={loadData}
|
2025-10-22 14:58:27 +00:00
|
|
|
|
getCachedLookupDataSource={getLookupDataSource}
|
2025-11-30 16:56:36 +00:00
|
|
|
|
isSelected={isSelected}
|
|
|
|
|
|
isFocused={isFocused}
|
|
|
|
|
|
isHovered={isHovered}
|
|
|
|
|
|
onSelectionChange={(checked) => handleCardSelection(rowId, checked)}
|
|
|
|
|
|
onClick={(e) => handleCardClick(rowId, idx, e)}
|
|
|
|
|
|
onDoubleClick={() => handleCardDoubleClick(rowId, row)}
|
|
|
|
|
|
onMouseEnter={() => setHoveredCardKey(rowId)}
|
|
|
|
|
|
onMouseLeave={() => setHoveredCardKey(null)}
|
|
|
|
|
|
tabIndex={isFocused ? 0 : -1}
|
2025-09-22 14:08:42 +00:00
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</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 09:11:15 +00:00
|
|
|
|
/>
|
2025-09-22 14:08:42 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<Pagination
|
|
|
|
|
|
currentPage={currentPage}
|
|
|
|
|
|
total={totalCount}
|
|
|
|
|
|
pageSize={pageSize}
|
|
|
|
|
|
onChange={onPageChange}
|
2025-09-22 09:11:15 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-09-22 14:08:42 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-09-22 09:11:15 +00:00
|
|
|
|
</Container>
|
2025-09-21 14:42:24 +00:00
|
|
|
|
</>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-21 15:11:12 +00:00
|
|
|
|
export default Card
|