Compare commits

..

11 Commits

141 changed files with 12372 additions and 21038 deletions

1016
README.md

File diff suppressed because it is too large Load Diff

View File

@ -3,49 +3,10 @@
Типовые стили
*/
//---------------------
//Подключение библиотек
//---------------------
import { STATE } from "./app.text"; //Текстовые ресурсы и константы
import { red, green, orange, grey } from "@mui/material/colors";
//----------------
//Интерфейс модуля
//----------------
//Цвета
export const APP_COLORS = {
[STATE.UNDEFINED]: {
color: "#dcdcdca0",
contrColor: "black"
},
[STATE.INFO]: {
color: "white",
contrColor: "black"
},
[STATE.OK]: {
color: green[200],
contrColor: green[900]
},
[STATE.ERR]: {
color: red[200],
contrColor: red[900]
},
[STATE.WARN]: {
color: orange[200],
contrColor: orange[900]
},
HOVER: {
color: grey[200],
contrColor: grey[900]
},
ACTIVE: {
color: grey[400],
contrColor: grey[900]
}
};
//Стили
export const APP_STYLES = {
SCROLL: {

View File

@ -18,8 +18,7 @@ export const TITLES = {
//Текст
export const TEXTS = {
LOADING: "Ожидайте...", //Ожидание завершения процесса
NO_DATA_FOUND: "Данных не найдено", //Отсутствие данных
NO_DATA_FOUND_SHORT: "Н.Д." //Отсутствие данных (кратко)
NO_DATA_FOUND: "Данных не найдено" //Отсутствие данных
};
//Текст кнопок
@ -30,15 +29,11 @@ export const BUTTONS = {
OK: "ОК", //Ок
CANCEL: "Отмена", //Отмена
CLOSE: "Закрыть", //Сокрытие
DETAIL: "Подробнее", //Отображение подробностей/детализации
HIDE: "Скрыть", //Скрытие информации
CLEAR: "Очистить", //Очистка
ORDER_ASC: "По возрастанию", //Сортировка по возрастанию
ORDER_DESC: "По убыванию", //Сортировка по убыванию
FILTER: "Фильтр", //Фильтрация
MORE: "Ещё", //Догрузка данных
APPLY: "Применить", //Сохранение без закрытия интерфейса ввода
SAVE: "Сохранить" //Сохранение
MORE: "Ещё" //Догрузка данных
};
//Метки атрибутов, сопроводительные надписи
@ -58,7 +53,6 @@ export const CAPTIONS = {
export const ERRORS = {
UNDER_CONSTRUCTION: "Панель в разработке",
P8O_API_UNAVAILABLE: '"ПАРУС 8 Онлайн" недоступен',
P8O_API_UNSUPPORTED: 'Функция "ПАРУС 8 Онлайн" не поддерживается',
DEFAULT: "Неожиданная ошибка"
};
@ -66,12 +60,3 @@ export const ERRORS = {
export const ERRORS_HTTP = {
404: "Адрес не найден"
};
//Типовые статусы
export const STATE = {
UNDEFINED: "UNDEFINED",
INFO: "INFORMATION",
OK: "OK",
ERR: "ERR",
WARN: "WARN"
};

View File

@ -86,9 +86,6 @@ const Workspace = ({ panels = [], selectedPanel, children } = {}) => {
//Подключение к контексту навигации
const { navigateRoot, navigatePanel } = useContext(NavigationCtx);
//Подключение к контексту приложения
const { appState } = useContext(ApplicationСtx);
//Отработка действия навигации домой
const handleHomeNavigate = () => navigateRoot();
@ -101,7 +98,6 @@ const Workspace = ({ panels = [], selectedPanel, children } = {}) => {
{...P8P_APP_WORKSPACE_CONFIG_PROPS}
panels={panels}
selectedPanel={selectedPanel}
caption={appState.appBarTitle}
onHomeNavigate={handleHomeNavigate}
onItemNavigate={handleItemNavigate}
>

View File

@ -7,7 +7,7 @@
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import Dialog from "@mui/material/Dialog"; //базовый класс диалога Material UI
import DialogTitle from "@mui/material/DialogTitle"; //Заголовок диалога
@ -18,8 +18,6 @@ import Typography from "@mui/material/Typography"; //Текст
import Button from "@mui/material/Button"; //Кнопки
import Container from "@mui/material/Container"; //Контейнер
import Box from "@mui/material/Box"; //Обёртка
import { BUTTONS, STATE } from "../../app.text"; //Типовые текстовые ресурсы и константы
import { APP_COLORS } from "../../app.styles"; //Типовые стили
//---------
//Константы
@ -27,9 +25,9 @@ import { APP_COLORS } from "../../app.styles"; //Типовые стили
//Варианты исполнения
const P8P_APP_MESSAGE_VARIANT = {
INFO: STATE.INFO,
WARN: STATE.WARN,
ERR: STATE.ERR
INFO: "information",
WARN: "warning",
ERR: "error"
};
//Стили
@ -38,35 +36,28 @@ const STYLES = {
wordBreak: "break-word"
},
INFO: {
titleText: {
color: APP_COLORS[STATE.INFO].contrColor
},
bodyText: {
color: APP_COLORS[STATE.INFO].contrColor
}
titleText: {},
bodyText: {}
},
WARN: {
titleText: {
color: APP_COLORS[STATE.WARN].contrColor
color: "orange"
},
bodyText: {
color: APP_COLORS[STATE.WARN].contrColor
color: "orange"
}
},
ERR: {
titleText: {
color: APP_COLORS[STATE.ERR].contrColor
color: "red"
},
bodyText: {
color: APP_COLORS[STATE.ERR].contrColor
color: "red"
}
},
INLINE_MESSAGE: {
with: "100%",
textAlign: "center"
},
FULL_ERROR_TEXT_BUTTON: {
color: APP_COLORS[STATE.WARN].contrColor
}
};
@ -75,25 +66,7 @@ const STYLES = {
//-----------
//Сообщение
const P8PAppMessage = ({
variant,
title,
titleText,
cancelBtn,
onCancel,
cancelBtnCaption,
okBtn,
onOk,
okBtnCaption,
open,
text,
fullErrorText,
showErrMoreCaption,
hideErrMoreCaption
}) => {
//Состояние подробной информации об ошибке
const [showFullErrorText, setShowFullErrorText] = useState(false);
const P8PAppMessage = ({ variant, title, titleText, cancelBtn, onCancel, cancelBtnCaption, okBtn, onOk, okBtnCaption, open, text }) => {
//Подбор стиля и ресурсов
let style = STYLES.INFO;
switch (variant) {
@ -113,7 +86,12 @@ const P8PAppMessage = ({
//Заголовок
let titlePart;
if (title && titleText) titlePart = <DialogTitle style={{ ...style.DEFAULT, ...style.titleText }}>{titleText}</DialogTitle>;
if (title && titleText)
titlePart = (
<DialogTitle id="message-dialog-title" style={{ ...style.DEFAULT, ...style.titleText }}>
{titleText}
</DialogTitle>
);
//Кнопка Отмена
let cancelBtnPart;
@ -124,26 +102,16 @@ const P8PAppMessage = ({
let okBtnPart;
if (okBtn && okBtnCaption)
okBtnPart = (
<Button onClick={() => (onOk ? onOk() : null)} autoFocus>
<Button onClick={() => (onOk ? onOk() : null)} color="primary" autoFocus>
{okBtnCaption}
</Button>
);
//Кнопка Подробнее
let fullErrorTextBtn;
if (fullErrorText && showErrMoreCaption && hideErrMoreCaption && variant === P8P_APP_MESSAGE_VARIANT.ERR)
fullErrorTextBtn = (
<Button onClick={() => setShowFullErrorText(!showFullErrorText)} sx={STYLES.FULL_ERROR_TEXT_BUTTON} autoFocus>
{!showFullErrorText ? showErrMoreCaption : hideErrMoreCaption}
</Button>
);
//Все действия
let actionsPart;
if (cancelBtnPart || okBtnPart)
actionsPart = (
<DialogActions>
{fullErrorTextBtn}
{okBtnPart}
{cancelBtnPart}
</DialogActions>
@ -151,10 +119,17 @@ const P8PAppMessage = ({
//Генерация содержимого
return (
<Dialog open={open || false} onClose={() => (onCancel ? onCancel() : null)}>
<Dialog
open={open || false}
aria-labelledby="message-dialog-title"
aria-describedby="message-dialog-description"
onClose={() => (onCancel ? onCancel() : null)}
>
{titlePart}
<DialogContent>
<DialogContentText style={style.bodyText}>{!showFullErrorText ? text : fullErrorText}</DialogContentText>
<DialogContentText id="message-dialog-description" style={style.bodyText}>
{text}
</DialogContentText>
</DialogContent>
{actionsPart}
</Dialog>
@ -173,10 +148,7 @@ P8PAppMessage.propTypes = {
onOk: PropTypes.func,
okBtnCaption: PropTypes.string,
open: PropTypes.bool,
text: PropTypes.string,
fullErrorText: PropTypes.string,
showErrMoreCaption: PropTypes.string,
hideErrMoreCaption: PropTypes.string
text: PropTypes.string
};
//Встроенное сообщение
@ -186,19 +158,13 @@ const P8PAppInlineMessage = ({ variant, text, okBtn, onOk, okBtnCaption }) => {
<Container style={STYLES.INLINE_MESSAGE}>
<Box p={1}>
<Typography
color={
variant === P8P_APP_MESSAGE_VARIANT.ERR
? APP_COLORS[STATE.ERR].contrColor
: variant === P8P_APP_MESSAGE_VARIANT.WARN
? APP_COLORS[STATE.WARN].contrColor
: APP_COLORS[STATE.INFO].contrColor
}
color={variant === P8P_APP_MESSAGE_VARIANT.ERR ? "error" : variant === P8P_APP_MESSAGE_VARIANT.WARN ? "primary" : "textSecondary"}
>
{text}
</Typography>
{okBtn && okBtnCaption ? (
<Box pt={1}>
<Button onClick={() => (onOk ? onOk() : null)} autoFocus>
<Button onClick={() => (onOk ? onOk() : null)} color="primary" autoFocus>
{okBtnCaption}
</Button>
</Box>
@ -250,28 +216,6 @@ const P8PAppInlineWarn = props => buildVariantInlineMessage(props, P8P_APP_MESSA
//Встраиваемое сообщение информации
const P8PAppInlineInfo = props => buildVariantInlineMessage(props, P8P_APP_MESSAGE_VARIANT.INFO);
//Диалог подсказки
const P8PHintDialog = ({ title, hint, onOk }) => {
return (
<Dialog open={true} onClose={() => (onOk ? onOk() : null)}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<div dangerouslySetInnerHTML={{ __html: hint }}></div>
</DialogContent>
<DialogActions>
<Button onClick={() => (onOk ? onOk() : null)}>{BUTTONS.OK}</Button>
</DialogActions>
</Dialog>
);
};
//Контроль свойств - Диалог подсказки
P8PHintDialog.propTypes = {
title: PropTypes.string.isRequired,
hint: PropTypes.string.isRequired,
onOk: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
@ -285,6 +229,5 @@ export {
P8PAppInlineMessage,
P8PAppInlineError,
P8PAppInlineWarn,
P8PAppInlineInfo,
P8PHintDialog
P8PAppInlineInfo
};

View File

@ -23,8 +23,7 @@ import {
ListItemIcon,
ListItemText
} from "@mui/material"; //Интерфейсные компоненты
import { P8PPanelsMenuDrawer, P8P_PANELS_MENU_PANEL_SHAPE } from "./p8p_panels_menu"; //Меню
import { APP_STYLES } from "../../app.styles"; //Типовые стили
import { P8PPanelsMenuDrawer, P8P_PANELS_MENU_PANEL_SHAPE } from "./p8p_panels_menu";
//---------
//Константы
@ -35,7 +34,6 @@ const APP_BAR_HEIGHT = "64px";
//Стили
const STYLES = {
DRAWER: { [`& .MuiDrawer-paper`]: { ...APP_STYLES.SCROLL } },
ROOT_BOX: { display: "flex" },
APP_BAR: { position: "fixed" },
APP_BAR_BUTTON: { mr: 2 },
@ -47,7 +45,7 @@ const STYLES = {
//-----------
//Рабочее пространство
const P8PAppWorkspace = ({ children, panels = [], selectedPanel, caption, closeCaption, homeCaption, onHomeNavigate, onItemNavigate } = {}) => {
const P8PAppWorkspace = ({ children, panels = [], selectedPanel, closeCaption, homeCaption, onHomeNavigate, onItemNavigate } = {}) => {
//Собственное состояния
const [open, setOpen] = useState(false);
@ -86,11 +84,11 @@ const P8PAppWorkspace = ({ children, panels = [], selectedPanel, caption, closeC
<Icon>{open ? "chevron_left" : "menu"}</Icon>
</IconButton>
<Typography variant="h6" noWrap component="div">
{caption || selectedPanel?.caption}
{selectedPanel?.caption}
</Typography>
</Toolbar>
</AppBar>
<Drawer anchor="left" open={open} onClose={handleDrawerClose} sx={STYLES.DRAWER}>
<Drawer anchor="left" open={open} onClose={handleDrawerClose}>
<List>
<ListItemButton onClick={handleDrawerClose}>
<ListItemIcon>
@ -120,7 +118,6 @@ P8PAppWorkspace.propTypes = {
children: PropTypes.element,
panels: PropTypes.arrayOf(P8P_PANELS_MENU_PANEL_SHAPE).isRequired,
selectedPanel: P8P_PANELS_MENU_PANEL_SHAPE,
caption: PropTypes.string,
closeCaption: PropTypes.string.isRequired,
homeCaption: PropTypes.string.isRequired,
onHomeNavigate: PropTypes.func,

View File

@ -7,7 +7,7 @@
//Подключение библиотек
//---------------------
import React, { useCallback, useEffect, useRef } from "react"; //Классы React
import React, { useEffect, useRef } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import Chart from "chart.js/auto"; //Диаграммы и графики
@ -37,26 +37,23 @@ const P8P_CHART_DATASET_SHAPE = PropTypes.shape({
//-----------
//График
const P8PChart = ({ type, title, legendPosition, options = {}, labels = [], datasets = [], onClick, style }) => {
const P8PChart = ({ type, title, legendPosition, options, labels, datasets, onClick, style }) => {
//Ссылки на DOM
const chartCanvasRef = useRef(null);
const chartRef = useRef(null);
//Обработка нажатия на элемент графика
const handleClick = useCallback(
e => {
const bar = chartRef.current.getElementsAtEventForMode(e, "nearest", { intersect: true }, true)[0];
if (onClick && bar)
onClick({
datasetIndex: bar.datasetIndex,
itemIndex: bar.index,
item: chartRef.current.data.datasets[bar.datasetIndex].items
? chartRef.current.data.datasets[bar.datasetIndex].items[bar.index]
: null
});
},
[onClick]
);
const handleClick = e => {
const bar = chartRef.current.getElementsAtEventForMode(e, "nearest", { intersect: true }, true)[0];
if (onClick && bar)
onClick({
datasetIndex: bar.datasetIndex,
itemIndex: bar.index,
item: chartRef.current.data.datasets[bar.datasetIndex].items
? chartRef.current.data.datasets[bar.datasetIndex].items[bar.index]
: null
});
};
//При подключении к старнице
useEffect(() => {
@ -92,10 +89,9 @@ const P8PChart = ({ type, title, legendPosition, options = {}, labels = [], data
if (chartRef.current) {
chartRef.current.data.labels = [...labels];
chartRef.current.data.datasets = [...datasets];
chartRef.current.options.onClick = handleClick;
chartRef.current.update();
}
}, [datasets, labels, handleClick]);
}, [datasets, labels]);
//Генерация содержимого
return (
@ -111,7 +107,7 @@ P8PChart.propTypes = {
title: PropTypes.string,
legendPosition: PropTypes.string,
options: PropTypes.object,
labels: PropTypes.arrayOf(PropTypes.string),
labels: PropTypes.arrayOf(PropTypes.string).isRequired,
datasets: PropTypes.arrayOf(P8P_CHART_DATASET_SHAPE),
onClick: PropTypes.func,
style: PropTypes.object

View File

@ -1,819 +0,0 @@
/*
Парус 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 (
<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 };

View File

@ -36,16 +36,15 @@ const P8P_DATA_GRID_FILTERS_HEIGHT = P8P_TABLE_FILTERS_HEIGHT;
//Таблица данных
const P8PDataGrid = ({
style = {},
columnsDef = [],
columnsDef,
filtersInitial,
groups = [],
rows = [],
groups,
rows,
size,
fixedHeader = false,
fixedColumns = 0,
morePages = false,
reloading = false,
reloading,
expandable,
orderAscMenuItemCaption,
orderDescMenuItemCaption,
@ -115,7 +114,6 @@ const P8PDataGrid = ({
//Генерация содержимого
return (
<P8PTable
style={style}
columnsDef={columnsDef}
groups={groups}
rows={rows}
@ -156,16 +154,15 @@ const P8PDataGrid = ({
//Контроль свойств - Таблица данных
P8PDataGrid.propTypes = {
style: PropTypes.object,
columnsDef: PropTypes.array,
columnsDef: PropTypes.array.isRequired,
filtersInitial: PropTypes.arrayOf(P8P_DATA_GRID_FILTER_SHAPE),
groups: PropTypes.array,
rows: PropTypes.array,
rows: PropTypes.array.isRequired,
size: PropTypes.string,
fixedHeader: PropTypes.bool,
fixedColumns: PropTypes.number,
morePages: PropTypes.bool,
reloading: PropTypes.bool,
reloading: PropTypes.bool.isRequired,
expandable: PropTypes.bool,
orderAscMenuItemCaption: PropTypes.string.isRequired,
orderDescMenuItemCaption: PropTypes.string.isRequired,

View File

@ -27,7 +27,7 @@ const STYLES = {
//-----------
//Полноэкранный диалог
const P8PFullScreenDialog = ({ title, onClose, contentProps, children }) => {
const P8PFullScreenDialog = ({ title, onClose, children }) => {
const handleClose = () => {
onClose ? onClose() : null;
};
@ -46,7 +46,7 @@ const P8PFullScreenDialog = ({ title, onClose, contentProps, children }) => {
</Toolbar>
</AppBar>
</DialogTitle>
<DialogContent {...(contentProps ? contentProps : {})}>{children}</DialogContent>
<DialogContent>{children}</DialogContent>
</Dialog>
);
};
@ -55,8 +55,7 @@ const P8PFullScreenDialog = ({ title, onClose, contentProps, children }) => {
P8PFullScreenDialog.propTypes = {
title: PropTypes.string.isRequired,
onClose: PropTypes.func,
children: PropTypes.element,
contentProps: PropTypes.object
children: PropTypes.element
};
//----------------

View File

@ -33,7 +33,7 @@ import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемо
//---------
//Уровни масштаба
const P8P_GANTT_ZOOM = [0, 1, 2, 3, 4, 5];
const P8P_GANTT_ZOOM = [0, 1, 2, 3, 4];
//Уровни масштаба (строковые наименования в терминах библиотеки)
const P8P_GANTT_ZOOM_VIEW_MODES = {
@ -41,8 +41,7 @@ const P8P_GANTT_ZOOM_VIEW_MODES = {
1: "Half Day",
2: "Day",
3: "Week",
4: "Month",
5: "Year"
4: "Month"
};
//Структура задачи
@ -139,7 +138,6 @@ const P8PGanttTaskEditor = ({
onCancel,
taskAttributeRenderer,
taskDialogRenderer,
taskDialogProps,
numbCaption,
nameCaption,
startCaption,
@ -187,7 +185,7 @@ const P8PGanttTaskEditor = ({
//Генерация содержимого
return (
<Dialog open onClose={handleCancel} {...(taskDialogProps ? taskDialogProps : {})}>
<Dialog open onClose={handleCancel}>
{taskDialogRenderer ? (
taskDialogRenderer({ task, taskAttributes, taskColors, close: handleCancel })
) : (
@ -316,7 +314,6 @@ P8PGanttTaskEditor.propTypes = {
onCancel: PropTypes.func,
taskAttributeRenderer: PropTypes.func,
taskDialogRenderer: PropTypes.func,
taskDialogProps: PropTypes.object,
numbCaption: PropTypes.string.isRequired,
nameCaption: PropTypes.string.isRequired,
startCaption: PropTypes.string.isRequired,
@ -349,7 +346,6 @@ const P8PGantt = ({
onTaskProgressChange,
taskAttributeRenderer,
taskDialogRenderer,
taskDialogProps,
noDataFoundText,
numbTaskEditorCaption,
nameTaskEditorCaption,
@ -470,7 +466,6 @@ const P8PGantt = ({
onCancel={handleTaskEditorCancel}
taskAttributeRenderer={taskAttributeRenderer}
taskDialogRenderer={taskDialogRenderer}
taskDialogProps={taskDialogProps}
numbCaption={numbTaskEditorCaption}
nameCaption={nameTaskEditorCaption}
startCaption={startTaskEditorCaption}
@ -506,7 +501,6 @@ P8PGantt.propTypes = {
onTaskProgressChange: PropTypes.func,
taskAttributeRenderer: PropTypes.func,
taskDialogRenderer: PropTypes.func,
taskDialogProps: PropTypes.object,
noDataFoundText: PropTypes.string.isRequired,
numbTaskEditorCaption: PropTypes.string.isRequired,
nameTaskEditorCaption: PropTypes.string.isRequired,

View File

@ -1,186 +0,0 @@
/*
Парус 8 - Панели мониторинга
Компонент: Индикатор
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { IconButton, Icon, Typography, Paper, Stack } from "@mui/material"; //Интерфейсные компоненты MUI
import { P8PHintDialog } from "./p8p_app_message"; //Диалог подсказки
import { TEXTS, STATE } from "../../app.text"; //Типовые текстовые ресурсы и константы
import { APP_COLORS } from "../../app.styles"; //Типовые стили
//---------
//Константы
//---------
//Варианты исполнения
const P8P_INDICATOR_VARIANT = {
ELEVATION: "elevation",
OUTLINED: "outlined"
};
//Состояния
const P8P_INDICATOR_STATE = {
UNDEFINED: STATE.UNDEFINED,
OK: STATE.OK,
WARN: STATE.WARN,
ERR: STATE.ERR
};
//Цвета заливки
const BG_COLOR = {
[STATE.OK]: APP_COLORS[STATE.OK].color,
[STATE.ERR]: APP_COLORS[STATE.ERR].color,
[STATE.WARN]: APP_COLORS[STATE.WARN].color
};
//Цвета текста и иконок
const COLOR = {
[STATE.OK]: APP_COLORS[STATE.OK].contrColor,
[STATE.ERR]: APP_COLORS[STATE.ERR].contrColor,
[STATE.WARN]: APP_COLORS[STATE.WARN].contrColor
};
//Стили
const STYLES = {
CONTAINER: (state, clickable, userColor, userBackgroundColor) => ({
padding: "10px",
width: "100%",
height: "100%",
overflow: "hidden",
...getBackgroundColor(state, userBackgroundColor),
...getColor(state, userColor),
display: "flex",
flexDirection: "column",
justifyContent: "center",
...(clickable
? {
cursor: "pointer",
"&:hover": { backgroundColor: APP_COLORS.HOVER.color },
"&:active": { backgroundColor: APP_COLORS.ACTIVE.color }
}
: {})
}),
ICON: (state, userColor) => ({ fontSize: "50px", ...getColor(state, userColor) }),
HINT_ICON: (state, userColor) => ({ fontSize: "1rem", ...getColor(state, userColor) }),
VALUE_CAPTION_STACK: { containerType: "inline-size", width: "100%", overflow: "hidden" },
CAPTION_TYPOGRAPHY: { width: "99cqw" }
};
//-----------------------
//Вспомогательные функции
//-----------------------
//Подбор цвета заливки
const getBackgroundColor = (state, userColor) =>
userColor ? { backgroundColor: userColor } : BG_COLOR[state] ? { backgroundColor: BG_COLOR[state] } : {};
//Подбор цвета текста
const getColor = (state, userColor) => (userColor ? { color: userColor } : COLOR[state] ? { color: COLOR[state] } : {});
//-----------
//Тело модуля
//-----------
//Индикатор
const P8PIndicator = ({
caption,
value,
icon = null,
state = STATE.UNDEFINED,
square = false,
elevation = 3,
variant = P8P_INDICATOR_VARIANT.ELEVATION,
hint = null,
onClick = null,
backgroundColor = null,
color = null
} = {}) => {
//Собственное состояние - отображение окна подсказки
const [showHint, setShowHint] = useState(false);
//При нажатии на индикатор
const handleClick = () => (onClick && !showHint ? onClick() : null);
//При нажатии на кнопку получения подсказки
const handleHintClick = e => {
setShowHint(true);
e.stopPropagation();
};
//При нажатии на кнопку закрытия подсказки
const handleHintClose = () => setShowHint(false);
//Представление текста значения индикатора
const valueTextView = <Typography variant={"h4"}>{[undefined, null, ""].includes(value) ? TEXTS.NO_DATA_FOUND_SHORT : value}</Typography>;
//Представление текста подписи индикатора
const captionView = (
<Typography align={"left"} noWrap={true} sx={STYLES.CAPTION_TYPOGRAPHY} title={caption}>
{caption}
</Typography>
);
//Представление подписи индикатора
const valueView = hint ? (
<>
{showHint && <P8PHintDialog title={caption} hint={hint} onOk={handleHintClose} />}
<Stack direction={"row"} alignItems={"start"}>
{valueTextView}
<IconButton onClick={handleHintClick}>
<Icon sx={STYLES.HINT_ICON(state, color)}>help_outline</Icon>
</IconButton>
</Stack>
</>
) : (
valueTextView
);
//Флаг активности индикатора
const clickable = onClick ? true : false;
//Представление
return (
<Paper
elevation={variant === P8P_INDICATOR_VARIANT.ELEVATION ? elevation : 0}
sx={STYLES.CONTAINER(state, clickable, color, backgroundColor)}
square={square}
variant={variant}
onClick={handleClick}
>
<Stack direction={"row"} alignItems={"center"} justifyContent={"space-between"}>
<Stack direction={"column"} alignItems={"start"} pr={2} sx={STYLES.VALUE_CAPTION_STACK}>
{valueView}
{captionView}
</Stack>
{icon ? <Icon sx={STYLES.ICON(state, color)}>{icon}</Icon> : null}
</Stack>
</Paper>
);
};
//Контроль свойств - Индикатор
P8PIndicator.propTypes = {
caption: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
icon: PropTypes.string,
state: PropTypes.oneOf(Object.values(P8P_INDICATOR_STATE)),
square: PropTypes.bool,
elevation: PropTypes.number,
variant: PropTypes.oneOf(Object.values(P8P_INDICATOR_VARIANT)),
hint: PropTypes.string,
onClick: PropTypes.func,
backgroundColor: PropTypes.string,
color: PropTypes.string
};
//----------------
//Интерфейс модуля
//----------------
export { P8P_INDICATOR_VARIANT, P8P_INDICATOR_STATE, P8PIndicator };

View File

@ -62,15 +62,8 @@ const STYLES = {
GRID_PANEL_CARD_CONTENT_TITLE_ICON: { paddingTop: "4px" },
GRID_PANEL_CARD_ACTIONS: { marginTop: "auto", display: "flex", justifyContent: "flex-end", alignItems: "flex-start" },
DESKTOP_GROUP_HEADER: { fontWeight: "bold", fontFamily: "tahoma, arial, verdana, sans-serif!important", fontSize: "13px!important" },
DESKTOP_ITEM_BUTTON: {
fontSize: "12px",
textTransform: "none",
"&:hover": { backgroundColor: "#c3e1ff" },
width: "150px",
height: "90px",
flexDirection: "column",
justifyContent: "flex-start"
},
DESKTOP_ITEM_BUTTON: { fontSize: "12px", textTransform: "none", "&:hover": { backgroundColor: "#c3e1ff" }, maxWidth: "150px" },
DESKTOP_ITEM_STACK: { justifyContent: "center", alignItems: "center", fontSize: "12px" },
DESKTOP_ITEM_ICON: { width: "48px", height: "48px", fontSize: "48px" },
DESKTOP_ITEM_CATION: {
display: "-webkit-box",
@ -135,14 +128,7 @@ const getPanelsLinks = ({ variant, panels, selectedPanel, group, defaultGroupTyt
<Card sx={STYLES.GRID_PANEL_CARD}>
{panel.preview ? (
<CardMedia component="img" alt={panel.name} image={panel.preview} sx={STYLES.GRID_PANEL_CARD_MEDIA} />
) : (
<CardMedia
component="img"
alt={panel.name}
image={"./img/default_preview.png"}
sx={STYLES.GRID_PANEL_CARD_MEDIA}
/>
)}
) : null}
<CardContent>
<Stack gap={1} direction="row" sx={STYLES.GRID_PANEL_CARD_CONTENT_TITLE}>
{panel.icon ? <Icon sx={STYLES.GRID_PANEL_CARD_CONTENT_TITLE_ICON}>{panel.icon}</Icon> : null}
@ -179,10 +165,12 @@ const getPanelsLinks = ({ variant, panels, selectedPanel, group, defaultGroupTyt
sx={STYLES.DESKTOP_ITEM_BUTTON}
title={panel.caption}
>
<Icon sx={STYLES.DESKTOP_ITEM_ICON}>{panel.icon}</Icon>
<Typography sx={STYLES.DESKTOP_ITEM_CATION} variant="body1">
{panel.caption}
</Typography>
<Stack sx={STYLES.DESKTOP_ITEM_STACK}>
<Icon sx={STYLES.DESKTOP_ITEM_ICON}>{panel.icon}</Icon>
<Typography sx={STYLES.DESKTOP_ITEM_CATION} variant="body1">
{panel.caption}
</Typography>
</Stack>
</Button>
)
);
@ -242,12 +230,7 @@ const P8PPanelsMenuDesktop = ({ group, onItemNavigate, panels = [], defaultGroup
const panelsLinks = getPanelsLinks({ variant: P8P_PANELS_MENU_VARIANT.DESKTOP, panels, group, defaultGroupTytle, onItemNavigate });
//Генерация содержимого
return (
<Box p={2}>
{panelsLinks[0]}
<Stack direction="row">{panelsLinks.map((l, i) => (i > 0 ? l : null))}</Stack>
</Box>
);
return <Box p={2}>{panelsLinks}</Box>;
};
//Контроль свойств - Меню панелей - рабочий стол

View File

@ -34,7 +34,7 @@ import {
Link
} from "@mui/material"; //Интерфейсные компоненты
import { useTheme } from "@mui/material/styles"; //Взаимодействие со стилями MUI
import { P8PAppInlineError, P8PHintDialog } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
import { P8P_TABLE_AT, HEADER_INITIAL_STATE, hasValue, p8pTableReducer } from "./p8p_table_reducer"; //Редьюсер состояния
//---------
@ -89,7 +89,9 @@ const P8P_TABLE_FILTERS_HEIGHT = "48px";
//Стили
const STYLES = {
TABLE: {},
TABLE: {
with: "100%"
},
TABLE_HEAD_STICKY: {
position: "sticky",
top: 0,
@ -116,9 +118,7 @@ const STYLES = {
},
TABLE_CELL_EXPAND_CONTAINER: {
paddingBottom: 0,
paddingTop: 0,
paddingLeft: 0,
paddingRight: 0
paddingTop: 0
},
TABLE_CELL_GROUP_HEADER: {
backgroundColor: "lightgray"
@ -288,6 +288,28 @@ P8PTableColumnMenu.propTypes = {
onItemClick: PropTypes.func
};
//Диалог подсказки
const P8PTableColumnHintDialog = ({ columnDef, okBtnCaption, onOk }) => {
return (
<Dialog open={true} aria-labelledby="filter-dialog-title" aria-describedby="filter-dialog-description" onClose={() => (onOk ? onOk() : null)}>
<DialogTitle id="filter-dialog-title">{columnDef.caption}</DialogTitle>
<DialogContent>
<div dangerouslySetInnerHTML={{ __html: columnDef.hint }}></div>
</DialogContent>
<DialogActions>
<Button onClick={() => (onOk ? onOk() : null)}>{okBtnCaption}</Button>
</DialogActions>
</Dialog>
);
};
//Контроль свойств - Диалог подсказки
P8PTableColumnHintDialog.propTypes = {
columnDef: PropTypes.object.isRequired,
okBtnCaption: PropTypes.string.isRequired,
onOk: PropTypes.func
};
//Диалог фильтра
const P8PTableColumnFilterDialog = ({
columnDef,
@ -464,17 +486,16 @@ P8PTableFiltersChips.propTypes = {
//Таблица
const P8PTable = ({
style = {},
columnsDef = [],
groups = [],
rows = [],
columnsDef,
groups,
rows,
orders,
filters,
size,
fixedHeader = false,
fixedColumns = 0,
morePages = false,
reloading = false,
reloading,
expandable,
orderAscMenuItemCaption,
orderDescMenuItemCaption,
@ -510,9 +531,7 @@ const P8PTable = ({
const [expanded, setExpanded] = useState({});
//Собственное состояния - развёрнутые группы
const [expandedGroups, setExpandedGroups] = useState(
Array.isArray(groups) && groups.length > 0 ? Object.assign({}, ...groups.map(g => ({ [g.name]: g.expanded }))) : {}
);
const [expandedGroups, setExpandedGroups] = useState({});
//Собственное состояние - колонка с отображаемой подсказкой
const [displayHintColumn, setDisplayHintColumn] = useState(null);
@ -679,8 +698,10 @@ const P8PTable = ({
//Генерация содержимого
return (
<div style={{ ...(style || {}) }}>
{displayHintColumn ? <P8PHintDialog title={displayHintColumnDef.caption} hint={displayHintColumnDef.hint} onOk={handleHintOk} /> : null}
<div>
{displayHintColumn ? (
<P8PTableColumnHintDialog columnDef={displayHintColumnDef} okBtnCaption={okFilterBtnCaption} onOk={handleHintOk} />
) : null}
{filterColumn ? (
<P8PTableColumnFilterDialog
columnDef={filterColumnDef}
@ -875,7 +896,6 @@ const P8PTable = ({
//Контроль свойств - Таблица
P8PTable.propTypes = {
style: PropTypes.object,
columnsDef: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
@ -911,7 +931,7 @@ P8PTable.propTypes = {
fixedHeader: PropTypes.bool,
fixedColumns: PropTypes.number,
morePages: PropTypes.bool,
reloading: PropTypes.bool,
reloading: PropTypes.bool.isRequired,
expandable: PropTypes.bool,
orderAscMenuItemCaption: PropTypes.string.isRequired,
orderDescMenuItemCaption: PropTypes.string.isRequired,

View File

@ -15,7 +15,6 @@ import { P8PAppWorkspace } from "./components/p8p_app_workspace"; //Рабоче
import { P8PTable, P8P_TABLE_DATA_TYPE, P8P_TABLE_SIZE, P8P_TABLE_FILTER_SHAPE } from "./components/p8p_table"; //Таблица данных
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"; //Диаграмма Ганта
import { P8PCyclogram } from "./components/p8p_cyclogram"; //Циклограмма
//---------
//Константы
@ -77,14 +76,6 @@ const P8P_GANTT_CONFIG_PROPS = {
cancelTaskEditorBtnCaption: BUTTONS.CANCEL
};
//Конфигурируемые свойства "Циклограммы" (P8PCyclogram)
const P8P_CYCLOGRAM_CONFIG_PROPS = {
noDataFoundText: TEXTS.NO_DATA_FOUND,
nameTaskEditorCaption: CAPTIONS.NAME,
okTaskEditorBtnCaption: BUTTONS.OK,
cancelTaskEditorBtnCaption: BUTTONS.CANCEL
};
//-----------------------
//Вспомогательные функции
//-----------------------
@ -99,7 +90,6 @@ const addConfigChildProps = children =>
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;
if (child.type.name === "P8PCyclogram") configProps = P8P_CYCLOGRAM_CONFIG_PROPS;
return React.createElement(child.type, { ...configProps, ...restProps }, addConfigChildProps(children));
});
@ -122,9 +112,6 @@ const P8PDataGridConfigWrapped = (props = {}) => <P8PDataGrid {...P8P_DATA_GRID_
//Обёртка для компонента "Диаграмма Ганта" (P8PGantt)
const P8PGanttConfigWrapped = (props = {}) => <P8PGantt {...P8P_GANTT_CONFIG_PROPS} {...props} />;
//Обёртка для компонента "Циклограмма" (P8PCyclogram)
const P8PCyclogramConfigWrapped = (props = {}) => <P8PCyclogram {...P8P_GANTT_CONFIG_PROPS} {...props} />;
//Универсальный элемент-обёртка в параметры конфигурации
const ConfigWrapper = ({ children }) => addConfigChildProps(children);
@ -145,7 +132,6 @@ export {
P8P_DATA_GRID_SIZE,
P8P_DATA_GRID_FILTER_SHAPE,
P8P_GANTT_CONFIG_PROPS,
P8P_CYCLOGRAM_CONFIG_PROPS,
P8P_GANTT_TASK_SHAPE,
P8P_GANTT_TASK_ATTRIBUTE_SHAPE,
P8P_GANTT_TASK_COLOR_SHAPE,
@ -154,6 +140,5 @@ export {
P8PTableConfigWrapped,
P8PDataGridConfigWrapped,
P8PGanttConfigWrapped,
P8PCyclogramConfigWrapped,
ConfigWrapper
};

View File

@ -22,8 +22,7 @@ const P8O_API = window.top?.parus?.clientApi;
//Структура объекта с описанием ошибок
const APPLICATION_CONTEXT_ERRORS_SHAPE = PropTypes.shape({
P8O_API_UNAVAILABLE: PropTypes.string.isRequired,
P8O_API_UNSUPPORTED: PropTypes.string.isRequired
P8O_API_UNAVAILABLE: PropTypes.string.isRequired
});
//----------------
@ -56,9 +55,6 @@ export const ApplicationContext = ({ errors, displaySizeGetter, guidGenerator, c
//Установка списка панелей
const setPanels = panels => dispatch({ type: APP_AT.LOAD_PANELS, payload: panels });
//Установка заголовка в шапке приложения
const setAppBarTitle = appBarTitle => dispatch({ type: APP_AT.SET_APP_BAR_TITLE, payload: appBarTitle });
//Поиск раздела по имени
const findPanelByName = name => state.panels.find(panel => panel.name == name);
@ -76,38 +72,21 @@ export const ApplicationContext = ({ errors, displaySizeGetter, guidGenerator, c
//Отображение раздела "ПАРУС 8 Онлайн"
const pOnlineShowUnit = useCallback(
({ unitCode, showMethod = "main", inputParameters, modal = true }) => {
if (P8O_API)
modal
? P8O_API.fn.openDocumentModal({ unitcode: unitCode, method: showMethod, inputParameters })
: P8O_API.fn.openDocument
? P8O_API.fn.openDocument({ unitcode: unitCode, method: showMethod, inputParameters })
: showMsgErr(errors.P8O_API_UNSUPPORTED);
({ unitCode, showMethod = "main", inputParameters }) => {
if (P8O_API) P8O_API.fn.openDocumentModal({ unitcode: unitCode, method: showMethod, inputParameters });
else showMsgErr(errors.P8O_API_UNAVAILABLE);
},
[showMsgErr, errors.P8O_API_UNAVAILABLE, errors.P8O_API_UNSUPPORTED]
[showMsgErr, errors.P8O_API_UNAVAILABLE]
);
//Отображение документа "ПАРУС 8 Онлайн"
const pOnlineShowDocument = useCallback(
({ unitCode, document, showMethod = "main", inRnParameter = "in_RN", modal = true }) => {
({ unitCode, document, showMethod = "main", inRnParameter = "in_RN" }) => {
if (P8O_API)
modal
? P8O_API.fn.openDocumentModal({
unitcode: unitCode,
method: showMethod,
inputParameters: [{ name: inRnParameter, value: document }]
})
: P8O_API.fn.openDocument
? P8O_API.fn.openDocument({
unitcode: unitCode,
method: showMethod,
inputParameters: [{ name: inRnParameter, value: document }]
})
: showMsgErr(errors.P8O_API_UNSUPPORTED);
P8O_API.fn.openDocumentModal({ unitcode: unitCode, method: showMethod, inputParameters: [{ name: inRnParameter, value: document }] });
else showMsgErr(errors.P8O_API_UNAVAILABLE);
},
[showMsgErr, errors.P8O_API_UNAVAILABLE, errors.P8O_API_UNSUPPORTED]
[showMsgErr, errors.P8O_API_UNAVAILABLE]
);
//Отображение словаря "ПАРУС 8 Онлайн"
@ -172,7 +151,6 @@ export const ApplicationContext = ({ errors, displaySizeGetter, guidGenerator, c
return (
<ApplicationСtx.Provider
value={{
setAppBarTitle,
findPanelByName,
pOnlineShowTab,
pOnlineShowUnit,

View File

@ -12,14 +12,12 @@ const APP_AT = {
SET_URL_BASE: "SET_URL_BASE", //Установка базового URL приложения
LOAD_PANELS: "LOAD_PANELS", //Загрузка списка панелей
SET_INITIALIZED: "SET_INITIALIZED", //Установка флага инициализированности приложения
SET_DISPLAY_SIZE: "SET_DISPLAY_SIZE", //Установка текущего типового размера экрана
SET_APP_BAR_TITLE: "SET_APP_BAR_TITLE" //Установка заголовка в шапке приложения
SET_DISPLAY_SIZE: "SET_DISPLAY_SIZE" //Установка текущего типового размера экрана
};
//Состояние приложения по умолчанию
const INITIAL_STATE = displaySizeGetter => ({
displaySize: displaySizeGetter(),
appBarTitle: "",
urlBase: "",
panels: [],
panelsLoaded: false,
@ -48,8 +46,6 @@ const handlers = {
[APP_AT.SET_INITIALIZED]: state => ({ ...state, initialized: true }),
//Установка текущего типового размера экрана
[APP_AT.SET_DISPLAY_SIZE]: (state, { payload }) => ({ ...state, displaySize: payload }),
//Установка заголовка в шапке приложения
[APP_AT.SET_APP_BAR_TITLE]: (state, { payload }) => ({ ...state, appBarTitle: payload }),
//Обработчик по умолчанию
DEFAULT: state => state
};

View File

@ -10,7 +10,6 @@
import React, { createContext, useContext, useCallback } from "react"; //ReactJS
import PropTypes from "prop-types"; //Контроль свойств компонента
import { MessagingСtx } from "./messaging"; //Контекст сообщений
import { formatErrorMessage } from "../core/utils"; //Вспомогательные функции
//---------
//Константы
@ -64,8 +63,7 @@ export const BackEndContext = ({ client, children }) => {
throwError = true,
showErrorMessage = true,
fullResponse = false,
spreadOutArguments = true,
signal = null
spreadOutArguments = true
} = {}) => {
try {
if (loader !== false) showLoader(loaderMessage);
@ -77,18 +75,12 @@ export const BackEndContext = ({ client, children }) => {
tagValueProcessor,
attributeValueProcessor,
throwError,
spreadOutArguments,
signal
spreadOutArguments
});
if (fullResponse === true || isRespErr(result)) return result;
else return result.XPAYLOAD;
} catch (e) {
if (showErrorMessage) {
//Разбираем текст ошибки
let errMsg = formatErrorMessage(e.message);
//Отображаем ошибку
showMsgErr(errMsg.text, null, errMsg.fullErrorText);
}
if (showErrorMessage) showMsgErr(e.message);
throw e;
} finally {
if (loader !== false) hideLoader();

View File

@ -33,9 +33,7 @@ const MESSAGING_CONTEXT_TEXTS_SHAPE = PropTypes.shape({
const MESSAGING_CONTEXT_BUTTONS_SHAPE = PropTypes.shape({
CLOSE: PropTypes.string.isRequired,
OK: PropTypes.string.isRequired,
CANCEL: PropTypes.string.isRequired,
DETAIL: PropTypes.string.isRequired,
HIDE: PropTypes.string.isRequired
CANCEL: PropTypes.string.isRequired
});
//----------------
@ -58,16 +56,12 @@ export const MessagingContext = ({ titles, texts, buttons, children }) => {
//Отображение сообщения
const showMsg = useCallback(
(type, text, msgOnOk = null, msgOnCancel = null, fullErrorText = null) =>
dispatch({ type: MSG_AT.SHOW_MSG, payload: { type, text, msgOnOk, msgOnCancel, fullErrorText } }),
(type, text, msgOnOk = null, msgOnCancel = null) => dispatch({ type: MSG_AT.SHOW_MSG, payload: { type, text, msgOnOk, msgOnCancel } }),
[]
);
//Отображение сообщения - ошибка
const showMsgErr = useCallback(
(text, msgOnOk = null, fullErrorText = null) => showMsg(MSG_TYPE.ERR, text, msgOnOk, null, fullErrorText),
[showMsg]
);
const showMsgErr = useCallback((text, msgOnOk = null) => showMsg(MSG_TYPE.ERR, text, msgOnOk), [showMsg]);
//Отображение сообщения - информация
const showMsgInfo = useCallback((text, msgOnOk = null) => showMsg(MSG_TYPE.INFO, text, msgOnOk), [showMsg]);
@ -132,7 +126,6 @@ export const MessagingContext = ({ titles, texts, buttons, children }) => {
open={true}
variant={state.msgType}
text={state.msgText}
fullErrorText={state.msgFullErrorText}
title
titleText={state.msgType == MSG_TYPE.ERR ? titles.ERR : state.msgType == MSG_TYPE.WARN ? titles.WARN : titles.INFO}
okBtn={true}
@ -141,8 +134,6 @@ export const MessagingContext = ({ titles, texts, buttons, children }) => {
cancelBtn={state.msgType == MSG_TYPE.WARN}
onCancel={handleMessageCancelClick}
cancelBtnCaption={buttons.CANCEL}
showErrMoreCaption={buttons.DETAIL}
hideErrMoreCaption={buttons.HIDE}
/>
) : null}
{children}

View File

@ -35,7 +35,6 @@ const INITIAL_STATE = {
msg: false,
msgType: MSG_TYPE.ERR,
msgText: null,
msgFullErrorText: null,
msgOnOk: null,
msgOnCancel: null
};
@ -60,7 +59,6 @@ const handlers = {
msg: true,
msgType: payload.type || MSG_TYPE.APP_ERR,
msgText: payload.text,
msgFullErrorText: payload.fullErrorText,
msgOnOk: payload.msgOnOk,
msgOnCancel: payload.msgOnCancel
}),

View File

@ -41,7 +41,7 @@ export const NavigationContext = ({ children }) => {
const navigate = useNavigate();
//Подключение к контексту приложения
const { findPanelByName, setAppBarTitle } = useContext(ApplicationСtx);
const { findPanelByName } = useContext(ApplicationСtx);
//Проверка наличия параметров запроса
const isNavigationSearch = () => (location.search ? true : false);
@ -65,8 +65,6 @@ export const NavigationContext = ({ children }) => {
const navigateTo = ({ path, search, state, replace = false }) => {
//Если указано куда переходить
if (path) {
//Сброс кастомного заголовка
setAppBarTitle("");
//Переходим к адресу
if (state) navigate(path, { state: JSON.stringify(state), replace });
else navigate({ pathname: path, search: queryString.stringify(search), replace });

View File

@ -34,7 +34,6 @@ const ERR_APPSERVER = "Ошибка сервера приложений"; //Об
const ERR_UNEXPECTED = "Неожиданный ответ сервера"; //Неожиданный ответ сервера
const ERR_NETWORK = "Ошибка соединения с сервером"; //Ошибка сети
const ERR_UNAUTH = "Сеанс завершен. Пройдите аутентификацию повторно."; //Ошибка аутентификации
const ERR_ABORTED = "Запрос прерван принудительно";
//-----------
//Тело модуля
@ -77,16 +76,7 @@ const getRespErrMessage = resp => (isRespErr(resp) && resp.SMESSAGE ? resp.SMESS
const getRespPayload = resp => (resp && resp.XPAYLOAD ? resp.XPAYLOAD : null);
//Исполнение действия на сервере
const executeAction = async ({
serverURL,
action,
payload = {},
isArray,
transformTagName,
tagValueProcessor,
attributeValueProcessor,
signal = null
} = {}) => {
const executeAction = async ({ serverURL, action, payload = {}, isArray, transformTagName, tagValueProcessor, attributeValueProcessor } = {}) => {
console.log(`EXECUTING ${action ? action : ""} ON ${serverURL} WITH PAYLOAD:`);
console.log(payload ? payload : "NO PAYLOAD");
let response = null;
@ -102,14 +92,11 @@ const executeAction = async ({
body: await buildXML(rqBody),
headers: {
"content-type": "application/xml"
},
...(signal ? { signal } : {})
}
});
} catch (e) {
//Прервано принудительно
if (signal?.aborted === true) throw new Error(ERR_ABORTED);
//Сетевая ошибка
else throw new Error(`${ERR_NETWORK}: ${e.message || "неопределённая ошибка"}`);
throw new Error(`${ERR_NETWORK}: ${e.message}`);
}
//Проверим на наличие ошибок HTTP - если есть вернём их
if (!response.ok) throw new Error(`${ERR_APPSERVER}: ${response.statusText}`);
@ -149,8 +136,7 @@ const executeStored = async ({
tagValueProcessor,
attributeValueProcessor,
throwError = true,
spreadOutArguments = false,
signal = null
spreadOutArguments = false
} = {}) => {
let res = null;
try {
@ -171,8 +157,7 @@ const executeStored = async ({
payload: { SSTORED: stored, XARGUMENTS: serverArgs, SRESP_ARG: respArg },
isArray,
tagValueProcessor,
attributeValueProcessor,
signal
attributeValueProcessor
});
if (spreadOutArguments === true && Array.isArray(res?.XPAYLOAD?.XOUT_ARGUMENTS)) {
let spreadArgs = {};
@ -208,11 +193,6 @@ const getConfig = async ({ throwError = true } = {}) => {
//----------------
export default {
ERR_APPSERVER,
ERR_UNEXPECTED,
ERR_NETWORK,
ERR_UNAUTH,
ERR_ABORTED,
SERV_DATA_TYPE_STR,
SERV_DATA_TYPE_NUMB,
SERV_DATA_TYPE_DATE,

View File

@ -33,42 +33,34 @@ const DISPLAY_SIZE = {
//Типовые пути конвертации в массив (при переводе XML -> JSON)
const XML_ALWAYS_ARRAY_PATHS = [
"XRESPOND.XPAYLOAD.XOUT_ARGUMENTS",
"XRESPOND.XPAYLOAD.XDATA_GRID.rows",
"XRESPOND.XPAYLOAD.XDATA_GRID.columnsDef",
"XRESPOND.XPAYLOAD.XDATA_GRID.columnsDef.values",
"XRESPOND.XPAYLOAD.XDATA_GRID.groups",
"XRESPOND.XPAYLOAD.XGANTT.taskAttributes",
"XRESPOND.XPAYLOAD.XGANTT.taskColors",
"XRESPOND.XPAYLOAD.XGANTT.tasks",
"XRESPOND.XPAYLOAD.XGANTT.tasks.dependencies",
"XRESPOND.XPAYLOAD.XROWS",
"XRESPOND.XPAYLOAD.XCOLUMNS_DEF",
"XRESPOND.XPAYLOAD.XCOLUMNS_DEF.values",
"XRESPOND.XPAYLOAD.XGROUPS",
"XRESPOND.XPAYLOAD.XGANTT_DEF.taskAttributes",
"XRESPOND.XPAYLOAD.XGANTT_DEF.taskColors",
"XRESPOND.XPAYLOAD.XGANTT_TASKS",
"XRESPOND.XPAYLOAD.XGANTT_TASKS.dependencies",
"XRESPOND.XPAYLOAD.XCHART.labels",
"XRESPOND.XPAYLOAD.XCHART.datasets",
"XRESPOND.XPAYLOAD.XCHART.datasets.data",
"XRESPOND.XPAYLOAD.XCHART.datasets.items",
"XRESPOND.XPAYLOAD.XCYCLOGRAM.taskAttributes",
"XRESPOND.XPAYLOAD.XCYCLOGRAM.columns",
"XRESPOND.XPAYLOAD.XCYCLOGRAM.groups",
"XRESPOND.XPAYLOAD.XCYCLOGRAM.tasks"
"XRESPOND.XPAYLOAD.XCHART.datasets.items"
];
//Типовые шаблоны конвертации в массив (при переводе XML -> JSON)
const XML_ALWAYS_ARRAY_PATH_PATTERNS = [
/(.*)XDATA_GRID.rows$/,
/(.*)XDATA_GRID.columnsDef$/,
/(.*)XDATA_GRID.columnsDef.values$/,
/(.*)XDATA_GRID.groups$/,
/(.*)XGANTT.taskAttributes$/,
/(.*)XGANTT.taskColors$/,
/(.*)XGANTT.tasks$/,
/(.*)XGANTT.tasks.dependencies$/,
/(.*)XROWS$/,
/(.*)XCOLUMNS_DEF$/,
/(.*)XCOLUMNS_DEF.values$/,
/(.*)XGROUPS$/,
/(.*)XGANTT_DEF.taskAttributes$/,
/(.*)XGANTT_DEF.taskColors$/,
/(.*)XGANTT_TASKS$/,
/(.*)XGANTT_TASKS.dependencies$/,
/(.*)XCHART.labels$/,
/(.*)XCHART.datasets$/,
/(.*)XCHART.datasets.data$/,
/(.*)XCHART.datasets.items$/,
/(.*)XCYCLOGRAM.taskAttributes$/,
/(.*)XCYCLOGRAM.columns$/,
/(.*)XCYCLOGRAM.groups$/,
/(.*)XCYCLOGRAM.tasks$/
/(.*)XCHART.datasets.items$/
];
//Типовой постфикс тега для массива (при переводе XML -> JSON)
@ -76,13 +68,11 @@ const XML_ALWAYS_ARRAY_POSTFIX = "__SYSTEM__ARRAY__";
//Типовые шаблоны конвертации значения атрибута в строку (при переводе XML -> JSON)
const XML_ATTR_ALWAYS_STR_PATH_PATTERNS = [
/(.*)XDATA_GRID.columnsDef.name$/,
/(.*)XDATA_GRID.columnsDef.caption$/,
/(.*)XDATA_GRID.columnsDef.parent$/,
/(.*)XDATA_GRID.groups.name$/,
/(.*)XDATA_GRID.groups.caption$/,
/(.*)XCYCLOGRAM.columns.name$/,
/(.*)XCYCLOGRAM.groups.name$/
/(.*)XCOLUMNS_DEF.name$/,
/(.*)XCOLUMNS_DEF.caption$/,
/(.*)XCOLUMNS_DEF.parent$/,
/(.*)XGROUPS.name$/,
/(.*)XGROUPS.caption$/
];
//-----------
@ -102,11 +92,12 @@ const getDisplaySize = () => {
};
//Глубокое копирование объекта
const deepCopyObject = obj => (structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj)));
const deepCopyObject = obj => JSON.parse(JSON.stringify(obj));
//Конвертация объекта в Base64 XML
const object2Base64XML = (obj, builderOptions) => {
const builder = new XMLBuilder(builderOptions);
//onOrderChanged({ orders: btoa(ordersBuilder.build(newOrders)) });
return btoa(unescape(encodeURIComponent(builder.build(obj))));
};
@ -149,43 +140,12 @@ const xml2JSON = ({ xmlDoc, isArray, transformTagName, tagValueProcessor, attrib
//Форматирование даты в формат РФ
const formatDateRF = value => (value ? dayjs(value).format("DD.MM.YYYY") : null);
//Форматирование даты и времени в формат РФ
const formatDateTimeRF = value => (value ? dayjs(value).format("DD.MM.YYYY HH:mm:ss") : null);
//Форматирование даты в формат JSON (только дата, без времени)
const formatDateJSONDateOnly = value => (value ? dayjs(value).format("YYYY-MM-DD") : null);
//Форматирование числа в "Денежном" формате РФ
const formatNumberRFCurrency = value => (hasValue(value) ? new Intl.NumberFormat("ru-RU", { minimumFractionDigits: 2 }).format(value) : null);
//Форматирование текста ошибки
const formatErrorMessage = errorMsg => {
//Инициализируем текст заголовка ошибки
let text = "";
//Пробуем извлечь заголовок текста ошибки
try {
//Если это ошибка Oracle
if (errorMsg.match(/^ORA-/)) {
//Считываем первую строку с заголовочным текстом ошибки
text = errorMsg.match(/^.*(?=(\nORA-))/)[0];
//Убираем лишнюю информацию и пробелы
text = text.replace(/ORA-\d*:/g, "").trim();
}
//Если это ошибка PG
if (errorMsg.match(/^SQL Error/)) {
//Считываем первую строку с заголовочным текстом ошибки
text = errorMsg.match(/.*(?=(\n.*Where)|(.*Where))/)[0];
//Убираем лишнюю информацию и пробелы
text = text.replace(/SQL Error \[\d*\]: ERROR:/g, "").trim();
}
} catch {
//Если произошла ошибка - оставляем полный текст ошибки
text = errorMsg;
}
//Возвращаем результат
return { text: text || errorMsg, fullErrorText: text ? errorMsg : null };
};
//Формирование уникального идентификатора
const genGUID = () =>
"10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
@ -203,9 +163,7 @@ export {
object2Base64XML,
xml2JSON,
formatDateRF,
formatDateTimeRF,
formatDateJSONDateOnly,
formatNumberRFCurrency,
formatErrorMessage,
genGUID
};

View File

@ -0,0 +1,271 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Панель мониторинга: Корневая панель доски задач
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState, useCallback } from "react"; //Классы React
import { DragDropContext, Droppable } from "react-beautiful-dnd"; //Работа с drag&drop
import { Stack, Box, IconButton, Icon } from "@mui/material"; //Интерфейсные компоненты
import { StatusCard } from "./components/status_card.js";
import { TaskDialog } from "./task_dialog.js"; //Компонент формы события
import { Filter } from "./filter.js"; //Компонент фильтров
import { useExtraData, useTasks, useSettings } from "./hooks/hooks.js"; //Вспомогательные хуки
import { useFilters } from "./hooks/filter_hooks.js";
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { NoteDialog } from "./components/note_dialog.js"; //Диалог примечания
import { SettingsDialog } from "./components/settings_dialog.js"; //Диалог дополнительных настроек
import { deepCopyObject } from "../../core/utils.js"; //Вспомогательные функции
//---------
//Константы
//---------
//Высота заголовка
const TITLE_HEIGHT = "64px";
//Нижний отступ заголовка
const TITLE_PADDING_BOTTOM = "16px";
//Высота фильтра
const FILTER_HEIGHT = "56px";
//Стили
const STYLES = {
CONTAINER: { width: "100%", padding: 0 },
FS_BOX: { display: "flex", alignItems: "center" },
SETTINGS_MARGIN: { marginLeft: "auto" },
STATUSES_STACK: { maxWidth: "99vw", paddingBottom: "5px", overflowX: "auto", ...APP_STYLES.SCROLL },
STATUS_BLOCK: statusColor => {
return {
width: "350px",
height: `calc(100vh - ${TITLE_HEIGHT} - ${TITLE_PADDING_BOTTOM} - ${FILTER_HEIGHT} - 8px)`,
backgroundColor: statusColor,
padding: "8px"
};
},
BLOCK_OPACITY: isAvailable => {
return isAvailable ? { opacity: 1 } : { opacity: 0.5 };
},
STATUSES_DIV: { position: "fixed", left: "8px", top: `calc(${TITLE_HEIGHT} + ${FILTER_HEIGHT})` },
CARD_CONTENT: {
padding: 0,
paddingRight: "5px",
paddingBottom: "5px !important",
overflowY: "auto",
maxHeight: `calc(100vh - ${TITLE_HEIGHT} - ${TITLE_PADDING_BOTTOM} - ${FILTER_HEIGHT} - 85px)`,
...APP_STYLES.SCROLL
},
MARK_INFO: {
textAlign: "left",
textOverflow: "ellipsis",
overflow: "hidden",
display: "-webkit-box",
hyphens: "auto",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 1,
maxWidth: "calc(300px)",
width: "-webkit-fill-available",
fontSize: "1.2rem",
cursor: "default"
},
PADDING_0: { padding: 0 }
};
//-----------
//Тело модуля
//-----------
//Корневая панель доски задач
const ClntTaskBoard = () => {
//Состояние вспомогательных диалогов
const [dialogsState, setDialogsState] = useState({
filterOpen: false,
settingsOpen: false,
note: { isOpen: false, callback: null },
taskDialogOpen: false
});
//Открыть-закрыть диалог фильтра
const handleFilterOpen = isOpen => {
setDialogsState(pv => ({ ...pv, filterOpen: isOpen }));
};
//Открыть-закрыть диалог дополнительных настроек
const handleSettingsOpen = () => setDialogsState(pv => ({ ...pv, settingsOpen: !dialogsState.settingsOpen }));
//Открыть-закрыть диалог примечания
const handleNoteOpen = (f = null) => setDialogsState(pv => ({ ...pv, note: { isOpen: !dialogsState.noteOpen, callback: f ? v => f(v) : null } }));
//Открыть-закрыть диалог события
const handleTaskDialogOpen = () => setDialogsState(pv => ({ ...pv, taskDialogOpen: !dialogsState.taskDialogOpen }));
//Состояние фильтров
const [filters, handleFiltersChange] = useFilters(handleFilterOpen);
//Состояние сортировок
const [orders, setOrders] = useState([]);
//Состояние дополнительных данных
const [extraData, getDocLinks, needUpdateExtraData] = useExtraData(filters.values.type);
//Состояние событий
const [tasks, handleReload, onDragEnd, needUpdateTasks] = useTasks({ filters, orders, extraData, getDocLinks });
//Состояние дополнительных настроек
const [settings, handleSettingsChange] = useSettings(tasks.statuses);
//Состояние доступных маршрутов события
const [availableRoutes, setAvailableRoutes] = useState({ sorce: "", routes: [] });
//При изменении сортировки
const handleOrderChanged = useCallback(
columnName => {
let newOrders = deepCopyObject(orders);
const colOrder = newOrders.find(o => o.name == columnName);
const newDirection = colOrder?.direction == "ASC" ? "DESC" : colOrder?.direction == "DESC" ? null : "ASC";
if (newDirection == null && colOrder) newOrders.splice(newOrders.indexOf(colOrder), 1);
if (newDirection != null && !colOrder) newOrders.push({ name: columnName, direction: newDirection });
if (newDirection != null && colOrder) colOrder.direction = newDirection;
setOrders(newOrders);
},
[orders]
);
//Очистка состояния доступных маршрутов события
const clearAvailableRoutesState = () => {
setAvailableRoutes({ sorce: "", routes: [] });
};
//Состояние перетаскиваемого события
const [dragItem, setDragItem] = useState({ type: "", status: "" });
//Захватить перетаскиваемый объект
const handleDragItemChange = (filtersType, statusCode) =>
setDragItem({
type: filtersType,
status: statusCode
});
//Отпустить перетаскиваемый объект
const handleDragItemClear = () => {
setDragItem({ type: "", status: "" });
};
//Проверка доступности карточки события
const isCardAvailable = code => {
return availableRoutes.sorce === code || availableRoutes.routes.find(r => r.dest === code) || !availableRoutes.sorce ? true : false;
};
//При смене типа события
useEffect(() => {
if (filters.values.type) {
//Обновление вспомогательных данных
filters.values.type !== extraData.typeLoaded ? needUpdateExtraData() : null;
//Обновление событий
needUpdateTasks();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters.values.type]);
//Генерация содержимого
return (
<Box sx={STYLES.CONTAINER}>
{dialogsState.settingsOpen ? (
<SettingsDialog initial={settings} onSettingsChange={handleSettingsChange} onOpen={handleSettingsOpen} />
) : null}
{dialogsState.taskDialogOpen ? (
<TaskDialog
taskType={dragItem.type}
taskStatus={dragItem.status}
onReload={handleReload}
onClose={() => {
handleTaskDialogOpen();
handleDragItemClear();
}}
/>
) : null}
<Box sx={STYLES.FS_BOX}>
<Stack direction="row">
<Filter
isFilterDialogOpen={dialogsState.filterOpen}
filter={filters.values}
docs={extraData.docLinks}
selectedDoc={filters.values.docLink ? extraData.docLinks.find(d => d.id === filters.values.docLink) : null}
onFilterChange={handleFiltersChange}
getDocLinks={getDocLinks}
onFilterOpen={() => handleFilterOpen(true)}
onFilterClose={() => handleFilterOpen(false)}
onReload={handleReload}
orders={orders}
onOrderChanged={handleOrderChanged}
/>
</Stack>
<IconButton title="Настройки" onClick={handleSettingsOpen} sx={STYLES.SETTINGS_MARGIN}>
<Icon>settings</Icon>
</IconButton>
</Box>
{dialogsState.note.isOpen ? (
<NoteDialog noteTypes={extraData.noteTypes} onCallback={n => dialogsState.note.callback(n)} onNoteOpen={handleNoteOpen} />
) : null}
{filters.loaded && filters.values.type && extraData.dataLoaded && tasks.groupsLoaded && tasks.tasksLoaded ? (
<DragDropContext
onDragStart={e => {
let srcCode = tasks.statuses.find(s => s.id == e.source.droppableId).code;
setAvailableRoutes({ sorce: srcCode, routes: [...extraData.evRoutes.filter(r => r.src === srcCode)] });
}}
onDragEnd={e => {
onDragEnd(e, extraData.evPoints, handleNoteOpen);
clearAvailableRoutesState();
}}
>
<div style={STYLES.STATUSES_DIV}>
<Droppable droppableId="Statuses" type="droppableTask">
{provided => (
<div ref={provided.innerRef}>
<Stack direction="row" spacing={2} sx={STYLES.STATUSES_STACK}>
{settings.statusesSort.sorted
? settings.statusesSort.statuses.map((status, index) => (
<div key={index}>
<Droppable isDropDisabled={!isCardAvailable(status.code)} droppableId={status.id.toString()}>
{provided => (
<div ref={provided.innerRef}>
<StatusCard
tasks={tasks}
status={status}
settings={settings}
extraData={extraData}
filtersType={filters.values.type}
isCardAvailable={isCardAvailable}
onReload={handleReload}
onDragItemChange={handleDragItemChange}
onTaskDialogOpen={handleTaskDialogOpen}
onNoteDialogOpen={handleNoteOpen}
placeholder={provided.placeholder}
/>
</div>
)}
</Droppable>
</div>
))
: null}
</Stack>
{provided.placeholder}
</div>
)}
</Droppable>
</div>
</DragDropContext>
) : null}
</Box>
);
};
//----------------
//Интерфейс модуля
//----------------
export { ClntTaskBoard };

View File

@ -0,0 +1,341 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Диалоговое окно фильтра отбора
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext, useEffect, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
Dialog,
DialogTitle,
IconButton,
Icon,
DialogContent,
DialogActions,
Button,
Box,
Stack,
Checkbox,
FormControlLabel,
Radio,
RadioGroup
} from "@mui/material"; //Интерфейсные компоненты
import { FilterInputField } from "./filter_input_field"; //Компонент поля ввода
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
import { hasValue } from "../../../core/utils"; //Вспомогательные функции
import { APP_STYLES } from "../../../../app.styles"; //Типовые стили
import { EVENT_STATES } from "../layouts"; //Перечисление состояний события
//---------
//Константы
//---------
//Стили
const STYLES = {
FILTERS_SCROLL: { overflowY: "auto", ...APP_STYLES.SCROLL },
DIALOG_ACTIONS: { justifyContent: "end", paddingRight: "24px", paddingLeft: "24px" },
CLOSE_BUTTON: {
position: "absolute",
right: 8,
top: 8,
color: theme => theme.palette.grey[500]
},
DOCLINK_STACK: { alignItems: "baseline" },
SELECT: { width: "450px" },
BOX_WITH_LEGEND: { border: "1px solid #939393" },
LEGEND: { textAlign: "left" }
};
//-----------------------
//Вспомогательные функции
//-----------------------
//Выбор типа события
const selectEventType = (value, showDictionary, callBack) => {
showDictionary({
unitCode: "ClientEventTypes",
showMethod: "dictionary",
inputParameters: [{ name: "pos_eventtypecode", value: value }],
callBack: res => (res.success === true ? callBack(res.outParameters.eventtypecode) : callBack(null))
});
};
//Выбор каталога
const selectCatalog = (value, showDictionary, callBack) => {
showDictionary({
unitCode: "CatalogTree",
inputParameters: [
{ name: "in_DOCNAME", value: "ClientEvents" },
{ name: "in_NAME", value: value }
],
callBack: res => (res.success === true ? callBack(res.outParameters.out_NAME) : callBack(null))
});
};
//Выбор исполнителя
const selectSendPerson = (value, showDictionary, callBack) => {
showDictionary({
unitCode: "AGNLIST",
showMethod: "agents",
inputParameters: [{ name: "pos_agnmnemo", value: value }],
callBack: res => (res.success === true ? callBack(res.outParameters.agnmnemo) : callBack(null))
});
};
//Выбор подразделения
const selectSendDivision = (value, showDictionary, callBack) => {
showDictionary({
unitCode: "INS_DEPARTMENT",
inputParameters: [{ name: "in_CODE", value: value }],
callBack: res => (res.success === true ? callBack(res.outParameters.out_CODE) : callBack(null))
});
};
//Выбор группы пользователей
const selectSendUsrGrp = (value, showDictionary, callBack) => {
showDictionary({
unitCode: "CostStaffGroups",
inputParameters: [{ name: "in_CODE", value: value }],
callBack: res => (res.success === true ? callBack(res.outParameters.out_CODE) : callBack(null))
});
};
//---------------
//Тело компонента
//---------------
//Диалоговое окно фильтра отбора
const FilterDialog = ({ initial, docs, onFilterChange, onFilterOpen, getDocLinks }) => {
//Собственное состояние
const [filter, setFilter] = useState({ ...initial });
//Состояние текущего типа события
const [curType, setCurType] = useState(initial.type);
//Состояние учётных документов
const [curDocLinks, setCurDocLinks] = useState(docs);
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Получение подкаталогов
const getSubCatalogs = useCallback(async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_SUBCATALOGS_GET",
args: {
SCRN_NAME: filter.catalog,
NSUBCAT: filter.wSubcatalogs ? 1 : 0
}
});
return data.SRESULT;
}, [executeStored, filter.catalog, filter.wSubcatalogs]);
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//При закрытии диалога без изменения фильтра
const handleCancel = () => onFilterOpen();
//При очистке фильтра
const handleClear = () => {
setFilter({
evState: EVENT_STATES[1],
type: "",
catalog: "",
crn: "",
wSubcatalogs: false,
sendPerson: "",
sendDivision: "",
sendUsrGrp: "",
docLink: ""
});
};
//При закрытии диалога с изменением фильтра
const handleOk = async () => {
if (filter.catalog && !filter.crn) {
const crns = await getSubCatalogs();
let filterCopy = { ...filter };
crns ? (filterCopy.crn = crns) : null;
onFilterChange(filterCopy);
onFilterOpen();
} else {
onFilterChange(filter);
onFilterOpen();
}
};
//При изменении значения элемента
const handleFilterItemChange = (item, value) => {
item === "type" && filter.docLink ? setFilter(pv => ({ ...pv, [item]: value, docLink: "" })) : setFilter(pv => ({ ...pv, [item]: value }));
};
//Очистка учётного документа
const clearDocLink = () => setFilter(pv => ({ ...pv, docLink: "" }));
//При изменении типа события
useEffect(() => {
if (curType) {
if (curType !== filter.type) {
clearDocLink();
setCurDocLinks([]);
} else if (curType === filter.type && curType === initial.type && !curDocLinks.length) setCurDocLinks(docs);
}
}, [curDocLinks, curType, docs, filter.type, initial.type]);
//Обработка изменений с каталогами
useEffect(() => {
if (!filter.catalog && filter.wSubcatalogs) setFilter(pv => ({ ...pv, wSubcatalogs: false }));
if (filter.catalog !== initial.catalog && filter.crn) setFilter(pv => ({ ...pv, crn: "" }));
if (filter.catalog === initial.catalog && filter.crn !== initial.crn && filter.wSubcatalogs === initial.wSubcatalogs)
setFilter(pv => ({ ...pv, crn: initial.crn }));
if (filter.catalog === initial.catalog && filter.wSubcatalogs !== initial.wSubcatalogs && !filter.wSubcatalogs)
setFilter(pv => ({ ...pv, crn: initial.crn.split(";")[0] }));
if (filter.catalog === initial.catalog && filter.wSubcatalogs !== initial.wSubcatalogs && filter.wSubcatalogs)
setFilter(pv => ({ ...pv, crn: "" }));
}, [filter.catalog, filter.crn, filter.wSubcatalogs, initial.catalog, initial.crn, initial.wSubcatalogs]);
//Генерация содержимого
return (
<div>
<Dialog open onClose={handleCancel} fullWidth maxWidth="sm">
<DialogTitle>Фильтр отбора</DialogTitle>
<IconButton aria-label="close" onClick={handleCancel} sx={STYLES.CLOSE_BUTTON}>
<Icon>close</Icon>
</IconButton>
<DialogContent sx={STYLES.FILTERS_SCROLL}>
<Box sx={STYLES.BOX_WITH_LEGEND} component="fieldset">
<legend style={STYLES.LEGEND}>Состояние</legend>
<RadioGroup
row
aria-labelledby="evState-label"
id="evState"
name="evState"
value={filter.evState}
onChange={e => handleFilterItemChange(e.target.name, e.target.value)}
>
{Object.keys(EVENT_STATES).map(function (k) {
return <FormControlLabel key={k} value={EVENT_STATES[k]} control={<Radio />} label={EVENT_STATES[k]} />;
})}
</RadioGroup>
</Box>
<Box component="section" p={1}>
<FilterInputField
elementCode="type"
elementValue={filter.type}
labelText="Тип"
dictionary={callBack => selectEventType(filter.type, pOnlineShowDictionary, callBack)}
onChange={handleFilterItemChange}
/>
</Box>
<Box component="section" p={1}>
<FilterInputField
elementCode="catalog"
elementValue={filter.catalog}
labelText="Каталог"
dictionary={callBack => selectCatalog(filter.catalog, pOnlineShowDictionary, callBack)}
onChange={handleFilterItemChange}
/>
<FormControlLabel
control={
<Checkbox
id="wSubcatalogs"
name="wSubcatalogs"
checked={filter.wSubcatalogs}
disabled={filter.catalog ? false : true}
onChange={e => handleFilterItemChange(e.target.name, e.target.checked)}
/>
}
label="Включая подкаталоги"
/>
</Box>
<Box component="section" p={1}>
<FilterInputField
elementCode="sendPerson"
elementValue={filter.sendPerson}
labelText="Исполнитель"
dictionary={callBack => selectSendPerson(filter.sendPerson, pOnlineShowDictionary, callBack)}
onChange={handleFilterItemChange}
/>
</Box>
<Box component="section" p={1}>
<FilterInputField
elementCode="sendDivision"
elementValue={filter.sendDivision}
labelText="Подразделение"
dictionary={callBack => selectSendDivision(filter.sendDivision, pOnlineShowDictionary, callBack)}
onChange={handleFilterItemChange}
/>
</Box>
<Box component="section" p={1}>
<FilterInputField
elementCode="sendUsrGrp"
elementValue={filter.sendUsrGrp}
labelText="Группа пользователей"
dictionary={callBack => selectSendUsrGrp(filter.sendUsrGrp, pOnlineShowDictionary, callBack)}
onChange={handleFilterItemChange}
/>
</Box>
<Box component="section" p={1}>
<Stack direction="row" sx={STYLES.DOCLINK_STACK}>
<FilterInputField
elementCode="docLink"
elementValue={filter.docLink}
labelText="Учётный документ"
items={curDocLinks}
disabled={!curDocLinks.length ? true : false}
onChange={handleFilterItemChange}
sx={STYLES.SELECT}
/>
<IconButton title="Очистить" disabled={!filter.docLink} onClick={clearDocLink}>
<Icon>clear</Icon>
</IconButton>
<IconButton
title="Обновить"
disabled={!((!curType || curType !== filter.type) && filter.type)}
onClick={() => {
setCurType(filter.type);
clearDocLink();
getDocLinks(filter.type).then(dl => setCurDocLinks(dl));
}}
>
<Icon>refresh</Icon>
</IconButton>
</Stack>
</Box>
</DialogContent>
<DialogActions sx={STYLES.DIALOG_ACTIONS}>
<Button disabled={!hasValue(filter.type)} variant="text" onClick={handleOk}>
ОК
</Button>
<Button variant="text" onClick={handleClear}>
Очистить
</Button>
<Button variant="text" onClick={handleCancel}>
Отмена
</Button>
</DialogActions>
</Dialog>
</div>
);
};
//Контроль свойств компонента - Диалоговое окно фильтра отбора
FilterDialog.propTypes = {
initial: PropTypes.object.isRequired,
docs: PropTypes.arrayOf(PropTypes.object),
onFilterChange: PropTypes.func.isRequired,
onFilterOpen: PropTypes.func.isRequired,
getDocLinks: PropTypes.func
};
//--------------------
//Интерфейс компонента
//--------------------
export { FilterDialog };

View File

@ -0,0 +1,134 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Поле ввода диалога фильтра
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { FormControl, InputLabel, Input, InputAdornment, IconButton, Icon, FormHelperText, Select, MenuItem } from "@mui/material"; //Интерфейсные компоненты
import { APP_STYLES } from "../../../../app.styles"; //Типовые стили
//---------
//Константы
//---------
//Стили
const STYLES = {
HELPER_TEXT: { color: "red" },
SELECT_MENU: w => {
return { overflowY: "auto", ...APP_STYLES.SCROLL, width: w ? w : null };
}
};
//---------------
//Тело компонента
//---------------
//Поле ввода
const FilterInputField = ({ elementCode, elementValue, labelText, onChange, required = false, items = null, dictionary, ...other }) => {
//Значение элемента
const [value, setValue] = useState(elementValue);
//При получении нового значения из вне
useEffect(() => {
setValue(elementValue);
}, [elementValue]);
//Выбор значения из словаря
const handleDictionaryClick = () => {
dictionary ? dictionary(res => (res ? handleChange({ target: { name: elementCode, value: res } }) : null)) : null;
};
//Изменение значения элемента
const handleChange = e => {
setValue(e.target.value);
if (onChange) onChange(e.target.name, e.target.value);
};
//Генерация поля с выбором из словаря Парус
const renderInput = validationError => {
return (
<Input
error={validationError}
id={elementCode}
name={elementCode}
value={value}
endAdornment={
dictionary ? (
<InputAdornment position="end">
<IconButton aria-label={`${elementCode} select`} onClick={handleDictionaryClick} edge="end">
<Icon>list</Icon>
</IconButton>
</InputAdornment>
) : null
}
aria-describedby={`${elementCode}-helper-text`}
label={labelText}
onChange={handleChange}
{...other}
/>
);
};
//Генерация поля с выпадающим списком
const renderSelect = (items, validationError) => {
return (
<Select
error={validationError}
id={elementCode}
name={elementCode}
value={value}
aria-describedby={`${elementCode}-helper-text`}
label={labelText}
MenuProps={{ slotProps: { paper: { sx: STYLES.SELECT_MENU(document.getElementById(elementCode)?.parentElement.clientWidth) } } }}
onChange={handleChange}
{...other}
>
{items
? items.map((item, i) => (
<MenuItem key={i} value={item.id}>
{item.descr}
</MenuItem>
))
: null}
</Select>
);
};
//Признак ошибки валидации
const validationError = !value && required ? true : false;
//Генерация содержимого
return (
<FormControl fullWidth variant="standard">
<InputLabel htmlFor={elementCode}>{labelText}</InputLabel>
{items ? renderSelect(items, validationError) : renderInput(validationError)}
{validationError ? (
<FormHelperText id={`${elementCode}-helper-text`} sx={STYLES.HELPER_TEXT}>
*Обязательное поле
</FormHelperText>
) : null}
</FormControl>
);
};
//Контроль свойств - Поле ввода
FilterInputField.propTypes = {
elementCode: PropTypes.string.isRequired,
elementValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
labelText: PropTypes.string.isRequired,
required: PropTypes.bool,
items: PropTypes.arrayOf(PropTypes.object),
dictionary: PropTypes.func,
onChange: PropTypes.func
};
//--------------------
//Интерфейс компонента
//--------------------
export { FilterInputField };

View File

@ -0,0 +1,139 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Диалог примечания
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
Dialog,
DialogTitle,
IconButton,
Icon,
DialogContent,
DialogActions,
Button,
TextField,
FormControl,
InputLabel,
Select,
MenuItem
} from "@mui/material"; //Интерфейсные компоненты
import { APP_STYLES } from "../../../../app.styles"; //Типовые стили
import { arrayFormer } from "../layouts"; //Формировщик массива
//Стили
const STYLES = {
DIALOG_ACTIONS: { justifyContent: "end", paddingRight: "24px", paddingLeft: "24px" },
CLOSE_BUTTON: {
position: "absolute",
right: 8,
top: 8,
color: theme => theme.palette.grey[500]
},
DIALOG_CONTENT: { paddingTop: 0, paddingBottom: 0 },
TEXT_FIELD: {
overflowY: "auto",
...APP_STYLES.SCROLL
}
};
//---------------
//Тело компонента
//---------------
//Диалог примечания
const NoteDialog = ({ noteTypes, onCallback, onNoteOpen }) => {
//Собственное состояние
const [note, setNote] = useState({ headerV: 0, text: "" });
//При изменении примечания
const handleNoteChange = value => setNote(pv => ({ ...pv, text: value }));
//При изменении заголовка примечания
const handleNoteHeaderChange = h => {
setNote(pv => ({ ...pv, headerV: h }));
};
//При закрытии диалога с изменением фильтра
const handleOK = () => {
onCallback({ header: noteTypes[note.headerV], text: note.text });
onNoteOpen();
};
//При закрытии диалога без изменения фильтра
const handleCancel = () => {
onNoteOpen();
};
//Генерация содержимого
return (
<Dialog open onClose={handleCancel} fullWidth maxWidth="sm">
<DialogTitle>Примечание</DialogTitle>
<IconButton aria-label="close" onClick={handleCancel} sx={STYLES.CLOSE_BUTTON}>
<Icon>close</Icon>
</IconButton>
<DialogContent sx={STYLES.DIALOG_CONTENT}>
<FormControl fullWidth variant="standard">
<InputLabel htmlFor="noteHeader">Заголовок примечания</InputLabel>
<Select
id="noteHeader"
name="noteHeader"
value={note.headerV}
aria-describedby="noteHeader-helper-text"
label="Заголовок примечания"
margin="dense"
onChange={e => handleNoteHeaderChange(e.target.value)}
>
{noteTypes
? arrayFormer(noteTypes).map((item, i) => (
<MenuItem key={i} value={i}>
{item}
</MenuItem>
))
: null}
</Select>
</FormControl>
<TextField
id="note"
label="Описание"
variant="standard"
fullWidth
required
multiline
minRows={7}
maxRows={7}
value={note.text}
margin="normal"
inputProps={{ sx: STYLES.TEXT_FIELD }}
onChange={e => handleNoteChange(e.target.value)}
/>
</DialogContent>
<DialogActions sx={STYLES.DIALOG_ACTIONS}>
<Button disabled={!note.text} variant="text" onClick={handleOK}>
ОК
</Button>
<Button variant="text" onClick={onNoteOpen}>
Отмена
</Button>
</DialogActions>
</Dialog>
);
};
//Контроль свойств - Диалог примечания
NoteDialog.propTypes = {
noteTypes: PropTypes.array,
onCallback: PropTypes.func.isRequired,
onNoteOpen: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { NoteDialog };

View File

@ -0,0 +1,93 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Выпадающий список выбора заливки событий
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { FormControl, InputLabel, Select, MenuItem } from "@mui/material"; //Интерфейсные компоненты
import { useColorRules } from "../hooks/hooks.js"; //Вспомогательные хуки
import { APP_STYLES } from "../../../../app.styles"; //Типовые стили
//---------
//Константы
//---------
//Стили
const STYLES = {
SELECT_MENU: w => {
return { overflowY: "auto", ...APP_STYLES.SCROLL, width: w ? w : null };
}
};
//---------------
//Тело компонента
//---------------
//Выпадающий список выбора заливки событий
const RulesSelect = ({ initRule, handleChange, ...other }) => {
//Состояние пользовательских настроек заливки событий
const [colorRules] = useColorRules();
//Собственное состояние
const [curRule, setCurRule] = useState(initRule > -1 ? "" : initRule);
//При получении нового значения заливки из вне
useEffect(() => {
if (
(colorRules.loaded && initRule > -1 && curRule === "") ||
(Number.isInteger(initRule) && Number.isInteger(curRule) && initRule !== curRule)
)
setCurRule(initRule);
}, [colorRules, curRule, initRule]);
//При изменении заливки событий
const handleRuleChange = e => {
let id = e.target.value;
setCurRule(id);
handleChange(id > -1 ? colorRules.rules[id] : {});
};
//Генерация содержимого
return colorRules ? (
<FormControl size="small" variant="standard" {...other}>
<InputLabel htmlFor="clrRules">Заливка событий</InputLabel>
<Select
id="clrRules"
name="clrRules"
value={curRule}
aria-describedby="clrRules-helper-text"
label="Заливка событий"
MenuProps={{ slotProps: { paper: { sx: STYLES.SELECT_MENU(document.getElementById("clrRules")?.parentElement.clientWidth) } } }}
onChange={handleRuleChange}
>
<MenuItem key={-1} value={-1}>
{"-"}
</MenuItem>
{colorRules.rules
? colorRules.rules.map((item, i) => (
<MenuItem key={i} value={item.id}>
{item.propName}
</MenuItem>
))
: null}
</Select>
</FormControl>
) : null;
};
//Контроль свойств - Выпадающий список выбора заливки событий
RulesSelect.propTypes = {
initRule: PropTypes.number.isRequired,
handleChange: PropTypes.func.isRequired
};
//--------------------
//Интерфейс компонента
//--------------------
export { RulesSelect };

View File

@ -0,0 +1,135 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Диалог дополнительных настроек
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Icon, Button, Box, Stack } from "@mui/material"; //Интерфейсные компоненты
import { RulesSelect } from "./rules_select.js"; //Выпадающий список выбора заливки событий
import { FilterInputField } from "./filter_input_field.js"; //Поле ввода
import { APP_STYLES } from "../../../../app.styles"; //Типовые стили
import { sortAttrs, sortDest } from "../layouts.js"; //Допустимые значение поля и направления сортировки
//---------
//Константы
//---------
//Стили
const STYLES = {
FILTERS_SCROLL: { overflowY: "auto", ...APP_STYLES.SCROLL },
DIALOG_ACTIONS: { justifyContent: "end", paddingRight: "24px", paddingLeft: "24px" },
CLOSE_BUTTON: {
position: "absolute",
right: 8,
top: 8,
color: theme => theme.palette.grey[500]
},
DOCLINK_STACK: { alignItems: "baseline" },
SELECT: { width: "100%" },
BOX_WITH_LEGEND: { border: "1px solid #939393" },
LEGEND: { textAlign: "left" },
SELECT_MENU: w => {
return { overflowY: "auto", ...APP_STYLES.SCROLL, width: w ? w : null };
}
};
//---------------
//Тело компонента
//---------------
//Диалог дополнительных настроек
const SettingsDialog = ({ initial, onSettingsChange, onOpen, ...other }) => {
//Состояние дополнительных настроек
const [settings, setSettings] = useState(
initial.statusesSort.attr ? { ...initial } : { ...initial, statusesSort: { sorted: true, attr: "name", dest: "asc" } }
);
//Изменение заливки событий
const handleColorRuleChange = cr => setSettings(pv => ({ ...pv, colorRule: cr }));
//Изменение поля сортировки
const handleSortAttrChange = (item, value) => setSettings(pv => ({ ...pv, statusesSort: { ...pv.statusesSort, [item]: value } }));
//Изменение направления сортировки
const handleSortDestChange = d => setSettings(pv => ({ ...pv, statusesSort: { ...pv.statusesSort, dest: d } }));
//Генерация содержимого
return (
<div {...other}>
<Dialog open onClose={onOpen} fullWidth maxWidth="sm">
<DialogTitle>Настройки</DialogTitle>
<IconButton aria-label="close" onClick={onOpen} sx={STYLES.CLOSE_BUTTON}>
<Icon>close</Icon>
</IconButton>
<DialogContent sx={STYLES.FILTERS_SCROLL}>
<Box component="section" p={1}>
<RulesSelect
initRule={settings.colorRule.id !== undefined ? settings.colorRule.id : -1}
handleChange={handleColorRuleChange}
sx={STYLES.SELECT}
/>
</Box>
<Box component="section" p={1}>
<Stack direction="row" sx={STYLES.DOCLINK_STACK}>
<FilterInputField
elementCode="attr"
elementValue={settings.statusesSort.attr}
labelText="Поле сортировки"
items={sortAttrs}
onChange={handleSortAttrChange}
MenuProps={{
slotProps: { paper: { sx: STYLES.SELECT_MENU(document.getElementById("attr")?.parentElement.clientWidth) } }
}}
sx={STYLES.SELECT}
/>
<IconButton
title={settings.statusesSort.dest === "asc" ? "По возрастанию" : "По убыванию"}
onClick={() => handleSortDestChange(sortDest[sortDest.indexOf(settings.statusesSort.dest) * -1])}
>
<Icon>{settings.statusesSort.dest === "asc" ? "arrow_upward" : "arrow_downward"}</Icon>
</IconButton>
</Stack>
</Box>
</DialogContent>
<DialogActions sx={STYLES.DIALOG_ACTIONS}>
<Button
variant="text"
onClick={() => {
onSettingsChange(settings);
onOpen();
}}
>
ОК
</Button>
<Button
variant="text"
onClick={() => setSettings(pv => ({ ...pv, statusesSort: { ...pv.statusesSort, attr: "name", dest: "asc" }, colorRule: {} }))}
>
Очистить
</Button>
<Button variant="text" onClick={onOpen}>
Отмена
</Button>
</DialogActions>
</Dialog>
</div>
);
};
//Контроль свойств компонента - Диалог дополнительных настроек
SettingsDialog.propTypes = {
initial: PropTypes.object.isRequired,
onSettingsChange: PropTypes.func.isRequired,
onOpen: PropTypes.func.isRequired
};
//--------------------
//Интерфейс компонента
//--------------------
export { SettingsDialog };

View File

@ -0,0 +1,172 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Карточка статуса событий
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Card, CardHeader, CardContent, Button, IconButton, Icon, Typography, Stack } from "@mui/material"; //Интерфейсные компоненты
import { TaskCard } from "./task_card.js"; //Компонент Карточка события
import { TaskCardSettings } from "./task_card_settings.js"; //Компонент Диалог настройки карточки событий
import { APP_STYLES } from "../../../../app.styles"; //Типовые стили
import { COLORS } from "../layouts.js"; //Цвета статусов
//---------
//Константы
//---------
//Высота заголовка
const TITLE_HEIGHT = "64px";
//Нижний отступ заголовка
const TITLE_PADDING_BOTTOM = "16px";
//Высота фильтра
const FILTER_HEIGHT = "56px";
//Стили
const STYLES = {
STATUS_BLOCK: statusColor => {
return {
width: "350px",
height: `calc(100vh - ${TITLE_HEIGHT} - ${TITLE_PADDING_BOTTOM} - ${FILTER_HEIGHT} - 8px)`,
backgroundColor: statusColor,
padding: "8px"
};
},
BLOCK_OPACITY: isAvailable => {
return isAvailable ? { opacity: 1 } : { opacity: 0.5 };
},
MARK_INFO: {
textAlign: "left",
textOverflow: "ellipsis",
overflow: "hidden",
display: "-webkit-box",
hyphens: "auto",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 1,
maxWidth: "calc(300px)",
width: "-webkit-fill-available",
fontSize: "1.2rem",
cursor: "default"
},
PADDING_0: { padding: 0 },
CARD_CONTENT: {
padding: 0,
paddingRight: "5px",
paddingBottom: "5px !important",
overflowY: "auto",
maxHeight: `calc(100vh - ${TITLE_HEIGHT} - ${TITLE_PADDING_BOTTOM} - ${FILTER_HEIGHT} - 85px)`,
...APP_STYLES.SCROLL
}
};
//---------------
//Тело компонента
//---------------
//Карточка статуса события
const StatusCard = ({
tasks,
status,
settings,
extraData,
filtersType,
isCardAvailable,
onReload,
onDragItemChange,
onTaskDialogOpen,
onNoteDialogOpen,
placeholder
}) => {
//Состояние диалога настройки
const [statusCardSettingsOpen, setStatusCardSettingsOpen] = useState(false);
//Открыть/закрыть диалог настройки
const handleStatusCardSettingsOpen = () => setStatusCardSettingsOpen(!statusCardSettingsOpen);
//Генерация содержимого
return (
<div>
{statusCardSettingsOpen ? (
<TaskCardSettings statuses={tasks.statuses} availableClrs={COLORS} onDialogOpen={handleStatusCardSettingsOpen} />
) : null}
<Card
className="category-card"
sx={{
...STYLES.STATUS_BLOCK(status.color),
...STYLES.BLOCK_OPACITY(isCardAvailable(status.code))
}}
>
<CardHeader
action={
<IconButton aria-label="settings" onClick={handleStatusCardSettingsOpen}>
<Icon>more_vert</Icon>
</IconButton>
}
title={
<Typography sx={STYLES.MARK_INFO} title={status[settings.statusesSort.attr] || status.name} variant="h5">
{status[settings.statusesSort.attr] || status.name}
</Typography>
}
subheader={
<Button
onClick={() => {
onDragItemChange(filtersType, status.code);
onTaskDialogOpen();
}}
>
+ Добавить
</Button>
}
sx={STYLES.PADDING_0}
/>
<CardContent sx={STYLES.CARD_CONTENT}>
<Stack spacing={1}>
{tasks.rows
.filter(item => item.category === status.id)
.map((item, index) => (
<TaskCard
task={item}
avatar={extraData.accounts.find(a => a.agnAbbr === item.sSender).image}
index={index}
onReload={onReload}
key={item.id}
eventPoints={extraData.evPoints}
colorRule={settings.colorRule}
pointSettings={extraData.evPoints.find(p => p.point === status.code)}
onOpenNoteDialog={onNoteDialogOpen}
/>
))}
{placeholder}
</Stack>
</CardContent>
</Card>
</div>
);
};
//Контроль свойств - Карточка статуса события
StatusCard.propTypes = {
tasks: PropTypes.object.isRequired,
status: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
extraData: PropTypes.object.isRequired,
filtersType: PropTypes.string.isRequired,
isCardAvailable: PropTypes.func.isRequired,
onReload: PropTypes.func.isRequired,
onDragItemChange: PropTypes.func.isRequired,
onTaskDialogOpen: PropTypes.func.isRequired,
onNoteDialogOpen: PropTypes.func.isRequired,
placeholder: PropTypes.object.isRequired
};
//--------------------
//Интерфейс компонента
//--------------------
export { StatusCard };

View File

@ -0,0 +1,577 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Карточка события
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext, useCallback } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Draggable } from "react-beautiful-dnd"; //Работа с drag&drop
import { Card, CardHeader, Typography, IconButton, Icon, Box, Menu, MenuItem, CardContent, Avatar, Stack } from "@mui/material"; //Интерфейсные компоненты
import { TaskDialog } from "../task_dialog"; //Форма события
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import { MessagingСtx } from "../../../context/messaging"; //Контекст сообщений
import { EVENT_INDICATORS, indicatorColorRule, bgColorRule } from "../layouts"; //Дополнительная разметка и вёрстка клиентских элементов
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER: { margin: "5px 0px", textAlign: "center" },
MENU_ITEM_DELIMITER: { borderBottom: "1px solid lightgrey" },
CARD: (indicatorClr, bgClr) => {
const i = indicatorClr ? { borderLeft: `solid ${indicatorClr}` } : null;
const bc = bgClr ? { backgroundColor: bgClr } : null;
return { ...i, ...bc };
},
CARD_HEADER_TITLE: {
padding: "4px",
width: "292px",
display: "-webkit-box",
hyphens: "auto",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 2,
overflow: "hidden"
},
CARD_HEADER_DESC: { padding: 0, cursor: "pointer" },
CARD_CONTENT: { padding: "4px !important" },
CARD_CONTENT_BOX: { display: "flex", alignItems: "center" },
ACCOUNT_STACK: { alignItems: "center", marginLeft: "auto" },
SECONDARY_TEXT: {
color: "text.secondary",
fontSize: 14
},
ICON_COLOR: linked => {
return { color: theme => (linked ? EVENT_INDICATORS.LINKED : theme.palette.grey[500]) };
}
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Действия карточки события
const DataCellCardActions = ({
taskRn,
menuItems,
cardActions,
onMethodsMenuButtonClick,
onMethodsMenuClose,
onReload,
eventPoints,
pointSettings,
onOpenNoteDialog
}) => {
return (
<Box sx={STYLES.BOX_ROW}>
<IconButton id={`${taskRn}_menu_button`} aria-haspopup="true" onClick={onMethodsMenuButtonClick}>
<Icon>more_vert</Icon>
</IconButton>
<Menu id={`${taskRn}_menu`} anchorEl={cardActions.anchorMenuMethods} open={cardActions.openMethods} onClose={onMethodsMenuClose}>
{menuItems.map(action => {
if (action.visible)
return (
<MenuItem
sx={action.delimiter ? STYLES.MENU_ITEM_DELIMITER : {}}
key={`${taskRn}_${action.method}`}
onClick={() => {
if (onOpenNoteDialog && action.method === "TASK_STATE_CHANGE") {
action.func(taskRn, action.needReload ? onReload : null, eventPoints, onOpenNoteDialog);
} else if (onOpenNoteDialog && action.method === "TASK_SEND" && pointSettings.addNoteOnSend) {
onOpenNoteDialog(n => action.func(taskRn, action.needReload ? onReload : null, n));
} else {
//Выполняем действие
action.func(taskRn, action.needReload ? onReload : null);
}
//Закрываем меню
onMethodsMenuClose();
}}
>
<Icon>{action.icon}</Icon>
<Typography pl={1}>{action.name}</Typography>
</MenuItem>
);
})}
</Menu>
</Box>
);
};
//Контроль свойств - Действия карточки события
DataCellCardActions.propTypes = {
taskRn: PropTypes.number.isRequired,
menuItems: PropTypes.array.isRequired,
cardActions: PropTypes.object.isRequired,
onMethodsMenuButtonClick: PropTypes.func.isRequired,
onMethodsMenuClose: PropTypes.func.isRequired,
onReload: PropTypes.func,
eventPoints: PropTypes.array,
pointSettings: PropTypes.object,
onOpenNoteDialog: PropTypes.func
};
//-----------
//Тело модуля
//-----------
//Карточка события
const TaskCard = ({ task, avatar, index, onReload, eventPoints, colorRule, pointSettings, onOpenNoteDialog }) => {
//Состояние диалога события
const [taskDialogOpen, setTaskDialogOpen] = useState(false);
//Состояние действий события
const [cardActions, setCardActions] = useState({ anchorMenuMethods: null, openMethods: false });
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Подключение к контексту сообщений
const { showMsgWarn } = useContext(MessagingСtx);
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//Подключение к контексту приложения
const { pOnlineShowDocument } = useContext(ApplicationСtx);
//Удаление контрагента
const deleteTask = useCallback(
async (nEvent, handleReload) => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_DELETE",
args: { NCLNEVENTS: nEvent }
});
//Если требуется перезагрузить данные
if (handleReload) {
handleReload();
}
},
[executeStored]
);
//Возврат в предыдущую точку события
const returnTask = useCallback(
async (nEvent, handleReload) => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_RETURN",
args: { NCLNEVENTS: nEvent }
});
//Если требуется перезагрузить данные
if (handleReload) {
handleReload();
}
},
[executeStored]
);
//По нажатию на открытие меню действий
const handleMethodsMenuButtonClick = event => {
setCardActions(pv => ({ ...pv, anchorMenuMethods: event.currentTarget, openMethods: true }));
};
//При закрытии меню
const handleMethodsMenuClose = () => {
setCardActions(pv => ({ ...pv, anchorMenuMethods: null, openMethods: false }));
};
//По нажатия действия "Редактировать"
const handleTaskEdit = () => {
setTaskDialogOpen(true);
};
//По нажатия действия "Редактировать в разделе"
const handleTaskEditClient = useCallback(
async nEvent => {
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_SELECT",
args: {
NCLNEVENTS: nEvent
}
});
if (data.NIDENT) {
pOnlineShowDictionary({
unitCode: "ClientEvents",
inputParameters: [{ name: "in_Ident", value: data.NIDENT }]
});
}
},
[executeStored, pOnlineShowDictionary]
);
//По нажатию действия "Удалить"
const handleTaskDelete = (nEvent, handleReload) => {
showMsgWarn("Удалить событие?", () => deleteTask(nEvent, handleReload));
};
//По нажатию действия "Выполнить возврат"
const handleTaskReturn = (nEvent, handleReload) => {
showMsgWarn("Выполнить возврат события в предыдущую точку?", () => returnTask(nEvent, handleReload));
};
//По нажатию действия "Примечания"
const handleEventNotesOpen = useCallback(
async nEvent => {
pOnlineShowDictionary({
unitCode: "ClientEventsNotes",
showMethod: "main",
inputParameters: [{ name: "in_PRN", value: nEvent }]
});
},
[pOnlineShowDictionary]
);
//По нажатию действия "Присоединенные документы"
const handleFileLinksOpen = useCallback(
async nEvent => {
pOnlineShowDictionary({
unitCode: "FileLinks",
showMethod: "main_link",
inputParameters: [
{ name: "in_PRN", value: nEvent },
{ name: "in_UNITCODE", value: "ClientEvents" }
]
});
},
[pOnlineShowDictionary]
);
//По нажатию действия "Перейти"
const handleStateChange = useCallback(
async (nEvent, handleReload, evPoints, handleNote) => {
//Выполняем инициализацию параметров
const firstStep = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: {
NSTEP: 1,
NEVENT: nEvent
}
});
if (firstStep) {
//Открываем раздел "Маршруты событий (точки перехода)" для выбора следующей точки
pOnlineShowDictionary({
unitCode: "EventRoutesPointsPasses",
showMethod: "main_passes",
inputParameters: [
{ name: "in_ENVTYPE_CODE", value: firstStep.SEVENT_TYPE },
{ name: "in_ENVSTAT_CODE", value: firstStep.SEVENT_STAT },
{ name: "in_POINT", value: firstStep.NPOINT }
],
callBack: async point => {
//Выполняем проверку необходимости выбора исполнителя
const secondStep = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: {
NIDENT: firstStep.NIDENT,
NSTEP: 2,
NPASS: point.outParameters.out_RN
}
});
const pointSettings = evPoints.find(ep => ep.point === point.outParameters.out_NEXT_POINT);
if (secondStep) {
//Если требуется выбрать получателя
if (secondStep.NSELECT_EXEC === 1) {
//Открываем раздел "Маршруты событий (исполнители в точках)" для выбора исполнителя
pOnlineShowDictionary({
unitCode: "EventRoutesPointExecuters",
showMethod: "executers",
inputParameters: [
{ name: "in_IDENT", value: firstStep.NIDENT },
{ name: "in_EVENT", value: nEvent },
{ name: "in_EVENT_TYPE", value: firstStep.SEVENT_TYPE },
{ name: "in_EVENT_STAT", value: firstStep.SEVENT_STAT },
{ name: "in_INIT_PERSON", value: firstStep.SINIT_PERSON },
{ name: "in_INIT_AUTHNAME", value: firstStep.SINIT_AUTHNAME },
{ name: "in_CLIENT_CLIENT", value: firstStep.SCLIENT_CLIENT },
{ name: "in_CLIENT_PERSON", value: firstStep.SCLIENT_PERSON }
],
callBack: async send => {
//Общие аргументы
const mainArgs = {
NIDENT: firstStep.NIDENT,
NSTEP: 3,
NEVENT: nEvent,
SEVENT_STAT: point.outParameters.out_NEXT_POINT,
SSEND_CLIENT: send.outParameters.out_CLIENT_CODE,
SSEND_DIVISION: send.outParameters.out_DIVISION_CODE,
SSEND_POST: send.outParameters.out_POST_CODE,
SSEND_PERFORM: send.outParameters.out_POST_IN_DIV_CODE,
SSEND_PERSON: send.outParameters.out_PERSON_CODE,
SSEND_STAFFGRP: send.outParameters.out_STAFFGRP_CODE,
SSEND_USER_GROUP: send.outParameters.out_USER_GROUP_CODE,
SSEND_USER_NAME: send.outParameters.out_USER_NAME,
NSEND_PREDEFINED_EXEC: send.outParameters.out_PREDEFINED_EXEC,
NSEND_PREDEFINED_PROC: send.outParameters.out_PREDEFINED_PROC
};
//Выполняем переход к выбранной точке с исполнителем
pointSettings.addNoteOnChst
? handleNote(async n => {
//Если требуется примечание
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: {
...mainArgs,
...{ SNOTE_HEADER: n.header, SNOTE: n.text }
}
});
//Если требуется перезагрузить данные
if (handleReload) {
handleReload();
}
})
: await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: mainArgs
});
//Если требуется перезагрузить данные
if (handleReload && !pointSettings.addNoteOnChst) {
handleReload();
}
}
});
} else {
//Общие аргументы
const mainArgs = {
NIDENT: firstStep.NIDENT,
NSTEP: 3,
NEVENT: nEvent,
SEVENT_STAT: point.outParameters.out_NEXT_POINT
};
//Выполняем переход к выбранной точке с предопределенным исполнителем
pointSettings.addNoteOnChst
? handleNote(async n => {
//Если требуется примечание
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: {
...mainArgs,
...{ SNOTE_HEADER: n.header, SNOTE: n.text }
}
});
//Если требуется перезагрузить данные
if (handleReload) {
handleReload();
}
})
: await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: mainArgs
});
//Если требуется перезагрузить данные
if (handleReload && !pointSettings.addNoteOnChst) {
handleReload();
}
}
}
}
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[executeStored, pOnlineShowDictionary]
);
//Изменение статуса события
const handleSend = useCallback(
async (nEvent, handleReload, note = null) => {
//Выполняем инициализацию параметров
const firstStep = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_SEND",
args: {
NSTEP: 1,
NEVENT: nEvent
}
});
if (firstStep) {
//Открываем раздел "Маршруты событий (исполнители в точках)" для выбора исполнителя
pOnlineShowDictionary({
unitCode: "EventRoutesPointExecuters",
showMethod: "executers",
inputParameters: [
{ name: "in_IDENT", value: firstStep.NIDENT },
{ name: "in_EVENT", value: nEvent },
{ name: "in_PERSON_CODE", value: firstStep.SSEND_PERSON },
{ name: "in_USER_NAME", value: firstStep.SSEND_USER_NAME },
{ name: "in_EVENT_TYPE", value: firstStep.SEVENT_TYPE },
{ name: "in_EVENT_STAT", value: firstStep.SEVENT_STAT },
{ name: "in_INIT_PERSON", value: firstStep.SINIT_PERSON },
{ name: "in_INIT_AUTHNAME", value: firstStep.SINIT_AUTHNAME },
{ name: "in_CLIENT_CLIENT", value: firstStep.SCLIENT_CLIENT },
{ name: "in_CLIENT_PERSON", value: firstStep.SCLIENT_PERSON }
],
callBack: async send => {
//Выполняем проверку необходимости выбора исполнителя
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_SEND",
args: {
NIDENT: firstStep.NIDENT,
NSTEP: 2,
NEVENT: nEvent,
SSEND_CLIENT: send.outParameters.out_CLIENT_CODE,
SSEND_DIVISION: send.outParameters.out_DIVISION_CODE,
SSEND_POST: send.outParameters.out_POST_CODE,
SSEND_PERFORM: send.outParameters.out_POST_IN_DIV_CODE,
SSEND_PERSON: send.outParameters.out_PERSON_CODE,
SSEND_STAFFGRP: send.outParameters.out_STAFFGRP_CODE,
SSEND_USER_GROUP: send.outParameters.out_USER_GROUP_CODE,
SSEND_USER_NAME: send.outParameters.out_USER_NAME,
NSEND_PREDEFINED_EXEC: send.outParameters.out_PREDEFINED_EXEC,
NSEND_PREDEFINED_PROC: send.outParameters.out_PREDEFINED_PROC,
SNOTE_HEADER: note.text ? note.header : null,
SNOTE: note.text ? note.text : null
}
});
//Если требуется перезагрузить данные
if (handleReload) {
handleReload();
}
}
});
}
},
[executeStored, pOnlineShowDictionary]
);
const mItems = [
{ method: "EDIT", name: "Исправить", icon: "edit", visible: false, delimiter: false, needReload: false, func: handleTaskEdit },
{
method: "EDIT_CLIENT",
name: "Исправить в разделе",
icon: "edit_note",
visible: true,
delimiter: false,
needReload: false,
func: handleTaskEditClient
},
{ method: "DELETE", name: "Удалить", icon: "delete", visible: true, delimiter: true, needReload: true, func: handleTaskDelete },
{
method: "TASK_STATE_CHANGE",
name: "Перейти",
icon: "turn_right",
visible: true,
delimiter: false,
needReload: true,
func: handleStateChange
},
{
method: "TASK_RETURN",
name: "Выполнить возврат",
icon: "turn_left",
visible: true,
delimiter: false,
needReload: true,
func: handleTaskReturn
},
{ method: "TASK_SEND", name: "Направить", icon: "send", visible: true, delimiter: true, needReload: true, func: handleSend },
{ method: "NOTES", name: "Примечания", icon: "event_note", visible: true, delimiter: true, needReload: false, func: handleEventNotesOpen },
{
method: "FILE_LINKS",
name: "Присоединенные документы",
icon: "attach_file",
visible: true,
delimiter: false,
needReload: false,
func: handleFileLinksOpen
}
];
//Генерация содержимого
return (
<Box>
{taskDialogOpen ? (
<TaskDialog
taskRn={task.nrn}
taskType={task.stype}
editable={pointSettings.banUpdate ? false : true}
onReload={onReload}
onClose={() => {
setTaskDialogOpen(false);
}}
/>
) : null}
<Draggable draggableId={task.id.toString()} key={task.id} index={index}>
{provided => (
<Card
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
sx={STYLES.CARD(indicatorColorRule(task), colorRule.color ? bgColorRule(task, colorRule) : null)}
>
<CardHeader
title={
<Typography
className="task-info"
sx={STYLES.CARD_HEADER_TITLE}
lang="ru"
onClick={() => {
mItems.find(a => (a.method === "EDIT" ? a.func(task.nrn, a.needReload ? onReload : null) : null));
}}
>
{task.sdescription}
</Typography>
}
sx={STYLES.CARD_HEADER_DESC}
action={
<DataCellCardActions
taskRn={task.nrn}
menuItems={mItems}
cardActions={cardActions}
onMethodsMenuButtonClick={handleMethodsMenuButtonClick}
onMethodsMenuClose={handleMethodsMenuClose}
onReload={onReload}
eventPoints={eventPoints}
pointSettings={pointSettings}
onOpenNoteDialog={onOpenNoteDialog}
/>
}
/>
<CardContent sx={STYLES.CARD_CONTENT}>
<Box sx={STYLES.CARD_CONTENT_BOX}>
<IconButton
title={task.nlinked_rn ? "Событие получено по статусной модели" : null}
onClick={
task.nlinked_rn ? () => pOnlineShowDocument({ unitCode: task.slinked_unit, document: task.nlinked_rn }) : null
}
sx={STYLES.ICON_COLOR(task.nlinked_rn)}
disabled={!task.nlinked_rn}
>
<Icon>assignment</Icon>
</IconButton>
<Typography sx={STYLES.SECONDARY_TEXT}>{task.name}</Typography>
{task.sSender ? (
<Stack direction="row" spacing={0.5} sx={STYLES.ACCOUNT_STACK}>
<Typography sx={STYLES.SECONDARY_TEXT}>{task.sSender}</Typography>
<Avatar src={avatar ? `data:image/png;base64,${avatar}` : null} />
</Stack>
) : null}
</Box>
</CardContent>
</Card>
)}
</Draggable>
</Box>
);
};
//Контроль свойств - Карточка события
TaskCard.propTypes = {
task: PropTypes.object.isRequired,
avatar: PropTypes.string,
index: PropTypes.number.isRequired,
onReload: PropTypes.func,
eventPoints: PropTypes.array,
colorRule: PropTypes.object,
pointSettings: PropTypes.object,
onOpenNoteDialog: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { TaskCard };

View File

@ -0,0 +1,126 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Диалог настройки карточки событий
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
Dialog,
DialogTitle,
IconButton,
Icon,
DialogContent,
DialogActions,
Button,
Box,
FormControl,
InputLabel,
Select,
MenuItem
} from "@mui/material"; //Интерфейсные компоненты
//---------
//Константы
//---------
//Стили
const STYLES = {
DIALOG_ACTIONS: { justifyContent: "end", paddingRight: "24px", paddingLeft: "24px" },
CLOSE_BUTTON: {
position: "absolute",
right: 8,
top: 8,
color: theme => theme.palette.grey[500]
},
BCKG_COLOR: backgroundColor => ({ backgroundColor: backgroundColor })
};
//---------------
//Тело компонента
//---------------
//Диалог настройки карточки событий
const TaskCardSettings = ({ statuses, availableClrs, onDialogOpen }) => {
//Собственное состояние
const [settings, setSettings] = useState({});
//Применение настройки статуса
const handleOk = settings => {
//Считываем статусы
let cloneS = statuses.slice();
//Изменяем статус у выбранного
cloneS[statuses.findIndex(x => x.id === settings.id)] = { ...settings };
setSettings(cloneS);
onDialogOpen();
};
//При изменении значения элемента
const handleSettingsItemChange = e => {
setSettings(pv => ({ ...pv, color: e.target.value }));
};
//Генерация содержимого
return (
<div>
<Dialog open onClose={onDialogOpen} fullWidth maxWidth="sm">
<DialogTitle>Настройки</DialogTitle>
<IconButton aria-label="close" onClick={onDialogOpen} sx={STYLES.CLOSE_BUTTON}>
<Icon>close</Icon>
</IconButton>
<DialogContent>
<Box component="section" p={1}>
<FormControl fullWidth>
<InputLabel id="color-label">Цвет</InputLabel>
<Select
defaultValue={settings.color}
labelId="color-label"
id="color"
label="Цвет"
variant="standard"
sx={STYLES.BCKG_COLOR(settings.color)}
onChange={handleSettingsItemChange}
>
<MenuItem key={0} value={settings.color} sx={STYLES.BCKG_COLOR(settings.color)}>
{settings.color}
</MenuItem>
{availableClrs.map((clr, i) => {
return (
<MenuItem key={i + 1} value={clr} sx={STYLES.BCKG_COLOR(clr)}>
{clr}
</MenuItem>
);
})}
</Select>
</FormControl>
</Box>
</DialogContent>
<DialogActions sx={STYLES.DIALOG_ACTIONS}>
<Button variant="text" onClick={handleOk}>
Применить
</Button>
<Button variant="text" onClick={onDialogOpen}>
Отмена
</Button>
</DialogActions>
</Dialog>
</div>
);
};
//Контроль свойств - Диалог настройки карточки событий
TaskCardSettings.propTypes = {
statuses: PropTypes.arrayOf(PropTypes.object).isRequired,
availableClrs: PropTypes.arrayOf(PropTypes.string).isRequired,
onDialogOpen: PropTypes.func.isRequired
};
//--------------------
//Интерфейс компонента
//--------------------
export { TaskCardSettings };

View File

@ -0,0 +1,154 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент панели: Форма события
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, Typography, Tabs, Tab, InputAdornment, IconButton, Icon } from "@mui/material"; //Интерфейсные компоненты
import { TaskFormTabInfo } from "./task_form_tab_info"; //Вкладка основной информации
import { TaskFormTabExecutor } from "./task_form_tab_executor"; //Вкладка информации об исполнителе
import { TaskFormTabProps } from "./task_form_tab_props"; //Вкладка информации со свойствами
import { useDocsProps } from "../hooks/task_dialog_hooks"; //Хук для получения свойств раздела "События"
import { DP_TYPE_PREFIX } from "../layouts"; //Префикс типа данных свойства
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER: { margin: "5px 0px", textAlign: "center" }
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Свойства вкладки
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`
};
}
//Вкладка информации
function CustomTabPanel(props) {
const { children, value, index, ...other } = props;
return (
<Box role="tabpanel" hidden={value !== index} id={`simple-tabpanel-${index}`} aria-labelledby={`simple-tab-${index}`} {...other}>
{value === index && <Box pt={1}>{children}</Box>}
</Box>
);
}
//Контроль свойств - Вкладка информации
CustomTabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
};
//Формирование кнопки для открытия раздела
export const getInputProps = (onClick, disabled = false, icon = "list") => {
return {
endAdornment: (
<InputAdornment position="end">
<IconButton disabled={disabled} aria-label={`select`} onClick={onClick} edge="end">
<Icon>{icon}</Icon>
</IconButton>
</InputAdornment>
)
};
};
//-----------
//Тело модуля
//-----------
//Форма события
const TaskForm = ({ task, taskType, setTask, editable, onEventNextNumbGet, onDPReady }) => {
//Состояние вкладки
const [tab, setTab] = useState(0);
//Состояние допустимых дополнительных свойств
const [docProps] = useDocsProps(taskType);
//При изменении вкладки
const handleTabChange = (e, newValue) => {
setTab(newValue);
};
//При изменении поля
const handleFieldEdit = e => {
setTask(pv => ({
...pv,
[e.target.id]: e.target.value,
//Связанные значения, если меняется одно, то необходимо обнулить другое
...(e.target.id === "sclnt_clnperson" ? { sclnt_clnclients: "" } : {}),
...(e.target.id === "sclnt_clnclients" ? { sclnt_clnperson: "" } : {})
}));
};
//При изменении свойства
const handlePropEdit = e => {
setTask(pv => ({
...pv,
docProps: { ...pv.docProps, [e.target.id]: e.target.value }
}));
};
//При заполнении всех обязательных свойств
useEffect(() => {
let i = 0;
docProps.props.filter(dp => dp.require === true).map(prop => (!task.docProps[`${DP_TYPE_PREFIX[prop.format]}DP_${prop.rn}`] ? i++ : null));
docProps.loaded && i === 0 ? onDPReady(true) : onDPReady(false);
}, [docProps, onDPReady, task.docProps]);
//Генерация содержимого
return (
<Box sx={STYLES.CONTAINER}>
<Typography pb={1} variant="h6">
{task.nrn ? "Исправление события" : "Добавление события"}
</Typography>
<Tabs value={tab} onChange={handleTabChange} aria-label="tabs of values">
<Tab label="Событие" {...a11yProps(0)} />
<Tab label="Исполнитель" {...a11yProps(1)} />
{docProps.props.length > 0 ? <Tab label="Свойства" {...a11yProps(2)} /> : null}
</Tabs>
<CustomTabPanel value={tab} index={0}>
<TaskFormTabInfo task={task} editable={editable} onFieldEdit={handleFieldEdit} onEventNextNumbGet={onEventNextNumbGet} />
</CustomTabPanel>
<CustomTabPanel value={tab} index={1}>
<TaskFormTabExecutor task={task} onFieldEdit={handleFieldEdit} />
</CustomTabPanel>
{docProps.props.length > 0 ? (
<CustomTabPanel value={tab} index={2}>
<TaskFormTabProps task={task} taskType={taskType} docProps={docProps} onPropEdit={handlePropEdit} />
</CustomTabPanel>
) : null}
</Box>
);
};
//Контроль свойств - Форма события
TaskForm.propTypes = {
task: PropTypes.object.isRequired,
taskType: PropTypes.string.isRequired,
setTask: PropTypes.func.isRequired,
editable: PropTypes.bool.isRequired,
onEventNextNumbGet: PropTypes.func.isRequired,
onDPReady: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskForm };

View File

@ -0,0 +1,176 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Вкладка информации об исполнителе
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useContext, useCallback } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, TextField } from "@mui/material"; //Интерфейсные компоненты
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
import { getInputProps } from "./task_form"; //Формирование кнопки доступа к разделу
import dayjs from "dayjs"; //Работа с датами
import customParseFormat from "dayjs/plugin/customParseFormat"; //Настройка пользовательского формата даты
//---------
//Константы
//---------
//Стили
const STYLES = {
BOX_WITH_LEGEND: { border: "1px solid #939393" },
BOX_SINGLE_COLUMN: { display: "flex", flexDirection: "column", gap: "10px" },
BOX_LEFT_ALIGN: { display: "flex", justifyContent: "flex-start" },
LEGEND: { textAlign: "left" }
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Подключение настройки пользовательского формата даты
dayjs.extend(customParseFormat);
//-----------
//Тело модуля
//-----------
//Вкладка информации об исполнителе
const TaskFormTabExecutor = ({ task, onFieldEdit }) => {
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//Отображение раздела "Сотрудники" для инициатора
const handleClientPersonOpen = useCallback(async () => {
pOnlineShowDictionary({
unitCode: "ClientPersons",
showMethod: "main",
inputParameters: [{ name: "in_CODE", value: task.sinit_clnperson }],
callBack: res => {
res.success
? onFieldEdit({
target: {
id: "sinit_clnperson",
value: res.outParameters.out_CODE
}
})
: null;
}
});
}, [onFieldEdit, pOnlineShowDictionary, task.sinit_clnperson]);
//Генерация содержимого
return (
<Box>
<Box sx={{ ...STYLES.BOX_WITH_LEGEND, ...STYLES.BOX_LEFT_ALIGN }} component="fieldset">
<legend style={STYLES.LEGEND}>Планирование</legend>
<TextField
id="dplan_date"
label="Начало работ"
InputLabelProps={{ shrink: true }}
type="datetime-local"
variant="standard"
value={task.dplan_date ? dayjs(task.dplan_date, "DD.MM.YYYY HH:mm").format("YYYY-MM-DD HH:mm") : ""}
onChange={onFieldEdit}
disabled={task.isUpdate}
></TextField>
</Box>
<Box sx={{ ...STYLES.BOX_WITH_LEGEND, ...STYLES.BOX_SINGLE_COLUMN }} component="fieldset">
<legend style={STYLES.LEGEND}>Инициатор</legend>
<TextField
id="sinit_clnperson"
label="Сотрудник"
value={task.sinit_clnperson}
variant="standard"
onChange={onFieldEdit}
disabled={task.isUpdate}
InputProps={getInputProps(() => handleClientPersonOpen(), task.isUpdate)}
></TextField>
<TextField
id="sinit_user"
label="Пользователь"
value={task.sinit_user}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField
id="sinit_reason"
label="Основание"
value={task.sinit_reason}
variant="standard"
onChange={onFieldEdit}
disabled={task.isUpdate}
></TextField>
</Box>
<Box sx={{ ...STYLES.BOX_WITH_LEGEND, ...STYLES.BOX_SINGLE_COLUMN }} component="fieldset">
<legend style={STYLES.LEGEND}>Направить</legend>
<TextField
id="sto_company"
label="Организация"
value={task.sto_company}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField
id="sto_department"
label="Подразделение"
value={task.sto_department}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField id="sto_clnpost" label="Должность" value={task.sto_clnpost} variant="standard" onChange={onFieldEdit} disabled></TextField>
<TextField
id="sto_clnpsdep"
label="Штатная должность"
value={task.sto_clnpsdep}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField
id="sto_clnperson"
label="Сотрудник"
value={task.sto_clnperson}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField
id="sto_fcstaffgrp"
label="Нештатная должность"
value={task.sto_fcstaffgrp}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField id="sto_user" label="Пользователь" value={task.sto_user} variant="standard" onChange={onFieldEdit} disabled></TextField>
<TextField
id="sto_usergrp"
label="Группа пользователей"
value={task.sto_usergrp}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
</Box>
</Box>
);
};
//Контроль свойств - Вкладка информации об исполнителе
TaskFormTabExecutor.propTypes = {
task: PropTypes.object.isRequired,
onFieldEdit: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskFormTabExecutor };

View File

@ -0,0 +1,225 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Вкладка основной информации
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useContext, useCallback } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, TextField } from "@mui/material"; //Интерфейсные компоненты
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
import { getInputProps } from "./task_form"; //Формирование кнопки доступа к разделу
//---------
//Константы
//---------
//Стили
const STYLES = {
BOX_WITH_LEGEND: { border: "1px solid #939393" },
BOX_SINGLE_COLUMN: { display: "flex", flexDirection: "column", gap: "10px" },
BOX_FEW_COLUMNS: { display: "flex", flexWrap: "wrap", justifyContent: "space-between" },
LEGEND: { textAlign: "left" },
TEXT_FIELD: (widthVal, greyDisabled = false) => ({
margin: "4px",
...(widthVal ? { width: widthVal } : {}),
...(greyDisabled
? {
"& .MuiInputBase-input.Mui-disabled": {
WebkitTextFillColor: "rgba(0, 0, 0, 0.87)"
},
"& .MuiInputLabel-root.Mui-disabled": {
WebkitTextFillColor: "rgba(0, 0, 0, 0.6)"
}
}
: {})
})
};
//-----------
//Тело модуля
//-----------
//Вкладка основной информации
const TaskFormTabInfo = ({ task, editable, onFieldEdit, onEventNextNumbGet }) => {
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//Отображение раздела "Каталоги" для событий
const handleCrnOpen = useCallback(async () => {
pOnlineShowDictionary({
unitCode: "CatalogTree",
showMethod: "main",
inputParameters: [
{ name: "in_DOCNAME", value: "ClientEvents" },
{ name: "in_NAME", value: task.scrn }
],
callBack: res => {
res.success
? onFieldEdit({
target: {
id: "scrn",
value: res.outParameters.out_NAME
}
})
: null;
}
});
}, [onFieldEdit, pOnlineShowDictionary, task.scrn]);
//Отображение раздела "Сотрудники" для клиента
const handleClientPersonOpen = useCallback(async () => {
pOnlineShowDictionary({
unitCode: "ClientPersons",
showMethod: "main",
inputParameters: [{ name: "in_CODE", value: task.sclnt_clnperson }],
callBack: res => {
res.success
? onFieldEdit({
target: {
id: "sclnt_clnperson",
value: res.outParameters.out_CODE
}
})
: null;
}
});
}, [onFieldEdit, pOnlineShowDictionary, task.sclnt_clnperson]);
//Отображение раздела "Клиенты"
const handleClientClientsOpen = useCallback(async () => {
pOnlineShowDictionary({
unitCode: "ClientClients",
showMethod: "main",
inputParameters: [{ name: "in_CLIENT_CODE", value: task.sclnt_clnclients }],
callBack: res => {
res.success
? onFieldEdit({
target: {
id: "sclnt_clnclients",
value: res.outParameters.out_CLIENT_CODE
}
})
: null;
}
});
}, [onFieldEdit, pOnlineShowDictionary, task.sclnt_clnclients]);
//Генерация содержимого
return (
<Box>
<Box sx={STYLES.BOX_WITH_LEGEND} component="fieldset">
<legend style={STYLES.LEGEND}>Событие</legend>
<Box sx={STYLES.BOX_FEW_COLUMNS}>
<TextField
sx={STYLES.TEXT_FIELD()}
id="scrn"
label="Каталог"
fullWidth
value={task.scrn}
variant="standard"
onChange={onFieldEdit}
InputProps={getInputProps(handleCrnOpen)}
required
disabled={task.isUpdate}
/>
<TextField
sx={STYLES.TEXT_FIELD("225px")}
id="sprefix"
label="Префикс"
value={task.sprefix}
variant="standard"
onChange={onFieldEdit}
required
disabled={task.isUpdate}
></TextField>
<TextField
sx={STYLES.TEXT_FIELD("225px")}
id="snumber"
label="Номер"
value={task.snumber}
variant="standard"
onChange={onFieldEdit}
required
disabled={task.isUpdate}
InputProps={getInputProps(onEventNextNumbGet, !task.sprefix || task.isUpdate, "refresh")}
></TextField>
<TextField
sx={STYLES.TEXT_FIELD("225px", !task.isUpdate)}
id="stype"
label="Тип"
value={task.stype}
variant="standard"
onChange={onFieldEdit}
disabled
required
></TextField>
<TextField
sx={STYLES.TEXT_FIELD("225px", !task.isUpdate)}
id="sstatus"
label="Статус"
value={task.sstatus}
variant="standard"
disabled
required
onChange={onFieldEdit}
></TextField>
<TextField
sx={STYLES.TEXT_FIELD()}
fullWidth
id="sdescription"
label="Описание"
value={task.sdescription}
variant="standard"
onChange={onFieldEdit}
disabled={!task.stype || !editable}
required
multiline
minRows={7}
maxRows={7}
></TextField>
</Box>
</Box>
<Box sx={{ ...STYLES.BOX_WITH_LEGEND, ...STYLES.BOX_SINGLE_COLUMN }} component="fieldset">
<legend style={STYLES.LEGEND}>Клиент</legend>
<TextField
sx={STYLES.TEXT_FIELD()}
id="sclnt_clnclients"
label="Организация"
value={task.sclnt_clnclients}
variant="standard"
onChange={onFieldEdit}
disabled={!task.stype}
InputProps={getInputProps(handleClientClientsOpen, !task.stype)}
></TextField>
<TextField
sx={STYLES.TEXT_FIELD()}
id="sclnt_clnperson"
label="Сотрудник"
value={task.sclnt_clnperson}
variant="standard"
onChange={onFieldEdit}
disabled={!task.stype}
InputProps={getInputProps(() => handleClientPersonOpen(), !task.stype)}
></TextField>
</Box>
</Box>
);
};
//Контроль свойств - Вкладка основной информации
TaskFormTabInfo.propTypes = {
task: PropTypes.object.isRequired,
editable: PropTypes.bool.isRequired,
onFieldEdit: PropTypes.func.isRequired,
onEventNextNumbGet: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskFormTabInfo };

View File

@ -0,0 +1,184 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Вкладка информации со свойствами
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, TextField } from "@mui/material"; //Интерфейсные компоненты
import { getInputProps } from "./task_form"; //Формирование кнопки доступа к разделу
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
import dayjs from "dayjs"; //Работа с датами
import customParseFormat from "dayjs/plugin/customParseFormat"; //Настройка пользовательского формата даты
import { DP_DEFAULT_VALUE, DP_TYPE_PREFIX, DP_IN_VALUE, DP_RETURN_VALUE, validationError, timeFromSqlFormat } from "../layouts"; //Дополнительная разметка и вёрстка клиентских элементов
//---------
//Константы
//---------
//Стили
const STYLES = {
BOX_WITH_LEGEND: { border: "1px solid #939393" },
LEGEND: { textAlign: "left" },
TEXT_FIELD: (widthVal, greyDisabled = false) => ({
margin: "4px",
...(widthVal ? { width: widthVal } : {}),
...(greyDisabled
? {
"& .MuiInputBase-input.Mui-disabled": {
WebkitTextFillColor: "rgba(0, 0, 0, 0.87)"
},
"& .MuiInputLabel-root.Mui-disabled": {
WebkitTextFillColor: "rgba(0, 0, 0, 0.6)"
}
}
: {})
})
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Подключение настройки пользовательского формата даты
dayjs.extend(customParseFormat);
//-----------
//Тело модуля
//-----------
//Вкладка информации со свойствами
const TaskFormTabProps = ({ task, docProps, onPropEdit }) => {
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//Выбор из словаря или дополнительного словаря
const handleDict = async (dp, curValue = null) => {
dp.entryType === 1
? pOnlineShowDictionary({
unitCode: dp.unitcode,
showMethod: dp.showMethodCode,
inputParameters: dp.paramRn ? [{ name: dp.paramIn, value: curValue }] : null,
callBack: res => {
res.success
? onPropEdit({
target: {
id: `${DP_TYPE_PREFIX[dp.format]}DP_${dp.rn}`,
value: res.outParameters[dp.paramOut]
}
})
: null;
}
})
: pOnlineShowDictionary({
unitCode: "ExtraDictionaries",
showMethod: "values",
inputParameters: [
{ name: "pos_rn", value: dp.extraDictRn },
{ name: DP_IN_VALUE[dp.format], value: curValue }
],
callBack: res => {
res.success
? onPropEdit({
target: {
id: `${DP_TYPE_PREFIX[dp.format]}DP_${dp.rn}`,
value: res.outParameters[DP_RETURN_VALUE[dp.format]]
}
})
: null;
}
});
};
//Инициализация дополнительного свойства
const initProp = prop => {
//Значение свойства
const value = task.docProps[`${DP_TYPE_PREFIX[prop.format]}DP_${prop.rn}`];
if (
(task.nrn || task.docProps[`${DP_TYPE_PREFIX[prop.format]}DP_${prop.rn}`]) &&
task.docProps[`${DP_TYPE_PREFIX[prop.format]}DP_${prop.rn}`] !== undefined
) {
//Строка или число
if (prop.format < 2) return prop.numPrecision ? String(value).replace(".", ",") : value;
//Дата
else if (prop.format === 2) {
//Дата без времени
if (prop.dataSubtype === 0) return dayjs(value).format("YYYY-MM-DD");
//Дата + время без секунд
else if (prop.dataSubtype === 1) return dayjs(value).format("YYYY-MM-DD HH:mm");
//Дата + время с секундами
else return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
}
//Время
else {
return timeFromSqlFormat(value);
}
} else if (task.nrn) {
return "";
} else return prop[DP_DEFAULT_VALUE[prop.format]];
};
//Генерация содержимого
return (
<Box>
<Box sx={STYLES.BOX_WITH_LEGEND} component="fieldset">
{docProps.props.map(dp => {
return dp.showInGrid ? (
<TextField
error={
!validationError(
task.docProps[`${DP_TYPE_PREFIX[dp.format]}DP_${dp.rn}`],
dp.format,
dp.numWidth,
dp.numPrecision,
dp.strWidth
)
}
key={dp.id}
sx={STYLES.TEXT_FIELD()}
id={`${DP_TYPE_PREFIX[dp.format]}DP_${dp.rn}`}
type={dp.format < 2 ? "string" : dp.format === 2 ? (dp.dataSubtype === 0 ? "date" : "datetime-local") : "time"}
label={dp.name}
fullWidth
value={initProp(dp)}
variant="standard"
onChange={onPropEdit}
inputProps={(dp.format === 2 && dp.dataSubtype === 2) || (dp.format === 3 && dp.dataSubtype === 1) ? { step: 1 } : {}}
InputProps={
dp.entryType > 0
? getInputProps(() => handleDict(dp, task.docProps[`${DP_TYPE_PREFIX[dp.format]}DP_${dp.rn}`]))
: null
}
InputLabelProps={
dp.format < 2
? {}
: {
shrink: true
}
}
required={dp.require}
disabled={dp.readonly}
/>
) : null;
})}
</Box>
</Box>
);
};
//Контроль свойств - Вкладка информации со свойствами
TaskFormTabProps.propTypes = {
task: PropTypes.object.isRequired,
docProps: PropTypes.object.isRequired,
onPropEdit: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskFormTabProps };

View File

@ -0,0 +1,204 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Фильтр отбора
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Chip, Stack, Icon, IconButton, Box, Menu, MenuItem, Typography } from "@mui/material"; //Интерфейсные компоненты
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { FilterDialog } from "./components/filter_dialog.js"; //Диалог фильтра
//---------
//Константы
//---------
//Стили
const STYLES = {
ICON_ORDERS: orders => {
return orders.length > 0 ? { color: "#1976d2" } : {};
},
ORDER_MENU: {
width: "260px"
},
ORDER_MENU_ITEM: {
display: "flex",
justifyContent: "space-between"
},
FILTERS_STACK: {
paddingBottom: "5px",
overflowX: "auto",
...APP_STYLES.SCROLL
},
FILTER_MAXW: { maxWidth: "99vw" }
};
//--------------------------
//Вспомогательные компоненты
//--------------------------
//Элемент меню сортировок
const SortMenuItem = ({ item, caption, orders, onOrderChanged }) => {
//Кнопка сортировки
const order = orders.find(o => o.name == item);
return (
<MenuItem sx={STYLES.ORDER_MENU_ITEM} key={item} onClick={() => onOrderChanged(item)}>
<Typography>{caption}</Typography>
{order ? order.direction === "ASC" ? <Icon>arrow_upward</Icon> : <Icon>arrow_downward</Icon> : null}
</MenuItem>
);
};
//Контроль свойств компонента - Элемент меню сортировок
SortMenuItem.propTypes = {
item: PropTypes.string.isRequired,
caption: PropTypes.string.isRequired,
orders: PropTypes.array,
onOrderChanged: PropTypes.func.isRequired
};
//Меню сортировок
const SortMenu = ({ menuOrders, onOrdersMenuClose, orders, onOrderChanged }) => {
return (
<Menu
id={`sort_menu`}
anchorEl={menuOrders.anchorMenuOrders}
open={menuOrders.openOrders}
onClose={onOrdersMenuClose}
MenuListProps={{ sx: STYLES.ORDER_MENU }}
>
<SortMenuItem item={"DCHANGE_DATE"} caption={"Дата последнего изменения"} orders={orders} onOrderChanged={onOrderChanged} />
<SortMenuItem item={"DPLAN_DATE"} caption={"Дата начала работ"} orders={orders} onOrderChanged={onOrderChanged} />
<SortMenuItem item={"SPREF_NUMB"} caption={"Номер"} orders={orders} onOrderChanged={onOrderChanged} />
</Menu>
);
};
//Контроль свойств компонента - Меню сортировок
SortMenu.propTypes = {
menuOrders: PropTypes.object.isRequired,
onOrdersMenuClose: PropTypes.func.isRequired,
orders: PropTypes.array,
onOrderChanged: PropTypes.func.isRequired
};
//Элемент фильтра
const FilterItem = ({ caption, value, onClick }) => {
//При нажатии на элемент
const handleClick = () => (onClick ? onClick() : null);
//Генерация содержимого
return (
<Chip
label={
<Stack direction={"row"} alignItems={"center"}>
<strong>{caption}</strong>
{value ? `:\u00A0${value}` : null}
</Stack>
}
variant="outlined"
onClick={handleClick}
/>
);
};
//Контроль свойств компонента - Элемент фильтра
FilterItem.propTypes = {
caption: PropTypes.string.isRequired,
value: PropTypes.any,
onClick: PropTypes.func
};
//---------------
//Тело компонента
//---------------
//Фильтр отбора
const Filter = ({
isFilterDialogOpen,
filter,
docs,
selectedDoc,
onFilterChange,
getDocLinks,
onFilterOpen,
onFilterClose,
onReload,
orders,
onOrderChanged,
...other
}) => {
//Состояние меню сортировки
const [menuOrders, setMenuOrders] = useState({ anchorMenuOrders: null, openOrders: false });
//По нажатию на открытие меню сортировки
const handleOrdersMenuButtonClick = event => {
setMenuOrders(pv => ({ ...pv, anchorMenuOrders: event.currentTarget, openOrders: true }));
};
//При закрытии меню
const handleOrdersMenuClose = () => {
setMenuOrders(pv => ({ ...pv, anchorMenuOrders: null, openOrders: false }));
};
//Генерация содержимого
return (
<div>
{isFilterDialogOpen ? (
<FilterDialog initial={filter} docs={docs} onFilterChange={onFilterChange} onFilterOpen={onFilterClose} getDocLinks={getDocLinks} />
) : null}
<Box {...other}>
<Stack direction="row" spacing={1} p={1} alignItems={"center"} sx={STYLES.FILTER_MAXW}>
<IconButton title="Обновить" onClick={onReload}>
<Icon>refresh</Icon>
</IconButton>
<IconButton title="Сортировать" sx={STYLES.ICON_ORDERS(orders)} onClick={handleOrdersMenuButtonClick}>
<Icon>sort</Icon>
</IconButton>
<IconButton title="Фильтр" onClick={onFilterOpen}>
<Icon>filter_alt</Icon>
</IconButton>
<Stack direction="row" spacing={1} alignItems={"center"} sx={STYLES.FILTERS_STACK}>
{filter.evState ? <FilterItem caption={"Состояние"} value={filter.evState} onClick={onFilterOpen} /> : null}
{filter.type ? <FilterItem caption={"Тип"} value={filter.type} onClick={onFilterOpen} /> : null}
{filter.catalog ? <FilterItem caption={"Каталог"} value={filter.catalog} onClick={onFilterOpen} /> : null}
{filter.wSubcatalogs ? <FilterItem caption={"Включая подкаталоги"} onClick={onFilterOpen} /> : null}
{filter.sendPerson ? <FilterItem caption={"Исполнитель"} value={filter.sendPerson} onClick={onFilterOpen} /> : null}
{filter.sendDivision ? <FilterItem caption={"Подразделение"} value={filter.sendDivision} onClick={onFilterOpen} /> : null}
{filter.sendUsrGrp ? <FilterItem caption={"Группа пользователей"} value={filter.sendUsrGrp} onClick={onFilterOpen} /> : null}
{filter.docLink && selectedDoc ? (
<FilterItem caption={"Учётный документ"} value={selectedDoc.descr} onClick={onFilterOpen} />
) : null}
</Stack>
</Stack>
<SortMenu menuOrders={menuOrders} onOrdersMenuClose={handleOrdersMenuClose} orders={orders} onOrderChanged={onOrderChanged} />
</Box>
</div>
);
};
//Контроль свойств компонента - Фильтр отбора
Filter.propTypes = {
isFilterDialogOpen: PropTypes.bool.isRequired,
filter: PropTypes.object.isRequired,
docs: PropTypes.arrayOf(PropTypes.object),
selectedDoc: PropTypes.object,
onFilterChange: PropTypes.func.isRequired,
getDocLinks: PropTypes.func,
onFilterOpen: PropTypes.func.isRequired,
onFilterClose: PropTypes.func.isRequired,
onReload: PropTypes.func.isRequired,
orders: PropTypes.array,
onOrderChanged: PropTypes.func.isRequired
};
//--------------------
//Интерфейс компонента
//--------------------
export { Filter };

View File

@ -0,0 +1,126 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Пользовательские хуки: Хуки фильтра
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useEffect, useCallback } from "react"; //Классы React
import { EVENT_STATES } from "../layouts"; //Перечисление состояний события
//-----------
//Тело модуля
//-----------
//Хук фильтра
const useFilters = filterOpen => {
//Состояние фильтра
const [filters, setFilters] = useState({
loaded: false,
isSetByUser: false,
needSave: false,
values: {
evState: EVENT_STATES[1],
type: "",
catalog: "",
crn: "",
wSubcatalogs: false,
sendPerson: "",
sendDivision: "",
sendUsrGrp: "",
docLink: ""
},
fArray: [
{ name: "NCLOSED", from: 0, to: 1 },
{ name: "SEVTYPE_CODE", from: "", to: "" },
{ name: "NCRN", from: "", to: "" },
{ name: "SSEND_PERSON", from: "", to: "" },
{ name: "SSEND_DIVISION", from: "", to: "" },
{ name: "SSEND_USRGRP", from: "", to: "" },
{ name: "NLINKED_RN", from: "", to: "" }
]
});
//Изменение фильтра
const handleFiltersChange = filters => {
setFilterValues(filters);
};
//Установить значение фильтра
const setFilterValues = (values, ns = true) => {
//Считываем массив фильтров
let filtersArr = filters.fArray.slice();
//Состояние
if (values.evState) {
if (values.evState === EVENT_STATES[0]) {
filtersArr.find(f => f.name === "NCLOSED").from = 0;
filtersArr.find(f => f.name === "NCLOSED").to = 1;
} else if (values.evState === EVENT_STATES[1]) {
filtersArr.find(f => f.name === "NCLOSED").from = 0;
filtersArr.find(f => f.name === "NCLOSED").to = 0;
} else if (values.evState === EVENT_STATES[2]) {
filtersArr.find(f => f.name === "NCLOSED").from = 1;
filtersArr.find(f => f.name === "NCLOSED").to = 1;
}
}
//Тип
filtersArr.find(f => f.name === "SEVTYPE_CODE").from = values.type ? values.type : null;
//Каталог
filtersArr.find(f => f.name === "NCRN").from = values.crn ? values.crn : null;
//Исполнитель
filtersArr.find(f => f.name === "SSEND_PERSON").from = values.sendPerson ? values.sendPerson : null;
//Подразделение
filtersArr.find(f => f.name === "SSEND_DIVISION").from = values.sendDivision ? values.sendDivision : null;
//Группа пользователей
filtersArr.find(f => f.name === "SSEND_USRGRP").from = values.sendUsrGrp ? values.sendUsrGrp : null;
//Учётный документ
filtersArr.find(f => f.name === "NLINKED_RN").from = values.docLink ? values.docLink : null;
//Устанавливаем фильтры
setFilters({ loaded: true, isSetByUser: true, needSave: ns, values: values, fArray: filtersArr });
};
//Загрузка значений фильтра из локального хранилища браузера
const loadLocalFilter = useCallback(async () => {
let vs = { ...filters.values };
if (localStorage.getItem("type")) {
Object.keys(vs).map(function (k) {
if (k === "wSubcatalogs") vs[k] = localStorage.getItem(k) === "true";
else k !== "docLink" ? (vs[k] = localStorage.getItem(k)) : null;
});
setFilterValues(vs, false);
filterOpen(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//При закрытии панели
useEffect(() => {
filters.needSave
? window.addEventListener("beforeunload", function () {
Object.keys(filters.values).map(function (k) {
k !== "docLink" ? localStorage.setItem(k, filters.values[k] ? filters.values[k] : "") : null;
});
})
: null;
}, [filters.needSave, filters.values]);
//При отсутствии пользовательских настроек фильтра
useEffect(() => {
if (!filters.isSetByUser) filterOpen(true);
}, [filterOpen, filters.isSetByUser]);
//При подключении к странице
useEffect(() => {
localStorage.length ? loadLocalFilter() : null;
}, [loadLocalFilter]);
return [filters, handleFiltersChange];
};
//----------------
//Интерфейс модуля
//----------------
export { useFilters };

View File

@ -0,0 +1,531 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Пользовательские хуки: Хуки основных данных
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect, useCallback } from "react"; //Классы React
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import { object2Base64XML } from "../../../core/utils"; //Вспомогательные функции
import { arrayFormer, randomColor } from "../layouts"; //Формировщик массива и формирование случайного цвета
//-----------
//Тело модуля
//-----------
//Хук получения событий
const useTasks = ({ filters, orders, extraData, getDocLinks }) => {
//Состояние событий
const [tasks, setTasks] = useState({
groupsLoaded: false,
tasksLoaded: false,
rows: [],
statuses: [],
reload: true
});
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//Надо обновить данные
const needUpdateTasks = () => setTasks(pv => ({ ...pv, groupsLoaded: false, tasksLoaded: false, reload: true }));
//Инициализация параметров события
const initTask = (id, gp, task) => {
//Добавление дополнительных свойств
let newDocProps = {};
Object.keys(task)
.filter(k => k.includes("DP_"))
.map(dp => (newDocProps = { ...newDocProps, [dp]: task[dp] }));
return {
id: id,
name: task.SPREF_NUMB,
category: gp,
nrn: task.NRN,
scrn: "",
sprefix: task.SEVPREF,
snumber: task.SEVNUMB,
stype: task.SEVTYPE_CODE,
sstatus: task.SEVSTAT_NAME,
sdescription: task.SEVDESCR,
sclnt_clnclients: "",
sclnt_clnperson: "",
dchange_date: task.DCHANGE_DATE,
dstart_date: task.DREG_DATE,
dexpire_date: task.DEXPIRE_DATE,
dplan_date: task.DPLAN_DATE,
sinit_clnperson: task.SINIT_PERSON,
sinit_user: "",
sinit_reason: "",
//SEND_CLIENT
sto_company: "",
//SEND_DIVISION
sto_department: task.SSEND_DIVISION,
//SEND_POST
sto_clnpost: "",
//SEND_PERFORM
sto_clnpsdep: "",
//SEND_PERSON
sto_clnperson: task.SSEND_PERSON,
//SEND_STAFFGRP
sto_fcstaffgrp: "",
//SEND_USER_AUTHID
sto_user: "",
//SEND_USER_GROUP
sto_usergrp: task.SSEND_USRGRP,
sSender: task.SSENDER,
scurrent_user: "",
slinked_unit: task.SLINKED_UNIT,
nlinked_rn: task.NLINKED_RN,
docProps: newDocProps
};
};
//Изменение статуса события (переносом)
const handleStateChange = useCallback(
async (nEvent, sNextStat, note) => {
try {
//Выполняем инициализацию параметров
const firstStep = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: {
NSTEP: 1,
NEVENT: nEvent,
SNEXT_STAT: sNextStat
}
});
if (firstStep) {
//Если требуется выбрать получателя
if (firstStep.NSELECT_EXEC === 1) {
//Открываем раздел "Маршруты событий (исполнители в точках)" для выбора исполнителя
pOnlineShowDictionary({
unitCode: "EventRoutesPointExecuters",
showMethod: "executers",
inputParameters: [
{ name: "in_IDENT", value: firstStep.NIDENT },
{ name: "in_EVENT", value: nEvent },
{ name: "in_EVENT_TYPE", value: firstStep.SEVENT_TYPE },
{ name: "in_EVENT_STAT", value: firstStep.SEVENT_STAT },
{ name: "in_INIT_PERSON", value: firstStep.SINIT_PERSON },
{ name: "in_INIT_AUTHNAME", value: firstStep.SINIT_AUTHNAME },
{ name: "in_CLIENT_CLIENT", value: firstStep.SCLIENT_CLIENT },
{ name: "in_CLIENT_PERSON", value: firstStep.SCLIENT_PERSON }
],
callBack: async send => {
//Общие аргументы
const mainArgs = {
NIDENT: firstStep.NIDENT,
NSTEP: 3,
NEVENT: nEvent,
SEVENT_STAT: firstStep.SEVENT_STAT,
SSEND_CLIENT: send.outParameters.out_CLIENT_CODE,
SSEND_DIVISION: send.outParameters.out_DIVISION_CODE,
SSEND_POST: send.outParameters.out_POST_CODE,
SSEND_PERFORM: send.outParameters.out_POST_IN_DIV_CODE,
SSEND_PERSON: send.outParameters.out_PERSON_CODE,
SSEND_STAFFGRP: send.outParameters.out_STAFFGRP_CODE,
SSEND_USER_GROUP: send.outParameters.out_USER_GROUP_CODE,
SSEND_USER_NAME: send.outParameters.out_USER_NAME,
NSEND_PREDEFINED_EXEC: send.outParameters.out_PREDEFINED_EXEC,
NSEND_PREDEFINED_PROC: send.outParameters.out_PREDEFINED_PROC
};
//Выполняем переход к выбранной точке с исполнителем
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: note
? {
...mainArgs,
SNOTE_HEADER: note.header,
SNOTE: note.text
}
: mainArgs
});
//Необходимо обновить данные
setTasks(pv => ({ ...pv, reload: true }));
}
});
} else {
//Общие аргументы
const mainArgs = { NIDENT: firstStep.NIDENT, NSTEP: 3, NEVENT: nEvent, SEVENT_STAT: firstStep.SEVENT_STAT };
//Выполняем переход к выбранной точке с предопределенным исполнителем
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: note
? {
...mainArgs,
...{ SNOTE_HEADER: note.header, SNOTE: note.text }
}
: mainArgs
});
//Необходимо обновить данные
setTasks(pv => ({ ...pv, reload: true }));
}
}
} catch (e) {
//Необходимо обновить данные
setTasks(pv => ({ ...pv, reload: true }));
}
},
[executeStored, pOnlineShowDictionary]
);
//Надо обновить события
const handleReload = useCallback(() => {
setTasks(pv => ({ ...pv, reload: true }));
}, []);
//Взаимодействие с событием (через перенос)
const onDragEnd = useCallback(
(result, eventPoints, openNoteDialog) => {
//Определяем нужные параметры
const { source, destination } = result;
//Если путь не указан
if (!destination) {
return;
}
//Если происходит изменение статуса
if (destination.droppableId !== source.droppableId) {
//Считываем строку, у которой изменяется статус
let row = tasks.rows.find(f => f.id === parseInt(result.draggableId));
//Формируем события с учетом изменения
let rows = tasks.rows.map(task =>
task.id === parseInt(result.draggableId)
? {
...task,
category: parseInt(result.destination.droppableId)
}
: task
);
//Мнемокод точки назначения
const destCode = tasks.statuses.find(s => s.id == destination.droppableId).code;
//Получение настройки точки назначения
const pointSettings = eventPoints.find(ep => ep.point === destCode);
//Если необходимо примечание при переходе
if (pointSettings.addNoteOnChst) {
//Изменяем статус события с добавлением примечания
openNoteDialog(n => {
setTasks(pv => ({ ...pv, rows: [...rows] }));
handleStateChange(row.nrn, destCode, n);
});
}
//Изменяем статус события
else {
//Переинициализируем строки с учетом изменений (для визуального отображения)
setTasks(pv => ({ ...pv, rows: [...rows] }));
handleStateChange(row.nrn, destCode);
}
}
},
[handleStateChange, tasks.rows, tasks.statuses]
);
//Перезагружать при изменении фильтра или сортировки
useEffect(() => {
filters || orders ? handleReload() : null;
}, [filters, orders, handleReload]);
useEffect(() => {
//Считывание данных с учетом фильтрации
let getTasks = async () => {
const ds = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_DATASET",
args: {
CFILTERS: { VALUE: object2Base64XML(filters.fArray, { arrayNodeName: "filters" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
CORDERS: { VALUE: object2Base64XML(orders, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
NINCLUDE_DEF: tasks.tasksLoaded ? 0 : 1
},
respArg: "COUT"
});
//Инициализируем статусы и события
let newGroups = [];
let newRows = [];
//Если статусы есть
if (ds.XDATA_GRID.groups) {
//Формируем структуру статусов
arrayFormer(ds.XDATA_GRID.groups).map((group, i) => {
newGroups.push({ id: i, code: group.name, name: group.caption, color: randomColor(i + 1) });
});
//Если есть события
if (ds.XDATA_GRID.rows) {
//Формируем структуру событий
arrayFormer(ds.XDATA_GRID.rows).map((task, i) => {
newRows.push(initTask(i, newGroups.find(x => x.name === task.groupName).id, task));
});
}
}
//Возвращаем информацию
return { statuses: [...newGroups], rows: [...newRows] };
};
//Считывание данных
let getData = async () => {
//Считываем информацию о задачах
let eventTasks = await getTasks();
//Добавление описания точки маршрута
eventTasks.statuses.map(s => (s["pointDescr"] = extraData.evPoints.find(ep => ep.point === s.code).pointDescr));
//Загружаем данные
setTasks(pv => ({
...pv,
groupsLoaded: true,
tasksLoaded: true,
statuses: eventTasks.statuses,
rows: eventTasks.rows,
reload: false
}));
};
//Если необходимо загрузить данные и указан тип событий и загружены все необходимые вспомогательные данные
if (
tasks.reload &&
filters.loaded &&
filters.values.type &&
extraData.dataLoaded &&
filters.values.type === extraData.typeLoaded &&
extraData.evPoints.length
) {
//Загружаем данные
getData();
}
}, [
tasks.reload,
filters.values.type,
filters.fArray,
orders,
executeStored,
SERV_DATA_TYPE_CLOB,
tasks.tasksLoaded,
extraData,
getDocLinks,
filters.loaded
]);
return [tasks, handleReload, onDragEnd, needUpdateTasks];
};
//Хук дополнительных данных
const useExtraData = filtersType => {
//Состояние дополнительных данных
const [extraData, setExtraData] = useState({
dataLoaded: false,
typeLoaded: "",
evRoutes: [],
evPoints: [],
noteTypes: [],
docLinks: [],
accounts: []
});
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Надо обновить данные
const needUpdateExtraData = () => setExtraData(pv => ({ ...pv, dataLoaded: false }));
//Получение учётных документов
const getDocLinks = useCallback(
async (type = filtersType) => {
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_DOCLINKS",
args: {
SEVNTYPE_CODE: type
},
respArg: "COUT"
});
//Инициализируем учётные документы
let newDocLinks = [];
//Если найдены учётные документы
if (data.XDOCLINKS) {
arrayFormer(data.XDOCLINKS).map(d => {
newDocLinks.push({ id: d.NRN, descr: d.SDESCR });
});
}
//Возвращаем сформированные учётные документы
return newDocLinks;
},
[executeStored, filtersType]
);
useEffect(() => {
//Получение вспомогательных данных
const getExtraData = async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_INFO_BY_CODE",
args: {
SEVNTYPE_CODE: filtersType
},
respArg: "COUT"
});
//Инициализируем маршруты событий
let newRoutes = data.XEVROUTES
? arrayFormer(data.XEVROUTES).reduce((prev, cur) => {
prev.push({ src: cur.SSOURCE, dest: cur.SDESTINATION });
return prev;
}, [])
: [];
//Инициализируем точки событий
let newPoints = data.XEVPOINTS
? arrayFormer(data.XEVPOINTS).reduce((prev, cur) => {
prev.push({
point: cur.SEVPOINT,
pointDescr: cur.SEVPOINT_DESCR,
addNoteOnChst: cur.ADDNOTE_ONCHST,
addNoteOnSend: cur.ADDNOTE_ONSEND,
banUpdate: cur.BAN_UPDATE
});
return prev;
}, [])
: [];
//Инициализируем типы заголовков примечаний
let newNoteTypes = data.XNOTETYPES
? arrayFormer(data.XNOTETYPES).reduce((prev, cur) => {
prev.push(cur.SNAME);
return prev;
}, [])
: [];
//Инициализируем пользователей
let newAccounts = data.XACCOUNTS
? arrayFormer(data.XACCOUNTS).reduce((prev, cur) => {
prev.push({
agnAbbr: cur.SAGNABBR,
image: cur.BIMAGE
});
return prev;
}, [])
: [];
//Загружаем учётные документы
let docLinks = await getDocLinks(filtersType);
//Возвращаем результат
return {
dataLoaded: true,
typeLoaded: filtersType,
evRoutes: [...newRoutes],
evPoints: [...newPoints],
noteTypes: [...newNoteTypes],
docLinks: [...docLinks],
accounts: [...newAccounts]
};
};
//Считывание данных
const updateExtraData = async () => {
let newExtraData = await getExtraData();
setExtraData(newExtraData);
};
//Если указан тип событий
if (filtersType) {
//Загружаем дополнительные данные
if (!extraData.typeLoaded || filtersType !== extraData.typeLoaded) {
//setExtraData(pv => ({ ...pv, dataLoaded: false }));
updateExtraData();
}
}
}, [executeStored, extraData.typeLoaded, filtersType, getDocLinks]);
return [extraData, getDocLinks, needUpdateExtraData];
};
//Хук для получения пользовательских настроек разметки
const useColorRules = () => {
//Собственное состояние
const [clrRules, setClrRules] = useState({ loaded: false, rules: [] });
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
useEffect(() => {
let getClrRules = async () => {
//Получаем массив пользовательских настроек
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_DP_RULES_GET",
respArg: "COUT"
});
//Инициализируем
let newClrRules = [];
if (data) {
//Формируем структуру настройки
arrayFormer(data.XRULES).map((cr, i) => {
let fromV;
let toV;
if (cr.STYPE === "number") {
fromV = cr.NFROM;
toV = cr.NTO;
} else if (cr.STYPE === "string") {
fromV = cr.SFROM;
toV = cr.STO;
} else {
fromV = cr.DFROM;
toV = cr.DTO;
}
newClrRules.push({ id: i, fieldCode: cr.SFIELD, propName: cr.SDP_NAME, color: cr.SCOLOR, vType: cr.STYPE, from: fromV, to: toV });
});
setClrRules({ loaded: true, rules: [...newClrRules] });
}
};
if (!clrRules.loaded) getClrRules();
}, [clrRules.loaded, executeStored]);
return [clrRules];
};
//Хук дополнительных настроек
const useSettings = statuses => {
//Собственное состояние
const [settings, setSettings] = useState({
statusesSort: {
sorted: false,
attr: localStorage.getItem("settingsSortAttr") ? localStorage.getItem("settingsSortAttr") : "name",
dest: localStorage.getItem("settingsSortDest") ? localStorage.getItem("settingsSortDest") : "asc",
statuses: []
},
colorRule: localStorage.getItem("settingsColorRule") ? JSON.parse(localStorage.getItem("settingsColorRule")) : {}
});
//Изменение состояния после сортировки
const afterSort = statuses => setSettings(pv => ({ ...pv, statusesSort: { ...pv.statusesSort, sorted: true, statuses: statuses } }));
//При закрытии диалога дополнительных настроек по кнопке ОК
const handleSettingsChange = s => {
setSettings({ ...s, statusesSort: { ...s.statusesSort, sorted: false } });
};
//При получении новых настроек сортировки
useEffect(() => {
//Подгрузкка новых статусов
if (statuses.length > 0 && statuses.toString() !== settings.statusesSort.statuses.toString() && settings.statusesSort.sorted)
setSettings(pv => ({ ...pv, statusesSort: { ...pv.statusesSort, sorted: false } }));
//Сортировка
if (statuses.length > 0 && !settings.statusesSort.sorted) {
const attr = settings.statusesSort.attr;
const d = settings.statusesSort.dest;
let s = statuses;
s.sort((a, b) => (d === "asc" ? a[attr].localeCompare(b[attr]) : b[attr].localeCompare(a[attr])));
afterSort(s);
}
}, [settings.statusesSort.attr, settings.statusesSort.dest, settings.statusesSort.sorted, settings.statusesSort.statuses, statuses]);
//Сохранение при закрытии панели
useEffect(() => {
window.addEventListener("beforeunload", function () {
localStorage.setItem("settingsSortAttr", settings.statusesSort.attr);
localStorage.setItem("settingsSortDest", settings.statusesSort.dest);
localStorage.setItem("settingsColorRule", JSON.stringify(settings.colorRule));
});
}, [settings.colorRule, settings.statusesSort.attr, settings.statusesSort.dest]);
return [settings, handleSettingsChange];
};
//----------------
//Интерфейс модуля
//----------------
export { useExtraData, useTasks, useSettings, useColorRules };

View File

@ -0,0 +1,327 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Пользовательские хуки: Хуки диалога события
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import { object2Base64XML } from "../../../core/utils"; //Вспомогательные функции
import { arrayFormer } from "../layouts"; //Формировщик массива
//-----------
//Тело модуля
//-----------
//Хук для события
const useClientEvent = (taskRn, taskType = "", taskStatus = "") => {
//Собственное состояние
const [task, setTask] = useState({
init: true,
nrn: taskRn,
scrn: "",
sprefix: "",
snumber: "",
stype: taskType,
sstatus: taskStatus,
sdescription: "",
sclnt_clnclients: "",
sclnt_clnperson: "",
dstart_date: "",
sinit_clnperson: "",
sinit_user: "",
sinit_reason: "",
sto_company: "",
sto_department: "",
sto_clnpost: "",
sto_clnpsdep: "",
sto_clnperson: "",
sto_fcstaffgrp: "",
sto_user: "",
sto_usergrp: "",
scurrent_user: "",
isUpdate: false,
insertDisabled: true,
updateDisabled: true,
docProps: {}
});
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
const initEventType = useCallback(async () => {
//Считываем параметры исходя из типа события
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVNTYPES_INIT",
args: {
SEVENT_TYPE: task.stype,
SCURRENT_PREF: task.sprefix
},
tagValueProcessor: () => undefined
});
if (data) {
setTask(pv => ({
...pv,
sprefix: data.SPREF,
snumber: data.SNUMB
}));
}
}, [task.sprefix, task.stype, executeStored]);
//Считывание следующего номера события
const getEventNextNumb = useCallback(async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_NEXTNUMB_GET",
args: {
SPREFIX: task.sprefix
}
});
if (data) {
setTask(pv => ({
...pv,
snumber: data.SEVENT_NUMB
}));
}
}, [executeStored, task.sprefix]);
//Добавление события
const insertEvent = useCallback(
async callBack => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_INSERT",
args: {
SCRN: task.scrn,
SPREF: task.sprefix,
SNUMB: task.snumber,
STYPE: task.stype,
SSTATUS: task.sstatus,
SPLAN_DATE: task.dplan_date,
SINIT_PERSON: task.sinit_clnperson,
SCLIENT_CLIENT: task.sclnt_clnclients,
SCLIENT_PERSON: task.sclnt_clnperson,
SDESCRIPTION: task.sdescription,
SREASON: task.sinit_reason,
CPROPS: {
VALUE: object2Base64XML(
[
Object.fromEntries(
Object.entries(task.docProps)
// eslint-disable-next-line no-unused-vars
.filter(([_, v]) => v != (null || ""))
)
],
{
arrayNodeName: "props"
}
),
SDATA_TYPE: SERV_DATA_TYPE_CLOB
}
}
});
callBack();
},
[
executeStored,
task.scrn,
task.sprefix,
task.snumber,
task.stype,
task.sstatus,
task.dplan_date,
task.sinit_clnperson,
task.sclnt_clnclients,
task.sclnt_clnperson,
task.sdescription,
task.sinit_reason,
task.docProps,
SERV_DATA_TYPE_CLOB
]
);
//Исправление события
const updateEvent = useCallback(
async callBack => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_UPDATE",
args: {
NCLNEVENTS: task.nrn,
SCLIENT_CLIENT: task.sclnt_clnclients,
SCLIENT_PERSON: task.sclnt_clnperson,
SDESCRIPTION: task.sdescription,
CPROPS: {
// eslint-disable-next-line no-unused-vars
VALUE: object2Base64XML([Object.fromEntries(Object.entries(task.docProps).filter(([_, v]) => v != (null || "")))], {
arrayNodeName: "props"
}),
SDATA_TYPE: SERV_DATA_TYPE_CLOB
}
}
});
callBack();
},
[SERV_DATA_TYPE_CLOB, executeStored, task.docProps, task.nrn, task.sclnt_clnclients, task.sclnt_clnperson, task.sdescription]
);
useEffect(() => {
if (task.init) {
if (taskRn) {
//Считывание параметров события
const readEvent = async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_GET",
args: {
NCLNEVENTS: task.nrn
},
respArg: "COUT"
});
let newDocProps = {};
Object.keys(data.XEVENT)
.filter(k => k.includes("DP_"))
.map(dp => (newDocProps = { ...newDocProps, [dp]: data.XEVENT[dp] }));
setTask(pv => ({
...pv,
scrn: data.XEVENT.SCRN,
sprefix: data.XEVENT.SPREF,
snumber: data.XEVENT.SNUMB,
stype: data.XEVENT.STYPE,
sstatus: data.XEVENT.SSTATUS,
sdescription: data.XEVENT.SDESCRIPTION,
sclnt_clnclients: data.XEVENT.SCLIENT_CLIENT,
sclnt_clnperson: data.XEVENT.SCLIENT_PERSON,
dplan_date: data.XEVENT.SPLAN_DATE,
sinit_clnperson: data.XEVENT.SINIT_PERSON,
sinit_user: data.XEVENT.SINIT_AUTHID,
sinit_reason: data.XEVENT.SREASON,
sto_company: data.XEVENT.SSEND_CLIENT,
sto_department: data.XEVENT.SSEND_DIVISION,
sto_clnpost: data.XEVENT.SSEND_POST,
sto_clnpsdep: data.XEVENT.SSEND_PERFORM,
sto_clnperson: data.XEVENT.SSEND_PERSON,
sto_fcstaffgrp: data.XEVENT.SSEND_STAFFGRP,
sto_user: data.XEVENT.SSEND_USER_NAME,
sto_usergrp: data.XEVENT.SSEND_USER_GROUP,
scurrent_user: data.XEVENT.SINIT_AUTHID,
isUpdate: true,
init: false,
docProps: newDocProps
}));
};
//Инициализация параметров события
readEvent();
} else {
//Считывание изначальных параметров события
const initEvent = async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_INIT",
args: {}
});
if (data) {
setTask(pv => ({
...pv,
sprefix: data.SPREF,
snumber: data.SNUMB,
scurrent_user: data.SINIT_AUTHNAME,
sinit_clnperson: data.SINIT_PERSON,
sinit_user: !data.SINIT_PERSON ? data.SINIT_AUTHNAME : "",
init: false
}));
}
};
//Инициализация изначальных параметров события
initEvent();
initEventType();
}
}
if (!task.init) {
setTask(pv => ({ ...pv, sinit_user: !task.sinit_clnperson ? task.scurrent_user : "" }));
}
}, [executeStored, task.init, task.nrn, task.stype, task.scurrent_user, task.sinit_clnperson, taskRn, initEventType]);
//Проверка доступности действия
useEffect(() => {
setTask(pv => ({
...pv,
insertDisabled:
!task.scrn ||
!task.sprefix ||
!task.snumber ||
!task.stype ||
!task.sstatus ||
!task.sdescription ||
(!task.sinit_clnperson && !task.sinit_user),
updateDisabled: !task.sdescription
}));
}, [task.scrn, task.sdescription, task.sinit_clnperson, task.sinit_user, task.snumber, task.sprefix, task.sstatus, task.stype]);
return [task, setTask, insertEvent, updateEvent, getEventNextNumb];
};
//Хук для получения свойств раздела "События"
const useDocsProps = taskType => {
//Собственное состояние
const [docProps, setDocsProps] = useState({ loaded: false, props: [] });
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
useEffect(() => {
let getDocsProps = async () => {
//Получаем массив пользовательских настроек
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_PROPS_GET",
args: { SEVNTYPE_CODE: taskType },
respArg: "COUT"
});
//Инициализируем
let newDocProps = [];
if (data) {
//Формируем структуру настройки
arrayFormer(data.XPROPS).map((dp, i) => {
newDocProps.push({
id: i,
rn: dp.NRN,
name: dp.SNAME,
readonly: dp.READONLY,
checkValue: dp.CHECK_VALUE,
checkUnique: dp.CHECK_UNIQUE,
require: dp.REQUIRE,
duplicateValue: dp.DUPLICATE_VALUE,
accessMode: dp.NACCESS_MODE,
showInGrid: dp.SHOW_IN_GRID,
defaultStr: dp.SDEFAULT_STR,
defaultNum: dp.NDEFAULT_NUM,
defaultDate: dp.DDEFAULT_DATE,
entryType: dp.NENTRY_TYPE,
format: dp.NFORMAT,
dataSubtype: dp.NDATA_SUBTYPE,
numWidth: dp.NNUM_WIDTH,
numPrecision: dp.NNUM_PRECISION,
strWidth: dp.NSTR_WIDTH,
unitcode: dp.SUNITCODE,
paramRn: dp.NPARAM_RN,
paramIn: dp.SPARAM_IN_CODE,
paramOut: dp.SPARAM_OUT_CODE,
showMethodRn: dp.NSHOW_METHOD_RN,
showMethodCode: dp.SMETHOD_CODE,
extraDictRn: dp.NEXTRA_DICT_RN,
initRn: dp.NINIT_RN
});
});
setDocsProps({ loaded: true, props: [...newDocProps] });
}
};
if (!docProps.loaded) getDocsProps();
}, [docProps.loaded, executeStored, taskType]);
return [docProps];
};
//----------------
//Интерфейс модуля
//----------------
export { useClientEvent, useDocsProps };

View File

@ -0,0 +1,16 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Панель мониторинга: Точка входа
*/
//---------------------
//Подключение библиотек
//---------------------
import { ClntTaskBoard } from "./clnt_task_board"; //Корневая панель выполнения работ
//----------------
//Интерфейс модуля
//----------------
export const RootClass = ClntTaskBoard;

View File

@ -0,0 +1,207 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Дополнительная разметка и вёрстка клиентских элементов
*/
//---------------------
//Подключение библиотек
//---------------------
//---------
//Константы
//---------
//Перечисление "Состояние события"
export const EVENT_STATES = Object.freeze({ 0: "Все", 1: "Не аннулированные", 2: "Аннулированные" });
//Допустимые значение поля сортировки
export const sortAttrs = [
{ id: "code", descr: "Мнемокод" },
{ id: "name", descr: "Наименование" },
{ id: "pointDescr", descr: "Описание точки маршрута" }
];
//Допустимые значения направления сортировки
export const sortDest = [];
sortDest[-1] = "desc";
sortDest[1] = "asc";
//Цвета статусов
export const COLORS = [
"mediumSlateBlue",
"lightSalmon",
"fireBrick",
"orange",
"gold",
"limeGreen",
"yellowGreen",
"mediumAquaMarine",
"paleTurquoise",
"steelBlue",
"skyBlue",
"tan"
];
//Перечисление "Цвет индикации"
export const EVENT_INDICATORS = Object.freeze({ EXPIRED: "#ff0000", EXPIRES_SOON: "#ffdf00", LINKED: "#1e90ff" });
//Перечисление Доп. свойства "Значение по умолчанию"
export const DP_DEFAULT_VALUE = Object.freeze({ 0: "defaultStr", 1: "defaultNum", 2: "defaultDate", 3: "defaultNum" });
//Перечисление Доп. свойства "Префикс формата данных"
export const DP_TYPE_PREFIX = Object.freeze({ 0: "S", 1: "N", 2: "D", 3: "N" });
//Перечисление Доп. свойства "Входящее значение дополнительного словаря"
export const DP_IN_VALUE = Object.freeze({ 0: "pos_str_value", 1: "pos_num_value", 2: "pos_date_value", 3: "pos_num_value" });
//Перечисление Доп. свойства "Исходящее значение дополнительного словаря"
export const DP_RETURN_VALUE = Object.freeze({ 0: "str_value", 1: "num_value", 2: "date_value", 3: "num_value" });
//Меню действий события
export const menuItems = (
handleEdit,
handleEditClient,
handleDelete,
handleTaskStateChange,
handleTaskReturn,
handleTaskSend,
handleNotes,
handleFileLinks
) => [
{ method: "EDIT", name: "Исправить", icon: "edit", visible: false, delimiter: false, needReload: false, func: handleEdit },
{
method: "EDIT_CLIENT",
name: "Исправить в разделе",
icon: "edit_note",
visible: true,
delimiter: false,
needReload: false,
func: handleEditClient
},
{ method: "DELETE", name: "Удалить", icon: "delete", visible: true, delimiter: true, needReload: true, func: handleDelete },
{
method: "TASK_STATE_CHANGE",
name: "Перейти",
icon: "turn_right",
visible: true,
delimiter: false,
needReload: true,
func: handleTaskStateChange
},
{
method: "TASK_RETURN",
name: "Выполнить возврат",
icon: "turn_left",
visible: true,
delimiter: false,
needReload: true,
func: handleTaskReturn
},
{ method: "TASK_SEND", name: "Направить", icon: "send", visible: true, delimiter: true, needReload: true, func: handleTaskSend },
{ method: "NOTES", name: "Примечания", icon: "event_note", visible: true, delimiter: true, needReload: false, func: handleNotes },
{
method: "FILE_LINKS",
name: "Присоединенные документы",
icon: "attach_file",
visible: true,
delimiter: false,
needReload: false,
func: handleFileLinks
}
];
//-----------
//Тело модуля
//-----------
//Формирование массива из 0, 1 и 1< элементов
export const arrayFormer = arr => {
return arr ? (arr.length ? arr : [arr]) : [];
};
//Конвертация формата HEX в формат RGB
const hexToRGB = hex => {
let r = parseInt(hex.slice(1, 3), 16);
let g = parseInt(hex.slice(3, 5), 16);
let b = parseInt(hex.slice(5, 7), 16);
let a = 0.5;
r = Math.round((a * (r / 255) + a * (255 / 255)) * 255);
g = Math.round((a * (g / 255) + a * (255 / 255)) * 255);
b = Math.round((a * (b / 255) + a * (255 / 255)) * 255);
return "rgb(" + r + ", " + g + ", " + b + ")";
};
//Проверка выполнения условия заливки события
export const bgColorRule = (task, colorRule) => {
let ruleCode;
let bgColor = null;
if (colorRule.vType === "string") ruleCode = `S${colorRule.fieldCode}`;
else if (colorRule.vType === "number") ruleCode = `N${colorRule.fieldCode}`;
else if (colorRule.vType === "date") ruleCode = `D${colorRule.fieldCode}`;
ruleCode ? (task.docProps[ruleCode] == colorRule.from ? (bgColor = hexToRGB(colorRule.color)) : null) : null;
return bgColor;
};
//Индикация истечения срока отработки события
export const indicatorColorRule = task => {
let sysDate = new Date();
let expireDate = task.dexpire_date ? new Date(task.dexpire_date) : null;
let daysDiff = null;
if (expireDate) {
daysDiff = ((expireDate.getTime() - sysDate.getTime()) / (1000 * 60 * 60 * 24)).toFixed(2);
if (daysDiff < 0) return EVENT_INDICATORS.EXPIRED;
else if (daysDiff < 4) return EVENT_INDICATORS.EXPIRES_SOON;
}
return null;
};
//Формирование случайного цвета
export const randomColor = index => {
const hue = index * 137.508;
return hslToRgba(hue, 50, 70);
};
//Цвет из hsl формата в rgba формат
const hslToRgba = (h, s, l) => {
s /= 100;
l /= 100;
const k = n => (n + h / 30) % 12;
const a = s * Math.min(l, 1 - l);
const f = n => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
return `rgba(${Math.floor(255 * f(0))},${Math.floor(255 * f(8))},${Math.floor(255 * f(4))},0.3)`;
};
//Формат дополнительного свойства типа число (длина, точность)
const DPNumFormat = (l, p) => new RegExp("^(\\d{1," + (l - p) + "}" + (p > 0 ? "((\\.|,)\\d{1," + p + "})?" : "") + ")?$");
//Формат дополнительного свойства типа строка (длина)
const DPStrFormat = l => new RegExp("^.{0," + l + "}$");
//Проверка валидности числа
const isValidDPNum = (length, prec, value) => {
return DPNumFormat(length, prec).test(value);
};
//Проверка валидности строки
const isValidDPStr = (length, value) => {
return DPStrFormat(length).test(value);
};
//Признак ошибки валидации
export const validationError = (value = "", format, numW, numPrec, strW) => {
if (format === 0) return isValidDPStr(strW, value);
else if (format === 1) {
return isValidDPNum(numW, numPrec, value);
} else return true;
};
//Конвертация времени в привычный формат
export const timeFromSqlFormat = ts => {
if (ts.indexOf(".") !== -1) {
let s = 24 * 60 * 60 * ts;
const h = Math.trunc(s / (60 * 60));
s = s % (60 * 60);
const m = Math.trunc(s / 60);
s = Math.round(s % 60);
const formattedTime = ("0" + h).slice(-2) + ":" + ("0" + m).slice(-2) + ":" + ("0" + s).slice(-2);
return formattedTime;
}
return ts;
};

View File

@ -0,0 +1,97 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент панели: Диалог формы события
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Dialog, DialogContent, DialogActions, Button } from "@mui/material"; //Интерфейсные компоненты
import { useClientEvent } from "./hooks/task_dialog_hooks"; //Хук для события
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { TaskForm } from "./components/task_form"; //Форма события
//---------
//Константы
//---------
//Стили
const STYLES = {
DIALOG_CONTENT: {
paddingBottom: "0px",
maxHeight: "740px",
minHeight: "740px",
...APP_STYLES.SCROLL
},
DIALOG_ACTIONS: { justifyContent: "end", paddingRight: "24px", paddingLeft: "24px" }
};
//-----------
//Тело модуля
//-----------
//Диалог с формой события
const TaskDialog = ({ taskRn, taskType, taskStatus, editable, onReload, onClose }) => {
//Собственное состояние
const [task, setTask, insertEvent, updateEvent, handleEventNextNumbGet] = useClientEvent(taskRn, taskType, taskStatus);
//Состояние заполненности всех обязательных свойств
const [dpReady, setDPReady] = useState(false);
//Изменение состояния заполненности всех обязательных свойств
const handleDPReady = v => setDPReady(v);
//Генерация содержимого
return (
<Dialog open onClose={onClose ? onClose : null} fullWidth>
<DialogContent sx={STYLES.DIALOG_CONTENT}>
<TaskForm
task={task}
taskType={taskType}
setTask={setTask}
editable={!taskRn || editable ? true : false}
onEventNextNumbGet={handleEventNextNumbGet}
onDPReady={handleDPReady}
/>
</DialogContent>
{onClose ? (
<DialogActions sx={STYLES.DIALOG_ACTIONS}>
{taskRn ? (
<Button onClick={() => updateEvent(onClose).then(onReload)} disabled={task.updateDisabled || !editable || !dpReady}>
Исправить
</Button>
) : (
<Button
onClick={() => {
insertEvent(onClose).then(onReload);
}}
disabled={task.insertDisabled || !dpReady}
>
Добавить
</Button>
)}
<Button onClick={onClose}>Закрыть</Button>
</DialogActions>
) : null}
</Dialog>
);
};
//Контроль свойств - Диалог с формой события
TaskDialog.propTypes = {
taskRn: PropTypes.number,
taskType: PropTypes.string.isRequired,
taskStatus: PropTypes.string,
editable: PropTypes.bool,
onReload: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskDialog };

View File

@ -119,8 +119,8 @@ const EqsPrfrm = () => {
let cF = 0;
let sF = 0;
let properties = [];
if (data.XDATA_GRID.rows != null) {
data.XDATA_GRID.rows.map(row => {
if (data.XROWS != null) {
data.XROWS.map(row => {
properties = [];
Object.entries(row).forEach(([key, value]) => properties.push({ name: key, data: value }));
let info2 = properties.find(element => {
@ -156,10 +156,11 @@ const EqsPrfrm = () => {
}
setDataGrid(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: [...(data.XDATA_GRID.rows || [])],
groups: [...(data.XDATA_GRID.groups || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: [...(data.XROWS || [])],
fixedHeader: data.XDATA_GRID.fixedHeader,
fixedColumns: data.XDATA_GRID.fixedColumns,
groups: [...(data.XGROUPS || [])],
dataLoaded: true,
reload: false
}));
@ -213,7 +214,7 @@ const EqsPrfrm = () => {
}
});
if (data.NIDENT) {
if (type == 0) pOnlineShowUnit({ unitCode: "EquipTechServices", inputParameters: [{ name: "in_Ident", value: data.NIDENT }] });
if (type == 0) pOnlineShowUnit({ unitCode: "EquipTechServices", inputParameters: [{ name: "in_SelectList_Ident", value: data.NIDENT }] });
else pOnlineShowUnit({ unitCode: "EquipRepairSheets", inputParameters: [{ name: "in_Ident", value: data.NIDENT }] });
} else showMsgErr(TEXTS.NO_DATA_FOUND);
};

View File

@ -15,7 +15,6 @@ import { P8PSVG } from "../../../components/p8p_svg"; //Интерактивны
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { useCostProductComposition, useProductDetailsTable } from "../hooks"; //Вспомогательные хуки
import { ProgressBox } from "./progress_box"; //Информация по прогрессу объекта
import { APP_STYLES } from "../../../../app.styles"; //Типовые стили
//---------
//Константы
@ -27,7 +26,6 @@ const STYLES = {
border: "1px solid",
borderRadius: "25px",
height: "35vh",
minHeight: "250px",
backgroundColor: "background.detail_table"
},
BOX_INFO_SUB: isMessage => ({
@ -49,7 +47,6 @@ const STYLES = {
border: "1px solid",
borderRadius: "25px",
height: "17vh",
minHeight: "120px",
backgroundColor: "background.detail_info"
},
PRODUCT_SELECTOR_CONTAINER: {
@ -60,7 +57,6 @@ const STYLES = {
border: "1px solid",
borderRadius: "25px",
height: "53vh",
minHeight: "379px",
marginTop: "16px",
backgroundColor: "background.product_selector"
},
@ -76,12 +72,7 @@ const STYLES = {
width: "280px",
borderBottom: "1px solid"
},
TABLE_DETAILS: {
backgroundColor: "background.detail_table",
height: "28vh",
minHeight: "187px",
...APP_STYLES.SCROLL
},
TABLE_DETAILS: { backgroundColor: "background.detail_table", height: "240px" },
TABLE_DETAILS_HEADER_CELL: maxWidth => ({
backgroundColor: "background.detail_table",
color: "text.detail_table.fontColor",
@ -116,7 +107,7 @@ const PlanSpecInfo = ({ planSpec }) => {
<Box sx={STYLES.PLAN_INFO_MAIN}>
<Box sx={STYLES.PLAN_INFO_SUB}>
<Typography variant="PlanSpecInfo" mt={1}>
Номер заказа:
Номер борта:
</Typography>
<Typography variant="subtitle2">{planSpec.SNUMB}</Typography>
</Box>

View File

@ -70,11 +70,11 @@ const PlanSpecsListItem = ({ card, cardIndex, onClick }) => {
return (
<Box sx={STYLES.CONTAINER} onClick={() => (onClick ? onClick(card, cardIndex) : null)}>
<PlanSpecsListItemImage card={card} />
<Box textAlign="center" height="70px">
<Box textAlign="center">
<Typography variant="PlanSpecInfo" sx={STYLES.TEXT_INFO_FIELD}>
Номер заказа
Номер борта
</Typography>
<Typography variant="h2">{card.SNUMB || "-"}</Typography>
<Typography variant="h2">{card.SNUMB}</Typography>
</Box>
<ProgressBox
progress={card.NPROGRESS}
@ -84,12 +84,12 @@ const PlanSpecsListItem = ({ card, cardIndex, onClick }) => {
progressVariant={"h3"}
detailVariant={"PlanSpecProgressDetail"}
/>
<Box height="70px">
<Box>
<Typography variant="PlanSpecInfo" sx={STYLES.TEXT_INFO_FIELD}>
Год выпуска:
</Typography>
<Typography variant="subtitle1" mt={-1}>
{card.NYEAR || "-"}
{card.NYEAR}
</Typography>
</Box>
</Box>

View File

@ -195,10 +195,9 @@ const useProductDetailsTable = (planSpec, product, orders, pageNumber, stored) =
});
setData(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
morePages: DATA_GRID_PAGE_SIZE == 0 ? false : (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE,
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
morePages: DATA_GRID_PAGE_SIZE == 0 ? false : (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE,
init: true
}));
} finally {

View File

@ -27,7 +27,6 @@ import {
Icon
} from "@mui/material"; //Интерфейсные элементы
import { ThemeProvider } from "@mui/material/styles"; //Подключение темы
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { PlanSpecsList } from "./components/plans_list"; //Список планов
import { PlanSpecDetail } from "./components/plan_detail"; //Детали плана
import { lightTheme, darkTheme } from "./styles/themes"; //Стиль темы
@ -64,8 +63,7 @@ const STYLES = {
display: "inline-block",
boxSizing: "border-box",
backgroundColor: "background.plans_drawer_paper",
color: "text.plans_finder.fontColor",
...APP_STYLES.SCROLL
color: "text.plans_finder.fontColor"
}
},
PLANS_LIST_BOX: { paddingTop: "20px" },
@ -242,32 +240,26 @@ const MechRecAssemblyMon = () => {
</Stack>
{state.init == true ? (
state.selectedPlanCtlg.NRN ? (
state.planSpecs.length !== 0 ? (
<>
<Typography variant="h3" sx={STYLES.MAIN_TITLE} pb={2}>
{title}
</Typography>
{state.planSpecsLoaded == true ? (
state.selectedPlanSpec.NRN ? (
<PlanSpecDetail
planSpec={state.selectedPlanSpec}
disableNavigatePrev={planDetailNavigation.disableNavigatePrev}
disableNavigateNext={planDetailNavigation.disableNavigateNext}
onNavigate={handlePlanDetailNavigateClick}
onBack={handlePlanDetailBackClick}
/>
) : (
<Box sx={STYLES.PLANS_LIST_BOX}>
<PlanSpecsList planSpecs={state.planSpecs} onItemClick={handlePlanClick} />
</Box>
)
) : null}
</>
) : (
<Typography variant="h4" sx={STYLES.MAIN_TITLE}>
В каталоге планов отсутствуют записи подходящих первичных документов
<>
<Typography variant="h3" sx={STYLES.MAIN_TITLE} pb={2}>
{title}
</Typography>
)
{state.planSpecsLoaded == true ? (
state.selectedPlanSpec.NRN ? (
<PlanSpecDetail
planSpec={state.selectedPlanSpec}
disableNavigatePrev={planDetailNavigation.disableNavigatePrev}
disableNavigateNext={planDetailNavigation.disableNavigateNext}
onNavigate={handlePlanDetailNavigateClick}
onBack={handlePlanDetailBackClick}
/>
) : (
<Box sx={STYLES.PLANS_LIST_BOX}>
<PlanSpecsList planSpecs={state.planSpecs} onItemClick={handlePlanClick} />
</Box>
)
) : null}
</>
) : (
<Typography variant="h4" sx={STYLES.MAIN_TITLE}>
Укажите каталог планов для отображения спецификаций

View File

@ -150,13 +150,12 @@ const useCostJobsSpecs = task => {
});
setCostJobsSpecs(pv => ({
...pv,
...data.XDATA_GRID,
task: task,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE
}));
};
loadData();
@ -257,13 +256,12 @@ const useEquipConfiguration = (task, fromAction) => {
});
setEquipConfiguration(pv => ({
...pv,
...data.XDATA_GRID,
task: task,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE
}));
};
loadData();

View File

@ -10,7 +10,6 @@
import React, { useContext, useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Drawer, Fab, Box, List, ListItemButton, ListItemText, Typography, TextField } from "@mui/material"; //Интерфейсные элементы
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
import { CostJobsSpecsDataGrid } from "./fcjobssp"; //Собственные хуки таблиц
import { useCostJobs, useFilteredFcjobs } from "./hooks"; //Вспомогательные хуки
@ -36,7 +35,7 @@ const STYLES = {
width: "350px",
display: "inline-block",
flexShrink: 0,
[`& .MuiDrawer-paper`]: { width: "350px", display: "inline-block", boxSizing: "border-box", ...APP_STYLES.SCROLL }
[`& .MuiDrawer-paper`]: { width: "350px", display: "inline-block", boxSizing: "border-box" }
},
CONTAINER: { textAlign: "center" }
};

View File

@ -1,281 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Выдача сменного задания на участок
Кастомные хуки
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useCallback, useEffect, useContext } from "react"; //Классы React
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { NavigationCtx } from "../../context/navigation"; //Контекст навигации
import { object2Base64XML } from "../../core/utils"; //Вспомогательные функции
//---------
//Константы
//---------
//Размер страницы данных
const DATA_GRID_PAGE_SIZE = 50;
//---------------------------------------------
//Вспомогательные функции форматирования данных
//---------------------------------------------
//-----------
//Тело модуля
//-----------
//Хук для основной таблицы
const useCostJobs = () => {
//Собственное состояние - таблица данных
const [state, setState] = useState({
init: false,
loaded: false,
jobInfo: {},
haveNote: false,
coeff: "1.0"
});
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Подключение к контексту навигации
const { getNavigationSearch } = useContext(NavigationCtx);
//При подключении компонента к странице
useEffect(() => {
const initJob = async fcJob => {
const data = await executeStored({
stored: "PKG_P8PANELS_MECHREC.FCJOBS_MP_INIT",
args: { NFCJOBS: parseInt(fcJob) },
respArg: "COUT",
attributeValueProcessor: (name, val) => (["NHAVE_NOTE"].includes(name) ? val == 1 : val)
});
setState(pv => ({
...pv,
init: true,
jobInfo: data.XFCJOBS ? data.XFCJOBS : {},
loaded: true
}));
};
if (!state.init) {
//Считаем параметры, переданные из действия
const actionPrms = getNavigationSearch();
//Иницализируем сменное задание
initJob(actionPrms.NRN);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return [state, setState];
};
//Хук для таблицы операций
const useCostJobsSpecs = task => {
//Собственное состояние - таблица данных
const [costJobsSpecs, setCostJobsSpecs] = useState({
task: null,
dataLoaded: false,
columnsDef: [],
orders: null,
rows: [],
selectedRow: {},
reload: true,
pageNumber: 1,
morePages: true,
fixedHeader: false,
fixedColumns: 0
});
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//Выдача задания
const issueCostJobsSpecs = useCallback(
async prms => {
try {
await executeStored({
stored: "PKG_P8PANELS_MECHREC.FCJOBSSP_MP_ISSUE",
args: { NFCJOBS: prms.NFCJOBS, NCOEFF: parseFloat(prms.NCOEFF) }
});
} catch (e) {
throw new Error(e.message);
}
},
[executeStored]
);
//При необходимости обновить данные таблицы
useEffect(() => {
//Если изменилось сменное задание - обновляем состояние
if (costJobsSpecs.dataLoaded && costJobsSpecs.task !== task) {
setCostJobsSpecs(pv => ({
...pv,
dataLoaded: false,
columnsDef: [],
orders: null,
rows: [],
selectedRow: {},
reload: true,
pageNumber: 1,
morePages: true
}));
}
//Если необходимо перезагрузить
if (costJobsSpecs.reload && task) {
const loadData = async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_MECHREC.FCJOBSSP_MP_DG_GET",
args: {
NFCJOBS: task,
NPAGE_NUMBER: costJobsSpecs.pageNumber,
NPAGE_SIZE: DATA_GRID_PAGE_SIZE,
CORDERS: { VALUE: object2Base64XML(costJobsSpecs.orders, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
NINCLUDE_DEF: costJobsSpecs.dataLoaded ? 0 : 1
},
respArg: "COUT",
attributeValueProcessor: (name, val) =>
name === "NSELECT" ? val === 1 : name === "SWORKERS_LIST" ? (val ? val.split(",").map(Number) : []) : val
});
setCostJobsSpecs(pv => ({
...pv,
...data.XDATA_GRID,
task: task,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
}));
};
loadData();
}
}, [
SERV_DATA_TYPE_CLOB,
costJobsSpecs.dataLoaded,
costJobsSpecs.orders,
costJobsSpecs.pageNumber,
costJobsSpecs.reload,
costJobsSpecs.task,
executeStored,
task
]);
return [costJobsSpecs, setCostJobsSpecs, issueCostJobsSpecs];
};
//Хук для рабочих
const useCostJobsWorkers = task => {
//Собственное состояние - таблица данных
const [costJobsWorkers, setCostJobsWorkers] = useState({
task: null,
dataLoaded: false,
columnsDef: [],
orders: null,
rows: [],
selectedRows: [],
reload: true,
pageNumber: 1,
morePages: true
});
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//Включение рабочего в строку сменного задания
const includeWorker = useCallback(
async prms => {
try {
await executeStored({
stored: "PKG_P8PANELS_MECHREC.FCJOBSSP_MP_INC_PERFORM",
args: {
NFCJOBSSP: prms.NFCJOBSSP,
SPERFORM_LIST: {
VALUE: Array.isArray(prms.SELECTED_WORKERS) ? prms.SELECTED_WORKERS.join(";") : prms.SELECTED_WORKERS,
SDATA_TYPE: SERV_DATA_TYPE_CLOB
},
NQUANT_PLAN: prms.NQUANT_PLAN
}
});
} catch (e) {
throw new Error(e.message);
}
},
[SERV_DATA_TYPE_CLOB, executeStored]
);
//Исключение рабочего из строки сменного задания
const excludeWorker = useCallback(
async prms => {
try {
await executeStored({
stored: "PKG_P8PANELS_MECHREC.FCJOBSSP_MP_EXC_PERFORM",
args: { NFCJOBSSP: prms.NFCJOBSSP }
});
} catch (e) {
throw new Error(e.message);
}
},
[executeStored]
);
//При необходимости обновить данные таблицы
useEffect(() => {
//Если изменилось сменное задание - обновляем состояние
if (costJobsWorkers.dataLoaded && costJobsWorkers.task !== task) {
setCostJobsWorkers(pv => ({
...pv,
dataLoaded: false,
columnsDef: [],
orders: null,
rows: [],
selectedRows: [],
reload: true,
pageNumber: 1,
morePages: true
}));
}
//Если необходимо перезагрузить
if (costJobsWorkers.reload && task) {
const loadData = async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_MECHREC.WORKERS_MP_DG_GET",
args: {
NFCJOBS: task,
NPAGE_NUMBER: costJobsWorkers.pageNumber,
NPAGE_SIZE: DATA_GRID_PAGE_SIZE,
CORDERS: { VALUE: object2Base64XML(costJobsWorkers.orders, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
NINCLUDE_DEF: costJobsWorkers.dataLoaded ? 0 : 1
},
respArg: "COUT",
attributeValueProcessor: (name, val) => (["NSELECT"].includes(name) ? val === 1 : val)
});
setCostJobsWorkers(pv => ({
...pv,
...data.XDATA_GRID,
task: task,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
}));
};
loadData();
}
}, [
SERV_DATA_TYPE_CLOB,
costJobsWorkers.dataLoaded,
costJobsWorkers.orders,
costJobsWorkers.pageNumber,
costJobsWorkers.reload,
costJobsWorkers.task,
executeStored,
task
]);
return [costJobsWorkers, setCostJobsWorkers, includeWorker, excludeWorker];
};
export { useCostJobs, useCostJobsSpecs, useCostJobsWorkers };

View File

@ -1,16 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Выдача сменного задания на участок
Панель мониторинга: Точка входа
*/
//---------------------
//Подключение библиотек
//---------------------
import { MechRecCostJobs } from "./mech_rec_cost_jobs_manage_mp"; //Корневая панель выдачи сменного задания на участок
//----------------
//Интерфейс модуля
//----------------
export const RootClass = MechRecCostJobs;

View File

@ -1,484 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Выдача сменного задания на участок
Панель мониторинга: Корневая панель выдачи сменного задания на участок
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import { Grid, Box, Typography, Checkbox, Icon, Stack, Button, Tooltip, TextField } from "@mui/material"; //Интерфейсные элементы
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { P8PDataGrid, P8P_DATA_GRID_SIZE, P8P_DATA_GRID_MORE_HEIGHT } from "../../components/p8p_data_grid"; //Таблица данных
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { useCostJobs, useCostJobsSpecs, useCostJobsWorkers } from "./hooks"; //Вспомогательные хуки
import { CostJobsSpecsInclude } from "./worker_include_dialog"; //Компонент диалога включения в задание
import { hasValue } from "../../core/utils"; //Вспомогательные функции
//---------
//Константы
//---------
//Мнемокод раздела операций
const UNIT_COST_JOBS_SPECS = "CostJobsSpecs";
//Мнемокод раздела исполнений должности
const UNIT_WORKERS = "ClientPostPerform";
//Высота основного заголовка
const MAIN_HEADER_HEIGHT = "35px";
//Высота подзаголовка
const SUB_HEADER_HEIGHT = "35px";
//Высота заголовка таблицы
const TABLE_HEADER_HEIGHT = "35px";
//Высота панели кнопок таблицы
const TABLE_BUTTONS_HEIGHT = "35px";
//Отступ таблицы
const TABLE_PADDING_TOP = "15px";
//Формат для коэффициент выполнения норм
const issueCoeffFormat = /^(?!.*\..*\.)[0-9]{0,3}(\.[0-9]{0,1})?$/;
//Стили
const STYLES = {
MAIN_HEADER: { height: MAIN_HEADER_HEIGHT, overflow: "hidden" },
SUB_HEADER: { height: SUB_HEADER_HEIGHT, overflow: "hidden" },
CONTAINER: { textAlign: "center" },
TABLE: { paddingTop: TABLE_PADDING_TOP },
TABLE_HEADER: { height: TABLE_HEADER_HEIGHT, overflow: "hidden" },
TABLE_BUTTONS: { display: "flex", justifyContent: "flex-end", height: TABLE_BUTTONS_HEIGHT, overflow: "hidden", alignItems: "flex-end" },
DATA_GRID_CONTAINER: morePages => ({
height: `calc(100vh - ${APP_BAR_HEIGHT} - ${MAIN_HEADER_HEIGHT} - ${SUB_HEADER_HEIGHT} - ${TABLE_HEADER_HEIGHT} - ${TABLE_BUTTONS_HEIGHT} - ${TABLE_PADDING_TOP} - 32px - ${
morePages ? P8P_DATA_GRID_MORE_HEIGHT : "0px"
})`,
...APP_STYLES.SCROLL
})
};
//Цвета
const colors = {
LINKED: "#bce0de",
UNAVAILABLE: "#949494",
WITH_WORKER: "#82df83"
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Проверка правильности значения коэффициент выполнения норм
const isValidIssueCoeff = value => {
return issueCoeffFormat.test(value);
};
//Форматирование значения ячейки
const dataCellRender = ({ row, columnDef, handleSelectChange, sUnit, selectedWorkerRows = [], selectedJobSpec }) => {
//Стиль
let cellStyle = {};
//Если это рабочие
if (sUnit === UNIT_WORKERS) {
//Признак недоступности
let disabled = true;
//Если в выбранной строке смены указан исполнитель факт
if (selectedJobSpec.NPERFORM_FACT) {
//Если это текущей исполнитель
if (selectedJobSpec.SWORKERS_LIST.includes(row["NRN"])) {
//Подсвечиваем строку рабочего
cellStyle = { backgroundColor: colors.LINKED };
}
} else {
//Если выбрана строка смены
if (selectedJobSpec.NRN) {
//Если текущий рабочий может принять задание
if (row["NLOADING"] < 100) {
//Подсвечиваем строку рабочего
cellStyle = { backgroundColor: colors.LINKED };
disabled = false;
}
}
}
//Если уже выбрано достаточное количество рабочих и текущий рабочий не отмечен
if (selectedJobSpec.NRESOURCE_NUMB === selectedWorkerRows.length && !selectedWorkerRows.includes(row["NRN"])) {
//Устанавливаем признак недоступности
disabled = true;
}
//Если загрузка рабочего больше 100
if (row["NLOADING"] >= 100) {
//Если поле не поле выбора
if (columnDef.name !== "NSELECT") {
//Указываем, что рабочее место недоступно
cellStyle = { ...cellStyle, color: colors.UNAVAILABLE };
}
}
//Для колонки выбора
if (columnDef.name === "NSELECT") {
return {
cellStyle,
data: (
<Box sx={STYLES.CONTAINER}>
<Checkbox
disabled={disabled}
checked={selectedWorkerRows.includes(row["NRN"])}
onChange={() => handleSelectChange({ NRN: row["NRN"], SUNIT: sUnit, BFULL_LOADED: row["NLOADING"] >= 100 })}
/>
</Box>
)
};
}
//Отформатированная колонка
return {
cellStyle,
data: row[columnDef.name]
};
}
//Если это сменное задание
if (sUnit === UNIT_COST_JOBS_SPECS) {
//Если указан исполнитель факт
if (row["NPERFORM_FACT"]) {
//Подсвечиваем сменное задание зеленым
cellStyle = { ...cellStyle, backgroundColor: colors.WITH_WORKER };
}
//Для колонки выбора
if (columnDef.name === "NSELECT") {
return {
cellStyle,
data: (
<Box sx={STYLES.CONTAINER}>
<Checkbox
disabled={row["DBEG_FACT"] ? true : false}
checked={row["NRN"] === selectedJobSpec.NRN}
onChange={() =>
handleSelectChange({
NRN: row["NRN"],
SUNIT: sUnit,
NPERFORM_FACT: row["NPERFORM_FACT"],
NRESOURCE_NUMB: row["NRESOURCE_NUMB"],
NQUANT_PLAN: row["NQUANT_PLAN"],
SWORKERS_LIST: row["SWORKERS_LIST"]
})
}
/>
</Box>
)
};
}
//Отформатированная колонка
return {
cellStyle,
data: row[columnDef.name]
};
}
//Необрабатываемый раздел
return {
data: row[columnDef.name]
};
};
//Генерация представления ячейки заголовка группы
export const headCellRender = ({ columnDef }) => {
if (columnDef.name === "NSELECT") {
return {
stackStyle: { padding: "2px", justifyContent: "space-around" },
data: <Icon>done</Icon>
};
} else {
return {
stackStyle: { padding: "2px" },
data: columnDef.caption
};
}
};
//-----------
//Тело модуля
//-----------
//Корневая панель выдачи сменного задания на участок
const MechRecCostJobs = () => {
//Состояние диалога включения в задание
const [showInclude, setShowInclude] = useState(false);
//Состояние информации о сменном задании
const [state, setState] = useCostJobs();
//Состояние таблицы сменных заданий
const [costJobsSpecs, setCostJobsSpecs, issueCostJobsSpecs] = useCostJobsSpecs(state.jobInfo.NRN);
//Состояние таблицы рабочих
const [costJobsWorkers, setCostJobsWorkers, includeWorker, excludeWorker] = useCostJobsWorkers(state.jobInfo.NRN);
//При изменении состояния сортировки операций
const handleCostJobsSpecOrderChanged = ({ orders }) => setCostJobsSpecs(pv => ({ ...pv, orders: [...orders], pageNumber: 1, reload: true }));
//При изменении количества отображаемых страниц операций
const handleCostJobsSpecPagesCountChanged = () => setCostJobsSpecs(pv => ({ ...pv, pageNumber: pv.pageNumber + 1, reload: true }));
//При изменении состояния сортировки рабочих
const handleCostJobsWorkersOrderChanged = ({ orders }) => setCostJobsWorkers(pv => ({ ...pv, orders: [...orders], pageNumber: 1, reload: true }));
//При изменении количества отображаемых страниц рабочих
const handleCostJobsWorkersPagesCountChanged = () => setCostJobsWorkers(pv => ({ ...pv, pageNumber: pv.pageNumber + 1, reload: true }));
//При исключении рабочих из строки сменного задания
const handleCostJobsSpecExcludeWorker = () => {
//Делаем асинхронно, чтобы при ошибке ничего не обновлять
const excludeAsync = async () => {
//Исключаем рабочего из строки сменного задания
try {
await excludeWorker({
NFCJOBSSP: costJobsSpecs.selectedRow.NRN
});
//Необходимо обновить данные
setCostJobsSpecs(pv => ({ ...pv, selectedRow: {}, pageNumber: 1, reload: true }));
setCostJobsWorkers(pv => ({ ...pv, selectedRows: [], pageNumber: 1, reload: true }));
} catch (e) {
throw new Error(e.message);
}
};
//Исключаем рабочего асинхронно
excludeAsync();
};
//Выдача задания операции
const handleCostJobsSpecIssue = () => {
//Делаем асинхронно, чтобы при ошибке ничего не обновлять
const issueAsync = async () => {
//Включаем рабочих в операции
try {
await issueCostJobsSpecs({ NFCJOBS: state.jobInfo.NRN, NCOEFF: state.coeff });
//Необходимо обновить данные
setCostJobsSpecs(pv => ({ ...pv, selectedRow: {}, pageNumber: 1, reload: true }));
setCostJobsWorkers(pv => ({ ...pv, selectedRows: [], pageNumber: 1, reload: true }));
} catch (e) {
throw new Error(e.message);
}
};
//Выдаем задание асинхронно
issueAsync();
};
//При изменение состояния выбора
const handleSelectChange = prms => {
//Выбранный элемент
let selectedRow = null;
//Буфер для выбранных рабочих
let selectedWorkers = [];
//Индекс рабочего в списке выбранных
let workerIndex = null;
//Исходим от раздела
switch (prms.SUNIT) {
//Сменное задание
case UNIT_COST_JOBS_SPECS:
//Определяем это новое отмеченное сменное задание или сброс старого
selectedRow = costJobsSpecs.selectedRow.NRN ? (costJobsSpecs.selectedRow.NRN === prms.NRN ? null : prms.NRN) : prms.NRN;
//Актуализируем строки
setCostJobsSpecs(pv => ({
...pv,
selectedRow: selectedRow
? {
NRN: selectedRow,
NPERFORM_FACT: prms.NPERFORM_FACT,
NRESOURCE_NUMB: prms.NRESOURCE_NUMB,
NQUANT_PLAN: prms.NQUANT_PLAN,
SWORKERS_LIST: prms.SWORKERS_LIST
}
: { NRN: null, NPERFORM_FACT: null, NRESOURCE_NUMB: null, NQUANT_PLAN: null, SWORKERS_LIST: [] }
}));
//Выходим
break;
//Рабочие центры
case UNIT_WORKERS:
//Инициализируем рабочими центрами
selectedWorkers = costJobsWorkers.selectedRows || [];
//Определяем индекс элемента в массиве
workerIndex = selectedWorkers.indexOf(prms.NRN);
//Если такого рег. номера нет в списке - добавляем, иначе удаляем
workerIndex > -1 ? selectedWorkers.splice(workerIndex, 1) : selectedWorkers.push(prms.NRN);
//Актуализируем строки
setCostJobsWorkers(pv => ({ ...pv, selectedRows: selectedWorkers }));
//Выходим
break;
default:
return;
}
};
//При открытии/закрытии диалога добавления
const handleShowIncludeChange = needShow => setShowInclude(needShow);
//При изменении коэффициент выполнения норм
const handleIssueCoeffChange = e => {
isValidIssueCoeff(e.target.value) ? setState(pv => ({ ...pv, coeff: e.target.value })) : null;
};
return (
<Box p={2}>
{state.loaded ? (
<Box sx={STYLES.CONTAINER}>
<Typography
sx={STYLES.MAIN_HEADER}
variant={"h6"}
>{`Сменное задание №${state.jobInfo.SDOC_NUMB} на ${state.jobInfo.SPERIOD}`}</Typography>
<Typography sx={STYLES.SUB_HEADER} variant={"h6"}>{`${state.jobInfo.SSUBDIV}`}</Typography>
<Box sx={STYLES.CONTAINER}>
<Grid container spacing={2}>
<Grid item sx={STYLES.CONTAINER} xs={6}>
<Typography sx={STYLES.TABLE_HEADER} variant={"h6"} color={"text.secondary"}>
Сменное задание
</Typography>
{costJobsWorkers.dataLoaded ? (
<>
<Box sx={STYLES.TABLE_BUTTONS}>
<Stack direction={"row"} spacing={1}>
<Tooltip
title={
state.jobInfo.NHAVE_NOTE
? "Сменное задание имеет строку с примечанием"
: "Коэффициент выполнения норм"
}
>
<TextField
name="editIssueValue"
variant="outlined"
sx={{ width: "68px" }}
inputProps={{ sx: { padding: "4.2px 14px" } }}
size="small"
value={state.coeff}
onChange={handleIssueCoeffChange}
disabled={state.jobInfo.NHAVE_NOTE}
/>
</Tooltip>
<Tooltip
title={
state.jobInfo.NHAVE_NOTE
? "Сменное задание имеет строку с примечанием"
: !hasValue(state.coeff)
? "Не указано значение коэффициент выполнения норм"
: null
}
>
<Box>
<Button
variant="contained"
size="small"
disabled={state.jobInfo.NHAVE_NOTE || !hasValue(state.coeff)}
onClick={handleCostJobsSpecIssue}
>
Выдать задания
</Button>
</Box>
</Tooltip>
</Stack>
</Box>
<Box sx={STYLES.TABLE}>
<P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS}
containerComponentProps={{ sx: STYLES.DATA_GRID_CONTAINER(costJobsSpecs.morePages), elevation: 4 }}
columnsDef={costJobsSpecs.columnsDef}
rows={costJobsSpecs.rows}
size={P8P_DATA_GRID_SIZE.SMALL}
morePages={costJobsSpecs.morePages}
reloading={costJobsSpecs.reload}
onOrderChanged={handleCostJobsSpecOrderChanged}
onPagesCountChanged={handleCostJobsSpecPagesCountChanged}
dataCellRender={prms =>
dataCellRender({
...prms,
handleSelectChange,
sUnit: UNIT_COST_JOBS_SPECS,
selectedJobSpec: costJobsSpecs.selectedRow
})
}
headCellRender={prms => headCellRender({ ...prms })}
fixedHeader={costJobsSpecs.fixedHeader}
fixedColumns={costJobsSpecs.fixedColumns}
/>
</Box>
</>
) : null}
</Grid>
<Grid item sx={STYLES.CONTAINER} xs={6}>
<Typography sx={STYLES.TABLE_HEADER} variant={"h6"} color={"text.secondary"}>
Рабочие
</Typography>
{costJobsWorkers.dataLoaded ? (
<>
<Box sx={STYLES.TABLE_BUTTONS}>
<Stack direction={"row"} spacing={1}>
<Button
variant="contained"
size="small"
disabled={!(costJobsSpecs.selectedRow.NRESOURCE_NUMB === costJobsWorkers.selectedRows.length)}
onClick={() => handleShowIncludeChange(true)}
>
Включить в задание
</Button>
<Button
variant="contained"
size="small"
disabled={!costJobsSpecs.selectedRow.NRN || !costJobsSpecs.selectedRow.NPERFORM_FACT}
onClick={handleCostJobsSpecExcludeWorker}
>
Исключить из задания
</Button>
</Stack>
</Box>
<Box sx={STYLES.TABLE}>
<P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS}
containerComponentProps={{ sx: STYLES.DATA_GRID_CONTAINER(costJobsWorkers.morePages), elevation: 4 }}
columnsDef={costJobsWorkers.columnsDef}
rows={costJobsWorkers.rows}
size={P8P_DATA_GRID_SIZE.SMALL}
morePages={costJobsWorkers.morePages}
reloading={costJobsWorkers.reload}
onOrderChanged={handleCostJobsWorkersOrderChanged}
onPagesCountChanged={handleCostJobsWorkersPagesCountChanged}
dataCellRender={prms =>
dataCellRender({
...prms,
handleSelectChange,
sUnit: UNIT_WORKERS,
selectedWorkerRows: costJobsWorkers.selectedRows,
selectedJobSpec: costJobsSpecs.selectedRow
})
}
headCellRender={prms => headCellRender({ ...prms })}
fixedHeader={true}
/>
</Box>
</>
) : null}
</Grid>
</Grid>
</Box>
</Box>
) : null}
{showInclude ? (
<CostJobsSpecsInclude
includePrms={{
NFCJOBSSP: costJobsSpecs.selectedRow.NRN,
SELECTED_WORKERS: costJobsWorkers.selectedRows,
NQUANT_PLAN: costJobsSpecs.selectedRow.NQUANT_PLAN
}}
setShowInclude={setShowInclude}
setCostJobsSpecs={setCostJobsSpecs}
setCostJobsWorkers={setCostJobsWorkers}
includeWorker={includeWorker}
/>
) : null}
</Box>
);
};
//----------------
//Интерфейс модуля
//----------------
export { MechRecCostJobs };

View File

@ -1,103 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Выдача сменного задания на участок
Панель мониторинга: Диалог включения рабочего в сменное задание
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, Button, Dialog, DialogTitle, DialogContent, TextField, DialogActions } from "@mui/material"; //Интерфейсные элементы
import { BUTTONS } from "../../../app.text"; //Текстовые ресурсы
//-----------
//Тело модуля
//-----------
//Диалог включения рабочего в сменное задание
const CostJobsSpecsInclude = ({ includePrms, setShowInclude, setCostJobsSpecs, setCostJobsWorkers, includeWorker }) => {
//Собственное состояние - Значение приоритета
const [state, setState] = useState(includePrms.NQUANT_PLAN);
//При закрытии включения рабочего
const handlePriorEditClose = () => setShowInclude(false);
//При включении рабочего в строку сменного задания
const costJobsSpecIncludeCostEquipment = () => {
//Делаем асинхронно, чтобы при ошибке ничего не обновлять
const includeAsync = async () => {
//Включаем рабочего в строку сменного задания
try {
await includeWorker({
NFCJOBSSP: includePrms.NFCJOBSSP,
SELECTED_WORKERS: includePrms.SELECTED_WORKERS,
NQUANT_PLAN: state
});
//Необходимо обновить все данные
setCostJobsSpecs(pv => ({ ...pv, selectedRow: {}, pageNumber: 1, reload: true }));
setCostJobsWorkers(pv => ({ ...pv, selectedRows: [], pageNumber: 1, reload: true }));
handlePriorEditClose();
} catch (e) {
throw new Error(e.message);
}
};
//Включаем рабочего асинхронно
includeAsync();
};
return (
<Dialog open onClose={() => handlePriorEditClose()}>
<DialogTitle>Включить в задание</DialogTitle>
<DialogContent>
<Box>
<TextField
name="editInculdeValue"
label="Количество"
variant="standard"
fullWidth
InputProps={{
type: "number",
inputProps: {
max: includePrms.NQUANT_PLAN,
min: 0
}
}}
value={state}
onChange={event => {
var value = parseInt(event.target.value, 10);
if (value > includePrms.NQUANT_PLAN) {
value = includePrms.NQUANT_PLAN;
}
if (value < 0) {
value = 0;
}
setState(value);
}}
/>
<Box></Box>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={costJobsSpecIncludeCostEquipment}>{BUTTONS.OK}</Button>
<Button onClick={() => handlePriorEditClose(null)}>{BUTTONS.CANCEL}</Button>
</DialogActions>
</Dialog>
);
};
//Контроль свойств - Диалог включения рабочего в сменное задание
CostJobsSpecsInclude.propTypes = {
includePrms: PropTypes.object.isRequired,
setShowInclude: PropTypes.func.isRequired,
setCostJobsSpecs: PropTypes.func.isRequired,
setCostJobsWorkers: PropTypes.func.isRequired,
includeWorker: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { CostJobsSpecsInclude };

View File

@ -48,21 +48,19 @@ const useCostRouteLists = (task, taskType) => {
NPAGE_SIZE: DATA_GRID_PAGE_SIZE,
NINCLUDE_DEF: costRouteLists.dataLoaded ? 0 : 1
},
attributeValueProcessor: (name, val) =>
["DEXEC_DATE", "DREL_DATE"].includes(name) ? formatDateRF(val) : ["SDOCPREF", "SDOCNUMB"].includes(name) ? undefined : val,
attributeValueProcessor: (name, val) => (["DEXEC_DATE", "DREL_DATE"].includes(name) ? formatDateRF(val) : val),
respArg: "COUT"
});
setCostRouteLists(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE,
quantPlanSum: data.XDATA_GRID.rows ? data.XDATA_GRID.rows.reduce((a, b) => a + b["NQUANT_PLAN"], 0) : 0,
uniqueNomns: data.XDATA_GRID.rows
? data.XDATA_GRID.rows.reduce((accumulator, current) => {
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE,
quantPlanSum: data.XROWS ? data.XROWS.reduce((a, b) => a + b["NQUANT_PLAN"], 0) : 0,
uniqueNomns: data.XROWS
? data.XROWS.reduce((accumulator, current) => {
if (!accumulator.find(item => item.SMATRES_PLAN_NOMEN === current.SMATRES_PLAN_NOMEN)) {
accumulator.push(current);
}
@ -124,12 +122,11 @@ const useIncomFromDeps = (task, taskType) => {
});
setIncomFromDeps(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -175,12 +172,11 @@ const useGoodsParties = mainRowRN => {
});
setGoodsParties(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -227,12 +223,11 @@ const useCostDeliveryLists = mainRowRN => {
});
setCostDeliveryLists(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -43,12 +43,11 @@ import { MessagingСtx } from "../../context/messaging"; //Контекст со
import { NavigationCtx } from "../../context/navigation"; //Контекст навигации
import { P8P_GANTT_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { P8PGantt, taskLegendDesc } from "../../components/p8p_gantt"; //Диаграмма Ганта
import { xml2JSON, formatDateJSONDateOnly, formatDateRF, hasValue } from "../../core/utils"; //Вспомогательные функции
import { useFilteredPlanCtlgs } from "./hooks"; //Вспомогательные хуки
import { CostRouteListsDataGrid } from "./datagrids/fcroutlst"; //Таблица "Маршрутные листы"
import { IncomFromDepsDataGrid } from "./datagrids/incomefromdeps"; //Таблица "Приходы из подразделений"
import { CostRouteListsDataGrid } from "./datagrids/fcroutlst";
import { IncomFromDepsDataGrid } from "./datagrids/incomefromdeps";
//---------
//Константы
@ -73,7 +72,7 @@ const STYLES = {
width: "350px",
display: "inline-block",
flexShrink: 0,
[`& .MuiDrawer-paper`]: { width: "350px", display: "inline-block", boxSizing: "border-box", ...APP_STYLES.SCROLL }
[`& .MuiDrawer-paper`]: { width: "350px", display: "inline-block", boxSizing: "border-box" }
},
GANTT_CONTAINER: { height: `calc(100vh - ${APP_BAR_HEIGHT})`, width: "100vw", paddingTop: "24px" },
GANTT_TITLE: { paddingLeft: "250px", paddingRight: "250px" },
@ -84,26 +83,7 @@ const STYLES = {
TASK_DIALOG_ACTION_CONTAINER: { border: 1, borderColor: "text.primary", borderRadius: "5px", width: "100%" },
FILTERS: { display: "table", float: "right" },
FILTERS_DATE: { display: "table-cell", verticalAlign: "middle" },
FILTERS_LEVEL: { display: "table-cell", verticalAlign: "middle", paddingLeft: "15px" },
FILTERS_LEVEL_CAPTION: { display: "flex", alignItems: "center" },
FILTERS_LEVEL_LIMIT_ICON: { padding: "0px 8px", color: "#9f9c9c" },
FILTERS_LIMIT_SELECT: nOutOfLimit => {
return nOutOfLimit === 1
? {
".MuiOutlinedInput-notchedOutline": {
borderColor: "#e9863c"
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: "#e9863c",
borderWidth: "0.15rem"
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: "#e9863c",
borderWidth: "0.15rem"
}
}
: {};
}
FILTERS_LEVEL: { display: "table-cell", verticalAlign: "middle", paddingLeft: "15px" }
};
//------------------------------------
@ -117,7 +97,7 @@ const parseProdPlanSpXML = async xmlDoc => {
attributeValueProcessor: (name, val) =>
["numb", "title"].includes(name) ? undefined : ["start", "end"].includes(name) ? formatDateJSONDateOnly(val) : val
});
return data.XDATA.XGANTT;
return data.XDATA;
};
//Форматирование для отображения количества документов
@ -255,10 +235,10 @@ const MechRecCostProdPlans = () => {
selectedPlanCtlg: null,
selectedPlanCtlgMaxLevel: null,
selectedPlanCtlgLevel: null,
selectedPlanCtlgOutOfLimit: 0,
selectedPlanCtlgSort: null,
selectedPlanCtlgMenuItems: null,
gantt: {},
selectedPlanCtlgGanttDef: {},
selectedPlanCtlgSpecs: [],
selectedTaskDetail: null,
selectedTaskDetailType: null,
planSpec: null
@ -278,9 +258,6 @@ const MechRecCostProdPlans = () => {
//Подключение к контексту навигации
const { getNavigationSearch } = useContext(NavigationCtx);
//Подключение к контексту сообщений
const { showMsgInfo } = useContext(MessagingСtx);
//Инициализация каталогов планов
const initPlanCtlgs = useCallback(async () => {
if (!state.init) {
@ -303,10 +280,10 @@ const MechRecCostProdPlans = () => {
selectedPlanCtlgSpecsLoaded: false,
selectedPlanCtlgMaxLevel: null,
selectedPlanCtlgLevel: null,
selectedPlanCtlgOutOfLimit: 0,
selectedPlanCtlgSort: null,
selectedPlanCtlgMenuItems: null,
gantt: {},
selectedPlanCtlgSpecs: [],
selectedPlanCtlgGanttDef: {},
showPlanList: false,
selectedTaskDetail: null,
selectedTaskDetailType: null
@ -321,10 +298,10 @@ const MechRecCostProdPlans = () => {
selectedPlanCtlg: null,
selectedPlanCtlgMaxLevel: null,
selectedPlanCtlgLevel: null,
selectedPlanCtlgOutOfLimit: 0,
selectedPlanCtlgSort: null,
selectedPlanCtlgMenuItems: null,
gantt: {},
selectedPlanCtlgSpecs: [],
selectedPlanCtlgGanttDef: {},
showPlanList: false,
selectedTaskDetail: null,
selectedTaskDetailType: null
@ -342,13 +319,13 @@ const MechRecCostProdPlans = () => {
...pv,
selectedPlanCtlgMaxLevel: data.NMAX_LEVEL,
selectedPlanCtlgLevel: level || level === 0 ? level : data.NMAX_LEVEL,
selectedPlanCtlgOutOfLimit: data.NOUT_OF_LIMIT,
selectedPlanCtlgSort: sort,
selectedPlanCtlgMenuItems: state.selectedPlanCtlgMenuItems
? state.selectedPlanCtlgMenuItems
: [...Array(data.NMAX_LEVEL).keys()].map(el => el + 1),
selectedPlanCtlgSpecsLoaded: true,
gantt: { ...doc, tasks: [...(doc?.tasks || [])] }
selectedPlanCtlgGanttDef: doc.XGANTT_DEF ? { ...doc.XGANTT_DEF } : {},
selectedPlanCtlgSpecs: [...(doc?.XGANTT_TASKS || [])]
}));
},
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -396,17 +373,6 @@ const MechRecCostProdPlans = () => {
setState(pv => ({ ...pv, selectedTaskDetail: taskRn, selectedTaskDetailType: taskType }));
};
//При открытии окна информации об ограничении уровня
const handleLevelLimitInfoOpen = () => {
//Отображаем информацию
showMsgInfo(
`Размер производственной программы превышает предельно допустимый для одновременного отображения в виде диаграммы Ганта.
Доступные для просмотра уровни вложенности ограничены.
Вы можете просматривать производственную программу частями, используя действие "Открытие панели Производственная программа" в спецификации "Выпуск"
раздела "Планы и отчеты производства изделий".`
);
};
//Генерация содержимого
return (
<Box>
@ -435,7 +401,7 @@ const MechRecCostProdPlans = () => {
<Grid container>
<Grid item xs={12}>
{state.selectedPlanCtlgSpecsLoaded ? (
state.gantt.tasks.length === 0 ? (
state.selectedPlanCtlgSpecs.length === 0 ? (
<Box pt={3}>
<InlineMsgInfo
okBtn={false}
@ -471,16 +437,8 @@ const MechRecCostProdPlans = () => {
</Select>
</Box>
<Box sx={STYLES.FILTERS_LEVEL}>
<Box sx={STYLES.FILTERS_LEVEL_CAPTION}>
<InputLabel id="select-label-level">До уровня</InputLabel>
{state.selectedPlanCtlgOutOfLimit === 1 ? (
<IconButton sx={STYLES.FILTERS_LEVEL_LIMIT_ICON} onClick={handleLevelLimitInfoOpen}>
<Icon>info</Icon>
</IconButton>
) : null}
</Box>
<InputLabel id="select-label-level">До уровня</InputLabel>
<Select
sx={STYLES.FILTERS_LIMIT_SELECT(state.selectedPlanCtlgOutOfLimit)}
labelId="select-label-level"
id="select-level"
value={state.selectedPlanCtlgLevel}
@ -501,9 +459,10 @@ const MechRecCostProdPlans = () => {
) : null}
<P8PGantt
{...P8P_GANTT_CONFIG_PROPS}
{...state.gantt}
{...state.selectedPlanCtlgGanttDef}
containerStyle={STYLES.GANTT_CONTAINER}
titleStyle={STYLES.GANTT_TITLE}
tasks={state.selectedPlanCtlgSpecs}
taskDialogRenderer={prms => taskDialogRenderer({ ...prms, handleTaskDetailOpen })}
/>
</Box>

View File

@ -62,12 +62,13 @@ const useMechRecDeptCostJobs = (subdiv, fullDate, workHours) => {
});
setCostJobs(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
fixedHeader: data.XDATA_GRID.fixedHeader,
fixedColumns: data.XDATA_GRID.fixedColumns,
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE_LARGE,
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE_LARGE,
date: fullDate
}));
};
@ -108,12 +109,11 @@ const useInsDepartment = fullDate => {
});
setInsDepartments(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE_SMALL
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE_SMALL
}));
};
if (insDepartments.reload) {

View File

@ -75,12 +75,13 @@ const useDeptCostProdPlans = () => {
});
setState(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
fixedHeader: data.XDATA_GRID.fixedHeader,
fixedColumns: data.XDATA_GRID.fixedColumns,
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE_LARGE
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE_LARGE
}));
};
if (state.reload) {
@ -143,12 +144,11 @@ const useCostRouteLists = task => {
});
setCostRouteLists(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE_SMALL
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE_SMALL
}));
};
if (costRouteLists.reload && task) {
@ -202,12 +202,11 @@ const useCostRouteListsSpecs = mainRowRN => {
});
setCostRouteListsSpecs(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE_LARGE
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE_LARGE
}));
};
if (costRouteListsSpecs.reload) {
@ -259,12 +258,11 @@ const useIncomFromDeps = task => {
});
setIncomFromDeps(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE_LARGE
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE_LARGE
}));
};
if (incomFromDeps.reload) {

View File

@ -41,7 +41,7 @@ const STYLES = {
width: "350px",
display: "inline-block",
flexShrink: 0,
[`& .MuiDrawer-paper`]: { width: "350px", display: "inline-block", boxSizing: "border-box", ...APP_STYLES.SCROLL }
[`& .MuiDrawer-paper`]: { width: "350px", display: "inline-block", boxSizing: "border-box" }
},
CONTAINER: { textAlign: "center" },
TITLE: { height: TITLE_HEIGHT, overflow: "hidden", paddingTop: TITLE_PADDING_TOP, paddingBottom: TITLE_PADDING_BOTTOM, display: "inline-table" },

View File

@ -1,52 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Редактор свойств компонента панели
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, Typography } from "@mui/material"; //Интерфейсные элементы
import { useComponentModule } from "./components/components_hooks"; //Хуки компонентов
import "./panels_editor.css"; //Стили редактора
//-----------
//Тело модуля
//-----------
//Редактор свойств компонента панели
const ComponentEditor = ({ id, path, settings = {}, valueProviders = {}, onSettingsChange = null } = {}) => {
//Подгрузка модуля редактора компонента (lazy здесь постоянно обновлялся при смене props, поэтому на хуке, от props независимого)
const [ComponentEditor, init] = useComponentModule({ path, module: "editor" });
//Расчёт флага наличия компонента
const haveComponent = path ? true : false;
//Формирование представления
return (
<Box className={"component-editor__wrap"}>
{haveComponent && init && (
<ComponentEditor.default id={id} {...settings} valueProviders={valueProviders} onSettingsChange={onSettingsChange} />
)}
{!haveComponent && <Typography align={"center"}>Компонент не определён</Typography>}
</Box>
);
};
//Контроль свойств компонента - редактор свойств компонента панели
ComponentEditor.propTypes = {
id: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
settings: PropTypes.object,
valueProviders: PropTypes.object,
onSettingsChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { ComponentEditor };

View File

@ -1,72 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Представление компонента панели
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, Typography } from "@mui/material"; //Интерфейсные элементы
import { useComponentModule } from "./components/components_hooks"; //Хуки компонентов
import "./panels_editor.css"; //Стили редактора
//-----------
//Тело модуля
//-----------
//Представление компонента панели
const ComponentView = ({ id, path, settings = {}, values = {}, onValuesChange = null } = {}) => {
//Подгрузка модуля представления компонента (lazy здесь постоянно обновлялся при смене props, поэтому на хуке, от props независимого)
const [ComponentView, init] = useComponentModule({ path, module: "view" });
//При смене значений
const handleValuesChange = values => onValuesChange && onValuesChange(id, values);
//Расчёт флага наличия компонента
const haveComponent = path ? true : false;
//Формирование представления
return (
<Box className={"component-view__wrap"}>
{haveComponent && init && <ComponentView.default id={id} {...settings} values={values} onValuesChange={handleValuesChange} />}
{!haveComponent && <Typography align={"center"}>Компонент не определён</Typography>}
</Box>
);
};
//Контроль свойств компонента - компонент панели
ComponentView.propTypes = {
id: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
settings: PropTypes.object,
values: PropTypes.object,
onValuesChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { ComponentView };
//--------------------------
//ВАЖНО: Можно на React.lazy
//--------------------------
//ПРИМЕР:
/*
import React, { Suspense, lazy } from "react"; //Классы React
const ComponentView = ({ path = null, props = {} } = {}) => {
const haveComponent = path ? true : false;
const ComponentView = haveComponent ? lazy(() => import(`./components/${path}/view`)) : null;
return (
<Paper sx={STYLES.CONTAINER}>
{haveComponent && (<Suspense fallback={null}><ComponentView {...props}/></Suspense>)}
{!haveComponent && <Typography align={"center"}>Компонент не определён</Typography>}
</Paper>
);
};
*/

View File

@ -1,56 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: График (редактор настроек)
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { DATA_SOURCE_SHAPE, DataSource, EditorBox, EditorSubHeader } from "../editors_common"; //Общие компоненты редакторов
import "../../panels_editor.css"; //Стили редактора
//-----------
//Тело модуля
//-----------
//График (редактор настроек)
const ChartEditor = ({ id, dataSource = null, valueProviders = {}, onSettingsChange = null } = {}) => {
//Собственное состояние - текущие настройки
const [settings, setSettings] = useState(null);
//При изменении компонента
useEffect(() => {
settings?.id != id && setSettings({ id, dataSource });
}, [settings, id, dataSource]);
//При сохранении изменений элемента
const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } }));
//При сохранении настроек
const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor });
//Формирование представления
return (
<EditorBox title={"Параметры графика"} onSave={handleSave}>
<EditorSubHeader title={"Источник данных"} />
<DataSource dataSource={settings?.dataSource} valueProviders={valueProviders} onChange={handleDataSourceChange} />
</EditorBox>
);
};
//Контроль свойств компонента - График (редактор настроек)
ChartEditor.propTypes = {
id: PropTypes.string.isRequired,
dataSource: DATA_SOURCE_SHAPE,
valueProviders: PropTypes.object,
onSettingsChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export default ChartEditor;

View File

@ -1,79 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: График (представление)
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Paper } from "@mui/material"; //Интерфейсные элементы
import { P8PChart } from "../../../../components/p8p_chart"; //График
import { useComponentDataSource } from "../components_hooks"; //Хуки для данных
import { DATA_SOURCE_SHAPE } from "../editors_common"; //Общие объекты компонентов
import { COMPONENT_MESSAGE_TYPE, COMPONENT_MESSAGES, ComponentInlineMessage } from "../views_common"; //Общие компоненты представлений
import "../../panels_editor.css"; //Стили редактора
//---------
//Константы
//---------
//Иконка компонента
const COMPONENT_ICON = "bar_chart";
//Наименование компонента
const COMPONENT_NAME = "График";
//Стили
const STYLES = {
CHART: { width: "100%", height: "100%", alignItems: "center", justifyContent: "center", display: "flex" }
};
//-----------
//Тело модуля
//-----------
//График (представление)
const Chart = ({ dataSource = null, values = {} } = {}) => {
//Собственное состояние - данные
const [data, error] = useComponentDataSource({ dataSource, values });
//Флаг настроенности графика
const haveConfing = dataSource?.stored ? true : false;
//Флаг наличия данных
const haveData = data?.init === true && !error ? true : false;
//Данные графика
const chart = data?.XCHART || {};
//Формирование представления
return (
<Paper className={"component-view__container component-view__container__empty"} elevation={6}>
{haveConfing && haveData ? (
<P8PChart style={STYLES.CHART} {...chart} />
) : (
<ComponentInlineMessage
icon={COMPONENT_ICON}
name={COMPONENT_NAME}
message={!haveConfing ? COMPONENT_MESSAGES.NO_SETTINGS : error ? error : COMPONENT_MESSAGES.NO_DATA_FOUND}
type={error ? COMPONENT_MESSAGE_TYPE.ERROR : COMPONENT_MESSAGE_TYPE.COMMON}
/>
)}
</Paper>
);
};
//Контроль свойств компонента - График (представление)
Chart.propTypes = {
dataSource: DATA_SOURCE_SHAPE,
values: PropTypes.object
};
//----------------
//Интерфейс модуля
//----------------
export default Chart;

View File

@ -1,129 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Описание
*/
//---------
//Константы
//---------
const COMPONETNS = [
{
name: "Форма",
path: "form",
settings: {
title: "Параметры формирования",
autoApply: true,
items: [
{
name: "AGENT",
caption: "Контрагент",
unitCode2: "AGNLIST",
unitName: "Контрагенты",
showMethod: "main",
showMethodName: "main",
parameter: "Мнемокод",
inputParameter: "in_AGNABBR",
outputParameter: "out_AGNABBR"
},
{
name: "DOC_TYPE",
caption: "Тип документа",
unitCode2: "DOCTYPES",
unitName: "Типы документов",
showMethod: "main",
showMethodName: "main",
parameter: "Мнемокод",
inputParameter: "in_DOCCODE",
outputParameter: "out_DOCCODE"
}
]
}
},
{
name: "График",
path: "chart",
settings2: {
dataSource: {
type: "USER_PROC",
userProc: рафТоп5ДогКонтрТип",
stored: "UDO_P_P8P_AGNCONTR_CHART",
respArg: "COUT",
arguments: [
{ name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" },
{ name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" }
]
}
}
},
{
name: "Таблица",
path: "table",
settings2: {
dataSource: {
type: "USER_PROC",
userProc: "ТаблицаДогКонтрТип",
stored: "UDO_P_P8P_AGNCONTR_TABLE",
respArg: "COUT",
arguments: [
{ name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" },
{ name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" }
]
}
}
},
{
name: "Индикатор",
path: "indicator",
settings: {
dataSource: {
type: "USER_PROC",
userProc: "ИндКолДогКонтрТип",
stored: "UDO_P_P8P_AGNCONTR_IND",
respArg: "COUT",
arguments: [
{ name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" },
{ name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" },
{
name: "NIND_TYPE",
caption: "Тип индикатора (0 - все, 1 - неутвержденные)",
dataType: "NUMB",
req: true,
value: "0",
valueSource: ""
}
]
}
}
},
{
name: "Индикатор2",
path: "indicator",
settings: {
dataSource: {
type: "USER_PROC",
userProc: "ИндКолДогКонтрТип",
stored: "UDO_P_P8P_AGNCONTR_IND",
respArg: "COUT",
arguments: [
{ name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" },
{ name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" },
{
name: "NIND_TYPE",
caption: "Тип индикатора (0 - все, 1 - неутвержденные)",
dataType: "NUMB",
req: true,
value: "1",
valueSource: ""
}
]
}
}
}
];
//----------------
//Интерфейс модуля
//----------------
export { COMPONETNS };

View File

@ -1,174 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Хуки компонентов
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect, useRef } from "react"; //Классы React
import client from "../../../core/client"; //Клиент взаимодействия с сервером приложений
import { formatErrorMessage } from "../../../core/utils"; //Общие вспомогательные функции
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import { DATA_SOURCE_TYPE, ARGUMENT_DATA_TYPE } from "./editors_common"; //Общие объекты редакторов
//-----------
//Тело модуля
//-----------
//Загрузка модуля компонента из модуля (можно применять как альтернативу React.lazy)
const useComponentModule = ({ path = null, module = "view" } = {}) => {
//Собственное состояние - импортированный модуль компонента
const [componentModule, setComponentModule] = useState(null);
//Собственное состояние - флаг готовности
const [init, setInit] = useState(false);
//При подмонтировании к странице
useEffect(() => {
//Динамическая загрузка модуля компонента из библиотеки
const importComponentModule = async () => {
setInit(false);
const moduleContent = await import(`./${path}/${module}`);
setComponentModule(moduleContent);
setInit(true);
};
if (path) importComponentModule();
}, [path, module]);
//Возвращаем интерфейс хука
return [componentModule, init];
};
//Описание пользовательской процедуры
const useUserProcDesc = ({ code, refresh }) => {
//Собственное состояние - флаг загрузки
const [isLoading, setLoading] = useState(false);
//Собственное состояние - данные
const [data, setData] = useState(null);
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При необходимости обновить данные компонента
useEffect(() => {
//Загрузка данных с сервера
const loadData = async () => {
try {
setLoading(true);
const data = await executeStored({
stored: "PKG_P8PANELS_EDITOR.USERPROCS_DESC",
args: { SCODE: code },
respArg: "COUT",
isArray: name => name === "arguments",
loader: false
});
setData(data?.XUSERPROC || null);
} finally {
setLoading(false);
}
};
//Если надо обновить и есть для чего получать данные
if (refresh > 0)
if (code) loadData();
else setData(null);
}, [refresh, code, executeStored]);
//Возвращаем интерфейс хука
return [data, isLoading];
};
//Получение данных компонента из источника
const useComponentDataSource = ({ dataSource, values }) => {
//Контроллер для прерывания запросов
const abortController = useRef(null);
//Собственное состояние - параметры исполнения
const [state, setState] = useState({ stored: null, storedArgs: [], respArg: null, reqSet: false });
//Собственное состояние - флаг загрузки
const [isLoading, setLoading] = useState(false);
//Собственное состояние - данные
const [data, setData] = useState({ init: false });
//Собственное состояние - ошибка получения данных
const [error, setError] = useState(null);
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При необходимости обновить данные
useEffect(() => {
//Загрузка данных с сервера
const loadData = async () => {
try {
setLoading(true);
abortController.current?.abort?.();
abortController.current = new AbortController();
const data = await executeStored({
stored: state.stored,
args: { ...(state.storedArgs ? state.storedArgs : {}) },
respArg: state.respArg,
loader: false,
signal: abortController.current.signal,
showErrorMessage: false
});
setError(null);
setData({ ...data, init: true });
} catch (e) {
if (e.message !== client.ERR_ABORTED) {
setError(formatErrorMessage(e.message).text);
setData({ init: false });
}
} finally {
setLoading(false);
}
};
if (state.reqSet) {
if (state.stored) loadData();
} else setData({ init: false });
return () => abortController.current?.abort?.();
}, [state.stored, state.storedArgs, state.respArg, state.reqSet, executeStored]);
//При изменении свойств
useEffect(() => {
setState(pv => {
if (dataSource?.type == DATA_SOURCE_TYPE.USER_PROC) {
const { stored, respArg } = dataSource;
let reqSet = true;
const storedArgs = {};
dataSource.arguments.forEach(argument => {
let v = argument.valueSource ? values[argument.valueSource] : argument.value;
storedArgs[argument.name] =
argument.dataType == ARGUMENT_DATA_TYPE.NUMB
? isNaN(parseFloat(v))
? null
: parseFloat(v)
: argument.dataType == ARGUMENT_DATA_TYPE.DATE
? new Date(v)
: String(v === undefined ? "" : v);
if (argument.req === true && [undefined, null, ""].includes(storedArgs[argument.name])) reqSet = false;
});
if (pv.stored != stored || pv.respArg != respArg || JSON.stringify(pv.storedArgs) != JSON.stringify(storedArgs)) {
if (!reqSet) {
setError("Не заданы обязательные параметры источника данных");
setData({ init: false });
}
return { stored, respArg, storedArgs, reqSet };
} else return pv;
} else return pv;
});
}, [dataSource, values]);
//Возвращаем интерфейс хука
return [data, error, isLoading];
};
//----------------
//Интерфейс модуля
//----------------
export { useComponentModule, useUserProcDesc, useComponentDataSource };

View File

@ -1,434 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Общие компоненты редакторов свойств
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
Box,
Stack,
IconButton,
Icon,
Typography,
Divider,
Chip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
InputAdornment,
MenuItem,
Menu,
Card,
CardContent,
CardActions,
CardActionArea
} from "@mui/material"; //Интерфейсные элементы
import client from "../../../core/client"; //Клиент БД
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
import { BUTTONS } from "../../../../app.text"; //Общие текстовые ресурсы
import { useUserProcDesc } from "./components_hooks"; //Общие хуки компонентов
import "../panels_editor.css"; //Стили редактора
//---------
//Константы
//---------
//Стили
const STYLES = {
CHIP: (fullWidth = false, multiLine = false) => ({
...(multiLine ? { height: "auto" } : {}),
"& .MuiChip-label": {
...(multiLine
? {
display: "block",
whiteSpace: "normal"
}
: {}),
...(fullWidth ? { width: "100%" } : {})
}
})
};
//Типы даных аргументов
const ARGUMENT_DATA_TYPE = {
STR: client.SERV_DATA_TYPE_STR,
NUMB: client.SERV_DATA_TYPE_NUMB,
DATE: client.SERV_DATA_TYPE_DATE
};
//Типы источников данных
const DATA_SOURCE_TYPE = {
USER_PROC: "USER_PROC",
QUERY: "QUERY"
};
//Типы источников данных (наименования)
const DATA_SOURCE_TYPE_NAME = {
[DATA_SOURCE_TYPE.USER_PROC]: "Пользовательская процедура",
[DATA_SOURCE_TYPE.QUERY]: "Запрос"
};
//Структура аргумента источника данных
const DATA_SOURCE_ARGUMENT_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
caption: PropTypes.string.isRequired,
dataType: PropTypes.oneOf(Object.values(ARGUMENT_DATA_TYPE)),
req: PropTypes.bool.isRequired,
value: PropTypes.any,
valueSource: PropTypes.string
});
//Начальное состояние аргумента источника данных
const DATA_SOURCE_ARGUMENT_INITIAL = {
name: "",
caption: "",
dataType: "",
req: false,
value: "",
valueSource: ""
};
//Структура источника данных
const DATA_SOURCE_SHAPE = PropTypes.shape({
type: PropTypes.oneOf([...Object.values(DATA_SOURCE_TYPE), ""]),
userProc: PropTypes.string,
stored: PropTypes.string,
respArg: PropTypes.string,
arguments: PropTypes.arrayOf(DATA_SOURCE_ARGUMENT_SHAPE)
});
//Начальное состояние истоника данных
const DATA_SOURCE_INITIAL = {
type: "",
userProc: "",
stored: "",
respArg: "",
arguments: []
};
//-----------
//Тело модуля
//-----------
//Контейнер редактора
const EditorBox = ({ title, children, onSave }) => {
//При нажатии на "Сохранить"
const handleSaveClick = (closeEditor = false) => onSave && onSave(closeEditor);
//Формирование представления
return (
<Box className={"component-editor__container"}>
<Divider>{title}</Divider>
<Stack direction={"column"} spacing={1}>
{children}
</Stack>
<Stack direction={"row"} justifyContent={"right"} p={1}>
<IconButton onClick={() => handleSaveClick(false)} title={BUTTONS.APPLY}>
<Icon>done</Icon>
</IconButton>
<IconButton onClick={() => handleSaveClick(true)} title={BUTTONS.SAVE}>
<Icon>done_all</Icon>
</IconButton>
</Stack>
</Box>
);
};
//Контроль свойств компонента - контейнер редактора
EditorBox.propTypes = {
title: PropTypes.string.isRequired,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
onSave: PropTypes.func
};
//Заголовок раздела редактора
const EditorSubHeader = ({ title }) => {
//Формирование представления
return (
<Divider className={"component-editor__divider"}>
<Chip label={title} size={"small"} />
</Divider>
);
};
//Контроль свойств компонента - заголовок раздела редактора
EditorSubHeader.propTypes = {
title: PropTypes.string.isRequired
};
//Диалог настройки
const ConfigDialog = ({ title, children, onOk, onCancel }) => {
//Формирование представления
return (
<Dialog onClose={onCancel} open>
<DialogTitle>{title}</DialogTitle>
<DialogContent>{children}</DialogContent>
<DialogActions>
<Button onClick={() => onOk && onOk()}>{BUTTONS.OK}</Button>
<Button onClick={() => onCancel && onCancel()}>{BUTTONS.CANCEL}</Button>
</DialogActions>
</Dialog>
);
};
//Контроль свойств компонента - диалог настройки
ConfigDialog.propTypes = {
title: PropTypes.string.isRequired,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//Диалог настройки источника данных
const ConfigDataSourceDialog = ({ dataSource = null, valueProviders = {}, onOk = null, onCancel = null } = {}) => {
//Собственное состояние - параметры элемента формы
const [state, setState] = useState({ ...DATA_SOURCE_INITIAL, ...dataSource });
//Собственное состояние - флаги обновление данных
const [refresh, setRefresh] = useState({ userProcDesc: 0 });
//Собственное состояние - элемент привязки меню выбора источника
const [valueProvidersMenuAnchorEl, setValueProvidersMenuAnchorEl] = useState(null);
//Описание выбранной пользовательской процедуры
const [userProcDesc] = useUserProcDesc({ code: state.userProc, refresh: refresh.userProcDesc });
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//Установка значения/привязки аргумента
const setArgumentValueSource = (index, value, valueSource) =>
setState(pv => ({
...pv,
arguments: pv.arguments.map((argument, i) => ({ ...argument, ...(i == index ? { value, valueSource } : {}) }))
}));
//Открытие/сокрытие меню выбора источника
const toggleValueProvidersMenu = target => setValueProvidersMenuAnchorEl(target instanceof Element ? target : null);
//При нажатии на очистку наименования пользовательской процедуры
const handleUserProcClearClick = () => setState({ ...DATA_SOURCE_INITIAL });
//При нажатии на выбор пользовательской процедуры в качестве источника данных
const handleUserProcSelectClick = () => {
pOnlineShowDictionary({
unitCode: "UserProcedures",
showMethod: "main",
inputParameters: [{ name: "in_CODE", value: state.userProc }],
callBack: res => {
if (res.success) {
setState(pv => ({ ...pv, type: DATA_SOURCE_TYPE.USER_PROC, userProc: res.outParameters.out_CODE }));
setRefresh(pv => ({ ...pv, userProcDesc: pv.userProcDesc + 1 }));
}
}
});
};
//При закрытии дилога с сохранением
const handleOk = () => onOk && onOk({ ...state });
//При закртии диалога отменой
const handleCancel = () => onCancel && onCancel();
//При очистке значения/связывания аргумента
const handleArgumentClearClick = index => setArgumentValueSource(index, "", "");
//При отображении меню связывания аргумента с поставщиком данных
const handleArgumentLinkMenuClick = e => setValueProvidersMenuAnchorEl(e.currentTarget);
//При выборе элемента меню связывания аргумента с поставщиком данных
const handleArgumentLinkClick = valueSource => {
setArgumentValueSource(valueProvidersMenuAnchorEl.id, "", valueSource);
toggleValueProvidersMenu();
};
//При вводе значения аргумента
const handleArgumentChange = (index, value) => setArgumentValueSource(index, value, "");
//При изменении описания пользовательской процедуры
useEffect(() => {
if (userProcDesc)
setState(pv => ({
...pv,
stored: userProcDesc?.stored?.name,
respArg: userProcDesc?.stored?.respArg,
arguments: (userProcDesc?.arguments || []).map(argument => ({ ...DATA_SOURCE_ARGUMENT_INITIAL, ...argument }))
}));
}, [userProcDesc]);
//Список значений
const values = Object.keys(valueProviders).reduce((res, key) => [...res, ...Object.keys(valueProviders[key])], []);
//Наличие значений
const isValues = values && values.length > 0 ? true : false;
//Меню привязки к поставщикам значений
const valueProvidersMenu = isValues && (
<Menu anchorEl={valueProvidersMenuAnchorEl} open={Boolean(valueProvidersMenuAnchorEl)} onClose={toggleValueProvidersMenu}>
{values.map((value, i) => (
<MenuItem key={i} onClick={() => handleArgumentLinkClick(value)}>
{value}
</MenuItem>
))}
</Menu>
);
//Формирование представления
return (
<ConfigDialog title="Настройка источника данных" onOk={handleOk} onCancel={handleCancel}>
<Stack direction={"column"} spacing={1}>
{valueProvidersMenu}
<TextField
type={"text"}
variant={"standard"}
value={state.userProc}
label={"Пользовательская процедура"}
InputLabelProps={{ shrink: true }}
InputProps={{
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleUserProcClearClick}>
<Icon>clear</Icon>
</IconButton>
<IconButton onClick={handleUserProcSelectClick}>
<Icon>list</Icon>
</IconButton>
</InputAdornment>
)
}}
/>
{Array.isArray(state?.arguments) &&
state.arguments.map((argument, i) => (
<TextField
key={i}
type={"text"}
variant={"standard"}
value={argument.value || argument.valueSource}
label={argument.caption}
onChange={e => handleArgumentChange(i, e.target.value)}
InputLabelProps={{ shrink: true }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => handleArgumentClearClick(i)}>
<Icon>clear</Icon>
</IconButton>
{isValues && (
<IconButton id={i} onClick={handleArgumentLinkMenuClick}>
<Icon>settings_ethernet</Icon>
</IconButton>
)}
</InputAdornment>
)
}}
/>
))}
</Stack>
</ConfigDialog>
);
};
//Контроль свойств компонента - Диалог настройки источника данных
ConfigDataSourceDialog.propTypes = {
dataSource: DATA_SOURCE_SHAPE,
valueProviders: PropTypes.object,
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//Источник данных
const DataSource = ({ dataSource = null, valueProviders = {}, onChange = null } = {}) => {
//Собственное состояние - отображение диалога настройки
const [configDlg, setConfigDlg] = useState(false);
//Уведомление родителя о смене настроек источника данных
const notifyChange = settings => onChange && onChange(settings);
//При нажатии на настройку источника данных
const handleSetup = () => setConfigDlg(true);
//При нажатии на настройку источника данных
const handleSetupOk = dataSource => {
setConfigDlg(false);
notifyChange(dataSource);
};
//При нажатии на настройку источника данных
const handleSetupCancel = () => setConfigDlg(false);
//При удалении настроек источника данных
const handleDelete = () => notifyChange({ ...DATA_SOURCE_INITIAL });
//Расчет флага "настроенности"
const configured = dataSource?.type ? true : false;
//Список аргументов
const args =
configured &&
dataSource.arguments.map((argument, i) => (
<Chip
key={i}
label={`:${argument.name} = ${argument.valueSource || argument.value || "NULL"}`}
variant={"outlined"}
sx={STYLES.CHIP(true)}
/>
));
//Формирование представления
return (
<>
{configDlg && (
<ConfigDataSourceDialog dataSource={dataSource} valueProviders={valueProviders} onOk={handleSetupOk} onCancel={handleSetupCancel} />
)}
{configured && (
<Card variant={"outlined"}>
<CardActionArea onClick={handleSetup}>
<CardContent>
<Typography variant={"subtitle1"} noWrap={true}>
{dataSource.type === DATA_SOURCE_TYPE.USER_PROC ? dataSource.userProc : "Источник без наименования"}
</Typography>
<Typography variant={"caption"} color={"text.secondary"} noWrap={true}>
{DATA_SOURCE_TYPE_NAME[dataSource.type] || "Неизвестный тип источника"}
</Typography>
<Stack direction={"column"} spacing={1} pt={2}>
{args}
</Stack>
</CardContent>
</CardActionArea>
<CardActions>
<IconButton onClick={handleDelete}>
<Icon>delete</Icon>
</IconButton>
</CardActions>
</Card>
)}
{!configured && (
<Button startIcon={<Icon>build</Icon>} onClick={handleSetup}>
Настроить
</Button>
)}
</>
);
};
//Контроль свойств компонента - Источник данных
DataSource.propTypes = {
dataSource: DATA_SOURCE_SHAPE,
valueProviders: PropTypes.object,
onChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { STYLES, ARGUMENT_DATA_TYPE, DATA_SOURCE_TYPE, DATA_SOURCE_SHAPE, DATA_SOURCE_INITIAL, EditorBox, EditorSubHeader, ConfigDialog, DataSource };

View File

@ -1,49 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Форма (общие константы)
*/
//---------------------
//Подключение библиотек
//---------------------
import PropTypes from "prop-types"; //Контроль свойств компонента
//----------------
//Интерфейс модуля
//----------------
//Структура элемента формы
export const ITEM_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
caption: PropTypes.string.isRequired,
unitCode: PropTypes.string,
unitName: PropTypes.string,
showMethod: PropTypes.string,
showMethodName: PropTypes.string,
parameter: PropTypes.string,
inputParameter: PropTypes.string,
outputParameter: PropTypes.string
});
//Начальное состояние элемента формы
export const ITEM_INITIAL = {
name: "",
caption: "",
unitCode: "",
unitName: "",
showMethod: "",
showMethodName: "",
parameter: "",
inputParameter: "",
outputParameter: ""
};
//Начальное состояние элементов формы
export const ITEMS_INITIAL = [];
//Ориентация элементов формы
export const ORIENTATION = {
H: "H",
V: "v"
};

View File

@ -1,306 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Форма (редактор настроек)
*/
//TODO: Контроль уникальности имени элемента формы
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState, useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
TextField,
Button,
Icon,
Select,
MenuItem,
FormControl,
InputLabel,
FormControlLabel,
Switch,
Chip,
Stack,
InputAdornment,
IconButton
} from "@mui/material"; //Интерфейсные элементы
import { ApplicationСtx } from "../../../../context/application"; //Контекст приложения
import { STYLES as COMMON_STYLES, EditorBox, EditorSubHeader, ConfigDialog } from "../editors_common"; //Общие компоненты редакторов
import { ITEM_SHAPE, ITEM_INITIAL, ITEMS_INITIAL, ORIENTATION } from "./common"; //Общие ресурсы и константы формы
//---------
//Константы
//---------
//Стили
const STYLES = {
CHIP_ITEM: { ...COMMON_STYLES.CHIP(true, false) }
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Редактор элемента
const ItemEditor = ({ item = null, onOk = null, onCancel = null } = {}) => {
//Собственное состояние - параметры элемента формы
const [state, setState] = useState({ ...ITEM_INITIAL, ...item });
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//При закрытии редактора с сохранением
const handleOk = () => onOk && onOk({ ...state });
//При закрытии редактора с отменой
const handleCancel = () => onCancel && onCancel();
//При изменении параметра элемента
const handleChange = e => setState(pv => ({ ...pv, [e.target.id]: e.target.value }));
//При нажатии на очистку раздела
const handleClearUnitClick = () =>
setState(pv => ({
...pv,
unitCode: "",
unitName: "",
showMethod: "",
showMethodName: "",
parameter: "",
inputParameter: "",
outputParameter: ""
}));
//При нажатии на выбор раздела
const handleSelectUnitClick = () => {
pOnlineShowDictionary({
unitCode: "Units",
showMethod: "methods",
inputParameters: [
{ name: "pos_unit_name", value: state.unitName },
{ name: "pos_method_name", value: state.showMethodName }
],
callBack: res =>
res.success &&
setState(pv => ({
...pv,
unitCode: res.outParameters.unit_code,
unitName: res.outParameters.unit_name,
showMethod: res.outParameters.method_code,
showMethodName: res.outParameters.method_name,
parameter: "",
inputParameter: "",
outputParameter: ""
}))
});
};
//При нажатии на выбор параметра метода вызова
const handleSelectUnitParameterClick = () => {
state.unitCode &&
state.showMethod &&
pOnlineShowDictionary({
unitCode: "UnitParams",
showMethod: "main",
inputParameters: [
{ name: "in_UNITCODE", value: state.unitCode },
{ name: "in_PARENT_METHOD_CODE", value: state.showMethod },
{ name: "in_PARAMNAME", value: state.parameter }
],
callBack: res =>
res.success &&
setState(pv => ({
...pv,
parameter: res.outParameters.out_PARAMNAME,
inputParameter: res.outParameters.out_IN_CODE,
outputParameter: res.outParameters.out_OUT_CODE
}))
});
};
//Формирование представления
return (
<ConfigDialog title={`${item ? "Изменение" : "Добавление"} элемента`} onOk={handleOk} onCancel={handleCancel}>
<Stack direction={"column"} spacing={1}>
<TextField type={"text"} variant={"standard"} value={state.name} label={"Имя"} id={"name"} onChange={handleChange} />
<TextField type={"text"} variant={"standard"} value={state.caption} label={"Приглашение"} id={"caption"} onChange={handleChange} />
<TextField
type={"text"}
variant={"standard"}
value={state.unitName}
label={"Раздел"}
InputLabelProps={{ shrink: state.unitName ? true : false }}
InputProps={{
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleClearUnitClick}>
<Icon>clear</Icon>
</IconButton>
<IconButton onClick={handleSelectUnitClick}>
<Icon>list</Icon>
</IconButton>
</InputAdornment>
)
}}
/>
<TextField
type={"text"}
variant={"standard"}
value={state.showMethodName}
label={"Метод вызова"}
InputLabelProps={{ shrink: state.showMethodName ? true : false }}
InputProps={{ readOnly: true }}
/>
<TextField
type={"text"}
variant={"standard"}
value={state.parameter}
label={"Параметр"}
InputLabelProps={{ shrink: state.parameter ? true : false }}
InputProps={{
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleSelectUnitParameterClick}>
<Icon>list</Icon>
</IconButton>
</InputAdornment>
)
}}
/>
</Stack>
</ConfigDialog>
);
};
//Контроль свойств - редактор элемента
ItemEditor.propTypes = {
item: ITEM_SHAPE,
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//-----------
//Тело модуля
//-----------
//Форма (редактор настроек)
const FormEditor = ({ id, title = "", orientation = ORIENTATION.V, autoApply = false, items = ITEMS_INITIAL, onSettingsChange = null } = {}) => {
//Собственное состояние - текущие настройки
const [settings, setSettings] = useState(null);
//Собственное состояние - предоставляемые в панель значения
const [providedValues, setProvidedValues] = useState([]);
//Собственное состояние - редактор элементов формы
const [itemEditor, setItemEditor] = useState({ display: false, index: null });
//При изменении значения настройки
const handleChange = e => setSettings({ ...settings, [e.target.name]: e.target.type === "checkbox" ? e.target.checked : e.target.value });
//При добавлении нового элемента
const handleItemAdd = () => setItemEditor({ display: true, index: null });
//При нажатии на элемент
const handleItemClick = i => setItemEditor({ display: true, index: i });
//При удалении элемента
const handleItemDelete = i => {
const items = [...settings.items];
items.splice(i, 1);
setSettings(pv => ({ ...pv, items }));
};
//При сохранении изменений элемента
const handleItemSave = item => {
const items = [...settings.items];
itemEditor.index == null ? items.push({ ...item }) : (items[itemEditor.index] = { ...item });
setSettings(pv => ({ ...pv, items }));
setItemEditor({ display: false, index: null });
};
//При отмене сохранения изменений элемента
const handleItemCancel = () => setItemEditor({ display: false, index: null });
//При сохранении настроек
const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, providedValues, closeEditor });
//При изменении компонента
useEffect(() => {
settings?.id != id && setSettings({ id, title, orientation, autoApply, items });
}, [settings, id, title, orientation, autoApply, items]);
//При изменении состава элементов формы
useEffect(() => {
Array.isArray(settings?.items) && setProvidedValues(settings.items.map(item => item.name));
}, [settings?.items]);
//Формирование представления
return (
settings && (
<EditorBox title={"Параметры формы"} onSave={handleSave}>
{itemEditor.display && (
<ItemEditor
item={itemEditor.index !== null ? { ...settings.items[itemEditor.index] } : null}
onCancel={handleItemCancel}
onOk={handleItemSave}
/>
)}
<EditorSubHeader title={"Общие"} />
<TextField type={"text"} variant={"standard"} value={settings.title} label={"Заголовок"} name={"title"} onChange={handleChange} />
<FormControl variant={"standard"}>
<InputLabel id={"orientation-label"}>Ориентация</InputLabel>
<Select
name={"orientation"}
value={settings.orientation}
labelId={"orientation-label"}
label={"Ориентация"}
onChange={handleChange}
>
<MenuItem value={ORIENTATION.V}>Вертикально</MenuItem>
<MenuItem value={ORIENTATION.H}>Горизонтально</MenuItem>
</Select>
</FormControl>
<FormControlLabel
control={<Switch name={"autoApply"} checked={settings.autoApply} onChange={handleChange} />}
label={"Автоподтверждение"}
/>
<EditorSubHeader title={"Элементы"} />
{Array.isArray(settings?.items) &&
settings.items.length > 0 &&
settings.items.map((item, i) => (
<Chip
key={i}
label={item.caption}
variant={"outlined"}
onClick={() => handleItemClick(i)}
onDelete={() => handleItemDelete(i)}
sx={STYLES.CHIP_ITEM}
/>
))}
<Button startIcon={<Icon>add</Icon>} onClick={handleItemAdd}>
Добавить элемент
</Button>
</EditorBox>
)
);
};
//Контроль свойств компонента - Форма (редактор настроек)
FormEditor.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string,
orientation: PropTypes.oneOf(Object.values(ORIENTATION)),
autoApply: PropTypes.bool,
items: PropTypes.arrayOf(ITEM_SHAPE),
onSettingsChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export default FormEditor;

View File

@ -1,168 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Форма (представление)
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState, useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Paper, Stack, Typography, Icon, TextField, IconButton, InputAdornment } from "@mui/material"; //Интерфейсные элементы
import { ApplicationСtx } from "../../../../context/application"; //Контекст приложения
import { COMPONENT_MESSAGES, ComponentInlineMessage } from "../views_common"; //Общие компоненты представлений
import { ITEM_SHAPE, ITEMS_INITIAL, ORIENTATION } from "./common"; //Общие ресурсы и константы формы
import "../../panels_editor.css"; //Стили редактора
//---------
//Константы
//---------
//Иконка компонента
const COMPONENT_ICON = "fact_check";
//Наименование компонента
const COMPONENT_NAME = "Форма";
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Элемент формы
const FormItem = ({ item = null, fullWidth = false, value = "", onChange = null } = {}) => {
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//При изменении значения элемента
const handleChange = e => onChange && onChange(e.target.id, e.target.value);
//При очистке значения элемента
const handleClear = () => onChange(item.name, "");
//При выборе значения из словаря
const handleDictionary = () =>
item.unitCode &&
item.showMethod &&
pOnlineShowDictionary({
unitCode: item.unitCode,
showMethod: item.showMethod,
inputParameters: [{ name: item.inputParameter, value }],
callBack: res => res.success && onChange && onChange(item.name, res.outParameters[item.outputParameter])
});
//Формирование представления
return (
item && (
<TextField
fullWidth={fullWidth}
type={"text"}
variant={"standard"}
value={value}
label={item.caption}
id={item.name}
onChange={handleChange}
{...(item.unitCode && {
InputLabelProps: { shrink: true },
InputProps: {
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleClear}>
<Icon>clear</Icon>
</IconButton>
<IconButton onClick={handleDictionary}>
<Icon>list</Icon>
</IconButton>
</InputAdornment>
)
}
})}
/>
)
);
};
//Контроль свойств - элемент формы
FormItem.propTypes = {
item: ITEM_SHAPE,
fullWidth: PropTypes.bool,
value: PropTypes.any,
onChange: PropTypes.func
};
//-----------
//Тело модуля
//-----------
//Форма (представление)
const Form = ({ title = null, orientation = ORIENTATION.V, autoApply = false, items = ITEMS_INITIAL, values = {}, onValuesChange = null } = {}) => {
//Собственное состояние - значения элементов
const [selfValues, setSelfValues] = useState({});
//При изменении состава элементов или значений
useEffect(() => setSelfValues(items.reduce((sV, item) => ({ ...sV, [item.name]: values[item.name] }), {})), [items, values]);
//При изменении значения элемента формы
const handleItemChange = (name, value) => {
setSelfValues(pv => ({ ...pv, [name]: value }));
autoApply && onValuesChange && onValuesChange({ ...selfValues, [name]: value });
};
//При подтверждении изменений формы
const handleOkClick = () => onValuesChange && onValuesChange({ ...selfValues });
//Флаг настроенности формы
const haveConfing = items && Array.isArray(items) && items.length > 0;
//Формирование представления
return (
<Paper className={`component-view__container ${!haveConfing && "component-view__container__empty"}`} elevation={6}>
{haveConfing ? (
<Stack direction={"column"}>
<Stack direction={"row"} justifyContent={"space-between"} alignItems={"center"}>
{title && (
<Typography align={"left"} color={"text.primary"} variant={"subtitle2"} noWrap={true}>
{title}
</Typography>
)}
{!autoApply && (
<IconButton onClick={handleOkClick}>
<Icon>done</Icon>
</IconButton>
)}
</Stack>
<Stack direction={orientation == ORIENTATION.V ? "column" : "row"} spacing={1} pt={1} pb={1}>
{items.map((item, i) => (
<FormItem
key={i}
item={item}
value={selfValues?.[item.name] || ""}
onChange={handleItemChange}
fullWidth={orientation == ORIENTATION.V}
/>
))}
</Stack>
</Stack>
) : (
<ComponentInlineMessage icon={COMPONENT_ICON} name={COMPONENT_NAME} message={COMPONENT_MESSAGES.NO_SETTINGS} />
)}
</Paper>
);
};
//Контроль свойств компонента - Форма (представление)
Form.propTypes = {
id: PropTypes.string.isRequired,
title: PropTypes.string,
orientation: PropTypes.oneOf(Object.values(ORIENTATION)),
autoApply: PropTypes.bool,
items: PropTypes.arrayOf(ITEM_SHAPE),
values: PropTypes.object,
onValuesChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export default Form;

View File

@ -1,56 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Индикатор (редактор настроек)
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { DATA_SOURCE_SHAPE, DataSource, EditorBox, EditorSubHeader } from "../editors_common"; //Общие компоненты редакторов
import "../../panels_editor.css"; //Стили редактора
//-----------
//Тело модуля
//-----------
//Индикатор (редактор настроек)
const IndicatorEditor = ({ id, dataSource = null, valueProviders = {}, onSettingsChange = null } = {}) => {
//Собственное состояние - текущие настройки
const [settings, setSettings] = useState(null);
//При изменении компонента
useEffect(() => {
settings?.id != id && setSettings({ id, dataSource });
}, [settings, id, dataSource]);
//При сохранении изменений элемента
const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } }));
//При сохранении настроек
const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor });
//Формирование представления
return (
<EditorBox title={"Параметры индикатора"} onSave={handleSave}>
<EditorSubHeader title={"Источник данных"} />
<DataSource dataSource={settings?.dataSource} valueProviders={valueProviders} onChange={handleDataSourceChange} />
</EditorBox>
);
};
//Контроль свойств компонента - Индикатор (редактор настроек)
IndicatorEditor.propTypes = {
id: PropTypes.string.isRequired,
dataSource: DATA_SOURCE_SHAPE,
valueProviders: PropTypes.object,
onSettingsChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export default IndicatorEditor;

View File

@ -1,84 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Индикатор (представление)
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Paper } from "@mui/material"; //Интерфейсные элементы
import { P8PIndicator } from "../../../../components/p8p_indicator"; //Компонент индикатора
import { useComponentDataSource } from "../components_hooks"; //Хуки для данных
import { DATA_SOURCE_SHAPE } from "../editors_common"; //Общие объекты компонентов
import { COMPONENT_MESSAGE_TYPE, COMPONENT_MESSAGES, ComponentInlineMessage } from "../views_common"; //Общие компоненты представлений
import "../../panels_editor.css"; //Стили редактора
//---------
//Константы
//---------
//Иконка компонента
const COMPONENT_ICON = "speed";
//Наименование компонента
const COMPONENT_NAME = "Индикатор";
//Стили
const STYLES = {
CONTAINER: { height: "100%" }
};
//-----------
//Тело модуля
//-----------
//Индикатор (представление)
const Indicator = ({ dataSource = null, values = {} } = {}) => {
//Собственное состояние - данные
const [data, error] = useComponentDataSource({ dataSource, values });
//Флаг настроенности индикатора
const haveConfing = dataSource?.stored ? true : false;
//Флаг наличия данных
const haveData = data?.init === true && !error ? true : false;
//Данные индикатора
const indicator = data?.XINDICATOR || {};
//Формирование представления
return (
<Paper
{...(haveConfing && haveData
? { sx: { ...STYLES.CONTAINER } }
: { className: "component-view__container component-view__container__empty" })}
elevation={6}
>
{haveConfing && haveData ? (
<P8PIndicator {...indicator} elevation={0} />
) : (
<ComponentInlineMessage
icon={COMPONENT_ICON}
name={COMPONENT_NAME}
message={!haveConfing ? COMPONENT_MESSAGES.NO_SETTINGS : error ? error : COMPONENT_MESSAGES.NO_DATA_FOUND}
type={error ? COMPONENT_MESSAGE_TYPE.ERROR : COMPONENT_MESSAGE_TYPE.COMMON}
/>
)}
</Paper>
);
};
//Контроль свойств компонента - Индикатор (представление)
Indicator.propTypes = {
dataSource: DATA_SOURCE_SHAPE,
values: PropTypes.object
};
//----------------
//Интерфейс модуля
//----------------
export default Indicator;

View File

@ -1,56 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Таблица (редактор настроек)
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { DATA_SOURCE_SHAPE, DataSource, EditorBox, EditorSubHeader } from "../editors_common"; //Общие компоненты редакторов
import "../../panels_editor.css"; //Стили редактора
//-----------
//Тело модуля
//-----------
//Таблица (редактор настроек)
const TableEditor = ({ id, dataSource = null, valueProviders = {}, onSettingsChange = null } = {}) => {
//Собственное состояние - текущие настройки
const [settings, setSettings] = useState(null);
//При изменении компонента
useEffect(() => {
settings?.id != id && setSettings({ id, dataSource });
}, [settings, id, dataSource]);
//При сохранении изменений элемента
const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } }));
//При сохранении настроек
const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor });
//Формирование представления
return (
<EditorBox title={"Параметры таблицы"} onSave={handleSave}>
<EditorSubHeader title={"Источник данных"} />
<DataSource dataSource={settings?.dataSource} valueProviders={valueProviders} onChange={handleDataSourceChange} />
</EditorBox>
);
};
//Контроль свойств компонента - Таблица (редактор настроек)
TableEditor.propTypes = {
id: PropTypes.string.isRequired,
dataSource: DATA_SOURCE_SHAPE,
valueProviders: PropTypes.object,
onSettingsChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export default TableEditor;

View File

@ -1,96 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Таблица (представление)
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Paper } from "@mui/material"; //Интерфейсные элементы
import { APP_STYLES } from "../../../../../app.styles"; //Типовые стили
import { P8PDataGrid } from "../../../../components/p8p_data_grid"; //Таблица данных
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { useComponentDataSource } from "../components_hooks"; //Хуки для данных
import { DATA_SOURCE_SHAPE } from "../editors_common"; //Общие объекты компонентов
import { COMPONENT_MESSAGE_TYPE, COMPONENT_MESSAGES, ComponentInlineMessage } from "../views_common"; //Общие компоненты представлений
import "../../panels_editor.css"; //Стили редактора
//---------
//Константы
//---------
//Иконка компонента
const COMPONENT_ICON = "table_view";
//Наименование компонента
const COMPONENT_NAME = "Таблица";
//Стили
const STYLES = {
CONTAINER: { display: "flex", height: "100%", overflow: "hidden" },
DATA_GRID: { width: "100%" },
DATA_GRID_CONTAINER: {
height: `calc(100%)`,
...APP_STYLES.SCROLL
}
};
//-----------
//Тело модуля
//-----------
//Таблица (представление)
const Table = ({ dataSource = null, values = {} } = {}) => {
//Собственное состояние - данные
const [data, error] = useComponentDataSource({ dataSource, values });
//Флаг настроенности таблицы
const haveConfing = dataSource?.stored ? true : false;
//Флаг наличия данных
const haveData = data?.init === true && !error ? true : false;
//Данные таблицы
const dataGrid = data?.XDATA_GRID || {};
//Формирование представления
return (
<Paper
{...(haveConfing && haveData
? { sx: { ...STYLES.CONTAINER } }
: { className: "component-view__container component-view__container__empty" })}
elevation={6}
>
{haveConfing && haveData ? (
<P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS}
{...dataGrid}
style={STYLES.DATA_GRID}
containerComponentProps={{ sx: STYLES.DATA_GRID_CONTAINER, elevation: 0 }}
/>
) : (
<ComponentInlineMessage
icon={COMPONENT_ICON}
name={COMPONENT_NAME}
message={!haveConfing ? COMPONENT_MESSAGES.NO_SETTINGS : error ? error : COMPONENT_MESSAGES.NO_DATA_FOUND}
type={error ? COMPONENT_MESSAGE_TYPE.ERROR : COMPONENT_MESSAGE_TYPE.COMMON}
/>
)}
</Paper>
);
};
//Контроль свойств компонента - Таблица (представление)
Table.propTypes = {
dataSource: DATA_SOURCE_SHAPE,
values: PropTypes.object
};
//----------------
//Интерфейс модуля
//----------------
export default Table;

View File

@ -1,67 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Общие компоненты представлений элементов панели
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, Icon, Typography } from "@mui/material"; //Интерфейсные элементы
import { TEXTS } from "../../../../app.text"; //Общие текстовые ресурсы
//---------
//Константы
//---------
//Типы сообщений
const COMPONENT_MESSAGE_TYPE = {
COMMON: "COMMON",
ERROR: "ERROR"
};
//Типовые сообщения
const COMPONENT_MESSAGES = {
NO_DATA_FOUND: TEXTS.NO_DATA_FOUND,
NO_SETTINGS: "Настройте компонент"
};
//-----------
//Тело модуля
//-----------
//Информационное сообщение внутри компонента
const ComponentInlineMessage = ({ icon, name, message, type = COMPONENT_MESSAGE_TYPE.COMMON }) => {
//Формирование представления
return (
<Stack direction={"column"}>
<Stack direction={"row"} justifyContent={"center"} alignItems={"center"}>
{icon && <Icon color={"disabled"}>{icon}</Icon>}
{name && (
<Typography align={"center"} color={"text.secondary"} variant={"button"}>
{name}
</Typography>
)}
</Stack>
<Typography align={"center"} color={type != COMPONENT_MESSAGE_TYPE.ERROR ? "text.secondary" : "error.dark"} variant={"caption"}>
{message}
</Typography>
</Stack>
);
};
//Контроль свойств - Информационное сообщение внутри компонента
ComponentInlineMessage.propTypes = {
icon: PropTypes.string,
name: PropTypes.string,
message: PropTypes.string.isRequired,
type: PropTypes.oneOf(Object.values(COMPONENT_MESSAGE_TYPE))
};
//----------------
//Интерфейс модуля
//----------------
export { COMPONENT_MESSAGE_TYPE, COMPONENT_MESSAGES, ComponentInlineMessage };

View File

@ -1,16 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Редактор панелей: точка входа
*/
//---------------------
//Подключение библиотек
//---------------------
import { PanelsEditor } from "./panels_editor"; //Корневая панель редактора
//----------------
//Интерфейс модуля
//----------------
export const RootClass = PanelsEditor;

View File

@ -1,87 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Элемент макета
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { IconButton, Icon, Stack } from "@mui/material"; //Интерфейсные элементы
import "./panels_editor.css"; //Кастомные стили
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER: selected => ({ zIndex: 1100, ...(selected ? { border: "2px dotted green" } : {}) }),
STACK_TOOLS: { position: "absolute", zIndex: 1200, height: "100%", backgroundColor: "#c0c0c07f" }
};
//-----------
//Тело модуля
//-----------
//Элемент макета
// eslint-disable-next-line react/display-name
const LayoutItem = React.forwardRef(
(
{ style, className, onMouseDown, onMouseUp, onTouchEnd, children, onSettingsClick, onDeleteClick, item, editMode = false, selected = false },
ref
) => {
//При нажатии на настройки
const handleSettingsClick = () => onSettingsClick && onSettingsClick(item.i);
//При нажатии на удаление
const handleDeleteClick = () => onDeleteClick && onDeleteClick(item.i);
//Формирование представления
return (
<div
style={{ ...style, ...STYLES.CONTAINER(selected) }}
className={`${className} layout-item__container`}
ref={ref}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onTouchEnd={onTouchEnd}
>
{editMode && (
<Stack direction={"column"} sx={STYLES.STACK_TOOLS}>
<IconButton onClick={handleSettingsClick}>
<Icon>settings</Icon>
</IconButton>
<IconButton onClick={handleDeleteClick}>
<Icon>delete</Icon>
</IconButton>
</Stack>
)}
{children}
</div>
);
}
);
//Контроль свойств компонента - элемент макета
LayoutItem.propTypes = {
style: PropTypes.object,
className: PropTypes.string,
onMouseDown: PropTypes.func,
onMouseUp: PropTypes.func,
onTouchEnd: PropTypes.func,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
onSettingsClick: PropTypes.func,
onDeleteClick: PropTypes.func,
item: PropTypes.object.isRequired,
editMode: PropTypes.bool,
selected: PropTypes.bool
};
//----------------
//Интерфейс модуля
//----------------
export { LayoutItem };

View File

@ -1,40 +0,0 @@
:root {
--border-color: #dee2e6;
--layout-bg: #ffffff;
}
.layout {
background-color: var(--layout-bg);
}
.layout-item__container {
border: 1px solid var(--border-color);
border-radius: 4px;
}
.component-editor__wrap {
}
.component-editor__container {
padding: 10px;
}
.component-editor__divider {
padding-top: 20px;
}
.component-view__wrap {
height: 100%;
}
.component-view__container {
height: 100%;
overflow: auto;
padding: 10px;
}
.component-view__container__empty {
display: flex;
align-items: center;
justify-content: center;
}

View File

@ -1,249 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Корневой компонент
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState, useContext } from "react"; //Классы React
import { Responsive, WidthProvider } from "react-grid-layout"; //Адаптивный макет
import { Box, Grid, Stack, Menu, MenuItem, IconButton, Icon, Fab } from "@mui/material"; //Интерфейсные элементы
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Рабочая область приложения
import { genGUID } from "../../core/utils"; //Общие вспомогательные функции
import { LayoutItem } from "./layout_item"; //Элемент макета
import { ComponentView } from "./component_view"; //Представление компонента панели
import { ComponentEditor } from "./component_editor"; //Редактор свойств компонента панели
import { COMPONETNS } from "./components/components"; //Описание доступных компонентов
import "react-grid-layout/css/styles.css"; //Стили для адаптивного макета
import "react-resizable/css/styles.css"; //Стили для адаптивного макета
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER: { display: "flex" },
GRID_CONTAINER: { height: `calc(100vh - ${APP_BAR_HEIGHT})` },
GRID_ITEM_INSPECTOR: { backgroundColor: "#e9ecef" },
FAB_EDIT: { position: "absolute", top: 12, right: 12, zIndex: 2000 }
};
//Заголовоки по умолчанию
const PANEL_CAPTION_EDIT_MODE = "Редактор панелей";
const PANEL_CAPTION_EXECUTE_MODE = "Исполнение панели";
//Начальное состояние размера макета
const INITIAL_BREAKPOINT = "lg";
//Начальное состояние макета
const INITIAL_LAYOUTS = {
[INITIAL_BREAKPOINT]: []
};
//-----------
//Тело модуля
//-----------
//Обёрдка для динамического макета
const ResponsiveGridLayout = WidthProvider(Responsive);
//Корневой компонент редактора панелей
const PanelsEditor = () => {
//Собственное состояние
const [components, setComponents] = useState({});
const [valueProviders, setValueProviders] = useState({});
const [layouts, setLayouts] = useState(INITIAL_LAYOUTS);
const [breakpoint, setBreakpoint] = useState(INITIAL_BREAKPOINT);
const [editMode, setEditMode] = useState(true);
const [editComponent, setEditComponent] = useState(null);
const [addMenuAnchorEl, setAddMenuAnchorEl] = useState(null);
//Подключение к контексту приложения
const { setAppBarTitle } = useContext(ApplicationСtx);
//Добвление компонента в макет
const addComponent = component => {
const id = genGUID();
setLayouts(pv => ({ ...pv, [breakpoint]: [...pv[breakpoint], { i: id, x: 0, y: 0, w: 4, h: 10 }] }));
setComponents(pv => ({ ...pv, [id]: { ...component } }));
};
//Удаление компонента из макета
const deleteComponent = id => {
setLayouts(pv => ({ ...pv, [breakpoint]: layouts[breakpoint].filter(item => item.i !== id) }));
setComponents(pv => ({ ...pv, [id]: { ...pv[id], deleted: true } }));
if (valueProviders[id]) {
const vPTmp = { ...valueProviders };
delete vPTmp[id];
setValueProviders(vPTmp);
}
editComponent === id && closeComponentSettingsEditor();
};
//Включение/выключение режима редиктирования
const toggleEditMode = () => {
if (!editMode) setAppBarTitle(PANEL_CAPTION_EDIT_MODE);
else setAppBarTitle(PANEL_CAPTION_EXECUTE_MODE);
setEditMode(!editMode);
};
//Открытие редактора настроек компонента
const openComponentSettingsEditor = id => setEditComponent(id);
//Закрытие реактора настроек компонента
const closeComponentSettingsEditor = () => setEditComponent(null);
//Открытие/сокрытие меню добавления
const toggleAddMenu = target => setAddMenuAnchorEl(target instanceof Element ? target : null);
//При изменении размера холста
const handleBreakpointChange = breakpoint => setBreakpoint(breakpoint);
//При изменении состояния макета
const handleLayoutChange = (currentLayout, layouts) => setLayouts(layouts);
//При нажатии на кнопку добалвения
const handleAddClick = e => toggleAddMenu(e.currentTarget);
//При выборе элемента меню добавления
const handleAddMenuItemClick = component => {
toggleAddMenu();
addComponent(component);
};
//При изменении значений в компоненте
const handleComponentValuesChange = (id, values) => setValueProviders(pv => ({ ...pv, [id]: { ...values } }));
//При нажатии на настройки компонента
const handleComponentSettingsClick = id => (editComponent === id ? closeComponentSettingsEditor() : openComponentSettingsEditor(id));
//При изменении настроек компонента
const handleComponentSettingsChange = ({ id = null, settings = {}, providedValues = [], closeEditor = false } = {}) => {
if (id && components[id]) {
const providedValuesInit = providedValues.reduce((res, providedValue) => ({ ...res, [providedValue]: undefined }), {});
if (valueProviders[id]) {
const vPTmp = { ...valueProviders[id] };
Object.keys(valueProviders[id]).forEach(key => !providedValues.includes(key) && delete vPTmp[key]);
setValueProviders(pv => ({ ...pv, [id]: { ...providedValuesInit, ...vPTmp } }));
} else setValueProviders(pv => ({ ...pv, [id]: providedValuesInit }));
setComponents(pv => ({ ...pv, [editComponent]: { ...pv[editComponent], settings: { ...settings } } }));
if (closeEditor === true) closeComponentSettingsEditor();
}
};
//При удалении компоненета
const handleComponentDeleteClick = id => deleteComponent(id);
//При подключении к странице
useEffect(() => {
addComponent(COMPONETNS[0]);
addComponent(COMPONETNS[3]);
addComponent(COMPONETNS[4]);
//addComponent(COMPONETNS[1]);
//addComponent(COMPONETNS[2]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//Текущие значения панели
const values = Object.keys(valueProviders).reduce((res, key) => ({ ...res, ...valueProviders[key] }), {});
//Меню добавления
const addMenu = (
<Menu anchorEl={addMenuAnchorEl} open={Boolean(addMenuAnchorEl)} onClose={toggleAddMenu}>
{COMPONETNS.map((comp, i) => (
<MenuItem key={i} onClick={() => handleAddMenuItemClick(comp)}>
{comp.name}
</MenuItem>
))}
</Menu>
);
//Кнопка редактирования
const editButton = !editMode && (
<Fab sx={STYLES.FAB_EDIT} size={"small"} color={"grey.700"} title={"Редактировать"} onClick={toggleEditMode}>
<Icon>edit</Icon>
</Fab>
);
//Панель инструмментов
const toolBar = (
<Stack direction={"row"} p={1}>
<IconButton onClick={toggleEditMode} title={"Запустить"}>
<Icon>play_arrow</Icon>
</IconButton>
<IconButton onClick={handleAddClick} title={"Добавить элемент"}>
<Icon>add</Icon>
</IconButton>
</Stack>
);
//Генерация содержимого
return (
<Box sx={STYLES.CONTAINER}>
{editButton}
{addMenu}
<Grid container sx={STYLES.GRID_CONTAINER} columns={25}>
<Grid item xs={editMode ? 20 : 25}>
<ResponsiveGridLayout
rowHeight={5}
className={"layout"}
layouts={layouts}
breakpoints={{ lg: 1200 }}
cols={{ lg: 12 }}
onBreakpointChange={handleBreakpointChange}
onLayoutChange={handleLayoutChange}
useCSSTransforms={true}
compactType={"vertical"}
isDraggable={editMode}
isResizable={editMode}
>
{layouts[breakpoint].map(item => (
<LayoutItem
key={item.i}
onSettingsClick={handleComponentSettingsClick}
onDeleteClick={handleComponentDeleteClick}
item={item}
editMode={editMode}
selected={editMode && editComponent === item.i}
>
<ComponentView
id={item.i}
path={components[item.i]?.path}
settings={components[item.i]?.settings}
values={values}
onValuesChange={handleComponentValuesChange}
/>
</LayoutItem>
))}
</ResponsiveGridLayout>
</Grid>
{editMode && (
<Grid item xs={5} sx={STYLES.GRID_ITEM_INSPECTOR}>
{toolBar}
{editComponent && (
<>
<ComponentEditor
id={editComponent}
path={components[editComponent].path}
settings={components[editComponent].settings}
valueProviders={valueProviders}
onSettingsChange={handleComponentSettingsChange}
/>
</>
)}
</Grid>
)}
</Grid>
</Box>
);
};
//----------------
//Интерфейс модуля
//----------------
export { PanelsEditor };

View File

@ -23,13 +23,6 @@ export const PANEL_UNITS = {
PROJECT_STAGE_ARTS: "PROJECT_STAGE_ARTS"
};
//Общие стили
export const COMMON_PROJECTS_STYLES = {
FULL_SCREEN_DIALOG_CONTENT: {
padding: 0
}
};
//-----------
//Тело модуля
//-----------
@ -257,7 +250,6 @@ export const rowExpandRender = ({
columnsDef,
row,
pOnlineShowDocument,
pOnlineShowUnit,
showStages,
showPayNotes,
showCostNotes,
@ -283,55 +275,42 @@ export const rowExpandRender = ({
const linkButtons = () =>
panelUnit === PANEL_UNITS.PROJECTS ? (
<>
<Button variant="outlined" onClick={() => showStages({ sender: row })}>
<Button fullWidth variant="contained" onClick={() => showStages({ sender: row })}>
Этапы
</Button>
<Button variant="outlined" onClick={() => pOnlineShowDocument({ unitCode: "Projects", document: row.NRN, modal: false })}>
К проекту
<Button fullWidth variant="contained" onClick={() => pOnlineShowDocument({ unitCode: "Projects", document: row.NRN })}>
В раздел
</Button>
</>
) : panelUnit === PANEL_UNITS.PROJECT_STAGES ? (
<>
<Button variant="outlined" onClick={() => showStageArts({ sender: row })}>
<Button fullWidth variant="contained" onClick={() => showStageArts({ sender: row })}>
Статьи
</Button>
<Button variant="outlined" onClick={() => showContracts({ sender: row })}>
<Button fullWidth variant="contained" onClick={() => showContracts({ sender: row })}>
Сисполнители
</Button>
<Button
variant="outlined"
onClick={() =>
pOnlineShowUnit({
unitCode: "Projects",
inputParameters: [
{ name: "in_RN", value: row.NPROJECT },
{ name: "in_STAGE_RN", value: row.NRN }
],
modal: false
})
}
>
К этапу
<Button fullWidth variant="contained" onClick={() => pOnlineShowDocument({ unitCode: "ProjectsStages", document: row.NRN })}>
В раздел
</Button>
</>
) : panelUnit === PANEL_UNITS.PROJECT_STAGE_CONTRACTS ? (
<Button
variant="outlined"
onClick={() => pOnlineShowDocument({ unitCode: row.SLNK_UNIT_SDOC_PREF, document: row.NLNK_DOCUMENT_SDOC_PREF, modal: false })}
fullWidth
variant="contained"
onClick={() => pOnlineShowDocument({ unitCode: row.SLNK_UNIT_SDOC_PREF, document: row.NLNK_DOCUMENT_SDOC_PREF })}
>
К договору
В раздел
</Button>
) : null;
//Сборка содержимого
return (
<Box p={2}>
<Grid container spacing={2}>
<Grid item xs={12} md={12}>
<Stack spacing={2} direction="row">
{linkButtons()}
</Stack>
<Grid item xs={12} md={1}>
<Stack spacing={2}>{linkButtons()}</Stack>
</Grid>
<Grid item xs={12} md={12}>
<Grid item xs={12} md={11}>
<Paper elevation={5}>
<Table sx={{ width: "100%" }} size="small">
<TableBody>

View File

@ -11,34 +11,23 @@ import React, { useState, useCallback, useEffect, useContext } from "react"; //
import { Box, Grid, Paper, Fab, Icon } from "@mui/material"; //Интерфейсные компоненты
import { object2Base64XML } from "../../core/utils"; //Вспомогательные процедуры и функции
import { TEXTS } from "../../../app.text"; //Тектовые ресурсы и константы
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
import { P8PDataGrid, P8P_DATA_GRID_SIZE, P8P_DATA_GRID_MORE_HEIGHT, P8P_DATA_GRID_FILTERS_HEIGHT } from "../../components/p8p_data_grid"; //Таблица данных
import { P8PDataGrid, P8P_DATA_GRID_SIZE } from "../../components/p8p_data_grid"; //Таблица данных
import { P8PFullScreenDialog } from "../../components/p8p_fullscreen_dialog"; //Полноэкранный диалог
import { P8PChart } from "../../components/p8p_chart"; //График
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { COMMON_PROJECTS_STYLES, PANEL_UNITS, headCellRender, dataCellRender, valueFormatter, rowExpandRender } from "./layouts"; //Дополнительная разметка и вёрстка клиентских элементов
import { PANEL_UNITS, headCellRender, dataCellRender, valueFormatter, rowExpandRender } from "./layouts"; //Дополнительная разметка и вёрстка клиентских элементов
import { Stages } from "./stages"; //Список этапов проекта
//---------
//Константы
//---------
//Высота графиков
const CHART_HEIGHT = "300px";
//Стили
const STYLES = {
TABLE_PROJECTS: (showCharts, morePages, filters) => ({
height: `calc(100vh - ${APP_BAR_HEIGHT} - ${showCharts ? CHART_HEIGHT : "0px"} - ${morePages ? P8P_DATA_GRID_MORE_HEIGHT : "0px"} - ${
filters ? P8P_DATA_GRID_FILTERS_HEIGHT : "0px"
} - 25px)`,
...APP_STYLES.SCROLL
}),
CHART: { maxHeight: CHART_HEIGHT, display: "flex", justifyContent: "center" },
CHART: { maxHeight: "300px", display: "flex", justifyContent: "center" },
CHART_PAPER: { height: "100%" },
CHART_FAB: { position: "absolute", top: 80, left: 16 }
};
@ -95,12 +84,11 @@ const Projects = () => {
});
setProjectsDataGrid(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= configSystemPageSize
morePages: (data.XROWS || []).length >= configSystemPageSize
}));
}
}, [
@ -231,16 +219,12 @@ const Projects = () => {
{projectsDataGrid.dataLoaded ? (
<P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS}
containerComponentProps={{
sx: STYLES.TABLE_PROJECTS(showCharts, projectsDataGrid.morePages, (projectsDataGrid.filters || []).length > 0)
}}
columnsDef={projectsDataGrid.columnsDef}
rows={projectsDataGrid.rows}
size={P8P_DATA_GRID_SIZE.SMALL}
filtersInitial={projectsDataGrid.filters}
morePages={projectsDataGrid.morePages}
reloading={projectsDataGrid.reload}
fixedHeader={true}
expandable={true}
headCellRender={headCellRender}
dataCellRender={prms => dataCellRender({ ...prms, panelUnit: PANEL_UNITS.PROJECTS, showStages })}
@ -260,11 +244,7 @@ const Projects = () => {
/>
) : null}
{projectsDataGrid.selectedProject ? (
<P8PFullScreenDialog
title={`Этапы проекта "${projectsDataGrid.selectedProject.SNAME_USL}"`}
onClose={handleStagesClose}
contentProps={{ sx: COMMON_PROJECTS_STYLES.FULL_SCREEN_DIALOG_CONTENT }}
>
<P8PFullScreenDialog title={`Этапы проекта "${projectsDataGrid.selectedProject.SNAME_USL}"`} onClose={handleStagesClose}>
<Stages
project={projectsDataGrid.selectedProject.NRN}
projectName={projectsDataGrid.selectedProject.SNAME_USL}

View File

@ -12,27 +12,13 @@ import PropTypes from "prop-types"; //Контроль свойств компо
import { Box } from "@mui/material"; //Интерфейсные компоненты
import { object2Base64XML } from "../../core/utils"; //Вспомогательные процедуры и функции
import { TEXTS } from "../../../app.text"; //Тектовые ресурсы и константы
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
import { P8PDataGrid, P8P_DATA_GRID_SIZE, P8P_DATA_GRID_FILTER_SHAPE, P8P_DATA_GRID_FILTERS_HEIGHT } from "../../components/p8p_data_grid"; //Таблица данных
import { P8PDataGrid, P8P_DATA_GRID_SIZE, P8P_DATA_GRID_FILTER_SHAPE } from "../../components/p8p_data_grid"; //Таблица данных
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { PANEL_UNITS, dataCellRender, valueFormatter } from "./layouts"; //Дополнительная разметка и вёрстка клиентских элементов
//---------
//Константы
//---------
//Стили
const STYLES = {
TABLE_ARTS: filters => ({
height: `calc(100vh - ${APP_BAR_HEIGHT} - ${filters ? P8P_DATA_GRID_FILTERS_HEIGHT : "0px"} - 16px)`,
...APP_STYLES.SCROLL
})
};
//-----------
//Тело модуля
//-----------
@ -71,9 +57,8 @@ const StageArts = ({ stage, filters }) => {
});
setStageArtsDataGrid(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: [...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: [...(data.XROWS || [])],
dataLoaded: true,
reload: false
}));
@ -114,12 +99,10 @@ const StageArts = ({ stage, filters }) => {
{stageArtsDataGrid.dataLoaded ? (
<P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS}
containerComponentProps={{ sx: STYLES.TABLE_ARTS((stageArtsDataGrid.filters || []).length > 0), elevation: 0 }}
columnsDef={stageArtsDataGrid.columnsDef}
filtersInitial={filters}
rows={stageArtsDataGrid.rows}
size={P8P_DATA_GRID_SIZE.SMALL}
fixedHeader={true}
morePages={false}
reloading={stageArtsDataGrid.reload}
dataCellRender={prms => dataCellRender({ ...prms, panelUnit: PANEL_UNITS.PROJECT_STAGE_ARTS, showCostNotes, showContracts })}

View File

@ -12,35 +12,13 @@ import PropTypes from "prop-types"; //Контроль свойств компо
import { Box } from "@mui/material"; //Интерфейсные компоненты
import { object2Base64XML } from "../../core/utils"; //Вспомогательные процедуры и функции
import { TEXTS } from "../../../app.text"; //Тектовые ресурсы и константы
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
import {
P8PDataGrid,
P8P_DATA_GRID_SIZE,
P8P_DATA_GRID_FILTER_SHAPE,
P8P_DATA_GRID_MORE_HEIGHT,
P8P_DATA_GRID_FILTERS_HEIGHT
} from "../../components/p8p_data_grid"; //Таблица данных
import { P8PDataGrid, P8P_DATA_GRID_SIZE, P8P_DATA_GRID_FILTER_SHAPE } from "../../components/p8p_data_grid"; //Таблица данных
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { PANEL_UNITS, dataCellRender, valueFormatter, rowExpandRender } from "./layouts"; //Дополнительная разметка и вёрстка клиентских элементов
//---------
//Константы
//---------
//Стили
const STYLES = {
TABLE_CONTRACTS: (morePages, filters) => ({
height: `calc(100vh - ${APP_BAR_HEIGHT} - ${morePages ? P8P_DATA_GRID_MORE_HEIGHT : "0px"} - ${
filters ? P8P_DATA_GRID_FILTERS_HEIGHT : "0px"
} - 16px)`,
...APP_STYLES.SCROLL
})
};
//-----------
//Тело модуля
//-----------
@ -89,12 +67,11 @@ const StageContracts = ({ stage, filters }) => {
});
setStageContractsDataGrid(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= configSystemPageSize
morePages: (data.XROWS || []).length >= configSystemPageSize
}));
}
}, [
@ -159,17 +136,12 @@ const StageContracts = ({ stage, filters }) => {
{stageContractsDataGrid.dataLoaded ? (
<P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS}
containerComponentProps={{
sx: STYLES.TABLE_CONTRACTS(stageContractsDataGrid.morePages, (stageContractsDataGrid.filters || []).length > 0),
elevation: 0
}}
columnsDef={stageContractsDataGrid.columnsDef}
filtersInitial={filters}
rows={stageContractsDataGrid.rows}
size={P8P_DATA_GRID_SIZE.SMALL}
morePages={stageContractsDataGrid.morePages}
reloading={stageContractsDataGrid.reload}
fixedHeader={true}
expandable={true}
dataCellRender={prms => dataCellRender({ ...prms, panelUnit: PANEL_UNITS.PROJECT_STAGE_CONTRACTS, pOnlineShowDocument })}
rowExpandRender={prms =>

View File

@ -12,15 +12,7 @@ import PropTypes from "prop-types"; //Контроль свойств компо
import { Box } from "@mui/material"; //Интерфейсные компоненты
import { object2Base64XML } from "../../core/utils"; //Вспомогательные процедуры и функции
import { TEXTS } from "../../../app.text"; //Тектовые ресурсы и константы
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
import {
P8PDataGrid,
P8P_DATA_GRID_SIZE,
P8P_DATA_GRID_FILTER_SHAPE,
P8P_DATA_GRID_MORE_HEIGHT,
P8P_DATA_GRID_FILTERS_HEIGHT
} from "../../components/p8p_data_grid"; //Таблица данных
import { P8PDataGrid, P8P_DATA_GRID_SIZE, P8P_DATA_GRID_FILTER_SHAPE } from "../../components/p8p_data_grid"; //Таблица данных
import { P8PFullScreenDialog } from "../../components/p8p_fullscreen_dialog"; //Полноэкранный диалог
import { StageArts } from "./stage_arts"; //Калькуляция этапа проекта
import { StageContracts } from "./stage_contracts"; //Договоры с соисполнителями этапа проекта
@ -28,21 +20,7 @@ import { BackEndСtx } from "../../context/backend"; //Контекст взаи
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { COMMON_PROJECTS_STYLES, PANEL_UNITS, headCellRender, dataCellRender, valueFormatter, rowExpandRender } from "./layouts"; //Дополнительная разметка и вёрстка клиентских элементов
//---------
//Константы
//---------
//Стили
const STYLES = {
TABLE_STAGES: (morePages, filters) => ({
height: `calc(100vh - ${APP_BAR_HEIGHT} - ${morePages ? P8P_DATA_GRID_MORE_HEIGHT : "0px"} - ${
filters ? P8P_DATA_GRID_FILTERS_HEIGHT : "0px"
} - 16px)`,
...APP_STYLES.SCROLL
})
};
import { PANEL_UNITS, headCellRender, dataCellRender, valueFormatter, rowExpandRender } from "./layouts"; //Дополнительная разметка и вёрстка клиентских элементов
//-----------
//Тело модуля
@ -93,12 +71,11 @@ const Stages = ({ project, projectName, filters }) => {
});
setStagesDataGrid(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= configSystemPageSize
morePages: (data.XROWS || []).length >= configSystemPageSize
}));
}
}, [
@ -177,17 +154,12 @@ const Stages = ({ project, projectName, filters }) => {
{stagesDataGrid.dataLoaded ? (
<P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS}
containerComponentProps={{
sx: STYLES.TABLE_STAGES(stagesDataGrid.morePages, (stagesDataGrid.filters || []).length > 0),
elevation: 0
}}
columnsDef={stagesDataGrid.columnsDef}
filtersInitial={filters}
rows={stagesDataGrid.rows}
size={P8P_DATA_GRID_SIZE.SMALL}
morePages={stagesDataGrid.morePages}
reloading={stagesDataGrid.reload}
fixedHeader={true}
expandable={true}
headCellRender={headCellRender}
dataCellRender={prms => dataCellRender({ ...prms, panelUnit: PANEL_UNITS.PROJECT_STAGES, showStageArts, showContracts })}
@ -196,7 +168,6 @@ const Stages = ({ project, projectName, filters }) => {
...prms,
panelUnit: PANEL_UNITS.PROJECT_STAGES,
pOnlineShowDocument,
pOnlineShowUnit,
showStageArts,
showContracts,
showPayNotes,
@ -214,7 +185,6 @@ const Stages = ({ project, projectName, filters }) => {
<P8PFullScreenDialog
title={`Договоры этапа "${stagesDataGrid.selectedStageNumb}" проекта "${projectName}"`}
onClose={handleStageContractsClose}
contentProps={{ sx: COMMON_PROJECTS_STYLES.FULL_SCREEN_DIALOG_CONTENT }}
>
<StageContracts stage={stagesDataGrid.showStageContracts} filters={stagesDataGrid.stageContractsFilters} />
</P8PFullScreenDialog>
@ -223,7 +193,6 @@ const Stages = ({ project, projectName, filters }) => {
<P8PFullScreenDialog
title={`Калькуляция этапа "${stagesDataGrid.selectedStageNumb}" проекта "${projectName}"`}
onClose={handleStageArtsClose}
contentProps={{ sx: COMMON_PROJECTS_STYLES.FULL_SCREEN_DIALOG_CONTENT }}
>
<StageArts stage={stagesDataGrid.showStageArts} filters={stagesDataGrid.stageArtsFilters} />
</P8PFullScreenDialog>

View File

@ -55,10 +55,11 @@ const PrjGraph = () => {
const data = await executeStored({ stored: "PKG_P8PANELS_PROJECTS.GRAPH", args: {}, respArg: "COUT" });
setdataGrid(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: [...(data.XDATA_GRID.rows || [])],
groups: [...(data.XDATA_GRID.groups || [])],
fixedHeader: data.XDATA_GRID.fixedHeader,
fixedColumns: data.XDATA_GRID.fixedColumns,
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: [...(data.XROWS || [])],
groups: [...(data.XGROUPS || [])],
dataLoaded: true,
reload: false
}));

View File

@ -1,170 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Фильтр
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Grid, Chip, Stack, Input, InputAdornment, IconButton, Icon } from "@mui/material"; //Интерфейсные элементы
import { FILTER_INITIAL, FILTER_ITEMS, PRICE_STRUCT_STATUS, PROJECT_STATE, FilterDialog } from "./filter_dialog"; //Компонент "Диалог фильтра"
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER: { paddingTop: "10px" },
FILTER: { maxWidth: "99vw" },
SEARCH_GRID_ITEM: { paddingRight: "15px" }
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Элемент фильтра
const FilterItem = ({ caption, value, defaultValue, onClick, onDelete }) => {
//При нажатии на элемент
const handleClick = () => (onClick ? onClick() : null);
//При нажатии на удаление элемента
const handleDelete = () => (onDelete ? onDelete() : null);
//Генерация содержимого
return (
<Chip
label={
<Stack direction={"row"} alignItems={"center"}>
<strong>{caption}</strong>:&nbsp;{value || defaultValue}
</Stack>
}
variant="outlined"
onClick={handleClick}
onDelete={onDelete ? handleDelete : null}
/>
);
};
//Контроль свойств компонента - Элемент фильтра
FilterItem.propTypes = {
caption: PropTypes.string.isRequired,
value: PropTypes.any,
defaultValue: PropTypes.string.isRequired,
onClick: PropTypes.func,
onDelete: PropTypes.func
};
//-----------
//Тело модуля
//-----------
//Фильтр
const Filter = ({ values, onChange }) => {
//Собственное состояние - отображение диалога ввода значений фильтра
const [isOpen, setIsOpen] = useState(false);
//Собственное состояние - строка поиска
const [search, setSearch] = useState(values.search);
//Передача сообщения об измении фильтра родителю
const notifyChange = values => (onChange ? onChange(values) : null);
//При закрытии диалога с сохранением значений
const handleFilterDialogOk = values => {
setIsOpen(false);
notifyChange(values);
};
//При закрытии диалога без сохранения значений
const handleFilterDialogCancel = () => setIsOpen(false);
//При нажатии на фильтр
const handleClick = () => setIsOpen(true);
//При выполнении поиска
const handleDoSearch = (clear = false) => {
if (clear === true) setSearch("");
notifyChange({ ...values, search: clear === true ? "" : search });
};
//При изменении значения в строке поиска
const handleSearchChange = e => setSearch(e.target.value);
//При нажатии клавиши в строке поиска
const handleSearchKeyPress = e => ([13, 27].includes(e.keyCode) ? handleDoSearch(e.keyCode == 27) : null);
//Формирование функции обработки очистки элемента фильтар
const buildFilterItemClearHandler = сode =>
values[сode] != FILTER_INITIAL[сode] ? () => notifyChange({ ...values, [сode]: FILTER_INITIAL[сode] }) : null;
//Генерация содержимого
return (
<Grid container sx={STYLES.CONTAINER}>
<Grid xs={10} item>
{isOpen ? <FilterDialog valuesInitial={values} onOk={handleFilterDialogOk} onCancel={handleFilterDialogCancel} /> : null}
<Stack direction="row" spacing={1} p={1} alignItems={"center"} sx={STYLES.FILTER} onClick={handleClick}>
<FilterItem
caption={"Тип заказа"}
value={values.prjType}
defaultValue={"Любой"}
onDelete={buildFilterItemClearHandler("prjType")}
/>
<FilterItem
caption={"Подразделение-ответственный"}
defaultValue={"Любое"}
value={values.insDep}
onDelete={buildFilterItemClearHandler("insDep")}
/>
<FilterItem
caption={"Статус структуры цены"}
defaultValue={"Неподдерживаемое значение"}
value={PRICE_STRUCT_STATUS.find(item => item.value == values.priceStructStatus)?.name}
onDelete={buildFilterItemClearHandler("priceStructStatus")}
/>
<FilterItem
caption={"Состояние проекта"}
defaultValue={"Неподдерживаемое значение"}
value={PROJECT_STATE.find(item => item.value == values.prjState)?.name}
onDelete={buildFilterItemClearHandler("prjState")}
/>
</Stack>
</Grid>
<Grid xs={2} item sx={STYLES.SEARCH_GRID_ITEM}>
<Input
fullWidth
placeholder="Поиск..."
value={search}
endAdornment={
<InputAdornment position="end">
<IconButton onClick={() => handleDoSearch(true)}>
<Icon>clear</Icon>
</IconButton>
<IconButton onClick={handleDoSearch}>
<Icon>search</Icon>
</IconButton>
</InputAdornment>
}
onKeyDown={handleSearchKeyPress}
onChange={handleSearchChange}
/>
</Grid>
</Grid>
);
};
//Контроль свойств компонента - Фильтр
Filter.propTypes = {
values: FILTER_ITEMS.isRequired,
onChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { FILTER_INITIAL, Filter };

View File

@ -1,147 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Диалог фильтра
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Button, Dialog, DialogTitle, DialogContent, DialogActions } from "@mui/material"; //Интерфейсные элементы
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
import { BUTTONS } from "../../../app.text"; //Типовые тексты
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { FormField } from "./layouts"; //Общие компоненты панели
//---------
//Константы
//---------
//Стили
const STYLES = {
DIALOG_CONTENT: { overflowY: "auto", ...APP_STYLES.SCROLL }
};
//Структура фильтра
const FILTER_ITEMS = PropTypes.shape({
prjType: PropTypes.string,
insDep: PropTypes.string,
priceStructStatus: PropTypes.number.isRequired,
prjState: PropTypes.number.isRequired,
search: PropTypes.string
});
//Начальное состояние фильтра
const FILTER_INITIAL = { prjType: "", insDep: "", priceStructStatus: 0, prjState: 0, search: "" };
//Статусы структуры цены
const PRICE_STRUCT_STATUS = [
{ value: 0, name: "Все" },
{ value: 1, name: "Есть статьи с расходом больше 90%" },
{ value: 2, name: "Есть статьи с перерасходом" }
];
//Состояния проекта
const PROJECT_STATE = [
{ value: 0, name: "Все" },
{ value: 1, name: "Открытые" },
{ value: 2, name: "Неоткрытые" }
];
//-----------
//Тело модуля
//-----------
//Диалог фильтра
const FilterDialog = ({ valuesInitial, onOk, onCancel }) => {
//Собственное состояние элементов фильтра
const [values, setValues] = useState({ ...valuesInitial });
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//Изменение элемента формы фильтра
const handleValueChanged = (name, value) => setValues(pv => ({ ...pv, [name]: value }));
//Сброс настроек фильтра
const handleResetClick = () => setValues({ ...FILTER_INITIAL });
//Сохранение фильтра
const handleOkClick = () => (onOk ? onOk(values) : null);
//Отмена фильтра
const handleCancelClick = () => (onCancel ? onCancel() : null);
//Выбор значения элемента формы из словаря
const selectFromDictionary = (unitCode, name, applyValue) => {
pOnlineShowDictionary({
unitCode,
showMethod: "main",
inputParameters: [{ name: "in_CODE", value: values[name] }],
callBack: res => applyValue(res.success ? [{ name, value: res.outParameters.out_CODE }] : null)
});
};
//Генерация содержимого
return (
<Dialog open onClose={handleCancelClick} fullWidth maxWidth={"md"}>
<DialogTitle>Фильтр отбора</DialogTitle>
<DialogContent sx={STYLES.DIALOG_CONTENT}>
<FormField
elementCode={"prjType"}
elementValue={values.prjType}
labelText={"Тип заказа"}
onChange={handleValueChanged}
dictionary={applyValue => selectFromDictionary("ProjectTypes", "prjType", applyValue)}
/>
<FormField
elementCode={"insDep"}
elementValue={values.insDep}
labelText={"Подразделение-ответственный"}
onChange={handleValueChanged}
dictionary={applyValue => selectFromDictionary("INS_DEPARTMENT", "insDep", applyValue)}
/>
<FormField
elementCode={"priceStructStatus"}
elementValue={values.priceStructStatus}
labelText={"Статус структуры цены"}
onChange={handleValueChanged}
list={PRICE_STRUCT_STATUS}
/>
<FormField
elementCode={"prjState"}
elementValue={values.prjState}
labelText={"Состояние проекта"}
onChange={handleValueChanged}
list={PROJECT_STATE}
/>
</DialogContent>
<DialogActions sx={STYLES.DIALOG_ACTIONS}>
<Button variant="text" onClick={handleOkClick}>
{BUTTONS.OK}
</Button>
<Button variant="text" onClick={handleResetClick}>
{BUTTONS.CLEAR}
</Button>
<Button variant="text" onClick={handleCancelClick}>
{BUTTONS.CANCEL}
</Button>
</DialogActions>
</Dialog>
);
};
//Контроль свойств компонента - Диалог фильтра
FilterDialog.propTypes = {
valuesInitial: FILTER_ITEMS.isRequired,
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { FILTER_ITEMS, FILTER_INITIAL, PRICE_STRUCT_STATUS, PROJECT_STATE, FilterDialog };

View File

@ -1,16 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Панель мониторинга: точка входа
*/
//---------------------
//Подключение библиотек
//---------------------
import { PrjInfo } from "./prj_info"; //Корневая панель информации о проектах
//----------------
//Интерфейс модуля
//----------------
export const RootClass = PrjInfo;

View File

@ -1,168 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Общие дополнительная разметка и вёрстка клиентских элементов
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, Icon, Input, InputAdornment, FormControl, Select, InputLabel, MenuItem, IconButton, Typography, Switch, Stack } from "@mui/material"; //Интерфейсные компоненты
//---------
//Константы
//---------
//Стили
const STYLES = {
STATE: value => ({ color: value === 1 ? "green" : "black" }),
COST_STATUS: color => ({ color, verticalAlign: "middle" }),
COST_READY: value => ({ color: value <= 30 ? "red" : value >= 80 ? "green" : "#e2af00" }),
TOGGLE_COLOR: checked => ({ color: checked ? "#006dd9" : "lightgrey" })
};
//-----------
//Тело модуля
//-----------
//Поле ввода формы
const FormField = ({ elementCode, elementValue, labelText, onChange, dictionary, list, type, ...other }) => {
//Значение элемента
const [value, setValue] = useState(elementValue);
//При получении нового значения из вне
useEffect(() => {
setValue(elementValue);
}, [elementValue]);
//Выбор значения из словаря
const handleDictionaryClick = () =>
dictionary ? dictionary(res => (res ? res.map(i => handleChange({ target: { name: i.name, value: i.value } })) : null)) : null;
//Изменение значения элемента (по событию)
const handleChange = e => {
setValue(e.target.value);
if (onChange) onChange(e.target.name, e.target.value);
};
//Генерация содержимого
return (
<Box p={1}>
<FormControl variant="standard" fullWidth {...other}>
{list ? (
<>
<InputLabel id={`${elementCode}Lable`} shrink>
{labelText}
</InputLabel>
<Select
labelId={`${elementCode}Lable`}
id={elementCode}
name={elementCode}
label={labelText}
value={value || value == 0 ? value : ""}
onChange={handleChange}
displayEmpty
>
{list.map((item, i) => (
<MenuItem key={i} value={item.value || item.value == 0 ? item.value : ""}>
{item.name}
</MenuItem>
))}
</Select>
</>
) : (
<>
<InputLabel {...(type == "date" ? { shrink: true } : {})} htmlFor={elementCode}>
{labelText}
</InputLabel>
<Input
id={elementCode}
name={elementCode}
value={value || value == 0 ? value : ""}
endAdornment={
dictionary ? (
<InputAdornment position="end">
<IconButton aria-label={`${elementCode} select`} onClick={handleDictionaryClick} edge="end">
<Icon>list</Icon>
</IconButton>
</InputAdornment>
) : null
}
{...(type ? { type } : {})}
onChange={handleChange}
/>
</>
)}
</FormControl>
</Box>
);
};
//Контроль свойств - Поле ввода формы
FormField.propTypes = {
elementCode: PropTypes.string.isRequired,
elementValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.instanceOf(Date)]),
labelText: PropTypes.string.isRequired,
onChange: PropTypes.func,
dictionary: PropTypes.func,
list: PropTypes.array,
type: PropTypes.string
};
//Переключатель
const Toggle = ({ labels, checked, onChange }) => {
//Обработка переключения
const handleChange = event => (onChange ? onChange(event.target.checked) : null);
//Генерация содержимого
return (
<Stack direction={"row"} spacing={1} alignItems={"center"} justifyContent={"center"}>
<Typography sx={STYLES.TOGGLE_COLOR(!checked)}>{labels[0]}</Typography>
<Switch checked={checked} size="small" onChange={handleChange} />
<Typography sx={STYLES.TOGGLE_COLOR(checked)}>{labels[1]}</Typography>
</Stack>
);
};
//Контроль свойств компонента - Переключатель
Toggle.propTypes = {
labels: PropTypes.arrayOf(PropTypes.string).isRequired,
checked: PropTypes.bool.isRequired,
onChange: PropTypes.func
};
//Формирование значения для колонки "Статус структуры цены"
const formatCostStatusValue = ({ value, onClick, type = 1 }) => {
const [text, color] =
value == 0
? ["Без отклонений", "lightgreen"]
: value == 1
? [type == 1 ? "Есть статьи с расходом более 90%" : "Расход более 90%", "#ffdf71"]
: value == 2
? [type == 1 ? "Есть статьи с перерасходом" : "Перерасход", "#eb6b6b"]
: ["Не определено", "lightgray"];
return onClick ? (
<IconButton onClick={onClick}>
<Icon sx={STYLES.COST_STATUS(color)} title={`${text}\nНажмите для детальной информации`}>
circle
</Icon>
</IconButton>
) : (
<Icon sx={STYLES.COST_STATUS(color)} title={text}>
circle
</Icon>
);
};
//Формирование значения для колонки "Готов (%, зтраты)"
const formatCostReadyValue = value => {
return <span style={STYLES.COST_READY(value)}>{value}</span>;
};
//----------------
//Интерфейс модуля
//----------------
export { STYLES as COMMON_STYLES, FormField, Toggle, formatCostStatusValue, formatCostReadyValue };

View File

@ -1,27 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Корневой компонент панели
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import { Projects } from "./projects"; //Список проектов
//-----------
//Тело модуля
//-----------
//Корневой компонент панели "Информация о проектах"
const PrjInfo = () => {
//Генерация содержимого
return <Projects />;
};
//----------------
//Интерфейс модуля
//----------------
export { PrjInfo };

View File

@ -1,70 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Список проектов
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext } from "react"; //Классы React
import { P8PDataGrid } from "../../components/p8p_data_grid"; //Таблица данных
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
import { useProjectsDataGrid } from "./projects_hooks"; //Хуки списка проектов
import { FILTER_INITIAL, Filter } from "./filter"; //Компонент "Фильтр"
import { PROJECTS_STYLES, projectDataCellRender, projectRowExpandRender } from "./projects_layouts"; //Дополнительная разметка и вёрстка клиентских элементов
//-----------
//Тело модуля
//-----------
//Список проектов
const Projects = () => {
//Собственное состояние
const [projects, setProjects] = useState({ pageNumber: 1, orders: [], filter: { ...FILTER_INITIAL } });
//Состояние таблицы проектов
const [projectsDataGrid] = useProjectsDataGrid({ ...projects.filter, pageNumber: projects.pageNumber, orders: projects.orders });
//Подключение к контексту приложения
const { pOnlineShowDocument } = useContext(ApplicationСtx);
//Отображение записи проекта в штатном разделе
const showProject = async rn => pOnlineShowDocument({ unitCode: "Projects", document: rn, modal: false });
//При изменении количества отображаемых страниц
const handlePagesCountChanged = () => setProjects(pv => ({ ...pv, pageNumber: pv.pageNumber + 1 }));
//При изменении состояния сортировки
const handleOrderChanged = ({ orders }) => setProjects(pv => ({ ...pv, orders: [...orders], pageNumber: 1 }));
//При изменении фильтра
const handleFilterChanged = values => setProjects(pv => ({ ...pv, filter: { ...values }, pageNumber: 1 }));
//Генерация содержимого
return (
<>
<Filter values={projects.filter} onChange={handleFilterChanged} />
{projectsDataGrid.init ? (
<P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS}
{...projectsDataGrid}
containerComponentProps={{ sx: PROJECTS_STYLES.DATA_GRID_CONTAINER(projectsDataGrid.morePages), elevation: 0 }}
expandable={true}
fixedHeader={true}
onPagesCountChanged={handlePagesCountChanged}
onOrderChanged={handleOrderChanged}
dataCellRender={prms => projectDataCellRender({ ...prms, showProject })}
rowExpandRender={projectRowExpandRender}
/>
) : null}
</>
);
};
//----------------
//Интерфейс модуля
//----------------
export { Projects };

View File

@ -1,82 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Список проектов: пользовательские хуки для взаимодействия с сервером
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect } from "react"; //Классы React
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { object2Base64XML, formatDateRF } from "../../core/utils"; //Вспомогательные функции
import config from "../../../app.config"; //Настройки приложения
//---------
//Константы
//---------
//Размер страницы данных
const DATA_GRID_PAGE_SIZE = config.SYSTEM.PAGE_SIZE;
//-----------
//Тело модуля
//-----------
//Получение данных проектов с сервера
const useProjectsDataGrid = ({ prjType, insDep, priceStructStatus, prjState, search, pageNumber, orders }) => {
//Собственное состояние - флаг загрузки
const [isLoading, setLoading] = useState(false);
//Собственное состояние - таблица данных
const [data, setData] = useState({ init: false, morePages: true });
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//При необходимости обновить данные таблицы
useEffect(() => {
//Загрузка данных таблицы с сервера
const loadData = async () => {
try {
setLoading(true);
const data = await executeStored({
stored: "PKG_P8PANELS_PROJECTS.INFO_PROJECTS_DG",
args: {
SPRJ_TYPE: prjType,
SINS_DEPARTMENT: insDep,
NCOST_STATUS: priceStructStatus,
NSTATE: prjState,
SSEARCH: search,
CORDERS: { VALUE: object2Base64XML(orders, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
NPAGE_NUMBER: pageNumber,
NPAGE_SIZE: DATA_GRID_PAGE_SIZE,
NINCLUDE_DEF: pageNumber == 1 ? 1 : 0
},
respArg: "COUT",
loader: true,
attributeValueProcessor: (name, val) => (["DBEGPLAN", "DENDPLAN"].includes(name) ? formatDateRF(val) : val)
});
setData(pv => ({
...pv,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef || [],
rows: pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...(pv.rows || []), ...(data.XDATA_GRID.rows || [])],
morePages: DATA_GRID_PAGE_SIZE == 0 ? false : (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE,
init: true
}));
} finally {
setLoading(false);
}
};
loadData();
}, [prjType, insDep, priceStructStatus, prjState, search, pageNumber, orders, executeStored, SERV_DATA_TYPE_CLOB]);
//Возвращаем интерфейс хука
return [data, isLoading];
};
//----------------
//Интерфейс модуля
//----------------
export { useProjectsDataGrid };

View File

@ -1,96 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Список проектов: дополнительная разметка и вёрстка клиентских элементов
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import { Icon, Stack, Paper, Link } from "@mui/material"; //Интерфейсные элементы
import { P8P_DATA_GRID_MORE_HEIGHT } from "../../components/p8p_data_grid"; //Таблица данных
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { COMMON_STYLES, formatCostStatusValue, formatCostReadyValue } from "./layouts"; //Общие стили и разметка панели
import { Stages } from "./stages"; //Компонент "Этапы проекта"
//---------
//Константы
//---------
//Высота фильтра (пиксели)
const FILTER_HEIGHT = "60px";
//Стили
const STYLES = {
DATA_GRID_CONTAINER: morePages => ({
height: `calc(100vh - ${APP_BAR_HEIGHT} - ${FILTER_HEIGHT} - ${morePages ? P8P_DATA_GRID_MORE_HEIGHT : "0px"} - 8px)`,
...APP_STYLES.SCROLL
})
};
//-----------
//Тело модуля
//-----------
//Формирование значения для колонки "Состояние" проекта
const formatPrjStateValue = value => {
const [text, icon] =
value == 0
? ["Зарегистрирован", "app_registration"]
: value == 1
? ["Открыт", "lock_open"]
: value == 2
? ["Остановлен", "do_not_disturb_on"]
: value == 3
? ["Закрыт", "lock_outline"]
: value == 4
? ["Согласован", "thumb_up_alt"]
: ["Исполнение прекращено", "block"];
return (
<Stack direction="row" gap={0.5} alignItems="center" justifyContent="center">
<Icon title={text} sx={COMMON_STYLES.STATE(value)}>
{icon}
</Icon>
</Stack>
);
};
//Форматирование ячеек таблицы "Проекты"
const projectDataCellRender = ({ row, columnDef, showProject }) => {
//Формирование представлений
switch (columnDef.name) {
case "NCOST_STATUS":
return { cellProps: { align: "center" }, data: formatCostStatusValue({ value: row[columnDef.name] }) };
case "NCOST_READY":
return { cellProps: { align: "center" }, data: formatCostReadyValue(row[columnDef.name]) };
case "NSTATE":
return { cellProps: { align: "center" }, data: formatPrjStateValue(row[columnDef.name]) };
case "SCODE":
return {
data: (
<Link component="button" align="left" underline="hover" onClick={() => showProject(row["NRN"])}>
{row[columnDef.name]}
</Link>
)
};
default:
return { data: row[columnDef.name] };
}
};
//Генерация представления расширения строки таблицы "Проектов"
const projectRowExpandRender = ({ row }) => {
return (
<Paper elevation={6}>
<Stages projectRn={row.NRN} projectCode={row.SCODE} />
</Paper>
);
};
//----------------
//Интерфейс модуля
//----------------
export { STYLES as PROJECTS_STYLES, projectDataCellRender, projectRowExpandRender };

View File

@ -1,173 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Детальная информация об этапе проекта
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Grid, Box, Typography, Paper, Drawer, IconButton, Icon } from "@mui/material"; //Интерфейсные элементы
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
import { TEXTS } from "../../../app.text"; //Тектовые ресурсы и константы
import { P8PDataGrid, P8P_DATA_GRID_SIZE } from "../../components/p8p_data_grid"; //Таблица данных
import { P8PChart } from "../../components/p8p_chart"; //График
import { P8PAppInlineError } from "../../components/p8p_app_message"; //Встраиваемое сообщение об ошибке
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { useStageDetailInfoDataGrid, useStageDetailArtsDataGrid, useStageDetailArtsChart } from "./stage_detail_hooks"; //Хуки детализации этапов проекта
import { Toggle } from "./layouts"; //Общая разметка и компоненты панели
import {
STAGE_DETAIL_STYLES,
stageDetailInfoHeadCellRender,
stageDetailInfoDataCellRender,
stageDetailArtsHeadCellRender,
stageDetailArtsDataCellRender
} from "./stage_detail_layouts"; //Дополнительная разметка и вёрстка клиентских элементов
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Данные этапа
const StageDetailData = ({ stageRn }) => {
//Собственное состояние
const [state, setState] = useState({ artsDisplayType: 0, artsChartType: 0 });
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Подключение к контексту приложения
const { pOnlineShowUnit } = useContext(ApplicationСtx);
//Подключение к контексту сообщений
const { showMsgErr } = useContext(MessagingСtx);
//Отображение журнала затрат (фактического, по рег. номеру ЛС и статьи затрат)
const showCostNotesFact = async ({ faceAccRn, artclRn }) => {
const data = await executeStored({
stored: "PKG_P8PANELS_PROJECTS.INFO_FCCOSTNOTES_FACT_SELECT",
args: { NFACEACC: faceAccRn, NFPDARTCL: artclRn }
});
if (data.NIDENT) pOnlineShowUnit({ unitCode: "CostNotes", inputParameters: [{ name: "in_IDENT", value: data.NIDENT }] });
else showMsgErr(TEXTS.NO_DATA_FOUND);
};
//Состояние таблицы с информацией об этапе
const [stageDeatilInfoDataGrid] = useStageDetailInfoDataGrid({ stageRn });
//Состояние таблицы с данными структуры цены
const [stageDeatilArtsDataGrid] = useStageDetailArtsDataGrid({ stageRn });
//Состояние графика с данными структуры цены
const [stageDeatilArtsChart] = useStageDetailArtsChart({ stageRn, display: state.artsDisplayType == 1, type: state.artsChartType });
//При изменении способа отображения структуры цены
const handleArtsDisplayTypeChange = checked => setState(pv => ({ ...pv, artsDisplayType: checked ? 1 : 0 }));
//При изменении типа данных графика структуры цены
const handleArtsChartTypeChange = checked => setState(pv => ({ ...pv, artsChartType: checked ? 1 : 0 }));
//Отработка нажатия на график
const handleChartClick = ({ item }) =>
state.artsChartType === 1 && item.NFACEACC && item.NFPDARTCL
? showCostNotesFact({ faceAccRn: item.NFACEACC, artclRn: item.NFPDARTCL })
: null;
//Генерация содержимого
return (
<Grid container spacing={2} sx={STAGE_DETAIL_STYLES.DATA_AREA_CONTAINER}>
<Grid item xs={5}>
<Typography variant={"h6"} sx={STAGE_DETAIL_STYLES.DATA_AREA_HEADER}>
Сведения
</Typography>
{stageDeatilInfoDataGrid.init ? (
<P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS}
containerComponentProps={{ sx: STAGE_DETAIL_STYLES.DATA_AREA, elevation: 6 }}
{...stageDeatilInfoDataGrid}
size={P8P_DATA_GRID_SIZE.SMALL}
fixedHeader={true}
headCellRender={stageDetailInfoHeadCellRender}
dataCellRender={stageDetailInfoDataCellRender}
/>
) : null}
</Grid>
<Grid item xs={7}>
<Box sx={STAGE_DETAIL_STYLES.DATA_AREA_HEADER_CONTAINER}>
<Typography variant={"h6"} sx={STAGE_DETAIL_STYLES.DATA_AREA_HEADER}>
Структура цены
</Typography>
<Toggle labels={["Таблица", "График"]} checked={state.artsDisplayType === 1} onChange={handleArtsDisplayTypeChange} />
</Box>
{state.artsDisplayType === 0 ? (
stageDeatilArtsDataGrid.init ? (
<P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS}
containerComponentProps={{ sx: STAGE_DETAIL_STYLES.DATA_AREA, elevation: 6 }}
{...stageDeatilArtsDataGrid}
size={P8P_DATA_GRID_SIZE.SMALL}
fixedHeader={true}
headCellRender={stageDetailArtsHeadCellRender}
dataCellRender={prms => stageDetailArtsDataCellRender({ ...prms, showCostNotesFact })}
/>
) : null
) : (
<Paper elevation={6} sx={STAGE_DETAIL_STYLES.DATA_AREA}>
<Box sx={STAGE_DETAIL_STYLES.CHART_CONTAINER}>
<Toggle labels={["План", "Факт"]} checked={state.artsChartType === 1} onChange={handleArtsChartTypeChange} />
{stageDeatilArtsDataGrid?.rows?.length > 0 ? (
stageDeatilArtsChart.init ? (
<P8PChart style={STAGE_DETAIL_STYLES.CHART} {...stageDeatilArtsChart} onClick={handleChartClick} />
) : null
) : (
<P8PAppInlineError text={TEXTS.NO_DATA_FOUND} />
)}
</Box>
</Paper>
)}
</Grid>
</Grid>
);
};
//Контроль свойств компонента - Данные этапа
StageDetailData.propTypes = {
stageRn: PropTypes.number
};
//-----------
//Тело модуля
//-----------
//Детальная информация об этапе проекта
const StageDetail = ({ stageRn, stageName, isOpen, onClose }) => {
return (
<Drawer anchor={"right"} open={isOpen} onClose={onClose} sx={STAGE_DETAIL_STYLES.STAGE_DETAIL_DRAWER}>
<Box sx={STAGE_DETAIL_STYLES.STAGE_DETAIL_HEADER}>
<IconButton sx={STAGE_DETAIL_STYLES.STAGE_DETAIL_CLOSE_BUTTON} size={"small"} onClick={onClose}>
<Icon>close</Icon>
</IconButton>
<Typography variant={"h6"} color={"white"} pl={2}>{`Этап: ${stageName}`}</Typography>
</Box>
<StageDetailData stageRn={stageRn} />
</Drawer>
);
};
//Контроль свойств компонента - Детальная информация об этапе проекта
StageDetail.propTypes = {
stageRn: PropTypes.number,
stageName: PropTypes.string,
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { StageDetail };

View File

@ -1,126 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Детальная информация об этапе проекта: пользовательские хуки для взаимодействия с сервером
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect } from "react"; //Классы React
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
//-----------
//Тело модуля
//-----------
//Детали этапа проекта - информация об этапе
const useStageDetailInfoDataGrid = ({ stageRn }) => {
//Собственное состояние - флаг загрузки
const [isLoading, setLoading] = useState(false);
//Собственное состояние - таблица данных
const [data, setData] = useState({ init: false });
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При необходимости обновить данные таблицы
useEffect(() => {
//Загрузка данных таблицы с сервера
const loadData = async () => {
try {
setLoading(true);
const data = await executeStored({
stored: "PKG_P8PANELS_PROJECTS.INFO_STAGE_DTL_DG",
args: { NPROJECTSTAGE: stageRn },
respArg: "COUT",
loader: true
});
setData(pv => ({ ...pv, ...data.XDATA_GRID, init: true }));
} finally {
setLoading(false);
}
};
if (stageRn) loadData();
}, [stageRn, executeStored]);
//Возвращаем интерфейс хука
return [data, isLoading];
};
//Детали этапа проекта - структура цены - таблица данных
const useStageDetailArtsDataGrid = ({ stageRn }) => {
//Собственное состояние - флаг загрузки
const [isLoading, setLoading] = useState(false);
//Собственное состояние - таблица данных
const [data, setData] = useState({ init: false });
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//При необходимости обновить данные таблицы
useEffect(() => {
//Загрузка данных таблицы с сервера
const loadData = async () => {
try {
setLoading(true);
const artsData = await executeStored({
stored: "PKG_P8PANELS_PROJECTS.INFO_STAGE_ARTS_DG",
args: { NPROJECTSTAGE: stageRn },
respArg: "COUT",
loader: true
});
setData(pv => ({ ...pv, ...artsData.XDATA_GRID, init: true }));
} finally {
setLoading(false);
}
};
if (stageRn) loadData();
}, [stageRn, executeStored, SERV_DATA_TYPE_CLOB]);
//Возвращаем интерфейс хука
return [data, isLoading];
};
//Детали этапа проекта - структура цены - график
const useStageDetailArtsChart = ({ stageRn, display, type }) => {
//Собственное состояние - флаг загрузки
const [isLoading, setLoading] = useState(false);
//Собственное состояние - график
const [data, setData] = useState({ init: false, currentType: null });
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При необходимости обновить данные таблицы
useEffect(() => {
//Загрузка данных таблицы с сервера
const loadData = async () => {
try {
setLoading(true);
const data = await executeStored({
stored: "PKG_P8PANELS_PROJECTS.INFO_STAGE_ARTS_CHART",
args: { NPROJECTSTAGE: stageRn, NTYPE: type },
respArg: "COUT",
loader: true
});
setData(pv => ({ ...pv, ...data.XCHART, currentType: type, init: true }));
} finally {
setLoading(false);
}
};
if (stageRn && display && data.currentType != type) loadData();
}, [stageRn, display, type, data.currentType, executeStored]);
//Возвращаем интерфейс хука
return [data, isLoading];
};
//----------------
//Интерфейс модуля
//----------------
export { useStageDetailInfoDataGrid, useStageDetailArtsDataGrid, useStageDetailArtsChart };

View File

@ -1,148 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Детальная информация об этапе проекта: дополнительная разметка и вёрстка клиентских элементов
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import { Link } from "@mui/material"; //Интерфейсные элементы
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
import { formatNumberRFCurrency } from "../../core/utils"; //Вспомогательные функции
import { formatCostStatusValue } from "./layouts"; //Общие стили и разметка панели
import { formatStageStatusValue } from "./stages_layouts"; //Cтили и разметка списка этапов проекта
//---------
//Константы
//---------
//Высота заголовка информационного блока
const DATA_AREA_HEADER_HEIGHT = "52px";
//Стили
const STYLES = {
STAGE_DETAIL_DRAWER: { flexShrink: 0, [`& .MuiDrawer-paper`]: { width: "70%", boxSizing: "border-box", ...APP_STYLES.SCROLL } },
STAGE_DETAIL_HEADER: {
height: APP_BAR_HEIGHT,
paddingLeft: "24px",
backgroundColor: "#1976d2",
display: "flex",
alignItems: "center",
justifyContent: "flex-start"
},
STAGE_DETAIL_CLOSE_BUTTON: { color: "white", marginBottom: "3px" },
DATA_AREA_CONTAINER: { paddingLeft: "10px", paddingRight: "10px" },
DATA_AREA: { height: `calc(100vh - ${APP_BAR_HEIGHT} - ${DATA_AREA_HEADER_HEIGHT} - 10px)`, overflowY: "auto", ...APP_STYLES.SCROLL },
DATA_AREA_HEADER_CONTAINER: { display: "flex", justifyContent: "space-between" },
DATA_AREA_HEADER: { paddingTop: "10px", paddingBottom: "10px" },
DATA_GRID_HEADER: { fontSize: "10pt", padding: "6px 10px" },
DATA_GRID_CELL: value => ({ fontSize: "9pt", padding: "6px 10px", ...(value ? { color: value > 0 ? "green" : "red" } : {}) }),
CHART_CONTAINER: { paddingTop: "20px" },
CHART: { maxHeight: "60vh", display: "flex", justifyContent: "center" }
};
//-----------
//Тело модуля
//-----------
//Форматирование заголовков колонок таблицы "Сведения"
const stageDetailInfoHeadCellRender = ({ columnDef }) => {
//Инициализируем общий стиль ячеек
let cellStyle = STYLES.DATA_GRID_HEADER;
//Формирование представлений
switch (columnDef.name) {
case "SATTR":
return { cellStyle, stackProps: { justifyContent: "left" } };
case "SVALUE":
return { cellStyle, stackProps: { justifyContent: "right" } };
default:
return { cellStyle: cellStyle };
}
};
//Форматирование ячеек строк таблицы "Сведения"
const stageDetailInfoDataCellRender = ({ row, columnDef }) => {
//Инициализируем общий стиль ячеек
let cellStyle = STYLES.DATA_GRID_CELL();
//Формирование представлений
switch (columnDef.name) {
case "SATTR":
return { cellStyle: { ...cellStyle, color: "#1976d2" }, cellProps: { align: "left" } };
case "SVALUE": {
const res = { cellStyle, cellProps: { align: "right" } };
if (["NCOST_SUM", "NSTAGE_COST_SUM"].includes(row["SCODE"]))
res.data = row["SVALUE"] || row["SVALUE"] === 0 ? formatNumberRFCurrency(row["SVALUE"]) : "-";
if (row["SCODE"] == "NSTATE")
res.data = formatStageStatusValue({ value: parseInt(row["SVALUE"]), addText: true, justifyContent: "right" });
return res;
}
default:
return { cellStyle };
}
};
//Форматирование заголовков колонок таблицы "Структура затрат"
const stageDetailArtsHeadCellRender = ({ columnDef }) => {
//Инициализируем общий стиль ячеек
let cellStyle = STYLES.DATA_GRID_HEADER;
//Формирование представлений
switch (columnDef.name) {
case "NSTATE":
return { cellStyle: { ...cellStyle, justifyContent: "center" }, stackStyle: { justifyContent: "center" } };
default:
return { cellStyle: cellStyle };
}
};
//Форматирование ячеек строк таблицы "Структура затрат"
const stageDetailArtsDataCellRender = ({ row, columnDef, showCostNotesFact }) => {
//Инициализируем общий стиль ячеек
let cellStyle = STYLES.DATA_GRID_CELL;
//Формирование представлений
switch (columnDef.name) {
case "NCOST_STATUS":
return {
cellProps: { align: "center" },
data: formatCostStatusValue({ value: row[columnDef.name], type: 0 })
};
case "NPLAN_SUM":
case "NPLAN_FACT_SUM":
return {
cellStyle: cellStyle(columnDef.name == "NPLAN_FACT_SUM" ? row[columnDef.name] : null),
data: row[columnDef.name] || row[columnDef.name] === 0 ? formatNumberRFCurrency(row[columnDef.name]) : "-"
};
case "NFACT_SUM":
return {
cellStyle: cellStyle(),
data:
row[columnDef.name] || row[columnDef.name] === 0 ? (
row[columnDef.name] > 0 ? (
<Link component="button" onClick={() => showCostNotesFact({ faceAccRn: row["NFACEACC"], artclRn: row["NFPDARTCL"] })}>
{formatNumberRFCurrency(row[columnDef.name])}
</Link>
) : (
formatNumberRFCurrency(row[columnDef.name])
)
) : (
"-"
)
};
default:
return { cellStyle: cellStyle(), data: row[columnDef.name] };
}
};
//----------------
//Интерфейс модуля
//----------------
export {
STYLES as STAGE_DETAIL_STYLES,
stageDetailInfoHeadCellRender,
stageDetailInfoDataCellRender,
stageDetailArtsHeadCellRender,
stageDetailArtsDataCellRender
};

View File

@ -1,95 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Список этапов проекта
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Typography } from "@mui/material"; //Интерфейсные элементы
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
import { P8PDataGrid } from "../../components/p8p_data_grid"; //Таблица данных
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { useStagesDataGrid } from "./stages_hooks"; //Хуки списка этапов проекта
import { STAGES_STYLES, projectStageDataCellRender } from "./stages_layouts"; //Дополнительная разметка и вёрстка клиентских элементов
import { StageDetail } from "./stage_detail"; //Компонент "Информация об этапе проекта"
//-----------
//Тело модуля
//-----------
//Список этапов проекта
const Stages = ({ projectRn, projectCode }) => {
//Собственное состояние
const [stages, setStages] = useState({ pageNumber: 1, orders: [] });
//Состояние таблицы этапов
const [stagesDataGrid] = useStagesDataGrid({ ...stages, projectRn });
//Состояние информации о этапе
const [stageInfo, setStageInfo] = useState({ showInfo: false, stage: null, sFaceAcc: null });
//Подключение к контексту приложения
const { pOnlineShowUnit } = useContext(ApplicationСtx);
//Отображение записи этапа проекта в штатном разделе
const showProjectStage = (prn, rn) => {
pOnlineShowUnit({
unitCode: "Projects",
inputParameters: [
{ name: "in_RN", value: prn },
{ name: "in_STAGE_RN", value: rn }
],
modal: false
});
};
//Отображение деталей этапа
const showStageDetails = stage => setStageInfo(pv => ({ ...pv, showInfo: true, stage: stage["NRN"], sFaceAcc: stage["SFACEACC"] }));
//При изменении количества отображаемых страниц
const handlePagesCountChanged = () => setStages(pv => ({ ...pv, pageNumber: pv.pageNumber + 1 }));
//При изменении состояния сортировки
const handleOrderChanged = ({ orders }) => setStages(pv => ({ ...pv, orders: [...orders], pageNumber: 1 }));
//Генерация содержимого
return stagesDataGrid.init ? (
<>
<div style={STAGES_STYLES.CONTAINER}>
<Typography variant={"subtitle2"} sx={STAGES_STYLES.TITLE}>
{`Этапы проекта "${projectCode}"`}
</Typography>
<P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS}
{...stagesDataGrid}
containerComponentProps={{ sx: STAGES_STYLES.DATA_GRID_CONTAINER, elevation: 0 }}
onPagesCountChanged={handlePagesCountChanged}
onOrderChanged={handleOrderChanged}
dataCellRender={prms => projectStageDataCellRender({ ...prms, showProjectStage, showStageDetails })}
/>
</div>
<StageDetail
stageRn={stageInfo.stage}
stageName={stageInfo.sFaceAcc}
isOpen={stageInfo.showInfo}
onClose={() => setStageInfo(pv => ({ ...pv, showInfo: false, stage: null, sFaceAcc: null }))}
/>
</>
) : null;
};
//Контроль свойств компонента - Список этапов проекта
Stages.propTypes = {
projectRn: PropTypes.number.isRequired,
projectCode: PropTypes.string.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { Stages };

View File

@ -1,78 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Список этапов проекта: пользовательские хуки для взаимодействия с сервером
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect } from "react"; //Классы React
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { object2Base64XML, formatDateRF } from "../../core/utils"; //Вспомогательные функции
import config from "../../../app.config"; //Настройки приложения
//---------
//Константы
//---------
//Размер страницы данных
const DATA_GRID_PAGE_SIZE = config.SYSTEM.PAGE_SIZE;
//-----------
//Тело модуля
//-----------
//Этапы проекта
const useStagesDataGrid = ({ projectRn, pageNumber, orders }) => {
//Собственное состояние - флаг загрузки
const [isLoading, setLoading] = useState(false);
//Собственное состояние - таблица данных
const [data, setData] = useState({ init: false, morePages: true });
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//При необходимости обновить данные таблицы
useEffect(() => {
//Загрузка данных таблицы с сервера
const loadData = async () => {
try {
setLoading(true);
const data = await executeStored({
stored: "PKG_P8PANELS_PROJECTS.INFO_STAGES_DG",
args: {
NPROJECT: projectRn,
CORDERS: { VALUE: object2Base64XML(orders, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
NPAGE_NUMBER: pageNumber,
NPAGE_SIZE: DATA_GRID_PAGE_SIZE,
NINCLUDE_DEF: pageNumber == 1 ? 1 : 0
},
respArg: "COUT",
loader: true,
attributeValueProcessor: (name, val) => (["DBEGPLAN", "DENDPLAN"].includes(name) ? formatDateRF(val) : val)
});
setData(pv => ({
...pv,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef || [],
rows: pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...(pv.rows || []), ...(data.XDATA_GRID.rows || [])],
morePages: DATA_GRID_PAGE_SIZE == 0 ? false : (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE,
init: true
}));
} finally {
setLoading(false);
}
};
if (projectRn) loadData();
}, [projectRn, orders, pageNumber, executeStored, SERV_DATA_TYPE_CLOB]);
//Возвращаем интерфейс хука
return [data, isLoading];
};
//----------------
//Интерфейс модуля
//----------------
export { useStagesDataGrid };

View File

@ -1,86 +0,0 @@
/*
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
Список этапов проекта: дополнительная разметка и вёрстка клиентских элементов
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import { Icon, Stack, Link } from "@mui/material"; //Интерфейсные элементы
import { formatNumberRFCurrency } from "../../core/utils"; //Спомогательные функции
import { COMMON_STYLES, formatCostStatusValue, formatCostReadyValue } from "./layouts"; //Общие стили и разметка панели
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER: { textAlign: "center", paddingTop: "10px", backgroundColor: "lightcyan" },
TITLE: { fontSize: "13pt", paddingBottom: "10px" },
DATA_GRID_CONTAINER: { backgroundColor: "lightcyan" }
};
//-----------
//Тело модуля
//-----------
//Формирование значения для колонки "Состояние" этапа
const formatStageStatusValue = ({ value, addText = false, justifyContent = "center" }) => {
const [text, icon] =
value == 0
? ["Зарегистрирован", "app_registration"]
: value == 1
? ["Открыт", "lock_open"]
: value == 2
? ["Закрыт", "lock_outline"]
: value == 3
? ["Согласован", "thumb_up_alt"]
: value == 4
? ["Исполнение прекращено", "block"]
: ["Остановлен", "do_not_disturb_on"];
return (
<Stack direction="row" gap={0.5} alignItems={"center"} justifyContent={justifyContent || "center"}>
<Icon title={text} sx={COMMON_STYLES.STATE(value)}>
{icon}
</Icon>
{addText == true ? text : null}
</Stack>
);
};
//Форматирование ячеек таблицы "Этапы проекта"
const projectStageDataCellRender = ({ row, columnDef, showProjectStage, showStageDetails }) => {
//Формирование представлений
switch (columnDef.name) {
case "NCOST_STATUS":
return {
cellProps: { align: "center" },
data: formatCostStatusValue({ value: row[columnDef.name], onClick: () => showStageDetails(row) })
};
case "NCOST_READY":
return { cellProps: { align: "center" }, data: formatCostReadyValue(row[columnDef.name]) };
case "NSTATE":
return { cellProps: { align: "center" }, data: formatStageStatusValue({ value: row[columnDef.name] }) };
case "SFACEACC":
return {
data: (
<Link component="button" align="left" underline="hover" onClick={() => showProjectStage(row["NPRN"], row["NRN"])}>
{row[columnDef.name]}
</Link>
)
};
case "NCOST_SUM":
return { data: formatNumberRFCurrency(row[columnDef.name]) };
default:
return { data: row[columnDef.name] };
}
};
//----------------
//Интерфейс модуля
//----------------
export { STYLES as STAGES_STYLES, projectStageDataCellRender, formatStageStatusValue };

View File

@ -57,12 +57,11 @@ const LabFactRptDtl = ({ periodId, title, onHide }) => {
});
setFactRptDtl(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= configSystemPageSize
morePages: (data.XROWS || []).length >= configSystemPageSize
}));
}
}, [

View File

@ -56,12 +56,11 @@ const LabPlanFOTDtl = ({ periodId, title, onHide }) => {
});
setPlanFOTDtl(pv => ({
...pv,
...data.XDATA_GRID,
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])],
dataLoaded: true,
reload: false,
morePages: (data.XDATA_GRID.rows || []).length >= configSystemPageSize
morePages: (data.XROWS || []).length >= configSystemPageSize
}));
}
}, [

Some files were not shown because too many files have changed in this diff Show More