Compare commits

..

120 Commits

Author SHA1 Message Date
Mikhail Chechnev
a20de79a40 ЦИТК-979 - Установка атрибутов сущности (клиент) 2025-08-14 14:21:23 +03:00
Mikhail Chechnev
fa00940776 ЦИТК-979 - Установка атрибутов сущности (сервер) 2025-08-14 14:17:25 +03:00
Mikhail Chechnev
4115961742 ЦИТК-979 - Контроль типов данных при организации связи, дополнительные события диаграммы (нажатие на атрибут, нажатие на панель), подсветка сущности при нажатии на атрибут, подсветка некорректных приёмников связи, функции получения атрибутики состояния атрибута сущности 2025-08-14 14:13:50 +03:00
Mikhail Chechnev
f6b29d5339 WEBAPP: в utils.js функция конвертации JSON в XML вынесена в отдельную object2XML (ранее была в составе object2Base64XML) 2025-08-14 14:07:12 +03:00
Mikhail Chechnev
b3e7d7bf7b ЦИТК-979 - Косметика в списке запросов "QueriesList" (сделан более компактным) 2025-08-14 13:56:35 +03:00
Mikhail Chechnev
f9b2f1ae34 ЦИТК-979 - Компонент для настройки свойств запроса, настройка атрибутов сущностей (начало, клиент) 2025-08-08 19:16:30 +03:00
Mikhail Chechnev
836a3db5e2 ЦИТК-979 - Формирование списка атрибутов сущности (сервер) 2025-08-08 19:09:59 +03:00
Mikhail Chechnev
6ff99591e2 WEBAPP: Свежая сборка 2025-07-23 01:28:35 +03:00
Mikhail Chechnev
c990cb2246 ЦИТК-979 - Базовый серверный API, таблица, клиент - начало (менеджер запросов, базовая диаграмма, IUD сущностей и связей) 2025-07-23 01:28:18 +03:00
Mikhail Chechnev
2830d8c5d9 WEBAPP: Добавление библиотеки ReactFlow 2025-07-23 01:26:58 +03:00
Mikhail Chechnev
87ca29f593 WEBAPP: Расширен набор типовых текстов приложения 2025-07-23 01:24:39 +03:00
Mikhail Chechnev
aad6bb2662 WEBAPP: Контейнер редактора - сокрытие панели сохранения при отсутствии обработчика 2025-07-23 01:24:15 +03:00
Mikhail Chechnev
afbb33c90f WEBAPP: Панель инструментов редактора - возможность блокировки кнопок 2025-07-23 01:23:35 +03:00
Mikhail Chechnev
759fc763e2 WEBAPP: Рефакторинг "Редактора панелей" - разбит на типовые компоненты, которые вынесены в ядро приложения, за пределы панели 2025-07-22 13:24:35 +03:00
Mikhail Chechnev
d792187ff9 Редактор панелей: применение P8PDialog, переименование серверного пакета 2025-07-21 11:52:08 +03:00
Mikhail Chechnev
a3fd089452 WEBAPP: "Редактор настроек РО" - перевод диалогов на типовые компоненты P8PDialog и P8PInput 2025-07-21 10:13:32 +03:00
Mikhail Chechnev
3f539065ba WEBAPP: Новые компоненты P8PDialog и P8PInput 2025-07-21 10:09:27 +03:00
Mim
7c515f7ebb ЦИТК-968 - "Производственная программа" - выбор документов плана
Reviewed-on: CITKParus/P8-Panels#36
2025-07-08 15:05:31 +03:00
Mikhail Chechnev
1a71cbdf1b WEBAPP: Свежая сборка 2025-07-07 10:28:07 +03:00
Mikhail Chechnev
077582bd4c ЦИТК-899 - рефакторинг панели, управление сортировкой строк/граф 2025-07-07 10:27:31 +03:00
Mikhail Chechnev
6958cfd904 БД: Обработка пустого respArg, полученного от пользовательской процедуры (возвращаем "<XDATA/>" в этом случае) - так парсер на стороне сервера приложений может корректно сформировать типовой ответ. 2025-07-07 09:09:24 +03:00
Mikhail Chechnev
f36d46525e WEBAPP: ApplicationСtx.setAppBarTitle обёрнута в useCallback, для безопасного использования в эффектах 2025-07-07 09:04:50 +03:00
Mikhail Chechnev
97072a9b60 WEBAPP: P8PDataGrid, P8PTable - поддержана пользовательское управление стилем корневого элемента Table 2025-07-07 09:01:55 +03:00
Dollerino
47d6b0cdb1 ЦИТК-968 - Доработка панели "Производственная программа" 2025-06-24 13:55:27 +03:00
Mim
11f29bcf0c ЦИТК-899 - Добавление возможности сортировки строк/граф раздела
Reviewed-on: CITKParus/P8-Panels#35
2025-06-23 18:53:14 +03:00
Dollerino
5c7a3b16b2 ЦИТК-899 - Добавление возможности сортировки строк/граф раздела 2025-06-23 13:52:03 +03:00
Mikhail Chechnev
2672bcd8be WEBAPP: Свежая сборка 2025-06-17 13:17:37 +03:00
Mim
c6688bd451 ЦИТК-957, ЦИТК-953, ЦИТК-939 - Исправление панели "Производственная программа"
Reviewed-on: CITKParus/P8-Panels#34
2025-06-17 13:09:30 +03:00
Mikhail Chechnev
fa71c76a7d WEBAPP: Свежая сборка 2025-06-11 22:07:39 +03:00
Mikhail Chechnev
418e77bf74 WEBAPP: Редактор панелей - применён P8PIndicator 2025-06-11 22:07:16 +03:00
Mikhail Chechnev
e4683cf991 WEBAPP+Документация: Примеры и документация P8PIndicator 2025-06-11 22:05:09 +03:00
Mikhail Chechnev
416eae7d88 WEBAPP+БД: Новый компонент - P8PIndicator 2025-06-11 22:01:54 +03:00
Mikhail Chechnev
fbbbd7c247 WEBAPP: P8PTable доработан для использования типового P8PHintDialog 2025-06-11 21:52:32 +03:00
Mikhail Chechnev
f4c665a74b WEBAPP: Типовые состояния и цвета для состояний в app.*.js, типовой диалог подсказки P8PHintDialog 2025-06-11 21:51:41 +03:00
Mikhail Chechnev
a639c6371c WEBAPP: Свежая сборка 2025-05-20 13:57:28 +03:00
Mikhail Chechnev
4f2a1d4034 WEBAPP: Редактор панелей - установка заголовка панели при переключении режима редактирования 2025-05-20 13:57:03 +03:00
Mikhail Chechnev
5a08fdf605 WEBAPP: Возможность установки произвольного заголовка приложения из панели 2025-05-20 13:56:28 +03:00
Mikhail Chechnev
be351f7920 WEBAPP: Свежая сборка 2025-05-20 11:56:55 +03:00
Mikhail Chechnev
4d59203604 WEBAPP: Редактор панелей (PoC) 2025-05-20 11:56:29 +03:00
Mikhail Chechnev
c734b62ba0 WEBAPP: добавлены библиотеки - RGL, css-loader и style-loader 2025-05-20 11:55:32 +03:00
Mikhail Chechnev
939efc0733 WEBAPP: Системные доработки (глубокое копирование structuredClone, тексты типовых кнопок, экспорт сообщений об ошибках из client) 2025-05-20 11:54:13 +03:00
Mikhail Chechnev
b2888efd62 Документация: описание "signal" в "executeStored" 2025-05-05 23:23:46 +03:00
Mikhail Chechnev
b1b1288e60 WEBAPP: Свежая сборка 2025-05-04 18:00:17 +03:00
Mikhail Chechnev
50e3970c93 WEBAPP: Поддержка AbortController для executeStored 2025-05-04 17:58:07 +03:00
Mikhail Chechnev
f418951695 Документация: Описание P8PDataGrid.style и исправления опечаток 2025-04-22 23:01:11 +03:00
Mikhail Chechnev
fe02011a25 WEBAPP: Свежая сборка 2025-04-22 22:58:05 +03:00
Mikhail Chechnev
6ebbd0f08f WEBAPP: P8PDataGrid - возможность управления стилем корневого контейнера 2025-04-22 22:56:54 +03:00
Dollerino
a81797f5ac ЦИТК-957, ЦИТК-953, ЦИТК-939 - Исправление панели "Производственная программа" 2025-04-02 15:58:46 +03:00
Mikhail Chechnev
1cd0177454 WEBAPP: Свежая сборка 2025-03-31 23:01:40 +03:00
Mikhail Chechnev
5cf9b5db85 ЦИТК-929 - пример диалога ошибки с кнопкой подробностей 2025-03-31 23:01:20 +03:00
Mikhail Chechnev
1954b27a27 WEBAPP: Свежая сборка 2025-03-31 22:53:04 +03:00
Mikhail Chechnev
2ce1fc8db2 ЦИТК-929 - Актуализация документации, косметика 2025-03-31 22:52:36 +03:00
Mim
9f99c99643 ЦИТК-929 - Вывод сообщения об ошибке без стэка вызова 2025-03-31 22:35:57 +03:00
Dollerino
6a41686a84 ЦИТК-929 - Вывод сообщения об ошибке без стэка вызова 2025-03-31 19:04:11 +03:00
Dollerino
a62daa4407 Update README.md 2025-03-28 13:57:36 +03:00
Dollerino
ef397b1818 ЦИТК-929 - Вывод сообщения об ошибке без стэка вызова 2025-03-28 13:41:02 +03:00
Mikhail Chechnev
1e580f806d WEBAPP: P8PGantt - возможность управления свойствами диалога редактора задачи 2025-03-21 18:25:18 +03:00
Mikhail Chechnev
6c935623ec WEBAPP: Свежая сборка 2025-03-20 11:05:54 +03:00
Mim
efc787d3a5 ЦИТК-945 - Доработка по замечаниям панели "Выдача сменного задания на участок" 2025-03-20 11:03:12 +03:00
Dollerino
c8790f85d9 ЦИТК-945 - Доработка панели "Выдача сменного задания на участок" 2025-03-19 15:51:10 +03:00
Mikhail Chechnev
ff4a67f375 Из файла конфигурации удалена лишняя табуляция 2025-03-19 15:02:29 +03:00
Mikhail Chechnev
e70c6b8e84 WEB APP: Свежая сборка 2025-03-19 14:48:24 +03:00
Mim
d06f3a2db1 ЦИТК-945 - Добавлена панель "Выдача сменного задания на участок" 2025-03-19 14:19:28 +03:00
Mikhail Chechnev
624b1bd7be WEB APP: P8PDataGrid - удалён некорректный стиль таблицы 2025-03-19 14:17:20 +03:00
Mikhail Chechnev
bab08ba1d3 WEBAPP: Свежая сборка 2025-03-05 14:19:21 +03:00
Mikhail Chechnev
fcc254178f WEBAPP: Добавлена библиотека "react-beautiful-dnd" для поддержки "перетаскивания" 2025-03-05 14:18:59 +03:00
Mikhail Chechnev
3eba0a52f0 WEBAPP: Новая панель - "Информация о проектах" 2025-03-05 14:17:25 +03:00
Mikhail Chechnev
72aa5bc89c Косметика в документации 2025-02-26 00:32:44 +03:00
Mikhail Chechnev
dca71f5383 Актуализация документации: описано применение http.sslVerify=false при "Server certificate verification failed" во время установки 2025-02-26 00:30:03 +03:00
Mikhail Chechnev
bdc032fa67 WEBAPP: Свежая сборка 2025-02-14 14:11:07 +03:00
Mikhail Chechnev
6d10f6258b WEBAPP: P8PDataGrid - необязательность для columnsDef, rows, значение по умолчанию для groups (так можно упрощать state в верхнеуровневых компонентах) 2025-02-14 14:09:36 +03:00
Mikhail Chechnev
852abd5482 WEBAPP: P8PChart - обновление onClick-функции, значения по умолчанию для options, labels, datasets, необязательность labels 2025-02-14 13:48:36 +03:00
Mikhail Chechnev
27bd43afb5 WEBAPP: подчистка лишних комментов 2025-02-14 13:30:35 +03:00
Mikhail Chechnev
83b0c75a3b БД: Серверный API - процедура подготовки поисковой строки от клиента для использования в SQL-запросе 2025-02-14 13:28:40 +03:00
Mikhail Chechnev
ba8b7a5ce0 БД: Панель "Работы проектов" - каскадная очистка буфера при удалении проекта 2025-02-14 13:27:23 +03:00
Mikhail Chechnev
cca42f0b38 WEBAPP: Свежая сборка 2025-02-07 14:02:05 +03:00
Mikhail Chechnev
9469b01295 Актуализация документации: P8PDataGrid (обязательность свойств и значения по умолчанию) 2025-02-07 14:01:23 +03:00
Mikhail Chechnev
40242dedd4 WEBAPP: P8PDataGrid - инициализация списка открытых групп при монтировании, необязательность "reloading" 2025-02-07 14:00:03 +03:00
Mikhail Chechnev
aca423f16e WEBAPP: Выдача карточки панели в главном меню с превью по умолчанию, в случае отсутствия такового в настройках 2025-02-07 13:51:26 +03:00
Dollerino
b3cfa176eb ЦИТК-945 - Добавлена панель "Выдача сменного задания на участок" 2025-01-27 18:30:15 +03:00
49c28750f6 Merge pull request 'main' (#7) from CITKParus/P8-Panels:main into main
Reviewed-on: Dollerok/P8-Panels#7
2025-01-27 15:26:38 +03:00
Mikhail Chechnev
ccad22f6a7 WEBAPP: Свежая сборка 2025-01-17 17:18:18 +03:00
Mikhail Chechnev
252ef9e263 Панель "Экономика проектов" - поддержан переход в разделы "Проекты" и "Договоры" в режиме "закладка" 2025-01-17 17:18:00 +03:00
Mikhail Chechnev
7683f1ad76 WEBAPP: Поддержка открытия раздела/документа на закладке 2025-01-17 14:42:06 +03:00
Mikhail Chechnev
5261253d36 WEBAPP: Свежая сборка 2025-01-16 16:04:19 +03:00
Mikhail Chechnev
b2cc839cba WEBAPP: Панель "Экономика проектов" - фиксированные заголовки таблиц и общесистемные стили скролов 2025-01-16 16:04:00 +03:00
Mikhail Chechnev
b06ae0d242 WEBAPP: Общесистемные стили скролов в примере "Таблица данных" 2025-01-16 15:44:41 +03:00
Mikhail Chechnev
dac9bc4ea2 WEBAPP: Общесистемные стили полос прокрутки в "выдвижных панелях" для панелей ПУПД 2025-01-16 15:41:12 +03:00
Mikhail Chechnev
efed4b8ea2 WEBAPP: стили скролов для теля панели и главного бокового меню панелей 2025-01-16 15:38:10 +03:00
Mikhail Chechnev
a8a76766f4 WEB APP: P8PFullScreenDialog - возможность передачи свойств контейнеру содержимого диалога (contentProps) 2025-01-16 15:36:45 +03:00
Mikhail Chechnev
6fa9d97da4 Исправление неверной ссылки на тип TDG_FILTERS в инструкции 2025-01-15 19:20:33 +03:00
Mim
014d3f2e9e ЦИТК-800 - Программный интерфейс P8PDataGrid и P8PGantt приведены к общесистемному 2025-01-15 16:38:16 +03:00
Dollerino
b617a27c48 ЦИТК-800 - Изменение программного интерфейса P8PDataGrid. P8PDataGrid и P8PGantt приведены к единому формату данных с сервера 2024-12-03 15:26:51 +03:00
686d8ee5aa Merge pull request 'main' (#6) from CITKParus/P8-Panels:main into main
Reviewed-on: Dollerok/P8-Panels#6
2024-12-03 14:33:27 +03:00
Mikhail Chechnev
252dd4174c WEB APP: Свежая сборка 2024-12-02 16:24:27 +03:00
Mim
e5215d29b5 ЦИТК-840 - Панель "Выполнение работ" - корректировка параметра метода вызова "Графики ТО и Ремонтов" 2024-12-02 16:22:23 +03:00
Mikhail Chechnev
0d9505834c WEB APP: Свежая сборка 2024-11-29 20:07:11 +03:00
Mim
3fa1c4b048 ЦИТК-930 - Панель "Мониторинг изделий", доработка по замечаниям Ловыгина 2024-11-29 20:04:20 +03:00
3b763ec0c4 Merge branch 'EqsPrfrm' of https://git.citpb.ru/davay-popozhe/P8-Panels into EqsPrfrm 2024-11-29 20:00:05 +03:00
69c3b101a6 ЦИТК-840 2024-11-29 19:57:18 +03:00
Dollerino
a45479bdca Merge branch 'main' of https://git.citpb.ru/Dollerok/P8-Panels 2024-11-28 18:19:10 +03:00
Dollerino
c84b96f21e ЦИТК-930 - Доработана панель "Мониторинг сборки изделий" 2024-11-28 18:18:44 +03:00
af9f9815c5 Merge pull request 'main' (#5) from CITKParus/P8-Panels:main into main
Reviewed-on: Dollerok/P8-Panels#5
2024-11-28 18:09:16 +03:00
Mikhail Chechnev
4b52cea683 WEB APP: Свежая сборка 2024-11-27 18:46:57 +03:00
Mim
147114a4ae ЦИТК-930 - Панель "Мониторинг сборки изделий" - реализована в "общем виде", удалены "авиационные термины" 2024-11-27 18:40:34 +03:00
Dollerino
49309e5c0e ЦИТК-930 - Доработка панели "Мониторинг сборки изделий" 2024-11-25 16:06:24 +03:00
2d1a07d071 Merge pull request 'main' (#4) from CITKParus/P8-Panels:main into main
Reviewed-on: Dollerok/P8-Panels#4
2024-11-25 15:38:25 +03:00
Mim
68a3e8b2be ЦИТК-928 - панель "Производственная программа" - исправление ошибки расчёта прогресса задачи (>100) 2024-11-21 15:18:00 +03:00
078550ad06 ЦИТК-928 2024-11-21 15:04:16 +03:00
1d6007c4b7 ЦИТК-928 2024-11-21 14:52:36 +03:00
Mikhail Chechnev
607e5d9ddc WEB APP: Свежая сборка 2024-11-20 16:05:44 +03:00
Mikhail Chechnev
2bfce51fed ЦИТК-904 - Code-review "P8PCyclogram" 2024-11-20 16:05:26 +03:00
Mim
db66f0c96b ЦИТК-904 - Компонент "P8PCyclogram" 2024-11-20 14:03:46 +03:00
Mikhail Chechnev
a8c9bf2fec WEB APP: Свежая сборка 2024-11-18 19:32:04 +03:00
Mikhail Chechnev
47532b9c19 WEB APP: Выравнивание кнопок на рабочем столе, масштаб "Год" для P8PGantt, функция форматирования даты со временем в формате РФ 2024-11-18 19:31:46 +03:00
Mikhail Chechnev
9f0c2aa946 Актуализация документации: особенности установки на разные релизы Парус-Онлайн 2024-11-08 19:48:28 +03:00
Dollerino
cf76c80519 ЦИТК-904 - Добавлен компонент "Циклограмма" 2024-10-23 16:30:05 +03:00
86b1dcd717 Merge pull request 'main' (#3) from CITKParus/P8-Panels:main into main
Reviewed-on: Dollerok/P8-Panels#3
2024-10-21 15:25:36 +03:00
3a02384346 Merge pull request 'main' (#6) from CITKParus/P8-Panels:main into EqsPrfrm
Reviewed-on: #6
2024-10-16 16:24:51 +03:00
0c8222ea7e Merge pull request 'main' (#2) from main into EqsPrfrm
Reviewed-on: #2
2024-09-20 16:11:31 +03:00
192 changed files with 31661 additions and 16170 deletions

1019
README.md

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -12,13 +12,20 @@ export const TITLES = {
INFO: "Информация", //Информационный блок
WARN: "Предупреждение", //Блок предупреждения
ERR: "Ошибка", //Информация об ошибке
DEFAULT_PANELS_GROUP: "Без привязки к группе" //Заголовок группы панелей по умолчанию
DEFAULT_PANELS_GROUP: "Без привязки к группе", //Заголовок группы панелей по умолчанию
DATA_SOURCE_CONFIG: "Настройка источника данных", //Заголовок для настройки источника данных
INSERT: "Добавление", //Заголовок для диалогов/форм добавления
UPDATE: "Исправление" //Заголовок для диалогов/форм исправления
};
//Текст
export const TEXTS = {
LOADING: "Ожидайте...", //Ожидание завершения процесса
NO_DATA_FOUND: "Данных не найдено" //Отсутствие данных
NO_DATA_FOUND: "Данных не найдено", //Отсутствие данных
NO_DATA_FOUND_SHORT: "Н.Д.", //Отсутствие данных (кратко)
NO_SETTINGS: "Настройки не определены", //Отстутсвие настроек
UNKNOWN_SOURCE_TYPE: "Неизвестный тип источника", //Отсуствие типа источника
UNNAMED_SOURCE: "Источник без наименования" //Отсутствие наименования источника
};
//Текст кнопок
@ -29,11 +36,19 @@ export const BUTTONS = {
OK: "ОК", //Ок
CANCEL: "Отмена", //Отмена
CLOSE: "Закрыть", //Сокрытие
DETAIL: "Подробнее", //Отображение подробностей/детализации
HIDE: "Скрыть", //Скрытие информации
CLEAR: "Очистить", //Очистка
ORDER_ASC: "По возрастанию", //Сортировка по возрастанию
ORDER_DESC: "По убыванию", //Сортировка по убыванию
FILTER: "Фильтр", //Фильтрация
MORE: "Ещё" //Догрузка данных
MORE: "Ещё", //Догрузка данных
APPLY: "Применить", //Сохранение без закрытия интерфейса ввода
SAVE: "Сохранить", //Сохранение
CONFIG: "Настроить", //Настройка
INSERT: "Добавить", //Добавление
UPDATE: "Исправить", //Исправление
DELETE: "Удалить" //Удаление
};
//Метки атрибутов, сопроводительные надписи
@ -46,17 +61,30 @@ export const CAPTIONS = {
START: "Начало",
END: "Окончание",
PROGRESS: "Прогресс",
LEGEND: "Легенда"
LEGEND: "Легенда",
USER_PROC: "Пользовательская процедура",
QUERY: "Запрос"
};
//Типовые сообщения об ошибках
export const ERRORS = {
UNDER_CONSTRUCTION: "Панель в разработке",
P8O_API_UNAVAILABLE: '"ПАРУС 8 Онлайн" недоступен',
DEFAULT: "Неожиданная ошибка"
P8O_API_UNSUPPORTED: 'Функция "ПАРУС 8 Онлайн" не поддерживается',
DEFAULT: "Неожиданная ошибка",
DATA_SOURCE_NO_REQ_ARGS: "Не заданы обязательные параметры источника данных"
};
//Типовые сообщения для ошибок HTTP
export const ERRORS_HTTP = {
404: "Адрес не найден"
};
//Типовые статусы
export const STATE = {
UNDEFINED: "UNDEFINED",
INFO: "INFORMATION",
OK: "OK",
ERR: "ERR",
WARN: "WARN"
};

View File

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

View File

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

View File

@ -0,0 +1,40 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Диалог настройки
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { P8PDialog } from "../p8p_dialog"; //Типовой диалог
//-----------
//Тело модуля
//-----------
//Диалог настройки
const P8PConfigDialog = ({ title, children, onOk, onCancel }) => {
//Формирование представления
return (
<P8PDialog title={title} onOk={onOk} onCancel={onCancel}>
{children}
</P8PDialog>
);
};
//Контроль свойств компонента - Диалог настройки
P8PConfigDialog.propTypes = {
title: PropTypes.string.isRequired,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PConfigDialog };

View File

@ -0,0 +1,113 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Источник данных
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, IconButton, Icon, Typography, Chip, Button, Card, CardContent, CardActions, CardActionArea } from "@mui/material"; //Интерфейсные элементы
import { BUTTONS, TEXTS } from "../../../app.text"; //Общие текстовые ресурсы
import { STYLES as COMMON_STYLES } from "./p8p_editors_common"; //Общие ресурсы редаторов
import { P8P_DATA_SOURCE_SHAPE, P8P_DATA_SOURCE_TYPE, P8P_DATA_SOURCE_TYPE_NAME, P8P_DATA_SOURCE_INITIAL } from "./p8p_data_source_common"; //Общие ресурсы компонента "Источник данных"
import { P8PDataSourceConfigDialog } from "./p8p_data_source_config_dialog"; //Диалог настройки источника данных
//-----------
//Тело модуля
//-----------
//Источник данных
const P8PDataSource = ({ dataSource = null, valueProviders = {}, onChange = null } = {}) => {
//Собственное состояние - отображение диалога настройки
const [configDlg, setConfigDlg] = useState(false);
//Уведомление родителя о смене настроек источника данных
const notifyChange = settings => onChange && onChange(settings);
//При нажатии на настройку источника данных
const handleSetup = () => setConfigDlg(true);
//При нажатии на настройку источника данных
const handleSetupOk = dataSource => {
setConfigDlg(false);
notifyChange(dataSource);
};
//При нажатии на настройку источника данных
const handleSetupCancel = () => setConfigDlg(false);
//При удалении настроек источника данных
const handleDelete = () => notifyChange({ ...P8P_DATA_SOURCE_INITIAL });
//Расчет флага "настроенности"
const configured = dataSource?.type ? true : false;
//Список аргументов
const args =
configured &&
dataSource.arguments.map((argument, i) => (
<Chip
key={i}
label={`:${argument.name} = ${argument.valueSource || argument.value || "NULL"}`}
variant={"outlined"}
sx={COMMON_STYLES.CHIP(true)}
/>
));
//Формирование представления
return (
<>
{configDlg && (
<P8PDataSourceConfigDialog
dataSource={dataSource}
valueProviders={valueProviders}
onOk={handleSetupOk}
onCancel={handleSetupCancel}
/>
)}
{configured && (
<Card variant={"outlined"}>
<CardActionArea onClick={handleSetup}>
<CardContent>
<Typography variant={"subtitle1"} noWrap={true}>
{dataSource.type === P8P_DATA_SOURCE_TYPE.USER_PROC ? dataSource.userProc : TEXTS.UNNAMED_SOURCE}
</Typography>
<Typography variant={"caption"} color={"text.secondary"} noWrap={true}>
{P8P_DATA_SOURCE_TYPE_NAME[dataSource.type] || TEXTS.UNKNOWN_SOURCE_TYPE}
</Typography>
<Stack direction={"column"} spacing={1} pt={2}>
{args}
</Stack>
</CardContent>
</CardActionArea>
<CardActions>
<IconButton onClick={handleDelete}>
<Icon>delete</Icon>
</IconButton>
</CardActions>
</Card>
)}
{!configured && (
<Button startIcon={<Icon>build</Icon>} onClick={handleSetup}>
{BUTTONS.CONFIG}
</Button>
)}
</>
);
};
//Контроль свойств компонента - Источник данных
P8PDataSource.propTypes = {
dataSource: P8P_DATA_SOURCE_SHAPE,
valueProviders: PropTypes.object,
onChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PDataSource };

View File

@ -0,0 +1,86 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Общие ресурсы компонента "Источник данных"
*/
//---------------------
//Подключение библиотек
//---------------------
import PropTypes from "prop-types"; //Контроль свойств компонента
import client from "../../core/client"; //Клиент БД
import { CAPTIONS } from "../../../app.text"; //Общие текстовые ресурсы
//---------
//Константы
//---------
//Типы даных аргументов
const P8P_DATA_SOURCE_ARGUMENT_DATA_TYPE = {
STR: client.SERV_DATA_TYPE_STR,
NUMB: client.SERV_DATA_TYPE_NUMB,
DATE: client.SERV_DATA_TYPE_DATE
};
//Структура аргумента источника данных
const P8P_DATA_SOURCE_ARGUMENT_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
caption: PropTypes.string.isRequired,
dataType: PropTypes.oneOf(Object.values(P8P_DATA_SOURCE_ARGUMENT_DATA_TYPE)),
req: PropTypes.bool.isRequired,
value: PropTypes.any,
valueSource: PropTypes.string
});
//Начальное состояние аргумента источника данных
const P8P_DATA_SOURCE_ARGUMENT_INITIAL = {
name: "",
caption: "",
dataType: "",
req: false,
value: "",
valueSource: ""
};
//Типы источников данных
const P8P_DATA_SOURCE_TYPE = {
USER_PROC: "USER_PROC",
QUERY: "QUERY"
};
//Типы источников данных (наименования)
const P8P_DATA_SOURCE_TYPE_NAME = {
[P8P_DATA_SOURCE_TYPE.USER_PROC]: CAPTIONS.USER_PROC,
[P8P_DATA_SOURCE_TYPE.QUERY]: CAPTIONS.QUERY
};
//Структура источника данных
const P8P_DATA_SOURCE_SHAPE = PropTypes.shape({
type: PropTypes.oneOf([...Object.values(P8P_DATA_SOURCE_TYPE), ""]),
userProc: PropTypes.string,
stored: PropTypes.string,
respArg: PropTypes.string,
arguments: PropTypes.arrayOf(P8P_DATA_SOURCE_ARGUMENT_SHAPE)
});
//Начальное состояние истоника данных
const P8P_DATA_SOURCE_INITIAL = {
type: "",
userProc: "",
stored: "",
respArg: "",
arguments: []
};
//----------------
//Интерфейс модуля
//----------------
export {
P8P_DATA_SOURCE_ARGUMENT_DATA_TYPE,
P8P_DATA_SOURCE_ARGUMENT_INITIAL,
P8P_DATA_SOURCE_SHAPE,
P8P_DATA_SOURCE_TYPE,
P8P_DATA_SOURCE_TYPE_NAME,
P8P_DATA_SOURCE_INITIAL
};

View File

@ -0,0 +1,185 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Диалог настройки источника данных
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useEffect, useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, IconButton, Icon, TextField, InputAdornment, MenuItem, Menu } from "@mui/material"; //Интерфейсные элементы
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
import { TITLES, CAPTIONS } from "../../../app.text"; //Общие текстовые ресурсы
import { P8PConfigDialog } from "./p8p_config_dialog"; //Типовой диалог настройки
import { P8P_DATA_SOURCE_TYPE, P8P_DATA_SOURCE_SHAPE, P8P_DATA_SOURCE_ARGUMENT_INITIAL, P8P_DATA_SOURCE_INITIAL } from "./p8p_data_source_common"; //Общие ресурсы компонента "Источник данных"
import { useUserProcDesc } from "./p8p_data_source_hooks"; //Хуки источников данных
//-----------
//Тело модуля
//-----------
//Диалог настройки источника данных
const P8PDataSourceConfigDialog = ({ dataSource = null, valueProviders = {}, onOk = null, onCancel = null } = {}) => {
//Собственное состояние - параметры элемента формы
const [state, setState] = useState({ ...P8P_DATA_SOURCE_INITIAL, ...dataSource });
//Собственное состояние - флаги обновление данных
const [refresh, setRefresh] = useState({ userProcDesc: 0 });
//Собственное состояние - элемент привязки меню выбора источника
const [valueProvidersMenuAnchorEl, setValueProvidersMenuAnchorEl] = useState(null);
//Описание выбранной пользовательской процедуры
const [userProcDesc] = useUserProcDesc({ code: state.userProc, refresh: refresh.userProcDesc });
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//Установка значения/привязки аргумента
const setArgumentValueSource = (index, value, valueSource) =>
setState(pv => ({
...pv,
arguments: pv.arguments.map((argument, i) => ({ ...argument, ...(i == index ? { value, valueSource } : {}) }))
}));
//Открытие/сокрытие меню выбора источника
const toggleValueProvidersMenu = target => setValueProvidersMenuAnchorEl(target instanceof Element ? target : null);
//При нажатии на очистку наименования пользовательской процедуры
const handleUserProcClearClick = () => setState({ ...P8P_DATA_SOURCE_INITIAL });
//При нажатии на выбор пользовательской процедуры в качестве источника данных
const handleUserProcSelectClick = () => {
pOnlineShowDictionary({
unitCode: "UserProcedures",
showMethod: "main",
inputParameters: [{ name: "in_CODE", value: state.userProc }],
callBack: res => {
if (res.success) {
setState(pv => ({ ...pv, type: P8P_DATA_SOURCE_TYPE.USER_PROC, userProc: res.outParameters.out_CODE }));
setRefresh(pv => ({ ...pv, userProcDesc: pv.userProcDesc + 1 }));
}
}
});
};
//При закрытии дилога с сохранением
const handleOk = () => onOk && onOk({ ...state });
//При закртии диалога отменой
const handleCancel = () => onCancel && onCancel();
//При очистке значения/связывания аргумента
const handleArgumentClearClick = index => setArgumentValueSource(index, "", "");
//При отображении меню связывания аргумента с поставщиком данных
const handleArgumentLinkMenuClick = e => setValueProvidersMenuAnchorEl(e.currentTarget);
//При выборе элемента меню связывания аргумента с поставщиком данных
const handleArgumentLinkClick = valueSource => {
setArgumentValueSource(valueProvidersMenuAnchorEl.id, "", valueSource);
toggleValueProvidersMenu();
};
//При вводе значения аргумента
const handleArgumentChange = (index, value) => setArgumentValueSource(index, value, "");
//При изменении описания пользовательской процедуры
useEffect(() => {
if (userProcDesc)
setState(pv => ({
...pv,
stored: userProcDesc?.stored?.name,
respArg: userProcDesc?.stored?.respArg,
arguments: (userProcDesc?.arguments || []).map(argument => ({ ...P8P_DATA_SOURCE_ARGUMENT_INITIAL, ...argument }))
}));
}, [userProcDesc]);
//Список значений
const values = Object.keys(valueProviders).reduce((res, key) => [...res, ...Object.keys(valueProviders[key])], []);
//Наличие значений
const isValues = values && values.length > 0 ? true : false;
//Меню привязки к поставщикам значений
const valueProvidersMenu = isValues && (
<Menu anchorEl={valueProvidersMenuAnchorEl} open={Boolean(valueProvidersMenuAnchorEl)} onClose={toggleValueProvidersMenu}>
{values.map((value, i) => (
<MenuItem key={i} onClick={() => handleArgumentLinkClick(value)}>
{value}
</MenuItem>
))}
</Menu>
);
//Формирование представления
return (
<P8PConfigDialog title={TITLES.DATA_SOURCE_CONFIG} onOk={handleOk} onCancel={handleCancel}>
<Stack direction={"column"} spacing={1}>
{valueProvidersMenu}
<TextField
type={"text"}
variant={"standard"}
value={state.userProc}
label={CAPTIONS.USER_PROC}
InputLabelProps={{ shrink: true }}
InputProps={{
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleUserProcClearClick}>
<Icon>clear</Icon>
</IconButton>
<IconButton onClick={handleUserProcSelectClick}>
<Icon>list</Icon>
</IconButton>
</InputAdornment>
)
}}
/>
{Array.isArray(state?.arguments) &&
state.arguments.map((argument, i) => (
<TextField
key={i}
type={"text"}
variant={"standard"}
value={argument.value || argument.valueSource}
label={argument.caption}
onChange={e => handleArgumentChange(i, e.target.value)}
InputLabelProps={{ shrink: true }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => handleArgumentClearClick(i)}>
<Icon>clear</Icon>
</IconButton>
{isValues && (
<IconButton id={i} onClick={handleArgumentLinkMenuClick}>
<Icon>settings_ethernet</Icon>
</IconButton>
)}
</InputAdornment>
)
}}
/>
))}
</Stack>
</P8PConfigDialog>
);
};
//Контроль свойств компонента - Диалог настройки источника данных
P8PDataSourceConfigDialog.propTypes = {
dataSource: P8P_DATA_SOURCE_SHAPE,
valueProviders: PropTypes.object,
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PDataSourceConfigDialog };

