Compare commits

...

111 Commits
main ... main

Author SHA1 Message Date
Mikhail Chechnev
9c78dd51c5 WEBAPP: Свежая сборка 2025-10-23 17:40:30 +03:00
Mikhail Chechnev
2ad4cfdba9 Панель "Доски задач" - добавление перенесено в заголовок, исправлены ошибки работы заливки событий по условиям 2025-10-23 17:39:50 +03:00
Mikhail Chechnev
a7a883ecc6 Адаптация пакета панелей ПУДП (PKG_P8PANELS_MECHREC) под PG 2025-10-22 19:48:37 +03:00
Mikhail Chechnev
18dac12396 ЦИТК-979 - Редактор запросов - переименовал процедуру формирования SQL-выражения 2025-10-15 15:18:17 +03:00
Mikhail Chechnev
d446fd96ab ЦИТК-979 - Редактор условия запроса - раздельное хранение канонического SQL-выражения и выражения с подстановками 2025-10-14 19:21:18 +03:00
Mikhail Chechnev
6efbb8508c ЦИТК-979 - Редактор условия запроса - реализовано добавление элемента условия по месту курсора/выделения 2025-10-14 14:45:53 +03:00
Mikhail Chechnev
3df01a36c6 ЦИТК-979 - Редактор запросов - исправлена ошибка сброса состояния диалога редактирования аргумента запроса при возникновении серверной ошибки 2025-10-14 14:13:03 +03:00
Mikhail Chechnev
1530bfa3bf ЦИТК-979 - Редактор запросов - Поддержка подстановки отладочных значений аргументов в SQL-выражение 2025-10-14 00:09:52 +03:00
Mikhail Chechnev
64145d7ac0 ЦИТК-979 - Редактор запросов - Контроль имени аргументов: только латинские буквы, цифры и подчеркивание, начало только с буквы, принудительный APPERCASE, контроль уникальности 2025-10-13 17:03:32 +03:00
Mikhail Chechnev
63e3d3833e ЦИТК-979 - Редактор запросов - контроль соответствия условий отбора запроса набору его аргументов, инкапсуляция обновления SQL-выражения запроса в базовом пакете 2025-10-13 16:27:51 +03:00
Mikhail Chechnev
db5bf1f72c WEBAPP: Свежая сборка 2025-10-10 22:35:10 +03:00
Mikhail Chechnev
0388d5630b ЦИТК-878 - "Доски задач" - включение панели в поставку 2025-10-10 22:27:16 +03:00
Mikhail Chechnev
c9e4894a40 ЦИТК-979 - Редактор запросов - сокрытие области текста запроса при отображении редактора 2025-10-10 22:22:42 +03:00
Mikhail Chechnev
9cbc52cff2 ЦИТК-878 - "Доски задач" - доработана под логику смены статуса события с релиза "июнь 2025", исправлена ошибка инициализации клиента с пустым хранилищем, купирована ошибка с отсутствием профиля настроек 2025-10-10 21:58:03 +03:00
Mikhail Chechnev
fe1e3ba04f ЦИТК-878 - Гранты для панели "Доски задач" 2025-10-10 21:09:00 +03:00
Mikhail Chechnev
676a20dd32 ЦИТК-878 - Валидация PKG_P8PANELS_CLNTTSKBRD под релиз июль 2025 2025-10-10 17:42:35 +03:00
Mim
659dfc7c10 ЦИТК-878 - Добавление панели "Доски задач"
Reviewed-on: CITKParus/P8-Panels#37
2025-10-10 13:34:39 +03:00
Mikhail Chechnev
a46cd28656 ЦИТК-979 - Поддержка обязательности связи в запросе 2025-10-10 13:33:22 +03:00
Mikhail Chechnev
6a6e58e88e ЦИТК-979 - Диалог настройки атрибута, ввод и контроль псевдонима атрибутов сущностей, короткие псевдонимы сущностей 2025-10-09 19:17:59 +03:00
Mikhail Chechnev
0767a12fa6 ЦИТК-979 - Область просмотра сформированного запроса вынесена в отдельный компонент 2025-09-25 14:12:27 +03:00
Mikhail Chechnev
be22cde138 ЦИТК-979 - Поддержка возможности указания отладочного значения аргумента запроса 2025-09-25 14:11:32 +03:00
Mikhail Chechnev
dae416cd83 WEBAPP: Поддержка пересчёта диалогов для P8PDialog и P8PInput 2025-09-25 14:06:44 +03:00
Mikhail Chechnev
c66216e47b ЦИТК-979 - Отображение текста сформированного SQL-запроса и предупреждений формирования (клиент) 2025-09-11 18:35:02 +03:00
Mikhail Chechnev
fbb0db8179 ЦИТК-979 - Ограничение ширины заголовка при отображении выбранной сущности в инспекторе 2025-09-11 18:32:23 +03:00
Mikhail Chechnev
3b03f00cc2 ЦИТК-979 - P8PEditorSubHeader - возможность установки предельной ширины и всплывающая подсказка 2025-09-11 18:30:45 +03:00
Mikhail Chechnev
aa9568c2fa ЦИТК-979 - Формирование SQL-выражения для запроса (сервер, начало) 2025-09-11 18:29:06 +03:00
Mikhail Chechnev
5706d59a92 ЦИТК-979 - Список аргументов запроса - косметика в коде 2025-09-04 15:51:21 +03:00
Mikhail Chechnev
ef1a63b4b6 ЦИТК-979 - Диалог настройки атрибутов сущности - фиксированная ширина, сообщение об отсутствии данных при поиске 2025-09-04 15:50:33 +03:00
Mikhail Chechnev
538643591f ЦИТК-979 - Добавление сущности в запрос из списка с поиском по БД 2025-09-04 15:48:16 +03:00
Mikhail Chechnev
f698bc1789 WEBAPP: P8PDialog - поддержка настройки ширины (width) и полноразмерности (fullWidth) 2025-09-04 15:36:27 +03:00
Mikhail Chechnev
f96425c80d ЦИТК-979 - Настройка запроса - условия отбора (выбор компонентов условий, очистка условия) 2025-09-03 15:17:26 +03:00
Mikhail Chechnev
7515a9cce3 ЦИТК-979 - Рефакторинг понятий Сущность, Атрибут, Отношение, Элемент диаграммы, Связь диаграммы 2025-09-03 15:16:12 +03:00
Mikhail Chechnev
86aa639ca0 ЦИТК-979 - Управляемый отступ сверху для P8PEditorSybHeader 2025-09-03 15:13:56 +03:00
Mikhail Chechnev
b09ae4d83b ЦИТК-979 - Настройка запроса - условия отбора (сервер и клиент) 2025-08-22 18:08:45 +03:00
Mikhail Chechnev
eef77cbd2d ЦИТК-979 - Настройка запроса - аргументы (клиент) + рефакторинг инспектора свойств 2025-08-22 14:31:06 +03:00
Mikhail Chechnev
38af01c9ef ЦИТК-979 - Иконки типов данных вынесены в общий модуль 2025-08-22 14:24:53 +03:00
Mikhail Chechnev
abaf9455a9 ЦИТК-979 - Настройка запроса - аргументы (сервер) 2025-08-22 13:39:42 +03:00
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
fef41f5186 merge upstream 2025-07-23 16:11:45 +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
0d03edbd17 merge upstream 2025-07-21 12:05:36 +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
Dollerino
c6d21c83b5 ЦИТК-878 - Добавление панели "Доски задач" 2025-07-11 18:00:29 +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
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
170 changed files with 36291 additions and 8226 deletions

161
README.md
View File

