forked from CITKParus/P8-Panels
WEB APP: Компонент - Диаграмма Ганта
This commit is contained in:
parent
eca8d40c94
commit
0d3158b261
12
app.text.js
12
app.text.js
@ -36,11 +36,17 @@ export const BUTTONS = {
|
||||
MORE: "Ещё" //Догрузка данных
|
||||
};
|
||||
|
||||
//Текст элементов ввода
|
||||
export const INPUTS = {
|
||||
//Метки атрибутов, сопроводительные надписи
|
||||
export const CAPTIONS = {
|
||||
VALUE: "Значение",
|
||||
VALUE_FROM: "С",
|
||||
VALUE_TO: "По"
|
||||
VALUE_TO: "По",
|
||||
NUMB: "Номер",
|
||||
NAME: "Наименование",
|
||||
START: "Начало",
|
||||
END: "Окончание",
|
||||
PROGRESS: "Прогресс",
|
||||
LEGEND: "Легенда"
|
||||
};
|
||||
|
||||
//Типовые сообщения об ошибках
|
||||
|
462
app/components/p8p_gantt.js
Normal file
462
app/components/p8p_gantt.js
Normal file
@ -0,0 +1,462 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Компонент: Диаграмма Ганта
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useEffect, useState, useCallback } 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
|
||||
});
|
||||
|
||||
//Структура динамического атрибута задачи
|
||||
const P8P_GANTT_TASK_ATTRIBUTE_SHAPE = PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
caption: PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
//Структура описания цвета задачи
|
||||
const P8P_GANTT_TASK_COLOR_SHAPE = PropTypes.shape({
|
||||
bgColor: PropTypes.string,
|
||||
textColor: PropTypes.string,
|
||||
desc: PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
TASK_EDITOR_LIST: { width: "100%", minWidth: 300, maxWidth: 700, bgcolor: "background.paper" }
|
||||
};
|
||||
|
||||
//--------------------------------
|
||||
//Вспомогательные классы и функции
|
||||
//--------------------------------
|
||||
|
||||
//Проверка существования значения
|
||||
const hasValue = value => typeof value !== "undefined" && value !== null && value !== "";
|
||||
|
||||
//Редактор задачи
|
||||
const P8PGanttTaskEditor = ({
|
||||
task,
|
||||
taskAttributes,
|
||||
taskColors,
|
||||
onOk,
|
||||
onCancel,
|
||||
taskAttributeRenderer,
|
||||
numbCaption,
|
||||
nameCaption,
|
||||
startCaption,
|
||||
endCaption,
|
||||
progressCaption,
|
||||
legendCaption,
|
||||
okBtnCaption,
|
||||
cancelBtnCaption
|
||||
}) => {
|
||||
//Собственное состояние
|
||||
const [state, setState] = useState({
|
||||
start: task.start,
|
||||
end: task.end,
|
||||
progress: task.progress
|
||||
});
|
||||
|
||||
//При сохранении
|
||||
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) => {
|
||||
console.log(newValue);
|
||||
setState(prev => ({ ...prev, progress: newValue }));
|
||||
};
|
||||
|
||||
//Описание легенды для задачи
|
||||
let legend = null;
|
||||
if (Array.isArray(taskColors)) {
|
||||
const colorDesc = taskColors.find(color => task.bgColor === color.bgColor && task.textColor === color.textColor);
|
||||
if (colorDesc)
|
||||
legend = (
|
||||
<ListItemText
|
||||
secondaryTypographyProps={{
|
||||
p: 1,
|
||||
sx: {
|
||||
...(colorDesc.bgColor ? { backgroundColor: colorDesc.bgColor } : {}),
|
||||
...(colorDesc.textColor ? { color: colorDesc.textColor } : {})
|
||||
}
|
||||
}}
|
||||
primary={legendCaption}
|
||||
secondary={colorDesc.desc}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Dialog open onClose={handleCancel}>
|
||||
<DialogContent>
|
||||
<List sx={STYLES.TASK_EDITOR_LIST}>
|
||||
<ListItem alignItems="flex-start">
|
||||
<ListItemText primary={numbCaption} secondary={task.numb} />
|
||||
</ListItem>
|
||||
<Divider component="li" />
|
||||
<ListItem alignItems="flex-start">
|
||||
<ListItemText primary={nameCaption} secondary={task.fullName} />
|
||||
</ListItem>
|
||||
<Divider component="li" />
|
||||
<ListItem alignItems="flex-start">
|
||||
<ListItemText
|
||||
secondaryTypographyProps={{ component: "span" }}
|
||||
primary={startCaption}
|
||||
secondary={
|
||||
<TextField
|
||||
error={!state.start}
|
||||
disabled={task.readOnly === true || task.readOnlyDates === true}
|
||||
name="start"
|
||||
fullWidth
|
||||
required
|
||||
InputLabelProps={{ shrink: true }}
|
||||
type={"date"}
|
||||
value={state.start}
|
||||
onChange={handlePeriodChanged}
|
||||
variant="standard"
|
||||
size="small"
|
||||
margin="normal"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider component="li" />
|
||||
<ListItem alignItems="flex-start">
|
||||
<ListItemText
|
||||
secondaryTypographyProps={{ component: "span" }}
|
||||
primary={endCaption}
|
||||
secondary={
|
||||
<TextField
|
||||
error={!state.end}
|
||||
disabled={task.readOnly === true || task.readOnlyDates === true}
|
||||
name="end"
|
||||
fullWidth
|
||||
required
|
||||
InputLabelProps={{ shrink: true }}
|
||||
type={"date"}
|
||||
value={state.end}
|
||||
onChange={handlePeriodChanged}
|
||||
variant="standard"
|
||||
size="small"
|
||||
margin="normal"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider component="li" />
|
||||
{hasValue(task.progress) ? (
|
||||
<>
|
||||
<ListItem alignItems="flex-start">
|
||||
<ListItemText
|
||||
secondaryTypographyProps={{ component: "span" }}
|
||||
primary={`${progressCaption}${
|
||||
task.readOnly === true || task.readOnlyProgress === true ? ` (${task.progress}%)` : ""
|
||||
}`}
|
||||
secondary={
|
||||
<Slider
|
||||
disabled={task.readOnly === true || task.readOnlyProgress === true}
|
||||
defaultValue={task.progress}
|
||||
valueLabelDisplay="auto"
|
||||
onChange={handleProgressChanged}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<Divider component="li" />
|
||||
</>
|
||||
) : null}
|
||||
{legend ? (
|
||||
<>
|
||||
<ListItem alignItems="flex-start">{legend}</ListItem>
|
||||
<Divider component="li" />
|
||||
</>
|
||||
) : null}
|
||||
{Array.isArray(taskAttributes) && taskAttributes.length > 0
|
||||
? taskAttributes
|
||||
.filter(attr => hasValue(task[attr.name]))
|
||||
.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 < taskAttributes.length - 1 ? <Divider component="li" /> : null}
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</List>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button disabled={!state.start || !state.end || task.readOnly} onClick={handleOk}>
|
||||
{okBtnCaption}
|
||||
</Button>
|
||||
<Button onClick={handleCancel}>{cancelBtnCaption}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Редактор задачи
|
||||
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,
|
||||
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 = ({
|
||||
height,
|
||||
title,
|
||||
titleStyle,
|
||||
onTitleClick,
|
||||
zoomBar,
|
||||
readOnly,
|
||||
readOnlyDates,
|
||||
readOnlyProgress,
|
||||
zoom,
|
||||
tasks,
|
||||
taskAttributes,
|
||||
taskColors,
|
||||
onTaskDatesChange,
|
||||
onTaskProgressChange,
|
||||
taskAttributeRenderer,
|
||||
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
|
||||
});
|
||||
|
||||
//Отображение диаграммы
|
||||
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]);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<div>
|
||||
{state.gantt && state.noData ? <P8PAppInlineError text={noDataFoundText} /> : null}
|
||||
{state.gantt && !state.noData && title ? (
|
||||
<Typography p={1} sx={{ ...(titleStyle ? titleStyle : {}) }} align="center" color="textSecondary" variant="subtitle1">
|
||||
{onTitleClick ? (
|
||||
<Link component="button" variant="body2" underline="hover" onClick={() => onTitleClick()}>
|
||||
{title}
|
||||
</Link>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</Typography>
|
||||
) : null}
|
||||
{state.gantt && !state.noData && zoomBar ? (
|
||||
<Box p={1}>
|
||||
<IconButton onClick={() => handleZoomChange(-1)} disabled={state.zoom == 0}>
|
||||
<Icon>zoom_in</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={() => handleZoomChange(1)} disabled={state.zoom == P8P_GANTT_ZOOM.length - 1}>
|
||||
<Icon>zoom_out</Icon>
|
||||
</IconButton>
|
||||
</Box>
|
||||
) : null}
|
||||
{state.editTask ? (
|
||||
<P8PGanttTaskEditor
|
||||
task={state.editTask}
|
||||
taskAttributes={taskAttributes}
|
||||
taskColors={taskColors}
|
||||
onOk={handleTaskEditorSave}
|
||||
onCancel={handleTaskEditorCancel}
|
||||
taskAttributeRenderer={taskAttributeRenderer}
|
||||
numbCaption={numbTaskEditorCaption}
|
||||
nameCaption={nameTaskEditorCaption}
|
||||
startCaption={startTaskEditorCaption}
|
||||
endCaption={endTaskEditorCaption}
|
||||
progressCaption={progressTaskEditorCaption}
|
||||
legendCaption={legendTaskEditorCaption}
|
||||
okBtnCaption={okTaskEditorBtnCaption}
|
||||
cancelBtnCaption={cancelTaskEditorBtnCaption}
|
||||
/>
|
||||
) : null}
|
||||
<div style={{ height, display: state.noData ? "none" : "" }}>
|
||||
<svg id="__gantt__" width="100%"></svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диаграмма Ганта
|
||||
P8PGantt.propTypes = {
|
||||
height: PropTypes.string.isRequired,
|
||||
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,
|
||||
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, P8PGantt };
|
@ -9,11 +9,12 @@
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import { deepCopyObject } from "./core/utils"; //Вспомогательные процедуры и функции
|
||||
import { TITLES, BUTTONS, TEXTS, INPUTS } from "../app.text"; //Текстовые ресурсы и константы
|
||||
import { TITLES, BUTTONS, TEXTS, CAPTIONS } from "../app.text"; //Текстовые ресурсы и константы
|
||||
import { P8PPanelsMenuGrid, P8P_PANELS_MENU_PANEL_SHAPE } from "./components/p8p_data_grid"; //Меню панелей
|
||||
import { P8PAppWorkspace } from "./components/p8p_app_workspace"; //Рабочее пространство
|
||||
import { P8PTable, P8P_TABLE_DATA_TYPE, P8P_TABLE_SIZE, P8P_TABLE_FILTER_SHAPE } from "./components/p8p_data_grid"; //Таблица данных
|
||||
import { P8PDataGrid, P8P_DATA_GRID_DATA_TYPE, P8P_DATA_GRID_SIZE, P8P_DATA_GRID_FILTER_SHAPE } from "./components/p8p_data_grid"; //Таблица данных
|
||||
import { P8PGantt, P8P_GANTT_TASK_SHAPE, P8P_GANTT_TASK_ATTRIBUTE_SHAPE, P8P_GANTT_TASK_COLOR_SHAPE } from "./components/p8p_gantt"; //Диаграмма Ганта
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
@ -36,9 +37,9 @@ const P8P_TABLE_CONFIG_PROPS = {
|
||||
orderAscMenuItemCaption: BUTTONS.ORDER_ASC,
|
||||
orderDescMenuItemCaption: BUTTONS.ORDER_DESC,
|
||||
filterMenuItemCaption: BUTTONS.FILTER,
|
||||
valueFilterCaption: INPUTS.VALUE,
|
||||
valueFromFilterCaption: INPUTS.VALUE_FROM,
|
||||
valueToFilterCaption: INPUTS.VALUE_TO,
|
||||
valueFilterCaption: CAPTIONS.VALUE,
|
||||
valueFromFilterCaption: CAPTIONS.VALUE_FROM,
|
||||
valueToFilterCaption: CAPTIONS.VALUE_TO,
|
||||
okFilterBtnCaption: BUTTONS.OK,
|
||||
clearFilterBtnCaption: BUTTONS.CLEAR,
|
||||
cancelFilterBtnCaption: BUTTONS.CANCEL,
|
||||
@ -51,9 +52,9 @@ const P8P_DATA_GRID_CONFIG_PROPS = {
|
||||
orderAscMenuItemCaption: BUTTONS.ORDER_ASC,
|
||||
orderDescMenuItemCaption: BUTTONS.ORDER_DESC,
|
||||
filterMenuItemCaption: BUTTONS.FILTER,
|
||||
valueFilterCaption: INPUTS.VALUE,
|
||||
valueFromFilterCaption: INPUTS.VALUE_FROM,
|
||||
valueToFilterCaption: INPUTS.VALUE_TO,
|
||||
valueFilterCaption: CAPTIONS.VALUE,
|
||||
valueFromFilterCaption: CAPTIONS.VALUE_FROM,
|
||||
valueToFilterCaption: CAPTIONS.VALUE_TO,
|
||||
okFilterBtnCaption: BUTTONS.OK,
|
||||
clearFilterBtnCaption: BUTTONS.CLEAR,
|
||||
cancelFilterBtnCaption: BUTTONS.CANCEL,
|
||||
@ -62,6 +63,19 @@ const P8P_DATA_GRID_CONFIG_PROPS = {
|
||||
objectsCopier: deepCopyObject
|
||||
};
|
||||
|
||||
//Конфигурируемые свойства "Диаграммы Ганта" (P8PGantt)
|
||||
const P8P_GANTT_CONFIG_PROPS = {
|
||||
noDataFoundText: TEXTS.NO_DATA_FOUND,
|
||||
numbTaskEditorCaption: CAPTIONS.NUMB,
|
||||
nameTaskEditorCaption: CAPTIONS.NAME,
|
||||
startTaskEditorCaption: CAPTIONS.START,
|
||||
endTaskEditorCaption: CAPTIONS.END,
|
||||
progressTaskEditorCaption: CAPTIONS.PROGRESS,
|
||||
legendTaskEditorCaption: CAPTIONS.LEGEND,
|
||||
okTaskEditorBtnCaption: BUTTONS.OK,
|
||||
cancelTaskEditorBtnCaption: BUTTONS.CANCEL
|
||||
};
|
||||
|
||||
//-----------------------
|
||||
//Вспомогательные функции
|
||||
//-----------------------
|
||||
@ -75,6 +89,7 @@ const addConfigChildProps = children =>
|
||||
if (child.type.name === "P8PPanelsMenuGrid") configProps = P8P_PANELS_MENU_GRID_CONFIG_PROPS;
|
||||
if (child.type.name === "P8PTable") configProps = P8P_TABLE_CONFIG_PROPS;
|
||||
if (child.type.name === "P8PDataGrid") configProps = P8P_DATA_GRID_CONFIG_PROPS;
|
||||
if (child.type.name === "P8PGantt") configProps = P8P_GANTT_CONFIG_PROPS;
|
||||
return React.createElement(child.type, { ...configProps, ...restProps }, addConfigChildProps(children));
|
||||
});
|
||||
|
||||
@ -89,11 +104,14 @@ const P8PPanelsMenuGridConfigWrapped = (props = {}) => <P8PPanelsMenuGrid {...P8
|
||||
const P8PAppWorkspaceConfigWrapped = (props = {}) => <P8PAppWorkspace {...P8P_APP_WORKSPACE_CONFIG_PROPS} {...props} />;
|
||||
|
||||
//Обёртка для компонента "Таблица" (P8PTable)
|
||||
const P8PTableConfigWrapped = (props = {}) => <P8PTable {...P8P_DATA_GRID_CONFIG_PROPS} {...props} />;
|
||||
const P8PTableConfigWrapped = (props = {}) => <P8PTable {...P8P_TABLE_CONFIG_PROPS} {...props} />;
|
||||
|
||||
//Обёртка для компонента "Таблица данных" (P8PDataGrid)
|
||||
const P8PDataGridConfigWrapped = (props = {}) => <P8PDataGrid {...P8P_DATA_GRID_CONFIG_PROPS} {...props} />;
|
||||
|
||||
//Обёртка для компонента "Диаграмма Ганта" (P8PGantt)
|
||||
const P8PGanttConfigWrapped = (props = {}) => <P8PGantt {...P8P_GANTT_CONFIG_PROPS} {...props} />;
|
||||
|
||||
//Универсальный элемент-обёртка в параметры конфигурации
|
||||
const ConfigWrapper = ({ children }) => addConfigChildProps(children);
|
||||
|
||||
@ -113,9 +131,14 @@ export {
|
||||
P8P_DATA_GRID_DATA_TYPE,
|
||||
P8P_DATA_GRID_SIZE,
|
||||
P8P_DATA_GRID_FILTER_SHAPE,
|
||||
P8P_GANTT_CONFIG_PROPS,
|
||||
P8P_GANTT_TASK_SHAPE,
|
||||
P8P_GANTT_TASK_ATTRIBUTE_SHAPE,
|
||||
P8P_GANTT_TASK_COLOR_SHAPE,
|
||||
P8PPanelsMenuGridConfigWrapped,
|
||||
P8PAppWorkspaceConfigWrapped,
|
||||
P8PTableConfigWrapped,
|
||||
P8PDataGridConfigWrapped,
|
||||
P8PGanttConfigWrapped,
|
||||
ConfigWrapper
|
||||
};
|
||||
|
@ -3,13 +3,18 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Парус 8 - Панели мониторинга</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Parus 8 monitoring WEB application" />
|
||||
<meta name="author" content="CITK Parus" />
|
||||
<!-- P8 Panels -->
|
||||
<link rel="stylesheet" href="./css/p8-panels.css" />
|
||||
<!-- MUI -->
|
||||
<link rel="stylesheet" href="./css/fonts-roboto.css" />
|
||||
<link rel="stylesheet" href="./css/fonts-material-icons.css" />
|
||||
<title>Парус 8 - Панели мониторинга</title>
|
||||
<!-- Gantt -->
|
||||
<script src="./libs/frappe-gantt/frappe-gantt.js"></script>
|
||||
<link rel="stylesheet" href="./libs/frappe-gantt/frappe-gantt.css" />
|
||||
</head>
|
||||
<body style="display: block; margin: 0px">
|
||||
<div id="app-content"></div>
|
||||
|
134
libs/frappe-gantt/frappe-gantt.css
Normal file
134
libs/frappe-gantt/frappe-gantt.css
Normal file
@ -0,0 +1,134 @@
|
||||
.gantt .grid-background {
|
||||
fill: none;
|
||||
}
|
||||
.gantt .grid-header {
|
||||
fill: #ffffff;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
.gantt .grid-row {
|
||||
fill: #ffffff;
|
||||
}
|
||||
.gantt .grid-row:nth-child(even) {
|
||||
fill: #f5f5f5;
|
||||
}
|
||||
.gantt .row-line {
|
||||
stroke: #ebeff2;
|
||||
}
|
||||
.gantt .tick {
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 0.2;
|
||||
}
|
||||
.gantt .tick.thick {
|
||||
stroke-width: 0.4;
|
||||
}
|
||||
.gantt .today-highlight {
|
||||
fill: #fcf8e3;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.gantt .arrow {
|
||||
fill: none;
|
||||
stroke: #666;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
.gantt .bar {
|
||||
fill: #b8c2cc;
|
||||
stroke: #8D99A6;
|
||||
stroke-width: 0;
|
||||
transition: stroke-width 0.3s ease;
|
||||
user-select: none;
|
||||
}
|
||||
.gantt .bar-progress {
|
||||
fill: #a3a3ff;
|
||||
}
|
||||
.gantt .bar-invalid {
|
||||
fill: transparent;
|
||||
stroke: #8D99A6;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 5;
|
||||
}
|
||||
.gantt .bar-invalid ~ .bar-label {
|
||||
fill: #555;
|
||||
}
|
||||
.gantt .bar-label {
|
||||
fill: #fff;
|
||||
dominant-baseline: central;
|
||||
text-anchor: middle;
|
||||
font-size: 12px;
|
||||
font-weight: lighter;
|
||||
}
|
||||
.gantt .bar-label.big {
|
||||
fill: #555;
|
||||
text-anchor: start;
|
||||
}
|
||||
.gantt .handle {
|
||||
fill: #ddd;
|
||||
cursor: ew-resize;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.gantt .bar-wrapper {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.gantt .bar-wrapper:hover .bar {
|
||||
fill: #a9b5c1;
|
||||
}
|
||||
.gantt .bar-wrapper:hover .bar-progress {
|
||||
fill: #8a8aff;
|
||||
}
|
||||
.gantt .bar-wrapper:hover .handle {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
.gantt .bar-wrapper.active .bar {
|
||||
fill: #a9b5c1;
|
||||
}
|
||||
.gantt .bar-wrapper.active .bar-progress {
|
||||
fill: #8a8aff;
|
||||
}
|
||||
.gantt .lower-text, .gantt .upper-text {
|
||||
font-size: 12px;
|
||||
text-anchor: middle;
|
||||
}
|
||||
.gantt .upper-text {
|
||||
fill: #555;
|
||||
}
|
||||
.gantt .lower-text {
|
||||
fill: #333;
|
||||
}
|
||||
.gantt .hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gantt-container {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
height: 100%;
|
||||
}
|
||||
.gantt-container .popup-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 0;
|
||||
color: #959da5;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.gantt-container .popup-wrapper .title {
|
||||
border-bottom: 3px solid #a3a3ff;
|
||||
padding: 10px;
|
||||
}
|
||||
.gantt-container .popup-wrapper .subtitle {
|
||||
padding: 10px;
|
||||
color: #dfe2e5;
|
||||
}
|
||||
.gantt-container .popup-wrapper .pointer {
|
||||
position: absolute;
|
||||
height: 5px;
|
||||
margin: 0 0 0 -5px;
|
||||
border: 5px solid transparent;
|
||||
border-top-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
1919
libs/frappe-gantt/frappe-gantt.js
Normal file
1919
libs/frappe-gantt/frappe-gantt.js
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user