forked from CITKParus/P8-Panels
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