@ -104,6 +104,9 @@
git clone https://git.citpb.ru/CITKParus/P8-Panels.git
```
> **Внимание:** если при клонировании репозитория возникает ошибка "Server certificate verification failed" - используйте ключ `http.sslVerify=false`:\
> `git clone https://git.citpb.ru/CITKParus/P8-Panels.git -c http.sslVerify=false`
6. Проведите компиляцию хранимых объектов БД из каталога "db" клонированного репозитория (компиляцию проводить под пользователем-владельцем схемы серверной части Системы, с последующей перекомпиляцией зависимых инвалидных объектов), затем исполните скрипт "grants.sql", размещённый в этом же каталоге.
7. Перезапустите сервер приложений "ПАРУС 8 Онлайн"
@ -337,6 +340,59 @@ const MyPanel = () => {
8. Выдайте права но новое действие в "Администраторе", при необходимости - начните новый сеанс в "ПАРУС 8 Онлайн" с очисткой системного кэша.
#### Настройка КОР-действия для вызова панели "Выдача сменного задания на участок" из раздела "Сменные задания"
Входящая в состав поставки фреймворка панель "Выдача сменного задания на участок" доступна для вызова из раздела "Сменные задания" (приложение "Планирование и учёт в дискретном производстве", главное меню > "Документы" > "Сменные задания").
Для настройки этой возможности:
1. Откройте раздел "Классы" приложения "Конструктор отраслевых расширений" (главное меню > "Учёт" > "Классы")
2. В дереве классов выберите "Сменные задания", а в списке классов - класс с кодом "CostJobs"
3. В спецификации "Методы", выбранного класса, зарегистрируйте новый метод со следующими атрибутами:
- `Мнемокод` - P8PANELS_OPEN
- `Наименование` - P8PANELS_OPEN
- `Тип метода` - Встроенный
- `Доступность` - Клиентский
4. Для добавленного метода `P8PANELS_OPEN` в спецификации "Параметры" зарегистрируйте следующий набор параметров:
| Имя | Наименование | Тип | Домен | Обязательный | Тип привязки | Контекст | Параметр действия |
| -------- | ------------------- | ------- | ------- | ------------ | ----------------- | -------------------- | ----------------- |
| NRN | Рег. номер записи | Входной | TRN | Нет | Контекст | Идентификатор записи | |
| SPANEL | Наименование панели | Входной | TSTRING | Да | Параметр действия | | SPANEL |
| SCAPTION | Заголовок вкладки | Входной | TSTRING | Нет | Параметр действия | | SCAPTION |
5. В спецификации "Действия", выбранного класса, зарегистрируйте новое действие со следующими атрибутами:
- `Тип` - Нестандартное
- `Код` - FCJOBS_OPEN_JOBS_MANAGE_MP
- `Наименование` - Открытие панели "Выдача сменного задания на участок"
- `Технология производства` - Конструктор
- `Реализующий метод` - P8PANELS_OPEN
- `Обработка записей` - Для одной текущей записи
- `Завершение транзакции` - После каждого вызова действия
- `Обновление выборки` - Не обновлять
6. Для добавленного действия `FCJOBS_OPEN_JOBS_MANAGE_MP` в спецификации "Параметры" зарегистрируйте следующий набор параметров:
| Имя | Домен | Тип привязки | Значение |
| -------- | ------- | ------------ | ---------------------------------- |
| SPANEL | TSTRING | Значение | MechRecCostJobsManageMP |
| SCAPTION | TSTRING | Значение | Выдача сменного задания на участок |
7. Откройте редактор формы представления данных класса "CostJobs" ("Сменные задания").
Для этого отметьте в списке классов запись с кодом "CostJobs", перейдите на закладку "Методы вызова", укажите метод вызова "main", в его контекстном меню укажите "Формы", в появившемся списке форм выполните действие "Редактор" для формы с наименованием "Форма просмотра".
В открывшемся редакторе формы перейдите в режим редактирования всплывающего меню заголовка (закладка "Таблицы", таблица "CostJobs", затем кнопка "Редактор источника", установить фокус на форме представления данных щелчком мыши, затем пункт "Всплывающее меню" в "Инспекторе объектов"). Найдите в меню пункт, созданный Системой для действия, зарегистрированного на шаге 5 (как правило имеет метку, совпадающую с наименованием действия). Расположите (перетаскиванием) этот пункт меню сразу после пункта "Отработать исполнение по штрих-кодам". Укажите для этого пункта следующие параметры в "Инспекторе объектов":
- `Заголовок` - Выдать сменное задание на участок…
Закройте окна редакторов с сохранением изменений.
8. Выдайте права но новое действие в "Администраторе", при необходимости - начните новый сеанс в "ПАРУС 8 Онлайн" с очисткой системного кэша.
#### Настройка КОР-действия для вызова панели "Производственная программа" из раздела "Планы и отчеты производства изделий"
Входящая в состав поставки фреймворка панель "Производственная программа" доступна для вызова из спецификации "Выпуск" раздела "Планы и отчеты производства изделий" (приложение "Планирование и учёт в дискретном производстве", главное меню > "Документы" > "Планы и отчеты производства изделий").
@ -460,7 +516,7 @@ c:\inetpub\p8web20\WebClient\Modules\P8-Panels>npm run build
- `isRespErr` - функция, проверка результата исполнения серверного объекта на наличие ошибок
- `getRespErrMessage` - функция, получение ошибки исполнения серверного объекта
- `getRespPayload` - функция, получение выходных значений, полученных после успешного исполнения
- `executeStored` -функция, асинхронное исполнение хранимой процедуры/функции БД Системы
- `executeStored` - функция, асинхронное исполнение хранимой процедуры/функции БД Системы
- `getConfig` - функция, асинхронное считывание параметров конфигурации, определённых в "p8panels.config" (возвращает их JSON-представление)
При формировании ответов, функции, получающие данные с сервера, возвращают типовые значения:
@ -528,7 +584,8 @@ c:\inetpub\p8web20\WebClient\Modules\P8-Panels>npm run build
throwError = true,
showErrorMessage = true,
fullResponse = false,
spreadOutArguments = true
spreadOutArguments = true,
signal = null
}
```
@ -541,7 +598,8 @@ c:\inetpub\p8web20\WebClient\Modules\P8-Panels>npm run build
`throwError` - необязательный, логический, признак генерации исключения, если `false` - возвращает ошибку в типовом формате\
`showErrorMessage` - необязательный, логический, признак отображения типового клиентского сообщение об ошибке, в случае её возникновения (только если `throwError = true`)\
`fullResponse` - необязательный, логический, признак возврата полного типового ответа сервера, если `false` - возвращается только содержимое `XPAYLOAD`\
`spreadOutArguments` - необязательный, логический, признак "разделения" значений выходных параметров исполняемого обхекта (игнорируется при наличии `respArg`), если `true` - `XPAYLOAD` будет содержать ответ в виде `{"ВЫХОДНОЙ_ПАРАМЕТР1": "ЗНАЧЕНИЕ", "ВЫХОДНОЙ_ПАРАМЕТР2": "ЗНАЧЕНИЕ", ...}`, если `false` - `XPAYLOAD` будет содержать ответ в виде `{XOUT_ARGUMENTS: [{SNAME: "ВЫХОДНОЙ_ПАРАМЕТР1", VALUE: "ЗНАЧЕНИЕ"}, {SNAME: "ВЫХОДНОЙ_ПАРАМЕТР2", VALUE: "ЗНАЧЕНИЕ"}, ...]}`
`spreadOutArguments` - необязательный, логический, признак "разделения" значений выходных параметров исполняемого обхекта (игнорируется при наличии `respArg`), если `true` - `XPAYLOAD` будет содержать ответ в виде `{"ВЫХОДНОЙ_ПАРАМЕТР1": "ЗНАЧЕНИЕ", "ВЫХОДНОЙ_ПАРАМЕТР2": "ЗНАЧЕНИЕ", ...}`, если `false` - `XPAYLOAD` будет содержать ответ в виде `{XOUT_ARGUMENTS: [{SNAME: "ВЫХОДНОЙ_ПАРАМЕТР1", VALUE: "ЗНАЧЕНИЕ"}, {SNAME: "ВЫХОДНОЙ_ПАРАМЕТР2", VALUE: "ЗНАЧЕНИЕ"}, ...]}`\
`signal` - необязательный, объект, экземпляр `AbortSignal` (например, `AbortController.signal`) для управления прерыванием выполнения запроса
**Результат:** объект с данными, размещёнными в `XPAYLOAD` ответа сервера (если `fullResponse = false`) или полный типовой ответ (описан выше).
@ -1035,7 +1093,7 @@ const Mui = ({ title }) => {
![Примеры модальных сообщений](docs/img/63.png)
###### `undefined showMsg(type, text, msgOnOk = null, msgOnCancel = null)`
###### `undefined showMsg(type, text, msgOnOk = null, msgOnCancel = null, fullErrorText = null)`
Отображает модальное окно сообщения заданного типа.
@ -1043,16 +1101,21 @@ const Mui = ({ title }) => {
`type` - обязательный, строка, тип отображаемого сообщения, `information|warning|error` (см. константу `MSG_TYPE` в "app/context/messaging_reducer" и константу `P8P_APP_MESSAGE_VARIANT` в "app/components/p8p_app_message")\
`text` - обязательный, строка, текст отображаемого сообщения\
`msgOnOk` - необязательный, функция, будет вызвана при нажатии на "ОК"/"ЗАКРЫТЬ" в сообщении
`msgOnCancel` - необязательный, функция, будет вызвана при нажатии на "ОТМЕНА" в сообщении (только для сообщений типа `warning`)
`msgOnOk` - необязательный, функция, будет вызвана при нажатии на "ОК"/"ЗАКРЫТЬ" в сообщении\
`msgOnCancel` - необязательный, функция, будет вызвана при нажатии на "ОТМЕНА" в сообщении (только для сообщений типа `warning`)\
`fullErrorText` - необязательный, строка, полный текст ошибки, используется только при `type="error"`. Если параметр указан, то в окно ошибки выводится кнопка "Подробнее", по нажатию на которую будет отображаться текст, указанный в данном параметре
**Результат:** функция не возвращает значимого результата
###### `undefined showMsgErr(text, msgOnOk = null)`
###### `undefined showMsgErr(text, msgOnOk = null, fullErrorText = null)`
Декоратор для `showMsg`, отображает модальное окно сообщения типа "Ошибка" (`type="error"`).
**Входные параметры:** аналогично `showMsg`
**Входные параметры:**
`text` - обязательный, строка, текст отображаемого сообщения\
`msgOnOk` - необязательный, функция, будет вызвана при нажатии на "ЗАКРЫТЬ" в сообщении\
`fullErrorText` - необязательный, строка, полный текст ошибки. Если параметр указан, то в окно ошибки выводится кнопка "Подробнее", по нажатию на которую будет отображаться текст, указанный в данном параметре
**Результат:** аналогично `showMsg`
@ -1140,6 +1203,20 @@ const Messages = ({ title }) => {
Ошибка
</Button>
<Divider sx={STYLES.DIVIDER} />
{/* Сообщение об ошибке (диалог с подробностями) */}
<Button
variant="contained"
onClick={() =>
showMsgErr(
"Что-то пошло не так :( ...но мы точно знаем что ;)",
null,
"Здесь подробная информация об ошибке (стек вызова СУБД, например)"
)
}
>
Ошибка с подробностями
</Button>
<Divider sx={STYLES.DIVIDER} />
{/* Предупреждение (диалог) */}
<Button
variant="contained"
@ -1190,7 +1267,7 @@ const Messages = ({ title }) => {
![Пример индикатора процесса](docs/img/65.png)
###### `undefined showMsg(message)`
###### `undefined showLoader(message)`
Отображает модальный индикатор процесса с указанным сообщением.
@ -1257,14 +1334,14 @@ const Loader = ({ title }) => {
- состоят из значительного числа интерфейсных примитивов
- имеют специальный API на стороне сервера БД Системы для управления их содержимым
Необходимо понимать, что с одной стороны, наличие серверного API в БД значительно упрощает взаимодействие с компонентом, с другой стороны - ограничивает возможности его примерения только теми прикладными задачами и функциональными возможностями, которые заложены в него. При этом "примитивы" HTML и MUI, хоть и сложнее в применении, но позволяют "собирать" практически любые интерфейсные решения на вкус разработчика.
Необходимо понимать, что с одной стороны, наличие серверного API в БД значительно упрощает взаимодействие с компонентом, с другой стороны - ограничивает возможности его применения только теми прикладными задачами и функциональными возможностями, которые заложены в него. При этом "примитивы" HTML и MUI, хоть и сложнее в применении, но позволяют "собирать" практически любые интерфейсные решения на вкус разработчика.
##### Таблица данных "P8PDataGrid"
Предназначена для формирования табличных представлений данных с поддержкой:
- постраничного вывода данных
- сортировки и отбора данных по колонкам на строне сервера БД
- сортировки и отбора данных по колонкам на стороне сервера БД
- сложных заголовков с возможностью отображения/сокрытия уровней
- разворачивающихся строк (accordion)
- группировки строк с возможностью отображения/сокрытия содержимого группы
@ -1291,6 +1368,8 @@ const MyPanel = () => {
**Свойства**
`style` - необязательный, объект, если задан, то будет применён в качестве атрибута `style` коневого контейнера (`div`) компонента\
`tableStyle` - необязательный, объект, если задан, то будет применён в качестве атрибута `sx` компонента `Table`\
`columnsDef` - необязательный, массив, описание колонок таблицы, содержит объекты вида `{caption: <ЗАГОЛОВОК_КОЛОНКИ>, dataType: <ТИП_ДАННЫХ - NUMB|STR|DATE>, filter: <ПРИЗНАК_ВОЗМОЖНОСТИ_ОТБОРА - true|false>, hint: <ОПИСАНИЕ_КОЛОНКИ_МОЖЕТ_СОДЕРЖАТЬ_HTML_РАЗМЕТКУ>, name: <НАИМЕНОВАНИЕ_КОЛОНКИ>, order: <ПРИЗНАК_ВОЗМОЖНОСТИ_СОРТИРОВКИ - true|false>, values: <МАССИВРЕДОПРЕДЕЛЁННЫХ_ЗНАЧЕНИЙ>, visible: <ПРИЗНАК_ВИДИМОСТИ_КОЛОНКИ - true|false>,expandable: <ПРИЗНАК_РАЗВОРАЧИВАЕМОСТИ_ГРУППОВОГО_ЗАГОЛОВКА - true|false>, expanded: <ПРИЗНАК_РАЗВЕРНУТОСТИ_ГРУППОВОГО_ЗАГОЛОВКА - true|false>, parent: <НАИМЕНОВАНИЕ_РОДИТЕЛЬСКОЙ_КОЛОНКИ_ВРУППОВОМ_ЗАГОЛОВКЕ>, width: <ШИРИНА_КОЛОНКИ>}`\
`filtersInitial` - необязательныей, массив, начальное состояние фильтров таблицы, содержит объекты вида `{name: <НАИМЕНОВАНИЕ_КОЛОНКИ>, from: <НАЧАЛО_ДИАПАЗОНА_ЗНАЧЕНИЙ_ФИЛЬТРА>, to: <ОКОНЧАНИЕ_ДИАПАЗОНА_ЗНАЧЕНИЙ_ФИЛЬТРА>}`\
`groups` - необязательный, массив групп данных, содержит объекты вида `{name: <ИМЯ_ГРУППЫ>, caption: <ЗАГОЛОВОКРУППЫ>, expandable: <ПРИЗНАК_РАЗВОРАЧИВАЕМОСТИ_ГРУППЫ - true|false>, expanded: <ПРИЗНАК_РАЗВЕРНУТОСТИ_ГРУППЫ - true|false>}`\
@ -1319,7 +1398,7 @@ const MyPanel = () => {
`rowExpandRender` - необязательный, функция формирования представления развёрнутой строки таблицы (если не указана - интерфейсный элемент для "разворачивания" строки не будет отображён, даже при `expandable=true`). Сигнатура функции `f({row, columnsDef})`. Будет вызвана в момент "развёртывания" строки таблицы пользователем, в функцию будет передан объект, поле `row` которого будет содержать данные текущей "разворачиваемой" строки таблицы, а поле `columnsDef` - описание колонок таблицы. Должна возвращать представление "развёрнутой" строки таблицы в виде значения или Rect-компонента.\
`valueFormatter` - необязательный, функция форматирования значений колонки (если не указана - форматирование согласно `columnsDef`). Сигнатура функции `f({value, columnDef})`. Будет вызвана в момент формирования ячейки таблицы (если ранее для ячейки `dataCellRender` не вернул специального представления) и в моммент формирования фильтра для ячейки. Должна возвращать отформатированное значение ячейки или React-компонент для её представления.\
`containerComponent` - необязательный, функциональный React-компонент или строка с именем HTML-тэга, будет применён для формирования в иерархии DOM элемента-обёртки (контейнера) таблицы (по умолчанию используется компонет библиотеки MUI - Paper)\
`containerComponentProps` - необязательный, объект, содержит свойства, которые будут переданы компоненту-контейнеру таблицы\
`containerComponentProps` - необязательный, объект, содержит свойства, которые будут переданы компоненту-контейнеру (`TableContainer`) таблицы (является дочерним для корневого `div`, контейнера всего компонента)\
`onOrderChanged` - необязательный, функция, будет вызвана при изменении пользователем состояния сортировок таблицы. Сигнатура функции `f({orders})`, результат функции не интерпретируется. В функцию передаётся объект, поле `orders` которого, содержит текущее состояние сортировок таблицы. Объект `orders` - массив, содержащий элементы вида `{name: <НАИМЕНОВАНИЕ_КОЛОНКИ>, direction: <ASC|DESC>}`. Функция применяется для инициации обновления данных в таблице.\
`onFilterChanged` - необязательный, функция, будет вызвана при изменении пользователем состояния фильтров таблицы. Сигнатура функции `f({filters})`, результат функции не интерпретируется. В функцию передаётся объект, поле `filters` которого, содержит текущее состояние фильтров таблицы. Объект `filters` - массив, содержащий элементы вида `{name: <НАИМЕНОВАНИЕ_КОЛОНКИ>, from: <ЗНАЧЕНИЕ_НАЧАЛА_ДИАПАЗОНА_ОТБОРА>, to: <ЗНАЧЕНИЕ_ОКОНЧАНИЯ_ДИАПАЗОНА_ОТБОРА>}`. Функция применяется для инициации обновления данных в таблице.\
`onPagesCountChanged` - необязательный, функция, будет вызвана при изменении пользователем количества отображаемых страниц данных таблицы. Сигнатура функции `f()`, результат функции не интерпретируется. Функция применяется для инициации обновления данных в таблице.\
@ -1900,7 +1979,7 @@ const Chart = ({ title }) => {
};
```
Полные актуальные исходные коды примеров можно увидеть в "db/PKG_P8PANELS_SAMPLES.pck" и "app/panels/samples/data_grid.js" данного репозитория соответственно.
Полные актуальные исходные коды примеров можно увидеть в "db/PKG_P8PANELS_SAMPLES.pck" и "app/panels/samples/chart.js" данного репозитория соответственно.
##### Диаграмма ганта "P8PGantt"
@ -1953,6 +2032,7 @@ const MyPanel = () => {
`onTaskProgressChange` - необязательный, функция, если указана - будет вызвана при изменении прогресса исполнения элемента диаграммы, сигнатура функции `f({task, progress})`, результат функции не интерпретируется. В функцию будет передан объект в поле `task`, которого, будет содержаться описание изменённой задачи (элемент массива `tasks`, см. выше описание полей), в поле `progress` - новое значение прогресса исполнения задачи.\
`taskAttributeRenderer` - необязательный, функция, если указана - будет вызвана при отображении диалога редактора здачи, результат функции будет применён для отображения области дополнительных атрибутов задачи в диалоге редактора, если не указана - дополнительные атрибуты будут отображены с форматированием по умолчанию. Сигнатура функции - `f({task, attribute})`, в функцию будет передан объект в поле `task`, которого, будет содержаться описание задачи для которой отображается редактор (элемент массива `tasks`, см. выше описание полей), в поле `attribute` - описание дополнительного атрибута формируемого в диалоге редактора (элемент массива `taskAttributes`, см. выше описание полей). Должна возвращать значение или React-компонент.\
`taskDialogRenderer` - необязательный, функция, если указана - будет вызвана до отображения диалога редактора задачи. Результат функции будет показан в качестве содержимого диалога редактора, вместо типовой формы. Сигнатура функции - `f({task, taskAttributes, taskColors, close})`, в функцию будет передан объект в поле `task`, которого, будет содержаться описание задачи для которой отображается редактор (элемент массива `tasks`, см. выше описание полей), в поле `taskAttributes` - массив `taskAttributes` (см. выше описание полей), описывающий состав полей задачи, в поле `taskColors` - массив `taskColors` (см. выше описание полей), описывающий цвета заливки, определённые для задачи, в поле `close` - функция закрытия диалога задачи, может быть вызвана возвращаемым Reac-компонентом для сокрытия диалога. Должна возвращать значение или React-компонент.\
`taskDialogProps` - необязательный, объект, содержит свойства, которые будут переданы компоненту-контейнеру (`Dialog`) редактора задачи.\
`noDataFoundText` - обязательный, строка, текст для отображения ошибки об отсутствии данных\
`numbTaskEditorCaption` - обязательный, строка, подпись стандартного атрибута `numb` в диалоге редактора задачи\
`nameTaskEditorCaption` - обязательный, строка, подпись стандартного атрибута `name` в диалоге редактора задачи\
@ -2862,6 +2942,61 @@ export { Cyclogram };
Полные актуальные исходные коды примеров можно увидеть в "db/PKG_P8PANELS_SAMPLES.pck" и "app/panels/samples/cyclogram.js" данного репозитория соответственно.
##### Индикатор "P8PIndicator"
Компонент предназначен для отображения данных в виде индикатора. Поддерживается:
- Цветовая индикация предопределёнными цветами в зависимости от состояния (не определено, позитивное, негативное, пограничное)
- Цветовая индикация пользовательскими цветами
- Обработка нажатий
- Отображение иконки
- Упрвление внешним видом (парение, рамка)
- Интерактивные подсказки
![Пример P8PIndicator](docs/img/74.png)
**Подключение**
Клиентская часть индикатора реализована в компоненте `P8PIndicator`, объявленном в "app/components/p8p_indicator". Для использования компонента на панели его необходимо импортировать:
```
import { P8PIndicator } from "../../components/p8p_indicator";
const MyPanel = () => {
return (
<div>
<P8PIndicator .../>
</div>
);
}
```
**Свойства**
`caption` - обязательный, строка, подпись индикатора\
`value` - обязательный, строка, значение индикатора\
`icon` - необязательный, строка, код иконки индикатора из символов шрифта [Google Material Icons](https://fonts.google.com/icons?icon.set=Material+Icons) (по умолчанию - не указана, если указана - отображается в левой части области индикатора)\
`state` - необязательный, строка, состояние индикатора, принимает значения `UNDEFINED|OK|ERR|WARN` (по умолчанию - `UNDEFINED`, см. константу `P8P_INDICATOR_STATE` в исходном коде компонента), определяет цвет заливки индикатора, если не указаны пользовательские цвета (см. ниже свойства `backgroundColor`и`color`)\
`square` - необязательный, логический, определяет необходимость скругления углов области индикатора (по умолчанию - `false`)\
`elevation` - необязательный, число, высота парения индикатора (по умолчанию - 3, используется только при `variant = 'elevation'`)\
`variant` - необязательный, строка, вариант исполнения, принимает значения `elevation|outlined` (по умолчанию - `elevation`, см. константу `P8P_INDICATOR_VARIANT` в исходном коде компонента), определяет внешний вид индикатора - парящая область или область с рамкой\
`hint` - необязательный, строка, текст подсказки для индикатора (если указан - слева от значения индикатора формируется кнопка открытия диалога с текстом подсказки, поддерживается HTML-форматирование)\
`onClick` - необязательный, функция, будет вызвана при нажатии пользователем на индикатор (если указана - индикатор формируется в виде кнопки), сигнатура функции `f()`, результат функции не интерпретируется\
`backgroundColor` - необязательный, строка, HTML-код пользовательского цвета фона, если указан - будет использован (вне зависимости от `state`) для заливки области индикатора (по умолчанию - не указан) \
`color` - необязательный, строка, HTML-код пользовательского цвета шрифта, если указан - будет использован (вне зависимости от `state`) для значения, подписи и иконки индикатора (по умолчанию - не указан)
**API на сервере БД**
Компонент `P8PIndicator` требует от разработчика передачи данных в определённом формате. С целью снижения трудозатрат на приведение собранных хранимым объектом данных Системы к форматам, потребляемым `P8PIndicator`, реализован специальный API на стороне сервера БД.
Для индикатора это (см. детальные описания программных интерфейсов в пакете `PKG_P8PANELS_VISUAL`):
`PKG_P8PANELS_VISUAL.TINDICATOR_MAKE` - функция, инициализация индикатора, возвращает объект для хранения его описания\
`PKG_P8PANELS_VISUAL.TINDICATOR_TO_XML` - функция, производит сериализацию объекта, описывающего индикатор, в специальный XML-формат, корректно интерпретируемый клиентским компонентом `P8PIndicator` при передаче в WEB-приложение
**Пример**
Полный актуальный исходный код примера можно увидеть в "app/panels/samples/indicator.js" данного репозитория.
### Ограничения дизайна пользовательского интерфейса
Фреймворк позволяет реализовать любые пользовательские интерфейсы, вёрстка которых не противоречит возможностям современного HTML. Тем не менее, при разработке пользовательских интерфейсов панелей важно придерживаться предложенных ниже правил. Это позволит создавать их в едином ключе и упростит работу конечного пользователя при их освоении.

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,21 @@ export const TITLES = {
INFO: "Информация", //Информационный блок
WARN: "Предупреждение", //Блок предупреждения
ERR: "Ошибка", //Информация об ошибке
DEFAULT_PANELS_GROUP: "Без привязки к группе" //Заголовок группы панелей по умолчанию
DEFAULT_PANELS_GROUP: "Без привязки к группе", //Заголовок группы панелей по умолчанию
DATA_SOURCE_CONFIG: "Настройка источника данных", //Заголовок для настройки источника данных
INSERT: "Добавление", //Заголовок для диалогов/форм добавления
UPDATE: "Исправление", //Заголовок для диалогов/форм исправления
CONFIG: "Настройка" //Заголовок для диалога настройки
};
//Текст
export const TEXTS = {
LOADING: "Ожидайте...", //Ожидание завершения процесса
NO_DATA_FOUND: "Данных не найдено" //Отсутствие данных
NO_DATA_FOUND: "Данных не найдено", //Отсутствие данных
NO_DATA_FOUND_SHORT: "Н.Д.", //Отсутствие данных (кратко)
NO_SETTINGS: "Настройки не определены", //Отстутсвие настроек
UNKNOWN_SOURCE_TYPE: "Неизвестный тип источника", //Отсуствие типа источника
UNNAMED_SOURCE: "Источник без наименования" //Отсутствие наименования источника
};
//Текст кнопок
@ -29,11 +37,19 @@ export const BUTTONS = {
OK: "ОК", //Ок
CANCEL: "Отмена", //Отмена
CLOSE: "Закрыть", //Сокрытие
DETAIL: "Подробнее", //Отображение подробностей/детализации
HIDE: "Скрыть", //Скрытие информации
CLEAR: "Очистить", //Очистка
ORDER_ASC: "По возрастанию", //Сортировка по возрастанию
ORDER_DESC: "По убыванию", //Сортировка по убыванию
FILTER: "Фильтр", //Фильтрация
MORE: "Ещё" //Догрузка данных
MORE: "Ещё", //Догрузка данных
APPLY: "Применить", //Сохранение без закрытия интерфейса ввода
SAVE: "Сохранить", //Сохранение
CONFIG: "Настроить", //Настройка
INSERT: "Добавить", //Добавление
UPDATE: "Исправить", //Исправление
DELETE: "Удалить" //Удаление
};
//Метки атрибутов, сопроводительные надписи
@ -46,7 +62,9 @@ export const CAPTIONS = {
START: "Начало",
END: "Окончание",
PROGRESS: "Прогресс",
LEGEND: "Легенда"
LEGEND: "Легенда",
USER_PROC: "Пользовательская процедура",
QUERY: "Запрос"
};
//Типовые сообщения об ошибках
@ -54,10 +72,20 @@ export const ERRORS = {
UNDER_CONSTRUCTION: "Панель в разработке",
P8O_API_UNAVAILABLE: '"ПАРУС 8 Онлайн" недоступен',
P8O_API_UNSUPPORTED: 'Функция "ПАРУС 8 Онлайн" не поддерживается',
DEFAULT: "Неожиданная ошибка"
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,49 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Заголовок раздела редактора
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Divider, Chip } from "@mui/material"; //Интерфейсные компоненты MUI
//---------
//Константы
//---------
//Стили
const STYLES = {
DIVIDER: pt => ({ paddingTop: pt || pt === 0 ? `${pt}px` : "20px" }),
CHIP: maxWidth => ({ cursor: "default", ...(maxWidth ? { maxWidth } : {}) })
};
//-----------
//Тело модуля
//-----------
//Заголовок раздела редактора
const P8PEditorSubHeader = ({ title, paddingTop, maxWidth }) => {
//Формирование представления
return (
<Divider sx={STYLES.DIVIDER(paddingTop)}>
<Chip label={title} size={"small"} title={title} sx={STYLES.CHIP(maxWidth)} />
</Divider>
);
};
//Контроль свойств компонента - Заголовок раздела редактора
P8PEditorSubHeader.propTypes = {
title: PropTypes.string.isRequired,
paddingTop: PropTypes.number,
maxWidth: PropTypes.string
};
//----------------
//Интерфейс модуля
//----------------
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

@ -47,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);
@ -86,7 +86,7 @@ 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>
@ -120,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

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

View File

@ -0,0 +1,111 @@
/*
Парус 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 P8P_DIALOG_WIDTH = {
XS: "xs",
SM: "sm",
MD: "md",
LG: "lg",
XL: "xl"
};
//-----------------------
//Вспомогательные функции
//-----------------------
//Формирование объекта вида {ключ: значение} из текущего состояния элементов ввода формы
const buildFormValues = inputsState =>
inputsState.reduce((res, input) => ({ ...res, [input.name]: input.value == undefined ? null : input.value }), {});
//-----------
//Тело модуля
//-----------
//Диалог
const P8PDialog = ({ title, width, fullWidth, inputs, children, onOk, onCancel, onClose, onInputChange }) => {
//Состояние элементов ввода диалога
const [inputsState, setInputsState] = useState([]);
//При изменении элемента ввода
const handleInputChange = (name, value) => {
//Если есть функция пересчета формы - вызовем её
const doNotChangeInputsState = onInputChange ? onInputChange(name, value, inputsState) : false;
//И ориентируясь на то, пересчитала ли она элементы ввода обновим собственное состояние.
//Если функция пересчета вернула "true", значит она пересчитала что-то, тогда новые настройки элементов придут через свойство inputs и будут обработаны в useEffect ниже.
//Следовательно, и нам здесь не надо состояние выставлять, т.к. всё будет перезаписано useEffectом.
if (!doNotChangeInputsState)
setInputsState(pv => pv.reduce((accum, cur) => [...accum, { ...cur, value: cur.name === name ? value : cur.value }], []));
};
//При нажатии на "ОК" диалога
const handleOk = () => onOk && onOk(buildFormValues(inputsState));
//При нажатии на "Отмена" диалога
const handleCancel = () => onCancel && onCancel();
//При нажатии на "Закрыть" диалога
const handleClose = () => (onClose ? onClose() : onCancel ? onCancel() : null);
//При изменении полей для ввода
useEffect(() => {
if (inputs && Array.isArray(inputs) && inputs.length > 0) setInputsState(inputs.map(input => ({ ...input })));
}, [inputs]);
//Расчет объектного представления текущих значений формы
const formValues = buildFormValues(inputsState);
//Формирование представления
return (
<Dialog onClose={handleClose} open {...{ ...(width ? { maxWidth: width } : {}), ...(fullWidth === true ? { fullWidth: true } : {}) }}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
{inputsState.map((input, i) => (
<P8PInput key={i} {...input} formValues={formValues} 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,
width: PropTypes.oneOf(Object.values(P8P_DIALOG_WIDTH)),
fullWidth: PropTypes.bool,
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,
onInputChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PDialog, P8P_DIALOG_WIDTH };

View File

@ -139,6 +139,7 @@ const P8PGanttTaskEditor = ({
onCancel,
taskAttributeRenderer,
taskDialogRenderer,
taskDialogProps,
numbCaption,
nameCaption,
startCaption,
@ -186,7 +187,7 @@ const P8PGanttTaskEditor = ({
//Генерация содержимого
return (
<Dialog open onClose={handleCancel}>
<Dialog open onClose={handleCancel} {...(taskDialogProps ? taskDialogProps : {})}>
{taskDialogRenderer ? (
taskDialogRenderer({ task, taskAttributes, taskColors, close: handleCancel })
) : (
@ -315,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,
@ -347,6 +349,7 @@ const P8PGantt = ({
onTaskProgressChange,
taskAttributeRenderer,
taskDialogRenderer,
taskDialogProps,
noDataFoundText,
numbTaskEditorCaption,
nameTaskEditorCaption,
@ -467,6 +470,7 @@ const P8PGantt = ({
onCancel={handleTaskEditorCancel}
taskAttributeRenderer={taskAttributeRenderer}
taskDialogRenderer={taskDialogRenderer}
taskDialogProps={taskDialogProps}
numbCaption={numbTaskEditorCaption}
nameCaption={nameTaskEditorCaption}
startCaption={startTaskEditorCaption}
@ -502,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 };

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

@ -0,0 +1,135 @@
/*
Парус 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 [current, setCurrent] = useState({ type: undefined, value: "" });
//При получении нового значения или типа из вне
useEffect(() => setCurrent({ value, type }), [type, value]);
//Выбор значения из словаря
const handleDictionaryClick = () => dictionary && dictionary(formValues, res => (res ? res.map(i => handleChangeByName(i.name, i.value)) : null));
//Изменение значения элемента (по событию)
const handleChange = e => {
setCurrent(pv => ({ ...pv, value: e.target.value }));
if (onChange) onChange(e.target.name, e.target.value);
};
//Изменение значения элемента (по имени и значению)
const handleChangeByName = (targetName, value) => {
if (targetName === name) setCurrent(pv => ({ ...pv, 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={current.value ? current.value : ""}
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(current.value) ? "" : current.value}
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 {...(current.type == "date" ? { shrink: true } : {})} htmlFor={name}>
{label}
</InputLabel>
<Input
id={name}
name={name}
value={current.value ? current.value : ""}
endAdornment={
dictionary ? (
<InputAdornment position="end">
<IconButton aria-label={`${name} select`} onClick={handleDictionaryClick} edge="end">
<Icon>list</Icon>
</IconButton>
</InputAdornment>
) : null
}
{...(current.type ? { type: current.type } : {})}
onChange={handleChange}
disabled={disabled}
/>
</>
)}
</FormControl>
</Box>
);
};
//Контроль свойств - Поле ввода
P8PInput.propTypes = P8P_INPUT;
//----------------
//Интерфейс модуля
//----------------
export { P8P_INPUT, P8PInput };

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,
@ -290,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,
@ -488,6 +464,8 @@ P8PTableFiltersChips.propTypes = {
//Таблица
const P8PTable = ({
style = {},
tableStyle = {},
columnsDef = [],
groups = [],
rows = [],
@ -702,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}
@ -735,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}>
@ -900,6 +876,8 @@ const P8PTable = ({
//Контроль свойств - Таблица
P8PTable.propTypes = {
style: PropTypes.object,
tableStyle: PropTypes.object,
columnsDef: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,

View File

@ -56,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);
@ -169,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

@ -102,14 +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);
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) => {
@ -158,6 +161,34 @@ const formatDateJSONDateOnly = value => (value ? dayjs(value).format("YYYY-MM-DD
//Форматирование числа в "Денежном" формате РФ
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 =>
@ -172,11 +203,13 @@ export {
hasValue,
getDisplaySize,
deepCopyObject,
object2XML,
object2Base64XML,
xml2JSON,
formatDateRF,
formatDateTimeRF,
formatDateJSONDateOnly,
formatNumberRFCurrency,
formatErrorMessage,
genGUID
};

View File

@ -0,0 +1,297 @@
/*
Парус 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, useColorRules, useStatuses } from "./hooks/hooks.js"; //Вспомогательные хуки
import { useTasks } from "./hooks/tasks_hooks.js"; //Хук событий
import { useFilters } from "./hooks/filter_hooks.js"; //Вспомогательные хуки фильтра
import { NoteDialog } from "./components/note_dialog.js"; //Диалог примечания
import { SettingsDialog } from "./components/settings_dialog.js"; //Диалог дополнительных настроек
import { deepCopyObject } from "../../core/utils.js"; //Вспомогательные функции
import { COMMON_STYLES } from "./styles"; //Общие стили
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
//---------
//Константы
//---------
//Высота фильтра
const FILTER_HEIGHT = "56px";
//Стили
const STYLES = {
CONTAINER: { width: "100%", padding: 0 },
BOX_FILTER: { display: "flex", alignItems: "center" },
ICON_BUTTON_SETTINGS: { marginLeft: "auto" },
STACK_STATUSES: { maxWidth: "99vw", paddingBottom: "5px", overflowX: "auto", ...COMMON_STYLES.SCROLL },
BOX_STATUSES: { position: "fixed", left: "8px", top: `calc(${APP_BAR_HEIGHT} + ${FILTER_HEIGHT})` }
};
//-----------
//Тело модуля
//-----------
//Корневая панель доски задач
const ClntTaskBoard = () => {
//Состояние фильтров
const [filters, handleFiltersChange] = useFilters();
//Состояние текущего загруженного фильтра
const [filterTypeLoaded, setFilterTypeLoaded] = useState(filters.values.sType);
//Состояние вспомогательных диалогов
const [dialogsState, setDialogsState] = useState({
filterDialogIsOpen: filters.isSetByUser,
settingsDialogIsOpen: false,
noteDialog: { isOpen: false, callback: null },
taskDialogIsOpen: false
});
//Состояние сортировок
const [orders, setOrders] = useState([]);
//Состояние дополнительных данных
const [extraData, setExtraData, handleDocLinksLoad] = useExtraData(filters.values.sType);
//Состояние статусов событий
const [statuses, statusesState, setStatuses, setStatusesState] = useStatuses(filters.values.sType);
//Состояние пользовательских настроек заливки событий
const [colorRules, setColorRules] = useColorRules();
//Состояние событий
const [tasks, setTasks, onDragEnd] = useTasks(filters.values, orders);
//Состояние доступных маршрутов события
const [availableRoutes, setAvailableRoutes] = useState({ source: "", routes: [] });
//При открытии/закрытии диалога фильтра
const handleFilterOpen = isOpen => {
setDialogsState(pv => ({ ...pv, filterDialogIsOpen: isOpen }));
};
//При открытии/закрытии диалога дополнительных настроек
const handleSettingsOpen = () => setDialogsState(pv => ({ ...pv, settingsDialogIsOpen: !pv.settingsDialogIsOpen }));
//При открытии/закрытии диалога примечания
const handleNoteOpen = (cb = null) => {
setDialogsState(pv => ({ ...pv, noteDialog: { isOpen: !dialogsState.noteDialog.isOpen, callback: cb ? v => cb(v) : null } }));
};
//При открытии/закрытии диалога события
const handleTaskDialogOpen = () => setDialogsState(pv => ({ ...pv, taskDialogIsOpen: !dialogsState.taskDialogIsOpen }));
//При необходимости обновить дополнительные данные
const handleExtraDataReload = useCallback(() => {
setExtraData(pv => ({ ...pv, reload: true }));
}, [setExtraData]);
//При необходимости обновить информацию о событиях
const handleTasksReload = useCallback(
(bAccountsReload = true) => {
setTasks(pv => ({ ...pv, reload: true, accountsReload: bAccountsReload }));
},
[setTasks]
);
//При необходимости обновить состояние статусов
const handleStatusesStateReload = useCallback(() => {
setStatusesState(pv => ({ ...pv, reload: true, sorted: false }));
}, [setStatusesState]);
//При изменении дополнительных настроек
const handleSettingsChange = (newSettings, statusesState) => {
setColorRules(pv => ({ ...pv, selectedColorRule: newSettings.selectedColorRule }));
setStatusesState({ ...statusesState, sorted: false });
};
//При изменении цвета карточки статуса
const handleSettingStatusColorChange = (changedStatus, newColor) => {
//Считываем массив статусов
let newStatuses = [...statuses];
//Изменяем цвет нужного статуса
newStatuses.find(status => status.ID === changedStatus.ID).color = newColor;
//Обновляем состояние
setStatuses([...newStatuses]);
};
//При изменении сортировки
const handleOrderChanged = columnName => {
//Копируем состояние сортировки
let newOrders = deepCopyObject(orders);
//Находим сортируемую колонку
const orderedColumn = newOrders.find(o => o.name == columnName);
//Определяем направление сортировки
const newDirection = orderedColumn?.direction == "ASC" ? "DESC" : orderedColumn?.direction == "DESC" ? null : "ASC";
//Если сортировка отключается - очищаем информацию о сортировке
if (newDirection == null && orderedColumn) newOrders.splice(newOrders.indexOf(orderedColumn), 1);
//Если сортировки не было - устанавливаем
if (newDirection != null && !orderedColumn) newOrders.push({ name: columnName, direction: newDirection });
//Если сортировка была и не отключается - изменяем
if (newDirection != null && orderedColumn) orderedColumn.direction = newDirection;
//Устанавливаем новую сортировку
setOrders(newOrders);
};
//При необходимости очистки доступных маршрутов события
const handleAvailableRoutesStateClear = () => {
setAvailableRoutes({ source: "", routes: [] });
};
//Проверка доступности карточки события
const isCardAvailable = code => {
return availableRoutes.source === code || availableRoutes.routes.find(r => r.SDESTINATION === code) || !availableRoutes.source ? true : false;
};
//При изменении фильтра
useEffect(() => {
//Если изменился тип
if (filters.loaded && filters.values.sType) {
//Если тип события изменился
if (filterTypeLoaded !== filters.values.sType) {
//Обновляем информацию о дополнительных данных
handleExtraDataReload();
//Обновляем информацию о статусах
handleStatusesStateReload();
//Обновляем текущий загруженный тип события
setFilterTypeLoaded(filters.values.sType);
}
//Обновляем информацию о событиях
handleTasksReload();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters.loaded, filters.values]);
//При изменении сортировки
useEffect(() => {
//Если есть все данные для загрузки событий
if (filters.loaded && filters.values.sType) {
//Обновляем информацию о событиях без обновления контрагентов
handleTasksReload(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [orders]);
//Генерация содержимого
return (
<Box sx={STYLES.CONTAINER}>
{dialogsState.settingsDialogIsOpen ? (
<SettingsDialog
initial={{ colorRules: colorRules, statusesState: statusesState }}
onSettingsChange={handleSettingsChange}
onClose={handleSettingsOpen}
/>
) : null}
{dialogsState.taskDialogIsOpen ? (
<TaskDialog taskType={filters.values.sType} onTasksReload={() => handleTasksReload(true)} onClose={handleTaskDialogOpen} />
) : null}
<Box sx={STYLES.BOX_FILTER}>
<Stack direction="row">
<Box>
<Stack direction="row" pl={1} pt={1}>
<IconButton onClick={handleTaskDialogOpen} title={"Добавить событие"}>
<Icon>add</Icon>
</IconButton>
</Stack>
</Box>
<Filter
isFilterDialogOpen={dialogsState.filterDialogIsOpen}
filter={filters.values}
docLinks={extraData.docLinks}
selectedDocLink={filters.values.sDocLink ? extraData.docLinks.find(d => d.NRN === filters.values.sDocLink) : null}
onFilterChange={handleFiltersChange}
onDocLinksLoad={handleDocLinksLoad}
onFilterOpen={() => handleFilterOpen(true)}
onFilterClose={() => handleFilterOpen(false)}
onTasksReload={handleTasksReload}
orders={orders}
onOrderChanged={handleOrderChanged}
/>
</Stack>
<IconButton title="Настройки" onClick={handleSettingsOpen} sx={STYLES.ICON_BUTTON_SETTINGS}>
<Icon>settings</Icon>
</IconButton>
</Box>
{dialogsState.noteDialog.isOpen ? (
<NoteDialog noteTypes={extraData.noteTypes} onCallback={note => dialogsState.noteDialog.callback(note)} onClose={handleNoteOpen} />
) : null}
{filters.loaded && filters.values.sType && extraData.dataLoaded && tasks.loaded ? (
<DragDropContext
onDragStart={path => {
//Поиск кода текущего статуса задачи
let sourceCode = statuses.find(status => status.ID == path.source.droppableId).SEVNSTAT_CODE;
//Устанавливаем доступные маршруты события
setAvailableRoutes({ source: sourceCode, routes: [...extraData.evRoutes.filter(route => route.SSOURCE === sourceCode)] });
}}
onDragEnd={path => {
//Если есть статус назначения
if (path.destination) {
//Определяем мнемокод статуса назначения
let destCode = statuses.find(status => status.ID == path.destination.droppableId).SEVNSTAT_CODE;
//Переносим событие
onDragEnd({ path: path, eventPoints: extraData.evPoints, openNoteDialog: handleNoteOpen, destCode: destCode });
}
//Очищаем информацию о доступных маршрутах события
handleAvailableRoutesStateClear();
}}
>
<Box sx={STYLES.BOX_STATUSES}>
<Droppable droppableId="Statuses" type="droppableTask">
{provided => (
<div ref={provided.innerRef}>
<Stack direction="row" spacing={2} sx={STYLES.STACK_STATUSES}>
{statusesState.sorted
? statuses.map((status, index) => (
<div key={index}>
<Droppable
isDropDisabled={!isCardAvailable(status.SEVNSTAT_CODE)}
droppableId={status.ID.toString()}
>
{provided => (
<div ref={provided.innerRef}>
<StatusCard
tasks={tasks}
status={status}
statusTitle={status[statusesState.attr] || status.SEVNSTAT_NAME}
colorRules={colorRules}
extraData={extraData}
isCardAvailable={isCardAvailable}
onTasksReload={handleTasksReload}
onNoteDialogOpen={handleNoteOpen}
onStatusColorChange={handleSettingStatusColorChange}
placeholder={provided.placeholder}
/>
</div>
)}
</Droppable>
</div>
))
: null}
</Stack>
{provided.placeholder}
</div>
)}
</Droppable>
</Box>
</DragDropContext>
) : null}
</Box>
);
};
//----------------
//Интерфейс модуля
//----------------
export { ClntTaskBoard };

View File

@ -0,0 +1,174 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Кастомное поле ввода
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { FormControl, InputLabel, Input, InputAdornment, IconButton, Icon, FormHelperText, Select, MenuItem, Typography } from "@mui/material"; //Интерфейсные компоненты
import { COMMON_STYLES } from "../styles"; //Общие стили
//---------
//Константы
//---------
//Стили
const STYLES = {
HELPER_TEXT: { color: "red" },
SELECT_MENU: width => {
return { ...COMMON_STYLES.SCROLL, width: width ? width + 24 : null };
}
};
//---------------
//Тело компонента
//---------------
//Кастомное поле ввода
const CustomInputField = ({
elementCode,
elementValue,
labelText,
onChange,
required = false,
items = null,
emptyItem = null,
dictionary,
menuItemRender,
...other
}) => {
//Значение элемента
const [value, setValue] = useState(elementValue);
//Состояние элемента HTML (для оптимизации ширины MenuItems)
const [anchorEl, setAnchorEl] = useState();
//При открытии меню заливки событий
const handleMenuOpen = e => {
//Устанавливаем элемент меню
setAnchorEl(e.target);
};
//При получении нового значения из вне
useEffect(() => {
setValue(elementValue);
}, [elementValue]);
//Изменение значения элемента
const handleChange = e => {
setValue(e.target.value);
if (onChange) onChange(e.target.name, e.target.value);
};
//Выбор значения из словаря
const handleDictionaryClick = () => {
dictionary ? dictionary(res => (res ? handleChange({ target: { name: elementCode, value: res } }) : null)) : null;
};
//Генерация поля с выбором из словаря Парус
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, anchorEl, handleMenuOpen, validationError) => {
//Формируем общий список элементов меню
const menuItems = emptyItem ? [emptyItem, ...items] : [...items];
//Генерация содержимого
return (
<Select
error={validationError}
id={elementCode}
name={elementCode}
//!!!Пересмотреть момент. При изменении типа происходит ререндер со старым значением учетного документа:
//1. Изменяется тип
//2. Очищается items (список учетных документов)
//3. Рисуется компонент со старым value и пустым items, из-за чего ошибка "You have provided an out-of-range value"
//4. Вызывается useEffect, меняется значение value на новое (пустое значение)
value={value}
aria-describedby={`${elementCode}-helper-text`}
label={labelText}
MenuProps={{ slotProps: { paper: { sx: STYLES.SELECT_MENU(anchorEl?.offsetWidth) } } }}
onChange={handleChange}
onOpen={handleMenuOpen}
{...other}
>
{menuItems
? menuItems.map((item, index) => {
let customRender = null;
if (menuItemRender) customRender = menuItemRender({ item: item, key: item?.key ?? index }) || null;
return customRender ? (
customRender
) : (
<MenuItem key={item?.key ?? index} value={item.id}>
<Typography variant="inherit" noWrap title={item.caption} component="div">
{item.caption}
</Typography>
</MenuItem>
);
})
: null}
</Select>
);
};
//Признак ошибки валидации
const validationError = !value && required ? true : false;
//Генерация содержимого
return (
<FormControl fullWidth variant="standard">
<InputLabel htmlFor={elementCode}>{labelText}</InputLabel>
{items ? renderSelect(items, anchorEl, handleMenuOpen, validationError) : renderInput(validationError)}
{validationError ? (
<FormHelperText id={`${elementCode}-helper-text`} sx={STYLES.HELPER_TEXT}>
*Обязательное поле
</FormHelperText>
) : null}
</FormControl>
);
};
//Контроль свойств - Кастомное поле ввода
CustomInputField.propTypes = {
elementCode: PropTypes.string.isRequired,
elementValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
labelText: PropTypes.string.isRequired,
required: PropTypes.bool,
items: PropTypes.arrayOf(PropTypes.object),
emptyItem: PropTypes.object,
dictionary: PropTypes.func,
onChange: PropTypes.func,
menuItemRender: PropTypes.func
};
//--------------------
//Интерфейс компонента
//--------------------
export { CustomInputField };

View File

@ -0,0 +1,336 @@
/*
Парус 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 { CustomInputField } from "./custom_input_field"; //Кастомное поле ввода
import { hasValue } from "../../../core/utils"; //Вспомогательные функции
import { EVENT_STATES } from "../layouts"; //Перечисление состояний события
import { COMMON_STYLES } from "../styles"; //Общие стили
import { useDictionary } from "../hooks/dict_hooks"; //Состояние открытия разделов
//---------
//Константы
//---------
//Стили
const STYLES = {
SELECT: { width: "450px" }
};
//---------------
//Тело компонента
//---------------
//Диалог фильтра отбора
const FilterDialog = ({ initial, onFilterChange, onFilterClose, onDocLinksLoad }) => {
//Собственное состояние
const [filter, setFilter] = useState(initial.filter);
//Состояние текущих учётных документов
const [curDocLinks, setCurDocLinks] = useState({ loaded: true, docLinks: initial.docLinks });
//Вспомогательные функции открытия раздела
const { handleCatalogTreeOpen, handleEventTypesOpen, handleAgnlistOpen, handleInsDepartmentOpen, handleCostStaffGroupsOpen } = useDictionary();
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При изменении типа события фильтра
const handleTypeChange = callBack =>
handleEventTypesOpen({
sCode: filter.sType,
callBack: res => {
callBack(res.outParameters.eventtypecode);
}
});
//При изменении каталога фильтра
const handleCrnChange = callBack =>
handleCatalogTreeOpen({
sUnitName: "ClientEvents",
sName: filter.sCrnName,
callBack: res => {
callBack(res.outParameters.out_NAME);
}
});
//При изменении исполнителя фильтра
const handleSendPersonChange = callBack =>
handleAgnlistOpen({
sMnemo: filter.sSendPerson,
callBack: res => {
callBack(res.outParameters.agnmnemo);
}
});
//При изменении подразделения фильтра
const handleSendDivisionChange = callBack =>
handleInsDepartmentOpen({
sCode: filter.sSendDivision,
callBack: res => {
callBack(res.outParameters.out_CODE);
}
});
//При изменении группы пользователей фильтра
const handleSendUsrGrpChange = callBack =>
handleCostStaffGroupsOpen({
sCode: filter.sSendUsrGrp,
callBack: res => {
callBack(res.outParameters.out_CODE);
}
});
//Считывание подкаталогов
const getSubCatalogs = useCallback(async () => {
//Считываем каталоги
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_SUBCATALOGS_GET",
args: {
SCRN_NAME: filter.sCrnName,
NSUBCAT: filter.bSubcatalogs ? 1 : 0
}
});
//Возвращаем список каталогов
return data.SRESULT;
}, [executeStored, filter.sCrnName, filter.bSubcatalogs]);
//При закрытии диалога с изменением фильтра
const handleDialogOk = async () => {
//Если указано имя каталога, но не загружен список рег. номеров
if (filter.sCrnName && !filter.sCrnRnList) {
//Загружаем список рег. номеров каталогов
const crns = await getSubCatalogs();
//Устанавливаем новый фильтр
onFilterChange({ ...filter, ...(crns ? { sCrnRnList: crns } : {}) });
//Закрываем диалог фильтра
onFilterClose();
} else {
//Устанавливаем новый фильтр
onFilterChange(filter);
//Закрываем диалог фильтра
onFilterClose();
}
};
//При очистке фильтра
const handleFilterClear = () => {
setFilter({
sState: EVENT_STATES[1],
sType: "",
sCrnName: "",
sCrnRnList: "",
bSubcatalogs: false,
sSendPerson: "",
sSendDivision: "",
sSendUsrGrp: "",
sDocLink: ""
});
};
//При изменении значения элемента
const handleFilterItemChange = (item, value) => {
//Если это изменение типа
if (item === "sType") {
//Указываем тип с очисткой информации об учетных документах
setFilter(pv => ({ ...pv, [item]: value, sDocLink: "" }));
setCurDocLinks(pv => ({ ...pv, loaded: false, docLinks: [] }));
} else {
//Обновляем значение поля
setFilter(pv => ({ ...pv, [item]: value }));
}
};
//При очистке учётного документа
const handleDocLinkClear = () => setFilter(pv => ({ ...pv, sDocLink: "" }));
//Обработка изменений с каталогами
useEffect(() => {
//Если каталог не указан, но галка подкаталогов установлена - снимаем её
if (!filter.sCrnName && filter.bSubcatalogs) setFilter(pv => ({ ...pv, bSubcatalogs: false }));
//Если изменился каталог и остался список рег. номеров каталогов - очищаем его
if (filter.sCrnName !== initial.sCrnName && filter.sCrnRnList) setFilter(pv => ({ ...pv, sCrnRnList: "" }));
//Если каталог равен изначальному
if (filter.sCrnName === initial.sCrnName) {
//Если признак подкаталогов равен изначальному, но список рег. номеров каталогов не соответствует - загружаем изначальный
if (filter.bSubcatalogs === initial.bSubcatalogs && filter.sCrnRnList !== initial.sCrnRnList) {
setFilter(pv => ({ ...pv, sCrnRnList: initial.sCrnRnList }));
}
//Если признак подкаталогов не равен изначальному
if (filter.bSubcatalogs !== initial.bSubcatalogs) {
//Если не установлен - считываем первый из списка рег. номеров изначальных каталогов
if (!filter.bSubcatalogs) {
setFilter(pv => ({
...pv,
sCrnRnList: initial.sCrnRnList.split(";")[0]
}));
} else {
//Если установлен - очищаем список рег. номеров каталогов для последующей загрузки
setFilter(pv => ({ ...pv, sCrnRnList: "" }));
}
}
}
}, [filter.sCrnName, filter.sCrnRnList, filter.bSubcatalogs, initial.sCrnName, initial.sCrnRnList, initial.bSubcatalogs]);
//Генерация содержимого
return (
<div>
<Dialog open onClose={onFilterClose} fullWidth maxWidth="sm">
<DialogTitle>Фильтр отбора</DialogTitle>
<IconButton aria-label="close" onClick={onFilterClose} sx={COMMON_STYLES.DIALOG_CLOSE_BUTTON}>
<Icon>close</Icon>
</IconButton>
<DialogContent sx={COMMON_STYLES.SCROLL}>
<Box sx={COMMON_STYLES.BOX_WITH_LEGEND} component="fieldset">
<legend style={COMMON_STYLES.LEGEND}>Состояние</legend>
<RadioGroup
row
aria-labelledby="sState-label"
id="sState"
name="sState"
value={filter.sState}
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}>
<CustomInputField
elementCode="sType"
elementValue={filter.sType}
labelText="Тип"
dictionary={callBack => handleTypeChange(callBack)}
onChange={handleFilterItemChange}
/>
</Box>
<Box component="section" p={1}>
<CustomInputField
elementCode="sCrnName"
elementValue={filter.sCrnName}
labelText="Каталог"
dictionary={callBack => handleCrnChange(callBack)}
onChange={handleFilterItemChange}
/>
<FormControlLabel
control={
<Checkbox
id="bSubcatalogs"
name="bSubcatalogs"
checked={filter.bSubcatalogs}
disabled={filter.sCrnName ? false : true}
onChange={e => handleFilterItemChange(e.target.name, e.target.checked)}
/>
}
label="Включая подкаталоги"
/>
</Box>
<Box component="section" p={1}>
<CustomInputField
elementCode="sSendPerson"
elementValue={filter.sSendPerson}
labelText="Исполнитель"
dictionary={callBack => handleSendPersonChange(callBack)}
onChange={handleFilterItemChange}
/>
</Box>
<Box component="section" p={1}>
<CustomInputField
elementCode="sSendDivision"
elementValue={filter.sSendDivision}
labelText="Подразделение"
dictionary={callBack => handleSendDivisionChange(callBack)}
onChange={handleFilterItemChange}
/>
</Box>
<Box component="section" p={1}>
<CustomInputField
elementCode="sSendUsrGrp"
elementValue={filter.sSendUsrGrp}
labelText="Группа пользователей"
dictionary={callBack => handleSendUsrGrpChange(callBack)}
onChange={handleFilterItemChange}
/>
</Box>
<Box component="section" p={1}>
<Stack direction="row" sx={COMMON_STYLES.STACK_DOCLINKS}>
<CustomInputField
elementCode="sDocLink"
elementValue={filter.sDocLink}
labelText="Учётный документ"
items={[...(curDocLinks.docLinks || [])].reduce((prev, cur) => [...prev, { id: cur.NRN, caption: cur.SDESCR }], [])}
disabled={!curDocLinks.docLinks.length ? true : false}
onChange={handleFilterItemChange}
sx={STYLES.SELECT}
/>
<IconButton title="Очистить" disabled={!filter.sDocLink} onClick={handleDocLinkClear}>
<Icon>clear</Icon>
</IconButton>
<IconButton
title="Обновить"
disabled={curDocLinks.loaded}
onClick={() => {
//Очищаем учетный документ
handleDocLinkClear();
//Загружаем учетные документы типа
onDocLinksLoad(filter.sType).then(dl => setCurDocLinks(pv => ({ ...pv, loaded: true, docLinks: dl })));
}}
>
<Icon>refresh</Icon>
</IconButton>
</Stack>
</Box>
</DialogContent>
<DialogActions sx={COMMON_STYLES.DIALOG_ACTIONS}>
<Button disabled={!hasValue(filter.sType)} variant="text" onClick={handleDialogOk}>
ОК
</Button>
<Button variant="text" onClick={handleFilterClear}>
Очистить
</Button>
<Button variant="text" onClick={onFilterClose}>
Отмена
</Button>
</DialogActions>
</Dialog>
</div>
);
};
//Контроль свойств компонента - Диалог фильтра отбора
FilterDialog.propTypes = {
initial: PropTypes.object.isRequired,
onFilterChange: PropTypes.func.isRequired,
onFilterClose: PropTypes.func.isRequired,
onDocLinksLoad: PropTypes.func
};
//--------------------
//Интерфейс компонента
//--------------------
export { FilterDialog };

View File

@ -0,0 +1,99 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Диалог примечания
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Dialog, DialogTitle, IconButton, Icon, DialogContent, DialogActions, Button, TextField } from "@mui/material"; //Интерфейсные компоненты
import { CustomInputField } from "./custom_input_field.js"; //Кастомное поле ввода
import { COMMON_STYLES } from "../styles"; //Общие стили
//Стили
const STYLES = {
DIALOG_CONTENT: { paddingTop: 0, paddingBottom: 0 }
};
//---------------
//Тело компонента
//---------------
//Диалог примечания
const NoteDialog = ({ noteTypes, onCallback, onClose }) => {
//Собственное состояние
const [note, setNote] = useState({ noteTypeIndex: 0, text: "" });
//При изменении примечания
const handleNoteChange = value => setNote(pv => ({ ...pv, text: value }));
//При изменении заголовка примечания
const handleNoteHeaderChange = (name, value) => {
setNote(pv => ({ ...pv, noteTypeIndex: value }));
};
//При закрытии диалога с изменением примечания
const handleDialogOk = () => {
//Передаем информацию о примечание в callback
onCallback({ header: noteTypes[note.noteTypeIndex], text: note.text });
onClose();
};
//Генерация содержимого
return (
<Dialog open onClose={onClose} fullWidth maxWidth="sm">
<DialogTitle>Примечание</DialogTitle>
<IconButton aria-label="close" onClick={onClose} sx={COMMON_STYLES.DIALOG_CLOSE_BUTTON}>
<Icon>close</Icon>
</IconButton>
<DialogContent sx={STYLES.DIALOG_CONTENT}>
<CustomInputField
elementCode="noteHeader"
elementValue={note.noteTypeIndex}
labelText="Заголовок примечания"
items={noteTypes.reduce((prev, cur) => [...prev, { id: prev.length, caption: cur }], [])}
onChange={handleNoteHeaderChange}
margin="dense"
/>
<TextField
id="note"
label="Описание"
variant="standard"
fullWidth
required
multiline
minRows={7}
maxRows={7}
value={note.text}
margin="normal"
inputProps={{ sx: COMMON_STYLES.SCROLL }}
onChange={e => handleNoteChange(e.target.value)}
/>
</DialogContent>
<DialogActions sx={COMMON_STYLES.DIALOG_ACTIONS}>
<Button disabled={!note.text} variant="text" onClick={handleDialogOk}>
ОК
</Button>
<Button variant="text" onClick={onClose}>
Отмена
</Button>
</DialogActions>
</Dialog>
);
};
//Контроль свойств - Диалог примечания
NoteDialog.propTypes = {
noteTypes: PropTypes.array,
onCallback: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { NoteDialog };

View File

@ -0,0 +1,149 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Диалог дополнительных настроек
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Icon, Button, Box, Stack, Typography } from "@mui/material"; //Интерфейсные компоненты
import { CustomInputField } from "./custom_input_field.js"; //Кастомное поле ввода
import { sortAttrs, sortDest } from "../layouts.js"; //Допустимые значение поля и направления сортировки
import { hasValue } from "../../../core/utils.js"; //Проверка наличия значения
import { COMMON_STYLES } from "../styles"; //Общие стили
//---------
//Константы
//---------
//Стили
const STYLES = {
SELECT: { width: "100%" }
};
//---------------
//Тело компонента
//---------------
//Диалог дополнительных настроек
const SettingsDialog = ({ initial, onSettingsChange, onClose, ...other }) => {
//Состояние дополнительных настроек
const [colorRules, seColorRules] = useState(initial.colorRules);
//Состояние статусов
const [statusesState, setStatusesState] = useState(initial.statusesState);
//Изменение поля сортировки
const handleSortAttrChange = (item, value) => setStatusesState(pv => ({ ...pv, [item]: value }));
//Изменение направления сортировки
const handleSortDestChange = newDirection => setStatusesState(pv => ({ ...pv, direction: newDirection }));
//При изменении правила заливки событий
const handleColorRuleChange = (item, value) => {
//Определяем новое правило заливки
let newColorRule = colorRules.rules[value];
//Обновляем в основных настройках
seColorRules(pv => ({ ...pv, selectedColorRule: newColorRule ? newColorRule : {} }));
};
//Генерация содержимого
return (
<div {...other}>
<Dialog open onClose={onClose} fullWidth maxWidth="sm">
<DialogTitle>Настройки</DialogTitle>
<IconButton aria-label="close" onClick={onClose} sx={COMMON_STYLES.DIALOG_CLOSE_BUTTON}>
<Icon>close</Icon>
</IconButton>
<DialogContent sx={COMMON_STYLES.SCROLL}>
<Box component="section" p={1}>
<CustomInputField
elementCode="clrRules"
elementValue={hasValue(colorRules.selectedColorRule.id) && colorRules.length !== 0 ? colorRules.selectedColorRule.id : -1}
labelText="Заливка событий*"
items={colorRules.rules.reduce(
(prev, cur) => [
...prev,
{
id: cur.id,
caption:
`${cur.SDP_NAME}` +
(cur.STYPE == "string"
? `${cur.fromValue ? `, значение "${cur.fromValue}"` : ""}`
: `${cur.fromValue ? `, с ${cur.fromValue}` : ""}` + `${cur.toValue ? `, по ${cur.toValue}` : ""}`) +
`${cur.SCOLOR ? `, ${cur.SCOLOR}` : ""}`
}
],
[]
)}
emptyItem={{ key: -1, id: -1, caption: "Нет" }}
onChange={handleColorRuleChange}
sx={STYLES.SELECT}
/>
</Box>
<Box component="section" p={1}>
<Stack direction="row" sx={COMMON_STYLES.STACK_DOCLINKS}>
<CustomInputField
elementCode="attr"
elementValue={statusesState.attr}
labelText="Порядок сортировки колонок"
items={sortAttrs.reduce((prev, cur) => [...prev, { id: cur.id, caption: cur.descr }], [])}
onChange={handleSortAttrChange}
sx={STYLES.SELECT}
/>
<IconButton
title={statusesState.direction === "asc" ? "По возрастанию" : "По убыванию"}
onClick={() => handleSortDestChange(sortDest[sortDest.indexOf(statusesState.direction) * -1])}
>
<Icon>{statusesState.direction === "asc" ? "arrow_upward" : "arrow_downward"}</Icon>
</IconButton>
</Stack>
</Box>
<Typography variant={"caption"}>
*Поддерживаются правила заливки, базирующиеся на дополнительных свойствах типа &quot;Строка&quot; или &quot;Число&quot;, из
профиля пользователя, настроенного для раздела &quot;События&quot; в WEB-интерфейсе данного приложения.
</Typography>
</DialogContent>
<DialogActions sx={COMMON_STYLES.DIALOG_ACTIONS}>
<Button
variant="text"
onClick={() => {
onSettingsChange(colorRules, statusesState);
onClose();
}}
>
ОК
</Button>
<Button
variant="text"
onClick={() => {
seColorRules(pv => ({ ...pv, selectedColorRule: {} }));
setStatusesState(pv => ({ ...pv, attr: "SEVNSTAT_NAME", direction: "asc" }));
}}
>
Очистить
</Button>
<Button variant="text" onClick={onClose}>
Отмена
</Button>
</DialogActions>
</Dialog>
</div>
);
};
//Контроль свойств компонента - Диалог дополнительных настроек
SettingsDialog.propTypes = {
initial: PropTypes.object.isRequired,
onSettingsChange: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};
//--------------------
//Интерфейс компонента
//--------------------
export { SettingsDialog };

View File

@ -0,0 +1,166 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Карточка статуса событий
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Card, CardHeader, CardContent, IconButton, Icon, Typography, Stack } from "@mui/material"; //Интерфейсные компоненты
import { TaskCard } from "./task_card.js"; //Компонент Карточка события
import { StatusCardSettings } from "./status_card_settings.js"; //Компонент Диалог настройки карточки событий
import { APP_STYLES } from "../../../../app.styles"; //Типовые стили
import { COLORS } from "../layouts.js"; //Цвета статусов
import { APP_BAR_HEIGHT } from "../../../components/p8p_app_workspace"; //Заголовок страницы
//---------
//Константы
//---------
//Нижний отступ заголовка
const TITLE_PADDING_BOTTOM = "16px";
//Высота фильтра
const FILTER_HEIGHT = "56px";
//Стили
const STYLES = {
STATUS_BLOCK: statusColor => {
return {
width: "350px",
height: `calc(100vh - ${APP_BAR_HEIGHT} - ${TITLE_PADDING_BOTTOM} - ${FILTER_HEIGHT} - 8px)`,
backgroundColor: statusColor,
padding: "8px"
};
},
BLOCK_OPACITY: isAvailable => {
return isAvailable ? { opacity: 1 } : { opacity: 0.5 };
},
CARD_HEADER_TITLE: {
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"
},
CARD_HEADER: { padding: 0 },
CARD_CONTENT: {
padding: 0,
paddingRight: "5px",
paddingBottom: "5px !important",
overflowY: "auto",
maxHeight: `calc(100vh - ${APP_BAR_HEIGHT} - ${TITLE_PADDING_BOTTOM} - ${FILTER_HEIGHT} - 55px)`,
...APP_STYLES.SCROLL
}
};
//---------------
//Тело компонента
//---------------
//Карточка статуса события
const StatusCard = ({
tasks,
status,
statusTitle,
colorRules,
extraData,
isCardAvailable,
onTasksReload,
onNoteDialogOpen,
onStatusColorChange,
placeholder
}) => {
//Состояние диалога настройки
const [statusCardSettingsOpen, setStatusCardSettingsOpen] = useState(false);
//Открыть/закрыть диалог настройки
const handleStatusCardSettingsOpen = () => setStatusCardSettingsOpen(!statusCardSettingsOpen);
//При изменении цвета статуса
const handleStatusColorChange = newColor => {
onStatusColorChange(status, newColor);
};
//Генерация содержимого
return (
<div>
{statusCardSettingsOpen ? (
<StatusCardSettings
statusColor={status.color}
availableColors={COLORS.includes(status.color) ? COLORS : [status.color, ...COLORS]}
onClose={handleStatusCardSettingsOpen}
onColorChange={handleStatusColorChange}
/>
) : null}
<Card
className="statusId-card"
sx={{
...STYLES.STATUS_BLOCK(status.color),
...STYLES.BLOCK_OPACITY(isCardAvailable(status.SEVNSTAT_CODE))
}}
>
<CardHeader
action={
<IconButton aria-label="settings" onClick={handleStatusCardSettingsOpen}>
<Icon>more_vert</Icon>
</IconButton>
}
title={
<Typography sx={STYLES.CARD_HEADER_TITLE} title={statusTitle} variant="h5">
{statusTitle}
</Typography>
}
sx={STYLES.CARD_HEADER}
/>
<CardContent sx={STYLES.CARD_CONTENT}>
<Stack spacing={1}>
{tasks.rows
.filter(item => item.sStatus === status.SEVNSTAT_NAME)
.map((item, index) => (
<TaskCard
task={item}
index={index}
onTasksReload={onTasksReload}
key={item.id}
colorRule={colorRules.selectedColorRule}
pointSettings={extraData.evPoints.find(p => p.SEVPOINT === status.SEVNSTAT_CODE)}
onOpenNoteDialog={onNoteDialogOpen}
/>
))}
{placeholder}
</Stack>
</CardContent>
</Card>
</div>
);
};
//Контроль свойств - Карточка статуса события
StatusCard.propTypes = {
tasks: PropTypes.object.isRequired,
status: PropTypes.object.isRequired,
statusTitle: PropTypes.string.isRequired,
colorRules: PropTypes.object.isRequired,
extraData: PropTypes.object.isRequired,
isCardAvailable: PropTypes.func.isRequired,
onTasksReload: PropTypes.func.isRequired,
onNoteDialogOpen: PropTypes.func.isRequired,
onStatusColorChange: PropTypes.func.isRequired,
placeholder: PropTypes.object.isRequired
};
//--------------------
//Интерфейс компонента
//--------------------
export { StatusCard };

View File

@ -0,0 +1,109 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Диалог настройки карточки статуса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Dialog, DialogTitle, IconButton, Icon, DialogContent, DialogActions, Button, Box, MenuItem, Typography } from "@mui/material"; //Интерфейсные компоненты
import { CustomInputField } from "./custom_input_field.js"; //Кастомное поле ввода
import { COMMON_STYLES } from "../styles"; //Общие стили
//---------
//Константы
//---------
//Стили
const STYLES = {
BCKG_COLOR: backgroundColor => ({ backgroundColor: backgroundColor })
};
//--------------------------------
//Вспомогательные классы и функции
//--------------------------------
//Генерация элемента меню
const menuItemRender = ({ item, key }) => {
//Генерация содержимого
return (
<MenuItem key={key} value={item.id} sx={STYLES.BCKG_COLOR(item.caption)}>
<Typography variant="inherit" noWrap title={item.caption} component="div">
{item.caption}
</Typography>
</MenuItem>
);
};
//---------------
//Тело компонента
//---------------
//Диалог настройки карточки статуса
const StatusCardSettings = ({ statusColor, availableColors, onClose, onColorChange }) => {
//Состояние индекса текущего цвета
const [colorIndex, setColorIndex] = useState(availableColors.indexOf(statusColor));
//При закрытии диалога с применением настройки статуса
const handleDialogOk = () => {
//Изменяем цвет статуса
onColorChange(availableColors[colorIndex]);
//Закрываем диалог
onClose();
};
//При изменении значения элемента
const handleSettingsItemChange = (item, value) => {
setColorIndex(value);
};
//Генерация содержимого
return (
<div>
<Dialog open onClose={onClose} fullWidth maxWidth="sm">
<DialogTitle>Настройки</DialogTitle>
<IconButton aria-label="close" onClick={onClose} sx={COMMON_STYLES.DIALOG_CLOSE_BUTTON}>
<Icon>close</Icon>
</IconButton>
<DialogContent>
<Box component="section" p={1}>
<CustomInputField
elementCode="color"
elementValue={colorIndex}
labelText="Цвет"
items={availableColors.reduce((prev, cur) => [...prev, { id: prev.length, caption: cur }], [])}
onChange={handleSettingsItemChange}
sx={STYLES.BCKG_COLOR(availableColors[colorIndex])}
menuItemRender={menuItemRender}
/>
</Box>
</DialogContent>
<DialogActions sx={COMMON_STYLES.DIALOG_ACTIONS}>
<Button variant="text" onClick={handleDialogOk}>
Применить
</Button>
<Button variant="text" onClick={onClose}>
Отмена
</Button>
</DialogActions>
</Dialog>
</div>
);
};
//Контроль свойств - Диалог настройки карточки статуса
StatusCardSettings.propTypes = {
statusColor: PropTypes.string.isRequired,
availableColors: PropTypes.arrayOf(PropTypes.string).isRequired,
onClose: PropTypes.func.isRequired,
onColorChange: PropTypes.func.isRequired
};
//--------------------
//Интерфейс компонента
//--------------------
export { StatusCardSettings };

View File

@ -0,0 +1,376 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Карточка события
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext, useCallback, useEffect } 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 { TASK_COLORS, getTaskExpiredColor, getTaskBgColorByRule, makeCardActionsArray } from "../layouts"; //Дополнительная разметка и вёрстка клиентских элементов
import { useDictionary } from "../hooks/dict_hooks"; //Состояние открытия разделов
import { useTasksFunctions } from "../hooks/tasks_hooks"; //Состояние вспомогательных функций событий
//---------
//Константы
//---------
//Стили
const STYLES = {
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: { padding: 0, cursor: "pointer" },
CARD_CONTENT: { padding: "4px !important" },
CARD_CONTENT_BOX: { display: "flex", alignItems: "center" },
STACK_SENDER: { alignItems: "center", marginLeft: "auto" },
TYPOGRAPHY_SECONDARY: {
color: "text.secondary",
fontSize: 14
},
ICON_COLOR: linked => {
return { color: theme => (linked ? TASK_COLORS.LINKED : theme.palette.grey[500]) };
}
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Действия карточки события
const CardActions = ({
taskRn,
menuItems,
cardActions,
onMethodsMenuButtonClick,
onMethodsMenuClose,
onTasksReload,
pointSettings,
onOpenNoteDialog
}) => {
//При нажатии на действие меню
const handleActionClick = action => {
//Выполняем действие
action.func({
nEvent: taskRn,
onReload: action.tasksReload ? () => onTasksReload(action.needAccountsReload) : null,
onNoteOpen: pointSettings.ADDNOTE_ONSEND ? onOpenNoteDialog : null
});
//Закрываем меню действий
onMethodsMenuClose();
};
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={() => handleActionClick(action)}
>
<Icon>{action.icon}</Icon>
<Typography pl={1}>{action.name}</Typography>
</MenuItem>
);
})}
</Menu>
</Box>
);
};
//Контроль свойств - Действия карточки события
CardActions.propTypes = {
taskRn: PropTypes.number.isRequired,
menuItems: PropTypes.array.isRequired,
cardActions: PropTypes.object.isRequired,
onMethodsMenuButtonClick: PropTypes.func.isRequired,
onMethodsMenuClose: PropTypes.func.isRequired,
onTasksReload: PropTypes.func,
pointSettings: PropTypes.object,
onOpenNoteDialog: PropTypes.func
};
//-----------
//Тело модуля
//-----------
//Карточка события
const TaskCard = ({ task, index, onTasksReload, colorRule, pointSettings, onOpenNoteDialog }) => {
//Состояние диалога события
const [taskDialogOpen, setTaskDialogOpen] = useState(false);
//Состояние действий события
const [cardActions, setCardActions] = useState({ anchorMenuMethods: null, openMethods: false });
//Состояние списка действий меню
const [menuItems, setMenuItems] = useState([]);
//Вспомогательные функции открытия раздела
const { handleClientEventsOpen, handleClientEventsNotesOpen, handleFileLinksOpen } = useDictionary();
//Состояние вспомогательных функций событий
const { handleTaskStateChange, handleTaskSend } = useTasksFunctions();
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Подключение к контексту сообщений
const { showMsgWarn } = useContext(MessagingСtx);
//Подключение к контексту приложения
const { pOnlineShowDocument } = useContext(ApplicationСtx);
//По нажатию на открытие меню действий
const handleMethodsMenuButtonClick = useCallback(event => {
setCardActions(pv => ({ ...pv, anchorMenuMethods: event.currentTarget, openMethods: true }));
}, []);
//При закрытии меню
const handleMethodsMenuClose = useCallback(() => {
setCardActions(pv => ({ ...pv, anchorMenuMethods: null, openMethods: false }));
}, []);
//При удалении контрагента
const handleTaskDelete = useCallback(
async ({ nEvent, onReload }) => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_DELETE",
args: { NCLNEVENTS: nEvent }
});
//Если требуется перезагрузить данные
onReload ? onReload() : null;
},
[executeStored]
);
//При возврате в предыдущую точку события
const handleTaskReturn = useCallback(
async ({ nEvent, onReload }) => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_RETURN",
args: { NCLNEVENTS: nEvent }
});
//Если требуется перезагрузить данные
onReload ? onReload() : null;
},
[executeStored]
);
//По нажатию действия "Направить"
const handleSendAction = useCallback(
async ({ nEvent, onReload, onNoteOpen }) => {
//Выполняем направление события
handleTaskSend({ nEvent, onReload, onNoteOpen });
},
[handleTaskSend]
);
//По нажатия действия "Редактировать"
const handleTaskEditAction = useCallback(() => {
setTaskDialogOpen(true);
}, []);
//По нажатия действия "Редактировать в разделе"
const handleTaskEditClientAction = useCallback(
async ({ nEvent }) => {
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_SELECT",
args: {
NCLNEVENTS: nEvent
}
});
if (data.NIDENT) {
//Открываем раздел "События" с фильтром по записи
handleClientEventsOpen({ nIdent: data.NIDENT });
}
},
[executeStored, handleClientEventsOpen]
);
//По нажатию действия "Удалить"
const handleTaskDeleteAction = useCallback(
({ nEvent, onReload }) => {
showMsgWarn("Удалить событие?", () => handleTaskDelete({ nEvent, onReload }));
},
[handleTaskDelete, showMsgWarn]
);
//По нажатию действия "Выполнить возврат"
const handleTaskReturnAction = useCallback(
({ nEvent, onReload }) => {
showMsgWarn("Выполнить возврат события в предыдущую точку?", () => handleTaskReturn({ nEvent, onReload }));
},
[handleTaskReturn, showMsgWarn]
);
//По нажатию действия "Примечания"
const handleEventNotesOpenAction = useCallback(
({ nEvent }) => {
handleClientEventsNotesOpen({ nPrn: nEvent });
},
[handleClientEventsNotesOpen]
);
//По нажатию действия "Присоединенные документы"
const handleTaskFileLinksOpenAction = useCallback(
({ nEvent }) => {
handleFileLinksOpen({ nPrn: nEvent, sUnitCode: "ClientEvents" });
},
[handleFileLinksOpen]
);
//По нажатию действия "Перейти"
const handleStateChangeAction = useCallback(
async ({ nEvent, onReload, onNoteOpen }) => {
//Выполняем изменения статуса события
handleTaskStateChange({ nEvent, onReload, onNoteOpen });
},
[handleTaskStateChange]
);
//При изменении ссылок в меню действий (для того, чтобы ссылка на объект менялась при реальной необходимости)
useEffect(() => {
//Устанавливаем список меню
setMenuItems(
makeCardActionsArray(
handleTaskEditAction,
handleTaskEditClientAction,
handleTaskDeleteAction,
handleStateChangeAction,
handleTaskReturnAction,
handleSendAction,
handleEventNotesOpenAction,
handleTaskFileLinksOpenAction
)
);
}, [
handleEventNotesOpenAction,
handleTaskFileLinksOpenAction,
handleSendAction,
handleStateChangeAction,
handleTaskDeleteAction,
handleTaskEditAction,
handleTaskEditClientAction,
handleTaskReturnAction
]);
//Генерация содержимого
return (
<Box>
{taskDialogOpen ? (
<TaskDialog
taskRn={task.nRn}
taskType={task.sType}
editable={pointSettings.BAN_UPDATE ? false : true}
onTasksReload={onTasksReload}
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(getTaskExpiredColor(task), colorRule.SCOLOR ? getTaskBgColorByRule(task, colorRule) : null)}
>
<CardHeader
title={
<Typography
className="task-info"
sx={STYLES.CARD_HEADER_TITLE}
lang="ru"
onClick={() => {
menuItems.find(action =>
action.method === "EDIT" ? action.func(task.nRn, action.tasksReload ? onTasksReload : null) : null
);
}}
>
{task.sDescription}
</Typography>
}
sx={STYLES.CARD_HEADER}
action={
<CardActions
taskRn={task.nRn}
menuItems={menuItems}
cardActions={cardActions}
onMethodsMenuButtonClick={handleMethodsMenuButtonClick}
onMethodsMenuClose={handleMethodsMenuClose}
onTasksReload={onTasksReload}
pointSettings={pointSettings}
onOpenNoteDialog={onOpenNoteDialog}
/>
}
/>
<CardContent sx={STYLES.CARD_CONTENT}>
<Box sx={STYLES.CARD_CONTENT_BOX}>
<IconButton
title={task.nLinkedRn ? "Событие получено по статусной модели" : null}
onClick={
task.nLinkedRn ? () => pOnlineShowDocument({ unitCode: task.sLinkedUnit, document: task.nLinkedRn }) : null
}
sx={STYLES.ICON_COLOR(task.nLinkedRn)}
disabled={!task.nLinkedRn}
>
<Icon>assignment</Icon>
</IconButton>
<Typography sx={STYLES.TYPOGRAPHY_SECONDARY}>{task.name}</Typography>
{task.sSender ? (
<Stack direction="row" spacing={0.5} sx={STYLES.STACK_SENDER}>
<Typography sx={STYLES.TYPOGRAPHY_SECONDARY}>{task.sSender}</Typography>
<Avatar src={task.avatar ? `data:image/png;base64,${task.avatar}` : null} />
</Stack>
) : null}
</Box>
</CardContent>
</Card>
)}
</Draggable>
</Box>
);
};
//Контроль свойств - Карточка события
TaskCard.propTypes = {
task: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
onTasksReload: PropTypes.func,
colorRule: PropTypes.object,
pointSettings: PropTypes.object,
onOpenNoteDialog: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { TaskCard };

View File

@ -0,0 +1,158 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент панели: Форма события
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useEffect, useCallback } 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 { hasValue } from "../../../core/utils";
//---------
//Константы
//---------
//Стили
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, onTaskChange, editable, onEventNextNumbGet, onDPReady }) => {
//Состояние вкладки
const [tab, setTab] = useState(0);
//Состояние допустимых дополнительных свойств
const [docProps] = useDocsProps(taskType);
//При изменении вкладки
const handleTabChange = (e, newValue) => {
setTab(newValue);
};
//При изменении поля
const handleFieldEdit = useCallback(
e => {
onTaskChange({
[e.target.id]: e.target.value,
//Связанные значения, если меняется одно, то необходимо обнулить другое
...(e.target.id === "sClntClnperson" ? { sClntClients: "" } : {}),
...(e.target.id === "sClntClients" ? { sClntClnperson: "" } : {})
});
},
[onTaskChange]
);
//При изменении доп. свойства
const handlePropEdit = useCallback(
(docProp, value) => {
onTaskChange({ docProps: { ...task.docProps, [docProp]: value } });
},
[onTaskChange, task.docProps]
);
//Проверка заполненности всех обязательных доп. свойств
useEffect(() => {
//Считываем количество незаполненных обязательных свойств
let notFilled = docProps.props.filter(docProp => docProp.BREQUIRE === true && !hasValue(task.docProps[docProp.SFORMATTED_ID])).length;
//Если доп. свойства загрузились и количество незаполненных = 0 - доп. свойства готовы, иначе не готовы
docProps.loaded && notFilled === 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,
onTaskChange: PropTypes.func.isRequired,
editable: PropTypes.bool.isRequired,
onEventNextNumbGet: PropTypes.func.isRequired,
onDPReady: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskForm };

View File

@ -0,0 +1,155 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Вкладка информации об исполнителе
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, TextField } from "@mui/material"; //Интерфейсные компоненты
import { getInputProps } from "./task_form"; //Формирование кнопки доступа к разделу
import dayjs from "dayjs"; //Работа с датами
import customParseFormat from "dayjs/plugin/customParseFormat"; //Настройка пользовательского формата даты
import { COMMON_STYLES } from "../styles"; //Общие стили
import { useDictionary } from "../hooks/dict_hooks"; //Состояние открытия разделов
//---------
//Константы
//---------
//Стили
const STYLES = {
BOX_LEFT_ALIGN: { display: "flex", justifyContent: "flex-start" }
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Подключение настройки пользовательского формата даты
dayjs.extend(customParseFormat);
//-----------
//Тело модуля
//-----------
//Вкладка информации об исполнителе
const TaskFormTabExecutor = ({ task, onFieldEdit }) => {
//Вспомогательные функции открытия раздела
const { handleClientPersonOpen } = useDictionary();
//При изменении сотрудника-инициатора
const handleInitClnpersonChange = () =>
handleClientPersonOpen({
sCode: task.sInitClnperson,
callBack: res => {
onFieldEdit({
target: {
id: "sInitClnperson",
value: res.outParameters.out_CODE
}
});
}
});
//Генерация содержимого
return (
<Box>
<Box sx={{ ...COMMON_STYLES.BOX_WITH_LEGEND, ...STYLES.BOX_LEFT_ALIGN }} component="fieldset">
<legend style={COMMON_STYLES.LEGEND}>Планирование</legend>
<TextField
id="dPlanDate"
label="Начало работ"
InputLabelProps={{ shrink: true }}
type="datetime-local"
variant="standard"
value={task.dPlanDate ? dayjs(task.dPlanDate, "DD.MM.YYYY HH:mm").format("YYYY-MM-DD HH:mm") : ""}
onChange={onFieldEdit}
disabled={task.isUpdate}
></TextField>
</Box>
<Box sx={{ ...COMMON_STYLES.BOX_WITH_LEGEND, ...COMMON_STYLES.BOX_SINGLE_COLUMN }} component="fieldset">
<legend style={COMMON_STYLES.LEGEND}>Инициатор</legend>
<TextField
id="sInitClnperson"
label="Сотрудник"
value={task.sInitClnperson}
variant="standard"
onChange={onFieldEdit}
disabled={task.isUpdate}
InputProps={getInputProps(() => handleInitClnpersonChange(), task.isUpdate)}
></TextField>
<TextField id="sInitUser" label="Пользователь" value={task.sInitUser} variant="standard" onChange={onFieldEdit} disabled></TextField>
<TextField
id="sInitReason"
label="Основание"
value={task.sInitReason}
variant="standard"
onChange={onFieldEdit}
disabled={task.isUpdate}
></TextField>
</Box>
<Box sx={{ ...COMMON_STYLES.BOX_WITH_LEGEND, ...COMMON_STYLES.BOX_SINGLE_COLUMN }} component="fieldset">
<legend style={COMMON_STYLES.LEGEND}>Направить</legend>
<TextField id="sToCompany" label="Организация" value={task.sToCompany} variant="standard" onChange={onFieldEdit} disabled></TextField>
<TextField
id="sToDepartment"
label="Подразделение"
value={task.sToDepartment}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField id="sToClnpost" label="Должность" value={task.sToClnpost} variant="standard" onChange={onFieldEdit} disabled></TextField>
<TextField
id="sToClnpsdep"
label="Штатная должность"
value={task.sToClnpsdep}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField
id="sToClnperson"
label="Сотрудник"
value={task.sToClnperson}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField
id="sToFcstaffgrp"
label="Нештатная должность"
value={task.sToFcstaffgrp}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
<TextField id="sToUser" label="Пользователь" value={task.sToUser} variant="standard" onChange={onFieldEdit} disabled></TextField>
<TextField
id="sToUsergrp"
label="Группа пользователей"
value={task.sToUsergrp}
variant="standard"
onChange={onFieldEdit}
disabled
></TextField>
</Box>
</Box>
);
};
//Контроль свойств - Вкладка информации об исполнителе
TaskFormTabExecutor.propTypes = {
task: PropTypes.object.isRequired,
onFieldEdit: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskFormTabExecutor };

View File

@ -0,0 +1,192 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Вкладка основной информации
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, TextField } from "@mui/material"; //Интерфейсные компоненты
import { getInputProps } from "./task_form"; //Формирование кнопки доступа к разделу
import { COMMON_STYLES } from "../styles"; //Общие стили
import { useDictionary } from "../hooks/dict_hooks"; //Состояние открытия разделов
//---------
//Константы
//---------
//Стили
const STYLES = {
BOX_FEW_COLUMNS: { display: "flex", flexWrap: "wrap", justifyContent: "space-between" }
};
//-----------
//Тело модуля
//-----------
//Вкладка основной информации
const TaskFormTabInfo = ({ task, editable, onFieldEdit, onEventNextNumbGet }) => {
//Вспомогательные функции открытия раздела
const { handleClientPersonOpen, handleCatalogTreeOpen, handleClientClientsOpen } = useDictionary();
//При изменении каталога
const handleCrnChange = () =>
handleCatalogTreeOpen({
sUnitName: "ClientEvents",
sName: task.sCrn,
callBack: res => {
onFieldEdit({
target: {
id: "sCrn",
value: res.outParameters.out_NAME
}
});
}
});
//При изменении клиента-сотрудника
const handleClntClnpersonChange = () =>
handleClientPersonOpen({
sCode: task.sClntClnperson,
callBack: res => {
onFieldEdit({
target: {
id: "sClntClnperson",
value: res.outParameters.out_CODE
}
});
}
});
//При изменении клиента-организации
const handleClntClientsChange = () =>
handleClientClientsOpen({
sCode: task.sClntClients,
callBack: res => {
onFieldEdit({
target: {
id: "sClntClients",
value: res.outParameters.out_CLIENT_CODE
}
});
}
});
//Генерация содержимого
return (
<Box>
<Box sx={COMMON_STYLES.BOX_WITH_LEGEND} component="fieldset">
<legend style={COMMON_STYLES.LEGEND}>Событие</legend>
<Box sx={STYLES.BOX_FEW_COLUMNS}>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD()}
id="sCrn"
label="Каталог"
fullWidth
value={task.sCrn}
variant="standard"
onChange={onFieldEdit}
InputProps={getInputProps(handleCrnChange)}
required
disabled={task.isUpdate}
/>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD("225px")}
id="sPrefix"
label="Префикс"
value={task.sPrefix}
variant="standard"
onChange={onFieldEdit}
required
disabled={task.isUpdate}
></TextField>
<TextField
sx={COMMON_STYLES.TASK_FORM_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={COMMON_STYLES.TASK_FORM_TEXT_FIELD("225px", !task.isUpdate)}
id="sType"
label="Тип"
value={task.sType}
variant="standard"
onChange={onFieldEdit}
disabled
required
></TextField>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD("225px", !task.isUpdate)}
id="sStatus"
label="Статус"
value={task.sStatus}
variant="standard"
disabled
required
onChange={onFieldEdit}
></TextField>
<TextField
sx={COMMON_STYLES.TASK_FORM_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={{ ...COMMON_STYLES.BOX_WITH_LEGEND, ...COMMON_STYLES.BOX_SINGLE_COLUMN }} component="fieldset">
<legend style={COMMON_STYLES.LEGEND}>Клиент</legend>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD()}
id="sClntClients"
label="Организация"
value={task.sClntClients}
variant="standard"
onChange={onFieldEdit}
disabled={!task.sType}
InputProps={getInputProps(() => handleClntClientsChange(), !task.sType)}
></TextField>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD()}
id="sClntClnperson"
label="Сотрудник"
value={task.sClntClnperson}
variant="standard"
onChange={onFieldEdit}
disabled={!task.sType}
InputProps={getInputProps(() => handleClntClnpersonChange(), !task.sType)}
></TextField>
</Box>
</Box>
);
};
//Контроль свойств - Вкладка основной информации
TaskFormTabInfo.propTypes = {
task: PropTypes.object.isRequired,
editable: PropTypes.bool.isRequired,
onFieldEdit: PropTypes.func.isRequired,
onEventNextNumbGet: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskFormTabInfo };

View File

@ -0,0 +1,169 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Вкладка информации со свойствами
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, TextField } from "@mui/material"; //Интерфейсные компоненты
import { getInputProps } from "./task_form"; //Формирование кнопки доступа к разделу
import dayjs from "dayjs"; //Работа с датами
import customParseFormat from "dayjs/plugin/customParseFormat"; //Настройка пользовательского формата даты
import { DP_DEFAULT_VALUE, DP_IN_VALUE, DP_RETURN_VALUE, validationError, formatSqlDate } from "../layouts"; //Дополнительная разметка и вёрстка клиентских элементов
import { COMMON_STYLES } from "../styles"; //Общие стили
import { useDictionary } from "../hooks/dict_hooks"; //Состояние открытия разделов
//---------
//Константы
//---------
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Подключение настройки пользовательского формата даты
dayjs.extend(customParseFormat);
//-----------
//Тело модуля
//-----------
//Вкладка информации со свойствами
const TaskFormTabProps = ({ task, docProps, onPropEdit }) => {
//Вспомогательные функции открытия раздела
const { handleExtraDictionariesOpen, handleUnitOpen } = useDictionary();
//Выбор из словаря или дополнительного словаря
const handleDictOpen = async (docProp, curValue = null) => {
//Если способ выбора - словарь
docProp.NENTRY_TYPE === 1
? handleUnitOpen({
sUnitCode: docProp.SUNITCODE,
sShowMethod: docProp.SMETHOD_CODE,
inputParameters: docProp.NPARAM_RN ? [{ name: docProp.SPARAM_IN_CODE, value: curValue }] : null,
callBack: res => {
onPropEdit(docProp.SFORMATTED_ID, res.outParameters[docProp.SPARAM_OUT_CODE]);
}
})
: //Если способ выбора - доп. словарь
handleExtraDictionariesOpen({
nRn: docProp.NEXTRA_DICT_RN,
sParamName: DP_IN_VALUE[docProp.NFORMAT],
paramValue: curValue,
callBack: res => {
onPropEdit(docProp.SFORMATTED_ID, res.outParameters[DP_RETURN_VALUE[docProp.NFORMAT]]);
}
});
};
//Инициализация дополнительного свойства
const initPropValue = prop => {
//Считываем значение свойства из события
const value = task.docProps[prop.SFORMATTED_ID];
//Если есть значение свойства
if (value) {
//Строка или число
if (prop.NFORMAT < 2) {
return prop.NNUM_PRECISION ? String(value).replace(".", ",") : value;
}
//Дата
if (prop.NFORMAT === 2) {
//Возвращаем значение исходя из подтипа даты
switch (prop.NDATA_SUBTYPE) {
//Дата без времени
case 0:
return dayjs(value).format("YYYY-MM-DD");
//Дата и время без секунд
case 1:
return dayjs(value).format("YYYY-MM-DD HH:mm");
//Дата и время с секундами
default:
return dayjs(value).format("YYYY-MM-DD HH:mm:ss");
}
}
//Если это ничего из вышестоящего - время
return formatSqlDate(value);
}
//Если нет значения, но это изменение события
if (task.nRn) {
//Возвращаем пустоту
return "";
}
//Если нет значения и это добавление события - возвращаем значение по умолчанию
return prop[DP_DEFAULT_VALUE[prop.NFORMAT]];
};
//Генерация содержимого
return (
<Box>
<Box sx={COMMON_STYLES.BOX_WITH_LEGEND} component="fieldset">
{docProps.props.map((docProp, index) => {
return docProp.BSHOW_IN_GRID ? (
<TextField
error={
!validationError(
task.docProps[docProp.SFORMATTED_ID],
docProp.NFORMAT,
docProp.NNUM_WIDTH,
docProp.NNUM_PRECISION,
docProp.NSTR_WIDTH
)
}
key={index}
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD()}
id={docProp.SFORMATTED_ID}
type={
docProp.NFORMAT < 2
? "string"
: docProp.NFORMAT === 2
? docProp.NDATA_SUBTYPE === 0
? "date"
: "datetime-local"
: "time"
}
label={docProp.SNAME}
fullWidth
value={initPropValue(docProp)}
variant="standard"
onChange={e => onPropEdit(e.target.id, e.target.value)}
inputProps={
(docProp.NFORMAT === 2 && docProp.NDATA_SUBTYPE === 2) || (docProp.NFORMAT === 3 && docProp.NDATA_SUBTYPE === 1)
? { step: 1 }
: {}
}
InputProps={
docProp.NENTRY_TYPE > 0 ? getInputProps(() => handleDictOpen(docProp, task.docProps[docProp.SFORMATTED_ID])) : null
}
InputLabelProps={
docProp.NFORMAT < 2
? {}
: {
shrink: true
}
}
required={docProp.BREQUIRE}
disabled={docProp.BREADONLY}
/>
) : null;
})}
</Box>
</Box>
);
};
//Контроль свойств - Вкладка информации со свойствами
TaskFormTabProps.propTypes = {
task: PropTypes.object.isRequired,
docProps: PropTypes.object.isRequired,
onPropEdit: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskFormTabProps };

View File

@ -0,0 +1,212 @@
/*
Парус 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 { FilterDialog } from "./components/filter_dialog.js"; //Диалог фильтра
import { COMMON_STYLES } from "./styles"; //Общие стили
//---------
//Константы
//---------
//Стили
const STYLES = {
ICON_ORDERS: orders => {
return orders.length > 0 ? { color: "#1976d2" } : {};
},
MENU_ORDER: {
width: "260px"
},
MENU_ITEM_ORDER: {
display: "flex",
justifyContent: "space-between"
},
FILTERS_STACK: {
paddingBottom: "5px",
...COMMON_STYLES.SCROLL
},
STACK_FILTER: { maxWidth: "99vw" }
};
//--------------------------
//Вспомогательные компоненты
//--------------------------
//Элемент меню сортировок
const SortMenuItem = ({ item, caption, orders, onOrderChanged }) => {
//Кнопка сортировки
const order = orders.find(order => order.name == item);
//Генерация содержимого
return (
<MenuItem sx={STYLES.MENU_ITEM_ORDER} 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.MENU_ORDER }}
>
<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,
docLinks,
selectedDocLink,
onFilterChange,
onDocLinksLoad,
onFilterOpen,
onFilterClose,
onTasksReload,
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, docLinks }}
// docLinks={docLinks}
onFilterChange={onFilterChange}
onFilterClose={onFilterClose}
onDocLinksLoad={onDocLinksLoad}
/>
) : null}
<Box {...other}>
<Stack direction="row" spacing={1} p={1} alignItems={"center"} sx={STYLES.STACK_FILTER}>
<IconButton title="Обновить" onClick={onTasksReload}>
<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.sState ? <FilterItem caption={"Состояние"} value={filter.sState} onClick={onFilterOpen} /> : null}
{filter.sType ? <FilterItem caption={"Тип"} value={filter.sType} onClick={onFilterOpen} /> : null}
{filter.sCrnName ? <FilterItem caption={"Каталог"} value={filter.sCrnName} onClick={onFilterOpen} /> : null}
{filter.bSubcatalogs ? <FilterItem caption={"Включая подкаталоги"} onClick={onFilterOpen} /> : null}
{filter.sSendPerson ? <FilterItem caption={"Исполнитель"} value={filter.sSendPerson} onClick={onFilterOpen} /> : null}
{filter.sSendDivision ? <FilterItem caption={"Подразделение"} value={filter.sSendDivision} onClick={onFilterOpen} /> : null}
{filter.sSendUsrGrp ? (
<FilterItem caption={"Группа пользователей"} value={filter.sSendUsrGrp} onClick={onFilterOpen} />
) : null}
{filter.sDocLink && selectedDocLink ? (
<FilterItem caption={"Учётный документ"} value={selectedDocLink.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,
docLinks: PropTypes.arrayOf(PropTypes.object),
selectedDocLink: PropTypes.object,
onFilterChange: PropTypes.func.isRequired,
onDocLinksLoad: PropTypes.func,
onFilterOpen: PropTypes.func.isRequired,
onFilterClose: PropTypes.func.isRequired,
onTasksReload: PropTypes.func.isRequired,
orders: PropTypes.array,
onOrderChanged: PropTypes.func.isRequired
};
//--------------------
//Интерфейс компонента
//--------------------
export { Filter };

View File

@ -0,0 +1,255 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Пользовательские хуки: Хуки открытия разделов
*/
//---------------------
//Подключение библиотек
//---------------------
import { useContext, useCallback } from "react"; //Классы React
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
//-----------
//Тело модуля
//-----------
//Состояние открытия разделов
const useDictionary = () => {
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//Отображение раздела "Сотрудники"
const handleClientPersonOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "ClientPersons",
showMethod: "main",
inputParameters: [{ name: "in_CODE", value: prms.sCode }],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Клиенты"
const handleClientClientsOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "ClientClients",
showMethod: "main",
inputParameters: [{ name: "in_CLIENT_CODE", value: prms.sCode }],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Каталоги"
const handleCatalogTreeOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "CatalogTree",
showMethod: "main",
inputParameters: [
{ name: "in_DOCNAME", value: prms.sUnitName },
{ name: "in_NAME", value: prms.sName }
],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Типы событий"
const handleEventTypesOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "ClientEventTypes",
showMethod: "dictionary",
inputParameters: [{ name: "pos_eventtypecode", value: prms.sCode }],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Контрагенты"
const handleAgnlistOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "AGNLIST",
showMethod: "agents",
inputParameters: [{ name: "pos_agnmnemo", value: prms.sMnemo }],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Штатные подразделения"
const handleInsDepartmentOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "INS_DEPARTMENT",
inputParameters: [{ name: "in_CODE", value: prms.sCode }],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Нештатные структуры"
const handleCostStaffGroupsOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "CostStaffGroups",
inputParameters: [{ name: "in_CODE", value: prms.sCode }],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Дополнительные словари"
const handleExtraDictionariesOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "ExtraDictionaries",
showMethod: "values",
inputParameters: [
{ name: "pos_rn", value: prms.nRn },
{ name: prms.sParamName, value: prms.paramValue }
],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Маршруты событий (исполнители в точках)"
const handleEventRoutesPointExecutersOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "EventRoutesPointExecuters",
showMethod: "executers",
inputParameters: prms.inputParameters,
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "События"
const handleClientEventsOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "ClientEvents",
inputParameters: [{ name: "in_Ident", value: prms.nIdent }]
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "События (примечания)"
const handleClientEventsNotesOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "ClientEventsNotes",
showMethod: "main",
inputParameters: [{ name: "in_PRN", value: prms.nPrn }]
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Присоединенные документы"
const handleFileLinksOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "FileLinks",
showMethod: "main_link",
inputParameters: [
{ name: "in_PRN", value: prms.nPrn },
{ name: "in_UNITCODE", value: prms.sUnitCode }
]
});
},
[pOnlineShowDictionary]
);
//Отображение раздела "Маршруты событий (точки перехода)"
const handleEventRoutesPointsPassessOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: "EventRoutesPointsPasses",
showMethod: "main_passes",
inputParameters: [
{ name: "in_ENVTYPE_CODE", value: prms.sEventType },
{ name: "in_ENVSTAT_CODE", value: prms.sEventStatus },
{ name: "in_POINT", value: prms.nPoint }
],
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
//Универсальное отображение раздела
const handleUnitOpen = useCallback(
async prms => {
pOnlineShowDictionary({
unitCode: prms.sUnitCode,
showMethod: prms.sShowMethod,
inputParameters: prms.inputParameters,
callBack: res => {
res.success ? prms.callBack(res) : null;
}
});
},
[pOnlineShowDictionary]
);
return {
handleClientPersonOpen,
handleClientClientsOpen,
handleCatalogTreeOpen,
handleEventTypesOpen,
handleAgnlistOpen,
handleInsDepartmentOpen,
handleCostStaffGroupsOpen,
handleExtraDictionariesOpen,
handleEventRoutesPointExecutersOpen,
handleClientEventsOpen,
handleClientEventsNotesOpen,
handleFileLinksOpen,
handleEventRoutesPointsPassessOpen,
handleUnitOpen
};
};
//----------------
//Интерфейс модуля
//----------------
export { useDictionary };

