206 lines
8.0 KiB
TypeScript
206 lines
8.0 KiB
TypeScript
import { Badge, Button, Flex, Input, Modal, ScrollAreaAutosize, Select, Stack, Table, TextInput } from '@mantine/core';
|
||
import { Cell, ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||
import { useEffect, useMemo, useState } from 'react';
|
||
import styles from './CustomTable.module.scss'
|
||
import { useRoles } from '../hooks/swrHooks';
|
||
import { IRole } from '../interfaces/role';
|
||
import { IconPlus } from '@tabler/icons-react';
|
||
import { CreateField } from '../interfaces/create';
|
||
import { AxiosResponse } from 'axios';
|
||
import FormFields from './FormFields';
|
||
import { useDisclosure } from '@mantine/hooks';
|
||
|
||
type CustomTableProps<T> = {
|
||
data: T[];
|
||
columns: ColumnDef<T>[];
|
||
createFields?: CreateField[];
|
||
submitHandler?: (data: T) => Promise<AxiosResponse>
|
||
}
|
||
|
||
const CustomTable = <T extends object>({
|
||
data: initialData,
|
||
columns,
|
||
createFields,
|
||
submitHandler
|
||
}: CustomTableProps<T>) => {
|
||
const [data, setData] = useState<T[]>(initialData);
|
||
const [searchText, setSearchText] = useState('');
|
||
const [editingCell, setEditingCell] = useState<{ rowIndex: string | number | null, columnId: string | number | null }>({ rowIndex: null, columnId: null });
|
||
|
||
const tableColumns = useMemo(() => columns, [columns]);
|
||
|
||
// Function to handle cell edit
|
||
const handleEditCell = (
|
||
rowIndex: number,
|
||
columnId: keyof T,
|
||
value: T[keyof T]
|
||
) => {
|
||
const updatedData = [...data];
|
||
updatedData[rowIndex][columnId] = value;
|
||
setData(updatedData);
|
||
//setEditingCell({ rowIndex: null, columnId: null });
|
||
};
|
||
|
||
const filteredData = useMemo(() => {
|
||
if (!searchText) return data;
|
||
|
||
return data.filter((row) =>
|
||
Object.values(row).some((value) =>
|
||
value?.toString().toLowerCase().includes(searchText.toLowerCase())
|
||
)
|
||
);
|
||
}, [data, searchText])
|
||
|
||
const table = useReactTable({
|
||
data: filteredData,
|
||
columns: tableColumns,
|
||
getCoreRowModel: getCoreRowModel(),
|
||
columnResizeMode: "onChange",
|
||
});
|
||
|
||
const [opened, { open, close }] = useDisclosure(false);
|
||
|
||
return (
|
||
<Stack h='100%'>
|
||
{createFields && submitHandler &&
|
||
<Modal opened={opened} onClose={close} title="Добавление объекта" centered>
|
||
<FormFields
|
||
fields={createFields}
|
||
submitHandler={submitHandler}
|
||
/>
|
||
</Modal>
|
||
}
|
||
|
||
<Flex w='100%' gap='sm'>
|
||
<TextInput
|
||
placeholder="Поиск"
|
||
value={searchText}
|
||
onChange={(e) => setSearchText(e.target.value)}
|
||
w='100%'
|
||
/>
|
||
{createFields && submitHandler &&
|
||
<Button
|
||
leftSection={<IconPlus />}
|
||
onClick={open}
|
||
style={{ flexShrink: 0 }}
|
||
>
|
||
Добавить
|
||
</Button>
|
||
}
|
||
</Flex>
|
||
|
||
<ScrollAreaAutosize offsetScrollbars style={{ borderRadius: '4px' }}>
|
||
<Table stickyHeader striped withColumnBorders highlightOnHover className={styles.table}>
|
||
<Table.Thead className={styles.thead}>
|
||
{table.getHeaderGroups().map(headerGroup => (
|
||
<Table.Tr key={headerGroup.id} className={styles.tr}>
|
||
{headerGroup.headers.map((header) => (
|
||
<Table.Th key={header.id} className={styles.th} w={header.getSize()}>
|
||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||
<div
|
||
className={styles.resize_handler}
|
||
onMouseDown={header.getResizeHandler()} //for desktop
|
||
onTouchStart={header.getResizeHandler()}
|
||
>
|
||
</div>
|
||
</Table.Th>
|
||
))}
|
||
</Table.Tr>
|
||
))}
|
||
</Table.Thead>
|
||
<Table.Tbody className={styles.tbody}>
|
||
{table.getRowModel().rows.map((row, rowIndex) => (
|
||
<Table.Tr key={row.id} className={styles.tr}>
|
||
{row.getVisibleCells().map(cell => {
|
||
const isEditing = editingCell.rowIndex === rowIndex && editingCell.columnId === cell.column.id;
|
||
|
||
return (
|
||
<Table.Td
|
||
key={cell.id}
|
||
onDoubleClick={() => setEditingCell({ rowIndex, columnId: cell.column.id })}
|
||
style={{ width: cell.column.getSize() }}
|
||
className={styles.td}
|
||
>
|
||
{isEditing ? (
|
||
<Input
|
||
type='text'
|
||
value={(data[rowIndex][cell.column.id as keyof T] as string)}
|
||
onChange={(e) => handleEditCell(rowIndex, (cell.column.id as keyof T), e.target.value as T[keyof T])}
|
||
onBlur={() => setEditingCell({ rowIndex: null, columnId: null })}
|
||
autoFocus
|
||
/>
|
||
) : (
|
||
<CellDisplay cell={cell} />
|
||
)}
|
||
</Table.Td>
|
||
);
|
||
})}
|
||
</Table.Tr>
|
||
))}
|
||
</Table.Tbody>
|
||
</Table>
|
||
</ScrollAreaAutosize>
|
||
</Stack>
|
||
);
|
||
};
|
||
|
||
type CellDisplayProps<T> = {
|
||
cell: Cell<T, unknown>;
|
||
}
|
||
|
||
const CellDisplay = <T extends object>({
|
||
cell
|
||
}: CellDisplayProps<T>) => {
|
||
const { roles } = useRoles()
|
||
|
||
const [roleOptions, setRoleOptions] = useState<{ label: string, value: string }[]>()
|
||
|
||
useEffect(() => {
|
||
if (Array.isArray(roles)) {
|
||
setRoleOptions(roles.map((role: IRole) => ({ label: role.name, value: role.id.toString() })))
|
||
}
|
||
}, [roles])
|
||
|
||
switch (cell.column.id) {
|
||
case 'activity':
|
||
return (
|
||
cell.getValue() ? (
|
||
<Badge fullWidth variant="light">
|
||
Активен
|
||
</Badge>
|
||
) : (
|
||
<Badge color="gray" fullWidth variant="light">
|
||
Отключен
|
||
</Badge>
|
||
)
|
||
)
|
||
case 'is_active':
|
||
return (
|
||
cell.getValue() ? (
|
||
<Badge fullWidth variant="light">
|
||
Активен
|
||
</Badge>
|
||
) : (
|
||
<Badge color="gray" fullWidth variant="light">
|
||
Отключен
|
||
</Badge>
|
||
)
|
||
)
|
||
case 'role_id':
|
||
return (
|
||
<Select
|
||
data={roleOptions}
|
||
value={Number(cell.getValue()).toString()}
|
||
variant="unstyled"
|
||
allowDeselect={false}
|
||
/>
|
||
)
|
||
default:
|
||
return (
|
||
flexRender(cell.column.columnDef.cell, cell.getContext())
|
||
)
|
||
}
|
||
}
|
||
|
||
export default CustomTable;
|