View File

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

View File

@ -0,0 +1,59 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Контейнер редактора
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, Divider, IconButton, Icon, Stack } from "@mui/material"; //Интерфейсные компоненты MUI
import { BUTTONS } from "../../../app.text"; //Общие текстовые ресурсы
//-----------
//Тело модуля
//-----------
//Контейнер редактора
const P8PEditorBox = ({ title, children, onSave }) => {
//При нажатии на "Сохранить"
const handleSaveClick = (closeEditor = false) => onSave && onSave(closeEditor);
//Флаг отображения кнопок сохранения
const showSaveBar = onSave ? true : false;
//Формирование представления
return (
<Box p={2}>
<Divider>{title}</Divider>
<Stack direction={"column"} spacing={1}>
{children}
</Stack>
{showSaveBar && (
<Stack direction={"row"} justifyContent={"right"} p={1}>
<IconButton onClick={() => handleSaveClick(false)} title={BUTTONS.APPLY}>
<Icon>done</Icon>
</IconButton>
<IconButton onClick={() => handleSaveClick(true)} title={BUTTONS.SAVE}>
<Icon>done_all</Icon>
</IconButton>
</Stack>
)}
</Box>
);
};
//Контроль свойств компонента - Контейнер редактора
P8PEditorBox.propTypes = {
title: PropTypes.string.isRequired,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
onSave: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PEditorBox };

