filters
This commit is contained in:
226
src/components/FilteredTable.tsx
Normal file
226
src/components/FilteredTable.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import {useState, useMemo, useEffect} from 'react'
|
||||
import { FieldInfo } from '../types/common'
|
||||
|
||||
interface FilteredTableProps {
|
||||
schema: FieldInfo[]
|
||||
data: object[]
|
||||
showFilters?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Компонент динамической фильтрованной таблицы.
|
||||
* Позволяет фильтровать данные по различным условиям, в зависимости от типа поля.
|
||||
*/
|
||||
export function FilteredTable({ schema, data, showFilters = false }: FilteredTableProps) {
|
||||
const [filters, setFilters] = useState<Record<string, unknown>>({})
|
||||
const [visibleColumns, setVisibleColumns] = useState<Record<string, boolean>>({})
|
||||
|
||||
// Изначальный массив данных для фильтрации
|
||||
const initialData = useMemo(() => data, [data])
|
||||
|
||||
// Массив отфильтрованных данных
|
||||
const filteredData = useMemo(() => {
|
||||
return initialData.filter(item => {
|
||||
return schema.every(field => {
|
||||
const value = item[field.name]
|
||||
const filter = filters[field.name]
|
||||
if (!filter) return true
|
||||
|
||||
// Обработка по типу
|
||||
if (field.type === 'string') {
|
||||
if (filter.searchText) {
|
||||
if (typeof value !== 'string') return false
|
||||
if (!value.toLowerCase().includes(filter.searchText.toLowerCase())) return false
|
||||
}
|
||||
if (filter.exactMatch && filter.searchText) {
|
||||
return value.toLowerCase() === filter.searchText.toLowerCase()
|
||||
|
||||
}
|
||||
if (filter.empty !== undefined) {
|
||||
const isEmpty = value === '' || value === null || value === undefined
|
||||
if (filter.empty && !isEmpty) return false
|
||||
if (filter.notEmpty && isEmpty) return false
|
||||
}
|
||||
} else if (field.type === 'date') {
|
||||
const dateValue = new Date(value).getTime()
|
||||
if (filter.fromDate) {
|
||||
const fromTime = new Date(filter.fromDate).getTime()
|
||||
if (dateValue < fromTime) return false
|
||||
}
|
||||
if (filter.toDate) {
|
||||
const toTime = new Date(filter.toDate).getTime()
|
||||
if (dateValue > toTime) return false
|
||||
}
|
||||
} else if (field.type === 'number') {
|
||||
const numValue = Number(value)
|
||||
if (filter.gte !== undefined && numValue < filter.gte) return false
|
||||
if (filter.lte !== undefined && numValue > filter.lte) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
}, [initialData, filters, schema])
|
||||
|
||||
// Обработчик изменения фильтров
|
||||
const handleFilterChange = (name: string, filterPart: unknown) => {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
[name]: {
|
||||
...prev[name],
|
||||
...filterPart
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// Обработчик переключения видимости колонок
|
||||
const handleColumnToggle = (name: string) => {
|
||||
setVisibleColumns(prev => ({
|
||||
...prev,
|
||||
[name]: !prev[name]
|
||||
}))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
let newVisibleColumns:Record<string, boolean> = {}
|
||||
|
||||
for (const field of schema) {
|
||||
newVisibleColumns = {...newVisibleColumns, [field.name]:true}
|
||||
|
||||
}
|
||||
setVisibleColumns(newVisibleColumns)
|
||||
}, [schema]);
|
||||
|
||||
console.log(visibleColumns)
|
||||
|
||||
return (
|
||||
<div className="filtered-table">
|
||||
{showFilters && (
|
||||
<div className="filters-block">
|
||||
{schema.map(field => (
|
||||
<div key={field.name} className="filter-item">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={visibleColumns[field.name]}
|
||||
onChange={() => handleColumnToggle(field.name)}
|
||||
/>
|
||||
{field.title}
|
||||
</label>
|
||||
{field.type === 'string' && (
|
||||
<div className="string-filters">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Поиск"
|
||||
onChange={(e) => handleFilterChange(field.name, {searchText: e.target.value})}
|
||||
/>
|
||||
<label>
|
||||
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters[field.name]?.exactMatch || false}
|
||||
onChange={() => handleFilterChange(field.name, {exactMatch: !filters[field.name]?.exactMatch})}
|
||||
/> Строгое совпадение
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters[field.name]?.notEmpty || false}
|
||||
onChange={(e) =>
|
||||
handleFilterChange(field.name, {
|
||||
notEmpty: e.target.checked,
|
||||
empty: false
|
||||
})
|
||||
}
|
||||
/>Не пустые
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters[field.name]?.empty || false}
|
||||
onChange={(e) =>
|
||||
handleFilterChange(field.name, {
|
||||
empty: e.target.checked,
|
||||
notEmpty: false
|
||||
})
|
||||
}
|
||||
/>Пустые
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{field.type === 'date' && (
|
||||
<div className="date-filters">
|
||||
<input
|
||||
type="date"
|
||||
onChange={(e) => handleFilterChange(field.name, { fromDate: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
onChange={(e) => handleFilterChange(field.name, { toDate: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{field.type === 'number' && (
|
||||
<div className="number-filters">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="От"
|
||||
onChange={(e) =>
|
||||
handleFilterChange(field.name, { gte: e.target.value !== '' ? Number(e.target.value) : undefined })
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="До"
|
||||
onChange={(e) =>
|
||||
handleFilterChange(field.name, { lte: e.target.value !== '' ? Number(e.target.value) : undefined })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<table className="filtered-table-main" style={{ overflowX: 'auto', display: 'block' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
{schema.map(
|
||||
(field) =>
|
||||
visibleColumns[field.name] !== false && (
|
||||
<th key={field.name} className="column-header">
|
||||
{field.title}
|
||||
</th>
|
||||
)
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredData.length > 0 ? (
|
||||
filteredData.map((item, i) => (
|
||||
<tr key={item['id'] || i}>
|
||||
{schema.map(
|
||||
(field) =>
|
||||
visibleColumns[field.name] !== false && (
|
||||
<td key={field.name} className="data-cell">
|
||||
{item[field.name]}
|
||||
</td>
|
||||
)
|
||||
)}
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={schema.length} className="no-data">
|
||||
Нет данных для отображения
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,20 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useApiDocumentation, useDataPage } from "../../API/hooks.ts";
|
||||
import { FieldInfo } from "../../types/common.ts";
|
||||
import {FilteredTable} from "../../components/FilteredTable.tsx";
|
||||
import {useState} from "react";
|
||||
import {LoadingData} from "../../components/LoadingData.tsx";
|
||||
|
||||
export function DataPage() {
|
||||
const { applicationName, endpointName } = useParams<{ applicationName: string; endpointName: string }>();
|
||||
|
||||
// Получаем схему документации API
|
||||
const { schema, loading: loadingSchema } = useApiDocumentation(applicationName || "", endpointName || "");
|
||||
|
||||
// Получаем данные с помощью пагинации
|
||||
const dataPage = useDataPage(applicationName || "", endpointName || "", {}, !loadingSchema);
|
||||
|
||||
const [filterShow, setFilterShow] = useState(false);
|
||||
|
||||
|
||||
|
||||
// Функция для экспорта данных в Excel (пока пустая)
|
||||
const handleExport = () => {
|
||||
// Логика для экспорта данных
|
||||
@@ -23,7 +27,7 @@ export function DataPage() {
|
||||
|
||||
// Функция для фильтрации данных (пока пустая)
|
||||
const handleFilters = () => {
|
||||
// Логика для фильтрации данных
|
||||
setFilterShow(!filterShow)
|
||||
};
|
||||
|
||||
// Если данные или схема загружаются, отображаем загрузку
|
||||
@@ -38,38 +42,13 @@ export function DataPage() {
|
||||
<button className="add-button" onClick={handleAdd}>Добавление</button>
|
||||
<button className="filters-button" onClick={handleFilters}>Фильтры</button>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<table className="data-table" style={{ overflowX: 'auto', display: 'block' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
{schema?.map((field: FieldInfo) => (
|
||||
<th key={field.name} className="column-header">
|
||||
{field.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dataPage.items && dataPage.items.length > 0 ? (
|
||||
dataPage.items.map((item: unknown) => (
|
||||
<tr key={item.id}>
|
||||
{schema?.map((field: FieldInfo) => (
|
||||
<td key={field.name} className="data-cell">
|
||||
{item[field.name] || "-"}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={schema?.length} className="no-data">
|
||||
Нет данных для отображения
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<LoadingData data={dataPage.items}>
|
||||
<FilteredTable
|
||||
schema={schema as FieldInfo[]}
|
||||
data={dataPage.items as object[]}
|
||||
showFilters={filterShow}
|
||||
/>
|
||||
</LoadingData>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user