View File

@ -0,0 +1,122 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Пользовательские хуки: Хуки фильтра
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useEffect, useCallback } from "react"; //Классы React
import { EVENT_STATES } from "../layouts"; //Перечисление состояний события
import { getLocalStorageValue } from "../layouts"; //Вспомогательные функции
//--------------------------
//Вспомогательные компоненты
//--------------------------
//Проверка возможности загрузки данных фильтра из локального хранилища
const isLocalStorageExists = () => {
return getLocalStorageValue("sType");
};
//-----------
//Тело модуля
//-----------
//Хук фильтра
//const useFilters = filterOpen => {
const useFilters = () => {
//Состояние фильтра
const [filters, setFilters] = useState({
loaded: false,
isSetByUser: !isLocalStorageExists(),
values: {
sState: EVENT_STATES[1],
sType: "",
sCrnName: "",
sCrnRnList: "",
bSubcatalogs: false,
sSendPerson: "",
sSendDivision: "",
sSendUsrGrp: "",
sDocLink: ""
}
});
//Установить значение фильтра
const setFilterValues = useCallback((values, isSetByUser = true) => {
setFilters({ loaded: true, isSetByUser: isSetByUser, values: values });
}, []);
//Загрузка значений фильтра из локального хранилища браузера
const loadLocalStorageValues = useCallback(async () => {
//Загружаем значения по умолчанию
let values = { ...filters.values };
//Обходим ключи объекта значений
for (let key in values) {
//Заполняем значениями из хранилища
switch (key) {
//Локальное хранилище не хранит булево, форматируем строку в булево
case "bSubcatalogs":
values[key] = getLocalStorageValue(key) === "true";
break;
//Не переносим информацию о связанных записях
case "sDocLink":
break;
//Переносим все остальные значения
default:
values[key] = getLocalStorageValue(key, "");
break;
}
}
//Устанавливаем значения фильтра
setFilterValues(values, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
//При изменении значений фильтра
const handleFiltersChange = useCallback(
filters => {
setFilterValues(filters);
},
[setFilterValues]
);
//Сохранение при закрытии панели
useEffect(() => {
//Обработка события закрытия
const onBeforeUnload = () => {
//Обходим ключи фильтра
for (let key in filters.values) {
//Если это не связи - сохраняем значение в хранилище
key !== "sDocLink" ? localStorage.setItem(key, filters.values[key] ? filters.values[key] : "") : null;
}
};
//Если данные были загружены и произошли изменения
if (filters.loaded && filters.isSetByUser) {
//Вешаем обработчик события закрытия
window.addEventListener("beforeunload", onBeforeUnload);
}
//Очищаем при размонтировании
return () => {
window.removeEventListener("beforeunload", onBeforeUnload);
};
}, [filters.loaded, filters.isSetByUser, filters.values]);
//При подключении к странице
useEffect(() => {
//Если требуется загрузить фильтр из локального хранилища
if (!filters.loaded && !filters.isSetByUser) {
loadLocalStorageValues();
}
}, [filters.isSetByUser, filters.loaded, loadLocalStorageValues]);
return [filters, handleFiltersChange];
};
//----------------
//Интерфейс модуля
//----------------
export { useFilters };

View File

@ -0,0 +1,243 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Пользовательские хуки: Хуки основных данных
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import { getRandomColor, getLocalStorageValue } from "../layouts"; //Вспомогательные функции
//-----------
//Тело модуля
//-----------
//Хук дополнительных данных
const useExtraData = filtersType => {
//Состояние дополнительных данных
const [extraData, setExtraData] = useState({
dataLoaded: false,
reload: false,
typeLoaded: "",
evRoutes: [],
evPoints: [],
noteTypes: [],
docLinks: []
});
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Считывание учётных документов
const handleDocLinksLoad = useCallback(
async (type = filtersType) => {
//Считываем данные
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_DOCLINKS_GET",
args: {
SEVNTYPE_CODE: type
},
isArray: name => name === "XDOCLINKS",
respArg: "COUT"
});
//Возвращаем учётные документы
return [...(data?.XDOCLINKS || [])];
},
[executeStored, filtersType]
);
useEffect(() => {
//Загрузка дополнительных данных
const loadExtraData = async () => {
//Считываем данные
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_GET_INFO_BY_CODE",
args: {
SEVNTYPE_CODE: filtersType
},
isArray: name => ["XEVROUTES", "XEVPOINTS", "XNOTETYPES"].includes(name),
respArg: "COUT"
});
//Форматируем типы примечаний под нужный формат
let noteTypes = [...(data?.XNOTETYPES || [])].reduce((prev, cur) => [...prev, cur.SNAME], []);
//Считываем учётные документы
let docLinks = await handleDocLinksLoad(filtersType);
//Обновляем дополнительные данные
setExtraData({
dataLoaded: true,
reload: false,
typeLoaded: filtersType,
evRoutes: [...(data?.XEVROUTES || [])],
evPoints: [...(data?.XEVPOINTS || [])],
noteTypes: [...noteTypes],
docLinks: [...docLinks]
});
};
//Если указан тип событий и необходимо обновить
if (extraData.reload && filtersType) {
//Загружаем дополнительные данные
if (!extraData.typeLoaded || filtersType !== extraData.typeLoaded) {
loadExtraData();
}
}
}, [executeStored, extraData.reload, extraData.typeLoaded, filtersType, handleDocLinksLoad]);
return [extraData, setExtraData, handleDocLinksLoad];
};
//Хук заливок пользовательских настроек
const useColorRules = () => {
//Собственное состояние
const [colorRules, setColorRules] = useState({
loaded: false,
rules: [],
selectedColorRule: JSON.parse(getLocalStorageValue("settingsColorRule")) || {}
});
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При необходимости загрузки заливок
useEffect(() => {
//Считывание пользовательских настроек
let getColorRules = async () => {
//Считываем данные
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_DP_RULES_GET",
isArray: name => name === "XRULES",
respArg: "COUT"
});
//Формируем массив правил заливки пользовательских настроек
let newColorRules = [...(data.XRULES || [])].reduce(
(prev, cur) => [
...prev,
{
id: prev.length,
SFIELD: cur.SFIELD,
SDP_NAME: cur.SDP_NAME,
SCOLOR: cur.SCOLOR,
STYPE: cur.STYPE,
fromValue: cur.NFROM ?? cur.SFROM ?? cur.DFROM,
toValue: cur.NTO ?? cur.STO ?? cur.DTO
}
],
[]
);
//Устанавливаем заливки пользовательских настроек
setColorRules(pv => ({ ...pv, loaded: true, rules: [...newColorRules] }));
};
if (!colorRules.loaded) getColorRules();
}, [colorRules.loaded, executeStored]);
//Сохранение при закрытии панели
useEffect(() => {
//Обработка события закрытия
const onBeforeUnload = () => {
localStorage.setItem("settingsColorRule", JSON.stringify(colorRules.selectedColorRule));
};
//Вешаем обработчик события закрытия
window.addEventListener("beforeunload", onBeforeUnload);
//Очищаем при размонтировании
return () => {
window.removeEventListener("beforeunload", onBeforeUnload);
};
}, [colorRules.selectedColorRule]);
return [colorRules, setColorRules];
};
//Хук статусов событий
const useStatuses = filterType => {
//Собственное состояние статусов
const [statuses, setStatuses] = useState([]);
//Состояние статусов
const [statusesState, setStatusesState] = useState({
sorted: false,
reload: true,
attr: getLocalStorageValue("statusesSortAttr", "SEVNSTAT_NAME"),
direction: getLocalStorageValue("statusesSortDirection", "asc")
});
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При необходимости сортировки статусов
useEffect(() => {
//Сортируем статусы
const sortStatuses = unsortedStatuses => {
//Инициализируем поле сортировки и порядок сортировки
const attr = statusesState.attr;
const direction = statusesState.direction;
//Сортируем
let sortedStatuses = unsortedStatuses.sort((a, b) =>
direction === "asc" ? a[attr].localeCompare(b[attr]) : b[attr].localeCompare(a[attr])
);
//Возвращаем
return sortedStatuses;
};
//Загружаем и сортируем статусы
const loadAndSortStatuses = async filterType => {
//Инициализируем статусы
let newStatuses = [];
//Если требуется перезагрузка
if (statusesState.reload) {
const loadedStatuses = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVNSTATS_LOAD",
args: {
SCLNEVNTYPES: filterType
},
isArray: name => name === "XSTATUS",
respArg: "COUT"
});
//Загружаем статусы и инициализируем цвета
newStatuses = [...(loadedStatuses?.XSTATUS || [])].reduce(
(prev, cur) => [...prev, { ...cur, color: getRandomColor(prev.length + 1) }],
[]
);
} else {
//Загружаем из состояния
newStatuses = [...statuses];
}
//Сортируем, если требуется
newStatuses = !statusesState.sorted ? sortStatuses(newStatuses) : newStatuses;
//Обновляем состояние статусов
setStatuses([...newStatuses]);
//Обновляем информацию о состоянии статусов
setStatusesState(pv => ({ ...pv, sorted: true, reload: false }));
};
//При необходимости изменения сортировки
if (filterType && (statusesState.reload || !statusesState.sorted)) {
//Считываем старые статусы или загружаем новые
loadAndSortStatuses(filterType);
}
}, [executeStored, filterType, statuses, statusesState.attr, statusesState.direction, statusesState.reload, statusesState.sorted]);
//Сохранение при закрытии панели
useEffect(() => {
//Обработка события закрытия
const onBeforeUnload = () => {
localStorage.setItem("statusesSortAttr", statusesState.attr);
localStorage.setItem("statusesSortDirection", statusesState.direction);
};
//Вешаем обработчик события закрытия
window.addEventListener("beforeunload", onBeforeUnload);
//Очищаем при размонтировании
return () => {
window.removeEventListener("beforeunload", onBeforeUnload);
};
}, [statusesState.attr, statusesState.direction]);
return [statuses, statusesState, setStatuses, setStatusesState];
};
//----------------
//Интерфейс модуля
//----------------
export { useExtraData, useColorRules, useStatuses };