View File

@ -0,0 +1,46 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Заголовок раздела редактора
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Divider, Chip } from "@mui/material"; //Интерфейсные компоненты MUI
//---------
//Константы
//---------
//Стили
const STYLES = {
DIVIDER: { paddingTop: "20px" }
};
//-----------
//Тело модуля
//-----------
//Заголовок раздела редактора
const P8PEditorSubHeader = ({ title }) => {
//Формирование представления
return (
<Divider sx={STYLES.DIVIDER}>
<Chip label={title} size={"small"} />
</Divider>
);
};
//Контроль свойств компонента - Заголовок раздела редактора
P8PEditorSubHeader.propTypes = {
title: PropTypes.string.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { P8PEditorSubHeader };

View File

@ -0,0 +1,53 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Панель инструментов редактора
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { IconButton, Icon, Stack } from "@mui/material"; //Интерфейсные компоненты MUI
//---------
//Константы
//---------
//Структура элемента панели инструментов редактора
const P8P_EDITOR_TOOL_BAR_ITEM_SHAPE = PropTypes.shape({
icon: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
disabled: PropTypes.bool,
onClick: PropTypes.func.isRequired
});
//-----------
//Тело модуля
//-----------
//Панель инструментов редактора
const P8PEditorToolBar = ({ items = [] }) => {
//Формирование представления
return (
<Stack direction={"row"} p={1}>
{items.map((item, i) => (
<IconButton key={i} onClick={item.onClick} title={item.title} disabled={item?.disabled === true}>
<Icon>{item.icon}</Icon>
</IconButton>
))}
</Stack>
);
};
//Контроль свойств компонента - Панель инструментов редактора
P8PEditorToolBar.propTypes = {
items: PropTypes.arrayOf(P8P_EDITOR_TOOL_BAR_ITEM_SHAPE)
};
//----------------
//Интерфейс модуля
//----------------
export { P8PEditorToolBar };

View File

@ -0,0 +1,30 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Общие ресурсы редакторов
*/
//---------
//Константы
//---------
//Стили
const STYLES = {
CHIP: (fullWidth = false, multiLine = false) => ({
...(multiLine ? { height: "auto" } : {}),
"& .MuiChip-label": {
...(multiLine
? {
display: "block",
whiteSpace: "normal"
}
: {}),
...(fullWidth ? { width: "100%" } : {})
}
})
};
//----------------
//Интерфейс модуля
//----------------
export { STYLES };

View File

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

View File

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

View File

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

View File

@ -0,0 +1,819 @@
/*
Парус 8 - Панели мониторинга
Компонент: Циклограмма
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState, useRef } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
Box,
Typography,
Dialog,
DialogActions,
DialogContent,
Button,
List,
ListItem,
ListItemText,
Link,
Divider,
IconButton,
Icon
} from "@mui/material"; //Интерфейсные компоненты
import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
import { hasValue } from "../core/utils"; //Вспомогательный функции
//---------
//Константы
//---------
//Уровни масштаба
const P8P_CYCLOGRAM_ZOOM = [0.2, 0.4, 0.7, 1, 1.5, 2, 2.5];
//Параметры элементов циклограммы
const NDEFAULT_LINE_HEIGHT = 20;
const NDEFAULT_HEADER_HEIGHT = 35;
//Высота заголовка
const TITLE_HEIGHT = "44px";
//Высота панели масштабирования
const ZOOM_HEIGHT = "56px";
//Стили
const STYLES = {
CYCLOGRAM_TITLE: { height: TITLE_HEIGHT },
CYCLOGRAM_ZOOM: { height: ZOOM_HEIGHT },
HEADER_COLUMN: {
fontSize: "12px",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "pre",
textAlign: "center",
lineHeight: "3",
padding: "0px 5px"
},
CYCLOGRAM_BOX: (noData, title, zoomBar) => ({
position: "relative",
overflow: "auto",
padding: "0px 8px",
height: `calc(100% - ${zoomBar ? ZOOM_HEIGHT : "0px"} - ${title ? TITLE_HEIGHT : "0px"})`,
display: noData ? "none" : ""
}),
GRID_ROW: index => (index % 2 === 0 ? { backgroundColor: "#ffffff" } : { backgroundColor: "#f5f5f5" }),
GROUP_HEADER_BOX: {
border: "1px solid",
backgroundColor: "#ebebeb",
display: "flex",
alignItems: "center",
justifyContent: "center"
},
GROUP_HEADER: {
fontSize: "14px",
textAlign: "center",
wordWrap: "break-word"
},
TASK_EDITOR_CONTENT: { minWidth: 400, overflowX: "auto" },
TASK_EDITOR_LIST: { width: "100%", minWidth: 300, maxWidth: 700, bgcolor: "background.paper" },
TASK_BOX: (lineHeight, bgColor, textColor, highlightColor) => ({
display: "flex",
alignItems: "center",
backgroundColor: bgColor ? bgColor : "#b4b9bf",
...(textColor ? { color: textColor } : {}),
height: lineHeight,
"&:hover": {
...(highlightColor
? { backgroundColor: `${highlightColor} !important`, filter: "brightness(1) !important" }
: { filter: "brightness(1.25) !important" }),
cursor: "pointer !important"
}
}),
TASK: lineHeight => {
const availableLines = Math.floor(lineHeight / 18);
return {
width: "100%",
fontSize: "12px",
overflowWrap: "break-word",
wordBreak: "break-all",
overflow: "hidden",
textOverflow: "ellipsis",
display: "-webkit-box",
lineHeight: "18px",
maxHeight: lineHeight,
WebkitLineClamp: availableLines < 1 ? 1 : availableLines,
WebkitBoxOrient: "vertical"
};
}
};
//Структура колонки
const P8P_CYCLOGRAM_COLUMN_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
start: PropTypes.number.isRequired,
end: PropTypes.number.isRequired
});
//Структура группы
const P8P_CYCLOGRAM_GROUP_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
height: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
visible: PropTypes.bool.isRequired
});
//Структура задачи
const P8P_CYCLOGRAM_TASK_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
rn: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
fullName: PropTypes.string.isRequired,
lineNumb: PropTypes.number.isRequired,
start: PropTypes.number.isRequired,
end: PropTypes.number.isRequired,
group: PropTypes.string,
bgColor: PropTypes.string,
textColor: PropTypes.string,
highlightColor: PropTypes.string
});
//Структура динамического атрибута задачи
const P8P_CYCLOGRAM_TASK_ATTRIBUTE_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
caption: PropTypes.string.isRequired,
visible: PropTypes.bool.isRequired
});
//--------------------------------
//Вспомогательные классы и функции
//--------------------------------
//Определение сдвига для максимальной ширины колонок
const getShift = (columns, currentColumnsMaxWidth, maxCyclogramWidth) => {
//Определяем доступное пространство для расширения
let maxWidthDiff = maxCyclogramWidth - currentColumnsMaxWidth;
//Инициализируем значение сдвига
let shift = 1;
//Если доступно больше ширины и есть пространство для расширения
if (maxCyclogramWidth > currentColumnsMaxWidth && maxCyclogramWidth - maxWidthDiff > columns.length) {
//Определяем доступный сдвиг колонок
shift = maxCyclogramWidth / currentColumnsMaxWidth;
}
//Возвращаем сдвиг
return shift;
};
//Формирование стилей для группы
const getGroupStyles = (indexGrp, highlightColor) => {
return `.main .TaskGrp${indexGrp}:hover .TaskGrp${indexGrp} {
${highlightColor ? `background: ${highlightColor};` : `filter: brightness(1.15);`}
}
.main:has(.TaskGrp${indexGrp}:hover) .TaskGrpHeader${indexGrp} {
display: block;
}
`;
//cursor: pointer;
};
//Фон строк таблицы
const P8PCyclogramRowsGrid = ({ rows, maxWidth, lineHeight }) => {
return (
<g>
{rows.map((el, index) => (
<foreignObject x="0" y={NDEFAULT_HEADER_HEIGHT + index * lineHeight} width={maxWidth} height={lineHeight} key={index}>
<Box sx={STYLES.GRID_ROW(index)} height={lineHeight} />
</foreignObject>
))}
</g>
);
};
//Контроль свойств - Фон строк таблицы
P8PCyclogramRowsGrid.propTypes = {
rows: PropTypes.array.isRequired,
maxWidth: PropTypes.number.isRequired,
lineHeight: PropTypes.number.isRequired
};
//Линии строк таблицы
const P8PCyclogramRowsLines = ({ rows, maxWidth, lineHeight }) => {
return (
<g>
{rows.map((el, index) => (
<line
x1="0"
y1={NDEFAULT_HEADER_HEIGHT + lineHeight + index * lineHeight}
x2={maxWidth}
y2={NDEFAULT_HEADER_HEIGHT + lineHeight + index * lineHeight}
key={index}
></line>
))}
</g>
);
};
//Контроль свойств - Линии строк таблицы
P8PCyclogramRowsLines.propTypes = {
rows: PropTypes.array.isRequired,
maxWidth: PropTypes.number.isRequired,
lineHeight: PropTypes.number.isRequired
};
//Линии колонок таблицы
const P8PCyclogramColumnsLines = ({ columns, shift, y1, y2 }) => {
//Инициализируем старт текущей колонки
let prevColumnEnd = 0;
return (
<g>
{columns.map((column, index) => {
//Аккумулируем окончание последней колонки с учетом сдвига
prevColumnEnd = index !== 0 ? prevColumnEnd + (columns[index - 1].end - columns[index - 1].start) * shift : 0;
return <line x1={prevColumnEnd} y1={y1} x2={prevColumnEnd} y2={y2} stroke="#e0e0e0" key={index} />;
})}
<line
x1={prevColumnEnd + (columns[columns.length - 1].end - columns[columns.length - 1].start) * shift}
y1={y1}
x2={prevColumnEnd + (columns[columns.length - 1].end - columns[columns.length - 1].start) * shift}
y2={y2}
stroke="#e0e0e0"
/>
</g>
);
};
//Контроль свойств - Линии колонок таблицы
P8PCyclogramColumnsLines.propTypes = {
columns: PropTypes.array.isRequired,
shift: PropTypes.number.isRequired,
y1: PropTypes.number.isRequired,
y2: PropTypes.number.isRequired
};
//Фон таблицы циклограммы
const P8PCyclogramGrid = ({ tasks, columns, shift, maxWidth, maxHeight, lineHeight }) => {
//Формируем массив строк исходя из максимального значения строки задачи
const rows = Array.from(Array(Math.max(...tasks.map(o => o.lineNumb)) + 1).keys());
return (
<g className="grid">
<rect x="0" y="0" width={maxWidth} height={maxHeight}></rect>
<P8PCyclogramRowsGrid rows={rows} maxWidth={maxWidth} lineHeight={lineHeight} />
<P8PCyclogramRowsLines rows={rows} maxWidth={maxWidth} lineHeight={lineHeight} />
<P8PCyclogramColumnsLines columns={columns} shift={shift} y1={NDEFAULT_HEADER_HEIGHT} y2={maxHeight} />
</g>
);
};
//Контроль свойств - Фон таблицы циклограммы
P8PCyclogramGrid.propTypes = {
tasks: PropTypes.array.isRequired,
columns: PropTypes.array.isRequired,
shift: PropTypes.number.isRequired,
maxWidth: PropTypes.number.isRequired,
maxHeight: PropTypes.number.isRequired,
lineHeight: PropTypes.number.isRequired
};
//Колонка заголовка циклограммы
const P8PCyclogramHeaderColumn = ({ column, start, shift, columnRenderer }) => {
//Рассчитываем ширину колонки
const columnWidth = column.end - column.start;
//Формируем собственное отображение, если требуется
const customView = columnRenderer ? columnRenderer({ column }) : null;
return (
<>
<foreignObject x={start} y="0" width={columnWidth * shift} height={NDEFAULT_HEADER_HEIGHT}>
{customView ? (
customView
) : (
<Typography sx={{ ...STYLES.HEADER_COLUMN, height: NDEFAULT_HEADER_HEIGHT }} title={column.name}>
{column.name}
</Typography>
)}
</foreignObject>
</>
);
};
//Контроль свойств - Колонка заголовка циклограммы
P8PCyclogramHeaderColumn.propTypes = {
column: PropTypes.object.isRequired,
start: PropTypes.number.isRequired,
shift: PropTypes.number.isRequired,
maxHeight: PropTypes.number.isRequired,
lastElement: PropTypes.bool,
columnRenderer: PropTypes.func
};
//Заголовок циклограммы
const P8PCyclogramHeader = ({ columns, shift, maxWidth, maxHeight, columnRenderer, headerBlock }) => {
//Инициализируем старт текущей колонки
let prevColumnEnd = 0;
return (
<g className="header" ref={headerBlock}>
<rect x="0" y="0" width={maxWidth} height={NDEFAULT_HEADER_HEIGHT} fill="#ffffff" stroke="#e0e0e0" strokeWidth="1.4"></rect>
{columns.map((column, index) => {
//Аккумулируем окончание последней колонки с учетом сдвига
prevColumnEnd = index !== 0 ? prevColumnEnd + (columns[index - 1].end - columns[index - 1].start) * shift : 0;
return (
<P8PCyclogramHeaderColumn
column={column}
shift={shift}
start={prevColumnEnd}
maxHeight={maxHeight}
lastElement={columns.length - 1 === index}
columnRenderer={columnRenderer}
key={index}
/>
);
})}
<g className="columnsDividers">
<P8PCyclogramColumnsLines columns={columns} shift={shift} y1={0} y2={NDEFAULT_HEADER_HEIGHT} />
</g>
</g>
);
};
//Контроль свойств - Заголовок циклограммы
P8PCyclogramHeader.propTypes = {
columns: PropTypes.array.isRequired,
shift: PropTypes.number.isRequired,
maxWidth: PropTypes.number.isRequired,
maxHeight: PropTypes.number.isRequired,
columnRenderer: PropTypes.func,
headerBlock: PropTypes.object
};
//Задача циклограммы
const P8PCyclogramTask = ({ task, indexGrp, shift, lineHeight, openTaskEditor, taskRenderer }) => {
//Рассчитываем ширину задачи
const width = task.end !== 0 ? (task.end - task.start) * shift : 0;
//Формируем собственное отображение, если требуется
const customView = taskRenderer ? taskRenderer({ task, taskHeight: lineHeight, taskWidth: width }) || {} : {};
return (
<foreignObject
x={task.start !== 0 ? task.start * shift : 0}
y={NDEFAULT_HEADER_HEIGHT + task.lineNumb * lineHeight}
width={width}
height={lineHeight}
>
<Box
className={hasValue(indexGrp) ? `TaskGrp${indexGrp}` : null}
sx={{ ...STYLES.TASK_BOX(lineHeight, task.bgColor, task.textColor, task.highlightColor), ...customView.taskStyle }}
{...customView.taskProps}
onClick={() => openTaskEditor(task)}
>
{customView.data ? (
customView.data
) : (
<Typography sx={STYLES.TASK(lineHeight)} title={task.name}>
{task.name}
</Typography>
)}
</Box>
</foreignObject>
);
};
//Контроль свойств - Группы циклограммы
P8PCyclogramTask.propTypes = {
task: PropTypes.object.isRequired,
indexGrp: PropTypes.number,
shift: PropTypes.number.isRequired,
lineHeight: PropTypes.number.isRequired,
openTaskEditor: PropTypes.func.isRequired,
taskRenderer: PropTypes.func
};
//Основная информация циклограммы
const P8PCyclogramMain = ({
columns,
groups,
tasks,
shift,
lineHeight,
maxWidth,
maxHeight,
openTaskEditor,
groupHeaderRenderer,
taskRenderer,
columnRenderer,
headerBlock
}) => {
//Инициализируем коллекцию тасков с группами
const tasksWithGroup = tasks.filter(task => hasValue(task.groupName));
//Инициализируем коллекцию тасков без групп
const tasksWithoutGroup = tasks.filter(task => !hasValue(task.groupName));
//Инициализируем коллекцию отображаемых групп
const visibleGroups = groups ? groups.filter(group => group.visible) : [];
return (
<g className="main">
<g className="tasks">
{visibleGroups.length !== 0
? visibleGroups.map((grp, indexGrp) => {
//Считываем задачи группы
let groupTasks = tasksWithGroup.filter(task => task.groupName === grp.name);
//Если по данной группе нет тасков - ничего не выводим
if (groupTasks.length === 0) {
return null;
}
return (
<g className={`TaskGrp${indexGrp}`} key={indexGrp}>
{groupTasks.map((task, index) => (
<P8PCyclogramTask
task={task}
indexGrp={indexGrp}
shift={shift}
lineHeight={lineHeight}
openTaskEditor={openTaskEditor}
taskRenderer={taskRenderer}
key={index}
/>
))}
<style>{getGroupStyles(indexGrp, grp.highlightColor)}</style>
</g>
);
})
: null}
<g className={`TasksWithoutGroups`}>
{tasksWithoutGroup.map((task, index) => {
return (
<P8PCyclogramTask
task={task}
shift={shift}
lineHeight={lineHeight}
openTaskEditor={openTaskEditor}
taskRenderer={taskRenderer}
key={index}
/>
);
})}
</g>
</g>
<P8PCyclogramHeader
columns={columns}
shift={shift}
maxWidth={maxWidth}
maxHeight={maxHeight}
columnRenderer={columnRenderer}
headerBlock={headerBlock}
/>
{visibleGroups.length !== 0 ? (
<g className="groups">
{visibleGroups.map((grp, indexGrp) => {
//Инициализируем параметры группы
let defaultView = null;
let customView = null;
let groupHeaderX = 0;
let groupHeaderY = 0;
let groupTasks = tasksWithGroup.filter(task => task.groupName === grp.name);
//Если по данной группе нет тасков - ничего не выводим
if (groupTasks.length === 0) {
return null;
}
//Если требуется отображать заголовок группы
if (grp.visible) {
//Формируем отображение по умолчанию
defaultView = (
<Box sx={{ ...STYLES.GROUP_HEADER_BOX, height: grp.height }}>
<Typography sx={{ ...STYLES.GROUP_HEADER, maxWidth: grp.width, maxHeight: grp.height }}>{grp.name}</Typography>
</Box>
);
//Формируем собственное отображение, если требуется
customView = groupHeaderRenderer ? groupHeaderRenderer({ group: grp }) : null;
//Рассчитываем координаты заголовка группы
groupHeaderX = Math.min(...groupTasks.map(o => o.start)) * shift;
groupHeaderY = NDEFAULT_HEADER_HEIGHT + Math.min(...groupTasks.map(o => o.lineNumb)) * lineHeight - grp.height - 5;
}
return (
<foreignObject
x={groupHeaderX}
y={groupHeaderY}
width={grp.width}
height={grp.height}
className={`TaskGrpHeader${indexGrp}`}
display="none"
key={indexGrp}
>
{customView ? customView : defaultView}
</foreignObject>
);
})}
</g>
) : null}
</g>
);
};
//Контроль свойств - Основная информация циклограммы
P8PCyclogramMain.propTypes = {
columns: PropTypes.array.isRequired,
groups: PropTypes.array,
tasks: PropTypes.array.isRequired,
shift: PropTypes.number.isRequired,
lineHeight: PropTypes.number.isRequired,
maxWidth: PropTypes.number.isRequired,
maxHeight: PropTypes.number.isRequired,
openTaskEditor: PropTypes.func.isRequired,
groupHeaderRenderer: PropTypes.func,
taskRenderer: PropTypes.func,
columnRenderer: PropTypes.func,
headerBlock: PropTypes.object
};
//Редактор задачи
const P8PCyclogramTaskEditor = ({
task,
taskAttributes,
onOk,
onCancel,
taskAttributeRenderer,
taskDialogRenderer,
nameCaption,
okBtnCaption,
cancelBtnCaption
}) => {
//Собственное состояние
const [state] = useState({
start: task.start,
end: task.end
});
//Отображаемые атрибуты
const dispTaskAttributes =
Array.isArray(taskAttributes) && taskAttributes.length > 0 ? taskAttributes.filter(attr => attr.visible && hasValue(task[attr.name])) : [];
//При сохранении
const handleOk = () => (onOk && state.start && state.end ? onOk() : null);
//При отмене
const handleCancel = () => (onCancel ? onCancel() : null);
//Генерация содержимого
return (
<Dialog open onClose={handleCancel}>
{taskDialogRenderer ? (
taskDialogRenderer({ task, taskAttributes, close: handleCancel })
) : (
<>
<DialogContent sx={STYLES.TASK_EDITOR_CONTENT}>
<List sx={STYLES.TASK_EDITOR_LIST}>
<ListItem alignItems="flex-start">
<ListItemText primary={nameCaption} secondary={task.fullName} />
</ListItem>
{dispTaskAttributes.length > 0 ? <Divider component="li" /> : null}
{dispTaskAttributes.length > 0
? dispTaskAttributes.map((attr, i) => {
const defaultView = task[attr.name];
const customView = taskAttributeRenderer ? taskAttributeRenderer({ task, attribute: attr }) : null;
return (
<React.Fragment key={i}>
<ListItem alignItems="flex-start">
<ListItemText
primary={attr.caption}
secondaryTypographyProps={{ component: "span" }}
secondary={customView ? customView : defaultView}
/>
</ListItem>
{i < dispTaskAttributes.length - 1 ? <Divider component="li" /> : null}
</React.Fragment>
);
})
: null}
</List>
</DialogContent>
<DialogActions>
<Button onClick={handleOk}>{okBtnCaption}</Button>
<Button onClick={handleCancel}>{cancelBtnCaption}</Button>
</DialogActions>
</>
)}
</Dialog>
);
};
//Контроль свойств - Редактор задачи
P8PCyclogramTaskEditor.propTypes = {
task: P8P_CYCLOGRAM_TASK_SHAPE,
taskAttributes: PropTypes.arrayOf(P8P_CYCLOGRAM_TASK_ATTRIBUTE_SHAPE),
onOk: PropTypes.func,
onCancel: PropTypes.func,
taskAttributeRenderer: PropTypes.func,
taskDialogRenderer: PropTypes.func,
nameCaption: PropTypes.string.isRequired,
okBtnCaption: PropTypes.string.isRequired,
cancelBtnCaption: PropTypes.string.isRequired
};
//Циклограмма
const P8PCyclogram = ({
containerStyle,
lineHeight,
title,
titleStyle,
onTitleClick,
zoomBar,
zoom,
columns,
columnRenderer,
groups,
groupHeaderRenderer,
tasks,
taskRenderer,
taskAttributes,
taskAttributeRenderer,
taskDialogRenderer,
noDataFoundText,
nameTaskEditorCaption,
okTaskEditorBtnCaption,
cancelTaskEditorBtnCaption
}) => {
//Хук основного блока (для последующего определения доступной ширины)
const mainBlock = useRef(null);
//Хук для заголовка таблицы
const headerBlock = useRef(null);
//Собственное состояние
const [state, setState] = useState({
noData: true,
loaded: false,
lineHeight: NDEFAULT_LINE_HEIGHT,
maxWidth: 0,
maxHeight: 0,
shift: 0,
zoom: P8P_CYCLOGRAM_ZOOM.includes(zoom) ? zoom : 1,
tasks: [],
editTask: null
});
//Обновление масштаба циклограммы
const handleZoomChange = direction => {
//Считываем текущий индекс
const currentIndex = P8P_CYCLOGRAM_ZOOM.indexOf(state.zoom);
setState(pv => ({
...pv,
zoom:
currentIndex + direction !== P8P_CYCLOGRAM_ZOOM.length && currentIndex + direction !== -1
? P8P_CYCLOGRAM_ZOOM[currentIndex + direction]
: pv.zoom
}));
};
//Открытие редактора задачи
const openTaskEditor = task => setState(pv => ({ ...pv, editTask: { ...task } }));
//При сохранении задачи в редакторе
const handleTaskEditorSave = () => {
setState(pv => ({ ...pv, editTask: null }));
};
//При закрытии редактора задачи без сохранения
const handleTaskEditorCancel = () => setState(pv => ({ ...pv, editTask: null }));
//При скролле блока
const handleScroll = e => {
//Изменяем позицию заголовка таблицы относительно скролла
headerBlock.current.setAttribute("transform", "translate(0," + e.currentTarget.scrollTop + ")");
};
//При изменении данных
useEffect(() => {
//Если есть колонки и задачи
if (Array.isArray(columns) && columns.length > 0 && Array.isArray(tasks) && tasks.length > 0) {
//Определяем текущую максимальную ширину колонок
let currentColumnsMaxWidth = Math.max(...columns.map(o => o.end));
//Определяем доступный сдвиг для ширины колонок (16 - паддинг по бокам)
let columnShift = getShift(columns, currentColumnsMaxWidth, mainBlock.current.offsetWidth - 16) * state.zoom;
//Устанавливаем значения исходя из колонок/задач
setState(pv => ({
...pv,
loaded: true,
lineHeight: lineHeight ? lineHeight : NDEFAULT_LINE_HEIGHT,
maxWidth: columnShift !== 0 ? currentColumnsMaxWidth * columnShift : currentColumnsMaxWidth,
maxHeight: NDEFAULT_HEADER_HEIGHT + (Math.max(...tasks.map(o => o.lineNumb)) + 1) * (lineHeight ? lineHeight : NDEFAULT_LINE_HEIGHT),
shift: columnShift,
tasks: tasks,
noData: false
}));
} else {
//Устанавливаем значения исходя из колонок/задач
setState(pv => ({
...pv,
noData: true
}));
}
}, [columns, lineHeight, state.zoom, tasks]);
//Генерация содержимого
return (
<>
<div ref={mainBlock} style={{ ...(containerStyle ? containerStyle : {}) }}>
{state.noData ? <P8PAppInlineError text={noDataFoundText} /> : null}
{state.loaded ? (
<>
{title ? (
<Typography
p={1}
sx={{ ...STYLES.CYCLOGRAM_TITLE, ...(titleStyle ? titleStyle : {}) }}
align="center"
color="textSecondary"
variant="subtitle1"
>
{onTitleClick ? (
<Link component="button" variant="body2" underline="hover" onClick={() => onTitleClick()}>
{title}
</Link>
) : (
title
)}
</Typography>
) : null}
{zoomBar ? (
<Box p={1} sx={STYLES.CYCLOGRAM_ZOOM}>
<IconButton
onClick={() => handleZoomChange(1)}
disabled={state.zoom == P8P_CYCLOGRAM_ZOOM[P8P_CYCLOGRAM_ZOOM.length - 1]}
>
<Icon>zoom_in</Icon>
</IconButton>
<IconButton onClick={() => handleZoomChange(-1)} disabled={state.zoom == P8P_CYCLOGRAM_ZOOM[0]}>
<Icon>zoom_out</Icon>
</IconButton>
</Box>
) : null}
<Box className="scroll" sx={STYLES.CYCLOGRAM_BOX(state.noData, title, zoomBar)} onScroll={handleScroll}>
<svg id="cyclogram" width={state.maxWidth} height={state.maxHeight}>
<P8PCyclogramGrid
tasks={state.tasks}
columns={columns}
shift={state.shift}
maxWidth={state.maxWidth}
maxHeight={state.maxHeight}
lineHeight={state.lineHeight}
/>
<P8PCyclogramMain
columns={columns}
groups={groups}
tasks={state.tasks}
shift={state.shift}
lineHeight={state.lineHeight}
maxWidth={state.maxWidth}
maxHeight={state.maxHeight}
groupHeaderRenderer={groupHeaderRenderer}
openTaskEditor={openTaskEditor}
taskRenderer={taskRenderer}
columnRenderer={columnRenderer}
headerBlock={headerBlock}
/>
</svg>
</Box>
</>
) : null}
{state.editTask ? (
<P8PCyclogramTaskEditor
task={state.editTask}
taskAttributes={taskAttributes}
onOk={handleTaskEditorSave}
onCancel={handleTaskEditorCancel}
taskAttributeRenderer={taskAttributeRenderer}
taskDialogRenderer={taskDialogRenderer}
nameCaption={nameTaskEditorCaption}
okBtnCaption={okTaskEditorBtnCaption}
cancelBtnCaption={cancelTaskEditorBtnCaption}
/>
) : null}
</div>
</>
);
};
//Контроль свойств - Циклограмма
P8PCyclogram.propTypes = {
containerStyle: PropTypes.object,
lineHeight: PropTypes.number,
title: PropTypes.string,
titleStyle: PropTypes.object,
onTitleClick: PropTypes.func,
zoomBar: PropTypes.bool,
zoom: PropTypes.number,
columns: PropTypes.arrayOf(P8P_CYCLOGRAM_COLUMN_SHAPE).isRequired,
columnRenderer: PropTypes.func,
groups: PropTypes.arrayOf(P8P_CYCLOGRAM_GROUP_SHAPE),
groupHeaderRenderer: PropTypes.func,
tasks: PropTypes.arrayOf(P8P_CYCLOGRAM_TASK_SHAPE).isRequired,
taskRenderer: PropTypes.func,
taskAttributes: PropTypes.arrayOf(P8P_CYCLOGRAM_TASK_ATTRIBUTE_SHAPE),
taskAttributeRenderer: PropTypes.func,
taskDialogRenderer: PropTypes.func,
noDataFoundText: PropTypes.string.isRequired,
nameTaskEditorCaption: PropTypes.string.isRequired,
okTaskEditorBtnCaption: PropTypes.string.isRequired,
cancelTaskEditorBtnCaption: PropTypes.string.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { P8PCyclogram };

View File

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

View File

@ -0,0 +1,76 @@
/*
Парус 8 - Панели мониторинга
Компонент: Диалог
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from "@mui/material"; //Интерфейсные компоненты
import { BUTTONS } from "../../app.text"; //Общие текстовые ресурсы
import { P8P_INPUT, P8PInput } from "./p8p_input"; //Поле ввода
//-----------
//Тело модуля
//-----------
//Диалог
const P8PDialog = ({ title, inputs = [], children, onOk, onCancel, onClose }) => {
//Состояние диалога
const [state, setState] = useState({});
//При изменении элемента ввода
const handleInputChange = (name, value) => setState(pv => ({ ...pv, [name]: value }));
//При нажатии на "ОК" диалога
const handleOk = () => onOk && onOk(state);
//При нажатии на "Отмена" диалога
const handleCancel = () => onCancel && onCancel();
//При нажатии на "Закрыть" диалога
const handleClose = () => (onClose ? onClose() : onCancel ? onCancel() : null);
//При подключении к старнице
useEffect(() => {
setState(inputs.reduce((res, input) => ({ ...res, [input.name]: input.value == undefined ? null : input.value }), {}));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//Формирование представления
return (
<Dialog onClose={handleClose} open>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
{inputs.map((input, i) => (
<P8PInput key={i} {...input} value={state[input.name]} formValues={state} onChange={handleInputChange} />
))}
{children}
</DialogContent>
<DialogActions>
{onOk && <Button onClick={handleOk}>{BUTTONS.OK}</Button>}
{onCancel && <Button onClick={handleCancel}>{BUTTONS.CANCEL}</Button>}
{onClose && <Button onClick={handleClose}>{BUTTONS.CLOSE}</Button>}
</DialogActions>
</Dialog>
);
};
//Контроль свойств - Диалог
P8PDialog.propTypes = {
title: PropTypes.string.isRequired,
inputs: PropTypes.arrayOf(PropTypes.shape(P8P_INPUT)),
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
onOk: PropTypes.func,
onCancel: PropTypes.func,
onClose: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PDialog };

View File

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

View File

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

View File

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

137
app/components/p8p_input.js Normal file
View File

@ -0,0 +1,137 @@
/*
Парус 8 - Панели мониторинга
Компонент: Поле ввода
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, Icon, Input, InputAdornment, FormControl, Select, InputLabel, MenuItem, IconButton, Autocomplete, TextField } from "@mui/material"; //Интерфейсные компоненты
//---------
//Константы
//---------
//Формат свойств поля ввода
const P8P_INPUT = {
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.instanceOf(Date)]),
label: PropTypes.string.isRequired,
onChange: PropTypes.func,
dictionary: PropTypes.func,
list: PropTypes.array,
type: PropTypes.string,
freeSolo: PropTypes.bool,
disabled: PropTypes.bool,
formValues: PropTypes.object
};
//-----------
//Тело модуля
//-----------
//Поле ввода
const P8PInput = ({ name, value, label, onChange, dictionary, list, type, freeSolo = false, disabled = false, formValues, ...other }) => {
//Значение элемента
const [currentValue, setCurrentValue] = useState(value);
//При получении нового значения из вне
useEffect(() => {
setCurrentValue(value);
}, [value]);
//Выбор значения из словаря
const handleDictionaryClick = () => dictionary && dictionary(formValues, res => (res ? res.map(i => handleChangeByName(i.name, i.value)) : null));
//Изменение значения элемента (по событию)
const handleChange = e => {
setCurrentValue(e.target.value);
if (onChange) onChange(e.target.name, e.target.value);
};
//Изменение значения элемента (по имени и значению)
const handleChangeByName = (targetName, value) => {
if (targetName === name) setCurrentValue(value);
if (onChange) onChange(targetName, value);
};
//Генерация содержимого
return (
<Box p={1}>
<FormControl variant={"standard"} fullWidth {...other}>
{list ? (
freeSolo ? (
<Autocomplete
id={name}
name={name}
freeSolo
disabled={disabled}
inputValue={currentValue ? currentValue : ""}
onChange={(event, newValue) => handleChangeByName(name, newValue)}
onInputChange={(event, newInputValue) => handleChangeByName(name, newInputValue)}
options={list}
renderInput={params => <TextField {...params} label={label} name={name} variant={"standard"} />}
/>
) : (
<>
<InputLabel id={`${name}Lable`} shrink>
{label}
</InputLabel>
<Select
labelId={`${name}Lable`}
id={name}
name={name}
label={label}
value={[undefined, null].includes(currentValue) ? "" : currentValue}
onChange={handleChange}
disabled={disabled}
displayEmpty
>
{list.map((item, i) => (
<MenuItem key={i} value={[undefined, null].includes(item.value) ? "" : item.value}>
{item.name}
</MenuItem>
))}
</Select>
</>
)
) : (
<>
<InputLabel {...(type == "date" ? { shrink: true } : {})} htmlFor={name}>
{label}
</InputLabel>
<Input
id={name}
name={name}
value={currentValue ? currentValue : ""}
endAdornment={
dictionary ? (
<InputAdornment position="end">
<IconButton aria-label={`${name} select`} onClick={handleDictionaryClick} edge="end">
<Icon>list</Icon>
</IconButton>
</InputAdornment>
) : null
}
{...(type ? { type } : {})}
onChange={handleChange}
disabled={disabled}
/>
</>
)}
</FormControl>
</Box>
);
};
//Контроль свойств - Поле ввода
P8PInput.propTypes = P8P_INPUT;
//----------------
//Интерфейс модуля
//----------------
export { P8P_INPUT, P8PInput };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,43 +36,60 @@ import {
Card,
CardHeader,
CardContent,
CardActions
CardActions,
Tooltip
} from "@mui/material"; //Интерфейсные элементы
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
import { NavigationCtx } from "../../context/navigation"; //Контекст навигации
import { P8P_GANTT_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { P8PGantt, taskLegendDesc } from "../../components/p8p_gantt"; //Диаграмма Ганта
import { xml2JSON, formatDateJSONDateOnly, formatDateRF, hasValue } from "../../core/utils"; //Вспомогательные функции
import { useFilteredPlanCtlgs } from "./hooks"; //Вспомогательные хуки
import { CostRouteListsDataGrid } from "./datagrids/fcroutlst";
import { IncomFromDepsDataGrid } from "./datagrids/incomefromdeps";
import { CostRouteListsDataGrid } from "./datagrids/fcroutlst"; //Таблица "Маршрутные листы"
import { IncomFromDepsDataGrid } from "./datagrids/incomefromdeps"; //Таблица "Приходы из подразделений"
//---------
//Константы
//---------
//Склонения для документов
const DECLINATIONS = ["план", "плана", "планов"];
const PLANS_DECLINATIONS = ["план", "плана", "планов"];
const SPEC_DECLINATIONS = ["элемент", "элемента", "элементов"];
//Поля сортировки
const SORT_REP_DATE = "DREP_DATE";
const SORT_REP_DATE_TO = "DREP_DATE_TO";
//Максимальное количество элементов
const MAX_TASKS = 10000;
//Стили
const STYLES = {
PLANS_FINDER: { marginTop: "10px", marginLeft: "10px", width: "93%" },
PLANS_CHECKBOX_HAVEDOCS: { alignContent: "space-around" },
PLANS_LIST_CONTAINER: { height: "100%", display: "flex", flexDirection: "column", justifyContent: "space-between" },
PLANS_LIST_ITEM_ZERODOCS: { backgroundColor: "#ebecec" },
PLANS_LIST_ITEM_PRIMARY: { wordWrap: "break-word" },
PLANS_LIST_ITEM_SECONDARY: { wordWrap: "break-word", fontSize: "0.6rem", textTransform: "uppercase" },
PLANS_LIST_ITEM_PLAN: {
backgroundColor: "#c7e7f1",
"&:hover": { backgroundColor: `#c7e7f1`, filter: "brightness(0.92) !important" }
},
PLANS_LIST_ITEM_PLAN_FIELD: {
marginLeft: "15px"
},
PLANS_LIST_FILTER_CONTAINER: { height: "calc(100% - 55px)", overflowY: "auto" },
PLANS_LIST_BUTTONS_CONTAINER: { display: "flex", justifyContent: "space-around", paddingBottom: "10px", height: "45px" },
PLANS_LIST_BUTTON: { minWidth: "125px", height: "35px" },
PLANS_BUTTON: { position: "absolute", top: `calc(${APP_BAR_HEIGHT} + 16px)`, left: "16px" },
PLANS_DRAWER: {
width: "350px",
display: "inline-block",
flexShrink: 0,
[`& .MuiDrawer-paper`]: { width: "350px", display: "inline-block", boxSizing: "border-box" }
[`& .MuiDrawer-paper`]: { width: "350px", display: "inline-block", boxSizing: "border-box", ...APP_STYLES.SCROLL }
},
GANTT_CONTAINER: { height: `calc(100vh - ${APP_BAR_HEIGHT})`, width: "100vw", paddingTop: "24px" },
GANTT_TITLE: { paddingLeft: "250px", paddingRight: "250px" },
@ -83,7 +100,26 @@ const STYLES = {
TASK_DIALOG_ACTION_CONTAINER: { border: 1, borderColor: "text.primary", borderRadius: "5px", width: "100%" },
FILTERS: { display: "table", float: "right" },
FILTERS_DATE: { display: "table-cell", verticalAlign: "middle" },
FILTERS_LEVEL: { display: "table-cell", verticalAlign: "middle", paddingLeft: "15px" }
FILTERS_LEVEL: { display: "table-cell", verticalAlign: "middle", paddingLeft: "15px" },
FILTERS_LEVEL_CAPTION: { display: "flex", alignItems: "center" },
FILTERS_LEVEL_LIMIT_ICON: { padding: "0px 8px", color: "#9f9c9c" },
FILTERS_LIMIT_SELECT: nOutOfLimit => {
return nOutOfLimit === 1
? {
".MuiOutlinedInput-notchedOutline": {
borderColor: "#e9863c"
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: "#e9863c",
borderWidth: "0.15rem"
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: "#e9863c",
borderWidth: "0.15rem"
}
}
: {};
}
};
//------------------------------------
@ -97,11 +133,13 @@ const parseProdPlanSpXML = async xmlDoc => {
attributeValueProcessor: (name, val) =>
["numb", "title"].includes(name) ? undefined : ["start", "end"].includes(name) ? formatDateJSONDateOnly(val) : val
});
return data.XDATA;
return data.XDATA.XGANTT;
};
//Форматирование для отображения количества документов
const formatCountDocs = nCountDocs => {
const formatCountDocs = (nCountDocs, nType = 0) => {
//Склонение документов
let DECLINATIONS = nType === 0 ? PLANS_DECLINATIONS : SPEC_DECLINATIONS;
//Получаем последнюю цифру в значении
let num = (nCountDocs % 100) % 10;
//Документов
@ -114,55 +152,146 @@ const formatCountDocs = nCountDocs => {
return `${nCountDocs} ${DECLINATIONS[2]}`;
};
//Изменение информации об отмеченных планах
const updateCtlgPlanInfo = (selectedPlans, plan) => {
//Результат изменений
let res = { selectedPlans: [...selectedPlans] || [], selectedPlansElements: plan.NCOUNT_DOCS };
//Определяем наличие в отмеченных планах
let selectedIndex = res.selectedPlans.indexOf(plan.NRN);
//Если уже есть в отмеченных - удаляем, нет - добавляем
if (selectedIndex > -1) {
//Удаляем план из выбранных
res.selectedPlans.splice(selectedIndex, 1);
//Переворачиваем сумму документов
res.selectedPlansElements = res.selectedPlansElements * -1;
} else {
//Добавляем план в выбранные
res.selectedPlans.push(plan.NRN);
}
//Возвращаем результат
return res;
};
//Список каталогов планов
const PlanCtlgsList = ({ planCtlgs = [], selectedPlanCtlg, filter, setFilter, onClick } = {}) => {
const PlanCtlgsList = ({
planCtlgs = [],
selectedPlans = [],
selectedPlanCtlg,
selectedPlansElements,
filter,
setFilter,
onCtlgClick,
onCtlgPlanClick,
onCtlgPlansOk,
onCtlgPlansCancel
} = {}) => {
//Генерация содержимого
return (
<div>
<TextField
sx={STYLES.PLANS_FINDER}
name="planFilter"
label="Каталог"
value={filter.ctlgName}
variant="standard"
fullWidth
onChange={event => {
setFilter(pv => ({ ...pv, ctlgName: event.target.value }));
}}
></TextField>
<FormGroup sx={STYLES.PLANS_CHECKBOX_HAVEDOCS}>
<FormControlLabel
control={<Checkbox checked={filter.haveDocs} onChange={event => setFilter(pv => ({ ...pv, haveDocs: event.target.checked }))} />}
label="Только с планами"
labelPlacement="end"
/>
</FormGroup>
<List>
{planCtlgs.map(p => (
<ListItemButton
sx={p.NCOUNT_DOCS == 0 ? STYLES.PLANS_LIST_ITEM_ZERODOCS : null}
key={p.NRN}
selected={p.NRN === selectedPlanCtlg}
onClick={() => (onClick ? onClick(p) : null)}
>
<ListItemText
primary={<Typography sx={STYLES.PLANS_LIST_ITEM_PRIMARY}>{p.SNAME}</Typography>}
secondary={<Typography sx={STYLES.PLANS_LIST_ITEM_SECONDARY}>{formatCountDocs(p.NCOUNT_DOCS)}</Typography>}
/>
</ListItemButton>
))}
</List>
</div>
<Box sx={STYLES.PLANS_LIST_CONTAINER}>
<Box sx={STYLES.PLANS_LIST_FILTER_CONTAINER}>
<TextField
sx={STYLES.PLANS_FINDER}
name="planFilter"
label="Каталог"
value={filter.ctlgName}
variant="standard"
fullWidth
onChange={event => {
setFilter(pv => ({ ...pv, ctlgName: event.target.value }));
}}
></TextField>
<FormGroup sx={STYLES.PLANS_CHECKBOX_HAVEDOCS}>
<FormControlLabel
control={
<Checkbox checked={filter.haveDocs} onChange={event => setFilter(pv => ({ ...pv, haveDocs: event.target.checked }))} />
}
label="Только с планами"
labelPlacement="end"
/>
</FormGroup>
<List>
{planCtlgs.map(ctlg => (
<Box key={ctlg.NRN}>
<ListItemButton
sx={ctlg.NCOUNT_DOCS == 0 ? STYLES.PLANS_LIST_ITEM_ZERODOCS : null}
key={ctlg.NRN}
selected={ctlg.NRN === selectedPlanCtlg}
onClick={() => (onCtlgClick ? onCtlgClick(ctlg) : null)}
disabled={ctlg.NCOUNT_DOCS == 0}
>
<ListItemText
primary={<Typography sx={STYLES.PLANS_LIST_ITEM_PRIMARY}>{ctlg.SNAME}</Typography>}
secondary={<Typography sx={STYLES.PLANS_LIST_ITEM_SECONDARY}>{formatCountDocs(ctlg.NCOUNT_DOCS, 0)}</Typography>}
/>
</ListItemButton>
{ctlg.NRN === selectedPlanCtlg && ctlg.XCRN_PLANS.length > 1
? ctlg.XCRN_PLANS.map(plan => (
<ListItemButton
sx={plan.NCOUNT_DOCS == 0 ? STYLES.PLANS_LIST_ITEM_ZERODOCS : STYLES.PLANS_LIST_ITEM_PLAN}
key={plan.NRN}
disabled={plan.NCOUNT_DOCS == 0}
onClick={() => (onCtlgPlanClick ? onCtlgPlanClick(plan) : null)}
>
<ListItemText
sx={STYLES.PLANS_LIST_ITEM_PLAN_FIELD}
primary={<Typography sx={STYLES.PLANS_LIST_ITEM_PRIMARY}>{plan.SNAME}</Typography>}
secondary={
<Typography sx={STYLES.PLANS_LIST_ITEM_SECONDARY}>
{formatCountDocs(plan.NCOUNT_DOCS, 1)}
</Typography>
}
/>
{plan.NCOUNT_DOCS !== 0 ? <Checkbox checked={selectedPlans.includes(plan.NRN)} /> : null}
</ListItemButton>
))
: null}
</Box>
))}
</List>
</Box>
<Box sx={STYLES.PLANS_LIST_BUTTONS_CONTAINER}>
<Tooltip
title={
!selectedPlanCtlg
? "Не выбран каталог планов"
: selectedPlans.length === 0
? "Не выбраны планы каталога"
: selectedPlansElements > MAX_TASKS
? `Выбранные планы превышают максимум элементов (выбрано: ${selectedPlansElements}, максимум: ${MAX_TASKS})`
: null
}
>
<Box>
<Button
sx={STYLES.PLANS_LIST_BUTTON}
variant="contained"
disabled={selectedPlans.length === 0 || selectedPlansElements > MAX_TASKS}
onClick={onCtlgPlansOk}
>
Применить
</Button>
</Box>
</Tooltip>
<Button sx={STYLES.PLANS_LIST_BUTTON} variant="contained" onClick={onCtlgPlansCancel}>
Отмена
</Button>
</Box>
</Box>
);
};
//Контроль свойств - Список каталогов планов
PlanCtlgsList.propTypes = {
planCtlgs: PropTypes.array,
selectedPlans: PropTypes.array,
selectedPlanCtlg: PropTypes.number,
onClick: PropTypes.func,
selectedPlansElements: PropTypes.number,
onCtlgClick: PropTypes.func,
onCtlgPlanClick: PropTypes.func,
filter: PropTypes.object,
setFilter: PropTypes.func
setFilter: PropTypes.func,
onCtlgPlansOk: PropTypes.func,
onCtlgPlansCancel: PropTypes.func
};
//Генерация диалога задачи
@ -231,14 +360,19 @@ const MechRecCostProdPlans = () => {
showPlanList: false,
planCtlgs: [],
planCtlgsLoaded: false,
selectedPlans: [],
selectedPlansElements: 0,
selectedPlanCtlgSpecsLoaded: false,
selectedPlanCtlg: null,
selectedPlanCtlgMaxLevel: null,
selectedPlanCtlgLevel: null,
selectedPlanCtlgOutOfLimit: 0,
selectedPlanCtlgSort: null,
selectedPlanCtlgMenuItems: null,
selectedPlanCtlgGanttDef: {},
selectedPlanCtlgSpecs: [],
loadedCtlg: null,
loadedPlans: [],
loadedElements: 0,
gantt: {},
selectedTaskDetail: null,
selectedTaskDetailType: null,
planSpec: null
@ -253,11 +387,14 @@ const MechRecCostProdPlans = () => {
const { InlineMsgInfo } = useContext(MessagingСtx);
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//Подключение к контексту навигации
const { getNavigationSearch } = useContext(NavigationCtx);
//Подключение к контексту сообщений
const { showMsgInfo } = useContext(MessagingСtx);
//Инициализация каталогов планов
const initPlanCtlgs = useCallback(async () => {
if (!state.init) {
@ -265,77 +402,101 @@ const MechRecCostProdPlans = () => {
stored: "PKG_P8PANELS_MECHREC.FCPRODPLAN_PP_CTLG_INIT",
args: {},
respArg: "COUT",
isArray: name => name === "XFCPRODPLAN_CRNS"
isArray: name => ["XFCPRODPLAN_CRNS", "XCRN_PLANS"].includes(name)
});
setState(pv => ({ ...pv, init: true, planCtlgs: [...(data?.XFCPRODPLAN_CRNS || [])], planCtlgsLoaded: true }));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.init, executeStored]);
//Выбор каталога планов
const selectPlan = project => {
setState(pv => ({
...pv,
selectedPlanCtlg: project,
selectedPlanCtlgSpecsLoaded: false,
selectedPlanCtlgMaxLevel: null,
selectedPlanCtlgLevel: null,
selectedPlanCtlgSort: null,
selectedPlanCtlgMenuItems: null,
selectedPlanCtlgSpecs: [],
selectedPlanCtlgGanttDef: {},
showPlanList: false,
selectedTaskDetail: null,
selectedTaskDetailType: null
}));
};
//Сброс выбора каталога планов
const unselectPlan = () =>
setState(pv => ({
...pv,
selectedPlanCtlgSpecsLoaded: false,
selectedPlanCtlg: null,
selectedPlanCtlgMaxLevel: null,
selectedPlanCtlgLevel: null,
selectedPlanCtlgSort: null,
selectedPlanCtlgMenuItems: null,
selectedPlanCtlgSpecs: [],
selectedPlanCtlgGanttDef: {},
showPlanList: false,
selectedTaskDetail: null,
selectedTaskDetailType: null
}));
//Загрузка списка спецификаций каталога планов
const loadPlanCtglSpecs = useCallback(
async (level = null, sort = null) => {
const data = await executeStored({
stored: "PKG_P8PANELS_MECHREC.FCPRODPLANSP_GET",
args: { NCRN: state.selectedPlanCtlg, NLEVEL: level, SSORT_FIELD: sort, NFCPRODPLANSP: state.planSpec }
args: {
NCRN: state.selectedPlanCtlg,
CFCPRODPLANS: {
VALUE: state.selectedPlans.length > 0 ? state.selectedPlans.join(";") : null,
SDATA_TYPE: SERV_DATA_TYPE_CLOB
},
NLEVEL: level,
SSORT_FIELD: sort,
NFCPRODPLANSP: state.planSpec
}
});
let doc = await parseProdPlanSpXML(data.COUT);
setState(pv => ({
...pv,
selectedPlanCtlgMaxLevel: data.NMAX_LEVEL,
selectedPlanCtlgLevel: level || level === 0 ? level : data.NMAX_LEVEL,
selectedPlanCtlgOutOfLimit: data.NOUT_OF_LIMIT,
selectedPlanCtlgSort: sort,
selectedPlanCtlgMenuItems: state.selectedPlanCtlgMenuItems
? state.selectedPlanCtlgMenuItems
: [...Array(data.NMAX_LEVEL).keys()].map(el => el + 1),
selectedPlanCtlgMenuItems: [...Array(data.NMAX_LEVEL).keys()].map(el => el + 1),
selectedPlanCtlgSpecsLoaded: true,
selectedPlanCtlgGanttDef: doc.XGANTT_DEF ? { ...doc.XGANTT_DEF } : {},
selectedPlanCtlgSpecs: [...(doc?.XGANTT_TASKS || [])]
gantt: { ...doc, tasks: [...(doc?.tasks || [])] },
loadedCtlg: state.selectedPlanCtlg,
loadedPlans: [...state.selectedPlans],
loadedElements: state.selectedPlansElements,
showPlanList: false
}));
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[executeStored, state.ident, state.selectedPlanCtlg, state.planSpec]
[executeStored, state.selectedPlanCtlg, state.selectedPlans, state.planSpec]
);
//Обработка нажатия на элемент в списке каталогов планов
const handleProjectClick = project => {
if (state.selectedPlanCtlg != project.NRN) selectPlan(project.NRN);
else unselectPlan();
const handleCtlgClick = project => {
//Если этот каталог не был выбран
if (state.selectedPlanCtlg != project.NRN) {
//Если выбран уже загруженный - укажем информацию о том, как он загружен
if (project.NRN === state.loadedCtlg) {
setState(pv => ({
...pv,
selectedPlanCtlg: project.NRN,
selectedPlans: [...pv.loadedPlans],
selectedPlansElements: pv.loadedElements
}));
} else {
setState(pv => ({
...pv,
selectedPlanCtlg: project.NRN,
selectedPlans: project.XCRN_PLANS.length === 1 ? [project.XCRN_PLANS[0].NRN] : [],
selectedPlansElements: 0
}));
}
} else {
setState(pv => ({ ...pv, selectedPlanCtlg: null, selectedPlans: [], selectedPlansElements: 0 }));
}
};
//Обработка нажатия на элемент в списке планов каталога
const handleCtlgPlanClick = plan => {
//Считываем обновленную информацию об отмеченных планах
let newPlansInfo = updateCtlgPlanInfo(state.selectedPlans, plan);
//Обновляем список отмеченных планов
setState(pv => ({
...pv,
selectedPlans: [...newPlansInfo.selectedPlans],
selectedPlansElements: pv.selectedPlansElements + newPlansInfo.selectedPlansElements
}));
};
//Обработка нажатия "ОК" при отборе планов
const handleSelectedPlansOk = () => {
//Загружаем диаграмму
loadPlanCtglSpecs(null, SORT_REP_DATE_TO);
};
//Обработка нажатия "Отмена" при отборе планов
const handleSelectedPlansCancel = () => {
setState(pv => ({
...pv,
selectedPlanCtlg: pv.loadedCtlg,
selectedPlans: [...pv.loadedPlans] || [],
selectedPlansElements: pv.loadedElements,
showPlanList: false
}));
};
//При подключении компонента к странице
@ -348,8 +509,8 @@ const MechRecCostProdPlans = () => {
//При смене выбранного каталога плана или при явном указании позиции спецификации плана
useEffect(() => {
if (state.selectedPlanCtlg || state.planSpec) loadPlanCtglSpecs(null, SORT_REP_DATE_TO);
}, [state.selectedPlanCtlg, state.planSpec, loadPlanCtglSpecs]);
if (state.planSpec) loadPlanCtglSpecs(null, SORT_REP_DATE_TO);
}, [state.planSpec, loadPlanCtglSpecs]);
//Выбор уровня
const handleChangeSelectLevel = selectedLevel => {
@ -373,6 +534,17 @@ const MechRecCostProdPlans = () => {
setState(pv => ({ ...pv, selectedTaskDetail: taskRn, selectedTaskDetailType: taskType }));
};
//При открытии окна информации об ограничении уровня
const handleLevelLimitInfoOpen = () => {
//Отображаем информацию
showMsgInfo(
`Размер производственной программы превышает предельно допустимый для одновременного отображения в виде диаграммы Ганта.
Доступные для просмотра уровни вложенности ограничены.
Вы можете просматривать производственную программу частями, используя действие "Открытие панели Производственная программа" в спецификации "Выпуск"
раздела "Планы и отчеты производства изделий".`
);
};
//Генерация содержимого
return (
<Box>
@ -381,18 +553,18 @@ const MechRecCostProdPlans = () => {
<Fab variant="extended" sx={STYLES.PLANS_BUTTON} onClick={() => setState(pv => ({ ...pv, showPlanList: !pv.showPlanList }))}>
Каталоги планов
</Fab>
<Drawer
anchor={"left"}
open={state.showPlanList}
onClose={() => setState(pv => ({ ...pv, showPlanList: false }))}
sx={STYLES.PLANS_DRAWER}
>
<Drawer anchor={"left"} open={state.showPlanList} onClose={handleSelectedPlansCancel} sx={STYLES.PLANS_DRAWER}>
<PlanCtlgsList
planCtlgs={filteredPlanCtgls}
selectedPlans={state.selectedPlans}
selectedPlanCtlg={state.selectedPlanCtlg}
selectedPlansElements={state.selectedPlansElements}
filter={filter}
setFilter={setFilter}
onClick={handleProjectClick}
onCtlgClick={handleCtlgClick}
onCtlgPlanClick={handleCtlgPlanClick}
onCtlgPlansOk={handleSelectedPlansOk}
onCtlgPlansCancel={handleSelectedPlansCancel}
/>
</Drawer>
</>
@ -401,7 +573,7 @@ const MechRecCostProdPlans = () => {
<Grid container>
<Grid item xs={12}>
{state.selectedPlanCtlgSpecsLoaded ? (
state.selectedPlanCtlgSpecs.length === 0 ? (
state.gantt.tasks.length === 0 ? (
<Box pt={3}>
<InlineMsgInfo
okBtn={false}
@ -437,8 +609,16 @@ const MechRecCostProdPlans = () => {
</Select>
</Box>
<Box sx={STYLES.FILTERS_LEVEL}>
<InputLabel id="select-label-level">До уровня</InputLabel>
<Box sx={STYLES.FILTERS_LEVEL_CAPTION}>
<InputLabel id="select-label-level">До уровня</InputLabel>
{state.selectedPlanCtlgOutOfLimit === 1 ? (
<IconButton sx={STYLES.FILTERS_LEVEL_LIMIT_ICON} onClick={handleLevelLimitInfoOpen}>
<Icon>info</Icon>
</IconButton>
) : null}
</Box>
<Select
sx={STYLES.FILTERS_LIMIT_SELECT(state.selectedPlanCtlgOutOfLimit)}
labelId="select-label-level"
id="select-level"
value={state.selectedPlanCtlgLevel}
@ -459,22 +639,21 @@ const MechRecCostProdPlans = () => {
) : null}
<P8PGantt
{...P8P_GANTT_CONFIG_PROPS}
{...state.selectedPlanCtlgGanttDef}
{...state.gantt}
containerStyle={STYLES.GANTT_CONTAINER}
titleStyle={STYLES.GANTT_TITLE}
tasks={state.selectedPlanCtlgSpecs}
taskDialogRenderer={prms => taskDialogRenderer({ ...prms, handleTaskDetailOpen })}
/>
</Box>
)
) : !state.selectedPlanCtlg ? (
) : !state.loadedCtlg ? (
<Box pt={3}>
<InlineMsgInfo
okBtn={false}
text={
state.planSpec
? "Загружаю график для выбранной позиции плана..."
: "Укажите каталог планов для отображения их спецификаций"
: "Укажите каталог планов или планы для отображения их спецификаций"
}
/>
</Box>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,44 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Хуки компонентов
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useEffect } from "react"; //Классы React
//-----------
//Тело модуля
//-----------
//Отложенная загрузка модуля компонента (как альтернативу можно применять React.lazy)
const useComponentModule = ({ path = null, module = "view" } = {}) => {
//Собственное состояние - импортированный модуль компонента
const [componentModule, setComponentModule] = useState(null);
//Собственное состояние - флаг готовности
const [init, setInit] = useState(false);
//При подмонтировании к странице
useEffect(() => {
//Динамическая загрузка модуля компонента из библиотеки
const importComponentModule = async () => {
setInit(false);
const moduleContent = await import(`./${path}/${module}`);
setComponentModule(moduleContent);
setInit(true);
};
if (path) importComponentModule();
}, [path, module]);
//Возвращаем интерфейс хука
return [componentModule, init];
};
//----------------
//Интерфейс модуля
//----------------
export { useComponentModule };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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