/* Парус 8 - Панели мониторинга Компонент: Диаграмма Ганта */ //--------------------- //Подключение библиотек //--------------------- import React, { useEffect, useState, useCallback, useRef } from "react"; //Классы React import PropTypes from "prop-types"; //Контроль свойств компонента import { Box, IconButton, Icon, Typography, Dialog, DialogActions, DialogContent, TextField, Button, List, ListItem, ListItemText, Divider, Slider, Link } from "@mui/material"; //Интерфейсные компоненты import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке //--------- //Константы //--------- //Уровни масштаба const P8P_GANTT_ZOOM = [0, 1, 2, 3, 4, 5]; //Уровни масштаба (строковые наименования в терминах библиотеки) const P8P_GANTT_ZOOM_VIEW_MODES = { 0: "Quarter Day", 1: "Half Day", 2: "Day", 3: "Week", 4: "Month", 5: "Year" }; //Структура задачи const P8P_GANTT_TASK_SHAPE = PropTypes.shape({ id: PropTypes.string.isRequired, rn: PropTypes.number.isRequired, numb: PropTypes.string.isRequired, name: PropTypes.string.isRequired, fullName: PropTypes.string.isRequired, start: PropTypes.string.isRequired, end: PropTypes.string.isRequired, progress: PropTypes.number, dependencies: PropTypes.array, readOnly: PropTypes.bool, readOnlyDates: PropTypes.bool, readOnlyProgress: PropTypes.bool, bgColor: PropTypes.string, textColor: PropTypes.string, bgProgressColor: PropTypes.string }); //Структура динамического атрибута задачи const P8P_GANTT_TASK_ATTRIBUTE_SHAPE = PropTypes.shape({ name: PropTypes.string.isRequired, caption: PropTypes.string.isRequired, visible: PropTypes.bool.isRequired }); //Структура описания цвета задачи const P8P_GANTT_TASK_COLOR_SHAPE = PropTypes.shape({ bgColor: PropTypes.string, textColor: PropTypes.string, bgProgressColor: PropTypes.string, desc: PropTypes.string.isRequired }); //Высота заголовка const TITLE_HEIGHT = "44px"; //Высота панели масштабирования const ZOOM_HEIGHT = "56px"; //Стили const STYLES = { TASK_EDITOR_CONTENT: { minWidth: 400, overflowX: "auto" }, TASK_EDITOR_LIST: { width: "100%", minWidth: 300, maxWidth: 700, bgcolor: "background.paper" }, GANTT_TITLE: { height: TITLE_HEIGHT }, GANTT_ZOOM: { height: ZOOM_HEIGHT }, GANTT: (noData, title, zoomBar) => ({ height: `calc(100% - ${zoomBar ? ZOOM_HEIGHT : "0px"} - ${title ? TITLE_HEIGHT : "0px"})`, display: noData ? "none" : "" }) }; //-------------------------------- //Вспомогательные классы и функции //-------------------------------- //Проверка существования значения const hasValue = value => typeof value !== "undefined" && value !== null && value !== ""; //Формирование описания для легенды const taskLegendDesc = ({ task, taskColors }) => { if (Array.isArray(taskColors) && taskColors.length > 0) { const colorDesc = taskColors.find( color => task.bgColor === color.bgColor && task.textColor === color.textColor && task.bgProgressColor === color.bgProgressColor ); if (colorDesc) return { text: colorDesc.desc, style: { ...(colorDesc.bgProgressColor ? { background: `linear-gradient(to right, ${colorDesc.bgProgressColor} ,${ colorDesc.bgColor ? colorDesc.bgColor : "transparent" })` } : colorDesc.bgColor ? { backgroundColor: colorDesc.bgColor } : {}), ...(colorDesc.textColor ? { color: colorDesc.textColor } : {}) } }; else return null; } else return null; }; //Редактор задачи const P8PGanttTaskEditor = ({ task, taskAttributes, taskColors, onOk, onCancel, taskAttributeRenderer, taskDialogRenderer, taskDialogProps, numbCaption, nameCaption, startCaption, endCaption, progressCaption, legendCaption, okBtnCaption, cancelBtnCaption }) => { //Собственное состояние const [state, setState] = useState({ start: task.start, end: task.end, progress: task.progress }); //Отображаемые атрибуты 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({ task, start: state.start, end: state.end, progress: state.progress }) : null); //При отмене const handleCancel = () => (onCancel ? onCancel() : null); //При изменении сроков const handlePeriodChanged = e => setState(prev => ({ ...prev, [e.target.name]: e.target.value })); //При изменении прогресса const handleProgressChanged = (e, newValue) => setState(prev => ({ ...prev, progress: newValue })); //Описание легенды для задачи const legendDesc = taskLegendDesc({ task, taskColors }); let legend = legendDesc ? ( ) : null; //Генерация содержимого return ( {taskDialogRenderer ? ( taskDialogRenderer({ task, taskAttributes, taskColors, close: handleCancel }) ) : ( <> } /> } /> {hasValue(task.progress) || legend || dispTaskAttributes.length > 0 ? : null} {hasValue(task.progress) ? ( <> } /> {legend || dispTaskAttributes.length > 0 ? : null} ) : null} {legend ? ( <> {legend} {dispTaskAttributes.length > 0 ? : null} ) : 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} )} ); }; //Контроль свойств - Редактор задачи P8PGanttTaskEditor.propTypes = { task: P8P_GANTT_TASK_SHAPE, taskAttributes: PropTypes.arrayOf(P8P_GANTT_TASK_ATTRIBUTE_SHAPE), taskColors: PropTypes.arrayOf(P8P_GANTT_TASK_COLOR_SHAPE), onOk: PropTypes.func, onCancel: PropTypes.func, taskAttributeRenderer: PropTypes.func, taskDialogRenderer: PropTypes.func, taskDialogProps: PropTypes.object, numbCaption: PropTypes.string.isRequired, nameCaption: PropTypes.string.isRequired, startCaption: PropTypes.string.isRequired, endCaption: PropTypes.string.isRequired, progressCaption: PropTypes.string.isRequired, legendCaption: PropTypes.string.isRequired, okBtnCaption: PropTypes.string.isRequired, cancelBtnCaption: PropTypes.string.isRequired }; //----------- //Тело модуля //----------- //Диаграмма Ганта const P8PGantt = ({ containerStyle, title, titleStyle, onTitleClick, zoomBar, readOnly, readOnlyDates, readOnlyProgress, zoom, tasks, taskAttributes, taskColors, onTaskDatesChange, onTaskProgressChange, taskAttributeRenderer, taskDialogRenderer, taskDialogProps, noDataFoundText, numbTaskEditorCaption, nameTaskEditorCaption, startTaskEditorCaption, endTaskEditorCaption, progressTaskEditorCaption, legendTaskEditorCaption, okTaskEditorBtnCaption, cancelTaskEditorBtnCaption }) => { //Собственное состояние const [state, setState] = useState({ noData: true, gantt: null, zoom: P8P_GANTT_ZOOM.includes(zoom) ? zoom : 3, editTask: null }); //Ссылки на DOM const svgContainerRef = useRef(null); //Отображение диаграммы const showGantt = useCallback(() => { if (!state.gantt) { // eslint-disable-next-line no-undef const gantt = new Gantt("#__gantt__", tasks, { view_mode: P8P_GANTT_ZOOM_VIEW_MODES[state.zoom], date_format: "YYYY-MM-DD", language: "ru", readOnly, readOnlyDates, readOnlyProgress, on_date_change: (task, start, end, isMain) => (onTaskDatesChange ? onTaskDatesChange({ task, start, end, isMain }) : null), on_progress_change: (task, progress) => (onTaskProgressChange ? onTaskProgressChange({ task, progress }) : null), on_click: openTaskEditor }); setState(pv => ({ ...pv, gantt, noData: false })); } else { state.gantt.refresh(tasks); setState(pv => ({ ...pv, noData: false })); } }, [state.gantt, state.zoom, readOnly, readOnlyDates, readOnlyProgress, tasks, onTaskDatesChange, onTaskProgressChange]); //Обновление масштаба диаграммы const handleZoomChange = direction => setState(pv => ({ ...pv, zoom: pv.zoom + direction < 0 ? 0 : pv.zoom + direction >= P8P_GANTT_ZOOM.length ? P8P_GANTT_ZOOM.length - 1 : pv.zoom + direction })); //Открытие редактора задачи const openTaskEditor = task => setState(pv => ({ ...pv, editTask: { ...task } })); //При сохранении задачи в редакторе const handleTaskEditorSave = ({ task, start, end, progress }) => { setState(pv => ({ ...pv, editTask: null })); if (onTaskDatesChange && (task.start != start || task.end != end)) onTaskDatesChange({ task, start, end, isMain: true }); if (onTaskProgressChange && task.progress != progress) onTaskProgressChange({ task, progress }); }; //При закрытии редактора задачи без сохранения const handleTaskEditorCancel = () => setState(pv => ({ ...pv, editTask: null })); //При изменении масштаба useEffect(() => { if (state.gantt) state.gantt.change_view_mode(P8P_GANTT_ZOOM_VIEW_MODES[state.zoom]); }, [state.gantt, state.zoom]); //При изменении списка задач useEffect(() => { if (Array.isArray(tasks) && tasks.length > 0) showGantt(); else setState(pv => ({ ...pv, noData: true })); // eslint-disable-next-line react-hooks/exhaustive-deps }, [tasks]); //При подключении компонента к старице useEffect(() => { svgContainerRef.current.children[0].classList.add("scroll"); }, []); //Генерация содержимого return (
{state.gantt && state.noData ? : null} {state.gantt && !state.noData && title ? ( {onTitleClick ? ( onTitleClick()}> {title} ) : ( title )} ) : null} {state.gantt && !state.noData && zoomBar ? ( handleZoomChange(-1)} disabled={state.zoom == 0}> zoom_in handleZoomChange(1)} disabled={state.zoom == P8P_GANTT_ZOOM.length - 1}> zoom_out ) : null} {state.editTask ? ( ) : null}
); }; //Контроль свойств - Диаграмма Ганта P8PGantt.propTypes = { containerStyle: PropTypes.object, title: PropTypes.string, titleStyle: PropTypes.object, onTitleClick: PropTypes.func, zoomBar: PropTypes.bool, readOnly: PropTypes.bool, readOnlyDates: PropTypes.bool, readOnlyProgress: PropTypes.bool, zoom: PropTypes.number, tasks: PropTypes.arrayOf(P8P_GANTT_TASK_SHAPE).isRequired, taskAttributes: PropTypes.arrayOf(P8P_GANTT_TASK_ATTRIBUTE_SHAPE), taskColors: PropTypes.arrayOf(P8P_GANTT_TASK_COLOR_SHAPE), onTaskDatesChange: PropTypes.func, onTaskProgressChange: PropTypes.func, taskAttributeRenderer: PropTypes.func, taskDialogRenderer: PropTypes.func, taskDialogProps: PropTypes.object, noDataFoundText: PropTypes.string.isRequired, numbTaskEditorCaption: PropTypes.string.isRequired, nameTaskEditorCaption: PropTypes.string.isRequired, startTaskEditorCaption: PropTypes.string.isRequired, endTaskEditorCaption: PropTypes.string.isRequired, progressTaskEditorCaption: PropTypes.string.isRequired, legendTaskEditorCaption: PropTypes.string.isRequired, okTaskEditorBtnCaption: PropTypes.string.isRequired, cancelTaskEditorBtnCaption: PropTypes.string.isRequired }; //---------------- //Интерфейс модуля //---------------- export { P8P_GANTT_TASK_SHAPE, P8P_GANTT_TASK_ATTRIBUTE_SHAPE, P8P_GANTT_TASK_COLOR_SHAPE, taskLegendDesc, P8PGantt };