View File

@ -0,0 +1,192 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Пользовательские хуки: Хуки диалога события
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect } from "react"; //Классы React
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
//-----------
//Тело модуля
//-----------
//Хук для события
const useClientEvent = (taskRn, taskType = "") => {
//Собственное состояние
const [task, setTask] = useState({
init: true,
nRn: taskRn,
sCrn: "",
sPrefix: "",
sNumber: "",
sType: taskType,
sStatus: "",
sDescription: "",
sClntClients: "",
sClntClnperson: "",
dStartDate: "",
sInitClnperson: "",
sInitUser: "",
sInitReason: "",
sToCompany: "",
sToDepartment: "",
sToClnpost: "",
sToClnpsdep: "",
sToClnperson: "",
sToFcstaffgrp: "",
sToUser: "",
sToUsergrp: "",
sCurrentUser: "",
isUpdate: false,
insertDisabled: true,
updateDisabled: true,
docProps: {}
});
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При инициализации события
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 docProps = Object.keys(data.XEVENT)
.filter(key => key.includes("DP_"))
.reduce((prev, key) => ({ ...prev, [key]: data.XEVENT[key] }), {});
//Устанавливаем информацию о событии
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,
sClntClients: data.XEVENT.SCLIENT_CLIENT,
sClntClnperson: data.XEVENT.SCLIENT_PERSON,
dPlanDate: data.XEVENT.SPLAN_DATE,
sInitClnperson: data.XEVENT.SINIT_PERSON,
sInitUser: data.XEVENT.SINIT_AUTHID,
sInitReason: data.XEVENT.SREASON,
sToCompany: data.XEVENT.SSEND_CLIENT,
sToDepartment: data.XEVENT.SSEND_DIVISION,
sToClnpost: data.XEVENT.SSEND_POST,
sToClnpsdep: data.XEVENT.SSEND_PERFORM,
sToClnperson: data.XEVENT.SSEND_PERSON,
sToFcstaffgrp: data.XEVENT.SSEND_STAFFGRP,
sToUser: data.XEVENT.SSEND_USER_NAME,
sToUsergrp: data.XEVENT.SSEND_USER_GROUP,
sCurrentUser: data.XEVENT.SINIT_AUTHID,
isUpdate: true,
init: false,
docProps: docProps
}));
};
//Инициализация параметров события
readEvent();
} else {
//Считывание изначальных параметров события
const initEvent = async () => {
//Инициализируем параметры события
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_INIT",
args: {
SEVENT_TYPE: task.sType
}
});
//Если есть данные
if (data) {
//Устанавливаем данные по событию
setTask(pv => ({
...pv,
sPrefix: data.SPREF,
sNumber: data.SNUMB,
sStatus: data.SSTATUS,
sCurrentUser: data.SINIT_AUTHNAME,
sInitClnperson: data.SINIT_PERSON,
sInitUser: !data.SINIT_PERSON ? data.SINIT_AUTHNAME : "",
init: false
}));
}
};
//Инициализация изначальных параметров события
initEvent();
}
}
if (!task.init) {
setTask(pv => ({ ...pv, sInitUser: !task.sInitClnperson ? task.sCurrentUser : "" }));
}
}, [executeStored, task.init, task.nRn, task.sType, task.sCurrentUser, task.sInitClnperson, taskRn]);
//Проверка доступности действия
useEffect(() => {
setTask(pv => ({
...pv,
insertDisabled:
!task.sCrn ||
!task.sPrefix ||
!task.sNumber ||
!task.sType ||
!task.sStatus ||
!task.sDescription ||
(!task.sInitClnperson && !task.sInitUser),
updateDisabled: !task.sDescription
}));
}, [task.sCrn, task.sDescription, task.sInitClnperson, task.sInitUser, task.sNumber, task.sPrefix, task.sStatus, task.sType]);
return [task, setTask];
};
//Хук для получения свойств раздела "События"
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 },
isArray: name => name === "XPROPS",
respArg: "COUT"
});
//Устанавливаем доп. свойства
setDocsProps({ loaded: true, props: [...(data?.XPROPS || [])] });
};
//Если доп. свойства не загружены
if (!docProps.loaded) {
//Загружаем доп. свойства
getDocsProps();
}
}, [docProps.loaded, executeStored, taskType]);
return [docProps];
};
//----------------
//Интерфейс модуля
//----------------
export { useClientEvent, useDocsProps };

