322 lines
14 KiB
TypeScript
322 lines
14 KiB
TypeScript
import React, { useState, memo } from 'react';
|
|
import DefaultTableSkeleton from '../components/DefaultTableSkeleton';
|
|
import Td from '../components/Td';
|
|
import type { TableProps, RowDataType, ColumnType } from '@/components/types/TableTypes';
|
|
import { Checkbox } from '../components/ui/checkbox';
|
|
import RowActionsDropdown from '../components/RowActionsDropdown';
|
|
import Pagination from '../components/Pagination';
|
|
|
|
const Table = <T extends RowDataType>({
|
|
columns,
|
|
data,
|
|
isloading = false,
|
|
onRowClick,
|
|
className = '',
|
|
rowClassName = '',
|
|
noDataMessage = 'هیچ دادهای یافت نشد',
|
|
headerClassName = 'bg-gray-50',
|
|
emptyRowsCount = 5,
|
|
showHeader = true,
|
|
actions = null,
|
|
actionsPosition = 'top',
|
|
selectable = false,
|
|
selectedActions = null,
|
|
onSelectionChange,
|
|
rowActions,
|
|
pagination,
|
|
}: TableProps<T>): React.ReactElement => {
|
|
|
|
const [selectedRows, setSelectedRows] = useState<T[]>([]);
|
|
|
|
const getRowClassName = (item: T, index: number): string => {
|
|
if (typeof rowClassName === 'function') {
|
|
return rowClassName(item, index);
|
|
}
|
|
return rowClassName;
|
|
};
|
|
|
|
const getCellClassName = (column: ColumnType<T>): string => {
|
|
const alignClass = column.align ? `text-${column.align}` : '';
|
|
return `px-3 md:px-6 py-3 md:py-4 whitespace-nowrap text-xs ${alignClass} ${column.className || ''}`.trim();
|
|
};
|
|
|
|
const handleRowClick = (item: T) => {
|
|
if (onRowClick) {
|
|
onRowClick(item.id, item);
|
|
}
|
|
};
|
|
|
|
const handleSelectAll = () => {
|
|
const dataArray = data || [];
|
|
const newSelectedRows = selectedRows.length === dataArray.length ? [] : [...dataArray];
|
|
setSelectedRows(newSelectedRows);
|
|
if (onSelectionChange) {
|
|
onSelectionChange(newSelectedRows);
|
|
}
|
|
};
|
|
|
|
const handleSingleRowSelect = (item: T, checked: boolean) => {
|
|
const newSelectedRows = checked
|
|
? [...selectedRows, item]
|
|
: selectedRows.filter((row) => row.id !== item.id);
|
|
setSelectedRows(newSelectedRows);
|
|
if (onSelectionChange) {
|
|
onSelectionChange(newSelectedRows);
|
|
}
|
|
};
|
|
|
|
const isRowSelected = (item: T): boolean => {
|
|
return selectedRows.some((row) => row.id === item.id);
|
|
};
|
|
|
|
// Gmail-style layout برای وقتی header نمایش داده نمیشود
|
|
const renderGmailStyleRows = () => {
|
|
const hasActions = (actionsPosition === 'top' || actionsPosition === 'above-header') && (actions || (selectable && selectedRows.length > 0 && selectedActions));
|
|
const roundedClass = hasActions ? 'rounded-b-2xl' : 'rounded-2xl';
|
|
|
|
if (isloading) {
|
|
return (
|
|
<div className={`bg-white ${roundedClass} overflow-hidden`}>
|
|
{Array.from({ length: emptyRowsCount }).map((_, index) => (
|
|
<div key={index} className={`h-[48px] bg-gray-100 animate-pulse ${index !== 0 ? 'border-t border-[#EAEDF5]' : ''}`}></div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!data || data.length === 0) {
|
|
return (
|
|
<div className={`bg-white ${roundedClass} py-6 md:py-8 text-center text-gray-500`}>
|
|
{noDataMessage}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`bg-white ${roundedClass} overflow-hidden`}>
|
|
{data.map((item, rowIndex) => (
|
|
<div
|
|
key={item.id}
|
|
className={`w-full min-h-[48px] hover:bg-[#F4F5F9] ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)} px-4 py-3 flex items-center gap-3 ${rowIndex !== 0 ? 'border-t border-[#EAEDF5]' : ''}`}
|
|
onClick={() => handleRowClick(item)}
|
|
>
|
|
{selectable && (
|
|
<div
|
|
className="flex-shrink-0 relative z-[5]"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<Checkbox
|
|
checked={isRowSelected(item)}
|
|
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex-1 min-w-0">
|
|
{/* محتوای اصلی - ستون اول */}
|
|
<div className="flex items-center mb-0.5 text-sm font-medium">
|
|
{columns[0]?.render ? columns[0].render(item, rowIndex) : (item[columns[0]?.key] as React.ReactNode)}
|
|
</div>
|
|
|
|
{/* اطلاعات اضافی - سه ستون آخر در یک خط */}
|
|
{columns.length > 1 && (
|
|
<div className="flex items-center gap-3 text-xs text-gray-500">
|
|
{columns.slice(1).map((col) => (
|
|
<div key={col.key} className="truncate">
|
|
{col.render ? col.render(item, rowIndex) : (item[col.key] as React.ReactNode)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{rowActions && (
|
|
<div
|
|
className="flex-shrink-0 relative z-[5]"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<RowActionsDropdown actions={rowActions(item)} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderTableBody = () => {
|
|
if (isloading) {
|
|
return (
|
|
<DefaultTableSkeleton tdCount={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} trCount={emptyRowsCount} />
|
|
);
|
|
}
|
|
|
|
if (!data || data.length === 0) {
|
|
return (
|
|
<tr>
|
|
<td colSpan={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} className="px-3 md:px-6 py-6 md:py-8 text-center text-gray-500">
|
|
{noDataMessage}
|
|
</td>
|
|
</tr>
|
|
);
|
|
}
|
|
return (data || []).map((item, rowIndex) => (
|
|
<tr
|
|
key={item.id}
|
|
className={`w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5] ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)}`}
|
|
onClick={() => handleRowClick(item)}
|
|
>
|
|
{selectable && (
|
|
<td
|
|
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<Checkbox
|
|
checked={isRowSelected(item)}
|
|
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
|
|
/>
|
|
</td>
|
|
)}
|
|
{columns.map((col) => (
|
|
<td
|
|
key={`${item.id}-${col.key}`}
|
|
className={getCellClassName(col)}
|
|
style={{ width: col.width }}
|
|
>
|
|
{col.render ? col.render(item, rowIndex) : item[col.key] as React.ReactNode}
|
|
</td>
|
|
))}
|
|
{rowActions && (
|
|
<td
|
|
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<RowActionsDropdown actions={rowActions(item)} />
|
|
</td>
|
|
)}
|
|
</tr>
|
|
));
|
|
};
|
|
|
|
const renderHeader = () => {
|
|
if (!showHeader || actionsPosition === 'header-replace') return null;
|
|
|
|
return (
|
|
<thead className={`h-[60px] md:h-[69px] bg-white text-sm text-[#8C90A3] ${headerClassName}`}>
|
|
<tr>
|
|
{selectable && (
|
|
<th
|
|
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5] first:rounded-tr-2xl md:first:rounded-tr-3xl"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<Checkbox
|
|
checked={(data?.length || 0) > 0 && selectedRows.length === (data?.length || 0)}
|
|
onCheckedChange={handleSelectAll}
|
|
/>
|
|
</th>
|
|
)}
|
|
{columns.map((col) => (
|
|
<Td key={col.key} text={col.title} />
|
|
))}
|
|
{rowActions && (
|
|
<th className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 rounded-tl-2xl md:rounded-tl-3xl"></th>
|
|
)}
|
|
</tr>
|
|
</thead >
|
|
);
|
|
};
|
|
|
|
const renderActions = () => {
|
|
if (!actions && (!selectable || selectedRows.length === 0 || !selectedActions)) return null;
|
|
|
|
return (
|
|
<div className="h-[64px] rounded-t-2xl md:rounded-t-3xl md:h-[74px] bg-white flex items-center px-3 md:px-6">
|
|
<div className='flex items-center gap-2 md:gap-4'>
|
|
{actions}
|
|
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// اگر header نشان داده نمیشود، از Gmail-style layout فقط در موبایل استفاده کن
|
|
if (!showHeader) {
|
|
return (
|
|
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
|
|
{/* نسخه موبایل - Gmail style */}
|
|
<div className={`md:hidden`}>
|
|
{(actionsPosition === 'top' || actionsPosition === 'above-header') && (
|
|
<div className="h-[64px] bg-white flex items-center px-4 rounded-t-2xl border-b border-[#EAEDF5]">
|
|
<div className='flex items-center gap-2'>
|
|
{actions}
|
|
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{renderGmailStyleRows()}
|
|
</div>
|
|
|
|
{/* نسخه دسکتاپ - Table معمولی */}
|
|
<div className={`hidden md:block`}>
|
|
{(actionsPosition === 'top' || actionsPosition === 'above-header') && (
|
|
<div className="h-[74px] bg-white flex items-center px-6 rounded-t-3xl">
|
|
<div className='flex items-center gap-4'>
|
|
{actions}
|
|
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className={`overflow-x-auto overflow-hidden ${(actionsPosition === 'top' || actionsPosition === 'above-header') && (actions || (selectable && selectedRows.length > 0 && selectedActions)) ? 'rounded-b-3xl' : 'rounded-3xl'} bg-white`}>
|
|
<table className="w-full text-sm border-collapse min-w-[600px]">
|
|
<tbody>
|
|
{renderTableBody()}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{pagination && (
|
|
<Pagination
|
|
currentPage={pagination.currentPage}
|
|
totalPages={pagination.totalPages}
|
|
onPageChange={pagination.onPageChange}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
|
|
{(actionsPosition === 'top' || actionsPosition === 'above-header') && renderActions()}
|
|
|
|
<div className={`overflow-x-auto overflow-hidden ${showHeader && actionsPosition !== 'header-replace' ? 'rounded-2xl md:rounded-3xl' : 'rounded-b-2xl md:rounded-b-3xl'} bg-white`}>
|
|
<table className="w-full text-sm border-collapse min-w-[600px]">
|
|
{renderHeader()}
|
|
{actionsPosition === 'header-replace' && (
|
|
<thead>
|
|
<tr>
|
|
<th colSpan={columns.length} className="p-0">
|
|
{renderActions()}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
)}
|
|
<tbody>
|
|
{renderTableBody()}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{pagination && (
|
|
<Pagination
|
|
currentPage={pagination.currentPage}
|
|
totalPages={pagination.totalPages}
|
|
onPageChange={pagination.onPageChange}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(Table) as <T extends RowDataType>(props: TableProps<T>) => React.ReactElement; |