/*
    Парус 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"; //Вспомогательный функции
import { useP8PCyclogram } from "./p8p_cyclogram_hooks"; //Хук для циклограммы

//---------
//Константы
//---------

//Уровни масштаба
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 (
        <g>
            {rows.map((el, index) => (
                <foreignObject x="0" y={NDEFAULT_HEADER_HEIGHT + index * lineHeight} width={maxWidth} height={lineHeight} key={index}>
                    <Box sx={STYLES.GRID_ROW(index)} height={lineHeight} />
                </foreignObject>
            ))}
        </g>
    );
};

//Контроль свойств - Фон строк таблицы
P8PCyclogramRowsGrid.propTypes = {
    rows: PropTypes.array.isRequired,
    maxWidth: PropTypes.number.isRequired,
    lineHeight: PropTypes.number.isRequired
};

//Линии строк таблицы
const P8PCyclogramRowsLines = ({ rows, maxWidth, lineHeight }) => {
    return (
        <g>
            {rows.map((el, index) => (
                <line
                    x1="0"
                    y1={NDEFAULT_HEADER_HEIGHT + lineHeight + index * lineHeight}
                    x2={maxWidth}
                    y2={NDEFAULT_HEADER_HEIGHT + lineHeight + index * lineHeight}
                    key={index}
                ></line>
            ))}
        </g>
    );
};

//Контроль свойств - Линии строк таблицы
P8PCyclogramRowsLines.propTypes = {
    rows: PropTypes.array.isRequired,
    maxWidth: PropTypes.number.isRequired,
    lineHeight: PropTypes.number.isRequired
};

//Линии колонок таблицы
const P8PCyclogramColumnsLines = ({ columns, shift, y1, y2 }) => {
    //Инициализируем старт текущей колонки
    let prevColumnEnd = 0;
    return (
        <g>
            {columns.map((column, index) => {
                //Аккумулируем окончание последней колонки с учетом сдвига
                prevColumnEnd = index !== 0 ? prevColumnEnd + (columns[index - 1].end - columns[index - 1].start) * shift : 0;
                return <line x1={prevColumnEnd} y1={y1} x2={prevColumnEnd} y2={y2} stroke="#e0e0e0" key={index} />;
            })}
            <line
                x1={prevColumnEnd + (columns[columns.length - 1].end - columns[columns.length - 1].start) * shift}
                y1={y1}
                x2={prevColumnEnd + (columns[columns.length - 1].end - columns[columns.length - 1].start) * shift}
                y2={y2}
                stroke="#e0e0e0"
            />
        </g>
    );
};

//Контроль свойств - Линии колонок таблицы
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 (
        <g className="grid">
            <rect x="0" y="0" width={maxWidth} height={maxHeight}></rect>
            <P8PCyclogramRowsGrid rows={rows} maxWidth={maxWidth} lineHeight={lineHeight} />
            <P8PCyclogramRowsLines rows={rows} maxWidth={maxWidth} lineHeight={lineHeight} />
            <P8PCyclogramColumnsLines columns={columns} shift={shift} y1={NDEFAULT_HEADER_HEIGHT} y2={maxHeight} />
        </g>
    );
};

//Контроль свойств - Фон таблицы циклограммы
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 (
        <>
            <foreignObject x={start} y="0" width={columnWidth * shift} height={NDEFAULT_HEADER_HEIGHT}>
                {customView ? (
                    customView
                ) : (
                    <Typography sx={{ ...STYLES.HEADER_COLUMN, height: NDEFAULT_HEADER_HEIGHT }} title={column.name}>
                        {column.name}
                    </Typography>
                )}
            </foreignObject>
        </>
    );
};

//Контроль свойств - Колонка заголовка циклограммы
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 (
        <g className="header" ref={headerBlock}>
            <rect x="0" y="0" width={maxWidth} height={NDEFAULT_HEADER_HEIGHT} fill="#ffffff" stroke="#e0e0e0" strokeWidth="1.4"></rect>
            {columns.map((column, index) => {
                //Аккумулируем окончание последней колонки с учетом сдвига
                prevColumnEnd = index !== 0 ? prevColumnEnd + (columns[index - 1].end - columns[index - 1].start) * shift : 0;
                return (
                    <P8PCyclogramHeaderColumn
                        column={column}
                        shift={shift}
                        start={prevColumnEnd}
                        maxHeight={maxHeight}
                        lastElement={columns.length - 1 === index}
                        columnRenderer={columnRenderer}
                        key={index}
                    />
                );
            })}
            <g className="columnsDividers">
                <P8PCyclogramColumnsLines columns={columns} shift={shift} y1={0} y2={NDEFAULT_HEADER_HEIGHT} />
            </g>
        </g>
    );
};

//Контроль свойств - Заголовок циклограммы
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 (
        <foreignObject
            x={task.start !== 0 ? task.start * shift : 0}
            y={NDEFAULT_HEADER_HEIGHT + task.lineNumb * lineHeight}
            width={width}
            height={lineHeight}
        >
            <Box
                className={hasValue(indexGrp) ? `TaskGrp${indexGrp}` : null}
                sx={{ ...STYLES.TASK_BOX(lineHeight, task.bgColor, task.textColor, task.highlightColor), ...customView.taskStyle }}
                {...customView.taskProps}
                onClick={() => openTaskEditor(task)}
            >
                {customView.data ? (
                    customView.data
                ) : (
                    <Typography sx={STYLES.TASK(lineHeight)} title={task.name}>
                        {task.name}
                    </Typography>
                )}
            </Box>
        </foreignObject>
    );
};

//Контроль свойств - Группы циклограммы
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 (
        <g className="main">
            <g className="tasks">
                {visibleGroups.length !== 0
                    ? visibleGroups.map((grp, indexGrp) => {
                          //Считываем задачи группы
                          let groupTasks = tasksWithGroup.filter(task => task.groupName === grp.name);
                          //Если по данной группе нет тасков - ничего не выводим
                          if (groupTasks.length === 0) {
                              return null;
                          }
                          return (
                              <g className={`TaskGrp${indexGrp}`} key={indexGrp}>
                                  {groupTasks.map((task, index) => (
                                      <P8PCyclogramTask
                                          task={task}
                                          indexGrp={indexGrp}
                                          shift={shift}
                                          lineHeight={lineHeight}
                                          openTaskEditor={openTaskEditor}
                                          taskRenderer={taskRenderer}
                                          key={index}
                                      />
                                  ))}
                                  <style>{getGroupStyles(indexGrp, grp.highlightColor)}</style>
                              </g>
                          );
                      })
                    : null}
                <g className={`TasksWithoutGroups`}>
                    {tasksWithoutGroup.map((task, index) => {
                        return (
                            <P8PCyclogramTask
                                task={task}
                                shift={shift}
                                lineHeight={lineHeight}
                                openTaskEditor={openTaskEditor}
                                taskRenderer={taskRenderer}
                                key={index}
                            />
                        );
                    })}
                </g>
            </g>
            <P8PCyclogramHeader
                columns={columns}
                shift={shift}
                maxWidth={maxWidth}
                maxHeight={maxHeight}
                columnRenderer={columnRenderer}
                headerBlock={headerBlock}
            />
            {visibleGroups.length !== 0 ? (
                <g className="groups">
                    {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 = (
                                <Box sx={{ ...STYLES.GROUP_HEADER_BOX, height: grp.height }}>
                                    <Typography sx={{ ...STYLES.GROUP_HEADER, maxWidth: grp.width, maxHeight: grp.height }}>{grp.name}</Typography>
                                </Box>
                            );
                            //Формируем собственное отображение, если требуется
                            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 (
                            <foreignObject
                                x={groupHeaderX}
                                y={groupHeaderY}
                                width={grp.width}
                                height={grp.height}
                                className={`TaskGrpHeader${indexGrp}`}
                                display="none"
                                key={indexGrp}
                            >
                                {customView ? customView : defaultView}
                            </foreignObject>
                        );
                    })}
                </g>
            ) : null}
        </g>
    );
};

//Контроль свойств - Основная информация циклограммы
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] = 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 (
        <Dialog open onClose={handleCancel}>
            {taskDialogRenderer ? (
                taskDialogRenderer({ task, taskAttributes, close: handleCancel })
            ) : (
                <>
                    <DialogContent sx={STYLES.TASK_EDITOR_CONTENT}>
                        <List sx={STYLES.TASK_EDITOR_LIST}>
                            <ListItem alignItems="flex-start">
                                <ListItemText primary={nameCaption} secondary={task.fullName} />
                            </ListItem>
                            {dispTaskAttributes.length > 0 ? <Divider component="li" /> : null}
                            {dispTaskAttributes.length > 0
                                ? dispTaskAttributes.map((attr, i) => {
                                      const defaultView = task[attr.name];
                                      const customView = taskAttributeRenderer ? taskAttributeRenderer({ task, attribute: attr }) : null;
                                      return (
                                          <React.Fragment key={i}>
                                              <ListItem alignItems="flex-start">
                                                  <ListItemText
                                                      primary={attr.caption}
                                                      secondaryTypographyProps={{ component: "span" }}
                                                      secondary={customView ? customView : defaultView}
                                                  />
                                              </ListItem>
                                              {i < dispTaskAttributes.length - 1 ? <Divider component="li" /> : null}
                                          </React.Fragment>
                                      );
                                  })
                                : null}
                        </List>
                    </DialogContent>
                    <DialogActions>
                        <Button onClick={handleOk}>{okBtnCaption}</Button>
                        <Button onClick={handleCancel}>{cancelBtnCaption}</Button>
                    </DialogActions>
                </>
            )}
        </Dialog>
    );
};

//Контроль свойств - Редактор задачи
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 (
        <>
            <div ref={mainBlock} style={{ ...(containerStyle ? containerStyle : {}) }}>
                {state.noData ? <P8PAppInlineError text={noDataFoundText} /> : null}
                {state.loaded ? (
                    <>
                        {title ? (
                            <Typography
                                p={1}
                                sx={{ ...STYLES.CYCLOGRAM_TITLE, ...(titleStyle ? titleStyle : {}) }}
                                align="center"
                                color="textSecondary"
                                variant="subtitle1"
                            >
                                {onTitleClick ? (
                                    <Link component="button" variant="body2" underline="hover" onClick={() => onTitleClick()}>
                                        {title}
                                    </Link>
                                ) : (
                                    title
                                )}
                            </Typography>
                        ) : null}
                        {zoomBar ? (
                            <Box p={1} sx={STYLES.CYCLOGRAM_ZOOM}>
                                <IconButton
                                    onClick={() => handleZoomChange(1)}
                                    disabled={state.zoom == P8P_CYCLOGRAM_ZOOM[P8P_CYCLOGRAM_ZOOM.length - 1]}
                                >
                                    <Icon>zoom_in</Icon>
                                </IconButton>
                                <IconButton onClick={() => handleZoomChange(-1)} disabled={state.zoom == P8P_CYCLOGRAM_ZOOM[0]}>
                                    <Icon>zoom_out</Icon>
                                </IconButton>
                            </Box>
                        ) : null}
                        <Box className="scroll" sx={STYLES.CYCLOGRAM_BOX(state.noData, title, zoomBar)} onScroll={handleScroll}>
                            <svg id="cyclogram" width={state.maxWidth} height={state.maxHeight}>
                                <P8PCyclogramGrid
                                    tasks={state.tasks}
                                    columns={columns}
                                    shift={state.shift}
                                    maxWidth={state.maxWidth}
                                    maxHeight={state.maxHeight}
                                    lineHeight={state.lineHeight}
                                />
                                <P8PCyclogramMain
                                    columns={columns}
                                    groups={groups}
                                    tasks={state.tasks}
                                    shift={state.shift}
                                    lineHeight={state.lineHeight}
                                    maxWidth={state.maxWidth}
                                    maxHeight={state.maxHeight}
                                    groupHeaderRenderer={groupHeaderRenderer}
                                    openTaskEditor={openTaskEditor}
                                    taskRenderer={taskRenderer}
                                    columnRenderer={columnRenderer}
                                    headerBlock={headerBlock}
                                />
                            </svg>
                        </Box>
                    </>
                ) : null}
                {state.editTask ? (
                    <P8PCyclogramTaskEditor
                        task={state.editTask}
                        taskAttributes={taskAttributes}
                        onOk={handleTaskEditorSave}
                        onCancel={handleTaskEditorCancel}
                        taskAttributeRenderer={taskAttributeRenderer}
                        taskDialogRenderer={taskDialogRenderer}
                        nameCaption={nameTaskEditorCaption}
                        okBtnCaption={okTaskEditorBtnCaption}
                        cancelBtnCaption={cancelTaskEditorBtnCaption}
                    />
                ) : null}
            </div>
        </>
    );
};

//Контроль свойств - Циклограмма
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, useP8PCyclogram };
