/*
Парус 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];
//Уровни масштаба (строковые наименования в терминах библиотеки)
const P8P_GANTT_ZOOM_VIEW_MODES = {
0: "Quarter Day",
1: "Half Day",
2: "Day",
3: "Week",
4: "Month"
};
//Структура задачи
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,
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 (
);
};
//Контроль свойств - Редактор задачи
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,
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,
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,
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 };