WEB-приложение - инициализация
This commit is contained in:
parent
841c7c1b94
commit
c9b12981cf
19
.eslintrc.json
Normal file
19
.eslintrc.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:react/recommended", "plugin:react-hooks/recommended"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"plugins": ["react", "react-hooks"],
|
||||
"settings": {
|
||||
"react": { "version": "detect" }
|
||||
},
|
||||
"rules": {}
|
||||
}
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"tabWidth": 4,
|
||||
"printWidth": 150,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid"
|
||||
}
|
20
app.config.js
Normal file
20
app.config.js
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Настройки приложения
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Системеые параметры
|
||||
const SYSTEM = {
|
||||
//Адрес сервера приложений "ПАРУС 8 Онлайн"
|
||||
SERVER: "../../DicAcc/"
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { SYSTEM };
|
57
app.text.js
Normal file
57
app.text.js
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Текстовые ресурсы и константы
|
||||
*/
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
//Заголовки
|
||||
export const TITLES = {
|
||||
MAIN_MENU: "Доступные панели", //Главное меню
|
||||
INFO: "Информация", //Информационный блок
|
||||
WARN: "Предупреждение", //Блок предупреждения
|
||||
ERR: "Ошибка", //Информация об ошибке
|
||||
DEFAULT_PANELS_GROUP: "Без привязки к группе" //Заголовок группы панелей по умолчанию
|
||||
};
|
||||
|
||||
//Текст
|
||||
export const TEXTS = {
|
||||
LOADING: "Ожидайте...", //Ожидание завершения процесса
|
||||
NO_DATA_FOUND: "Данных не найдено" //Отсутствие данных
|
||||
};
|
||||
|
||||
//Текст кнопок
|
||||
export const BUTTONS = {
|
||||
NAVIGATE_HOME: "Домой", //Переход к домашней странице
|
||||
NAVIGATE_BACK: "Назад", //Возврат назад по навигации
|
||||
NAVIGATE: "Перейти", //Переход к разделу/панели/адресу
|
||||
OK: "ОК", //Ок
|
||||
CANCEL: "Отмена", //Отмена
|
||||
CLOSE: "Закрыть", //Сокрытие
|
||||
CLEAR: "Очистить", //Очистка
|
||||
ORDER_ASC: "По возрастанию", //Сортировка по возрастанию
|
||||
ORDER_DESC: "По убыванию", //Сортировка по убыванию
|
||||
FILTER: "Фильтр", //Фильтрация
|
||||
MORE: "Ещё" //Догрузка данных
|
||||
};
|
||||
|
||||
//Текст элементов ввода
|
||||
export const INPUTS = {
|
||||
VALUE: "Значение",
|
||||
VALUE_FROM: "С",
|
||||
VALUE_TO: "По"
|
||||
};
|
||||
|
||||
//Типовые сообщения об ошибках
|
||||
export const ERROR = {
|
||||
UNDER_CONSTRUCTION: "Панель в разработке",
|
||||
P8O_API_UNAVAILABLE: '"ПАРУС 8 Онлайн" недоступен',
|
||||
DEFAULT: "Неожиданная ошибка"
|
||||
};
|
||||
|
||||
//Типовые сообщения для ошибок HTTP
|
||||
export const ERROR_HTTP = {
|
||||
404: "Адрес не найден"
|
||||
};
|
162
app/app.js
Normal file
162
app/app.js
Normal file
@ -0,0 +1,162 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Приложение
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useContext, useEffect } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { createHashRouter, RouterProvider, useRouteError } from "react-router-dom"; //Роутер
|
||||
import { ApplicationСtx } from "./context/application"; //Контекст приложения
|
||||
import { NavigationContext, NavigationCtx, getRootLocation } from "./context/navigation"; //Контекст навигации
|
||||
import { P8PAppErrorPage } from "./components/p8p_app_error_page"; //Страница с ошибкой
|
||||
import { P8PAppWorkspace } from "./components/p8p_app_workspace"; //Рабочее пространство панели
|
||||
import { P8PPanelsMenuGrid, PANEL_SHAPE } from "./components/p8p_panels_menu"; //Меню панелей
|
||||
import { TITLES, BUTTONS, ERROR, ERROR_HTTP } from "../app.text"; //Текстовые ресурсы и константы
|
||||
|
||||
//--------------------------
|
||||
//Вспомогательные компоненты
|
||||
//--------------------------
|
||||
|
||||
//Обработка ошибок роутинга
|
||||
const RouterError = ({ homePath }) => {
|
||||
//Подключение к контексту навигации
|
||||
const { navigateTo } = useContext(NavigationCtx);
|
||||
|
||||
//Извлечем ошибку роутинга
|
||||
const routeError = useRouteError();
|
||||
|
||||
//Отработка нажатия на кнопку навигации
|
||||
const handleNavigate = () => navigateTo({ path: `${homePath.startsWith("/") ? "" : "/"}${homePath}` });
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<P8PAppErrorPage
|
||||
errorMessage={ERROR_HTTP[routeError.status] ? ERROR_HTTP[routeError.status] : ERROR.DEFAULT}
|
||||
onNavigate={handleNavigate}
|
||||
navigateCaption={BUTTONS.NAVIGATE_HOME}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - обработка ошибок роутинга
|
||||
RouterError.propTypes = {
|
||||
homePath: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
//Главное меню приложения
|
||||
const MainMenu = ({ panels = [] } = {}) => {
|
||||
//Подключение к контексту навигации
|
||||
const { navigatePanel } = useContext(NavigationCtx);
|
||||
|
||||
//Отработка действия навигации элемента меню
|
||||
const handleItemNavigate = panel => navigatePanel(panel);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<P8PPanelsMenuGrid
|
||||
panels={panels}
|
||||
title={TITLES.MAIN_MENU}
|
||||
onItemNavigate={handleItemNavigate}
|
||||
navigateCaption={BUTTONS.NAVIGATE}
|
||||
defaultGroupTytle={TITLES.DEFAULT_PANELS_GROUP}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - главное меню приложения
|
||||
MainMenu.propTypes = {
|
||||
panels: PropTypes.arrayOf(PANEL_SHAPE).isRequired
|
||||
};
|
||||
|
||||
//Рабочее пространство панели
|
||||
const Workspace = ({ panels = [], selectedPanel, children } = {}) => {
|
||||
//Подключение к контексту навигации
|
||||
const { navigateRoot, navigatePanel } = useContext(NavigationCtx);
|
||||
|
||||
//Отработка действия навигации домой
|
||||
const handleHomeNavigate = () => navigateRoot();
|
||||
|
||||
//Отработка действия навигации элемента меню
|
||||
const handleItemNavigate = panel => navigatePanel(panel);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<P8PAppWorkspace
|
||||
panels={panels}
|
||||
selectedPanel={selectedPanel}
|
||||
closeCaption={BUTTONS.CLOSE}
|
||||
homeCaption={BUTTONS.NAVIGATE_HOME}
|
||||
onHomeNavigate={handleHomeNavigate}
|
||||
onItemNavigate={handleItemNavigate}
|
||||
>
|
||||
{children}
|
||||
</P8PAppWorkspace>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - главное меню приложения
|
||||
Workspace.propTypes = {
|
||||
panels: PropTypes.arrayOf(PANEL_SHAPE).isRequired,
|
||||
selectedPanel: PANEL_SHAPE,
|
||||
children: PropTypes.element
|
||||
};
|
||||
|
||||
//Обёртывание элемента в контекст навигации
|
||||
const wrapNavigationContext = children => <NavigationContext>{children}</NavigationContext>;
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Приложение
|
||||
const App = () => {
|
||||
//Собственное состояние
|
||||
const [routes, setRoutes] = useState([]);
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { appState } = useContext(ApplicationСtx);
|
||||
|
||||
//Инициализация роутера
|
||||
const content = routes.length > 0 ? <RouterProvider router={createHashRouter(routes)}></RouterProvider> : null;
|
||||
|
||||
//При изменении состояния загрузки панелей
|
||||
useEffect(() => {
|
||||
if (appState.panelsLoaded) {
|
||||
//Сборка "веток" для панелей
|
||||
let routes = [
|
||||
{
|
||||
path: getRootLocation(),
|
||||
element: wrapNavigationContext(<MainMenu panels={appState.panels} />),
|
||||
errorElement: wrapNavigationContext(<RouterError homePath={getRootLocation()} />)
|
||||
}
|
||||
];
|
||||
for (const panel of appState.panels) {
|
||||
// eslint-disable-next-line no-undef
|
||||
const p = require(`./panels/${panel.path}`);
|
||||
routes.push({
|
||||
path: `${panel.url}/*`,
|
||||
element: wrapNavigationContext(
|
||||
<Workspace panels={appState.panels} selectedPanel={panel}>
|
||||
<p.RootClass />
|
||||
</Workspace>
|
||||
),
|
||||
errorElement: wrapNavigationContext(<RouterError homePath={panel.url} />)
|
||||
});
|
||||
}
|
||||
setRoutes(routes);
|
||||
}
|
||||
}, [appState.panels, appState.panelsLoaded]);
|
||||
|
||||
//Генерация содержимого
|
||||
return content;
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { App };
|
42
app/components/p8p_app_error_page.js
Normal file
42
app/components/p8p_app_error_page.js
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Компонент: Страница ошибки
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Box } from "@mui/material"; //Контейнер
|
||||
import { P8PAppInlineError } from "./p8p_app_message"; //Сообщения
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Страница ошибки
|
||||
const P8PAppErrorPage = ({ errorMessage, onNavigate, navigateCaption }) => {
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight="100vh">
|
||||
<div>
|
||||
<P8PAppInlineError text={errorMessage} okBtn={onNavigate ? true : false} onOk={onNavigate} okBtnCaption={navigateCaption} />
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Страница ошибки
|
||||
P8PAppErrorPage.propTypes = {
|
||||
errorMessage: PropTypes.string.isRequired,
|
||||
onNavigate: PropTypes.func,
|
||||
navigateCaption: PropTypes.string
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8PAppErrorPage };
|
233
app/components/p8p_app_message.js
Normal file
233
app/components/p8p_app_message.js
Normal file
@ -0,0 +1,233 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Компонент: Сообщение
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import Dialog from "@mui/material/Dialog"; //базовый класс диалога Material UI
|
||||
import DialogTitle from "@mui/material/DialogTitle"; //Заголовок диалога
|
||||
import DialogContent from "@mui/material/DialogContent"; //Содержимое диалога
|
||||
import DialogContentText from "@mui/material/DialogContentText"; //Текст содержимого диалога
|
||||
import DialogActions from "@mui/material/DialogActions"; //Область действий диалога
|
||||
import Typography from "@mui/material/Typography"; //Текст
|
||||
import Button from "@mui/material/Button"; //Кнопки
|
||||
import Container from "@mui/material/Container"; //Контейнер
|
||||
import Box from "@mui/material/Box"; //Обёртка
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Варианты исполнения
|
||||
const P8P_APP_MESSAGE_VARIANT = {
|
||||
INFO: "information",
|
||||
WARN: "warning",
|
||||
ERR: "error"
|
||||
};
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
DEFAULT: {
|
||||
wordBreak: "break-word"
|
||||
},
|
||||
INFO: {
|
||||
titleText: {},
|
||||
bodyText: {}
|
||||
},
|
||||
WARN: {
|
||||
titleText: {
|
||||
color: "orange"
|
||||
},
|
||||
bodyText: {
|
||||
color: "orange"
|
||||
}
|
||||
},
|
||||
ERR: {
|
||||
titleText: {
|
||||
color: "red"
|
||||
},
|
||||
bodyText: {
|
||||
color: "red"
|
||||
}
|
||||
},
|
||||
INLINE_MESSAGE: {
|
||||
with: "100%",
|
||||
textAlign: "center"
|
||||
}
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Сообщение
|
||||
const P8PAppMessage = ({ variant, title, titleText, cancelBtn, onCancel, cancelBtnCaption, okBtn, onOk, okBtnCaption, open, text }) => {
|
||||
//Подбор стиля и ресурсов
|
||||
let style = STYLES.INFO;
|
||||
switch (variant) {
|
||||
case P8P_APP_MESSAGE_VARIANT.INFO: {
|
||||
style = STYLES.INFO;
|
||||
break;
|
||||
}
|
||||
case P8P_APP_MESSAGE_VARIANT.WARN: {
|
||||
style = STYLES.WARN;
|
||||
break;
|
||||
}
|
||||
case P8P_APP_MESSAGE_VARIANT.ERR: {
|
||||
style = STYLES.ERR;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//Заголовок
|
||||
let titlePart;
|
||||
if (title && titleText)
|
||||
titlePart = (
|
||||
<DialogTitle id="message-dialog-title" style={{ ...style.DEFAULT, ...style.titleText }}>
|
||||
{titleText}
|
||||
</DialogTitle>
|
||||
);
|
||||
|
||||
//Кнопка Отмена
|
||||
let cancelBtnPart;
|
||||
if (cancelBtn && cancelBtnCaption && variant === P8P_APP_MESSAGE_VARIANT.WARN)
|
||||
cancelBtnPart = <Button onClick={() => (onCancel ? onCancel() : null)}>{cancelBtnCaption}</Button>;
|
||||
|
||||
//Кнопка OK
|
||||
let okBtnPart;
|
||||
if (okBtn && okBtnCaption)
|
||||
okBtnPart = (
|
||||
<Button onClick={() => (onOk ? onOk() : null)} color="primary" autoFocus>
|
||||
{okBtnCaption}
|
||||
</Button>
|
||||
);
|
||||
|
||||
//Все действия
|
||||
let actionsPart;
|
||||
if (cancelBtnPart || okBtnPart)
|
||||
actionsPart = (
|
||||
<DialogActions>
|
||||
{okBtnPart}
|
||||
{cancelBtnPart}
|
||||
</DialogActions>
|
||||
);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Dialog
|
||||
open={open || false}
|
||||
aria-labelledby="message-dialog-title"
|
||||
aria-describedby="message-dialog-description"
|
||||
onClose={() => (onCancel ? onCancel() : null)}
|
||||
>
|
||||
{titlePart}
|
||||
<DialogContent>
|
||||
<DialogContentText id="message-dialog-description" style={style.bodyText}>
|
||||
{text}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
{actionsPart}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Сообщение
|
||||
P8PAppMessage.propTypes = {
|
||||
variant: PropTypes.string.isRequired,
|
||||
title: PropTypes.bool,
|
||||
titleText: PropTypes.string,
|
||||
cancelBtn: PropTypes.bool,
|
||||
onCancel: PropTypes.func,
|
||||
cancelBtnCaption: PropTypes.string,
|
||||
okBtn: PropTypes.bool,
|
||||
onOk: PropTypes.func,
|
||||
okBtnCaption: PropTypes.string,
|
||||
open: PropTypes.bool,
|
||||
text: PropTypes.string
|
||||
};
|
||||
|
||||
//Встроенное сообщение
|
||||
const P8PAppInlineMessage = ({ variant, text, okBtn, onOk, okBtnCaption }) => {
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Container style={STYLES.INLINE_MESSAGE}>
|
||||
<Box p={5}>
|
||||
<Typography
|
||||
color={variant === P8P_APP_MESSAGE_VARIANT.ERR ? "error" : variant === P8P_APP_MESSAGE_VARIANT.WARN ? "primary" : "textSecondary"}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
{okBtn && okBtnCaption ? (
|
||||
<Box pt={2}>
|
||||
<Button onClick={() => (onOk ? onOk() : null)} color="primary" autoFocus>
|
||||
{okBtnCaption}
|
||||
</Button>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Встроенное сообщение
|
||||
P8PAppInlineMessage.propTypes = {
|
||||
variant: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
okBtn: PropTypes.bool,
|
||||
onOk: PropTypes.func,
|
||||
okBtnCaption: PropTypes.string
|
||||
};
|
||||
|
||||
//Формирование типового сообщения
|
||||
const buildVariantMessage = (props, variant) => {
|
||||
//Извлекаем необходимые свойства
|
||||
let { open, titleText } = props;
|
||||
|
||||
//Генерация содержимого
|
||||
return <P8PAppMessage {...props} variant={variant} open={open === undefined ? true : open} title={titleText ? true : false} okBtn={true} />;
|
||||
};
|
||||
|
||||
//Формирование типового встроенного сообщения
|
||||
const buildVariantInlineMessage = (props, variant) => {
|
||||
//Генерация содержимого
|
||||
return <P8PAppInlineMessage {...props} variant={variant} />;
|
||||
};
|
||||
|
||||
//Сообщение об ошибке
|
||||
const P8PAppMessageErr = props => buildVariantMessage(props, P8P_APP_MESSAGE_VARIANT.ERR);
|
||||
|
||||
//Сообщение предупреждения
|
||||
const P8PAppMessageWarn = props => buildVariantMessage(props, P8P_APP_MESSAGE_VARIANT.WARN);
|
||||
|
||||
//Сообщение информации
|
||||
const P8PAppMessageInfo = props => buildVariantMessage(props, P8P_APP_MESSAGE_VARIANT.INFO);
|
||||
|
||||
//Встраиваемое сообщение об ошибке
|
||||
const P8PAppInlineError = props => buildVariantInlineMessage(props, P8P_APP_MESSAGE_VARIANT.ERR);
|
||||
|
||||
//Встраиваемое cообщение предупреждения
|
||||
const P8PAppInlineWarn = props => buildVariantInlineMessage(props, P8P_APP_MESSAGE_VARIANT.WARN);
|
||||
|
||||
//Встраиваемое сообщение информации
|
||||
const P8PAppInlineInfo = props => buildVariantInlineMessage(props, P8P_APP_MESSAGE_VARIANT.INFO);
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export {
|
||||
P8P_APP_MESSAGE_VARIANT,
|
||||
P8PAppMessage,
|
||||
P8PAppMessageErr,
|
||||
P8PAppMessageWarn,
|
||||
P8PAppMessageInfo,
|
||||
P8PAppInlineMessage,
|
||||
P8PAppInlineError,
|
||||
P8PAppInlineWarn,
|
||||
P8PAppInlineInfo
|
||||
};
|
52
app/components/p8p_app_progress.js
Normal file
52
app/components/p8p_app_progress.js
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Компонент: Индикатор процесса
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import Dialog from "@mui/material/Dialog"; //базовый класс диалога Material UI
|
||||
import DialogTitle from "@mui/material/DialogTitle"; //Заголовок диалога
|
||||
import DialogContent from "@mui/material/DialogContent"; //Содержимое диалога
|
||||
import DialogContentText from "@mui/material/DialogContentText"; //Текст содержимого диалога
|
||||
import LinearProgress from "@mui/material/LinearProgress"; //Индикатор
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Индикатора прогресса
|
||||
const P8PAppProgress = props => {
|
||||
//Извлекаем необходимые свойства
|
||||
let { open, title, text } = props;
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={open || false} aria-labelledby="progress-dialog-title" aria-describedby="progress-dialog-description">
|
||||
{title ? <DialogTitle id="progress-dialog-title">{title}</DialogTitle> : null}
|
||||
<DialogContent>
|
||||
<DialogContentText id="progress-dialog-description">{text}</DialogContentText>
|
||||
<LinearProgress />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Индикатора прогресса
|
||||
P8PAppProgress.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
text: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8PAppProgress };
|
128
app/components/p8p_app_workspace.js
Normal file
128
app/components/p8p_app_workspace.js
Normal file
@ -0,0 +1,128 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Компонент: Рабочее пространство
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import {
|
||||
AppBar,
|
||||
CssBaseline,
|
||||
Icon,
|
||||
Box,
|
||||
Toolbar,
|
||||
IconButton,
|
||||
Typography,
|
||||
Drawer,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from "@mui/material"; //Интерфейсные компоненты
|
||||
import { P8PPanelsMenuDrawer, PANEL_SHAPE } from "./p8p_panels_menu";
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
ROOT_BOX: { display: "flex" },
|
||||
APP_BAR: { position: "fixed" },
|
||||
APP_BAR_BUTTON: { mr: 2 },
|
||||
MAIN: { flexGrow: 1 }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Рабочее пространство
|
||||
const P8PAppWorkspace = ({ children, panels = [], selectedPanel, closeCaption, homeCaption, onHomeNavigate, onItemNavigate } = {}) => {
|
||||
//Собственное состояния
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
//Отработка открытия бокового меню
|
||||
const handleDrawerOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
//Отработка закрытия бового меню
|
||||
const handleDrawerClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
//Отработка нажатия на домашнюю страницу
|
||||
const handleHomeClick = () => (onHomeNavigate ? onHomeNavigate() : null);
|
||||
|
||||
//Отработка нажатия на элемент бокового меню
|
||||
const handleItemNavigate = panel => {
|
||||
handleDrawerClose();
|
||||
onItemNavigate ? onItemNavigate(panel) : null;
|
||||
};
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box sx={STYLES.ROOT_BOX}>
|
||||
<CssBaseline />
|
||||
<AppBar sx={STYLES.APP_BAR}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
onClick={open ? handleDrawerClose : handleDrawerOpen}
|
||||
edge="start"
|
||||
sx={STYLES.APP_BAR_BUTTON}
|
||||
>
|
||||
<Icon>{open ? "chevron_left" : "menu"}</Icon>
|
||||
</IconButton>
|
||||
<Typography variant="h6" noWrap component="div">
|
||||
{selectedPanel?.caption}
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer anchor="left" open={open} onClose={handleDrawerClose}>
|
||||
<List>
|
||||
<ListItemButton onClick={handleDrawerClose}>
|
||||
<ListItemIcon>
|
||||
<Icon>close</Icon>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={closeCaption} />
|
||||
</ListItemButton>
|
||||
<ListItemButton onClick={handleHomeClick}>
|
||||
<ListItemIcon>
|
||||
<Icon>home</Icon>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={homeCaption} />
|
||||
</ListItemButton>
|
||||
</List>
|
||||
<P8PPanelsMenuDrawer panels={panels} selectedPanel={selectedPanel} onItemNavigate={handleItemNavigate} />
|
||||
</Drawer>
|
||||
<main style={STYLES.MAIN}>
|
||||
<Toolbar />
|
||||
{children}
|
||||
</main>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Рабочее пространство
|
||||
P8PAppWorkspace.propTypes = {
|
||||
children: PropTypes.element,
|
||||
panels: PropTypes.arrayOf(PANEL_SHAPE).isRequired,
|
||||
selectedPanel: PANEL_SHAPE,
|
||||
closeCaption: PropTypes.string.isRequired,
|
||||
homeCaption: PropTypes.string.isRequired,
|
||||
onHomeNavigate: PropTypes.func,
|
||||
onItemNavigate: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8PAppWorkspace };
|
162
app/components/p8p_data_grid.js
Normal file
162
app/components/p8p_data_grid.js
Normal file
@ -0,0 +1,162 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Компонент: Таблица данных
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { deepCopyObject } from "../core/utils"; //Вспомогательные процедуры и функции
|
||||
import { P8PTable, P8P_TABLE_SIZE, P8P_TABLE_DATA_TYPE, P8P_FILTER_SHAPE } from "./p8p_table"; //Таблица
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Размеры отступов
|
||||
const P8PDATA_GRID_SIZE = P8P_TABLE_SIZE;
|
||||
|
||||
//Типы данных
|
||||
const P8PDATA_GRID_DATA_TYPE = P8P_TABLE_DATA_TYPE;
|
||||
|
||||
//Формат фильтра
|
||||
const P8PDATA_GRID_FILTER_SHAPE = P8P_FILTER_SHAPE;
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Таблица данных
|
||||
const P8PDataGrid = ({
|
||||
columnsDef,
|
||||
filtersInitial,
|
||||
rows,
|
||||
size,
|
||||
morePages,
|
||||
reloading,
|
||||
expandable,
|
||||
orderAscMenuItemCaption,
|
||||
orderDescMenuItemCaption,
|
||||
filterMenuItemCaption,
|
||||
valueFilterCaption,
|
||||
valueFromFilterCaption,
|
||||
valueToFilterCaption,
|
||||
okFilterBtnCaption,
|
||||
clearFilterBtnCaption,
|
||||
cancelFilterBtnCaption,
|
||||
morePagesBtnCaption,
|
||||
noDataFoundText,
|
||||
headCellRender,
|
||||
dataCellRender,
|
||||
rowExpandRender,
|
||||
valueFormatter,
|
||||
onOrderChanged,
|
||||
onFilterChanged,
|
||||
onPagesCountChanged
|
||||
}) => {
|
||||
//Собственное состояние - сортировки
|
||||
const [orders, setOrders] = useState([]);
|
||||
|
||||
//Собственное состояние - фильтры
|
||||
const [filters, setFilters] = useState(filtersInitial || []);
|
||||
|
||||
//При изменении состояния сортировки
|
||||
const handleOrderChanged = ({ columnName, direction }) => {
|
||||
let newOrders = deepCopyObject(orders);
|
||||
const curOrder = newOrders.find(o => o.name == columnName);
|
||||
if (direction == null && curOrder) newOrders.splice(newOrders.indexOf(curOrder), 1);
|
||||
if (direction != null && !curOrder) newOrders.push({ name: columnName, direction });
|
||||
if (direction != null && curOrder) curOrder.direction = direction;
|
||||
setOrders(newOrders);
|
||||
if (onOrderChanged) onOrderChanged({ orders: newOrders });
|
||||
};
|
||||
|
||||
//При изменении состояния фильтра
|
||||
const handleFilterChanged = ({ columnName, from, to }) => {
|
||||
let newFilters = deepCopyObject(filters);
|
||||
let curFilter = newFilters.find(f => f.name == columnName);
|
||||
if (from == null && to == null && curFilter) newFilters.splice(newFilters.indexOf(curFilter), 1);
|
||||
if ((from != null || to != null) && !curFilter) newFilters.push({ name: columnName, from, to });
|
||||
if ((from != null || to != null) && curFilter) {
|
||||
curFilter.from = from;
|
||||
curFilter.to = to;
|
||||
}
|
||||
setFilters(newFilters);
|
||||
if (onFilterChanged) onFilterChanged({ filters: newFilters });
|
||||
};
|
||||
|
||||
//При изменении количества отображаемых страниц
|
||||
const handlePagesCountChanged = () => {
|
||||
if (onPagesCountChanged) onPagesCountChanged();
|
||||
};
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<P8PTable
|
||||
columnsDef={columnsDef}
|
||||
rows={rows}
|
||||
orders={orders}
|
||||
filters={filters}
|
||||
size={size || P8PDATA_GRID_SIZE.MEDIUM}
|
||||
morePages={morePages}
|
||||
reloading={reloading}
|
||||
expandable={expandable}
|
||||
orderAscMenuItemCaption={orderAscMenuItemCaption}
|
||||
orderDescMenuItemCaption={orderDescMenuItemCaption}
|
||||
filterMenuItemCaption={filterMenuItemCaption}
|
||||
valueFilterCaption={valueFilterCaption}
|
||||
valueFromFilterCaption={valueFromFilterCaption}
|
||||
valueToFilterCaption={valueToFilterCaption}
|
||||
okFilterBtnCaption={okFilterBtnCaption}
|
||||
clearFilterBtnCaption={clearFilterBtnCaption}
|
||||
cancelFilterBtnCaption={cancelFilterBtnCaption}
|
||||
morePagesBtnCaption={morePagesBtnCaption}
|
||||
noDataFoundText={noDataFoundText}
|
||||
headCellRender={headCellRender}
|
||||
dataCellRender={dataCellRender}
|
||||
rowExpandRender={rowExpandRender}
|
||||
valueFormatter={valueFormatter}
|
||||
onOrderChanged={handleOrderChanged}
|
||||
onFilterChanged={handleFilterChanged}
|
||||
onPagesCountChanged={handlePagesCountChanged}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Таблица данных
|
||||
P8PDataGrid.propTypes = {
|
||||
columnsDef: PropTypes.array.isRequired,
|
||||
filtersInitial: PropTypes.arrayOf(P8PDATA_GRID_FILTER_SHAPE),
|
||||
rows: PropTypes.array.isRequired,
|
||||
size: PropTypes.string,
|
||||
morePages: PropTypes.bool.isRequired,
|
||||
reloading: PropTypes.bool.isRequired,
|
||||
expandable: PropTypes.bool,
|
||||
orderAscMenuItemCaption: PropTypes.string.isRequired,
|
||||
orderDescMenuItemCaption: PropTypes.string.isRequired,
|
||||
filterMenuItemCaption: PropTypes.string.isRequired,
|
||||
valueFilterCaption: PropTypes.string.isRequired,
|
||||
valueFromFilterCaption: PropTypes.string.isRequired,
|
||||
valueToFilterCaption: PropTypes.string.isRequired,
|
||||
okFilterBtnCaption: PropTypes.string.isRequired,
|
||||
clearFilterBtnCaption: PropTypes.string.isRequired,
|
||||
cancelFilterBtnCaption: PropTypes.string.isRequired,
|
||||
morePagesBtnCaption: PropTypes.string.isRequired,
|
||||
noDataFoundText: PropTypes.string,
|
||||
headCellRender: PropTypes.func,
|
||||
dataCellRender: PropTypes.func,
|
||||
rowExpandRender: PropTypes.func,
|
||||
valueFormatter: PropTypes.func,
|
||||
onOrderChanged: PropTypes.func,
|
||||
onFilterChanged: PropTypes.func,
|
||||
onPagesCountChanged: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8PDataGrid, P8PDATA_GRID_SIZE, P8PDATA_GRID_DATA_TYPE, P8PDATA_GRID_FILTER_SHAPE };
|
65
app/components/p8p_fullscreen_dialog.js
Normal file
65
app/components/p8p_fullscreen_dialog.js
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Компонент: Полноэкранный диалог
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Dialog, AppBar, Toolbar, IconButton, Typography, Icon, DialogContent, DialogTitle } from "@mui/material"; //Интерфейсные компоненты
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
DIALOG_TITLE: { padding: 0 },
|
||||
APP_BAR: { position: "relative" },
|
||||
TITLE_TYPOGRAPHY: { ml: 2, flex: 1 }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Полноэкранный диалог
|
||||
const P8PFullScreenDialog = ({ title, onClose, children }) => {
|
||||
const handleClose = () => {
|
||||
onClose ? onClose() : null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog fullScreen open onClose={handleClose} scroll="paper">
|
||||
<DialogTitle sx={STYLES.DIALOG_TITLE}>
|
||||
<AppBar sx={STYLES.APP_BAR}>
|
||||
<Toolbar>
|
||||
<IconButton edge="start" color="inherit" onClick={handleClose} aria-label="close">
|
||||
<Icon>close</Icon>
|
||||
</IconButton>
|
||||
<Typography sx={STYLES.TITLE_TYPOGRAPHY} variant="h6" component="div">
|
||||
{title}
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</DialogTitle>
|
||||
<DialogContent>{children}</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Полноэкранный диалог
|
||||
P8PFullScreenDialog.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
children: PropTypes.element
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8PFullScreenDialog };
|
231
app/components/p8p_panels_menu.js
Normal file
231
app/components/p8p_panels_menu.js
Normal file
@ -0,0 +1,231 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Компонент: Меню панелей
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Icon,
|
||||
Box,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardMedia,
|
||||
Stack,
|
||||
Grid,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from "@mui/material"; //Интерфейсные компоненты
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Типы меню
|
||||
const VARIANT = {
|
||||
DRAWER: "DRAWER",
|
||||
GRID: "GRID"
|
||||
};
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONTAINER: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "flex-start",
|
||||
minHeight: "100vh"
|
||||
},
|
||||
TITLE: {
|
||||
textTransform: "uppercase",
|
||||
textAlign: "center",
|
||||
fontWeight: "bold"
|
||||
},
|
||||
GRID: {
|
||||
maxWidth: 1200,
|
||||
direction: "row",
|
||||
justifyContent: "left",
|
||||
alignItems: "stretch"
|
||||
},
|
||||
PANEL_CARD: {
|
||||
maxWidth: 400,
|
||||
height: "100%",
|
||||
flexDirection: "column",
|
||||
display: "flex"
|
||||
},
|
||||
PANEL_CARD_MEDIA: {
|
||||
height: 140
|
||||
},
|
||||
PANEL_CARD_CONTENT_TITLE: {
|
||||
alignItems: "center"
|
||||
},
|
||||
PANEL_CARD_ACTIONS: {
|
||||
marginTop: "auto",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "flex-start"
|
||||
}
|
||||
};
|
||||
|
||||
//Структура элемента описания панели
|
||||
const PANEL_SHAPE = PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
caption: PropTypes.string.isRequired,
|
||||
desc: PropTypes.string.isRequired,
|
||||
group: PropTypes.string,
|
||||
icon: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
preview: PropTypes.string.isRequired,
|
||||
showInPanelsList: PropTypes.bool.isRequired,
|
||||
url: PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
//--------------------------------
|
||||
//Вспомогательные классы и функции
|
||||
//--------------------------------
|
||||
|
||||
//Формирование групп
|
||||
const getGroups = panels => {
|
||||
let res = [];
|
||||
let addDefaultGroup = false;
|
||||
for (const panel of panels)
|
||||
if (panel.showInPanelsList == true) {
|
||||
if (panel.group && !res.includes(panel.group)) res.push(panel.group);
|
||||
if (!panel.group) addDefaultGroup = true;
|
||||
}
|
||||
if (addDefaultGroup || res.length == 0) res.push(null);
|
||||
return res;
|
||||
};
|
||||
|
||||
//Формирование ссылок на панели
|
||||
const getPanelsLinks = ({ variant, panels, selectedPanel, defaultGroupTytle, navigateCaption, onItemNavigate }) => {
|
||||
//Получим группы
|
||||
let grps = getGroups(panels);
|
||||
|
||||
//Построим ссылки
|
||||
const panelsLinks = [];
|
||||
for (const grp of grps) {
|
||||
if (!(grps.length == 1 && grps[0] == null))
|
||||
panelsLinks.push(
|
||||
variant === VARIANT.GRID ? (
|
||||
<Grid item xs={12} sm={12} md={12} lg={12} xl={12} key={grp}>
|
||||
<Typography variant="h5" color="secondary">
|
||||
{grp ? grp : defaultGroupTytle}
|
||||
</Typography>
|
||||
</Grid>
|
||||
) : (
|
||||
<Divider key={grp} />
|
||||
)
|
||||
);
|
||||
for (const panel of panels) {
|
||||
if (panel.showInPanelsList == true && ((grp && panel.group === grp) || (!grp && !panel.group)))
|
||||
panelsLinks.push(
|
||||
variant === VARIANT.GRID ? (
|
||||
<Grid item xs={12} sm={6} md={4} lg={4} xl={4} key={panel.name}>
|
||||
<Card sx={STYLES.PANEL_CARD}>
|
||||
{panel.preview ? (
|
||||
<CardMedia component="img" alt={panel.name} image={panel.preview} sx={STYLES.PANEL_CARD_MEDIA} />
|
||||
) : null}
|
||||
<CardContent>
|
||||
<Stack gap={1} direction="row" sx={STYLES.PANEL_CARD_CONTENT_TITLE}>
|
||||
{panel.icon ? <Icon>{panel.icon}</Icon> : null}
|
||||
<Typography variant="h5">{panel.caption}</Typography>
|
||||
</Stack>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{panel.desc}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions sx={STYLES.PANEL_CARD_ACTIONS}>
|
||||
<Button size="large" onClick={() => (onItemNavigate ? onItemNavigate(panel) : null)}>
|
||||
{navigateCaption}
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
) : (
|
||||
<ListItem key={panel.name} disablePadding>
|
||||
<ListItemButton
|
||||
selected={selectedPanel?.name === panel.name}
|
||||
onClick={() => (onItemNavigate ? onItemNavigate(panel) : null)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Icon>{panel.icon}</Icon>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={panel.caption} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//Вернём ссылки
|
||||
return panelsLinks;
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Меню панелей - сдвигающееся боковое меню
|
||||
const P8PPanelsMenuDrawer = ({ onItemNavigate, panels = [], selectedPanel } = {}) => {
|
||||
//Формируем ссылки на панели
|
||||
const panelsLinks = getPanelsLinks({ variant: VARIANT.DRAWER, panels, selectedPanel, onItemNavigate });
|
||||
|
||||
//Генерация содержимого
|
||||
return <List sx={{ paddingTop: 0 }}>{panelsLinks}</List>;
|
||||
};
|
||||
|
||||
//Контроль свойств - Меню панелей - сдвигающееся боковое меню
|
||||
P8PPanelsMenuDrawer.propTypes = {
|
||||
onItemNavigate: PropTypes.func,
|
||||
panels: PropTypes.arrayOf(PANEL_SHAPE).isRequired,
|
||||
selectedPanel: PANEL_SHAPE
|
||||
};
|
||||
|
||||
//Меню панелей - грид
|
||||
const P8PPanelsMenuGrid = ({ title, onItemNavigate, navigateCaption, panels = [], defaultGroupTytle } = {}) => {
|
||||
//Формируем ссылки на панели
|
||||
const panelsLinks = getPanelsLinks({ variant: VARIANT.GRID, panels, defaultGroupTytle, navigateCaption, onItemNavigate });
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box sx={STYLES.CONTAINER}>
|
||||
<Grid container spacing={2} p={2} sx={STYLES.GRID}>
|
||||
{title ? (
|
||||
<Grid item xs={12} sm={12} md={12} lg={12} xl={12}>
|
||||
<Typography variant="h5" color="primary" sx={STYLES.TITLE}>
|
||||
{title}
|
||||
</Typography>
|
||||
</Grid>
|
||||
) : null}
|
||||
{panelsLinks}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Меню панелей - грид
|
||||
P8PPanelsMenuGrid.propTypes = {
|
||||
title: PropTypes.string,
|
||||
onItemNavigate: PropTypes.func,
|
||||
navigateCaption: PropTypes.string.isRequired,
|
||||
panels: PropTypes.arrayOf(PANEL_SHAPE).isRequired,
|
||||
defaultGroupTytle: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { PANEL_SHAPE, P8PPanelsMenuDrawer, P8PPanelsMenuGrid };
|
726
app/components/p8p_table.js
Normal file
726
app/components/p8p_table.js
Normal file
@ -0,0 +1,726 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Компонент: Таблица
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useEffect, useState, useMemo } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
IconButton,
|
||||
Icon,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Divider,
|
||||
Stack,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
Chip,
|
||||
Container
|
||||
} from "@mui/material"; //Интерфейсные компоненты
|
||||
import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Размеры отступов
|
||||
const P8P_TABLE_SIZE = {
|
||||
SMALL: "small",
|
||||
MEDIUM: "medium"
|
||||
};
|
||||
|
||||
//Типы данных
|
||||
const P8P_TABLE_DATA_TYPE = {
|
||||
STR: "STR",
|
||||
NUMB: "NUMB",
|
||||
DATE: "DATE"
|
||||
};
|
||||
|
||||
//Направления сортировки
|
||||
const P8P_TABLE_COLUMN_ORDER_DIRECTIONS = {
|
||||
ASC: "ASC",
|
||||
DESC: "DESC"
|
||||
};
|
||||
|
||||
//Действия панели инструментов столбца
|
||||
const P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS = {
|
||||
ORDER_TOGGLE: "ORDER_TOGGLE",
|
||||
FILTER_TOGGLE: "FILTER_TOGGLE"
|
||||
};
|
||||
|
||||
//Действия меню столбца
|
||||
const P8P_TABLE_COLUMN_MENU_ACTIONS = {
|
||||
ORDER_ASC: "ORDER_ASC",
|
||||
ORDER_DESC: "ORDER_DESC",
|
||||
FILTER: "FILTER"
|
||||
};
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
TABLE: {
|
||||
with: "100%"
|
||||
},
|
||||
TABLE_ROW: {
|
||||
"&:last-child td, &:last-child th": { border: 0 }
|
||||
},
|
||||
TABLE_CELL_EXPAND_CONTAINER: {
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0
|
||||
},
|
||||
TABLE_COLUMN_STACK: {
|
||||
alignItems: "center"
|
||||
},
|
||||
TABLE_COLUMN_MENU_ITEM_ICON: {
|
||||
paddingRight: "10px"
|
||||
},
|
||||
FILTER_CHIP: {
|
||||
alignItems: "center"
|
||||
},
|
||||
MORE_BUTTON_CONTAINER: {
|
||||
with: "100%",
|
||||
textAlign: "center",
|
||||
padding: "5px"
|
||||
}
|
||||
};
|
||||
|
||||
//Структура элемента описания фильтра
|
||||
const P8P_FILTER_SHAPE = PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
from: PropTypes.any,
|
||||
to: PropTypes.any
|
||||
});
|
||||
|
||||
//--------------------------------
|
||||
//Вспомогательные классы и функции
|
||||
//--------------------------------
|
||||
|
||||
//Проверка существования значения
|
||||
const hasValue = value => typeof value !== "undefined" && value !== null && value !== "";
|
||||
|
||||
//Панель инструментов столбца
|
||||
const P8PTableColumnToolBar = ({ columnDef, orders, filters, onItemClick }) => {
|
||||
//Кнопка сортировки
|
||||
const order = orders.find(o => o.name == columnDef.name);
|
||||
let orderButton = null;
|
||||
if (order)
|
||||
orderButton = (
|
||||
<IconButton onClick={() => (onItemClick ? onItemClick(P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.ORDER_TOGGLE, columnDef.name) : null)}>
|
||||
<Icon>{order.direction === P8P_TABLE_COLUMN_ORDER_DIRECTIONS.ASC ? "arrow_upward" : "arrow_downward"}</Icon>
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
//Кнопка фильтрации
|
||||
const filter = filters.find(f => f.name == columnDef.name);
|
||||
let filterButton = null;
|
||||
if (hasValue(filter?.from) || hasValue(filter?.to))
|
||||
filterButton = (
|
||||
<IconButton onClick={() => (onItemClick ? onItemClick(P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.FILTER_TOGGLE, columnDef.name) : null)}>
|
||||
<Icon>filter_alt</Icon>
|
||||
</IconButton>
|
||||
);
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<>
|
||||
{orderButton}
|
||||
{filterButton}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Панель инструментов столбца
|
||||
P8PTableColumnToolBar.propTypes = {
|
||||
columnDef: PropTypes.object.isRequired,
|
||||
orders: PropTypes.array.isRequired,
|
||||
filters: PropTypes.array.isRequired,
|
||||
onItemClick: PropTypes.func
|
||||
};
|
||||
|
||||
//Меню столбца
|
||||
const P8PTableColumnMenu = ({ columnDef, orderAscItemCaption, orderDescItemCaption, filterItemCaption, onItemClick }) => {
|
||||
//Собственное состояние
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
|
||||
//Флаг отображения
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
//По нажатию на открытие меню
|
||||
const handleMenuButtonClick = event => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
//По нажатию на пункт меню
|
||||
const handleMenuItemClick = (event, index, action, columnName) => {
|
||||
if (onItemClick) onItemClick(action, columnName);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
//При закрытии меню
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
//Формирование списка элементов меню в зависимости от описания колонки таблицы
|
||||
const menuItems = [];
|
||||
if (columnDef.order === true) {
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
key={"orderAsc"}
|
||||
onClick={(event, index) => handleMenuItemClick(event, index, P8P_TABLE_COLUMN_MENU_ACTIONS.ORDER_ASC, columnDef.name)}
|
||||
>
|
||||
<Icon sx={STYLES.TABLE_COLUMN_MENU_ITEM_ICON}>arrow_upward</Icon>
|
||||
{orderAscItemCaption}
|
||||
</MenuItem>
|
||||
);
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
key={"orderDesc"}
|
||||
onClick={(event, index) => handleMenuItemClick(event, index, P8P_TABLE_COLUMN_MENU_ACTIONS.ORDER_DESC, columnDef.name)}
|
||||
>
|
||||
<Icon sx={STYLES.TABLE_COLUMN_MENU_ITEM_ICON}>arrow_downward</Icon>
|
||||
{orderDescItemCaption}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
if (columnDef.filter === true) {
|
||||
if (menuItems.length > 0) menuItems.push(<Divider key={"divider"} sx={{ my: 0.5 }} />);
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
key={"filter"}
|
||||
onClick={(event, index) => handleMenuItemClick(event, index, P8P_TABLE_COLUMN_MENU_ACTIONS.FILTER, columnDef.name)}
|
||||
>
|
||||
<Icon sx={STYLES.TABLE_COLUMN_MENU_ITEM_ICON}>filter_alt</Icon>
|
||||
{filterItemCaption}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
//Генерация содержимого
|
||||
return menuItems.length > 0 ? (
|
||||
<>
|
||||
<IconButton id={`${columnDef.name}_menu_button`} aria-haspopup="true" onClick={handleMenuButtonClick}>
|
||||
<Icon>more_vert</Icon>
|
||||
</IconButton>
|
||||
<Menu id={`${columnDef.name}_menu`} anchorEl={anchorEl} open={open} onClose={handleMenuClose}>
|
||||
{menuItems}
|
||||
</Menu>
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
//Контроль свойств - Меню столбца
|
||||
P8PTableColumnMenu.propTypes = {
|
||||
columnDef: PropTypes.object.isRequired,
|
||||
orderAscItemCaption: PropTypes.string.isRequired,
|
||||
orderDescItemCaption: PropTypes.string.isRequired,
|
||||
filterItemCaption: PropTypes.string.isRequired,
|
||||
onItemClick: PropTypes.func
|
||||
};
|
||||
|
||||
//Диалог фильтра
|
||||
const P8PTableColumnFilterDialog = ({
|
||||
columnDef,
|
||||
from,
|
||||
to,
|
||||
valueCaption,
|
||||
valueFromCaption,
|
||||
valueToCaption,
|
||||
okBtnCaption,
|
||||
clearBtnCaption,
|
||||
cancelBtnCaption,
|
||||
valueFormatter,
|
||||
onOk,
|
||||
onClear,
|
||||
onCancel
|
||||
}) => {
|
||||
//Собственное состояние - значения с-по
|
||||
const [filterValues, setFilterValues] = useState({ from, to });
|
||||
|
||||
//Отработка воода значения в фильтр
|
||||
const handleFilterTextFieldChanged = e => {
|
||||
setFilterValues(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
||||
};
|
||||
|
||||
//Элементы ввода значений фильтра
|
||||
let inputs = null;
|
||||
if (Array.isArray(columnDef.values) && columnDef.values.length > 0) {
|
||||
inputs = (
|
||||
<TextField
|
||||
name="from"
|
||||
fullWidth
|
||||
select
|
||||
label={valueCaption}
|
||||
variant="standard"
|
||||
value={filterValues.from}
|
||||
onChange={handleFilterTextFieldChanged}
|
||||
>
|
||||
{columnDef.values.map((v, i) => (
|
||||
<MenuItem key={i} value={v}>
|
||||
{valueFormatter ? valueFormatter({ value: v, columnDef }) : v}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
);
|
||||
} else {
|
||||
switch (columnDef.dataType) {
|
||||
case P8P_TABLE_DATA_TYPE.STR: {
|
||||
inputs = (
|
||||
<TextField
|
||||
name="from"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}
|
||||
value={filterValues.from}
|
||||
onChange={handleFilterTextFieldChanged}
|
||||
label={valueCaption}
|
||||
variant="standard"
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case P8P_TABLE_DATA_TYPE.NUMB:
|
||||
case P8P_TABLE_DATA_TYPE.DATE: {
|
||||
inputs = (
|
||||
<>
|
||||
<TextField
|
||||
name="from"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
type={columnDef.dataType == P8P_TABLE_DATA_TYPE.NUMB ? "number" : "date"}
|
||||
value={filterValues.from}
|
||||
onChange={handleFilterTextFieldChanged}
|
||||
label={valueFromCaption}
|
||||
variant="standard"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
name="to"
|
||||
InputLabelProps={{ shrink: true }}
|
||||
type={columnDef.dataType == P8P_TABLE_DATA_TYPE.NUMB ? "number" : "date"}
|
||||
value={filterValues.to}
|
||||
onChange={handleFilterTextFieldChanged}
|
||||
label={valueToCaption}
|
||||
variant="standard"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={true}
|
||||
aria-labelledby="filter-dialog-title"
|
||||
aria-describedby="filter-dialog-description"
|
||||
onClose={() => (onCancel ? onCancel(columnDef.name) : null)}
|
||||
>
|
||||
<DialogTitle id="filter-dialog-title">{columnDef.caption}</DialogTitle>
|
||||
<DialogContent>{inputs}</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => (onOk ? onOk(columnDef.name, filterValues.from, filterValues.to) : null)}>{okBtnCaption}</Button>
|
||||
<Button onClick={() => (onClear ? onClear(columnDef.name) : null)} variant="secondary">
|
||||
{clearBtnCaption}
|
||||
</Button>
|
||||
<Button onClick={() => (onCancel ? onCancel(columnDef.name) : null)}>{cancelBtnCaption}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог фильтра
|
||||
P8PTableColumnFilterDialog.propTypes = {
|
||||
columnDef: PropTypes.object.isRequired,
|
||||
from: PropTypes.any,
|
||||
to: PropTypes.any,
|
||||
valueCaption: PropTypes.string.isRequired,
|
||||
valueFromCaption: PropTypes.string.isRequired,
|
||||
valueToCaption: PropTypes.string.isRequired,
|
||||
okBtnCaption: PropTypes.string.isRequired,
|
||||
clearBtnCaption: PropTypes.string.isRequired,
|
||||
cancelBtnCaption: PropTypes.string.isRequired,
|
||||
valueFormatter: PropTypes.func,
|
||||
onOk: PropTypes.func,
|
||||
onClear: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//Сводный фильтр
|
||||
const P8PTableFiltersChips = ({ filters, columnsDef, valueFromCaption, valueToCaption, onFilterChipClick, onFilterChipDelete, valueFormatter }) => {
|
||||
return (
|
||||
<Stack direction="row" spacing={1} pb={2}>
|
||||
{filters.map((filter, i) => {
|
||||
const columnDef = columnsDef.find(columnDef => columnDef.name == filter.name);
|
||||
return (
|
||||
<Chip
|
||||
key={i}
|
||||
label={
|
||||
<Stack direction="row" sx={STYLES.FILTER_CHIP}>
|
||||
<strong>{columnDef.caption}</strong>:
|
||||
{hasValue(filter.from) && !columnDef.values && columnDef.dataType != P8P_TABLE_DATA_TYPE.STR
|
||||
? `${valueFromCaption.toLowerCase()} `
|
||||
: null}
|
||||
{hasValue(filter.from) ? (valueFormatter ? valueFormatter({ value: filter.from, columnDef }) : filter.from) : null}
|
||||
{hasValue(filter.to) && !columnDef.values && columnDef.dataType != P8P_TABLE_DATA_TYPE.STR
|
||||
? ` ${valueToCaption.toLowerCase()} `
|
||||
: null}
|
||||
{hasValue(filter.to) ? (valueFormatter ? valueFormatter({ value: filter.to, columnDef }) : filter.to) : null}
|
||||
</Stack>
|
||||
}
|
||||
variant="outlined"
|
||||
onClick={() => (onFilterChipClick ? onFilterChipClick(columnDef.name) : null)}
|
||||
onDelete={() => (onFilterChipDelete ? onFilterChipDelete(columnDef.name) : null)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Сводный фильтр
|
||||
P8PTableFiltersChips.propTypes = {
|
||||
filters: PropTypes.array.isRequired,
|
||||
columnsDef: PropTypes.array.isRequired,
|
||||
valueFromCaption: PropTypes.string.isRequired,
|
||||
valueToCaption: PropTypes.string.isRequired,
|
||||
onFilterChipClick: PropTypes.func,
|
||||
onFilterChipDelete: PropTypes.func,
|
||||
valueFormatter: PropTypes.func
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Таблица
|
||||
const P8PTable = ({
|
||||
columnsDef,
|
||||
rows,
|
||||
orders,
|
||||
filters,
|
||||
size,
|
||||
morePages,
|
||||
reloading,
|
||||
expandable,
|
||||
orderAscMenuItemCaption,
|
||||
orderDescMenuItemCaption,
|
||||
filterMenuItemCaption,
|
||||
valueFilterCaption,
|
||||
valueFromFilterCaption,
|
||||
valueToFilterCaption,
|
||||
okFilterBtnCaption,
|
||||
clearFilterBtnCaption,
|
||||
cancelFilterBtnCaption,
|
||||
morePagesBtnCaption,
|
||||
noDataFoundText,
|
||||
headCellRender,
|
||||
dataCellRender,
|
||||
rowExpandRender,
|
||||
valueFormatter,
|
||||
onOrderChanged,
|
||||
onFilterChanged,
|
||||
onPagesCountChanged
|
||||
}) => {
|
||||
//Собственное состояние - фильтруемая колонка
|
||||
const [filterColumn, setFilterColumn] = useState(null);
|
||||
|
||||
//Собственное состояние - развёрнутые строки
|
||||
const [expanded, setExpanded] = useState({});
|
||||
|
||||
//Описание фильтруемой колонки
|
||||
const filterColumnDef = filterColumn ? columnsDef.find(columnDef => columnDef.name == filterColumn) || null : null;
|
||||
|
||||
//Значения фильтра фильтруемой колонки
|
||||
const [filterColumnFrom, filterColumnTo] = filterColumn
|
||||
? (() => {
|
||||
const filter = filters.find(filter => filter.name == filterColumn);
|
||||
return filter ? [filter.from == null ? "" : filter.from, filter.to == null ? "" : filter.to] : ["", ""];
|
||||
})()
|
||||
: ["", ""];
|
||||
|
||||
//Определение списка видимых колонок
|
||||
const visibleColumns = useMemo(() => columnsDef.filter(columnDef => columnDef.visible === true), [columnsDef]);
|
||||
|
||||
//Определение количества видимых колонок
|
||||
const visibleColumnsCount = useMemo(() => visibleColumns.length + (expandable === true ? 1 : 0), [visibleColumns, expandable]);
|
||||
|
||||
//Выравнивание в зависимости от типа данных
|
||||
const getAlignByDataType = dataType =>
|
||||
dataType === P8P_TABLE_DATA_TYPE.DATE ? "center" : dataType === P8P_TABLE_DATA_TYPE.NUMB ? "right" : "left";
|
||||
|
||||
//Упорядочение содержимого в зависимости от типа данных
|
||||
const getJustifyContentByDataType = dataType =>
|
||||
dataType === P8P_TABLE_DATA_TYPE.DATE ? "center" : dataType === P8P_TABLE_DATA_TYPE.NUMB ? "flex-end" : "flex-start";
|
||||
|
||||
//Отработка нажатия на элемент пункта меню
|
||||
const handleToolBarItemClick = (action, columnName) => {
|
||||
switch (action) {
|
||||
case P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.ORDER_TOGGLE: {
|
||||
const colOrder = orders.find(o => o.name == columnName);
|
||||
const newDirection =
|
||||
colOrder?.direction == P8P_TABLE_COLUMN_ORDER_DIRECTIONS.ASC
|
||||
? P8P_TABLE_COLUMN_ORDER_DIRECTIONS.DESC
|
||||
: colOrder?.direction == P8P_TABLE_COLUMN_ORDER_DIRECTIONS.DESC
|
||||
? null
|
||||
: P8P_TABLE_COLUMN_ORDER_DIRECTIONS.ASC;
|
||||
if (onOrderChanged) onOrderChanged({ columnName, direction: newDirection });
|
||||
break;
|
||||
}
|
||||
case P8P_TABLE_COLUMN_TOOL_BAR_ACTIONS.FILTER_TOGGLE:
|
||||
setFilterColumn(columnName);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
//Отработка нажатия на пункты меню
|
||||
const handleMenuItemClick = (action, columnName) => {
|
||||
switch (action) {
|
||||
case P8P_TABLE_COLUMN_MENU_ACTIONS.ORDER_ASC:
|
||||
onOrderChanged({ columnName, direction: P8P_TABLE_COLUMN_ORDER_DIRECTIONS.ASC });
|
||||
break;
|
||||
case P8P_TABLE_COLUMN_MENU_ACTIONS.ORDER_DESC:
|
||||
onOrderChanged({ columnName, direction: P8P_TABLE_COLUMN_ORDER_DIRECTIONS.DESC });
|
||||
break;
|
||||
case P8P_TABLE_COLUMN_MENU_ACTIONS.FILTER:
|
||||
setFilterColumn(columnName);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
//Отработка ввода значения фильтра колонки
|
||||
const handleFilterOk = (columnName, from, to) => {
|
||||
if (onFilterChanged) onFilterChanged({ columnName, from: from === "" ? null : from, to: to === "" ? null : to });
|
||||
setFilterColumn(null);
|
||||
};
|
||||
|
||||
//Отработка очистки значения фильтра колонки
|
||||
const handleFilterClear = columnName => {
|
||||
if (onFilterChanged) onFilterChanged({ columnName, from: null, to: null });
|
||||
setFilterColumn(null);
|
||||
};
|
||||
|
||||
//Отработка отмены ввода значения фильтра колонки
|
||||
const handleFilterCancel = () => {
|
||||
setFilterColumn(null);
|
||||
};
|
||||
|
||||
//Отработка нажатия на элемент сводного фильтра
|
||||
const handleFilterChipClick = columnName => setFilterColumn(columnName);
|
||||
|
||||
//Отработка удаления элемента сводного фильтра
|
||||
const handleFilterChipDelete = columnName => (onFilterChanged ? onFilterChanged({ columnName, from: null, to: null }) : null);
|
||||
|
||||
//Отработка нажатия на кнопку догрузки страницы
|
||||
const handleMorePagesBtnClick = () => {
|
||||
if (onPagesCountChanged) onPagesCountChanged();
|
||||
};
|
||||
|
||||
//Отработка нажатия на кнопку раскрытия элемента
|
||||
const handleExpandClick = rowIndex => {
|
||||
if (expanded[rowIndex] === true)
|
||||
setExpanded(pv => {
|
||||
let res = { ...pv };
|
||||
delete res[rowIndex];
|
||||
return res;
|
||||
});
|
||||
else setExpanded(pv => ({ ...pv, [rowIndex]: true }));
|
||||
};
|
||||
|
||||
//При перезагрузке данных
|
||||
useEffect(() => {
|
||||
if (reloading) setExpanded({});
|
||||
}, [reloading]);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<>
|
||||
{filterColumn ? (
|
||||
<P8PTableColumnFilterDialog
|
||||
columnDef={filterColumnDef}
|
||||
from={filterColumnFrom}
|
||||
to={filterColumnTo}
|
||||
valueCaption={valueFilterCaption}
|
||||
valueFromCaption={valueFromFilterCaption}
|
||||
valueToCaption={valueToFilterCaption}
|
||||
okBtnCaption={okFilterBtnCaption}
|
||||
clearBtnCaption={clearFilterBtnCaption}
|
||||
cancelBtnCaption={cancelFilterBtnCaption}
|
||||
valueFormatter={valueFormatter}
|
||||
onOk={handleFilterOk}
|
||||
onClear={handleFilterClear}
|
||||
onCancel={handleFilterCancel}
|
||||
/>
|
||||
) : null}
|
||||
{Array.isArray(filters) && filters.length > 0 ? (
|
||||
<P8PTableFiltersChips
|
||||
filters={filters}
|
||||
columnsDef={columnsDef}
|
||||
valueFromCaption={valueFromFilterCaption}
|
||||
valueToCaption={valueToFilterCaption}
|
||||
onFilterChipClick={handleFilterChipClick}
|
||||
onFilterChipDelete={handleFilterChipDelete}
|
||||
valueFormatter={valueFormatter}
|
||||
/>
|
||||
) : null}
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={STYLES.TABLE} size={size || P8P_TABLE_SIZE.MEDIUM}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{expandable && rowExpandRender ? <TableCell key="head-cell-expand-control" align="center"></TableCell> : null}
|
||||
{visibleColumns.map((columnDef, j) => {
|
||||
let customRender = {};
|
||||
if (headCellRender) customRender = headCellRender({ columnDef }) || {};
|
||||
return (
|
||||
<TableCell
|
||||
key={`head-cell-${j}`}
|
||||
align={getAlignByDataType(columnDef.dataType)}
|
||||
sx={{ ...customRender.cellStyle }}
|
||||
{...customRender.cellProps}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent={getJustifyContentByDataType(columnDef.dataType)}
|
||||
sx={{ ...STYLES.TABLE_COLUMN_STACK, ...customRender.stackStyle }}
|
||||
{...customRender.stackProps}
|
||||
>
|
||||
{customRender.data ? customRender.data : columnDef.caption}
|
||||
<P8PTableColumnToolBar
|
||||
columnDef={columnDef}
|
||||
orders={orders}
|
||||
filters={filters}
|
||||
onItemClick={handleToolBarItemClick}
|
||||
/>
|
||||
<P8PTableColumnMenu
|
||||
columnDef={columnDef}
|
||||
orderAscItemCaption={orderAscMenuItemCaption}
|
||||
orderDescItemCaption={orderDescMenuItemCaption}
|
||||
filterItemCaption={filterMenuItemCaption}
|
||||
onItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.length > 0
|
||||
? rows.map((row, i) => (
|
||||
<React.Fragment key={`data-${i}`}>
|
||||
<TableRow key={`data-row-${i}`} sx={STYLES.TABLE_ROW}>
|
||||
{expandable && rowExpandRender ? (
|
||||
<TableCell key={`data-cell-expand-control-${i}`} align="center">
|
||||
<IconButton onClick={() => handleExpandClick(i)}>
|
||||
<Icon>{expanded[i] === true ? "keyboard_arrow_down" : "keyboard_arrow_right"}</Icon>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
) : null}
|
||||
{visibleColumns.map((columnDef, j) => {
|
||||
let customRender = {};
|
||||
if (dataCellRender) customRender = dataCellRender({ row, columnDef }) || {};
|
||||
return (
|
||||
<TableCell
|
||||
key={`data-cell-${j}`}
|
||||
align={getAlignByDataType(columnDef.dataType)}
|
||||
sx={{ ...customRender.cellStyle }}
|
||||
{...customRender.cellProps}
|
||||
>
|
||||
{customRender.data
|
||||
? customRender.data
|
||||
: valueFormatter
|
||||
? valueFormatter({ value: row[columnDef.name], columnDef })
|
||||
: row[columnDef.name]}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
{expandable && rowExpandRender && expanded[i] === true ? (
|
||||
<TableRow key={`data-row-expand-${i}`}>
|
||||
<TableCell sx={STYLES.TABLE_CELL_EXPAND_CONTAINER} colSpan={visibleColumnsCount}>
|
||||
{rowExpandRender({ columnsDef, row })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
))
|
||||
: null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{rows.length == 0 ? (
|
||||
noDataFoundText && !reloading ? (
|
||||
<P8PAppInlineError text={noDataFoundText} />
|
||||
) : null
|
||||
) : morePages ? (
|
||||
<Container style={STYLES.MORE_BUTTON_CONTAINER}>
|
||||
<Button fullWidth onClick={handleMorePagesBtnClick}>
|
||||
{morePagesBtnCaption}
|
||||
</Button>
|
||||
</Container>
|
||||
) : null}
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Таблица
|
||||
P8PTable.propTypes = {
|
||||
columnsDef: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
caption: PropTypes.string.isRequired,
|
||||
order: PropTypes.bool.isRequired,
|
||||
filter: PropTypes.bool.isRequired,
|
||||
dataType: PropTypes.string.isRequired,
|
||||
values: PropTypes.array
|
||||
})
|
||||
).isRequired,
|
||||
rows: PropTypes.array.isRequired,
|
||||
orders: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
direction: PropTypes.string.isRequired
|
||||
})
|
||||
).isRequired,
|
||||
filters: PropTypes.arrayOf(P8P_FILTER_SHAPE).isRequired,
|
||||
size: PropTypes.string,
|
||||
morePages: PropTypes.bool.isRequired,
|
||||
reloading: PropTypes.bool.isRequired,
|
||||
expandable: PropTypes.bool,
|
||||
orderAscMenuItemCaption: PropTypes.string.isRequired,
|
||||
orderDescMenuItemCaption: PropTypes.string.isRequired,
|
||||
filterMenuItemCaption: PropTypes.string.isRequired,
|
||||
valueFilterCaption: PropTypes.string.isRequired,
|
||||
valueFromFilterCaption: PropTypes.string.isRequired,
|
||||
valueToFilterCaption: PropTypes.string.isRequired,
|
||||
okFilterBtnCaption: PropTypes.string.isRequired,
|
||||
clearFilterBtnCaption: PropTypes.string.isRequired,
|
||||
cancelFilterBtnCaption: PropTypes.string.isRequired,
|
||||
morePagesBtnCaption: PropTypes.string.isRequired,
|
||||
noDataFoundText: PropTypes.string,
|
||||
headCellRender: PropTypes.func,
|
||||
dataCellRender: PropTypes.func,
|
||||
rowExpandRender: PropTypes.func,
|
||||
valueFormatter: PropTypes.func,
|
||||
onOrderChanged: PropTypes.func,
|
||||
onFilterChanged: PropTypes.func,
|
||||
onPagesCountChanged: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8PTable, P8P_TABLE_DATA_TYPE, P8P_TABLE_SIZE, P8P_FILTER_SHAPE };
|
149
app/context/application.js
Normal file
149
app/context/application.js
Normal file
@ -0,0 +1,149 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Контекст: Приложение
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useReducer, createContext, useEffect, useContext, useCallback } from "react"; //ReactJS
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { getDisplaySize } from "../core/utils"; //Вспомогательные функции
|
||||
import { APP_AT, INITIAL_STATE, applicationReducer } from "./application_reducer"; //Редьюсер состояния
|
||||
import { MessagingСtx } from "./messaging"; //Контекст отображения сообщений
|
||||
import { BackEndСtx } from "./backend"; //Контекст взаимодействия с сервером
|
||||
import { ERROR } from "../../app.text"; //Текстовые ресурсы и константы
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Клиентский API "ПАРУС 8 Онлайн"
|
||||
const P8O_API = window.parent?.parus?.clientApi;
|
||||
|
||||
//--------------------------------
|
||||
//Вспомогательные классы и функции
|
||||
//--------------------------------
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
//Контекст приложения
|
||||
export const ApplicationСtx = createContext();
|
||||
|
||||
//Провайдер контекста приложения
|
||||
export const ApplicationContext = ({ children }) => {
|
||||
//Подключим редьюсер состояния
|
||||
const [state, dispatch] = useReducer(applicationReducer, INITIAL_STATE);
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { getConfig, getRespPayload } = useContext(BackEndСtx);
|
||||
|
||||
//Подключение к контексту отображения сообщений
|
||||
const { showMsgErr } = useContext(MessagingСtx);
|
||||
|
||||
//Установка флага инициализированности приложения
|
||||
const setInitialized = () => dispatch({ type: APP_AT.SET_INITIALIZED });
|
||||
|
||||
//Установка текущего размера экрана
|
||||
const setDisplaySize = displaySize => dispatch({ type: APP_AT.SET_DISPLAY_SIZE, payload: displaySize });
|
||||
|
||||
//Установка списка панелей
|
||||
const setPanels = panels => dispatch({ type: APP_AT.LOAD_PANELS, payload: panels });
|
||||
|
||||
//Поиск раздела по имени
|
||||
const findPanelByName = name => state.panels.find(panel => panel.name == name);
|
||||
|
||||
//Отображение раздела "ПАРУС 8 Онлайн"
|
||||
const pOnlineShowUnit = useCallback(
|
||||
({ unitCode, showMethod = "main", inputParameters }) => {
|
||||
if (P8O_API) P8O_API.fn.openDocumentModal({ unitcode: unitCode, method: showMethod, inputParameters });
|
||||
else showMsgErr(ERROR.P8O_API_UNAVAILABLE);
|
||||
},
|
||||
[showMsgErr]
|
||||
);
|
||||
|
||||
//Отображение документа "ПАРУС 8 Онлайн"
|
||||
const pOnlineShowDocument = useCallback(
|
||||
({ unitCode, document, showMethod = "main", inRnParameter = "in_RN" }) => {
|
||||
if (P8O_API)
|
||||
P8O_API.fn.openDocumentModal({ unitcode: unitCode, method: showMethod, inputParameters: [{ name: inRnParameter, value: document }] });
|
||||
else showMsgErr(ERROR.P8O_API_UNAVAILABLE);
|
||||
},
|
||||
[showMsgErr]
|
||||
);
|
||||
|
||||
//Отображение словаря "ПАРУС 8 Онлайн"
|
||||
const pOnlineShowDictionary = useCallback(
|
||||
({ unitCode, showMethod = "main", inputParameters, callBack }) => {
|
||||
if (P8O_API)
|
||||
P8O_API.fn.openDictionary({ unitcode: unitCode, method: showMethod, inputParameters }, res => (callBack ? callBack(res) : null));
|
||||
else showMsgErr(ERROR.P8O_API_UNAVAILABLE);
|
||||
},
|
||||
[showMsgErr]
|
||||
);
|
||||
|
||||
//Исполнение пользовательской процедуры "ПАРУС 8 Онлайн"
|
||||
const pOnlineUserProcedure = useCallback(
|
||||
({ code, inputParameters, callBack }) => {
|
||||
if (P8O_API) P8O_API.fn.performUserProcedureSync({ code, inputParameters }, res => (callBack ? callBack(res) : null));
|
||||
else showMsgErr(ERROR.P8O_API_UNAVAILABLE);
|
||||
},
|
||||
[showMsgErr]
|
||||
);
|
||||
|
||||
//Исполнение пользовательского отчёта "ПАРУС 8 Онлайн"
|
||||
const pOnlineUserReport = useCallback(
|
||||
({ code, inputParameters }) => {
|
||||
if (P8O_API) P8O_API.fn.performUserReport({ code, inputParameters });
|
||||
else showMsgErr(ERROR.P8O_API_UNAVAILABLE);
|
||||
},
|
||||
[showMsgErr]
|
||||
);
|
||||
|
||||
//Инициализация приложения
|
||||
const initApp = useCallback(async () => {
|
||||
//Читаем конфигурацию с сервера
|
||||
let res = await getConfig();
|
||||
//Сохраняем список панелей
|
||||
setPanels(getRespPayload(res)?.Panels?.Panel);
|
||||
//Установим флаг завершения инициализации
|
||||
setInitialized();
|
||||
}, [getConfig, getRespPayload]);
|
||||
|
||||
//Обработка подключения контекста к странице
|
||||
useEffect(() => {
|
||||
if (!state.initialized) {
|
||||
//Слушаем изменение размеров окна
|
||||
window.addEventListener("resize", () => {
|
||||
setDisplaySize(getDisplaySize());
|
||||
});
|
||||
//Инициализируем приложение
|
||||
initApp();
|
||||
}
|
||||
}, [state.initialized, initApp]);
|
||||
|
||||
//Вернём компонент провайдера
|
||||
return (
|
||||
<ApplicationСtx.Provider
|
||||
value={{
|
||||
findPanelByName,
|
||||
pOnlineShowUnit,
|
||||
pOnlineShowDocument,
|
||||
pOnlineShowDictionary,
|
||||
pOnlineUserProcedure,
|
||||
pOnlineUserReport,
|
||||
appState: state
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ApplicationСtx.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Провайдер контекста приложения
|
||||
ApplicationContext.propTypes = {
|
||||
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node])
|
||||
};
|
68
app/context/application_reducer.js
Normal file
68
app/context/application_reducer.js
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Контекст: Приложение - редьюсер состояния
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { getDisplaySize } from "../core/utils"; //Вспомогательные функции
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Типы действий
|
||||
const APP_AT = {
|
||||
LOAD_PANELS: "LOAD_PANELS", //Загрузка списка панелей
|
||||
SET_INITIALIZED: "SET_INITIALIZED", //Установка флага инициализированности приложения
|
||||
SET_DISPLAY_SIZE: "SET_DISPLAY_SIZE" //Установка текущего типового размера экрана
|
||||
};
|
||||
|
||||
//Состояние приложения по умолчанию
|
||||
const INITIAL_STATE = {
|
||||
displaySize: getDisplaySize(),
|
||||
panels: [],
|
||||
panelsLoaded: false,
|
||||
initialized: false
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Обработчики действий
|
||||
const handlers = {
|
||||
//Загрузка списка панелей
|
||||
[APP_AT.LOAD_PANELS]: (state, { payload }) => {
|
||||
let panels = [];
|
||||
if (payload && Array.isArray(payload)) for (let p of payload) panels.push({ ...p });
|
||||
return {
|
||||
...state,
|
||||
panels,
|
||||
panelsLoaded: true
|
||||
};
|
||||
},
|
||||
//Установка текущего типового размера экрана
|
||||
[APP_AT.SET_INITIALIZED]: state => ({ ...state, initialized: true }),
|
||||
//Установка текущего типового размера экрана
|
||||
[APP_AT.SET_DISPLAY_SIZE]: (state, { payload }) => ({ ...state, displaySize: payload }),
|
||||
//Обработчик по умолчанию
|
||||
DEFAULT: state => state
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
//Константы
|
||||
export { APP_AT, INITIAL_STATE };
|
||||
|
||||
//Редьюсер состояния
|
||||
export const applicationReducer = (state, action) => {
|
||||
//Подберём обработчик
|
||||
const handle = handlers[action.type] || handlers.DEFAULT;
|
||||
//Исполним его
|
||||
return handle(state, action);
|
||||
};
|
104
app/context/backend.js
Normal file
104
app/context/backend.js
Normal file
@ -0,0 +1,104 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Контекст: Взаимодействие с серверным API
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { createContext, useContext, useCallback } from "react"; //ReactJS
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import client from "../core/client"; //Клиент для взаимодействия с сервером
|
||||
import { MessagingСtx } from "./messaging"; //Контекст сообщений
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
//Контекст взаимодействия с серверным API
|
||||
export const BackEndСtx = createContext();
|
||||
|
||||
//Провайдер контекста взаимодействия с серверным API
|
||||
export const BackEndContext = ({ children }) => {
|
||||
//Подключение к контексту сообщений
|
||||
const { showLoader, hideLoader, showMsgErr } = useContext(MessagingСtx);
|
||||
|
||||
//Проверка ответа на наличие ошибки
|
||||
const isRespErr = useCallback(resp => client.isRespErr(resp), []);
|
||||
|
||||
//Извлечение ошибки из ответа
|
||||
const getRespErrMessage = useCallback(resp => client.getRespErrMessage(resp), []);
|
||||
|
||||
//Извлечение полезного содержимого из ответа
|
||||
const getRespPayload = useCallback(resp => client.getRespPayload(resp), []);
|
||||
|
||||
//Запуск хранимой процедуры
|
||||
const executeStored = useCallback(
|
||||
async ({
|
||||
stored,
|
||||
args,
|
||||
respArg,
|
||||
loader = true,
|
||||
loaderMessage = "",
|
||||
throwError = true,
|
||||
showErrorMessage = true,
|
||||
fullResponse = false,
|
||||
spreadOutArguments = true
|
||||
} = {}) => {
|
||||
try {
|
||||
if (loader !== false) showLoader(loaderMessage);
|
||||
let result = await client.executeStored({ stored, args, respArg, throwError, spreadOutArguments });
|
||||
if (fullResponse === true || isRespErr(result)) return result;
|
||||
else return result.XPAYLOAD;
|
||||
} catch (e) {
|
||||
if (showErrorMessage) showMsgErr(e.message);
|
||||
throw e;
|
||||
} finally {
|
||||
if (loader !== false) hideLoader();
|
||||
}
|
||||
},
|
||||
[showLoader, hideLoader, isRespErr, showMsgErr]
|
||||
);
|
||||
|
||||
//Загрузка настроек панелей
|
||||
const getConfig = useCallback(
|
||||
async ({ loader = true, loaderMessage = "", throwError = true, showErrorMessage = true } = {}) => {
|
||||
try {
|
||||
if (loader !== false) showLoader(loaderMessage);
|
||||
let result = await client.getConfig({ throwError });
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (showErrorMessage) showMsgErr(e.message);
|
||||
throw e;
|
||||
} finally {
|
||||
if (loader !== false) hideLoader();
|
||||
}
|
||||
},
|
||||
[showLoader, hideLoader, showMsgErr]
|
||||
);
|
||||
|
||||
//Вернём компонент провайдера
|
||||
return (
|
||||
<BackEndСtx.Provider
|
||||
value={{
|
||||
SERV_DATA_TYPE_STR: client.SERV_DATA_TYPE_STR,
|
||||
SERV_DATA_TYPE_NUMB: client.SERV_DATA_TYPE_NUMB,
|
||||
SERV_DATA_TYPE_DATE: client.SERV_DATA_TYPE_DATE,
|
||||
SERV_DATA_TYPE_CLOB: client.SERV_DATA_TYPE_CLOB,
|
||||
isRespErr,
|
||||
getRespErrMessage,
|
||||
getRespPayload,
|
||||
executeStored,
|
||||
getConfig
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</BackEndСtx.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Провайдер контекста взаимодействия с серверным API
|
||||
BackEndContext.propTypes = {
|
||||
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node])
|
||||
};
|
109
app/context/messaging.js
Normal file
109
app/context/messaging.js
Normal file
@ -0,0 +1,109 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Контекст: Сообщения
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useReducer, createContext, useCallback } from "react"; //ReactJS
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { P8PAppProgress } from "../components/p8p_app_progress"; //Индикатор процесса
|
||||
import { P8PAppMessage } from "../components/p8p_app_message"; //Диалог сообщения
|
||||
import { MSG_AT, MSG_DLGT, INITIAL_STATE, messagingReducer } from "./messaging_reducer"; //Редьюсер состояния
|
||||
import { TITLES, TEXTS, BUTTONS } from "../../app.text"; //Текстовые ресурсы и константы
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
//Контекст сообщений
|
||||
export const MessagingСtx = createContext();
|
||||
|
||||
//Провайдер контекста сообщений
|
||||
export const MessagingContext = ({ children }) => {
|
||||
//Подключим редьюсер состояния
|
||||
const [state, dispatch] = useReducer(messagingReducer, INITIAL_STATE);
|
||||
|
||||
//Отображение загрузчика
|
||||
const showLoader = useCallback(message => dispatch({ type: MSG_AT.SHOW_LOADER, payload: message }), []);
|
||||
|
||||
//Сокрытие загрузчика
|
||||
const hideLoader = useCallback(() => dispatch({ type: MSG_AT.HIDE_LOADER }), []);
|
||||
|
||||
//Отображение сообщения
|
||||
const showMsg = useCallback(
|
||||
(type, text, msgOnOk = null, msgOnCancel = null) => dispatch({ type: MSG_AT.SHOW_MSG, payload: { type, text, msgOnOk, msgOnCancel } }),
|
||||
[]
|
||||
);
|
||||
|
||||
//Отображение сообщения - ошибка
|
||||
const showMsgErr = useCallback((text, msgOnOk = null) => showMsg(MSG_DLGT.ERR, text, msgOnOk), [showMsg]);
|
||||
|
||||
//Отображение сообщения - информация
|
||||
const showMsgInfo = useCallback((text, msgOnOk = null) => showMsg(MSG_DLGT.INFO, text, msgOnOk), [showMsg]);
|
||||
|
||||
//Отображение сообщения - предупреждение
|
||||
const showMsgWarn = useCallback((text, msgOnOk = null, msgOnCancel = null) => showMsg(MSG_DLGT.WARN, text, msgOnOk, msgOnCancel), [showMsg]);
|
||||
|
||||
//Сокрытие сообщения
|
||||
const hideMsg = useCallback(
|
||||
(cancel = false) => {
|
||||
dispatch({ type: MSG_AT.HIDE_MSG });
|
||||
if (!cancel && state.msgOnOk) state.msgOnOk();
|
||||
if (cancel && state.msgOnCancel) state.msgOnCancel();
|
||||
},
|
||||
[state]
|
||||
);
|
||||
|
||||
//Отработка нажатия на "ОК" в сообщении
|
||||
const handleMessageOkClick = () => {
|
||||
hideMsg(false);
|
||||
};
|
||||
|
||||
//Отработка нажатия на "Отмена" в сообщении
|
||||
const handleMessageCancelClick = () => {
|
||||
hideMsg(true);
|
||||
};
|
||||
|
||||
//Вернём компонент провайдера
|
||||
return (
|
||||
<MessagingСtx.Provider
|
||||
value={{
|
||||
showLoader,
|
||||
hideLoader,
|
||||
showMsg,
|
||||
showMsgErr,
|
||||
showMsgInfo,
|
||||
showMsgWarn,
|
||||
hideMsg,
|
||||
MSG_DLGT,
|
||||
msgState: state
|
||||
}}
|
||||
>
|
||||
{state.loading ? <P8PAppProgress open={true} text={state.loadingMessage || TEXTS.LOADING} /> : null}
|
||||
{state.msg ? (
|
||||
<P8PAppMessage
|
||||
open={true}
|
||||
variant={state.msgType}
|
||||
text={state.msgText}
|
||||
title
|
||||
titleText={state.msgType == MSG_DLGT.ERR ? TITLES.ERR : state.msgType == MSG_DLGT.WARN ? TITLES.WARN : TITLES.INFO}
|
||||
okBtn={true}
|
||||
onOk={handleMessageOkClick}
|
||||
okBtnCaption={[MSG_DLGT.ERR, MSG_DLGT.INFO].includes(state.msgType) ? BUTTONS.CLOSE : BUTTONS.OK}
|
||||
cancelBtn={state.msgType == MSG_DLGT.WARN}
|
||||
onCancel={handleMessageCancelClick}
|
||||
cancelBtnCaption={BUTTONS.CANCEL}
|
||||
/>
|
||||
) : null}
|
||||
{children}
|
||||
</MessagingСtx.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Провайдер контекста сообщений
|
||||
MessagingContext.propTypes = {
|
||||
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node])
|
||||
};
|
84
app/context/messaging_reducer.js
Normal file
84
app/context/messaging_reducer.js
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Контекст: Сообщения - редьюсер состояния
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { P8P_APP_MESSAGE_VARIANT } from "../components/p8p_app_message"; //Диалог сообщения
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Типы действий
|
||||
const MSG_AT = {
|
||||
SHOW_LOADER: "SHOW_LOADER", //Отображение индикатора загрузки
|
||||
HIDE_LOADER: "HIDE_LOADER", //Сокрытие индикатора загрузки
|
||||
SHOW_MSG: "SHOW_MSG", //Отображение сообщения
|
||||
HIDE_MSG: "HIDE_MSG" //Сокрытие сообщения
|
||||
};
|
||||
|
||||
//Типы диалогов сообщений
|
||||
const MSG_DLGT = {
|
||||
INFO: P8P_APP_MESSAGE_VARIANT.INFO, //Тип диалога - информация
|
||||
WARN: P8P_APP_MESSAGE_VARIANT.WARN, //Тип диалога - предупреждение
|
||||
ERR: P8P_APP_MESSAGE_VARIANT.ERR //Тип диалога - ошибка
|
||||
};
|
||||
|
||||
//Состояние сообщений по умолчанию
|
||||
const INITIAL_STATE = {
|
||||
loading: false,
|
||||
loadingMessage: "",
|
||||
msg: false,
|
||||
msgType: MSG_DLGT.ERR,
|
||||
msgText: null,
|
||||
msgOnOk: null,
|
||||
msgOnCancel: null
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Обработчики действий
|
||||
const handlers = {
|
||||
//Отображение индикатора обработки данных
|
||||
[MSG_AT.SHOW_LOADER]: (state, { payload }) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
loadingMessage: payload
|
||||
}),
|
||||
//Сокрытие индикатора обработки данных
|
||||
[MSG_AT.HIDE_LOADER]: state => ({ ...state, loading: false }),
|
||||
//Отображение сообщения
|
||||
[MSG_AT.SHOW_MSG]: (state, { payload }) => ({
|
||||
...state,
|
||||
msg: true,
|
||||
msgType: payload.type || MSG_DLGT.APP_ERR,
|
||||
msgText: payload.text,
|
||||
msgOnOk: payload.msgOnOk,
|
||||
msgOnCancel: payload.msgOnCancel
|
||||
}),
|
||||
//Сокрытие сообщения
|
||||
[MSG_AT.HIDE_MSG]: state => ({ ...state, msg: false, msgOnOk: null, msgOnCancel: null }),
|
||||
//Обработчик по умолчанию
|
||||
DEFAULT: state => state
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
//Константы
|
||||
export { MSG_AT, MSG_DLGT, INITIAL_STATE };
|
||||
|
||||
//Редьюсер состояния
|
||||
export const messagingReducer = (state, action) => {
|
||||
//Подберём обработчик
|
||||
const handle = handlers[action.type] || handlers.DEFAULT;
|
||||
//Исполним его
|
||||
return handle(state, action);
|
||||
};
|
123
app/context/navigation.js
Normal file
123
app/context/navigation.js
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Контекст: Навигация
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { createContext, useContext } from "react"; //ReactJS
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { useLocation, useNavigate } from "react-router-dom"; //Роутер приложения
|
||||
import queryString from "query-string"; //Работа со строкой запроса
|
||||
import { ApplicationСtx } from "./application"; //Контекст приложения
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Типовые пути
|
||||
const PATHS = {
|
||||
ROOT: "/" //Корень приложения
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
//Получение корневого пути
|
||||
export const getRootLocation = () => PATHS.ROOT;
|
||||
|
||||
//Контекст навигации
|
||||
export const NavigationCtx = createContext();
|
||||
|
||||
//Провайдер контекста навигации
|
||||
export const NavigationContext = ({ children }) => {
|
||||
//Подключение к объекту роутера для управления навигацией
|
||||
const location = useLocation();
|
||||
|
||||
//Подключение к объекту роутера для управления навигацией
|
||||
const navigate = useNavigate();
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { findPanelByName } = useContext(ApplicationСtx);
|
||||
|
||||
//Проверка наличия параметров запроса
|
||||
const isNavigationSearch = () => (location.search ? true : false);
|
||||
|
||||
//Считываение параметров запроса
|
||||
const getNavigationSearch = () => queryString.parse(location.search);
|
||||
|
||||
//Проверка наличия параметров запроса (передаваемых через состояние)
|
||||
const isNavigationState = () => (location.state ? true : false);
|
||||
|
||||
//Считываение параметров запроса (передаваемых через состояние)
|
||||
const getNavigationState = () => (isNavigationState() ? JSON.parse(location.state) : null);
|
||||
|
||||
//Обновление текущей страницы
|
||||
const refresh = () => window.location.reload();
|
||||
|
||||
//Возврат на предыдущую страницу
|
||||
const navigateBack = () => navigate(-1);
|
||||
|
||||
//Переход к адресу внутри приложения
|
||||
const navigateTo = ({ path, search, state, replace = false }) => {
|
||||
//Если указано куда переходить
|
||||
if (path) {
|
||||
//Переходим к адресу
|
||||
if (state) navigate(path, { state: JSON.stringify(state), replace });
|
||||
else navigate({ pathname: path, search: queryString.stringify(search), replace });
|
||||
//Флаг успешного перехода
|
||||
return true;
|
||||
}
|
||||
//Переход не состоялся
|
||||
else return false;
|
||||
};
|
||||
|
||||
//Переход к домашней страничке
|
||||
const navigateRoot = state => navigateTo({ path: getRootLocation(), state });
|
||||
|
||||
//Переход к панели
|
||||
const navigatePanel = (panel, state) => {
|
||||
if (panel) {
|
||||
let path = getRootLocation();
|
||||
path = !path.endsWith("/") && !panel.url.startsWith("/") ? `${path}/${panel.url}` : `${path}${panel.url}`;
|
||||
navigateTo({ path, state });
|
||||
} else return false;
|
||||
};
|
||||
|
||||
//Переход к панели по наименованию
|
||||
const navigatePanelByName = (name, state) => navigatePanel(findPanelByName(name), state);
|
||||
|
||||
//Переход к произвольному адресу
|
||||
const navigateURL = url => {
|
||||
window.open(url, "_self");
|
||||
};
|
||||
|
||||
//Вернём компонент провайдера
|
||||
return (
|
||||
<NavigationCtx.Provider
|
||||
value={{
|
||||
getNavigationSearch,
|
||||
isNavigationSearch,
|
||||
getNavigationState,
|
||||
isNavigationState,
|
||||
refresh,
|
||||
navigateTo,
|
||||
navigateBack,
|
||||
navigateRoot,
|
||||
navigatePanel,
|
||||
navigatePanelByName,
|
||||
navigateURL
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NavigationCtx.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Провайдер контекста навигации
|
||||
NavigationContext.propTypes = {
|
||||
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node])
|
||||
};
|
216
app/core/client.js
Normal file
216
app/core/client.js
Normal file
@ -0,0 +1,216 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Ядро: Клиент для взаимодействия с сервером приложений "Парус 8 Онлайн"
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { XMLParser, XMLBuilder } from "fast-xml-parser"; //Конвертация XML в JSON и JSON в XML
|
||||
import dayjs from "dayjs"; //Работа с датами
|
||||
import { SYSTEM } from "../../app.config"; //Настройки приложения
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Коды функций сервера
|
||||
const SRV_FN_CODE_EXEC_STORED = "EXEC_STORED"; //Код функции сервера "Запуск хранимой процедуры"
|
||||
|
||||
//Типы данных сервера
|
||||
const SERV_DATA_TYPE_STR = "STR"; //Тип данных "строка"
|
||||
const SERV_DATA_TYPE_NUMB = "NUMB"; //Тип данных "число"
|
||||
const SERV_DATA_TYPE_DATE = "DATE"; //Тип данных "дата"
|
||||
const SERV_DATA_TYPE_CLOB = "CLOB"; //Тип данных "текст"
|
||||
|
||||
//Состояния ответов сервера
|
||||
const RESP_STATUS_OK = "OK"; //Успех
|
||||
const RESP_STATUS_ERR = "ERR"; //Ошибка
|
||||
|
||||
//Типовые ошибки клиента
|
||||
const ERR_APPSERVER = "Ошибка сервера приложений"; //Общая ошибка клиента
|
||||
const ERR_UNEXPECTED = "Неожиданный ответ сервера"; //Неожиданный ответ сервера
|
||||
const ERR_NETWORK = "Ошибка соединения с сервером"; //Ошибка сети
|
||||
|
||||
//Типовые пути конвертации в массив (при переводе XML -> JSON)
|
||||
const XML_ALWAYS_ARRAY_PATHS = [
|
||||
"XRESPOND.XPAYLOAD.XOUT_ARGUMENTS",
|
||||
"XRESPOND.XPAYLOAD.XROWS",
|
||||
"XRESPOND.XPAYLOAD.XCOLUMNS_DEF",
|
||||
"XRESPOND.XPAYLOAD.XCOLUMNS_DEF.values"
|
||||
];
|
||||
|
||||
//Типовой постфикс тега для массива (при переводе XML -> JSON)
|
||||
const XML_ALWAYS_ARRAY_POSTFIX = "__SYSTEM__ARRAY__";
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Определение типа данных значения аргумента
|
||||
const getServerDataType = value => {
|
||||
let res = SERV_DATA_TYPE_STR;
|
||||
if (typeof value == "number") res = SERV_DATA_TYPE_NUMB;
|
||||
if (value instanceof Date) res = SERV_DATA_TYPE_DATE;
|
||||
return res;
|
||||
};
|
||||
|
||||
//Формирование стандартного ответа - ошибка
|
||||
const makeRespErr = ({ message }) => ({ SSTATUS: RESP_STATUS_ERR, SMESSAGE: message });
|
||||
|
||||
//Разбор XML
|
||||
const parseXML = (xmlDoc, isArray, transformTagName) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
let opts = {
|
||||
ignoreDeclaration: true,
|
||||
ignoreAttributes: false,
|
||||
parseAttributeValue: true,
|
||||
attributeNamePrefix: ""
|
||||
};
|
||||
if (isArray) opts.isArray = isArray;
|
||||
if (transformTagName) opts.transformTagName = transformTagName;
|
||||
const parser = new XMLParser(opts);
|
||||
resolve(parser.parse(xmlDoc));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//Формирование XML
|
||||
const buildXML = jsonObj => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const builder = new XMLBuilder({ ignoreAttributes: false, oneListGroup: true });
|
||||
resolve(builder.build(jsonObj));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//Проверка ответа на наличие ошибки
|
||||
const isRespErr = resp => resp && resp?.SSTATUS && resp?.SSTATUS === RESP_STATUS_ERR;
|
||||
|
||||
//Извлечение ошибки из ответа
|
||||
const getRespErrMessage = resp => (isRespErr(resp) && resp.SMESSAGE ? resp.SMESSAGE : "");
|
||||
|
||||
//Извлечение полезного содержимого из ответа
|
||||
const getRespPayload = resp => (resp && resp.XPAYLOAD ? resp.XPAYLOAD : null);
|
||||
|
||||
//Исполнение действия на сервере
|
||||
const executeAction = async ({ serverURL, action, payload = {}, isArray, transformTagName } = {}) => {
|
||||
console.log(`EXECUTING ${action ? action : ""} ON ${serverURL} WITH PAYLOAD:`);
|
||||
console.log(payload ? payload : "NO PAYLOAD");
|
||||
let response = null;
|
||||
let responseJSON = null;
|
||||
try {
|
||||
//Сформируем типовой запрос
|
||||
const rqBody = {
|
||||
XREQUEST: { SACTION: action, XPAYLOAD: payload }
|
||||
};
|
||||
//Выполняем запрос
|
||||
response = await fetch(serverURL, {
|
||||
method: "POST",
|
||||
body: await buildXML(rqBody),
|
||||
headers: {
|
||||
"content-type": "application/xml"
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
//Сетевая ошибка
|
||||
throw new Error(`${ERR_NETWORK}: ${e.message}`);
|
||||
}
|
||||
//Проверим на наличие ошибок HTTP - если есть вернём их
|
||||
if (!response.ok) throw new Error(`${ERR_APPSERVER}: ${response.statusText}`);
|
||||
//Ошибок нет - пробуем разобрать
|
||||
try {
|
||||
let responseText = await response.text();
|
||||
//console.log("SERVER RESPONSE TEXT:");
|
||||
//console.log(responseText);
|
||||
responseJSON = await parseXML(responseText, isArray, transformTagName);
|
||||
} catch (e) {
|
||||
//Что-то пошло не так при парсинге
|
||||
throw new Error(ERR_UNEXPECTED);
|
||||
}
|
||||
//Разобрали, проверяем структуру ответа на обязательные атрибуты
|
||||
if (
|
||||
!responseJSON?.XRESPOND ||
|
||||
!responseJSON?.XRESPOND?.SSTATUS ||
|
||||
![RESP_STATUS_ERR, RESP_STATUS_OK].includes(responseJSON?.XRESPOND?.SSTATUS) ||
|
||||
(responseJSON?.XRESPOND?.SSTATUS === RESP_STATUS_OK && responseJSON?.XRESPOND?.XPAYLOAD == undefined) ||
|
||||
(responseJSON?.XRESPOND?.SSTATUS === RESP_STATUS_ERR && responseJSON?.XRESPOND?.SMESSAGE == undefined)
|
||||
)
|
||||
throw new Error(ERR_UNEXPECTED);
|
||||
//Всё хорошо - возвращаем (без корня, он не нужен)
|
||||
console.log("SERVER RESPONSE JSON:");
|
||||
console.log(responseJSON.XRESPOND);
|
||||
return responseJSON.XRESPOND;
|
||||
};
|
||||
|
||||
//Запуск хранимой процедуры
|
||||
const executeStored = async ({ stored, args, respArg, throwError = true, spreadOutArguments = false } = {}) => {
|
||||
let res = null;
|
||||
try {
|
||||
let serverArgs = [];
|
||||
if (args)
|
||||
for (const arg in args) {
|
||||
let typedArg = false;
|
||||
if (Object.hasOwn(args[arg], "VALUE") && Object.hasOwn(args[arg], "SDATA_TYPE") && args[arg]?.SDATA_TYPE) typedArg = true;
|
||||
const dataType = typedArg ? args[arg].SDATA_TYPE : getServerDataType(args[arg]);
|
||||
let value = typedArg ? args[arg].VALUE : args[arg];
|
||||
if (dataType === SERV_DATA_TYPE_DATE) value = dayjs(value).format("YYYY-MM-DDTHH:mm:ss");
|
||||
serverArgs.push({ XARGUMENT: { SNAME: arg, VALUE: value, SDATA_TYPE: dataType } });
|
||||
}
|
||||
res = await executeAction({
|
||||
serverURL: `${SYSTEM.SERVER}${!SYSTEM.SERVER.endsWith("/") ? "/" : ""}Process`,
|
||||
action: SRV_FN_CODE_EXEC_STORED,
|
||||
payload: { SSTORED: stored, XARGUMENTS: serverArgs, SRESP_ARG: respArg },
|
||||
isArray: (name, jPath) => XML_ALWAYS_ARRAY_PATHS.indexOf(jPath) !== -1 || jPath.endsWith(XML_ALWAYS_ARRAY_POSTFIX)
|
||||
});
|
||||
if (spreadOutArguments === true && Array.isArray(res?.XPAYLOAD?.XOUT_ARGUMENTS)) {
|
||||
let spreadArgs = {};
|
||||
for (let arg of res.XPAYLOAD.XOUT_ARGUMENTS) spreadArgs[arg.SNAME] = arg.VALUE;
|
||||
delete res.XPAYLOAD.XOUT_ARGUMENTS;
|
||||
res.XPAYLOAD = { ...res.XPAYLOAD, ...spreadArgs };
|
||||
}
|
||||
} catch (e) {
|
||||
if (throwError) throw e;
|
||||
else return makeRespErr({ message: e.message });
|
||||
}
|
||||
if (res.SSTATUS === RESP_STATUS_ERR && throwError === true) throw new Error(res.SMESSAGE);
|
||||
return res;
|
||||
};
|
||||
|
||||
//Чтение конфигурации плагина
|
||||
const getConfig = async ({ throwError = true } = {}) => {
|
||||
let res = null;
|
||||
try {
|
||||
res = await executeAction({
|
||||
serverURL: `${SYSTEM.SERVER}${!SYSTEM.SERVER.endsWith("/") ? "/" : ""}GetConfig`
|
||||
});
|
||||
} catch (e) {
|
||||
if (throwError) throw e;
|
||||
else return makeRespErr({ message: e.message });
|
||||
}
|
||||
if (res.SSTATUS === RESP_STATUS_ERR && throwError === true) throw new Error(res.SMESSAGE);
|
||||
return res;
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default {
|
||||
SERV_DATA_TYPE_STR,
|
||||
SERV_DATA_TYPE_NUMB,
|
||||
SERV_DATA_TYPE_DATE,
|
||||
SERV_DATA_TYPE_CLOB,
|
||||
isRespErr,
|
||||
getRespErrMessage,
|
||||
getRespPayload,
|
||||
executeStored,
|
||||
getConfig
|
||||
};
|
69
app/core/utils.js
Normal file
69
app/core/utils.js
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Ядро: Вспомогательные функции
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { XMLBuilder } from "fast-xml-parser"; //Конвертация XML в JSON и JSON в XML
|
||||
import dayjs from "dayjs"; //Работа с датами
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Коды типовых размеров экранов
|
||||
const DISPLAY_SIZE_CODE = {
|
||||
XS: "XS", //eXtra Small - супер маленький экран
|
||||
SM: "SM", //Small - маленький экран
|
||||
MD: "MD", //Middle - средний экран
|
||||
LG: "LG" //Large - большой экран
|
||||
};
|
||||
|
||||
//Типовые размеры экранов
|
||||
const DISPLAY_SIZE = {
|
||||
[DISPLAY_SIZE_CODE.XS]: { WIDTH_FROM: 0, WIDTH_TO: 767 }, //eXtra Small - супер маленький экран < 768px
|
||||
[DISPLAY_SIZE_CODE.SM]: { WIDTH_FROM: 768, WIDTH_TO: 991 }, //Small - маленький экран >= 768px
|
||||
[DISPLAY_SIZE_CODE.MD]: { WIDTH_FROM: 992, WIDTH_TO: 1199 }, //Middle - средний экран >= 992px
|
||||
[DISPLAY_SIZE_CODE.LG]: { WIDTH_FROM: 1200, WIDTH_TO: 1000000 } //Large - большой экран >= 1200px
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Проверка существования значения
|
||||
const hasValue = value => typeof value !== "undefined" && value !== null && value !== "";
|
||||
|
||||
//Проверка типа устройства
|
||||
const getDisplaySize = () => {
|
||||
let res = DISPLAY_SIZE_CODE.MD;
|
||||
Object.keys(DISPLAY_SIZE).map(dspl => {
|
||||
if (window.innerWidth >= DISPLAY_SIZE[dspl].WIDTH_FROM && window.innerWidth <= DISPLAY_SIZE[dspl].WIDTH_TO) res = dspl;
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
//Глубокое копирование объекта
|
||||
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))));
|
||||
};
|
||||
|
||||
//Форматирование даты в формат РФ
|
||||
const formatDateRF = value => (value ? dayjs(value).format("DD.MM.YYYY") : null);
|
||||
|
||||
//Форматирование числа в "Денежном" формате РФ
|
||||
const formatNumberRFCurrency = value => (hasValue(value) ? new Intl.NumberFormat("ru-RU", { minimumFractionDigits: 2 }).format(value) : null);
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { hasValue, getDisplaySize, deepCopyObject, object2Base64XML, formatDateRF, formatNumberRFCurrency };
|
19
app/index.js
Normal file
19
app/index.js
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Точка входа в приложение
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //React
|
||||
import { createRoot } from "react-dom/client"; //Работа с DOM в React
|
||||
import Root from "./root"; //Корневой компонент приложения
|
||||
|
||||
//-----------
|
||||
//Точка входа
|
||||
//-----------
|
||||
|
||||
const root = createRoot(document.getElementById("app-content"));
|
||||
root.render(<Root />);
|
32
app/panels/dummy/dummy.js
Normal file
32
app/panels/dummy/dummy.js
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Загулшка
|
||||
Панель-заглушка
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useContext } from "react"; //Классы React
|
||||
import { NavigationCtx } from "../../context/navigation"; //Контекст навигации
|
||||
import { P8PAppErrorPage } from "../../components/p8p_app_error_page"; //Страница с ошибкой
|
||||
import { BUTTONS, ERROR } from "../../../app.text"; //Текстовые ресурсы и константы
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Заглушка
|
||||
const Dummy = () => {
|
||||
//Подключение к контексту навигации
|
||||
const { navigateBack } = useContext(NavigationCtx);
|
||||
|
||||
//Генерация содержимого
|
||||
return <P8PAppErrorPage errorMessage={ERROR.UNDER_CONSTRUCTION} onNavigate={() => navigateBack()} navigateCaption={BUTTONS.NAVIGATE_BACK} />;
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { Dummy };
|
16
app/panels/dummy/index.js
Normal file
16
app/panels/dummy/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Заглушка
|
||||
Панель-заглушка: точка входа
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { Dummy } from "./dummy"; //Панель-заглушка
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export const RootClass = Dummy;
|
16
app/panels/prj_fin/index.js
Normal file
16
app/panels/prj_fin/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Экономика проектов
|
||||
Панель мониторинга: Точка входа
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { PrjFin } from "./prj_fin"; //Корневая панель экономики проекта
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export const RootClass = PrjFin;
|
59
app/panels/prj_fin/prj_fin.js
Normal file
59
app/panels/prj_fin/prj_fin.js
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Экономика проектов
|
||||
Панель мониторинга: Корневая панель экономики проектов
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState } from "react"; //Классы React
|
||||
import { Box } from "@mui/material"; //Интерфейсные компоненты
|
||||
import { P8PFullScreenDialog } from "../../components/p8p_fullscreen_dialog"; //Полноэкранный диалог
|
||||
import { Projects } from "./projects"; //Список проектов
|
||||
import { Stages } from "./stages"; //Список этапов проекта
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Корневая панель экономики проекта
|
||||
const PrjFin = () => {
|
||||
//Собственное состояние
|
||||
const [prjFinPanel, setPrjFinPanel] = useState({
|
||||
selectedProject: null,
|
||||
stagesFilters: []
|
||||
});
|
||||
|
||||
//При открытии списка этапов проекта
|
||||
const handleStagesOpen = ({ project = {}, filters = [] } = {}) => {
|
||||
setPrjFinPanel(pv => ({ ...pv, selectedProject: { ...project }, stagesFilters: [...filters] }));
|
||||
};
|
||||
|
||||
//При закрытии списка этапов проекта
|
||||
const handleStagesClose = () => {
|
||||
setPrjFinPanel(pv => ({ ...pv, selectedProject: null, stagesFilters: [] }));
|
||||
};
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box p={2}>
|
||||
<Projects onStagesOpen={handleStagesOpen} />
|
||||
{prjFinPanel.selectedProject ? (
|
||||
<P8PFullScreenDialog title={`Этапы проекта "${prjFinPanel.selectedProject.SNAME_USL}"`} onClose={handleStagesClose}>
|
||||
<Stages
|
||||
project={prjFinPanel.selectedProject.NRN}
|
||||
projectName={prjFinPanel.selectedProject.SNAME_USL}
|
||||
filters={prjFinPanel.stagesFilters}
|
||||
/>
|
||||
</P8PFullScreenDialog>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { PrjFin };
|
339
app/panels/prj_fin/projects.js
Normal file
339
app/panels/prj_fin/projects.js
Normal file
@ -0,0 +1,339 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Экономика проектов
|
||||
Панель мониторинга: Список проктов
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useCallback, useEffect, useContext } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Grid, Icon, Stack, Link, Button, Table, TableBody, TableRow, TableCell, Typography, Box, Paper, IconButton } from "@mui/material"; //Интерфейсные компоненты
|
||||
import { hasValue, formatDateRF, formatNumberRFCurrency, object2Base64XML } from "../../core/utils"; //Вспомогательные процедуры и функции
|
||||
import { BUTTONS, TEXTS, INPUTS } from "../../../app.text"; //Тектовые ресурсы и константы
|
||||
import { P8PDataGrid, P8PDATA_GRID_SIZE } from "../../components/p8p_data_grid"; //Таблица данных
|
||||
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
|
||||
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
|
||||
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
|
||||
|
||||
//-----------------------
|
||||
//Вспомогательные функции
|
||||
//-----------------------
|
||||
|
||||
//Количество записей на странице
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
//Формирование значения для колонки "Состояние проекта"
|
||||
const formatPrjStateValue = (value, addText = false) => {
|
||||
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}>{icon}</Icon>
|
||||
{addText == true ? text : null}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
//Формирование значения для контрольных колонок
|
||||
const formatCtrlValue = (value, addText = false) => {
|
||||
if (hasValue(value)) {
|
||||
const [text, icon, color] = value == 0 ? ["В норме", "done", "green"] : ["Требует внимания", "error", "red"];
|
||||
return (
|
||||
<Stack direction="row" gap={0.5} alignItems="center" justifyContent="center">
|
||||
<Icon title={text} sx={{ color }}>
|
||||
{icon}
|
||||
</Icon>
|
||||
{addText == true ? text : null}
|
||||
</Stack>
|
||||
);
|
||||
} else return value;
|
||||
};
|
||||
|
||||
//Форматирование значений колонок
|
||||
const valueFormatter = ({ value, columnDef }) => {
|
||||
switch (columnDef.name) {
|
||||
case "NSTATE":
|
||||
return formatPrjStateValue(value, true);
|
||||
case "DBEGPLAN":
|
||||
case "DENDPLAN":
|
||||
return formatDateRF(value);
|
||||
case "NCTRL_FIN":
|
||||
case "NCTRL_CONTR":
|
||||
case "NCTRL_COEXEC":
|
||||
case "NCTRL_PERIOD":
|
||||
case "NCTRL_COST":
|
||||
case "NCTRL_ACT":
|
||||
return formatCtrlValue(value, true);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
//Генерация представления ячейки заголовка
|
||||
const headCellRender = ({ columnDef }) => {
|
||||
switch (columnDef.name) {
|
||||
case "NSTATE":
|
||||
case "NCTRL_FIN":
|
||||
case "NCTRL_CONTR":
|
||||
case "NCTRL_COEXEC":
|
||||
case "NCTRL_PERIOD":
|
||||
case "NCTRL_COST":
|
||||
case "NCTRL_ACT":
|
||||
return {
|
||||
stackProps: { justifyContent: "center" },
|
||||
cellProps: { align: "center" }
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
//Генерация представления ячейки c данными
|
||||
const dataCellRender = ({ row, columnDef }, handleStagesOpen) => {
|
||||
switch (columnDef.name) {
|
||||
case "SCODE":
|
||||
case "SNAME_USL":
|
||||
return {
|
||||
data: (
|
||||
<Link component="button" variant="body2" align="left" underline="hover" onClick={() => handleStagesOpen({ project: row })}>
|
||||
{row[columnDef.name]}
|
||||
</Link>
|
||||
)
|
||||
};
|
||||
case "NSTATE":
|
||||
return {
|
||||
cellProps: { align: "center" },
|
||||
data: formatPrjStateValue(row[columnDef.name], false)
|
||||
};
|
||||
case "NCTRL_FIN":
|
||||
case "NCTRL_CONTR":
|
||||
case "NCTRL_COEXEC":
|
||||
case "NCTRL_PERIOD":
|
||||
case "NCTRL_COST":
|
||||
case "NCTRL_ACT":
|
||||
return {
|
||||
cellProps: { align: "center" },
|
||||
data: hasValue(row[columnDef.name]) ? (
|
||||
<IconButton onClick={() => handleStagesOpen({ project: row, filters: [{ name: columnDef.name, from: row[columnDef.name] }] })}>
|
||||
{formatCtrlValue(row[columnDef.name], false)}
|
||||
</IconButton>
|
||||
) : null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
//Генерация представления расширения строки
|
||||
const rowExpandRender = ({ columnsDef, row }, pOnlineShowDocument, showProjectPayNotes, handleStagesOpen) => {
|
||||
const cardColumns = columnsDef.filter(
|
||||
columnDef =>
|
||||
columnDef.visible == false &&
|
||||
columnDef.name != "NRN" &&
|
||||
!columnDef.name.startsWith("SLNK_UNIT_") &&
|
||||
!columnDef.name.startsWith("NLNK_DOCUMENT_") &&
|
||||
hasValue(row[columnDef.name])
|
||||
);
|
||||
const formatColumnValue = (name, value) =>
|
||||
name.startsWith("N") ? formatNumberRFCurrency(value) : name.startsWith("D") ? formatDateRF(value) : value;
|
||||
return (
|
||||
<Box p={2}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={1}>
|
||||
<Stack spacing={2}>
|
||||
<Button fullWidth variant="contained" onClick={() => handleStagesOpen({ project: row })}>
|
||||
Этапы
|
||||
</Button>
|
||||
<Button fullWidth variant="contained" onClick={() => pOnlineShowDocument({ unitCode: "Projects", document: row.NRN })}>
|
||||
<nobr>В раздел</nobr>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={11}>
|
||||
<Paper elevation={5}>
|
||||
<Table sx={{ width: "100%" }} size="small">
|
||||
<TableBody>
|
||||
{cardColumns.map((cardColumn, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell sx={{ width: "1px", whiteSpace: "nowrap" }}>
|
||||
<Typography variant="h6" color="primary">
|
||||
<nowrap>{cardColumn.caption}:</nowrap>
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ paddingLeft: 0 }}>
|
||||
{hasValue(row[`SLNK_UNIT_${cardColumn.name}`]) && hasValue(row[`NLNK_DOCUMENT_${cardColumn.name}`]) ? (
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
align="left"
|
||||
underline="always"
|
||||
onClick={() => {
|
||||
if (["NFIN_IN", "NFIN_OUT"].includes(cardColumn.name))
|
||||
showProjectPayNotes(row.NRN, row[`NLNK_DOCUMENT_${cardColumn.name}`]);
|
||||
else
|
||||
pOnlineShowDocument({
|
||||
unitCode: row[`SLNK_UNIT_${cardColumn.name}`],
|
||||
document: row[`NLNK_DOCUMENT_${cardColumn.name}`]
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
{formatColumnValue(cardColumn.name, row[cardColumn.name])}
|
||||
</Typography>
|
||||
</Link>
|
||||
) : (
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
{formatColumnValue(cardColumn.name, row[cardColumn.name])}
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Список проектов
|
||||
const Projects = ({ onStagesOpen }) => {
|
||||
//Собственное состояние
|
||||
const [projectsDataGrid, setProjectsDataGrid] = useState({
|
||||
dataLoaded: false,
|
||||
columnsDef: [],
|
||||
filters: null,
|
||||
orders: null,
|
||||
rows: [],
|
||||
reload: true,
|
||||
pageNumber: 1,
|
||||
morePages: true
|
||||
});
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowDocument, pOnlineShowUnit } = useContext(ApplicationСtx);
|
||||
|
||||
//Подключение к контексту сообщений
|
||||
const { showMsgErr } = useContext(MessagingСtx);
|
||||
|
||||
//Загрузка данных проектов с сервера
|
||||
const loadProjects = useCallback(async () => {
|
||||
if (projectsDataGrid.reload) {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.LIST",
|
||||
args: {
|
||||
CFILTERS: { VALUE: object2Base64XML(projectsDataGrid.filters, { arrayNodeName: "filters" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
|
||||
CORDERS: { VALUE: object2Base64XML(projectsDataGrid.orders, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
|
||||
NPAGE_NUMBER: projectsDataGrid.pageNumber,
|
||||
NPAGE_SIZE: PAGE_SIZE,
|
||||
NINCLUDE_DEF: projectsDataGrid.dataLoaded ? 0 : 1
|
||||
},
|
||||
respArg: "COUT"
|
||||
});
|
||||
setProjectsDataGrid(pv => ({
|
||||
...pv,
|
||||
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.XROWS || []).length >= PAGE_SIZE
|
||||
}));
|
||||
}
|
||||
}, [
|
||||
projectsDataGrid.reload,
|
||||
projectsDataGrid.filters,
|
||||
projectsDataGrid.orders,
|
||||
projectsDataGrid.dataLoaded,
|
||||
projectsDataGrid.pageNumber,
|
||||
executeStored,
|
||||
SERV_DATA_TYPE_CLOB
|
||||
]);
|
||||
|
||||
//Отображение журнала платежей по этапу проекта
|
||||
const showProjectPayNotes = async (project, direction) => {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.SELECT_FIN",
|
||||
args: { NRN: project, NDIRECTION: direction }
|
||||
});
|
||||
if (data.NIDENT) pOnlineShowUnit({ unitCode: "PayNotes", inputParameters: [{ name: "in_SelectList_Ident", value: data.NIDENT }] });
|
||||
else showMsgErr(TEXTS.NO_DATA_FOUND);
|
||||
};
|
||||
|
||||
//При изменении состояния фильтра
|
||||
const handleFilterChanged = ({ filters }) => setProjectsDataGrid(pv => ({ ...pv, filters: [...filters], pageNumber: 1, reload: true }));
|
||||
|
||||
//При изменении состояния сортировки
|
||||
const handleOrderChanged = ({ orders }) => setProjectsDataGrid(pv => ({ ...pv, orders: [...orders], pageNumber: 1, reload: true }));
|
||||
|
||||
//При изменении количества отображаемых страниц
|
||||
const handlePagesCountChanged = () => setProjectsDataGrid(pv => ({ ...pv, pageNumber: pv.pageNumber + 1, reload: true }));
|
||||
|
||||
//При открытии списка этапов
|
||||
const handleStagesOpen = ({ project, filters }) => (onStagesOpen ? onStagesOpen({ project, filters }) : null);
|
||||
|
||||
//При необходимости обновить данные
|
||||
useEffect(() => {
|
||||
loadProjects();
|
||||
}, [projectsDataGrid.reload, loadProjects]);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<>
|
||||
{projectsDataGrid.dataLoaded ? (
|
||||
<P8PDataGrid
|
||||
columnsDef={projectsDataGrid.columnsDef}
|
||||
rows={projectsDataGrid.rows}
|
||||
size={P8PDATA_GRID_SIZE.SMALL}
|
||||
morePages={projectsDataGrid.morePages}
|
||||
reloading={projectsDataGrid.reload}
|
||||
expandable={true}
|
||||
orderAscMenuItemCaption={BUTTONS.ORDER_ASC}
|
||||
orderDescMenuItemCaption={BUTTONS.ORDER_DESC}
|
||||
filterMenuItemCaption={BUTTONS.FILTER}
|
||||
valueFilterCaption={INPUTS.VALUE}
|
||||
valueFromFilterCaption={INPUTS.VALUE_FROM}
|
||||
valueToFilterCaption={INPUTS.VALUE_TO}
|
||||
okFilterBtnCaption={BUTTONS.OK}
|
||||
clearFilterBtnCaption={BUTTONS.CLEAR}
|
||||
cancelFilterBtnCaption={BUTTONS.CANCEL}
|
||||
morePagesBtnCaption={BUTTONS.MORE}
|
||||
noDataFoundText={TEXTS.NO_DATA_FOUND}
|
||||
headCellRender={headCellRender}
|
||||
dataCellRender={prms => dataCellRender(prms, handleStagesOpen)}
|
||||
rowExpandRender={prms => rowExpandRender(prms, pOnlineShowDocument, showProjectPayNotes, handleStagesOpen)}
|
||||
valueFormatter={valueFormatter}
|
||||
onOrderChanged={handleOrderChanged}
|
||||
onFilterChanged={handleFilterChanged}
|
||||
onPagesCountChanged={handlePagesCountChanged}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Список проектов
|
||||
Projects.propTypes = {
|
||||
onStagesOpen: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { Projects };
|
201
app/panels/prj_fin/stage_arts.js
Normal file
201
app/panels/prj_fin/stage_arts.js
Normal file
@ -0,0 +1,201 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Экономика проектов
|
||||
Панель мониторинга: Калькуляция этапа проекта
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useCallback, useEffect, useContext } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Box, Icon, Stack, Link } from "@mui/material"; //Интерфейсные компоненты
|
||||
import { hasValue, formatNumberRFCurrency, object2Base64XML } from "../../core/utils"; //Вспомогательные процедуры и функции
|
||||
import { BUTTONS, TEXTS, INPUTS } from "../../../app.text"; //Тектовые ресурсы и константы
|
||||
import { P8PDataGrid, P8PDATA_GRID_SIZE, P8PDATA_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"; //Контекст сообщений
|
||||
|
||||
//-----------------------
|
||||
//Вспомогательные функции
|
||||
//-----------------------
|
||||
|
||||
//Формирование значения для контрольных колонок
|
||||
const formatCtrlValue = (value, addText = false) => {
|
||||
if (hasValue(value)) {
|
||||
const [text, icon, color] = value == 0 ? ["В норме", "done", "green"] : ["Требует внимания", "error", "red"];
|
||||
return (
|
||||
<Stack direction="row" gap={0.5} alignItems="center" justifyContent="center">
|
||||
<Icon title={text} sx={{ color }}>
|
||||
{icon}
|
||||
</Icon>
|
||||
{addText == true ? text : null}
|
||||
</Stack>
|
||||
);
|
||||
} else return value;
|
||||
};
|
||||
|
||||
//Форматирование значений колонок
|
||||
const valueFormatter = ({ value, columnDef }) => {
|
||||
switch (columnDef.name) {
|
||||
case "NPLAN":
|
||||
case "NCOST_FACT":
|
||||
case "NCONTR":
|
||||
return formatNumberRFCurrency(value);
|
||||
case "NCTRL_COST":
|
||||
case "NCTRL_CONTR":
|
||||
return formatCtrlValue(value, true);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
//Генерация представления ячейки c данными
|
||||
const dataCellRender = ({ row, columnDef }, showStageArtCostNotes, showStageArtContracts) => {
|
||||
switch (columnDef.name) {
|
||||
case "NCOST_FACT":
|
||||
case "NCONTR":
|
||||
return {
|
||||
data: row[columnDef.name] ? (
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
align="left"
|
||||
underline="hover"
|
||||
onClick={() => (columnDef.name === "NCOST_FACT" ? showStageArtCostNotes(row.NRN) : showStageArtContracts(row.NRN))}
|
||||
>
|
||||
{formatNumberRFCurrency(row[columnDef.name])}
|
||||
</Link>
|
||||
) : null
|
||||
};
|
||||
case "NCTRL_COST":
|
||||
case "NCTRL_CONTR":
|
||||
return {
|
||||
data: (
|
||||
<Stack sx={{ justifyContent: "right" }} direction="row" spacing={1}>
|
||||
<div style={{ color: row[columnDef.name] === 1 ? "red" : "green", display: "flex", alignItems: "center" }}>
|
||||
{formatNumberRFCurrency(row[columnDef.name === "NCTRL_COST" ? "NCOST_DIFF" : "NCONTR_LEFT"])}
|
||||
</div>
|
||||
{formatCtrlValue(row[columnDef.name], false)}
|
||||
</Stack>
|
||||
)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Калькуляция этапа проекта
|
||||
const StageArts = ({ stage, filters }) => {
|
||||
//Собственное состояние
|
||||
const [stageArtsDataGrid, setStageArtsDataGrid] = useState({
|
||||
dataLoaded: false,
|
||||
columnsDef: [],
|
||||
filters: [...filters],
|
||||
rows: [],
|
||||
reload: true
|
||||
});
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowUnit } = useContext(ApplicationСtx);
|
||||
|
||||
//Подключение к контексту сообщений
|
||||
const { showMsgErr } = useContext(MessagingСtx);
|
||||
|
||||
//Загрузка данных калькуляции этапа с сервера
|
||||
const loadStageArts = useCallback(async () => {
|
||||
if (stageArtsDataGrid.reload) {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.STAGE_ARTS_LIST",
|
||||
args: {
|
||||
NSTAGE: stage,
|
||||
CFILTERS: { VALUE: object2Base64XML(stageArtsDataGrid.filters, { arrayNodeName: "filters" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
|
||||
NINCLUDE_DEF: stageArtsDataGrid.dataLoaded ? 0 : 1
|
||||
},
|
||||
respArg: "COUT"
|
||||
});
|
||||
setStageArtsDataGrid(pv => ({
|
||||
...pv,
|
||||
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef,
|
||||
rows: [...(data.XROWS || [])],
|
||||
dataLoaded: true,
|
||||
reload: false
|
||||
}));
|
||||
}
|
||||
}, [stage, stageArtsDataGrid.reload, stageArtsDataGrid.filters, stageArtsDataGrid.dataLoaded, executeStored, SERV_DATA_TYPE_CLOB]);
|
||||
|
||||
//Отображение журнала затрат по статье калькуляции
|
||||
const showStageArtCostNotes = async article => {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.STAGE_ARTS_SELECT_COST_FACT",
|
||||
args: { NSTAGE: stage, NFPDARTCL: article }
|
||||
});
|
||||
if (data.NIDENT) pOnlineShowUnit({ unitCode: "CostNotes", inputParameters: [{ name: "in_SelectList_Ident", value: data.NIDENT }] });
|
||||
else showMsgErr(TEXTS.NO_DATA_FOUND);
|
||||
};
|
||||
|
||||
//Отображение договоров по статье калькуляции
|
||||
const showStageArtContracts = async article => {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.STAGE_ARTS_SELECT_CONTR",
|
||||
args: { NSTAGE: stage, NFPDARTCL: article }
|
||||
});
|
||||
if (data.NIDENT) pOnlineShowUnit({ unitCode: "Contracts", inputParameters: [{ name: "in_Ident", value: data.NIDENT }] });
|
||||
else showMsgErr(TEXTS.NO_DATA_FOUND);
|
||||
};
|
||||
|
||||
//При изменении состояния фильтра
|
||||
const handleFilterChanged = ({ filters }) => setStageArtsDataGrid(pv => ({ ...pv, filters, reload: true }));
|
||||
|
||||
//При необходимости обновить данные
|
||||
useEffect(() => {
|
||||
loadStageArts();
|
||||
}, [stageArtsDataGrid.reload, loadStageArts]);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box pt={2}>
|
||||
{stageArtsDataGrid.dataLoaded ? (
|
||||
<P8PDataGrid
|
||||
columnsDef={stageArtsDataGrid.columnsDef}
|
||||
filtersInitial={filters}
|
||||
rows={stageArtsDataGrid.rows}
|
||||
size={P8PDATA_GRID_SIZE.SMALL}
|
||||
morePages={false}
|
||||
reloading={stageArtsDataGrid.reload}
|
||||
orderAscMenuItemCaption={BUTTONS.ORDER_ASC}
|
||||
orderDescMenuItemCaption={BUTTONS.ORDER_DESC}
|
||||
filterMenuItemCaption={BUTTONS.FILTER}
|
||||
valueFilterCaption={INPUTS.VALUE}
|
||||
valueFromFilterCaption={INPUTS.VALUE_FROM}
|
||||
valueToFilterCaption={INPUTS.VALUE_TO}
|
||||
okFilterBtnCaption={BUTTONS.OK}
|
||||
clearFilterBtnCaption={BUTTONS.CLEAR}
|
||||
cancelFilterBtnCaption={BUTTONS.CANCEL}
|
||||
morePagesBtnCaption={BUTTONS.MORE}
|
||||
noDataFoundText={TEXTS.NO_DATA_FOUND}
|
||||
dataCellRender={prms => dataCellRender(prms, showStageArtCostNotes, showStageArtContracts)}
|
||||
valueFormatter={valueFormatter}
|
||||
onFilterChanged={handleFilterChanged}
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Калькуляция этапа проекта
|
||||
StageArts.propTypes = {
|
||||
stage: PropTypes.number.isRequired,
|
||||
filters: PropTypes.arrayOf(P8PDATA_GRID_FILTER_SHAPE)
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { StageArts };
|
256
app/panels/prj_fin/stage_contracts.js
Normal file
256
app/panels/prj_fin/stage_contracts.js
Normal file
@ -0,0 +1,256 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Экономика проектов
|
||||
Панель мониторинга: Договоры с соисполнителями этапа проекта
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useCallback, useEffect, useContext } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Box, Stack, Grid, Paper, Table, TableBody, TableRow, TableCell, Typography, Button, Link } from "@mui/material"; //Интерфейсные компоненты
|
||||
import { hasValue, formatDateRF, formatNumberRFCurrency, object2Base64XML } from "../../core/utils"; //Вспомогательные процедуры и функции
|
||||
import { BUTTONS, TEXTS, INPUTS } from "../../../app.text"; //Тектовые ресурсы и константы
|
||||
import { P8PDataGrid, P8PDATA_GRID_SIZE, P8PDATA_GRID_FILTER_SHAPE } from "../../components/p8p_data_grid"; //Таблица данных
|
||||
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
|
||||
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
|
||||
|
||||
//-----------------------
|
||||
//Вспомогательные функции
|
||||
//-----------------------
|
||||
|
||||
//Количество записей на странице
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
//Форматирование значений колонок
|
||||
const valueFormatter = ({ value, columnDef }) => {
|
||||
switch (columnDef.name) {
|
||||
case "DDOC_DATE":
|
||||
case "DCSTAGE_BEGIN_DATE":
|
||||
case "DCSTAGE_END_DATE":
|
||||
return formatDateRF(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
//Генерация представления ячейки c данными
|
||||
const dataCellRender = ({ row, columnDef }, pOnlineShowDocument) => {
|
||||
switch (columnDef.name) {
|
||||
case "SDOC_PREF":
|
||||
case "SDOC_NUMB":
|
||||
return {
|
||||
data: (
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
align="left"
|
||||
underline="hover"
|
||||
onClick={() =>
|
||||
pOnlineShowDocument({
|
||||
unitCode: row[`SLNK_UNIT_${columnDef.name}`],
|
||||
document: row[`NLNK_DOCUMENT_${columnDef.name}`]
|
||||
})
|
||||
}
|
||||
>
|
||||
{row[columnDef.name]}
|
||||
</Link>
|
||||
)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
//Генерация представления расширения строки
|
||||
const rowExpandRender = ({ columnsDef, row }, pOnlineShowDocument) => {
|
||||
const cardColumns = columnsDef.filter(
|
||||
columnDef =>
|
||||
columnDef.visible == false &&
|
||||
columnDef.name != "NRN" &&
|
||||
!columnDef.name.startsWith("SLNK_UNIT_") &&
|
||||
!columnDef.name.startsWith("NLNK_DOCUMENT_") &&
|
||||
hasValue(row[columnDef.name])
|
||||
);
|
||||
const formatColumnValue = (name, value) =>
|
||||
name.startsWith("N") ? formatNumberRFCurrency(value) : name.startsWith("D") ? formatDateRF(value) : value;
|
||||
return (
|
||||
<Box p={2}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={1}>
|
||||
<Stack spacing={2}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
onClick={() => pOnlineShowDocument({ unitCode: row.SLNK_UNIT_SDOC_PREF, document: row.NLNK_DOCUMENT_SDOC_PREF })}
|
||||
>
|
||||
<nobr>В раздел</nobr>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={11}>
|
||||
<Paper elevation={5}>
|
||||
<Table sx={{ width: "100%" }} size="small">
|
||||
<TableBody>
|
||||
{cardColumns.map((cardColumn, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell sx={{ width: "1px", whiteSpace: "nowrap" }}>
|
||||
<Typography variant="h6" color="primary">
|
||||
{cardColumn.caption}:
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ paddingLeft: 0 }}>
|
||||
{hasValue(row[`SLNK_UNIT_${cardColumn.name}`]) && hasValue(row[`NLNK_DOCUMENT_${cardColumn.name}`]) ? (
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
align="left"
|
||||
underline="always"
|
||||
onClick={() =>
|
||||
pOnlineShowDocument({
|
||||
unitCode: row[`SLNK_UNIT_${cardColumn.name}`],
|
||||
document: row[`NLNK_DOCUMENT_${cardColumn.name}`]
|
||||
})
|
||||
}
|
||||
>
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
{formatColumnValue(cardColumn.name, row[cardColumn.name])}
|
||||
</Typography>
|
||||
</Link>
|
||||
) : (
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
{formatColumnValue(cardColumn.name, row[cardColumn.name])}
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Договоры с соисполнителями этапа проекта
|
||||
const StageContracts = ({ stage, filters }) => {
|
||||
//Собственное состояние
|
||||
const [stageContractsDataGrid, setStageContractsDataGrid] = useState({
|
||||
dataLoaded: false,
|
||||
columnsDef: [],
|
||||
filters: [...filters],
|
||||
orders: null,
|
||||
rows: [],
|
||||
reload: true,
|
||||
pageNumber: 1,
|
||||
morePages: true
|
||||
});
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowDocument } = useContext(ApplicationСtx);
|
||||
|
||||
//Загрузка данных этапов с сервера
|
||||
const loadStageContracts = useCallback(async () => {
|
||||
if (stageContractsDataGrid.reload) {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.STAGE_CONTRACTS_LIST",
|
||||
args: {
|
||||
NSTAGE: stage,
|
||||
CFILTERS: {
|
||||
VALUE: object2Base64XML(stageContractsDataGrid.filters, { arrayNodeName: "filters" }),
|
||||
SDATA_TYPE: SERV_DATA_TYPE_CLOB
|
||||
},
|
||||
CORDERS: { VALUE: object2Base64XML(stageContractsDataGrid.orders, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
|
||||
NPAGE_NUMBER: stageContractsDataGrid.pageNumber,
|
||||
NPAGE_SIZE: PAGE_SIZE,
|
||||
NINCLUDE_DEF: stageContractsDataGrid.dataLoaded ? 0 : 1
|
||||
},
|
||||
respArg: "COUT"
|
||||
});
|
||||
setStageContractsDataGrid(pv => ({
|
||||
...pv,
|
||||
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.XROWS || []).length >= PAGE_SIZE
|
||||
}));
|
||||
}
|
||||
}, [
|
||||
stage,
|
||||
stageContractsDataGrid.reload,
|
||||
stageContractsDataGrid.filters,
|
||||
stageContractsDataGrid.orders,
|
||||
stageContractsDataGrid.dataLoaded,
|
||||
stageContractsDataGrid.pageNumber,
|
||||
executeStored,
|
||||
SERV_DATA_TYPE_CLOB
|
||||
]);
|
||||
|
||||
//При изменении состояния фильтра
|
||||
const handleFilterChanged = ({ filters }) => setStageContractsDataGrid(pv => ({ ...pv, filters, pageNumber: 1, reload: true }));
|
||||
|
||||
//При изменении состояния сортировки
|
||||
const handleOrderChanged = ({ orders }) => setStageContractsDataGrid(pv => ({ ...pv, orders, pageNumber: 1, reload: true }));
|
||||
|
||||
//При изменении количества отображаемых страниц
|
||||
const handlePagesCountChanged = () => setStageContractsDataGrid(pv => ({ ...pv, pageNumber: pv.pageNumber + 1, reload: true }));
|
||||
|
||||
//При необходимости обновить данные
|
||||
useEffect(() => {
|
||||
loadStageContracts();
|
||||
}, [stageContractsDataGrid.reload, loadStageContracts]);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box pt={2}>
|
||||
{stageContractsDataGrid.dataLoaded ? (
|
||||
<P8PDataGrid
|
||||
columnsDef={stageContractsDataGrid.columnsDef}
|
||||
filtersInitial={filters}
|
||||
rows={stageContractsDataGrid.rows}
|
||||
size={P8PDATA_GRID_SIZE.SMALL}
|
||||
morePages={stageContractsDataGrid.morePages}
|
||||
reloading={stageContractsDataGrid.reload}
|
||||
expandable={true}
|
||||
orderAscMenuItemCaption={BUTTONS.ORDER_ASC}
|
||||
orderDescMenuItemCaption={BUTTONS.ORDER_DESC}
|
||||
filterMenuItemCaption={BUTTONS.FILTER}
|
||||
valueFilterCaption={INPUTS.VALUE}
|
||||
valueFromFilterCaption={INPUTS.VALUE_FROM}
|
||||
valueToFilterCaption={INPUTS.VALUE_TO}
|
||||
okFilterBtnCaption={BUTTONS.OK}
|
||||
clearFilterBtnCaption={BUTTONS.CLEAR}
|
||||
cancelFilterBtnCaption={BUTTONS.CANCEL}
|
||||
morePagesBtnCaption={BUTTONS.MORE}
|
||||
noDataFoundText={TEXTS.NO_DATA_FOUND}
|
||||
dataCellRender={prms => dataCellRender(prms, pOnlineShowDocument)}
|
||||
rowExpandRender={prms => rowExpandRender(prms, pOnlineShowDocument)}
|
||||
valueFormatter={valueFormatter}
|
||||
onOrderChanged={handleOrderChanged}
|
||||
onFilterChanged={handleFilterChanged}
|
||||
onPagesCountChanged={handlePagesCountChanged}
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Договоры с соисполнителями этапа проекта
|
||||
StageContracts.propTypes = {
|
||||
stage: PropTypes.number.isRequired,
|
||||
filters: PropTypes.arrayOf(P8PDATA_GRID_FILTER_SHAPE)
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { StageContracts };
|
408
app/panels/prj_fin/stages.js
Normal file
408
app/panels/prj_fin/stages.js
Normal file
@ -0,0 +1,408 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Экономика проектов
|
||||
Панель мониторинга: Список этапов проекта
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useCallback, useEffect, useContext } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Box, Icon, Stack, Grid, Paper, Table, TableBody, TableRow, TableCell, Typography, Button, IconButton, Link } from "@mui/material"; //Интерфейсные компоненты
|
||||
import { hasValue, formatDateRF, formatNumberRFCurrency, object2Base64XML } from "../../core/utils"; //Вспомогательные процедуры и функции
|
||||
import { BUTTONS, TEXTS, INPUTS } from "../../../app.text"; //Тектовые ресурсы и константы
|
||||
import { P8PDataGrid, P8PDATA_GRID_SIZE, P8PDATA_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"; //Договоры с соисполнителями этапа проекта
|
||||
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
|
||||
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
|
||||
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
|
||||
|
||||
//-----------------------
|
||||
//Вспомогательные функции
|
||||
//-----------------------
|
||||
|
||||
//Количество записей на странице
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
//Формирование значения для колонки "Состояние"
|
||||
const formatStageStatusValue = (value, addText = false) => {
|
||||
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="center">
|
||||
<Icon title={text}>{icon}</Icon>
|
||||
{addText == true ? text : null}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
//Формирование значения для контрольных колонок
|
||||
const formatCtrlValue = (value, addText = false) => {
|
||||
if (hasValue(value)) {
|
||||
const [text, icon, color] = value == 0 ? ["В норме", "done", "green"] : ["Требует внимания", "error", "red"];
|
||||
return (
|
||||
<Stack direction="row" gap={0.5} alignItems="center" justifyContent="center">
|
||||
<Icon title={text} sx={{ color }}>
|
||||
{icon}
|
||||
</Icon>
|
||||
{addText == true ? text : null}
|
||||
</Stack>
|
||||
);
|
||||
} else return value;
|
||||
};
|
||||
|
||||
//Форматирование значений колонок
|
||||
const valueFormatter = ({ value, columnDef }) => {
|
||||
switch (columnDef.name) {
|
||||
case "NSTATE":
|
||||
return formatStageStatusValue(value, true);
|
||||
case "DBEGPLAN":
|
||||
case "DENDPLAN":
|
||||
return formatDateRF(value);
|
||||
case "NCTRL_FIN":
|
||||
case "NCTRL_CONTR":
|
||||
case "NCTRL_COEXEC":
|
||||
case "NCTRL_PERIOD":
|
||||
case "NCTRL_COST":
|
||||
case "NCTRL_ACT":
|
||||
return formatCtrlValue(value, true);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
//Генерация представления ячейки заголовка
|
||||
const headCellRender = ({ columnDef }) => {
|
||||
switch (columnDef.name) {
|
||||
case "NSTATE":
|
||||
case "NCTRL_FIN":
|
||||
case "NCTRL_CONTR":
|
||||
case "NCTRL_COEXEC":
|
||||
case "NCTRL_COST":
|
||||
case "NCTRL_ACT":
|
||||
return {
|
||||
stackProps: { justifyContent: "center" },
|
||||
cellProps: { align: "center" }
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
//Генерация представления ячейки c данными
|
||||
const dataCellRender = ({ row, columnDef }, showStageArts) => {
|
||||
switch (columnDef.name) {
|
||||
case "NSTATE":
|
||||
return {
|
||||
cellProps: { align: "center" },
|
||||
data: formatStageStatusValue(row[columnDef.name], false)
|
||||
};
|
||||
case "NCTRL_FIN":
|
||||
case "NCTRL_COEXEC":
|
||||
case "NCTRL_ACT":
|
||||
return {
|
||||
cellProps: { align: "center" },
|
||||
data: formatCtrlValue(row[columnDef.name], false)
|
||||
};
|
||||
case "NCTRL_CONTR":
|
||||
case "NCTRL_COST":
|
||||
return {
|
||||
cellProps: { align: "center" },
|
||||
data: hasValue(row[columnDef.name]) ? (
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
showStageArts({ stage: row.NRN, stageNumb: row.SNUMB, filters: [{ name: columnDef.name, from: row[columnDef.name] }] })
|
||||
}
|
||||
>
|
||||
{formatCtrlValue(row[columnDef.name], false)}
|
||||
</IconButton>
|
||||
) : null
|
||||
};
|
||||
case "NCTRL_PERIOD":
|
||||
return {
|
||||
cellProps: { align: "right" },
|
||||
data: hasValue(row[columnDef.name]) ? (
|
||||
<Stack sx={{ justifyContent: "right" }} direction="row" spacing={1}>
|
||||
<div style={{ color: row[columnDef.name] === 1 ? "red" : "green", display: "flex", alignItems: "center" }}>
|
||||
{row.NDAYS_LEFT} дн.
|
||||
</div>
|
||||
{formatCtrlValue(row[columnDef.name], false)}
|
||||
</Stack>
|
||||
) : null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
//Генерация представления расширения строки
|
||||
const rowExpandRender = ({ columnsDef, row }, pOnlineShowDocument, showStageArts, showStageContracts, showStagePayNotes, showStageCostNotes) => {
|
||||
const cardColumns = columnsDef.filter(
|
||||
columnDef =>
|
||||
columnDef.visible == false &&
|
||||
columnDef.name != "NRN" &&
|
||||
!columnDef.name.startsWith("SLNK_UNIT_") &&
|
||||
!columnDef.name.startsWith("NLNK_DOCUMENT_") &&
|
||||
hasValue(row[columnDef.name])
|
||||
);
|
||||
const formatColumnValue = (name, value) =>
|
||||
name.startsWith("N") ? formatNumberRFCurrency(value) : name.startsWith("D") ? formatDateRF(value) : value;
|
||||
return (
|
||||
<Box p={2}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={1}>
|
||||
<Stack spacing={2}>
|
||||
<Button fullWidth variant="contained" onClick={() => showStageArts({ stage: row.NRN, stageNumb: row.SNUMB })}>
|
||||
<nobr>Статьи</nobr>
|
||||
</Button>
|
||||
<Button fullWidth variant="contained" onClick={() => showStageContracts({ stage: row.NRN, stageNumb: row.SNUMB })}>
|
||||
<nobr>Сисполнители</nobr>
|
||||
</Button>
|
||||
<Button fullWidth variant="contained" onClick={() => pOnlineShowDocument({ unitCode: "ProjectsStages", document: row.NRN })}>
|
||||
<nobr>В раздел</nobr>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={11}>
|
||||
<Paper elevation={5}>
|
||||
<Table sx={{ width: "100%" }} size="small">
|
||||
<TableBody>
|
||||
{cardColumns.map((cardColumn, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell sx={{ width: "1px", whiteSpace: "nowrap" }}>
|
||||
<Typography variant="h6" color="primary">
|
||||
{cardColumn.caption}:
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ paddingLeft: 0 }}>
|
||||
{hasValue(row[`SLNK_UNIT_${cardColumn.name}`]) && hasValue(row[`NLNK_DOCUMENT_${cardColumn.name}`]) ? (
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
align="left"
|
||||
underline="always"
|
||||
onClick={() => {
|
||||
if (["NFIN_IN", "NFIN_OUT"].includes(cardColumn.name))
|
||||
showStagePayNotes(row.NRN, row[`NLNK_DOCUMENT_${cardColumn.name}`]);
|
||||
else if (cardColumn.name == "NCOST_FACT") showStageCostNotes(row.NRN);
|
||||
else
|
||||
pOnlineShowDocument({
|
||||
unitCode: row[`SLNK_UNIT_${cardColumn.name}`],
|
||||
document: row[`NLNK_DOCUMENT_${cardColumn.name}`]
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
{formatColumnValue(cardColumn.name, row[cardColumn.name])}
|
||||
</Typography>
|
||||
</Link>
|
||||
) : (
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
{["NDAYS_LEFT", "NINCOME_PRC"].includes(cardColumn.name)
|
||||
? row[cardColumn.name]
|
||||
: formatColumnValue(cardColumn.name, row[cardColumn.name])}
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Список этапов проекта
|
||||
const Stages = ({ project, projectName, filters }) => {
|
||||
//Собственное состояние
|
||||
const [stagesDataGrid, setStagesDataGrid] = useState({
|
||||
dataLoaded: false,
|
||||
columnsDef: [],
|
||||
filters: [...filters],
|
||||
orders: null,
|
||||
rows: [],
|
||||
reload: true,
|
||||
pageNumber: 1,
|
||||
morePages: true,
|
||||
selectedStageNumb: null,
|
||||
showStageArts: null,
|
||||
stageArtsFilters: [],
|
||||
showStageContracts: null,
|
||||
stageContractsFilters: []
|
||||
});
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowDocument, pOnlineShowUnit } = useContext(ApplicationСtx);
|
||||
|
||||
//Подключение к контексту сообщений
|
||||
const { showMsgErr } = useContext(MessagingСtx);
|
||||
|
||||
//Загрузка данных этапов с сервера
|
||||
const loadStages = useCallback(async () => {
|
||||
if (stagesDataGrid.reload) {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.STAGES_LIST",
|
||||
args: {
|
||||
NPRN: project,
|
||||
CFILTERS: { VALUE: object2Base64XML(stagesDataGrid.filters, { arrayNodeName: "filters" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
|
||||
CORDERS: { VALUE: object2Base64XML(stagesDataGrid.orders, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
|
||||
NPAGE_NUMBER: stagesDataGrid.pageNumber,
|
||||
NPAGE_SIZE: PAGE_SIZE,
|
||||
NINCLUDE_DEF: stagesDataGrid.dataLoaded ? 0 : 1
|
||||
},
|
||||
respArg: "COUT"
|
||||
});
|
||||
setStagesDataGrid(pv => ({
|
||||
...pv,
|
||||
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.XROWS || []).length >= PAGE_SIZE
|
||||
}));
|
||||
}
|
||||
}, [
|
||||
project,
|
||||
stagesDataGrid.reload,
|
||||
stagesDataGrid.filters,
|
||||
stagesDataGrid.orders,
|
||||
stagesDataGrid.dataLoaded,
|
||||
stagesDataGrid.pageNumber,
|
||||
executeStored,
|
||||
SERV_DATA_TYPE_CLOB
|
||||
]);
|
||||
|
||||
//Отображение журнала платежей по этапу проекта
|
||||
const showStagePayNotes = async (stage, direction) => {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.STAGES_SELECT_FIN",
|
||||
args: { NRN: stage, NDIRECTION: direction }
|
||||
});
|
||||
if (data.NIDENT) pOnlineShowUnit({ unitCode: "PayNotes", inputParameters: [{ name: "in_SelectList_Ident", value: data.NIDENT }] });
|
||||
else showMsgErr(TEXTS.NO_DATA_FOUND);
|
||||
};
|
||||
|
||||
//Отображение журнала затрат по этапу проекта
|
||||
const showStageCostNotes = async stage => {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.STAGES_SELECT_COST_FACT",
|
||||
args: { NRN: stage }
|
||||
});
|
||||
if (data.NIDENT) pOnlineShowUnit({ unitCode: "CostNotes", inputParameters: [{ name: "in_SelectList_Ident", value: data.NIDENT }] });
|
||||
else showMsgErr(TEXTS.NO_DATA_FOUND);
|
||||
};
|
||||
|
||||
//Отображение статей калькуляции по этапу проекта
|
||||
const showStageArts = ({ stage, stageNumb, filters = [] } = {}) => {
|
||||
setStagesDataGrid(pv => ({ ...pv, showStageArts: stage, selectedStageNumb: stageNumb, stageArtsFilters: [...filters] }));
|
||||
};
|
||||
|
||||
//Отображение договоров с соисполнителями по этапу проекта
|
||||
const showStageContracts = ({ stage, stageNumb, filters = [] } = {}) => {
|
||||
setStagesDataGrid(pv => ({ ...pv, showStageContracts: stage, selectedStageNumb: stageNumb, stageContractsFilters: [...filters] }));
|
||||
};
|
||||
|
||||
//При изменении состояния фильтра
|
||||
const handleFilterChanged = ({ filters }) => setStagesDataGrid(pv => ({ ...pv, filters, pageNumber: 1, reload: true }));
|
||||
|
||||
//При изменении состояния сортировки
|
||||
const handleOrderChanged = ({ orders }) => setStagesDataGrid(pv => ({ ...pv, orders, pageNumber: 1, reload: true }));
|
||||
|
||||
//При изменении количества отображаемых страниц
|
||||
const handlePagesCountChanged = () => setStagesDataGrid(pv => ({ ...pv, pageNumber: pv.pageNumber + 1, reload: true }));
|
||||
|
||||
//При закрытии списка договоров этапа
|
||||
const handleStageContractsClose = () => setStagesDataGrid(pv => ({ ...pv, showStageContracts: null, stageContractsFilters: [] }));
|
||||
|
||||
//При закрытии калькуляции этапа
|
||||
const handleStageArtsClose = () => setStagesDataGrid(pv => ({ ...pv, showStageArts: null, stageArtsFilters: [] }));
|
||||
|
||||
//При необходимости обновить данные
|
||||
useEffect(() => {
|
||||
loadStages();
|
||||
}, [stagesDataGrid.reload, loadStages]);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box pt={2}>
|
||||
{stagesDataGrid.dataLoaded ? (
|
||||
<P8PDataGrid
|
||||
columnsDef={stagesDataGrid.columnsDef}
|
||||
filtersInitial={filters}
|
||||
rows={stagesDataGrid.rows}
|
||||
size={P8PDATA_GRID_SIZE.SMALL}
|
||||
morePages={stagesDataGrid.morePages}
|
||||
reloading={stagesDataGrid.reload}
|
||||
expandable={true}
|
||||
orderAscMenuItemCaption={BUTTONS.ORDER_ASC}
|
||||
orderDescMenuItemCaption={BUTTONS.ORDER_DESC}
|
||||
filterMenuItemCaption={BUTTONS.FILTER}
|
||||
valueFilterCaption={INPUTS.VALUE}
|
||||
valueFromFilterCaption={INPUTS.VALUE_FROM}
|
||||
valueToFilterCaption={INPUTS.VALUE_TO}
|
||||
okFilterBtnCaption={BUTTONS.OK}
|
||||
clearFilterBtnCaption={BUTTONS.CLEAR}
|
||||
cancelFilterBtnCaption={BUTTONS.CANCEL}
|
||||
morePagesBtnCaption={BUTTONS.MORE}
|
||||
noDataFoundText={TEXTS.NO_DATA_FOUND}
|
||||
headCellRender={headCellRender}
|
||||
dataCellRender={prms => dataCellRender(prms, showStageArts)}
|
||||
rowExpandRender={prms =>
|
||||
rowExpandRender(prms, pOnlineShowDocument, showStageArts, showStageContracts, showStagePayNotes, showStageCostNotes)
|
||||
}
|
||||
valueFormatter={valueFormatter}
|
||||
onOrderChanged={handleOrderChanged}
|
||||
onFilterChanged={handleFilterChanged}
|
||||
onPagesCountChanged={handlePagesCountChanged}
|
||||
/>
|
||||
) : null}
|
||||
{stagesDataGrid.showStageContracts ? (
|
||||
<P8PFullScreenDialog
|
||||
title={`Договоры этапа "${stagesDataGrid.selectedStageNumb}" проекта "${projectName}"`}
|
||||
onClose={handleStageContractsClose}
|
||||
>
|
||||
<StageContracts stage={stagesDataGrid.showStageContracts} filters={stagesDataGrid.stageContractsFilters} />
|
||||
</P8PFullScreenDialog>
|
||||
) : null}
|
||||
{stagesDataGrid.showStageArts ? (
|
||||
<P8PFullScreenDialog
|
||||
title={`Калькуляция этапа "${stagesDataGrid.selectedStageNumb}" проекта "${projectName}"`}
|
||||
onClose={handleStageArtsClose}
|
||||
>
|
||||
<StageArts stage={stagesDataGrid.showStageArts} filters={stagesDataGrid.stageArtsFilters} />
|
||||
</P8PFullScreenDialog>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Список этапов проекта
|
||||
Stages.propTypes = {
|
||||
project: PropTypes.number.isRequired,
|
||||
projectName: PropTypes.string.isRequired,
|
||||
filters: PropTypes.arrayOf(P8PDATA_GRID_FILTER_SHAPE)
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { Stages };
|
16
app/panels/prj_jobs/index.js
Normal file
16
app/panels/prj_jobs/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Работы проектов
|
||||
Панель мониторинга: Точка входа
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { PrjJobs } from "./prj_jobs"; //Корневая панель работ проектов
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export const RootClass = PrjJobs;
|
163
app/panels/prj_jobs/prj_jobs.js
Normal file
163
app/panels/prj_jobs/prj_jobs.js
Normal file
@ -0,0 +1,163 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Работы проектов
|
||||
Панель мониторинга: Корневая панель работ проектов
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useContext, useState } from "react"; //Классы React
|
||||
import Button from "@mui/material/Button"; //Кнопка
|
||||
import Typography from "@mui/material/Typography"; //Текст
|
||||
import { NavigationCtx } from "../../context/navigation"; //Контекст навигации
|
||||
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
|
||||
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
|
||||
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Корневая панель работ проектов
|
||||
const PrjJobs = () => {
|
||||
//Собственное состояние
|
||||
let [result, setResult] = useState("");
|
||||
|
||||
//Подключение к контексту навигации
|
||||
const { navigateBack, navigateRoot, isNavigationState, getNavigationState, navigatePanelByName } = useContext(NavigationCtx);
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored } = useContext(BackEndСtx);
|
||||
|
||||
//Подключение к контексту сообщений
|
||||
const { showMsgErr, showMsgWarn, showMsgInfo } = useContext(MessagingСtx);
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowDocument, pOnlineShowDictionary, pOnlineUserProcedure, pOnlineUserReport } = useContext(ApplicationСtx);
|
||||
|
||||
//Выполнение запроса к серверу
|
||||
const makeReq = async throwError => {
|
||||
try {
|
||||
const data = await executeStored({
|
||||
throwError,
|
||||
showErrorMessage: false,
|
||||
stored: "UDO_P_P8PANELS_TEST",
|
||||
args: { NRN: 123, SCODE: "123", DDATE: new Date() },
|
||||
respArg: "COUT",
|
||||
spreadOutArguments: false
|
||||
});
|
||||
setResult(JSON.stringify(data));
|
||||
} catch (e) {
|
||||
setResult("");
|
||||
showMsgErr(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<div>
|
||||
<h1>Это панель работ!</h1>
|
||||
<br />
|
||||
<h2>Параметры: {isNavigationState() ? JSON.stringify(getNavigationState()) : "НЕ ПЕРЕДАНЫ"}</h2>
|
||||
<br />
|
||||
<Button onClick={() => navigatePanelByName("PrjFin", { someDataFromJobs: 321 })}>В панель финансов</Button>
|
||||
<br />
|
||||
<Button onClick={navigateBack}>Назад</Button>
|
||||
<br />
|
||||
<Button onClick={() => navigateRoot()}>Домой</Button>
|
||||
<br />
|
||||
<Button onClick={navigateBack}>Назад</Button>
|
||||
<br />
|
||||
<Button onClick={() => navigateRoot()}>Домой</Button>
|
||||
<br />
|
||||
<Button onClick={() => makeReq(false)}>Без Exception</Button>
|
||||
<br />
|
||||
<Button onClick={() => makeReq(true)}>С Exception</Button>
|
||||
<br />
|
||||
<Button
|
||||
onClick={() =>
|
||||
showMsgWarn(
|
||||
"Вы уверены?",
|
||||
() => showMsgInfo("Делаем"),
|
||||
() => showMsgErr("Не делаем")
|
||||
)
|
||||
}
|
||||
>
|
||||
ВОРНИНГ
|
||||
</Button>
|
||||
<br />
|
||||
<Typography variant="h4">RESULT: {result}</Typography>
|
||||
<br />
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<input id="dictionaryData" />
|
||||
<button
|
||||
onClick={() =>
|
||||
pOnlineShowDictionary({
|
||||
unitCode: "OKATO",
|
||||
inputParameters: [
|
||||
{
|
||||
name: "in_CODE",
|
||||
value: document.getElementById("dictionaryData").value
|
||||
}
|
||||
],
|
||||
callBack: res => {
|
||||
console.log(res);
|
||||
if (res.success === true) document.getElementById("dictionaryData").value = res.outParameters.out_CODE;
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
...
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
pOnlineUserProcedure({
|
||||
code: "UDO_P_AGNLIST_INSERT",
|
||||
inputParameters: [
|
||||
{
|
||||
name: "SOKATO",
|
||||
value: document.getElementById("dictionaryData").value
|
||||
}
|
||||
],
|
||||
|
||||
callBack: res => {
|
||||
console.log(res);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
Добавить
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
pOnlineUserReport({
|
||||
code: "Список событий",
|
||||
inputParameters: [
|
||||
{
|
||||
name: "DDATE_FROM",
|
||||
value: new Date()
|
||||
},
|
||||
{
|
||||
name: "SPERSON",
|
||||
value: "Иванов"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
>
|
||||
Список событий
|
||||
</button>
|
||||
<button onClick={() => pOnlineShowDocument({ unitCode: "AGNLIST", document: 28904399 })}>Раздел - КА - ФФФ</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { PrjJobs };
|
37
app/root.js
Normal file
37
app/root.js
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Корневой класс приложения (обёртка для контекста)
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import { MessagingContext } from "./context/messaging"; //Контекст сообщений
|
||||
import { BackEndContext } from "./context/backend"; //Контекст взаимодействия с сервером
|
||||
import { ApplicationContext } from "./context/application"; //Контекст приложения
|
||||
import { App } from "./app"; //Приложение
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Обёртка для контекста
|
||||
const Root = () => {
|
||||
return (
|
||||
<MessagingContext>
|
||||
<BackEndContext>
|
||||
<ApplicationContext>
|
||||
<App />
|
||||
</ApplicationContext>
|
||||
</BackEndContext>
|
||||
</MessagingContext>
|
||||
);
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default Root;
|
23
css/fonts-material-icons.css
Normal file
23
css/fonts-material-icons.css
Normal file
@ -0,0 +1,23 @@
|
||||
/* fallback */
|
||||
@font-face {
|
||||
font-family: "Material Icons";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(../fonts/material-icons.woff2) format("woff2");
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: "Material Icons";
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-feature-settings: "liga";
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
148
css/fonts-roboto.css
Normal file
148
css/fonts-roboto.css
Normal file
@ -0,0 +1,148 @@
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-cyrillic-ext-300-normal.woff2) format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-cyrillic-300-normal.woff2) format("woff2");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-latin-ext-300-normal.woff2) format("woff2");
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-latin-300-normal.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122,
|
||||
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-cyrillic-ext-400-normal.woff2) format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-cyrillic-400-normal.woff2) format("woff2");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-latin-ext-400-normal.woff2) format("woff2");
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-latin-400-normal.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122,
|
||||
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-cyrillic-ext-500-normal.woff2) format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-cyrillic-500-normal.woff2) format("woff2");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-latin-ext-500-normal.woff2) format("woff2");
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-latin-500-normal.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122,
|
||||
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-cyrillic-ext-700-normal.woff2) format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-cyrillic-700-normal.woff2) format("woff2");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-latin-ext-700-normal.woff2) format("woff2");
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(../fonts/roboto-latin-700-normal.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122,
|
||||
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
0
css/p8-panels.css
Normal file
0
css/p8-panels.css
Normal file
349
dist/p8-panels.js
vendored
Normal file
349
dist/p8-panels.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
fonts/material-icons.woff2
Normal file
BIN
fonts/material-icons.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-cyrillic-300-normal.woff2
Normal file
BIN
fonts/roboto-cyrillic-300-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-cyrillic-400-normal.woff2
Normal file
BIN
fonts/roboto-cyrillic-400-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-cyrillic-500-normal.woff2
Normal file
BIN
fonts/roboto-cyrillic-500-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-cyrillic-700-normal.woff2
Normal file
BIN
fonts/roboto-cyrillic-700-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-cyrillic-ext-300-normal.woff2
Normal file
BIN
fonts/roboto-cyrillic-ext-300-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-cyrillic-ext-400-normal.woff2
Normal file
BIN
fonts/roboto-cyrillic-ext-400-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-cyrillic-ext-500-normal.woff2
Normal file
BIN
fonts/roboto-cyrillic-ext-500-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-cyrillic-ext-700-normal.woff2
Normal file
BIN
fonts/roboto-cyrillic-ext-700-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-latin-300-normal.woff2
Normal file
BIN
fonts/roboto-latin-300-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-latin-400-normal.woff2
Normal file
BIN
fonts/roboto-latin-400-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-latin-500-normal.woff2
Normal file
BIN
fonts/roboto-latin-500-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-latin-700-normal.woff2
Normal file
BIN
fonts/roboto-latin-700-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-latin-ext-300-normal.woff2
Normal file
BIN
fonts/roboto-latin-ext-300-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-latin-ext-400-normal.woff2
Normal file
BIN
fonts/roboto-latin-ext-400-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-latin-ext-500-normal.woff2
Normal file
BIN
fonts/roboto-latin-ext-500-normal.woff2
Normal file
Binary file not shown.
BIN
fonts/roboto-latin-ext-700-normal.woff2
Normal file
BIN
fonts/roboto-latin-ext-700-normal.woff2
Normal file
Binary file not shown.
BIN
img/default_preview.png
Normal file
BIN
img/default_preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
BIN
img/prj_fin.png
Normal file
BIN
img/prj_fin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
BIN
img/prj_jobs.jpg
Normal file
BIN
img/prj_jobs.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 155 KiB |
18
index.html
Normal file
18
index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Parus 8 monitoring WEB application" />
|
||||
<meta name="author" content="CITK Parus" />
|
||||
<link rel="stylesheet" href="./css/p8-panels.css" />
|
||||
<link rel="stylesheet" href="./css/fonts-roboto.css" />
|
||||
<link rel="stylesheet" href="./css/fonts-material-icons.css" />
|
||||
<title>Парус 8 - Панели мониторинга</title>
|
||||
</head>
|
||||
<body style="display: block; margin: 0px">
|
||||
<div id="app-content"></div>
|
||||
<script src="dist/p8-panels.js"></script>
|
||||
</body>
|
||||
</html>
|
4595
package-lock.json
generated
Normal file
4595
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
Normal file
40
package.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "parus_8_panels_plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "Monitoring panels plugin for \"PARUS 8 Online\" web application",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "set NODE_ENV=production&webpack",
|
||||
"dev": "set NODE_ENV=development&webpack",
|
||||
"lint": "eslint app"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/CITKParus/P8-Panels.git"
|
||||
},
|
||||
"author": "CITK Parus",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/CITKParus/P8-Panels/issues"
|
||||
},
|
||||
"homepage": "https://github.com/CITKParus/P8-Panels#readme",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
"@babel/preset-react": "^7.22.5",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/material": "^5.14.4",
|
||||
"babel-loader": "^9.1.3",
|
||||
"dayjs": "^1.11.9",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-plugin-react": "^7.33.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"fast-xml-parser": "^4.2.7",
|
||||
"query-string": "^8.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.15.0",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.4"
|
||||
}
|
||||
}
|
46
webpack.config.js
Normal file
46
webpack.config.js
Normal file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Настройки упаковщика
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
const webpack = require("webpack");
|
||||
const path = require("path");
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
let mode = "development";
|
||||
if (process.env.NODE_ENV == "production") mode = "production";
|
||||
|
||||
module.exports = {
|
||||
mode,
|
||||
entry: "./app/index.js",
|
||||
watch: mode == "development",
|
||||
watchOptions: {
|
||||
aggregateTimeout: 20
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
publicPath: "/dist/",
|
||||
filename: "p8-panels.js"
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
presets: ["@babel/preset-react"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user