/* Парус 8 - Панели мониторинга Компонент: Таблица */ //--------------------- //Подключение библиотек //--------------------- import React, { useEffect, useState, useMemo } from "react"; //Классы React import PropTypes from "prop-types"; //Контроль свойств компонента import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton, Icon, Menu, MenuItem, Divider, Stack, Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Chip, Container } from "@mui/material"; //Интерфейсные компоненты import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке //--------- //Константы //--------- //Размеры отступов const P8P_TABLE_SIZE = { SMALL: "small", MEDIUM: "medium" }; //Типы данных const P8P_TABLE_DATA_TYPE = { STR: "STR", NUMB: "NUMB", DATE: "DATE" }; //Направления сортировки const P8P_TABLE_COLUMN_ORDER_DIRECTIONS = { ASC: "ASC", DESC: "DESC" }; //Действия панели инструментов столбца const P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS = { ORDER_TOGGLE: "ORDER_TOGGLE", FILTER_TOGGLE: "FILTER_TOGGLE" }; //Действия меню столбца const P8P_TABLE_COLUMN_MENU_ACTIONS = { ORDER_ASC: "ORDER_ASC", ORDER_DESC: "ORDER_DESC", FILTER: "FILTER" }; //Стили const STYLES = { TABLE: { with: "100%" }, TABLE_ROW: { "&:last-child td, &:last-child th": { border: 0 } }, TABLE_CELL_EXPAND_CONTAINER: { paddingBottom: 0, paddingTop: 0 }, TABLE_COLUMN_STACK: { alignItems: "center" }, TABLE_COLUMN_MENU_ITEM_ICON: { paddingRight: "10px" }, FILTER_CHIP: { alignItems: "center" }, MORE_BUTTON_CONTAINER: { with: "100%", textAlign: "center", padding: "5px" } }; //Структура элемента описания фильтра const P8P_FILTER_SHAPE = PropTypes.shape({ name: PropTypes.string.isRequired, from: PropTypes.any, to: PropTypes.any }); //-------------------------------- //Вспомогательные классы и функции //-------------------------------- //Проверка существования значения const hasValue = value => typeof value !== "undefined" && value !== null && value !== ""; //Панель инструментов столбца const P8PTableColumnToolBar = ({ columnDef, orders, filters, onItemClick }) => { //Кнопка сортировки const order = orders.find(o => o.name == columnDef.name); let orderButton = null; if (order) orderButton = ( (onItemClick ? onItemClick(P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.ORDER_TOGGLE, columnDef.name) : null)}> {order.direction === P8P_TABLE_COLUMN_ORDER_DIRECTIONS.ASC ? "arrow_upward" : "arrow_downward"} ); //Кнопка фильтрации const filter = filters.find(f => f.name == columnDef.name); let filterButton = null; if (hasValue(filter?.from) || hasValue(filter?.to)) filterButton = ( (onItemClick ? onItemClick(P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.FILTER_TOGGLE, columnDef.name) : null)}> filter_alt ); //Генерация содержимого return ( <> {orderButton} {filterButton} ); }; //Контроль свойств - Панель инструментов столбца P8PTableColumnToolBar.propTypes = { columnDef: PropTypes.object.isRequired, orders: PropTypes.array.isRequired, filters: PropTypes.array.isRequired, onItemClick: PropTypes.func }; //Меню столбца const P8PTableColumnMenu = ({ columnDef, orderAscItemCaption, orderDescItemCaption, filterItemCaption, onItemClick }) => { //Собственное состояние const [anchorEl, setAnchorEl] = useState(null); //Флаг отображения const open = Boolean(anchorEl); //По нажатию на открытие меню const handleMenuButtonClick = event => { setAnchorEl(event.currentTarget); }; //По нажатию на пункт меню const handleMenuItemClick = (event, index, action, columnName) => { if (onItemClick) onItemClick(action, columnName); setAnchorEl(null); }; //При закрытии меню const handleMenuClose = () => { setAnchorEl(null); }; //Формирование списка элементов меню в зависимости от описания колонки таблицы const menuItems = []; if (columnDef.order === true) { menuItems.push( handleMenuItemClick(event, index, P8P_TABLE_COLUMN_MENU_ACTIONS.ORDER_ASC, columnDef.name)} > arrow_upward {orderAscItemCaption} ); menuItems.push( handleMenuItemClick(event, index, P8P_TABLE_COLUMN_MENU_ACTIONS.ORDER_DESC, columnDef.name)} > arrow_downward {orderDescItemCaption} ); } if (columnDef.filter === true) { if (menuItems.length > 0) menuItems.push(); menuItems.push( handleMenuItemClick(event, index, P8P_TABLE_COLUMN_MENU_ACTIONS.FILTER, columnDef.name)} > filter_alt {filterItemCaption} ); } //Генерация содержимого return menuItems.length > 0 ? ( <> more_vert {menuItems} ) : null; }; //Контроль свойств - Меню столбца P8PTableColumnMenu.propTypes = { columnDef: PropTypes.object.isRequired, orderAscItemCaption: PropTypes.string.isRequired, orderDescItemCaption: PropTypes.string.isRequired, filterItemCaption: PropTypes.string.isRequired, onItemClick: PropTypes.func }; //Диалог фильтра const P8PTableColumnFilterDialog = ({ columnDef, from, to, valueCaption, valueFromCaption, valueToCaption, okBtnCaption, clearBtnCaption, cancelBtnCaption, valueFormatter, onOk, onClear, onCancel }) => { //Собственное состояние - значения с-по const [filterValues, setFilterValues] = useState({ from, to }); //Отработка воода значения в фильтр const handleFilterTextFieldChanged = e => { setFilterValues(prev => ({ ...prev, [e.target.name]: e.target.value })); }; //Элементы ввода значений фильтра let inputs = null; if (Array.isArray(columnDef.values) && columnDef.values.length > 0) { inputs = ( {columnDef.values.map((v, i) => ( {valueFormatter ? valueFormatter({ value: v, columnDef }) : v} ))} ); } else { switch (columnDef.dataType) { case P8P_TABLE_DATA_TYPE.STR: { inputs = ( ); break; } case P8P_TABLE_DATA_TYPE.NUMB: case P8P_TABLE_DATA_TYPE.DATE: { inputs = ( <>   ); break; } } } return ( (onCancel ? onCancel(columnDef.name) : null)} > {columnDef.caption} {inputs} ); }; //Контроль свойств - Диалог фильтра P8PTableColumnFilterDialog.propTypes = { columnDef: PropTypes.object.isRequired, from: PropTypes.any, to: PropTypes.any, valueCaption: PropTypes.string.isRequired, valueFromCaption: PropTypes.string.isRequired, valueToCaption: PropTypes.string.isRequired, okBtnCaption: PropTypes.string.isRequired, clearBtnCaption: PropTypes.string.isRequired, cancelBtnCaption: PropTypes.string.isRequired, valueFormatter: PropTypes.func, onOk: PropTypes.func, onClear: PropTypes.func, onCancel: PropTypes.func }; //Сводный фильтр const P8PTableFiltersChips = ({ filters, columnsDef, valueFromCaption, valueToCaption, onFilterChipClick, onFilterChipDelete, valueFormatter }) => { return ( {filters.map((filter, i) => { const columnDef = columnsDef.find(columnDef => columnDef.name == filter.name); return ( {columnDef.caption}:  {hasValue(filter.from) && !columnDef.values && columnDef.dataType != P8P_TABLE_DATA_TYPE.STR ? `${valueFromCaption.toLowerCase()} ` : null} {hasValue(filter.from) ? (valueFormatter ? valueFormatter({ value: filter.from, columnDef }) : filter.from) : null} {hasValue(filter.to) && !columnDef.values && columnDef.dataType != P8P_TABLE_DATA_TYPE.STR ? ` ${valueToCaption.toLowerCase()} ` : null} {hasValue(filter.to) ? (valueFormatter ? valueFormatter({ value: filter.to, columnDef }) : filter.to) : null} } variant="outlined" onClick={() => (onFilterChipClick ? onFilterChipClick(columnDef.name) : null)} onDelete={() => (onFilterChipDelete ? onFilterChipDelete(columnDef.name) : null)} /> ); })} ); }; //Контроль свойств - Сводный фильтр P8PTableFiltersChips.propTypes = { filters: PropTypes.array.isRequired, columnsDef: PropTypes.array.isRequired, valueFromCaption: PropTypes.string.isRequired, valueToCaption: PropTypes.string.isRequired, onFilterChipClick: PropTypes.func, onFilterChipDelete: PropTypes.func, valueFormatter: PropTypes.func }; //----------- //Тело модуля //----------- //Таблица const P8PTable = ({ columnsDef, rows, orders, filters, size, morePages, reloading, expandable, orderAscMenuItemCaption, orderDescMenuItemCaption, filterMenuItemCaption, valueFilterCaption, valueFromFilterCaption, valueToFilterCaption, okFilterBtnCaption, clearFilterBtnCaption, cancelFilterBtnCaption, morePagesBtnCaption, noDataFoundText, headCellRender, dataCellRender, rowExpandRender, valueFormatter, onOrderChanged, onFilterChanged, onPagesCountChanged }) => { //Собственное состояние - фильтруемая колонка const [filterColumn, setFilterColumn] = useState(null); //Собственное состояние - развёрнутые строки const [expanded, setExpanded] = useState({}); //Описание фильтруемой колонки const filterColumnDef = filterColumn ? columnsDef.find(columnDef => columnDef.name == filterColumn) || null : null; //Значения фильтра фильтруемой колонки const [filterColumnFrom, filterColumnTo] = filterColumn ? (() => { const filter = filters.find(filter => filter.name == filterColumn); return filter ? [filter.from == null ? "" : filter.from, filter.to == null ? "" : filter.to] : ["", ""]; })() : ["", ""]; //Определение списка видимых колонок const visibleColumns = useMemo(() => columnsDef.filter(columnDef => columnDef.visible === true), [columnsDef]); //Определение количества видимых колонок const visibleColumnsCount = useMemo(() => visibleColumns.length + (expandable === true ? 1 : 0), [visibleColumns, expandable]); //Выравнивание в зависимости от типа данных const getAlignByDataType = dataType => dataType === P8P_TABLE_DATA_TYPE.DATE ? "center" : dataType === P8P_TABLE_DATA_TYPE.NUMB ? "right" : "left"; //Упорядочение содержимого в зависимости от типа данных const getJustifyContentByDataType = dataType => dataType === P8P_TABLE_DATA_TYPE.DATE ? "center" : dataType === P8P_TABLE_DATA_TYPE.NUMB ? "flex-end" : "flex-start"; //Отработка нажатия на элемент пункта меню const handleToolBarItemClick = (action, columnName) => { switch (action) { case P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.ORDER_TOGGLE: { const colOrder = orders.find(o => o.name == columnName); const newDirection = colOrder?.direction == P8P_TABLE_COLUMN_ORDER_DIRECTIONS.ASC ? P8P_TABLE_COLUMN_ORDER_DIRECTIONS.DESC : colOrder?.direction == P8P_TABLE_COLUMN_ORDER_DIRECTIONS.DESC ? null : P8P_TABLE_COLUMN_ORDER_DIRECTIONS.ASC; if (onOrderChanged) onOrderChanged({ columnName, direction: newDirection }); break; } case P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.FILTER_TOGGLE: setFilterColumn(columnName); break; } }; //Отработка нажатия на пункты меню const handleMenuItemClick = (action, columnName) => { switch (action) { case P8P_TABLE_COLUMN_MENU_ACTIONS.ORDER_ASC: onOrderChanged({ columnName, direction: P8P_TABLE_COLUMN_ORDER_DIRECTIONS.ASC }); break; case P8P_TABLE_COLUMN_MENU_ACTIONS.ORDER_DESC: onOrderChanged({ columnName, direction: P8P_TABLE_COLUMN_ORDER_DIRECTIONS.DESC }); break; case P8P_TABLE_COLUMN_MENU_ACTIONS.FILTER: setFilterColumn(columnName); break; } }; //Отработка ввода значения фильтра колонки const handleFilterOk = (columnName, from, to) => { if (onFilterChanged) onFilterChanged({ columnName, from: from === "" ? null : from, to: to === "" ? null : to }); setFilterColumn(null); }; //Отработка очистки значения фильтра колонки const handleFilterClear = columnName => { if (onFilterChanged) onFilterChanged({ columnName, from: null, to: null }); setFilterColumn(null); }; //Отработка отмены ввода значения фильтра колонки const handleFilterCancel = () => { setFilterColumn(null); }; //Отработка нажатия на элемент сводного фильтра const handleFilterChipClick = columnName => setFilterColumn(columnName); //Отработка удаления элемента сводного фильтра const handleFilterChipDelete = columnName => (onFilterChanged ? onFilterChanged({ columnName, from: null, to: null }) : null); //Отработка нажатия на кнопку догрузки страницы const handleMorePagesBtnClick = () => { if (onPagesCountChanged) onPagesCountChanged(); }; //Отработка нажатия на кнопку раскрытия элемента const handleExpandClick = rowIndex => { if (expanded[rowIndex] === true) setExpanded(pv => { let res = { ...pv }; delete res[rowIndex]; return res; }); else setExpanded(pv => ({ ...pv, [rowIndex]: true })); }; //При перезагрузке данных useEffect(() => { if (reloading) setExpanded({}); }, [reloading]); //Генерация содержимого return ( <> {filterColumn ? ( ) : null} {Array.isArray(filters) && filters.length > 0 ? ( ) : null} {expandable && rowExpandRender ? : null} {visibleColumns.map((columnDef, j) => { let customRender = {}; if (headCellRender) customRender = headCellRender({ columnDef }) || {}; return ( {customRender.data ? customRender.data : columnDef.caption} ); })} {rows.length > 0 ? rows.map((row, i) => ( {expandable && rowExpandRender ? ( handleExpandClick(i)}> {expanded[i] === true ? "keyboard_arrow_down" : "keyboard_arrow_right"} ) : null} {visibleColumns.map((columnDef, j) => { let customRender = {}; if (dataCellRender) customRender = dataCellRender({ row, columnDef }) || {}; return ( {customRender.data ? customRender.data : valueFormatter ? valueFormatter({ value: row[columnDef.name], columnDef }) : row[columnDef.name]} ); })} {expandable && rowExpandRender && expanded[i] === true ? ( {rowExpandRender({ columnsDef, row })} ) : null} )) : null}
{rows.length == 0 ? ( noDataFoundText && !reloading ? ( ) : null ) : morePages ? ( ) : null}
); }; //Контроль свойств - Таблица P8PTable.propTypes = { columnsDef: PropTypes.arrayOf( PropTypes.shape({ name: PropTypes.string.isRequired, caption: PropTypes.string.isRequired, order: PropTypes.bool.isRequired, filter: PropTypes.bool.isRequired, dataType: PropTypes.string.isRequired, values: PropTypes.array }) ).isRequired, rows: PropTypes.array.isRequired, orders: PropTypes.arrayOf( PropTypes.shape({ name: PropTypes.string.isRequired, direction: PropTypes.string.isRequired }) ).isRequired, filters: PropTypes.arrayOf(P8P_FILTER_SHAPE).isRequired, size: PropTypes.string, morePages: PropTypes.bool.isRequired, reloading: PropTypes.bool.isRequired, expandable: PropTypes.bool, orderAscMenuItemCaption: PropTypes.string.isRequired, orderDescMenuItemCaption: PropTypes.string.isRequired, filterMenuItemCaption: PropTypes.string.isRequired, valueFilterCaption: PropTypes.string.isRequired, valueFromFilterCaption: PropTypes.string.isRequired, valueToFilterCaption: PropTypes.string.isRequired, okFilterBtnCaption: PropTypes.string.isRequired, clearFilterBtnCaption: PropTypes.string.isRequired, cancelFilterBtnCaption: PropTypes.string.isRequired, morePagesBtnCaption: PropTypes.string.isRequired, noDataFoundText: PropTypes.string, headCellRender: PropTypes.func, dataCellRender: PropTypes.func, rowExpandRender: PropTypes.func, valueFormatter: PropTypes.func, onOrderChanged: PropTypes.func, onFilterChanged: PropTypes.func, onPagesCountChanged: PropTypes.func }; //---------------- //Интерфейс модуля //---------------- export { P8PTable, P8P_TABLE_DATA_TYPE, P8P_TABLE_SIZE, P8P_FILTER_SHAPE };