/* Парус 8 - Панели мониторинга Компонент: Циклограмма */ //--------------------- //Подключение библиотек //--------------------- import React, { useEffect, useState, useRef } from "react"; //Классы React import PropTypes from "prop-types"; //Контроль свойств компонента import { Box, Typography, Dialog, DialogActions, DialogContent, Button, List, ListItem, ListItemText, Link, Divider, IconButton, Icon } from "@mui/material"; //Интерфейсные компоненты import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке import { hasValue } from "../core/utils"; //Вспомогательный функции //--------- //Константы //--------- //Уровни масштаба const P8P_CYCLOGRAM_ZOOM = [0.2, 0.4, 0.7, 1, 1.5, 2, 2.5]; //Параметры элементов циклограммы const NDEFAULT_LINE_HEIGHT = 20; const NDEFAULT_HEADER_HEIGHT = 35; //Высота заголовка const TITLE_HEIGHT = "44px"; //Высота панели масштабирования const ZOOM_HEIGHT = "56px"; //Стили const STYLES = { CYCLOGRAM_TITLE: { height: TITLE_HEIGHT }, CYCLOGRAM_ZOOM: { height: ZOOM_HEIGHT }, HEADER_COLUMN: { fontSize: "12px", textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "pre", textAlign: "center", lineHeight: "3", padding: "0px 5px" }, CYCLOGRAM_BOX: (noData, title, zoomBar) => ({ position: "relative", overflow: "auto", padding: "0px 8px", height: `calc(100% - ${zoomBar ? ZOOM_HEIGHT : "0px"} - ${title ? TITLE_HEIGHT : "0px"})`, display: noData ? "none" : "" }), GRID_ROW: index => (index % 2 === 0 ? { backgroundColor: "#ffffff" } : { backgroundColor: "#f5f5f5" }), GROUP_HEADER_BOX: { border: "1px solid", backgroundColor: "#ebebeb", display: "flex", alignItems: "center", justifyContent: "center" }, GROUP_HEADER: { fontSize: "14px", textAlign: "center", wordWrap: "break-word" }, TASK_EDITOR_CONTENT: { minWidth: 400, overflowX: "auto" }, TASK_EDITOR_LIST: { width: "100%", minWidth: 300, maxWidth: 700, bgcolor: "background.paper" }, TASK_BOX: (lineHeight, bgColor, textColor, highlightColor) => ({ display: "flex", alignItems: "center", backgroundColor: bgColor ? bgColor : "#b4b9bf", ...(textColor ? { color: textColor } : {}), height: lineHeight, "&:hover": { ...(highlightColor ? { backgroundColor: `${highlightColor} !important`, filter: "brightness(1) !important" } : { filter: "brightness(1.25) !important" }), cursor: "pointer !important" } }), TASK: lineHeight => { const availableLines = Math.floor(lineHeight / 18); return { width: "100%", fontSize: "12px", overflowWrap: "break-word", wordBreak: "break-all", overflow: "hidden", textOverflow: "ellipsis", display: "-webkit-box", lineHeight: "18px", maxHeight: lineHeight, WebkitLineClamp: availableLines < 1 ? 1 : availableLines, WebkitBoxOrient: "vertical" }; } }; //Структура колонки const P8P_CYCLOGRAM_COLUMN_SHAPE = PropTypes.shape({ name: PropTypes.string.isRequired, start: PropTypes.number.isRequired, end: PropTypes.number.isRequired }); //Структура группы const P8P_CYCLOGRAM_GROUP_SHAPE = PropTypes.shape({ name: PropTypes.string.isRequired, height: PropTypes.number.isRequired, width: PropTypes.number.isRequired, visible: PropTypes.bool.isRequired }); //Структура задачи const P8P_CYCLOGRAM_TASK_SHAPE = PropTypes.shape({ id: PropTypes.string.isRequired, rn: PropTypes.number.isRequired, name: PropTypes.string.isRequired, fullName: PropTypes.string.isRequired, lineNumb: PropTypes.number.isRequired, start: PropTypes.number.isRequired, end: PropTypes.number.isRequired, group: PropTypes.string, bgColor: PropTypes.string, textColor: PropTypes.string, highlightColor: PropTypes.string }); //Структура динамического атрибута задачи const P8P_CYCLOGRAM_TASK_ATTRIBUTE_SHAPE = PropTypes.shape({ name: PropTypes.string.isRequired, caption: PropTypes.string.isRequired, visible: PropTypes.bool.isRequired }); //-------------------------------- //Вспомогательные классы и функции //-------------------------------- //Определение сдвига для максимальной ширины колонок const getShift = (columns, currentColumnsMaxWidth, maxCyclogramWidth) => { //Определяем доступное пространство для расширения let maxWidthDiff = maxCyclogramWidth - currentColumnsMaxWidth; //Инициализируем значение сдвига let shift = 1; //Если доступно больше ширины и есть пространство для расширения if (maxCyclogramWidth > currentColumnsMaxWidth && maxCyclogramWidth - maxWidthDiff > columns.length) { //Определяем доступный сдвиг колонок shift = maxCyclogramWidth / currentColumnsMaxWidth; } //Возвращаем сдвиг return shift; }; //Формирование стилей для группы const getGroupStyles = (indexGrp, highlightColor) => { return `.main .TaskGrp${indexGrp}:hover .TaskGrp${indexGrp} { ${highlightColor ? `background: ${highlightColor};` : `filter: brightness(1.15);`} } .main:has(.TaskGrp${indexGrp}:hover) .TaskGrpHeader${indexGrp} { display: block; } `; //cursor: pointer; }; //Фон строк таблицы const P8PCyclogramRowsGrid = ({ rows, maxWidth, lineHeight }) => { return ( {rows.map((el, index) => ( ))} ); }; //Контроль свойств - Фон строк таблицы P8PCyclogramRowsGrid.propTypes = { rows: PropTypes.array.isRequired, maxWidth: PropTypes.number.isRequired, lineHeight: PropTypes.number.isRequired }; //Линии строк таблицы const P8PCyclogramRowsLines = ({ rows, maxWidth, lineHeight }) => { return ( {rows.map((el, index) => ( ))} ); }; //Контроль свойств - Линии строк таблицы P8PCyclogramRowsLines.propTypes = { rows: PropTypes.array.isRequired, maxWidth: PropTypes.number.isRequired, lineHeight: PropTypes.number.isRequired }; //Линии колонок таблицы const P8PCyclogramColumnsLines = ({ columns, shift, y1, y2 }) => { //Инициализируем старт текущей колонки let prevColumnEnd = 0; return ( {columns.map((column, index) => { //Аккумулируем окончание последней колонки с учетом сдвига prevColumnEnd = index !== 0 ? prevColumnEnd + (columns[index - 1].end - columns[index - 1].start) * shift : 0; return ; })} ); }; //Контроль свойств - Линии колонок таблицы P8PCyclogramColumnsLines.propTypes = { columns: PropTypes.array.isRequired, shift: PropTypes.number.isRequired, y1: PropTypes.number.isRequired, y2: PropTypes.number.isRequired }; //Фон таблицы циклограммы const P8PCyclogramGrid = ({ tasks, columns, shift, maxWidth, maxHeight, lineHeight }) => { //Формируем массив строк исходя из максимального значения строки задачи const rows = Array.from(Array(Math.max(...tasks.map(o => o.lineNumb)) + 1).keys()); return ( ); }; //Контроль свойств - Фон таблицы циклограммы P8PCyclogramGrid.propTypes = { tasks: PropTypes.array.isRequired, columns: PropTypes.array.isRequired, shift: PropTypes.number.isRequired, maxWidth: PropTypes.number.isRequired, maxHeight: PropTypes.number.isRequired, lineHeight: PropTypes.number.isRequired }; //Колонка заголовка циклограммы const P8PCyclogramHeaderColumn = ({ column, start, shift, columnRenderer }) => { //Рассчитываем ширину колонки const columnWidth = column.end - column.start; //Формируем собственное отображение, если требуется const customView = columnRenderer ? columnRenderer({ column }) : null; return ( <> {customView ? ( customView ) : ( {column.name} )} ); }; //Контроль свойств - Колонка заголовка циклограммы P8PCyclogramHeaderColumn.propTypes = { column: PropTypes.object.isRequired, start: PropTypes.number.isRequired, shift: PropTypes.number.isRequired, maxHeight: PropTypes.number.isRequired, lastElement: PropTypes.bool, columnRenderer: PropTypes.func }; //Заголовок циклограммы const P8PCyclogramHeader = ({ columns, shift, maxWidth, maxHeight, columnRenderer, headerBlock }) => { //Инициализируем старт текущей колонки let prevColumnEnd = 0; return ( {columns.map((column, index) => { //Аккумулируем окончание последней колонки с учетом сдвига prevColumnEnd = index !== 0 ? prevColumnEnd + (columns[index - 1].end - columns[index - 1].start) * shift : 0; return ( ); })} ); }; //Контроль свойств - Заголовок циклограммы P8PCyclogramHeader.propTypes = { columns: PropTypes.array.isRequired, shift: PropTypes.number.isRequired, maxWidth: PropTypes.number.isRequired, maxHeight: PropTypes.number.isRequired, columnRenderer: PropTypes.func, headerBlock: PropTypes.object }; //Задача циклограммы const P8PCyclogramTask = ({ task, indexGrp, shift, lineHeight, openTaskEditor, taskRenderer }) => { //Рассчитываем ширину задачи const width = task.end !== 0 ? (task.end - task.start) * shift : 0; //Формируем собственное отображение, если требуется const customView = taskRenderer ? taskRenderer({ task, taskHeight: lineHeight, taskWidth: width }) || {} : {}; return ( openTaskEditor(task)} > {customView.data ? ( customView.data ) : ( {task.name} )} ); }; //Контроль свойств - Группы циклограммы P8PCyclogramTask.propTypes = { task: PropTypes.object.isRequired, indexGrp: PropTypes.number, shift: PropTypes.number.isRequired, lineHeight: PropTypes.number.isRequired, openTaskEditor: PropTypes.func.isRequired, taskRenderer: PropTypes.func }; //Основная информация циклограммы const P8PCyclogramMain = ({ columns, groups, tasks, shift, lineHeight, maxWidth, maxHeight, openTaskEditor, groupHeaderRenderer, taskRenderer, columnRenderer, headerBlock }) => { //Инициализируем коллекцию тасков с группами const tasksWithGroup = tasks.filter(task => hasValue(task.groupName)); //Инициализируем коллекцию тасков без групп const tasksWithoutGroup = tasks.filter(task => !hasValue(task.groupName)); //Инициализируем коллекцию отображаемых групп const visibleGroups = groups ? groups.filter(group => group.visible) : []; return ( {visibleGroups.length !== 0 ? visibleGroups.map((grp, indexGrp) => { //Считываем задачи группы let groupTasks = tasksWithGroup.filter(task => task.groupName === grp.name); //Если по данной группе нет тасков - ничего не выводим if (groupTasks.length === 0) { return null; } return ( {groupTasks.map((task, index) => ( ))} ); }) : null} {tasksWithoutGroup.map((task, index) => { return ( ); })} {visibleGroups.length !== 0 ? ( {visibleGroups.map((grp, indexGrp) => { //Инициализируем параметры группы let defaultView = null; let customView = null; let groupHeaderX = 0; let groupHeaderY = 0; let groupTasks = tasksWithGroup.filter(task => task.groupName === grp.name); //Если по данной группе нет тасков - ничего не выводим if (groupTasks.length === 0) { return null; } //Если требуется отображать заголовок группы if (grp.visible) { //Формируем отображение по умолчанию defaultView = ( {grp.name} ); //Формируем собственное отображение, если требуется customView = groupHeaderRenderer ? groupHeaderRenderer({ group: grp }) : null; //Рассчитываем координаты заголовка группы groupHeaderX = Math.min(...groupTasks.map(o => o.start)) * shift; groupHeaderY = NDEFAULT_HEADER_HEIGHT + Math.min(...groupTasks.map(o => o.lineNumb)) * lineHeight - grp.height - 5; } return ( {customView ? customView : defaultView} ); })} ) : null} ); }; //Контроль свойств - Основная информация циклограммы P8PCyclogramMain.propTypes = { columns: PropTypes.array.isRequired, groups: PropTypes.array, tasks: PropTypes.array.isRequired, shift: PropTypes.number.isRequired, lineHeight: PropTypes.number.isRequired, maxWidth: PropTypes.number.isRequired, maxHeight: PropTypes.number.isRequired, openTaskEditor: PropTypes.func.isRequired, groupHeaderRenderer: PropTypes.func, taskRenderer: PropTypes.func, columnRenderer: PropTypes.func, headerBlock: PropTypes.object }; //Редактор задачи const P8PCyclogramTaskEditor = ({ task, taskAttributes, onOk, onCancel, taskAttributeRenderer, taskDialogRenderer, nameCaption, okBtnCaption, cancelBtnCaption }) => { //Собственное состояние const [state, setState] = useState({ start: task.start, end: task.end }); //Отображаемые атрибуты const dispTaskAttributes = Array.isArray(taskAttributes) && taskAttributes.length > 0 ? taskAttributes.filter(attr => attr.visible && hasValue(task[attr.name])) : []; //При сохранении const handleOk = () => (onOk && state.start && state.end ? onOk() : null); //При отмене const handleCancel = () => (onCancel ? onCancel() : null); //Генерация содержимого return ( {taskDialogRenderer ? ( taskDialogRenderer({ task, taskAttributes, close: handleCancel }) ) : ( <> {dispTaskAttributes.length > 0 ? : null} {dispTaskAttributes.length > 0 ? dispTaskAttributes.map((attr, i) => { const defaultView = task[attr.name]; const customView = taskAttributeRenderer ? taskAttributeRenderer({ task, attribute: attr }) : null; return ( {i < dispTaskAttributes.length - 1 ? : null} ); }) : null} )} ); }; //Контроль свойств - Редактор задачи P8PCyclogramTaskEditor.propTypes = { task: P8P_CYCLOGRAM_TASK_SHAPE, taskAttributes: PropTypes.arrayOf(P8P_CYCLOGRAM_TASK_ATTRIBUTE_SHAPE), onOk: PropTypes.func, onCancel: PropTypes.func, taskAttributeRenderer: PropTypes.func, taskDialogRenderer: PropTypes.func, nameCaption: PropTypes.string.isRequired, okBtnCaption: PropTypes.string.isRequired, cancelBtnCaption: PropTypes.string.isRequired }; //Циклограмма const P8PCyclogram = ({ containerStyle, lineHeight, title, titleStyle, onTitleClick, zoomBar, zoom, columns, columnRenderer, groups, groupHeaderRenderer, tasks, taskRenderer, taskAttributes, taskAttributeRenderer, taskDialogRenderer, noDataFoundText, nameTaskEditorCaption, okTaskEditorBtnCaption, cancelTaskEditorBtnCaption }) => { //Хук основного блока (для последующего определения доступной ширины) const mainBlock = useRef(null); //Хук для заголовка таблицы const headerBlock = useRef(null); //Собственное состояние const [state, setState] = useState({ noData: true, loaded: false, lineHeight: NDEFAULT_LINE_HEIGHT, maxWidth: 0, maxHeight: 0, shift: 0, zoom: P8P_CYCLOGRAM_ZOOM.includes(zoom) ? zoom : 1, tasks: [], editTask: null }); //Обновление масштаба циклограммы const handleZoomChange = direction => { //Считываем текущий индекс const currentIndex = P8P_CYCLOGRAM_ZOOM.indexOf(state.zoom); setState(pv => ({ ...pv, zoom: currentIndex + direction !== P8P_CYCLOGRAM_ZOOM.length && currentIndex + direction !== -1 ? P8P_CYCLOGRAM_ZOOM[currentIndex + direction] : pv.zoom })); }; //Открытие редактора задачи const openTaskEditor = task => setState(pv => ({ ...pv, editTask: { ...task } })); //При сохранении задачи в редакторе const handleTaskEditorSave = () => { setState(pv => ({ ...pv, editTask: null })); }; //При закрытии редактора задачи без сохранения const handleTaskEditorCancel = () => setState(pv => ({ ...pv, editTask: null })); //При скролле блока const handleScroll = e => { //Изменяем позицию заголовка таблицы относительно скролла headerBlock.current.setAttribute("transform", "translate(0," + e.currentTarget.scrollTop + ")"); }; //При изменении данных useEffect(() => { //Если есть колонки и задачи if (Array.isArray(columns) && columns.length > 0 && Array.isArray(tasks) && tasks.length > 0) { //Определяем текущую максимальную ширину колонок let currentColumnsMaxWidth = Math.max(...columns.map(o => o.end)); //Определяем доступный сдвиг для ширины колонок (16 - паддинг по бокам) let columnShift = getShift(columns, currentColumnsMaxWidth, mainBlock.current.offsetWidth - 16) * state.zoom; //Устанавливаем значения исходя из колонок/задач setState(pv => ({ ...pv, loaded: true, lineHeight: lineHeight ? lineHeight : NDEFAULT_LINE_HEIGHT, maxWidth: columnShift !== 0 ? currentColumnsMaxWidth * columnShift : currentColumnsMaxWidth, maxHeight: NDEFAULT_HEADER_HEIGHT + (Math.max(...tasks.map(o => o.lineNumb)) + 1) * (lineHeight ? lineHeight : NDEFAULT_LINE_HEIGHT), shift: columnShift, tasks: tasks, noData: false })); } else { //Устанавливаем значения исходя из колонок/задач setState(pv => ({ ...pv, noData: true })); } }, [columns, lineHeight, state.zoom, tasks]); //Генерация содержимого return ( <>
{state.noData ? : null} {state.loaded ? ( <> {title ? ( {onTitleClick ? ( onTitleClick()}> {title} ) : ( title )} ) : null} {zoomBar ? ( handleZoomChange(1)} disabled={state.zoom == P8P_CYCLOGRAM_ZOOM[P8P_CYCLOGRAM_ZOOM.length - 1]} > zoom_in handleZoomChange(-1)} disabled={state.zoom == P8P_CYCLOGRAM_ZOOM[0]}> zoom_out ) : null} ) : null} {state.editTask ? ( ) : null}
); }; //Контроль свойств - Циклограмма P8PCyclogram.propTypes = { containerStyle: PropTypes.object, lineHeight: PropTypes.number, title: PropTypes.string, titleStyle: PropTypes.object, onTitleClick: PropTypes.func, zoomBar: PropTypes.bool, zoom: PropTypes.number, columns: PropTypes.arrayOf(P8P_CYCLOGRAM_COLUMN_SHAPE).isRequired, columnRenderer: PropTypes.func, groups: PropTypes.arrayOf(P8P_CYCLOGRAM_GROUP_SHAPE), groupHeaderRenderer: PropTypes.func, tasks: PropTypes.arrayOf(P8P_CYCLOGRAM_TASK_SHAPE).isRequired, taskRenderer: PropTypes.func, taskAttributes: PropTypes.arrayOf(P8P_CYCLOGRAM_TASK_ATTRIBUTE_SHAPE), taskAttributeRenderer: PropTypes.func, taskDialogRenderer: PropTypes.func, noDataFoundText: PropTypes.string.isRequired, nameTaskEditorCaption: PropTypes.string.isRequired, okTaskEditorBtnCaption: PropTypes.string.isRequired, cancelTaskEditorBtnCaption: PropTypes.string.isRequired }; //---------------- //Интерфейс модуля //---------------- export { P8PCyclogram };