View File

@ -0,0 +1,451 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Пользовательские хуки: Хуки событий
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useContext, useEffect, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import { object2Base64XML } from "../../../core/utils"; //Вспомогательные функции
import { convertFilterValuesToArray } from "../layouts"; //Вспомогательные функции
import { useDictionary } from "./dict_hooks"; //Состояние открытия разделов
//-----------
//Тело модуля
//-----------
//Хук обработки перехода события
const useTasksFunctions = () => {
//Состояние открытия раздела
const { handleEventRoutesPointExecutersOpen, handleEventRoutesPointsPassessOpen } = useDictionary();
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Выполнение направления события
const handleSendExec = useCallback(
//Выполняем финальное перенаправление события
async ({ mainArgs, onReload = null }) => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_SEND",
args: { ...mainArgs }
});
//Если требуется перезагрузить данные
onReload ? onReload() : null;
},
[executeStored]
);
//При направлении события
const handleSend = useCallback(
async ({ mainArgs, onReload = null, onNoteOpen = null }) => {
//Если требуется добавить примечание
if (onNoteOpen) {
//Открываем примечание с коллбэком на направление события
onNoteOpen(async note => {
//Выполняем изменение статуса
handleSendExec({ mainArgs: { ...mainArgs, SNOTE_HEADER: note.header, SNOTE: note.text }, onReload });
});
} else {
//Выполняем изменение статуса
handleSendExec({ mainArgs, onReload });
}
},
[handleSendExec]
);
//По нажатию действия "Направить"
const handleTaskSend = useCallback(
async ({ nEvent, onReload = null, onNoteOpen = null }) => {
//Выполняем инициализацию параметров
const firstStep = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_SEND",
args: {
NSTEP: 1,
NEVENT: nEvent
}
});
if (firstStep) {
//Открываем раздел "Маршруты событий (исполнители в точках)" для выбора исполнителя
handleEventRoutesPointExecutersOpen({
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: sendPrms => {
//Собираем основные параметры направления события
const mainArgs = {
NIDENT: firstStep.NIDENT,
NSTEP: 2,
NEVENT: nEvent,
SSEND_CLIENT: sendPrms.outParameters.out_CLIENT_CODE,
SSEND_DIVISION: sendPrms.outParameters.out_DIVISION_CODE,
SSEND_POST: sendPrms.outParameters.out_POST_CODE,
SSEND_PERFORM: sendPrms.outParameters.out_POST_IN_DIV_CODE,
SSEND_PERSON: sendPrms.outParameters.out_PERSON_CODE,
SSEND_STAFFGRP: sendPrms.outParameters.out_STAFFGRP_CODE,
SSEND_USER_GROUP: sendPrms.outParameters.out_USER_GROUP_CODE,
SSEND_USER_NAME: sendPrms.outParameters.out_USER_NAME,
NSEND_PREDEFINED_EXEC: sendPrms.outParameters.out_PREDEFINED_EXEC,
NSEND_PREDEFINED_PROC: sendPrms.outParameters.out_PREDEFINED_PROC
};
//Перенаправляем событие
handleSend({ nEvent, mainArgs, onReload, onNoteOpen });
}
});
}
},
[executeStored, handleEventRoutesPointExecutersOpen, handleSend]
);
//Выполнение изменения статуса события
const handleStateChangeExec = useCallback(
async ({ mainArgs, onReload = null }) => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: { ...mainArgs }
});
//Если требуется перезагрузить данные
onReload ? onReload() : null;
},
[executeStored]
);
//При изменении статуса события
const handleStateChange = useCallback(
async ({ mainArgs, onReload = null, onNoteOpen = null }) => {
//Если необходимо добавить примечание
if (onNoteOpen) {
//Открываем примечание с коллбэком на изменение статуса
onNoteOpen(async note => {
//Выполняем изменение статуса
handleStateChangeExec({ mainArgs: { ...mainArgs, SNOTE_HEADER: note.header, SNOTE: note.text }, onReload });
});
} else {
//Выполняем изменение статуса
handleStateChangeExec({ mainArgs, onReload });
}
},
[handleStateChangeExec]
);
//При выборе исполнителя
const handleExecuterSelect = useCallback(
async ({ nEvent, pointInfo, onReload = null, onNoteOpen = null }) => {
//Если требуется выбрать получателя
if (pointInfo.NSELECT_EXEC === 1) {
//Открываем раздел "Маршруты событий (исполнители в точках)" для выбора исполнителя
handleEventRoutesPointExecutersOpen({
inputParameters: [
{ name: "in_IDENT", value: pointInfo.NIDENT },
{ name: "in_EVENT", value: nEvent },
{ name: "in_EVENT_TYPE", value: pointInfo.SEVENT_TYPE },
{ name: "in_EVENT_STAT", value: pointInfo.SEVENT_STAT },
{ name: "in_INIT_PERSON", value: pointInfo.SINIT_PERSON },
{ name: "in_INIT_AUTHNAME", value: pointInfo.SINIT_AUTHNAME },
{ name: "in_CLIENT_CLIENT", value: pointInfo.SCLIENT_CLIENT },
{ name: "in_CLIENT_PERSON", value: pointInfo.SCLIENT_PERSON }
],
callBack: sendPrms => {
const mainArgs = {
NIDENT: pointInfo.NIDENT,
NSTEP: 4,
NEVENT: nEvent,
SEVENT_STAT: pointInfo.SEVENT_STAT,
SSEND_CLIENT: sendPrms.outParameters.out_CLIENT_CODE,
SSEND_DIVISION: sendPrms.outParameters.out_DIVISION_CODE,
SSEND_POST: sendPrms.outParameters.out_POST_CODE,
SSEND_PERFORM: sendPrms.outParameters.out_POST_IN_DIV_CODE,
SSEND_PERSON: sendPrms.outParameters.out_PERSON_CODE,
SSEND_STAFFGRP: sendPrms.outParameters.out_STAFFGRP_CODE,
SSEND_USER_GROUP: sendPrms.outParameters.out_USER_GROUP_CODE,
SSEND_USER_NAME: sendPrms.outParameters.out_USER_NAME,
NSEND_PREDEFINED_EXEC: sendPrms.outParameters.out_PREDEFINED_EXEC,
NSEND_PREDEFINED_PROC: sendPrms.outParameters.out_PREDEFINED_PROC
};
//Выполняем изменение статуса
handleStateChange({ mainArgs, onReload, onNoteOpen });
}
});
} else {
//Общие аргументы
const mainArgs = {
NIDENT: pointInfo.NIDENT,
NSTEP: 4,
NEVENT: nEvent,
SEVENT_STAT: pointInfo.SEVENT_STAT
};
//Выполняем изменение статуса
handleStateChange({ mainArgs, onReload, onNoteOpen });
}
},
[handleEventRoutesPointExecutersOpen, handleStateChange]
);
//При выполнении третьего шага
const handleMakeThirdStep = useCallback(
async ({ nEvent, pointInfo, onReload = null, onNoteOpen = null }) => {
//Выполняем переход на следующий шаг
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: {
NIDENT: pointInfo.NIDENT,
NSTEP: 3,
NPASS: pointInfo.NPASS
}
});
//Выполняем выбор исполнителя
handleExecuterSelect({
nEvent,
pointInfo,
onReload,
onNoteOpen
});
},
[executeStored, handleExecuterSelect]
);
//При выполнении второго шага
const handleMakeSecondStep = useCallback(
async ({ nEvent, pointInfo, onReload = null, onNoteOpen = null }) => {
//Состояние параметров текущего действия
let currentPointInfo = { ...pointInfo };
//Выполняем переход на следующий шаг
const secondStep = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: {
NIDENT: currentPointInfo.NIDENT,
NSTEP: 2,
NPASS: currentPointInfo.NPASS
}
});
//Устанавливаем признак необходимости выбора исполнителя
currentPointInfo.NSELECT_EXEC = secondStep.NSELECT_EXEC;
//Выполняем третий шаг
handleMakeThirdStep({ nEvent, pointInfo: currentPointInfo, onReload, onNoteOpen });
},
[executeStored, handleMakeThirdStep]
);
//При выборе следующей точки события
const handleNextPointSelect = useCallback(
({ nEvent, pointInfo, onReload = null, onNoteOpen = null }) => {
//Состояние параметров текущего действия
let currentPointInfo = { ...pointInfo };
//Открываем раздел "Маршруты событий (точки перехода)" для выбора следующей точки
handleEventRoutesPointsPassessOpen({
sEventType: currentPointInfo.SEVENT_TYPE,
sEventStatus: currentPointInfo.SEVENT_STAT,
nPoint: currentPointInfo.NPOINT,
callBack: async point => {
//Устанавливаем полученную точку перехода
currentPointInfo.NPASS = point.outParameters.out_RN;
currentPointInfo.SEVENT_STAT = point.outParameters.out_NEXT_POINT;
//Выполняем второй шаг
handleMakeSecondStep({ nEvent, pointInfo: currentPointInfo, onReload, onNoteOpen });
}
});
},
[handleEventRoutesPointsPassessOpen, handleMakeSecondStep]
);
//По нажатию действия "Перейти"
const handleTaskStateChange = useCallback(
async ({ nEvent, sNextStat = null, onReload = null, onNoteOpen = null }) => {
//Выполняем инициализацию параметров
const eventInfo = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_STATE_CHANGE",
args: {
NSTEP: 1,
NEVENT: nEvent,
SNEXT_STAT: sNextStat
}
});
//Если информация о события проинициализирована
if (eventInfo) {
//Если следующий статус неопределен
if (!sNextStat) {
//Выполнение перехода с выбором точки
handleNextPointSelect({
nEvent,
pointInfo: eventInfo,
onReload,
onNoteOpen
});
} else {
//Выполняем второй шаг
handleMakeSecondStep({ nEvent, pointInfo: eventInfo, onReload, onNoteOpen });
}
}
},
[executeStored, handleMakeSecondStep, handleNextPointSelect]
);
return { handleTaskStateChange, handleTaskSend };
};
//Хук получения событий
const useTasks = (filterValues, ordersValues) => {
//Состояние событий
const [tasks, setTasks] = useState({
loaded: false,
rows: [],
reload: false,
accountsReload: false,
loadedAccounts: []
});
//Состояние вспомогательных функций событий
const { handleTaskStateChange } = useTasksFunctions();
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//Инициализация параметров события
const initTask = (id, task, avatar = null) => {
//Фильтруем доп. свойства
let newDocProps = Object.keys(task)
.filter(key => key.includes("DP_"))
.reduce((prev, key) => ({ ...prev, [key]: task[key] }), {});
//Возвращаем структуру события
return {
id: id,
avatar: avatar,
name: task.SPREF_NUMB,
nRn: task.NRN,
sCrn: "",
sPrefix: task.SEVPREF,
sNumber: task.SEVNUMB,
sType: task.SEVTYPE_CODE,
sStatus: task.SEVSTAT_NAME,
sDescription: task.SEVDESCR,
sClntClients: "",
sClntClnperson: "",
dchange_date: task.DCHANGE_DATE,
dStartDate: task.DREG_DATE,
dExpireDate: task.DEXPIRE_DATE,
dPlanDate: task.DPLAN_DATE,
sInitClnperson: task.SINIT_PERSON,
sInitUser: "",
sInitReason: "",
sToCompany: "",
sToDepartment: task.SSEND_DIVISION,
sToClnpost: "",
sToClnpsdep: "",
sToClnperson: task.SSEND_PERSON,
sToFcstaffgrp: "",
sToUser: "",
sToUsergrp: task.SSEND_USRGRP,
sSender: task.SSENDER,
sCurrentUser: "",
sLinkedUnit: task.SLINKED_UNIT,
nLinkedRn: task.NLINKED_RN,
docProps: newDocProps
};
};
//Взаимодействие с событием (через перенос)
const onDragEnd = useCallback(
({ path, eventPoints, openNoteDialog, destCode }) => {
//Определяем нужные параметры
const { source, destination } = path;
//Если путь не указан
if (!destination) {
return;
}
//Если происходит изменение статуса
if (destination.droppableId !== source.droppableId) {
//Конвертим ID переносимого события
let nDraggableTaskId = parseInt(path.draggableId);
//Считываем строку, у которой изменяется статус
let task = tasks.rows.find(r => r.id === nDraggableTaskId);
//Изменяем статус у события
task.statusId = parseInt(path.destination.droppableId);
//Получение настройки точки назначения
const pointSettings = eventPoints.find(eventPoint => eventPoint.SEVPOINT === destCode);
//Изменяем статус события с добавлением примечания
handleTaskStateChange({
nEvent: task.nRn,
sNextStat: destCode,
onReload: () => setTasks(pv => ({ ...pv, reload: true, accountsReload: true })),
onNoteOpen: pointSettings.ADDNOTE_ONCHST ? openNoteDialog : null
});
}
},
[handleTaskStateChange, tasks.rows]
);
//При необходимости перезагрузки данных
useEffect(() => {
//Считывание данных с учетом фильтрации
let getTasks = async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_LOAD",
args: {
CFILTERS: {
VALUE: object2Base64XML(convertFilterValuesToArray(filterValues), { arrayNodeName: "filters" }),
SDATA_TYPE: SERV_DATA_TYPE_CLOB
},
CORDERS: { VALUE: object2Base64XML(ordersValues, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
NINCLUDE_ACCOUNTS: tasks.accountsReload ? 1 : 0
},
isArray: name => name === "XAGENTS",
respArg: "COUT"
});
//Считываем информацию о событиях
let events = data.XCLNEVENTS.XDATA.XDATA_GRID;
//Считываем иноформацию о контрагентах
let accounts = tasks.accountsReload ? [...(data.XAGENTS_WITH_IMG.XAGENTS || [])] : tasks.loadedAccounts;
//Инициализируем события
let newRows = [];
//Если есть события
if (events.rows) {
//Формируем структуру событий
newRows = [...(events.rows || [])].reduce(
(prev, cur) => [...prev, initTask(prev.length, cur, accounts.find(agent => agent.SAGNABBR === cur.SSENDER)?.BIMAGE)],
[]
);
}
//Возвращаем информацию
return { rows: [...newRows], loadedAccounts: accounts };
};
//Считывание данных
let getData = async () => {
//Считываем информацию о задачах
let eventTasks = await getTasks();
//Загружаем данные
setTasks(pv => ({
...pv,
loaded: true,
rows: eventTasks.rows,
loadedAccounts: eventTasks.loadedAccounts,
reload: false,
accountsReload: false
}));
};
//Если необходимо загрузить данные и указан тип событий и загружены все необходимые вспомогательные данные
if (tasks.reload) {
//Загружаем данные
getData();
}
}, [SERV_DATA_TYPE_CLOB, executeStored, filterValues, ordersValues, tasks.accountsReload, tasks.loadedAccounts, tasks.reload]);
return [tasks, setTasks, onDragEnd];
};
//----------------
//Интерфейс модуля
//----------------
export { useTasksFunctions, useTasks };

