import { useCallback, useEffect, useState, useRef, KeyboardEvent, useMemo } from 'react' import { GridDto } from '@/proxy/form/models' import { Button, Pagination, Select, Checkbox } from '@/components/ui' import classNames from 'classnames' import { FaCog, FaSearch, FaSortAmountDown, FaSortAmountUp } 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' import { layoutTypes } from '../admin/listForm/edit/types' import { useListFormCustomDataSource } from './useListFormCustomDataSource' interface CardProps { listFormCode: string searchParams?: URLSearchParams isSubForm?: boolean level?: number refreshData?: () => Promise gridDto?: GridDto } type Option = { value: number label: string } const Card = (props: CardProps) => { const { listFormCode, searchParams, gridDto } = props const { createSelectDataSource } = useListFormCustomDataSource({} as any) const [data, setData] = useState([]) const [totalCount, setTotalCount] = useState(0) const [currentPage, setCurrentPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [pageSizeOptions, setPageSizeOptions] = useState([]) const [gridDataSource, setGridDataSource] = useState>() 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([]) // Selection features - gridDto değiştiğinde güncellenmeli const [selectedKeys, setSelectedKeys] = useState>(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(null) const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc') // Focus and keyboard navigation const [focusedCardIndex, setFocusedCardIndex] = useState(-1) const cardRefs = useRef<(HTMLDivElement | null)[]>([]) const containerRef = useRef(null) // Hover state const [hoveredCardKey, setHoveredCardKey] = useState(null) const { states } = useStoreState((state) => state.admin.lists) const { setStates } = useStoreActions((a) => a.admin.lists) // props.searchParams varsa onunla başlat const [urlSearchParams, setUrlSearchParams] = useState(() => { 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 }) const onPageSizeSelect = ({ value }: Option) => { setPageSize(value) setCurrentPage(1) } const onPageChange = (page: number) => { setCurrentPage(page) } 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 } const handleSelectAll = async (checked: boolean) => { if (checked) { const keyField = gridDto?.gridOptions.keyFieldName 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) 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 } } 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, layoutTypes.card ) 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, states]) 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() // selectionMode = page ise sayfa değiştiğinde seçimi temizle const selectAllMode = gridDto?.gridOptions?.selectionDto?.selectAllMode?.toLowerCase() if (selectAllMode === 'page') { setSelectedKeys(new Set()) } } }, [gridDataSource, loadData, currentPage, gridDto]) 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' }) } }, [data]) if (!gridDto) return null return ( <>
{selectionMode === 'multiple' && gridDto?.gridOptions?.selectionDto?.allowSelectAll && (
0 && (gridDto?.gridOptions?.selectionDto?.selectAllMode?.toLowerCase() === 'allpages' ? selectedKeys.size === totalCount : selectedKeys.size === data.length) } onChange={(checked: boolean) => handleSelectAll(checked)} className="cursor-pointer" /> {selectedKeys.size > 0 ? `${selectedKeys.size} seçili` : 'Tümünü Seç'}
)} {selectionMode !== 'none' && selectedKeys.size > 0 && !gridDto?.gridOptions?.selectionDto?.allowSelectAll && (
{selectedKeys.size} seçili
)}
{/* Export Buttons */} {gridDto?.gridOptions?.exportDto?.enabled && (
)} {/* Sort Dropdown */} {gridDto && gridDto.columnFormats && gridDto.columnFormats.length > 0 && (
{sortColumn && ( )}
)}
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" /> {checkPermission(gridDto?.gridOptions.permissionDto.u) && ( )}
{loading ? ( ) : data.length === 0 ? (

Uygun kayıt bulunamadı

Farklı filtreler deneyin veya yeni bir kayıt ekleyin.

) : (
{gridDataSource && data.map((row, idx) => { const keyField = gridDto.gridOptions.keyFieldName const rowId = row[keyField!] const isSelected = selectedKeys.has(rowId) const isFocused = focusedCardIndex === idx const isHovered = hoveredCardKey === rowId return ( (cardRefs.current[idx] = el)} isSubForm={true} key={rowId || idx} row={row} gridDto={gridDto} listFormCode={listFormCode} dataSource={gridDataSource} refreshData={loadData} getCachedLookupDataSource={getLookupDataSource} 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} /> ) })}
)} {gridDto.gridOptions.pagerOptionDto?.visible && totalCount > pageSize && (
Toplam {totalCount} kayıt