diff --git a/api/src/Erp.Platform.Application.Contracts/ListForms/GridOptionsDto/LayoutDto.cs b/api/src/Erp.Platform.Application.Contracts/ListForms/GridOptionsDto/LayoutDto.cs index 90968b86..9222eff8 100644 --- a/api/src/Erp.Platform.Application.Contracts/ListForms/GridOptionsDto/LayoutDto.cs +++ b/api/src/Erp.Platform.Application.Contracts/ListForms/GridOptionsDto/LayoutDto.cs @@ -4,7 +4,6 @@ public class LayoutDto { public bool Grid { get; set; } = true; public bool Card { get; set; } = true; - public bool CardView { get; set; } = true; public bool Pivot { get; set; } = true; public bool Chart { get; set; } = true; public bool Tree { get; set; } = true; diff --git a/api/src/Erp.Platform.DbMigrator/Seeds/SeederDefaults.cs b/api/src/Erp.Platform.DbMigrator/Seeds/SeederDefaults.cs index d1f54aea..4c009509 100644 --- a/api/src/Erp.Platform.DbMigrator/Seeds/SeederDefaults.cs +++ b/api/src/Erp.Platform.DbMigrator/Seeds/SeederDefaults.cs @@ -66,7 +66,6 @@ public static class SeederDefaults { Grid = true, Card = true, - CardView = true, Pivot = true, Chart = true, Tree = true, diff --git a/ui/src/proxy/form/models.ts b/ui/src/proxy/form/models.ts index 842372f2..9f9b5d58 100644 --- a/ui/src/proxy/form/models.ts +++ b/ui/src/proxy/form/models.ts @@ -894,7 +894,6 @@ export interface WidgetEditDto { export interface LayoutDto { grid: boolean card: boolean - cardView: boolean pivot: boolean tree: boolean chart: boolean diff --git a/ui/src/views/admin/listForm/edit/FormTabDetails.tsx b/ui/src/views/admin/listForm/edit/FormTabDetails.tsx index b4ccc679..cd91a35d 100644 --- a/ui/src/views/admin/listForm/edit/FormTabDetails.tsx +++ b/ui/src/views/admin/listForm/edit/FormTabDetails.tsx @@ -24,7 +24,6 @@ const schema = Yup.object().shape({ layoutDto: Yup.object().shape({ grid: Yup.boolean(), card: Yup.boolean(), - cardView: Yup.boolean(), pivot: Yup.boolean(), chart: Yup.boolean(), tree: Yup.boolean(), @@ -311,20 +310,6 @@ function FormTabDetails( /> - - - -
-
+

-
+

-
+

{permissionList?.groups.map((group) => ( @@ -255,7 +255,7 @@ function UsersPermission({ ))}
-
+

{selectedGroupPermissions.map((permission) => ( diff --git a/ui/src/views/list/Card.tsx b/ui/src/views/list/Card.tsx deleted file mode 100644 index 9ff20758..00000000 --- a/ui/src/views/list/Card.tsx +++ /dev/null @@ -1,806 +0,0 @@ -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' -import { useLocalization } from '@/utils/hooks/useLocalization' - -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 { translate } = useLocalization() - 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 }) - // Memoize getLookupDataSource to prevent recalculation on each render - const memoizedGetLookupDataSource = useCallback( - (editorOptions: any, colData: any, row?: any) => getLookupDataSource(editorOptions, colData, row), - [getLookupDataSource] - ) - const [layoutCount, setLayoutCount] = useState(4) - const [searchText, setSearchText] = useState('') - const [prevValue, setPrevValue] = useState('') - const [loading, setLoading] = useState(false) - - // Progressive rendering state - const [renderedCount, setRenderedCount] = useState(0) - const [isProgressiveRendering, setIsProgressiveRendering] = 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 = useCallback((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 - }, [sortColumn, sortOrder, urlSearchParams]) - - const handleSelectAll = useCallback(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()) - } - }, [gridDto, gridDataSource, totalCount, data]) - - const handleCardSelection = useCallback((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) - }, [selectedKeys, selectionMode]) - - const handleCardClick = useCallback((key: any, index: number, event: React.MouseEvent) => { - setFocusedCardIndex(index) - if (selectionMode !== 'none' && event.ctrlKey) { - handleCardSelection(key, !selectedKeys.has(key)) - } - }, [selectionMode, handleCardSelection, selectedKeys]) - - const handleCardDoubleClick = useCallback((key: any, row: any) => { - // Navigate to edit page on double click - if (!gridDto?.gridOptions.editingOptionDto?.allowUpdating) return - - window.open( - ROUTES_ENUM.protected.admin.formEdit - .replace(':listFormCode', listFormCode) - .replace(':id', key), - isPwaMode ? '_self' : '_blank' - ) - }, [gridDto, listFormCode, isPwaMode]) - - 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 = useCallback((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 - } - }, [data, gridDto, focusedCardIndex, layoutCount, handleCardDoubleClick, selectionMode, handleCardSelection, selectedKeys]) - - 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) - setIsProgressiveRendering(false) - setRenderedCount(0) - - 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) - - // Veri yüklendi, şimdi progressive rendering başlat - if (res.data && res.data.length > 0) { - setIsProgressiveRendering(true) - setRenderedCount(0) - } - }) - .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]) - - 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(() => { - // Sadece yeni data yüklendiginde scroll yap - smooth yerine auto kullanılarak performans artırılıyor - if (data.length > 0 && currentPage === 1) { - window.scrollTo({ top: 0, behavior: 'auto' }) - } - }, [currentPage]) - - // Progressive rendering effect - kartları küçük batch'lerde render et - useEffect(() => { - if (!isProgressiveRendering || renderedCount >= data.length) { - if (renderedCount >= data.length && isProgressiveRendering) { - setIsProgressiveRendering(false) - } - return - } - - // Her seferde 3 kart render et - daha küçük batch size daha smooth experience - const batchSize = 3 - const timeoutId = setTimeout(() => { - setRenderedCount(prev => Math.min(prev + batchSize, data.length)) - }, 10) // 10ms gecikme - UI'ın nefes almasını sağlar - - return () => clearTimeout(timeoutId) - }, [isProgressiveRendering, renderedCount, data.length]) - - 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} ${translate('::App.Platform.Card.Selected')}` : translate('::App.Platform.Card.SelectAll')} - -
- )} - - {selectionMode !== 'none' && selectedKeys.size > 0 && !gridDto?.gridOptions?.selectionDto?.allowSelectAll && ( -
- - {selectedKeys.size} {translate('::App.Platform.Card.Selected')} - -
- )} -
- -
- {/* 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 ? ( -
- -

- {translate('::App.Platform.Card.NoRecordsFound')} -

-

- {translate('::App.Platform.Card.TryDifferentFilters')} -

-
- ) : ( - <> -
- {gridDataSource && - data.slice(0, isProgressiveRendering ? renderedCount : data.length).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={memoizedGetLookupDataSource} - 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} - /> - ) - })} -
- - {/* Progressive rendering sırasında loading göster */} - {isProgressiveRendering && renderedCount < data.length && ( -
-
-

- {translate('::App.Platform.Card.Loading')} ({renderedCount}/{data.length}) -

-
- )} - - )} - - {gridDto.gridOptions.pagerOptionDto?.visible && totalCount > pageSize && ( -
-
- - {translate('::App.Platform.Card.TotalRecords').replace('{0}', totalCount.toString())} - -