View File

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

View File

@ -0,0 +1,284 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Дополнительная разметка и вёрстка клиентских элементов
*/
//---------
//Константы
//---------
//Перечисление "Состояние события"
export const EVENT_STATES = Object.freeze({ 0: "Все", 1: "Не аннулированные", 2: "Аннулированные" });
//Допустимые значение поля сортировки
export const sortAttrs = [
{ id: "SEVNSTAT_CODE", descr: "Мнемокод статуса" },
{ id: "SEVNSTAT_NAME", descr: "Наименование статуса" },
{ id: "SEVPOINT_DESCR", 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 TASK_COLORS = Object.freeze({ EXPIRED: "#ff0000", EXPIRES_SOON: "#ffdf00", LINKED: "#1e90ff" });
//Перечисление Доп. свойства "Значение по умолчанию"
export const DP_DEFAULT_VALUE = Object.freeze({ 0: "SDEFAULT_STR", 1: "NDEFAULT_NUM", 2: "DDEFAULT_DATE", 3: "NDEFAULT_NUM" });
//Перечисление Доп. свойства "Префикс формата данных"
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" });
//-----------
//Тело модуля
//-----------
//Формирование массива из 0, 1 и более элементов
export const makeArray = arr => {
return arr ? (arr.length ? arr : [arr]) : [];
};
//Конвертация формата HEX в формат RGB
const convertHexToRGB = 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 getTaskBgColorByRule = (task, colorRule) => {
//Исходя из типа определяем наименование и возвращаем цвет заливки
switch (colorRule.STYPE) {
case "number":
return (!colorRule.fromValue || Number(task.docProps[`N${colorRule.SFIELD}`]) >= Number(colorRule.fromValue)) &&
(!colorRule.toValue || Number(task.docProps[`N${colorRule.SFIELD}`]) <= Number(colorRule.toValue))
? convertHexToRGB(colorRule.SCOLOR)
: null;
default:
return task.docProps[`S${colorRule.SFIELD}`] == colorRule.fromValue ? convertHexToRGB(colorRule.SCOLOR) : null;
}
};
//Индикация истечения срока отработки события
export const getTaskExpiredColor = task => {
//Определяем текущую дату
let sysDate = new Date();
//Определяем дату истечения срока события
let expireDate = task.dExpireDate ? new Date(task.dExpireDate) : null;
//Если дата истечения срока определена
if (expireDate) {
//Определяем разницу между датами
let daysDiff = ((expireDate.getTime() - sysDate.getTime()) / (1000 * 60 * 60 * 24)).toFixed(2);
//Если разница меньше 0 - срок истечен
if (daysDiff < 0) return TASK_COLORS.EXPIRED;
//Если разница меньше 4 - скоро истечет
if (daysDiff < 4) return TASK_COLORS.EXPIRES_SOON;
}
return null;
};
//Цвет из hsl формата в rgba формат
const convertHslToRgba = (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)`;
};
//Формирование случайного цвета
export const getRandomColor = index => {
const hue = index * 137.508;
return convertHslToRgba(hue, 50, 70);
};
//Формат дополнительного свойства типа число (длина, точность)
const formatRegExpNum = (width, precision) =>
new RegExp("^(\\d{1," + (width - precision) + "}" + (precision > 0 ? "((\\.|,)\\d{1," + precision + "})?" : "") + ")?$");
//Формат дополнительного свойства типа строка (длина)
const formatRegExpStr = length => new RegExp("^.{0," + length + "}$");
//Проверка валидности числа
const isValidNum = (width, precision, value) => {
return formatRegExpNum(width, precision).test(value);
};
//Проверка валидности строки
const isValidStr = (length, value) => {
return formatRegExpStr(length).test(value);
};
//Признак ошибки валидации
export const validationError = (value = "", format, numWidth, numPrecision, strLength) => {
//Исходим от формата
switch (format) {
//Проверка строки
case 0:
return isValidStr(strLength, value);
//Проверка числа
case 1:
return isValidNum(numWidth, numPrecision, value);
//Остальное не проверяем
default:
return true;
}
};
//Конвертация времени в привычный формат
export const formatSqlDate = timeStamp => {
//Если есть разделитель
if (timeStamp.indexOf(".") !== -1) {
//Определяем секунды
let seconds = 24 * 60 * 60 * timeStamp;
//Определяем часы
const hours = Math.trunc(seconds / (60 * 60));
//Переопределяем секунды
seconds = seconds % (60 * 60);
//Определяем минуты
const minutes = Math.trunc(seconds / 60);
//Определяем остаток секунд
seconds = Math.round(seconds % 60);
//Форматируем
const formattedTime = ("0" + hours).slice(-2) + ":" + ("0" + minutes).slice(-2) + ":" + ("0" + seconds).slice(-2);
//Возвращаем результат
return formattedTime;
}
return timeStamp;
};
//Считывание значений из локального хранилища
export const getLocalStorageValue = (sName, defaultValue = null) => localStorage.getItem(sName) || defaultValue;
//Форматирование фильтра в массив для отбора
export const convertFilterValuesToArray = filterValues => {
//Инициализируем значение "с" состояния ("Все", "Не аннулированные" - 0, "Аннулированые" - 1)
let nClosedFrom = filterValues.sState ? ([EVENT_STATES[0], EVENT_STATES[1]].includes(filterValues.sState) ? 0 : 1) : 0;
//Инициализируем значение "по" состояния ("Все", "Аннулированные" - 1, "Не аннулированные" - 0)
let nClosedTo = filterValues.sState ? ([EVENT_STATES[0], EVENT_STATES[2]].includes(filterValues.sState) ? 1 : 0) : 0;
//Формируем массив значений фильтра
let filterValuesArray = [
{ name: "NCLOSED", from: nClosedFrom, to: nClosedTo },
{ name: "SEVTYPE_CODE", from: filterValues.sType, to: null },
{ name: "NCRN", from: filterValues.sCrnRnList, to: null },
{ name: "SSEND_PERSON", from: filterValues.sSendPerson, to: null },
{ name: "SSEND_DIVISION", from: filterValues.sSendDivision, to: null },
{ name: "SSEND_USRGRP", from: filterValues.sSendUsrGrp, to: null },
{ name: "NLINKED_RN", from: filterValues.sDocLink, to: null }
];
return filterValuesArray;
};
//Формирование массива действий карточки события
export const makeCardActionsArray = (onEdit, onEditClient, onDelete, onStateChange, onReturn, onSend, onNotesOpen, onFileLinksOpen) => {
//Формируем список действий карточки
return [
{
method: "EDIT",
name: "Исправить",
icon: "edit",
visible: false,
delimiter: false,
tasksReload: false,
needAccountsReload: false,
func: onEdit
},
{
method: "EDIT_CLIENT",
name: "Исправить в разделе",
icon: "edit_note",
visible: true,
delimiter: false,
tasksReload: false,
needAccountsReload: false,
func: onEditClient
},
{
method: "DELETE",
name: "Удалить",
icon: "delete",
visible: true,
delimiter: true,
tasksReload: true,
needAccountsReload: false,
func: onDelete
},
{
method: "TASK_STATE_CHANGE",
name: "Перейти",
icon: "turn_right",
visible: true,
delimiter: false,
tasksReload: true,
needAccountsReload: true,
func: onStateChange
},
{
method: "TASK_RETURN",
name: "Выполнить возврат",
icon: "turn_left",
visible: true,
delimiter: false,
tasksReload: true,
needAccountsReload: true,
func: onReturn
},
{
method: "TASK_SEND",
name: "Направить",
icon: "send",
visible: true,
delimiter: true,
tasksReload: true,
needAccountsReload: true,
func: onSend
},
{
method: "NOTES",
name: "Примечания",
icon: "event_note",
visible: true,
delimiter: true,
tasksReload: false,
needAccountsReload: false,
func: onNotesOpen
},
{
method: "FILE_LINKS",
name: "Присоединенные документы",
icon: "attach_file",
visible: true,
delimiter: false,
tasksReload: false,
needAccountsReload: false,
func: onFileLinksOpen
}
];
};

View File

@ -0,0 +1,48 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Общие стили
*/
//---------------------
//Подключение библиотек
//---------------------
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
//---------
//Константы
//---------
//Общие стили
export const COMMON_STYLES = {
TASK_FORM_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)"
}
}
: {})
}),
BOX_WITH_LEGEND: { border: "1px solid #939393" },
BOX_SINGLE_COLUMN: { display: "flex", flexDirection: "column", gap: "10px" },
LEGEND: { textAlign: "left" },
SELECT_MENU: width => {
return { overflowY: "auto", ...APP_STYLES.SCROLL, width: width ? width : null };
},
STACK_DOCLINKS: { alignItems: "baseline" },
SCROLL: { ...APP_STYLES.SCROLL, overflowY: "auto" },
DIALOG_ACTIONS: { justifyContent: "end", paddingRight: "24px", paddingLeft: "24px" },
DIALOG_CLOSE_BUTTON: {
position: "absolute",
right: 8,
top: 8,
color: theme => theme.palette.grey[500]
},
ZERO_PADDING: { padding: 0 }
};

View File

@ -0,0 +1,179 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент панели: Диалог формы события
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useCallback, useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Dialog, DialogContent, DialogActions, Button } from "@mui/material"; //Интерфейсные компоненты
import { useClientEvent } from "./hooks/task_dialog_hooks"; //Хук для события
import { TaskForm } from "./components/task_form"; //Форма события
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { object2Base64XML } from "../../core/utils"; //Вспомогательные функции
import { COMMON_STYLES } from "./styles"; //Общие стили
//---------
//Константы
//---------
//Стили
const STYLES = {
DIALOG_CONTENT: {
paddingBottom: "0px",
maxHeight: "740px",
minHeight: "740px",
...COMMON_STYLES.SCROLL
}
};
//-----------
//Тело модуля
//-----------
//Диалог формы события
const TaskDialog = ({ taskRn, taskType, editable, onTasksReload, onClose }) => {
//Собственное состояние
const [task, setTask] = useClientEvent(taskRn, taskType);
//Состояние заполненности всех обязательных свойств
const [dpReady, setDPReady] = useState(false);
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//При изменении заполненности всех обязательных свойств
const handleDPReady = useCallback(v => setDPReady(v), []);
//При изменении информации о задаче
const handleTaskChange = useCallback(
newTaskValues => {
setTask(pv => ({ ...pv, ...newTaskValues }));
},
[setTask]
);
//При добавлении события
const handleInsertTask = 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.dPlanDate,
SINIT_PERSON: task.sInitClnperson,
SCLIENT_CLIENT: task.sClntClients,
SCLIENT_PERSON: task.sClntClnperson,
SDESCRIPTION: task.sDescription,
SREASON: task.sInitReason,
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();
};
//При исправлении события
const handleUpdateEvent = async callBack => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_UPDATE",
args: {
NCLNEVENTS: task.nRn,
SCLIENT_CLIENT: task.sClntClients,
SCLIENT_PERSON: task.sClntClnperson,
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();
};
//При считывании следующего номера события
const handleEventNextNumbGet = 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, setTask, task.sPrefix]);
//Генерация содержимого
return (
<Dialog open onClose={onClose ? onClose : null} fullWidth>
<DialogContent sx={STYLES.DIALOG_CONTENT}>
<TaskForm
task={task}
taskType={taskType}
onTaskChange={handleTaskChange}
editable={!taskRn || editable ? true : false}
onEventNextNumbGet={handleEventNextNumbGet}
onDPReady={handleDPReady}
/>
</DialogContent>
{onClose ? (
<DialogActions sx={COMMON_STYLES.DIALOG_ACTIONS}>
{taskRn ? (
<Button
onClick={() => handleUpdateEvent(onClose).then(onTasksReload)}
disabled={task.updateDisabled || !editable || !dpReady}
>
Исправить
</Button>
) : (
<Button onClick={() => handleInsertTask(onClose).then(onTasksReload)} disabled={task.insertDisabled || !dpReady}>
Добавить
</Button>
)}
<Button onClick={onClose}>Закрыть</Button>
</DialogActions>
) : null}
</Dialog>
);
};
//Контроль свойств - Диалог формы события
TaskDialog.propTypes = {
taskRn: PropTypes.number,
taskType: PropTypes.string.isRequired,
editable: PropTypes.bool,
onTasksReload: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskDialog };

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,7 +48,8 @@ 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 => ({

View File

@ -36,7 +36,8 @@ import {
Card,
CardHeader,
CardContent,
CardActions
CardActions,
Tooltip
} from "@mui/material"; //Интерфейсные элементы
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
@ -55,19 +56,34 @@ 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",
@ -84,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"
}
}
: {};
}
};
//------------------------------------
@ -102,7 +137,9 @@ const parseProdPlanSpXML = async xmlDoc => {
};
//Форматирование для отображения количества документов
const formatCountDocs = nCountDocs => {
const formatCountDocs = (nCountDocs, nType = 0) => {
//Склонение документов
let DECLINATIONS = nType === 0 ? PLANS_DECLINATIONS : SPEC_DECLINATIONS;
//Получаем последнюю цифру в значении
let num = (nCountDocs % 100) % 10;
//Документов
@ -115,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
};
//Генерация диалога задачи
@ -232,12 +360,18 @@ const MechRecCostProdPlans = () => {
showPlanList: false,
planCtlgs: [],
planCtlgsLoaded: false,
selectedPlans: [],
selectedPlansElements: 0,
selectedPlanCtlgSpecsLoaded: false,
selectedPlanCtlg: null,
selectedPlanCtlgMaxLevel: null,
selectedPlanCtlgLevel: null,
selectedPlanCtlgOutOfLimit: 0,
selectedPlanCtlgSort: null,
selectedPlanCtlgMenuItems: null,
loadedCtlg: null,
loadedPlans: [],
loadedElements: 0,
gantt: {},
selectedTaskDetail: null,
selectedTaskDetailType: 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,74 +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,
gantt: {},
showPlanList: false,
selectedTaskDetail: null,
selectedTaskDetailType: null
}));
};
//Сброс выбора каталога планов
const unselectPlan = () =>
setState(pv => ({
...pv,
selectedPlanCtlgSpecsLoaded: false,
selectedPlanCtlg: null,
selectedPlanCtlgMaxLevel: null,
selectedPlanCtlgLevel: null,
selectedPlanCtlgSort: null,
selectedPlanCtlgMenuItems: null,
gantt: {},
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,
gantt: { ...doc, tasks: [...(doc?.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
}));
};
//При подключении компонента к странице
@ -345,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 => {
@ -370,6 +534,17 @@ const MechRecCostProdPlans = () => {
setState(pv => ({ ...pv, selectedTaskDetail: taskRn, selectedTaskDetailType: taskType }));
};
//При открытии окна информации об ограничении уровня
const handleLevelLimitInfoOpen = () => {
//Отображаем информацию
showMsgInfo(
`Размер производственной программы превышает предельно допустимый для одновременного отображения в виде диаграммы Ганта.
Доступные для просмотра уровни вложенности ограничены.
Вы можете просматривать производственную программу частями, используя действие "Открытие панели Производственная программа" в спецификации "Выпуск"
раздела "Планы и отчеты производства изделий".`
);
};
//Генерация содержимого
return (
<Box>
@ -378,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>
</>
@ -434,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}
@ -463,14 +646,14 @@ const MechRecCostProdPlans = () => {
/>
</Box>
)
) : !state.selectedPlanCtlg ? (
) : !state.loadedCtlg ? (
<Box pt={3}>
<InlineMsgInfo
okBtn={false}
text={
state.planSpec
? "Загружаю график для выбранной позиции плана..."
: "Укажите каталог планов для отображения их спецификаций"
: "Укажите каталог планов или планы для отображения их спецификаций"
}
/>
</Box>

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,42 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Обще ресурсы и константы
*/
//---------
//Константы
//---------
//Типы данных
const DATA_TYPE = { STR: 0, NUMB: 1, DATE: 2 };
//Иконки типов данных
const DATA_TYPE_ICON = {
[DATA_TYPE.STR]: "format_align_left",
[DATA_TYPE.NUMB]: "pin",
[DATA_TYPE.DATE]: "calendar_month"
};
//Типы элементов диаграммы
const NODE_TYPE = {
ENTITY: "entity",
ATTRIBUTE: "attribute"
};
//Типы сущностей
const ENTITY_TYPE = {
VIEW: "VIEW",
TABLE: "TABLE"
};
//Иконки типов сущностей
const ENTITY_TYPE_ICON = {
[ENTITY_TYPE.VIEW]: "table_view",
[ENTITY_TYPE.TABLE]: "table_rows"
};
//----------------
//Интерфейс модуля
//----------------
export { DATA_TYPE, DATA_TYPE_ICON, NODE_TYPE, ENTITY_TYPE, ENTITY_TYPE_ICON };

View File

@ -0,0 +1,89 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компоненты: Аргумент запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, ListItem, IconButton, Icon, ListItemButton, ListItemText, Typography, ListItemIcon, Chip } from "@mui/material"; //Компоненты UI
import { STYLES as COMMON_STYLES } from "../../../../components/editors/p8p_editors_common"; //Общие ресурсы редаторов
import { DATA_TYPE, DATA_TYPE_ICON } from "../../common"; //Общие ресурсы и константы редактора запросов
//---------
//Константы
//---------
//Варианты представления
const ARGUMENT_VARIANT = {
LIST_ITEM: "LIST_ITEM",
CHIP: "CHIP"
};
//Структура аргумента
const ARGUMENT_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
dataType: PropTypes.oneOf(Object.values(DATA_TYPE)),
mandatory: PropTypes.oneOf([0, 1]).isRequired
});
//Иконки
const ICONS = { ...DATA_TYPE_ICON, DEFAULT: "category" };
//-----------
//Тело модуля
//-----------
//Аргумент запроса
const Argument = ({ arg, variant, onClick = null, onDelete = null }) => {
//Заголовок аргумента
const title = `${arg.mandatory == 1 ? "*" : ""}${arg.title}`;
//Иконка аргумента
const icon = ICONS[arg.dataType] || ICONS.DEFAULT;
//Формирование представления
return variant == ARGUMENT_VARIANT.LIST_ITEM ? (
<ListItem disablePadding>
<ListItemButton onClick={() => onClick && onClick(arg)} dense>
<ListItemIcon>
<Icon>{icon}</Icon>
</ListItemIcon>
<ListItemText
primary={title}
secondaryTypographyProps={{ component: "div" }}
secondary={
<Stack direction={"column"}>
<Typography variant={"caption"}>{arg.name}</Typography>
</Stack>
}
/>
<Stack direction={"row"}>
<IconButton onClick={e => onDelete && onDelete(e, arg)} title={"Удалить"}>
<Icon>delete</Icon>
</IconButton>
</Stack>
</ListItemButton>
</ListItem>
) : variant == ARGUMENT_VARIANT.CHIP ? (
<Chip icon={<Icon>{icon}</Icon>} label={title} variant={"outlined"} sx={COMMON_STYLES.CHIP(true)} />
) : null;
};
//Контроль свойств компонента - Аргумент запроса
Argument.propTypes = {
arg: ARGUMENT_SHAPE,
variant: PropTypes.oneOf(Object.values(ARGUMENT_VARIANT)).isRequired,
onClick: PropTypes.func,
onDelete: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { Argument, ARGUMENT_VARIANT, ARGUMENT_SHAPE };

View File

@ -0,0 +1,146 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компоненты: Атрибут сущности
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Handle, Position, useStore } from "reactflow"; //Библиотека редактора диаграмм
import { Box, Stack, Icon, Typography } from "@mui/material"; //Компоненты UI
import { DATA_TYPE, DATA_TYPE_ICON } from "../../common"; //Общие ресурсы и константы редактора запросов
//---------
//Константы
//---------
//Типовые цвета точек привязки
const HANDLE_BORDER_COLOR = "#69db7c";
const HANDLE_BORDER_COLOR_INVALID = "#ff0000";
const HANDLE_BORDER_COLOR_DISABLED = "#adb5bd";
//Стили
const STYLES = {
CONTAINER: { display: "flex", width: "100%", height: "100%", cursor: "default" },
HANDLE_SOURCE: isConnecting => ({
width: 14,
height: 14,
right: -10,
border: `2px solid ${isConnecting ? HANDLE_BORDER_COLOR_DISABLED : HANDLE_BORDER_COLOR}`,
borderRadius: 7,
background: "white"
}),
HANDLE_TARGET: (isConnecting, isValidConnection) => ({
width: isConnecting ? 14 : 0,
height: 14,
left: isConnecting ? -7 : 0,
border: `2px solid ${isValidConnection ? HANDLE_BORDER_COLOR : HANDLE_BORDER_COLOR_INVALID}`,
borderRadius: 7,
background: "white",
visibility: isConnecting ? "visible" : "hidden"
}),
CONTENT_STACK: { width: "100%" },
TITLE_NAME_STACK: { width: "100%", containerType: "inline-size" },
ATTR_PROP_ICON: { fontSize: "0.9rem" }
};
//Иконки
const ICONS = { ...DATA_TYPE_ICON, DEFAULT: "category" };
//Структура данных об атрибуте сущности
const ATTRIBUTE_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
parentEntity: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
dataType: PropTypes.oneOf(Object.values(DATA_TYPE)),
agg: PropTypes.string,
alias: PropTypes.string,
use: PropTypes.oneOf([0, 1]).isRequired,
show: PropTypes.oneOf([0, 1]).isRequired
});
//-----------------------
//Вспомогательные функции
//-----------------------
//Получение атрибутики состояния включения атрибута в запрос
const attrGetUse = (attr, callToAction = false) => {
return [attr.use === 1, `${attr.use === 1 ? "Включен в запрос" : "Не включен в запрос"}${callToAction ? "- нажмите, чтобы изменить" : ""}`];
};
//Получение атрибутики состояния отображения атрибута в результатах запроса
const attrGetShow = (attr, callToAction = false) => {
return [
`${attr.show == 1 ? "Отображается в результатах запроса" : "Не отображается в результатах запроса"}${
callToAction ? "- нажмите, чтобы изменить" : ""
}`,
attr.show == 1 ? "visibility" : "visibility_off"
];
};
//-----------
//Тело модуля
//-----------
//Атрибут сущности
const Attribute = ({ data }) => {
//Поиск идентификатора соединяемого элемента
const [connectionNodeId, targetConnectionNode, connectionStatus] = useStore(state => [
state.connectionNodeId,
state?.connectionEndHandle?.nodeId,
state.connectionStatus
]);
//Флаг выполнения соединения сущностей
const isConnecting = Boolean(connectionNodeId);
//Флаг корректности соединения сущностей
const isValidConnection = !(data.id == targetConnectionNode && connectionStatus == "invalid");
//Получим атрибуты состояния отображения
const [showTitle, showIcon] = attrGetShow(data);
//Формирование представления
return (
<Box p={1} sx={STYLES.CONTAINER}>
<Handle type={"source"} position={Position.Right} style={STYLES.HANDLE_SOURCE(isConnecting)} />
<Handle
type={"target"}
position={Position.Left}
isConnectableStart={false}
style={STYLES.HANDLE_TARGET(isConnecting, isValidConnection)}
/>
<Stack direction={"row"} alignItems={"center"} spacing={1} sx={STYLES.CONTENT_STACK}>
<Icon color={"action"}>{ICONS[data.dataType] || ICONS.DEFAULT}</Icon>
<Stack direction={"column"} alignItems={"left"} sx={STYLES.TITLE_NAME_STACK}>
<Typography variant={"body2"} noWrap title={data.title}>
{data.title}
</Typography>
<Stack direction={"row"} alignItems={"center"} spacing={0.5}>
<Typography component={"div"} variant={"caption"} color={"text.secondary"} noWrap title={data.name}>
{`${data.name},`}
</Typography>
<Icon color={"action"} sx={STYLES.ATTR_PROP_ICON} title={showTitle}>
{showIcon}
</Icon>
</Stack>
</Stack>
</Stack>
</Box>
);
};
//Контроль свойств компонента - Атрибут сущности
Attribute.propTypes = {
data: ATTRIBUTE_SHAPE
};
//----------------
//Интерфейс модуля
//----------------
export { Attribute, ATTRIBUTE_SHAPE, attrGetUse, attrGetShow };

View File

@ -0,0 +1,144 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компоненты: Сущность запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, Stack, ListItem, Icon, ListItemButton, Typography } from "@mui/material"; //Компоненты UI
import { ENTITY_TYPE, ENTITY_TYPE_ICON } from "../../common"; //Общие ресурсы и константы редактора запросов
import { ATTRIBUTE_SHAPE } from "../attribute/attribute"; //Описание атрибута сущности
//---------
//Константы
//---------
//Варианты представления сущности
const ENTITY_VARIANT = {
LIST_ITEM: "LIST_ITEM",
DIAGRAM: "DIAGRAM"
};
//Иконки
const ICONS = { ...ENTITY_TYPE_ICON, DEFAULT: "border_clear" };
//Структура данных о сущности запроса
const ENTITY_SHAPE = PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.oneOf(Object.values(ENTITY_TYPE)).isRequired,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
attrs: PropTypes.arrayOf(ATTRIBUTE_SHAPE).isRequired
});
//Структура данных о сущности запроса (краткая)
const ENTITY_BRIEF_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.oneOf(Object.values(ENTITY_TYPE)).isRequired
});
//Стили
const STYLES = {
CONTAINER: selected => ({
width: "100%",
height: "100%",
border: "1px solid var(--border-color-dark)",
borderRadius: "6px",
boxShadow: "var(--shadow-entity)",
overflow: "hidden",
backgroundColor: "white",
cursor: "move",
...(selected
? {
outline: "1px solid var(--outline-color)",
borderColor: "var(--outline-color)"
}
: {})
}),
CONTENT_STACK: variant => ({
width: "100%",
backgroundColor: "var(--entity-title-bg)",
...(variant === ENTITY_VARIANT.DIAGRAM ? { height: "50px" } : {})
}),
CAPTIONS_TYPOGRAPHY: variant => ({ ...(variant === ENTITY_VARIANT.DIAGRAM ? { maxWidth: "100px", overflow: "hidden" } : {}) })
};
//-----------
//Тело модуля
//-----------
//Сущность запроса
const Entity = ({ data, variant = ENTITY_VARIANT.DIAGRAM, selected = false, onClick = null }) => {
//Иконка
const icon = ICONS[data.type] || ICONS.DEFAULT;
//Всплывающая подсказка
const iconTitle = data.type === ENTITY_TYPE.VIEW ? "Представление" : "Таблица";
//Содержимое самой сущности
const entContent = (
<Stack
direction={"row"}
alignItems={"center"}
justifyContent={variant === ENTITY_VARIANT.DIAGRAM ? "center" : "left"}
p={1}
sx={STYLES.CONTENT_STACK(variant)}
>
<Icon color={"action"} title={iconTitle}>
{icon}
</Icon>
<Stack direction={"column"} alignItems={"left"} pl={1}>
<Typography
variant={variant === ENTITY_VARIANT.DIAGRAM ? "subtitle2" : "body2"}
noWrap={variant === ENTITY_VARIANT.DIAGRAM ? true : false}
title={data.title}
sx={STYLES.CAPTIONS_TYPOGRAPHY(variant)}
>
{data.title}
</Typography>
<Typography
component={"div"}
variant={"caption"}
color={"text.secondary"}
noWrap={variant === ENTITY_VARIANT.DIAGRAM ? true : false}
title={data.name}
sx={STYLES.CAPTIONS_TYPOGRAPHY(variant)}
>
{data.name}
</Typography>
</Stack>
</Stack>
);
//Формирование представления
return variant == ENTITY_VARIANT.LIST_ITEM ? (
<ListItem disablePadding>
<ListItemButton onClick={() => onClick && onClick(data)} dense>
{entContent}
</ListItemButton>
</ListItem>
) : variant == ENTITY_VARIANT.DIAGRAM ? (
<Box sx={STYLES.CONTAINER(selected)}>{entContent}</Box>
) : null;
};
//Контроль свойств компонента - Сущность запроса
Entity.propTypes = {
data: PropTypes.oneOfType([ENTITY_SHAPE, ENTITY_BRIEF_SHAPE]).isRequired,
variant: PropTypes.string,
selected: PropTypes.bool,
onClick: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { Entity, ENTITY_VARIANT, ENTITY_SHAPE, ENTITY_BRIEF_SHAPE };

View File

@ -0,0 +1,83 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Инспектор свойств
*/
//---------------------
//Подключение библиотек
//---------------------
import React 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 { ENTITY_SHAPE } from "../entity/entity"; //Описание сущности
import { RELATION_SHAPE } from "../relation/relation"; //Описание связи
import { ARGUMENT_SHAPE } from "../argument/argument"; //Описание аргумента запроса
import { InspectorQueryArguments } from "../inspector_query_args/inspector_query_args"; //Управление аргументами запроса
import { InspectorQueryConditions } from "../inspector_query_cond/inspector_query_cond"; //Управление условиями отбора запроса
import { InspectorQueryEntities } from "../inspector_query_ents/inspector_query_ents"; //Управление сущностями запроса
import { InspectorQueryRelations } from "../inspector_query_rls/inspector_query_rls"; //Управление связями запроса
import { InspectorQueryArea } from "../inspector_query_area/inspector_query_area"; //Область SQL-выражения
//-----------
//Тело модуля
//-----------
//Инспектор свойств
const Inspector = ({
query,
entity,
relation,
entities = [],
args = [],
cond = null,
substArgsVals = 0,
qry = "",
qryBnd = "",
qryMsg = "",
onOptionsChanged = null
}) => {
//При изменении настроек запроса
const handleOptionsChanged = () => onOptionsChanged && onOptionsChanged();
//Генерация содержимого
return (
<P8PEditorBox title={"Настройки запроса"}>
<P8PEditorSubHeader title={"Аргументы"} />
<InspectorQueryArguments query={query} args={args} onOptionsChanged={handleOptionsChanged} />
<P8PEditorSubHeader title={"Условия отбора"} />
<InspectorQueryConditions query={query} cond={cond} entities={entities} args={args} onOptionsChanged={handleOptionsChanged} />
<P8PEditorSubHeader title={"Сущности"} />
<InspectorQueryEntities query={query} entity={entity} onOptionsChanged={handleOptionsChanged} />
{relation && (
<>
<P8PEditorSubHeader title={"Связь"} />
<InspectorQueryRelations query={query} relation={relation} onOptionsChanged={handleOptionsChanged} />
</>
)}
<InspectorQueryArea query={query} substArgsVals={substArgsVals} qry={qry} qryBnd={qryBnd} qryMsg={qryMsg} />
</P8PEditorBox>
);
};
//Контроль свойств компонента - Инспектор свойств
Inspector.propTypes = {
query: PropTypes.number.isRequired,
entity: ENTITY_SHAPE,
relation: RELATION_SHAPE,
entities: PropTypes.arrayOf(ENTITY_SHAPE),
args: PropTypes.arrayOf(ARGUMENT_SHAPE),
cond: PropTypes.string,
substArgsVals: PropTypes.number,
qry: PropTypes.string,
qryBnd: PropTypes.string,
qryMsg: PropTypes.string,
onOptionsChanged: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { Inspector };

View File

@ -0,0 +1,35 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Пользовательские хуки для работы с областью SQL-выражения
*/
//---------------------
//Подключение библиотек
//---------------------
import { useContext, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../../context/backend"; //Контекст взаимодействия с сервером
//-----------
//Тело модуля
//-----------
//Работа с областью SQL-выражения
const useQuerySQLExpr = query => {
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Установка флага сокрытия/отображения значений аргументов в SQL-выражении запроса
const toggleSubstArgsVals = useCallback(async () => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_OPT_SUBST_ARGS_VALS_TGL", args: { NRN: query }, loader: false });
}, [query, executeStored]);
//Возвращаем интерфейс хука
return { toggleSubstArgsVals };
};
//----------------
//Интерфейс модуля
//----------------
export { useQuerySQLExpr };

View File

@ -0,0 +1,180 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Область SQL-выражения
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Fab, Icon, Drawer, IconButton, TextField, Stack, Box, Snackbar, Alert } from "@mui/material"; //Компоненты MUI
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
import { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения
import { useQuerySQLExpr } from "./hooks"; //Пользовательские хуки для работы с SQL-выражением
//---------
//Константы
//---------
//Стили
const STYLES = {
SQL_FAB: {
position: "absolute",
bottom: 16,
right: 16
},
SQL_TEXT_FIELD: {
fontSize: "0.9rem",
...APP_STYLES.SCROLL
},
SNACKBAR_ALERT: { width: "100%" }
};
//Начальное состояние всплывающего сообщения
const SNACK_BAR_MESSAGE_INIT = { text: null, type: null };
//-----------
//Тело модуля
//-----------
//Область SQL-выражения
const InspectorQueryArea = ({ query, substArgsVals = 0, qry = "", qryBnd = "", qryMsg = "" }) => {
//Собственное состояние - отображение запроса с подстановками
const [showQryBnd, setShowQryBnd] = useState(substArgsVals);
//Собственное состояние - текст всплывающего сообщения
const [snackBarMessage, setSnackBarMessage] = useState(SNACK_BAR_MESSAGE_INIT);
//Собственное состояние - отображение области SQL запроса
const [displaySQL, setDisplaySQL] = useState(false);
//Собственное состояние - развёрнутость
const [expanded, setExpanded] = useState(false);
//Работа с SQL-выражением
const { toggleSubstArgsVals } = useQuerySQLExpr(query);
//При нажатии на кнопку отображения/сокрытия значений аргументов в SQL-выражении запроса
const handleToggleSubstArgsValsClick = async () => {
await toggleSubstArgsVals();
setShowQryBnd(showQryBnd === 1 ? 0 : 1);
};
//При нажатии на кнопку копирования текста запроса
const handleCopyClick = async () => {
try {
await navigator.clipboard.writeText(qry);
setSnackBarMessage({ text: `Текст запроса скопирован в буфер обмена` });
} catch (e) {
setSnackBarMessage({ text: `Ошибка копирования текста запроса в буфер обмена: ${e.message}`, type: "error" });
}
};
//При нажатии на кнопку развёртывания
const handleExpandClick = () => setExpanded(!expanded);
//При нажатии на кнопку отображения SQL запроса
const handleShowSQLClick = () => setDisplaySQL(true);
//При нажатии на кнопку сокрытия SQL запроса
const handleCloseSQLClick = () => setDisplaySQL(false);
//При закрытии всплывающего сообщения
const handleSnackBarClose = () => setSnackBarMessage(SNACK_BAR_MESSAGE_INIT);
//Расчет размеров тектовых полей
const [qryRows, qryMsgRows] = expanded ? [15, 6] : [5, 3];
//Расчет параметров отображения запроса
const [dispQry, qryViewTitle, qryViewIcon] =
showQryBnd === 0 ? [qry, "Отобразить значения аргументов", "code"] : [qryBnd, "Скрыть значения аргументов", "code_off"];
//При изменении состояние отображения подстановок в запросе
useEffect(() => setShowQryBnd(substArgsVals), [substArgsVals]);
//Генерация содержимого
return (
<>
{(qry || qryMsg) && (
<Fab color={qryMsg ? "warning" : "default"} sx={STYLES.SQL_FAB} title={"Показать текст SQL запроса"} onClick={handleShowSQLClick}>
<Icon>join_left</Icon>
</Fab>
)}
{displaySQL && (
<Drawer open onClose={handleCloseSQLClick} anchor={"bottom"}>
<Box p={2}>
<Stack direction={"row"} justifyContent={"right"} spacing={2}>
{qry && (
<>
<IconButton onClick={handleToggleSubstArgsValsClick} title={qryViewTitle}>
<Icon>{qryViewIcon}</Icon>
</IconButton>
<IconButton onClick={handleCopyClick} title={"Скопировать текст запроса"}>
<Icon>content_copy</Icon>
</IconButton>
</>
)}
<IconButton onClick={handleExpandClick} title={expanded ? "Свернуть" : "Развернуть"}>
<Icon>{expanded ? "expand_more" : "expand_less"}</Icon>
</IconButton>
<IconButton onClick={handleCloseSQLClick} title={BUTTONS.HIDE}>
<Icon>close</Icon>
</IconButton>
</Stack>
<Stack direction={"column"} spacing={2}>
{dispQry && (
<TextField
label={"Текст запроса"}
multiline
fullWidth
value={dispQry}
minRows={qryRows}
maxRows={qryRows}
variant={"standard"}
focused
inputProps={{ sx: STYLES.SQL_TEXT_FIELD, disabled: true }}
/>
)}
{qryMsg && (
<TextField
label={"Предупреждения"}
color={"warning"}
multiline
fullWidth
value={qryMsg}
minRows={qryMsgRows}
maxRows={qryMsgRows}
variant={"standard"}
focused
inputProps={{ sx: STYLES.SQL_TEXT_FIELD, disabled: true }}
/>
)}
</Stack>
</Box>
</Drawer>
)}
<Snackbar open={Boolean(snackBarMessage.text)} autoHideDuration={3000} onClose={handleSnackBarClose}>
<Alert severity={snackBarMessage.type || "success"} sx={STYLES.SNACKBAR_ALERT} onClose={handleSnackBarClose}>
{snackBarMessage.text}
</Alert>
</Snackbar>
</>
);
};
//Контроль свойств компонента - Область SQL-выражения
InspectorQueryArea.propTypes = {
query: PropTypes.number.isRequired,
substArgsVals: PropTypes.number,
qry: PropTypes.string,
qryBnd: PropTypes.string,
qryMsg: PropTypes.string
};
//----------------
//Интерфейс модуля
//----------------
export { InspectorQueryArea };

View File

@ -0,0 +1,95 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Диалог добавления/исправления аргумента запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог
import { TITLES } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
import { DATA_TYPE } from "../../common"; //Общие константы редактора
//-----------
//Тело модуля
//-----------
//Диалог добавления/исправления аргумента запроса
const ArgIUDialog = ({ name = "", title = "", dataType = DATA_TYPE.NUMB, mandatory = 0, value = "", insert = true, onOk, onCancel }) => {
//Собственное состояние - текущие значения полей аргумента запроса
const [current, setCurrent] = useState({ name, title, dataType, mandatory, value });
//Нажатие на кнопку "Ok"
const handleOk = () => onOk && onOk(current);
//Нажатие на кнопку "Отмена"
const handleCancel = () => onCancel && onCancel();
//При изменении значений в поле ввода
const handleInputChange = (name, value) => {
//Сохраним в состоянии новое значение, если сменили тип данных - сбросим "отладочное значение"
setCurrent(pv => ({ ...pv, [name]: value, ...(name === "dataType" ? { value: "" } : {}) }));
//Мы сами пересчитали форму, туда придут новые настройки элементов ввода через свойство inputs, не надо делать пересчет состояния внутри диалога
return true;
};
//Генерация содержимого
return (
<P8PDialog
title={`${insert === true ? TITLES.INSERT : TITLES.UPDATE} аргумента`}
inputs={[
{ name: "name", value: current.name, label: "Имя", disabled: insert != true },
{ name: "title", value: current.title, label: "Приглашение" },
{
name: "dataType",
value: current.dataType,
label: "Тип данных",
list: [
{ name: "Строка", value: DATA_TYPE.STR },
{ name: "Число", value: DATA_TYPE.NUMB },
{ name: "Дата", value: DATA_TYPE.DATE }
]
},
{
name: "mandatory",
value: current.mandatory,
label: "Обязательный",
list: [
{ name: "Нет", value: 0 },
{ name: "Да", value: 1 }
]
},
{
name: "value",
value: current.value,
label: "Значение (для отладки)",
type: current.dataType === DATA_TYPE.NUMB ? "number" : current.dataType === DATA_TYPE.DATE ? "date" : "text"
}
]}
onOk={handleOk}
onCancel={handleCancel}
onInputChange={handleInputChange}
/>
);
};
//Контроль свойств - Диалог добавления/исправления аргумента запроса
ArgIUDialog.propTypes = {
name: PropTypes.string,
title: PropTypes.string,
dataType: PropTypes.number,
mandatory: PropTypes.number,
value: PropTypes.string,
insert: PropTypes.bool,
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { ArgIUDialog };

View File

@ -0,0 +1,62 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент: Список аргументов
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { List } from "@mui/material"; //Интерфейсные компоненты MUI
import { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения
import { Argument, ARGUMENT_VARIANT, ARGUMENT_SHAPE } from "../argument/argument"; //Аргумент запроса
//---------
//Константы
//---------
//Стили
const STYLES = {
LIST: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL }
};
//-----------
//Тело модуля
//-----------
//Список аргументов
const ArgsList = ({ args = [], onSelect = null, onDelete = null } = {}) => {
//При нажатии на элемент списка
const handleItemClick = arg => onSelect && onSelect(arg);
//При нажатии на удалении элемента списка
const handleItemDeleteClick = (e, arg) => {
e.stopPropagation();
onDelete && onDelete(arg);
};
//Формирование представления
return (
<List sx={STYLES.LIST}>
{args &&
args.map((arg, i) => (
<Argument key={i} arg={arg} variant={ARGUMENT_VARIANT.LIST_ITEM} onClick={handleItemClick} onDelete={handleItemDeleteClick} />
))}
</List>
);
};
//Контроль свойств компонента - Список аргументов
ArgsList.propTypes = {
args: PropTypes.arrayOf(ARGUMENT_SHAPE),
onSelect: PropTypes.func,
onDelete: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { ArgsList };

View File

@ -0,0 +1,66 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Пользовательские хуки настройки аргументов запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import { useContext, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../../context/backend"; //Контекст взаимодействия с сервером
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//-----------
//Тело модуля
//-----------
//Работа с аргументами запроса
const useQueryArgs = query => {
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Добавление аргумента запроса
const addArg = useCallback(
async (name, title, dataType, mandatory, value) => {
await executeStored({
stored: "PKG_P8PANELS_QE.QUERY_OPT_ARG_ADD",
args: { NRN: query, SNAME: name, STITLE: title, NDATA_TYPE: dataType, NMANDATORY: mandatory, SVALUE: value },
loader: false
});
},
[query, executeStored]
);
//Исправление аргумента запроса
const editArg = useCallback(
async (name, title, dataType, mandatory, value) => {
await executeStored({
stored: "PKG_P8PANELS_QE.QUERY_OPT_ARG_EDIT",
args: { NRN: query, SNAME: name, STITLE: title, NDATA_TYPE: dataType, NMANDATORY: mandatory, SVALUE: value },
loader: false
});
},
[query, executeStored]
);
//Удаление аргумента запроса
const removeArg = useCallback(
async name => {
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_OPT_ARG_REMOVE", args: { NRN: query, SNAME: name }, loader: false });
},
[query, executeStored]
);
//Возвращаем интерфейс хука
return { addArg, editArg, removeArg };
};
//----------------
//Интерфейс модуля
//----------------
export { useQueryArgs };

View File

@ -0,0 +1,107 @@
/*
Парус 8 - Панели мониторинга - Редактор запросов
Компонент инспектора - Аргументы запроса
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, Icon, Button, Card, CardContent, CardActionArea } from "@mui/material"; //Интерфейсные элементы
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы
import { useQueryArgs } from "./hooks"; //Хуки для работы с аргументами запроса на сервере
import { QueryArgsDialog } from "./query_args_dialog"; //Диалог настройки состава атрибутов
import { Argument, ARGUMENT_SHAPE, ARGUMENT_VARIANT } from "../argument/argument"; //Аргумент запроса
//-----------
//Тело модуля
//-----------
//Компонент инспектора - Аргументы запроса
const InspectorQueryArguments = ({ query, args = [], onOptionsChanged = null }) => {
//Собственное состояние - отображение диалога настройки состава аргументов
const [openQueryArgsDialog, setOpenQueryArgsDialog] = useState(false);
//Хук для взаимодействия с сервером
const { addArg, editArg, removeArg } = useQueryArgs(query);
//Уведомление родителя об изменении свойств
const notifyOptionsChanged = () => onOptionsChanged && onOptionsChanged();
//При нажатии на настройку аргументов
const handleSetup = () => setOpenQueryArgsDialog(true);
//При добавлении аргумента
const handleArgAdd = async (arg, cb) => {
await addArg(arg.name, arg.title, arg.dataType, arg.mandatory, arg.value);
cb && cb();
notifyOptionsChanged();
};
//При изменении аргумента
const handleArgEdit = async (arg, cb) => {
await editArg(arg.name, arg.title, arg.dataType, arg.mandatory, arg.value);
cb && cb();
notifyOptionsChanged();
};
//При удалении аргумента
const handleArgRemove = async arg => {
await removeArg(arg.name);
notifyOptionsChanged();
};
//Закрытие диалога настройки состава аргументов по "Закрыть"
const handleQueryArgsDialogClose = () => setOpenQueryArgsDialog(false);
//Расчет флага "настроенности"
const configured = args && args.length > 0 ? true : false;
//Формирование представления
return (
<>
{openQueryArgsDialog && (
<QueryArgsDialog
args={args}
onArgAdd={handleArgAdd}
onArgEdit={handleArgEdit}
onArgRemove={handleArgRemove}
onClose={handleQueryArgsDialogClose}
/>
)}
{configured && (
<Card variant={"outlined"}>
<CardActionArea onClick={handleSetup}>
<CardContent>
<Stack direction={"column"} spacing={1}>
{args.map((arg, i) => (
<Argument key={i} arg={arg} variant={ARGUMENT_VARIANT.CHIP} />
))}
</Stack>
</CardContent>
</CardActionArea>
</Card>
)}
{!configured && (
<Button startIcon={<Icon>build</Icon>} onClick={handleSetup}>
{BUTTONS.CONFIG}
</Button>
)}
</>
);
};
//Контроль свойств компонента - Компонент инспектора - Аргументы запроса
InspectorQueryArguments.propTypes = {
query: PropTypes.number.isRequired,
args: PropTypes.arrayOf(ARGUMENT_SHAPE),
onOptionsChanged: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { InspectorQueryArguments };

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