WEB-приложение - инициализация

This commit is contained in:
Mikhail Chechnev 2023-09-24 22:22:48 +03:00
parent 841c7c1b94
commit c9b12981cf
62 changed files with 9612 additions and 0 deletions

19
.eslintrc.json Normal file
View 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
View File

@ -0,0 +1,5 @@
# Dependency directories
node_modules/
# VS Code
.vscode/

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"useTabs": false,
"tabWidth": 4,
"printWidth": 150,
"trailingComma": "none",
"arrowParens": "avoid"
}

20
app.config.js Normal file
View File

@ -0,0 +1,20 @@
/*
Парус 8 - Панели мониторинга
Настройки приложения
*/
//---------
//Константы
//---------
//Системеые параметры
const SYSTEM = {
//Адрес сервера приложений "ПАРУС 8 Онлайн"
SERVER: "../../DicAcc/"
};
//----------------
//Интерфейс модуля
//----------------
export { SYSTEM };

57
app.text.js Normal file
View 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
View 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 };

View 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 };

View 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
};

View 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 };

View 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 };

View 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 };

View 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 };

View 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
View 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"
/>
&nbsp;
<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>:&nbsp;
{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
View 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])
};

View 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
View 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
View 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])
};

View 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
View 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
View 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
View 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
View 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
View 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
View File

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

View File

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

View 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 };

View 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 };

View 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 };

View 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}:&nbsp;
</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 };

View 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}:&nbsp;
</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 };

View File

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

View 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
View 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;

View 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
View 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
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
img/default_preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
img/prj_fin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
img/prj_jobs.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

18
index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

40
package.json Normal file
View 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
View 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"]
}
}
}
]
}
};