Compare commits

..

2 Commits
main ... main

204 changed files with 8343 additions and 42390 deletions

182
README.md
View File

@ -104,21 +104,8 @@
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", размещённый в этом же каталоге.
При компиляции учитывайте следующее:
- объекты `PKG_P8PANELS`, `PKG_P8PANELS_BASE`, `PKG_P8PANELS_VISUAL` и `P8PNL_SELECTLIST` - обязательны к компиляции. **Это ядро серверной части расширения. Без этих объектов его работа невозможна. Все остальные, перечисленные ниже объекты серверной части - прикладные, не обязательны к компиляции и на функционирование непосредственно фреймворка влияния не оказывают.**
- объекты `PKG_P8PANELS_SAMPLES`, `P8PNL_SMPL_CYCLOGRAM` и `P8PNL_SMPL_GANTT` - обеспечивают работу панели "Samples" ("Примеры для разработчиков"). Это абстрактный набор примеров применения фреймворка, не привязанный жестко к комплектации Системы. Должен откомпилироваться практически в любой комплектации Системы (единственное требование - наличие словаря "Контрагенты").
- объекты `PKG_P8PANELS_PROJECTS`, `P8PNL_JB_JOBS`, `P8PNL_JB_JOBSPREV`, `P8PNL_JB_PERIODS`, `P8PNL_JB_PRJCTS` и `P8PNL_JB_PRMS` - могут быть откомпилированы в Системе, имеющей в комлектации приложения "Планирование и учёт в проектах" и "Управление закупками, складом и реализацией". Эти объекты отвечают за работу панелей группы "Планирование и учёт в проектах" ("PrjFin" - "Экономика проектов", "PrjJobs" - "Работы проектов", "PrjGraph" - "Графики проектов", "PrjInfo" - "Информация о проектах"). Компилируйте только если комлектация Системы включает указанные приложения.
- объект `PKG_P8PANELS_CLNTTSKBRD` - может быть откомпилирован в Системе, имеющей в комлектации приложение "Управление деловыми процессами". Этот объект отвечает за работу панелей группы "Управление деловыми процессами" ("ClntTaskBoard" - "Доски задач"). Компилируйте только если комлектация Системы включает указанное приложение.
- объект `PKG_P8PANELS_MECHREC` - может быть откомпилирован в Системе, имеющей в комлектации приложение "Планирование и учёт в дискретном производстве". Этот объект отвечает за работу панелей группы "Планирование и учёт в дискретном производстве" ("MechRecCostProdPlans" - "Производственная программа", "MechRecDeptCostProdPlans" - "Производственный план цеха", "MechRecCostJobsManage" - "Выдача сменного задания", "MechRecCostJobsManageMP" - "Выдача сменного задания на участок", "MechRecDeptCostJobs" - "Загрузка цеха", "MechRecAssemblyMon" - "Мониторинг сборки изделий"). Компилируйте только если комлектация Системы включает указанное приложение.
- объект `PKG_P8PANELS_EQUIPSRV` - может быть откомпилирован в Системе, имеющей в комлектации приложение "Управление техническим обслуживанием и ремонтами". Этот объект отвечает за работу панелей группы "Управление техническим обслуживанием и ремонтами" ("EqsPrfrm" - "Выполнение работ по ТОиР"). Компилируйте только если комлектация Системы включает указанное приложение.
- объект `PKG_P8PANELS_RRPCONFED` - может быть откомпилирован в Системе, имеющей в комлектации приложение "Сервис регламентированной и управленческой отчётности". Этот объект отвечает за работу панели "RrpConfEditor" ("Редактор настройки регламентированного отчёта"). Компилируйте только если комлектация Системы включает указанное приложение.
7. Перезапустите сервер приложений "ПАРУС 8 Онлайн"
> **Внимание:** при установке учитывайте следующее:
@ -129,12 +116,7 @@ git clone https://git.citpb.ru/CITKParus/P8-Panels.git
> - Права доступа - файлы конфигурации и файлы дистрибутива фреймворка должны быть доступны процессу WEB-сервера
>
> - **Для Windows 7 и прочих устаревших версий Windows**
>
> - Версия IIS, доступная для этих ОС, зачастую не имеет автоматической поддержки шрифтов в формате "WOFF2", применяемых фреймворком. Это может вызывать некорректное отображение панелей, иконок, некоторых элементов пользовательского интерфейса. Добавте в файл "web.config", сервера приложений "ПАРУС 8 Онлайн", строку для определения формата: `<mimeMap fileExtension=".woff2" mimeType="application/font-woff2" />`. Путь для добавления данной настройки в файл "web.config": `configuration/system.webServer/staticContent/<mimeMap fileExtension=".woff2" mimeType="application/font-woff2" />`
>
> - **Для всех**
>
> - Если после установки фреймворка, при открытии панелей появляется ошибка `Internal server error` (HTTP-статус ответа сервера - 500), и Вы уверены, что перепроверили всё описанное выше в интрукции и не нашли ошибок установки, то: остановите сервер приложений "ПАРУС 8 Онлайн", сделайте резервную копию его каталога "bin" и удалите каталог, затем скачайте "web.zip" из дистрибутива Вашего релиза "ПАРУС 8 Онлайн" и восстановите удалённый каталог "bin", распаковав его из архива. Затем - запустите сервер приложений "ПАРУС 8 Онлайн".
## V. Подключение панелей
@ -355,59 +337,6 @@ 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 Онлайн" с очисткой системного кэша.
#### Настройка КОР-действия для вызова панели "Производственная программа" из раздела "Планы и отчеты производства изделий"
Входящая в состав поставки фреймворка панель "Производственная программа" доступна для вызова из спецификации "Выпуск" раздела "Планы и отчеты производства изделий" (приложение "Планирование и учёт в дискретном производстве", главное меню > "Документы" > "Планы и отчеты производства изделий").
@ -531,7 +460,7 @@ c:\inetpub\p8web20\WebClient\Modules\P8-Panels>npm run build
- `isRespErr` - функция, проверка результата исполнения серверного объекта на наличие ошибок
- `getRespErrMessage` - функция, получение ошибки исполнения серверного объекта
- `getRespPayload` - функция, получение выходных значений, полученных после успешного исполнения
- `executeStored` - функция, асинхронное исполнение хранимой процедуры/функции БД Системы
- `executeStored` -функция, асинхронное исполнение хранимой процедуры/функции БД Системы
- `getConfig` - функция, асинхронное считывание параметров конфигурации, определённых в "p8panels.config" (возвращает их JSON-представление)
При формировании ответов, функции, получающие данные с сервера, возвращают типовые значения:
@ -599,8 +528,7 @@ c:\inetpub\p8web20\WebClient\Modules\P8-Panels>npm run build
throwError = true,
showErrorMessage = true,
fullResponse = false,
spreadOutArguments = true,
signal = null
spreadOutArguments = true
}
```
@ -613,8 +541,7 @@ 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: "ЗНАЧЕНИЕ"}, ...]}`\
`signal` - необязательный, объект, экземпляр `AbortSignal` (например, `AbortController.signal`) для управления прерыванием выполнения запроса
`spreadOutArguments` - необязательный, логический, признак "разделения" значений выходных параметров исполняемого обхекта (игнорируется при наличии `respArg`), если `true` - `XPAYLOAD` будет содержать ответ в виде `{"ВЫХОДНОЙ_ПАРАМЕТР1": "ЗНАЧЕНИЕ", "ВЫХОДНОЙ_ПАРАМЕТР2": "ЗНАЧЕНИЕ", ...}`, если `false` - `XPAYLOAD` будет содержать ответ в виде `{XOUT_ARGUMENTS: [{SNAME: "ВЫХОДНОЙ_ПАРАМЕТР1", VALUE: "ЗНАЧЕНИЕ"}, {SNAME: "ВЫХОДНОЙ_ПАРАМЕТР2", VALUE: "ЗНАЧЕНИЕ"}, ...]}`
**Результат:** объект с данными, размещёнными в `XPAYLOAD` ответа сервера (если `fullResponse = false`) или полный типовой ответ (описан выше).
@ -1108,7 +1035,7 @@ const Mui = ({ title }) => {
![Примеры модальных сообщений](docs/img/63.png)
###### `undefined showMsg(type, text, msgOnOk = null, msgOnCancel = null, fullErrorText = null)`
###### `undefined showMsg(type, text, msgOnOk = null, msgOnCancel = null)`
Отображает модальное окно сообщения заданного типа.
@ -1116,21 +1043,16 @@ 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`)\
`fullErrorText` - необязательный, строка, полный текст ошибки, используется только при `type="error"`. Если параметр указан, то в окно ошибки выводится кнопка "Подробнее", по нажатию на которую будет отображаться текст, указанный в данном параметре
`msgOnOk` - необязательный, функция, будет вызвана при нажатии на "ОК"/"ЗАКРЫТЬ" в сообщении
`msgOnCancel` - необязательный, функция, будет вызвана при нажатии на "ОТМЕНА" в сообщении (только для сообщений типа `warning`)
**Результат:** функция не возвращает значимого результата
###### `undefined showMsgErr(text, msgOnOk = null, fullErrorText = null)`
###### `undefined showMsgErr(text, msgOnOk = null)`
Декоратор для `showMsg`, отображает модальное окно сообщения типа "Ошибка" (`type="error"`).
**Входные параметры:**
`text` - обязательный, строка, текст отображаемого сообщения\
`msgOnOk` - необязательный, функция, будет вызвана при нажатии на "ЗАКРЫТЬ" в сообщении\
`fullErrorText` - необязательный, строка, полный текст ошибки. Если параметр указан, то в окно ошибки выводится кнопка "Подробнее", по нажатию на которую будет отображаться текст, указанный в данном параметре
**Входные параметры:** аналогично `showMsg`
**Результат:** аналогично `showMsg`
@ -1218,20 +1140,6 @@ const Messages = ({ title }) => {
Ошибка
</Button>
<Divider sx={STYLES.DIVIDER} />
{/* Сообщение об ошибке (диалог с подробностями) */}
<Button
variant="contained"
onClick={() =>
showMsgErr(
"Что-то пошло не так :( ...но мы точно знаем что ;)",
null,
"Здесь подробная информация об ошибке (стек вызова СУБД, например)"
)
}
>
Ошибка с подробностями
</Button>
<Divider sx={STYLES.DIVIDER} />
{/* Предупреждение (диалог) */}
<Button
variant="contained"
@ -1282,7 +1190,7 @@ const Messages = ({ title }) => {
![Пример индикатора процесса](docs/img/65.png)
###### `undefined showLoader(message)`
###### `undefined showMsg(message)`
Отображает модальный индикатор процесса с указанным сообщением.
@ -1349,14 +1257,14 @@ const Loader = ({ title }) => {
- состоят из значительного числа интерфейсных примитивов
- имеют специальный API на стороне сервера БД Системы для управления их содержимым
Необходимо понимать, что с одной стороны, наличие серверного API в БД значительно упрощает взаимодействие с компонентом, с другой стороны - ограничивает возможности его применения только теми прикладными задачами и функциональными возможностями, которые заложены в него. При этом "примитивы" HTML и MUI, хоть и сложнее в применении, но позволяют "собирать" практически любые интерфейсные решения на вкус разработчика.
Необходимо понимать, что с одной стороны, наличие серверного API в БД значительно упрощает взаимодействие с компонентом, с другой стороны - ограничивает возможности его примерения только теми прикладными задачами и функциональными возможностями, которые заложены в него. При этом "примитивы" HTML и MUI, хоть и сложнее в применении, но позволяют "собирать" практически любые интерфейсные решения на вкус разработчика.
##### Таблица данных "P8PDataGrid"
Предназначена для формирования табличных представлений данных с поддержкой:
- постраничного вывода данных
- сортировки и отбора данных по колонкам на стороне сервера БД
- сортировки и отбора данных по колонкам на строне сервера БД
- сложных заголовков с возможностью отображения/сокрытия уровней
- разворачивающихся строк (accordion)
- группировки строк с возможностью отображения/сокрытия содержимого группы
@ -1383,17 +1291,11 @@ 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>}`\
`rows` - необязательный, массив, отображаемые таблицой строки данных, содержит объекты вида `{groupName: <ИМЯ_ГРУППЫ_СОДЕРЖАЩЕЙ_СТРОКУ>, <ИМЯ_КОЛОНКИ>: <ЗНАЧЕНИЕ>}`\
`size` - необязательный, строка, размер отступов при вёрстке таблицы, `small|medium` (см. константу `P8P_DATA_GRID_SIZE` в исходном коде компонента)\
`pageNumber` - необязательный, число, номер отображаемой страницы таблицы, по умолчанию - 1\
`pagesCount` - необязательный, число, количество страниц таблицы, по умолчанию - 0. При значении > 0, параметр `morePages` игнорируется, а у таблицы появляется область отображения страниц (исходя из параметров `pagesAlign` и `pagesPosition`)\
`pagesAlign` - необязательный, строка, размещение области страниц по вертикали, по умолчанию `right`, поддерживает значения `left|right|center` (см. константу `P8P_DATA_GRID_PAGINATOR_ALIGN` в исходном коде компонента)\
`pagesPosition` - необязательный, строка, размещение области страниц по горизонтали, по умолчанию `bottom`, поддерживает значения `top|bottom|both` (см. константу `P8P_DATA_GRID_PAGINATOR_POSITION` в исходном коде компонента)\
`fixedHeader` - необязательный, логический, признак фиксации заголовка таблицы, по умолчанию - `false`\
`fixedColumns` - необязательный, число, количество фиксированных колонок слева, по умолчанию - 0\
`morePages` - необязательный, логический, признак отображения кнопки догрузки данных, по умолчанию - `false`\
@ -1417,11 +1319,10 @@ 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` - необязательный, объект, содержит свойства, которые будут переданы компоненту-контейнеру (`TableContainer`) таблицы (является дочерним для корневого `div`, контейнера всего компонента)\
`containerComponentProps` - необязательный, объект, содержит свойства, которые будут переданы компоненту-контейнеру таблицы\
`onOrderChanged` - необязательный, функция, будет вызвана при изменении пользователем состояния сортировок таблицы. Сигнатура функции `f({orders})`, результат функции не интерпретируется. В функцию передаётся объект, поле `orders` которого, содержит текущее состояние сортировок таблицы. Объект `orders` - массив, содержащий элементы вида `{name: <НАИМЕНОВАНИЕ_КОЛОНКИ>, direction: <ASC|DESC>}`. Функция применяется для инициации обновления данных в таблице.\
`onFilterChanged` - необязательный, функция, будет вызвана при изменении пользователем состояния фильтров таблицы. Сигнатура функции `f({filters})`, результат функции не интерпретируется. В функцию передаётся объект, поле `filters` которого, содержит текущее состояние фильтров таблицы. Объект `filters` - массив, содержащий элементы вида `{name: <НАИМЕНОВАНИЕ_КОЛОНКИ>, from: <ЗНАЧЕНИЕ_НАЧАЛА_ДИАПАЗОНА_ОТБОРА>, to: <ЗНАЧЕНИЕ_ОКОНЧАНИЯ_ДИАПАЗОНА_ОТБОРА>}`. Функция применяется для инициации обновления данных в таблице.\
`onPagesCountChanged` - необязательный, функция, будет вызвана при изменении пользователем количества отображаемых страниц данных таблицы. Сигнатура функции `f()`, результат функции не интерпретируется. Функция применяется для инициации обновления данных в таблице.\
`onPageChanged` - необязательный, функция, будет вызвана при изменении пользователем страницы в области отображения страниц (при `pagesCount` > 0). Сигнатура функции `f({page})`, результат функции не интерпретируется. В функцию передается объект, поле `page` которого, содержит номер выбранной страницы. Значение `page` - число. Функция применяется для инициации обновления данных в таблице.
`objectsCopier` - обязательный, функция глубокого копирования объектов (применяется при обслуживании собственного состояния таблицы данных). Сигнатура функции `f(Object)`, должна возвращать глубокую копию объекта.
Некоторые параметры таблицы данных вынесены в свойства компонента `P8PDataGrid` для минимизации его связи с фреймворком и поддержания возможности стороннего использования (например, свойства `orderAscMenuItemCaption`, `okFilterBtnCaption`, `objectsCopier` и т.п.) . Тем не менее, в настройках фреймворка и его окружении уже есть реализации для данных свойств. Например, в "app.text.js" уже содержатся объявления типовых констант для текстов подписей кнопок и пунктов меню, в "app/core/utils.js" реализована элементарная функция глубокого копиования `deepCopyObject`. Поэтому, в "app/config_wrapper.js" для привязки свойств `P8PDataGrid` к контексту фреймворка реализованы специальные декораторы и объекты-шаблоны, облегчающие подключение экземпляра `P8PDataGrid` к панели и снимающие с разработчика необходимость указывать некоторые из перечисленных выше обязательных свойств. В предложенном ниже примере, из модуля "config_wrapper" в панель импортируется объект `P8P_DATA_GRID_CONFIG_PROPS`, который уже содержт преднастроенное описание свойств `orderAscMenuItemCaption`, `orderDescMenuItemCaption`, `filterMenuItemCaption`, `valueFilterCaption`, `valueFromFilterCaption`, `valueToFilterCaption`, `okFilterBtnCaption`, `clearFilterBtnCaption`, `cancelFilterBtnCaption`, `morePagesBtnCaption`, `noDataFoundText` и `objectsCopier`, полученное из окружения фреймворка. Таким образом, прикладной разработчик может не указывать их значения при использовании `P8PDataGrid` (если по каким-то причинам не хочет их переопределить, конечно).
@ -1445,7 +1346,6 @@ const MyPanel = () => {
Для таблицы данных это (см. детальные описания программных интерфейсов в пакете `PKG_P8PANELS_VISUAL`):
`PKG_P8PANELS_VISUAL.TDG_MAKE` - функция, инициализация таблицы данных, возвращает объект для хранения описания таблицы\
`PKG_P8PANELS_VISUAL.TDG_PAGES_COUNT_SET` - процедура, служит для обновления значения количества отображаемых страниц\
`PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF` - процедура, добавление описания колонки в таблицу, принимает на вход объект с описанием таблицы и параметры, описывающие добавляемую колонку (её имя, заголовок, тип данных, видимость, доступность отбора и сортировки, набор предопределённых значений и т.д.)\
`PKG_P8PANELS_VISUAL.TDG_COL_VALS_ADD` - процедура, служит для формирования коллекции предопределённых значений колонки таблицы (подготовленная коллекция передаётся в `RCOL_VALS` вызова `TDG_ADD_COL_DEF`, если необходимо)\
`PKG_P8PANELS_VISUAL.TDG_ADD_GROUP` - процедура, служит для добавления описания группы в таблицу данных, на вход принимает объект для хранения описания таблицы и параметры добавляемой группы\
@ -2000,7 +1900,7 @@ const Chart = ({ title }) => {
};
```
Полные актуальные исходные коды примеров можно увидеть в "db/PKG_P8PANELS_SAMPLES.pck" и "app/panels/samples/chart.js" данного репозитория соответственно.
Полные актуальные исходные коды примеров можно увидеть в "db/PKG_P8PANELS_SAMPLES.pck" и "app/panels/samples/data_grid.js" данного репозитория соответственно.
##### Диаграмма ганта "P8PGantt"
@ -2053,7 +1953,6 @@ 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` в диалоге редактора задачи\
@ -2963,61 +2862,6 @@ 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,49 +3,10 @@
Типовые стили
*/
//---------------------
//Подключение библиотек
//---------------------
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,21 +12,13 @@ export const TITLES = {
INFO: "Информация", //Информационный блок
WARN: "Предупреждение", //Блок предупреждения
ERR: "Ошибка", //Информация об ошибке
DEFAULT_PANELS_GROUP: "Без привязки к группе", //Заголовок группы панелей по умолчанию
DATA_SOURCE_CONFIG: "Настройка источника данных", //Заголовок для настройки источника данных
INSERT: "Добавление", //Заголовок для диалогов/форм добавления
UPDATE: "Исправление", //Заголовок для диалогов/форм исправления
CONFIG: "Настройка" //Заголовок для диалога настройки
DEFAULT_PANELS_GROUP: "Без привязки к группе" //Заголовок группы панелей по умолчанию
};
//Текст
export const TEXTS = {
LOADING: "Ожидайте...", //Ожидание завершения процесса
NO_DATA_FOUND: "Данных не найдено", //Отсутствие данных
NO_DATA_FOUND_SHORT: "Н.Д.", //Отсутствие данных (кратко)
NO_SETTINGS: "Настройки не определены", //Отстутсвие настроек
UNKNOWN_SOURCE_TYPE: "Неизвестный тип источника", //Отсуствие типа источника
UNNAMED_SOURCE: "Источник без наименования" //Отсутствие наименования источника
NO_DATA_FOUND: "Данных не найдено" //Отсутствие данных
};
//Текст кнопок
@ -37,19 +29,11 @@ export const BUTTONS = {
OK: "ОК", //Ок
CANCEL: "Отмена", //Отмена
CLOSE: "Закрыть", //Сокрытие
DETAIL: "Подробнее", //Отображение подробностей/детализации
HIDE: "Скрыть", //Скрытие информации
CLEAR: "Очистить", //Очистка
ORDER_ASC: "По возрастанию", //Сортировка по возрастанию
ORDER_DESC: "По убыванию", //Сортировка по убыванию
FILTER: "Фильтр", //Фильтрация
MORE: "Ещё", //Догрузка данных
APPLY: "Применить", //Сохранение без закрытия интерфейса ввода
SAVE: "Сохранить", //Сохранение
CONFIG: "Настроить", //Настройка
INSERT: "Добавить", //Добавление
UPDATE: "Исправить", //Исправление
DELETE: "Удалить" //Удаление
MORE: "Ещё" //Догрузка данных
};
//Метки атрибутов, сопроводительные надписи
@ -62,9 +46,7 @@ export const CAPTIONS = {
START: "Начало",
END: "Окончание",
PROGRESS: "Прогресс",
LEGEND: "Легенда",
USER_PROC: "Пользовательская процедура",
QUERY: "Запрос"
LEGEND: "Легенда"
};
//Типовые сообщения об ошибках
@ -72,20 +54,10 @@ export const ERRORS = {
UNDER_CONSTRUCTION: "Панель в разработке",
P8O_API_UNAVAILABLE: '"ПАРУС 8 Онлайн" недоступен',
P8O_API_UNSUPPORTED: 'Функция "ПАРУС 8 Онлайн" не поддерживается',
DEFAULT: "Неожиданная ошибка",
DATA_SOURCE_NO_REQ_ARGS: "Не заданы обязательные параметры источника данных"
DEFAULT: "Неожиданная ошибка"
};
//Типовые сообщения для ошибок HTTP
export const ERRORS_HTTP = {
404: "Адрес не найден"
};
//Типовые статусы
export const STATE = {
UNDEFINED: "UNDEFINED",
INFO: "INFORMATION",
OK: "OK",
ERR: "ERR",
WARN: "WARN"
};

View File

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

View File

@ -1,121 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Действия
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Icon, Button } from "@mui/material"; //Интерфейсные элементы
import { P8PCAEditor } from "./p8p_component_action/editor"; //Редактор действия
import { P8PChipList } from "./p8p_chip_list"; //Дополнительные настройки редактора
import { P8P_CA_SHAPE, P8P_CAS_INITIAL, P8P_CA_TYPE } from "./p8p_component_action/common"; //Общие ресурсы действий
//---------
//Константы
//---------
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
const getActionChipText = (type, params) => {
//Определяем от типа
switch (type) {
//Открыть раздел
case P8P_CA_TYPE.openUnit.code:
return params.unitName;
//Открыть панель
case P8P_CA_TYPE.openPanel.code:
return params.panelValue.value;
//Установить переменную
case P8P_CA_TYPE.setVariable.code:
return params.reduce((prev, cur) => [...prev, cur.variableSource], []).join(", ");
//Для всех остальных
default:
return "Не определен";
}
};
//-----------
//Тело модуля
//-----------
//Действия
const P8PActions = ({ actions = P8P_CAS_INITIAL, valueProviders = {}, areas = [], valueTypes = [], onChange = null } = {}) => {
//Собственное состояние - редактор действий
const [actionEditor, setActionEditor] = useState({ display: false, index: null });
//При изменении действий
const handleActionsChange = actions => onChange && onChange(actions);
//При добавлении действия
const handleActionAdd = () => setActionEditor({ display: true, index: null });
//При нажатии на действие
const handleActionClick = i => setActionEditor({ display: true, index: i });
//При удалении действия
const handleActionDelete = i => {
const newActions = [...actions];
newActions.splice(i, 1);
handleActionsChange(newActions);
};
//При отмене сохранения изменений действия
const handleActionCancel = () => setActionEditor({ display: false, index: null });
//При сохранении изменений действия
const handleActionSave = action => {
const newActions = [...actions];
actionEditor.index == null ? newActions.push({ ...action }) : (newActions[actionEditor.index] = { ...action });
handleActionsChange(newActions);
setActionEditor({ display: false, index: null });
};
//Определяем структуру действий для отображения
const actionChips = actions.map(item => {
//Собираем текст действия
let text = getActionChipText(item.type, item.params);
//Формируем структуру для отображения карточки действия
return { text: text, title: text, icon: P8P_CA_TYPE[item.type]?.icon, iconTitle: P8P_CA_TYPE[item.type]?.name };
});
//Формирование представления
return (
<>
{actionEditor.display && (
<P8PCAEditor
areas={areas}
valueTypes={valueTypes}
action={actionEditor.index !== null ? { ...actions[actionEditor.index] } : null}
onCancel={handleActionCancel}
onOk={handleActionSave}
valueProviders={valueProviders}
/>
)}
<P8PChipList items={actionChips} onClick={handleActionClick} onDelete={handleActionDelete} />
<Button startIcon={<Icon>add</Icon>} onClick={handleActionAdd}>
Добавить действие
</Button>
</>
);
};
//Контроль свойств компонента - действия
P8PActions.propTypes = {
actions: PropTypes.arrayOf(P8P_CA_SHAPE),
valueProviders: PropTypes.object,
areas: PropTypes.array,
valueTypes: PropTypes.array,
onChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PActions };

View File

@ -1,88 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Список элементов
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Chip, Typography, Stack, Icon } from "@mui/material"; //Интерфейсные элементы
import { STYLES as COMMON_STYLES } from "./p8p_editors_common"; //Общие ресурсы редакторов
//---------
//Константы
//---------
//Стили
const STYLES = {
CHIP_ITEM: { ...COMMON_STYLES.CHIP(true, false) },
STACK_ITEM: { alignItems: "center" },
TYPOGRAPHY_INFO: { overflow: "hidden" }
};
//-----------
//Тело модуля
//-----------
//Список элементов
const P8PChipList = ({ items, labelRender, onClick, onDelete }) => {
//При нажатии на элемент
const handleClick = index => onClick && onClick(index);
//При удалении элемента
const handleDelete = index => onDelete && onDelete(index);
//Формирование представления
return (
<>
{Array.isArray(items) &&
items.length > 0 &&
items.map((item, i) => (
<Chip
key={i}
label={
labelRender ? (
labelRender(item)
) : (
<Stack direction="row" spacing={1} sx={STYLES.STACK_ITEM}>
{item.icon ? (
<>
<Icon sx={{ color: "grey" }} title={item?.iconTitle}>
{item.icon}
</Icon>
<Typography variant="body2">-</Typography>
</>
) : null}
<Typography variant="body2" title={item?.title} sx={STYLES.TYPOGRAPHY_INFO}>
{item.text}
</Typography>
</Stack>
)
}
variant={"outlined"}
onClick={onClick && !item.disableClick ? () => handleClick(i) : null}
onDelete={onDelete && !item.disableDelete ? () => handleDelete(i) : null}
deleteIcon={item.deleteIcon ? <Icon>{item.deleteIcon}</Icon> : null}
sx={STYLES.CHIP_ITEM}
/>
))}
</>
);
};
//Контроль свойств - список элементов
P8PChipList.propTypes = {
items: PropTypes.array,
labelRender: PropTypes.func,
onClick: PropTypes.func,
onDelete: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PChipList };

View File

@ -1,148 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Общие ресурсы компонента "Редактор действия"
*/
//---------------------
//Подключение библиотек
//---------------------
import PropTypes from "prop-types"; //Контроль свойств компонента
//---------
//Константы
//---------
//Значения типа по умолчанию
const P8P_CA_DEF_TYPE_VALUE = {
TEXT_VALUE: "Значение",
VARIABLE: "Переменная"
};
//Типы значений по умолчанию
const P8P_CA_DEF_VALUE_TYPES = [...Object.values(P8P_CA_DEF_TYPE_VALUE)];
//Типы действия
const P8P_CA_TYPE = {
openUnit: { code: "openUnit", name: "Открыть раздел", icon: "article" },
openPanel: { code: "openPanel", name: "Открыть панель", icon: "storage" },
setVariable: { code: "setVariable", name: "Установить переменную", icon: "mediation" }
};
//Список полей связываемых с проводником значений
const P8P_CA_SOURCE_FIELDS = ["variableSource"];
//Список полей хранящих объект типа {"Тип", "Значение"}, где может храниться связь с проводником значений
const P8P_CA_OBJECT_SOURCE_FIELDS = ["resultValue", "tabValue", "panelValue"];
//Структура значения с типом
const P8P_CA_OBJECT_VALUE_SHAPE = PropTypes.shape({
type: PropTypes.string,
value: PropTypes.string
});
//Структура входных параметров метода вызова
const P8P_CA_INPUT_PARAM_SHAPE = PropTypes.shape({
parameter: PropTypes.string,
inputParameter: PropTypes.string,
resultValue: P8P_CA_OBJECT_VALUE_SHAPE
});
//Структура параметров действия "Открыть раздел"
const P8P_CA_OPEN_UNIT_PARAMS_SHAPE = PropTypes.shape({
unitCode: PropTypes.string,
unitName: PropTypes.string,
showMethod: PropTypes.string,
showMethodName: PropTypes.string,
inputParams: PropTypes.arrayOf(P8P_CA_INPUT_PARAM_SHAPE),
modal: PropTypes.bool
});
//Структура параметров действия "Открыть панель"
const P8P_CA_OPEN_PANEL_PARAMS_SHAPE = PropTypes.shape({
tabValue: P8P_CA_OBJECT_VALUE_SHAPE,
panelValue: P8P_CA_OBJECT_VALUE_SHAPE
});
//Структура связи переменной действия "Установить переменную"
const P8P_CA_VARIABLE_LINK_SHAPE = PropTypes.shape({
variableSource: PropTypes.string,
resultValue: P8P_CA_OBJECT_VALUE_SHAPE
});
//Структура действия
const P8P_CA_SHAPE = PropTypes.shape({
type: PropTypes.oneOf(Object.keys(P8P_CA_TYPE)).isRequired,
area: PropTypes.string,
element: PropTypes.string,
params: PropTypes.oneOfType([P8P_CA_OPEN_UNIT_PARAMS_SHAPE, P8P_CA_OPEN_PANEL_PARAMS_SHAPE, PropTypes.arrayOf(P8P_CA_VARIABLE_LINK_SHAPE)])
});
//Начальное состояние объекта значения с типом
const P8P_CA_OBJECT_VALUE_INITIAL = {
type: P8P_CA_DEF_TYPE_VALUE.TEXT_VALUE,
value: ""
};
//Начальное состояние входного параметра метода вызова
const P8P_CA_INPUT_PARAM_INITIAL = {
parameter: "",
inputParameter: "",
resultValue: { ...P8P_CA_OBJECT_VALUE_INITIAL }
};
//Начальное состояние параметров действия "Открыть раздел"
const P8P_CA_OPEN_UNIT_INITIAL = {
unitCode: "",
unitName: "",
showMethod: "",
showMethodName: "",
inputParams: [],
modal: true
};
//Начальное состояние параметров действия "Открыть панель"
const P8P_CA_OPEN_PANEL_INITIAL = {
tabValue: { ...P8P_CA_OBJECT_VALUE_INITIAL },
panelValue: { ...P8P_CA_OBJECT_VALUE_INITIAL }
};
//Начальное состояние параметра связи действия "Установить переменную"
const P8P_CA_VARIABLE_LINK_INITIAL = {
variableSource: "",
resultValue: { ...P8P_CA_OBJECT_VALUE_INITIAL }
};
//Начальное состояние действия
const P8P_CA_INITIAL = {
type: P8P_CA_TYPE.openUnit.code,
area: "",
element: "",
params: { ...P8P_CA_OPEN_UNIT_INITIAL }
};
//Начальное состояние действий
const P8P_CAS_INITIAL = [];
//----------------
//Интерфейс модуля
//----------------
export {
P8P_CA_DEF_TYPE_VALUE,
P8P_CA_DEF_VALUE_TYPES,
P8P_CA_TYPE,
P8P_CA_SOURCE_FIELDS,
P8P_CA_OBJECT_SOURCE_FIELDS,
P8P_CA_OBJECT_VALUE_SHAPE,
P8P_CA_OPEN_UNIT_PARAMS_SHAPE,
P8P_CA_OPEN_PANEL_PARAMS_SHAPE,
P8P_CA_VARIABLE_LINK_SHAPE,
P8P_CA_SHAPE,
P8P_CA_INPUT_PARAM_INITIAL,
P8P_CA_OPEN_UNIT_INITIAL,
P8P_CA_OPEN_PANEL_INITIAL,
P8P_CA_VARIABLE_LINK_INITIAL,
P8P_CA_INITIAL,
P8P_CAS_INITIAL
};

View File

@ -1,159 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Редактор действия "Открыть панель"
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box } from "@mui/material"; //Интерфейсные элементы
import { P8P_CA_DEF_TYPE_VALUE, P8P_CA_OPEN_PANEL_PARAMS_SHAPE } from "./common"; //Общие ресурсы действий
import { P8PCAFieldWithType } from "./field_with_type"; //Поле с выбором типа значения
import { PanelsManager } from "../../../panels/panels_editor/components/panels_manager/panels_manager"; //Менеджер панелей
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER_UNIT: { display: "flex", flexDirection: "column", gap: "10px" },
BOX_SETTINGS: { display: "flex", flexDirection: "row", gap: "15px" }
};
//Ключ значений о странице
const SPROP_TAB = "tabValue";
//Ключа значений о панели
const SPROP_PANEL = "panelValue";
//-----------
//Тело модуля
//-----------
//Редактор действия "Открыть панель"
const P8PCAPanelOpenOptions = ({ panel, valueTypes, isValues, onStateChange, onValueSourceMenuClick }) => {
//Собственное состояние - Отображение менеджера панелей
const [openPanelsManager, setOpenPanelsManager] = useState(false);
//При открытии менеджера панелей
const handlePanelsManagerOpen = () => setOpenPanelsManager(true);
//При закрытии менеджера панелей
const handlePanelsManagerClose = () => setOpenPanelsManager(false);
//При выборе панели в менеджере панелей
const handlePanelSelect = selectedPanel => {
//Устанавливаем мнемокод выбранной панели
onStateChange({ ...panel, panelValue: { ...panel.panelValue, value: selectedPanel.code } });
//Закрываем менеджер панелей
handlePanelsManagerClose();
};
//При изменении типа значения
const handleValueTypeChange = (value, keyProp) =>
onStateChange({
[keyProp]: { type: value, value: "" }
});
//При изменении значения
const handleValueChange = (value, keyProp) =>
onStateChange({
[keyProp]: { ...panel[keyProp], value: value }
});
//При очистке значения
const handleClearValueClick = keyProp =>
onStateChange({
[keyProp]: { ...panel[keyProp], value: "" }
});
//При изменении переменной
const handleValueSourceChange = (valueSource, keyProp) =>
onStateChange({
[keyProp]: { ...panel[keyProp], value: valueSource }
});
//При нажатии на выбор переменной
const handleValueSourceSelect = (e, keyProp) =>
isValues
? onValueSourceMenuClick &&
onValueSourceMenuClick(e, valueSource => {
handleValueSourceChange(valueSource, keyProp);
})
: null;
//Формирование представления
return (
<Box sx={STYLES.CONTAINER_UNIT}>
{openPanelsManager && (
<PanelsManager
current={panel.panelValue.value}
isEditable={false}
onPanelSelect={handlePanelSelect}
onCancel={handlePanelsManagerClose}
/>
)}
<Box sx={STYLES.BOX_SETTINGS}>
<P8PCAFieldWithType
valueTypes={valueTypes}
name={SPROP_TAB}
valueLabel={"Наименование вкладки"}
groupLabel={"Настройки вкладки"}
item={panel.tabValue}
isValueReadOnly={panel.tabValue.type === P8P_CA_DEF_TYPE_VALUE.VARIABLE}
onTypeChange={e => handleValueTypeChange(e.target.value, SPROP_TAB)}
onValueChange={e => handleValueChange(e.target.value, SPROP_TAB)}
endAdornments={[
{ icon: "clear", onClick: () => handleClearValueClick(SPROP_TAB), isDisabled: false },
{
icon: "settings_ethernet",
onClick: e => handleValueSourceSelect(e, SPROP_TAB),
isDisabled: panel.tabValue.type !== P8P_CA_DEF_TYPE_VALUE.VARIABLE
}
]}
/>
<P8PCAFieldWithType
valueTypes={valueTypes}
name={SPROP_PANEL}
valueLabel={"Панель"}
groupLabel={"Настройки панели"}
item={panel.panelValue}
isValueReadOnly={panel.panelValue.type === P8P_CA_DEF_TYPE_VALUE.VARIABLE}
onTypeChange={e => handleValueTypeChange(e.target.value, SPROP_PANEL)}
onValueChange={e => handleValueChange(e.target.value, SPROP_PANEL)}
endAdornments={[
{ icon: "clear", onClick: () => handleClearValueClick(SPROP_PANEL), isDisabled: false },
{
icon: "list",
onClick: handlePanelsManagerOpen,
isDisabled: panel.panelValue.type === P8P_CA_DEF_TYPE_VALUE.VARIABLE
},
{
icon: "settings_ethernet",
onClick: e => handleValueSourceSelect(e, SPROP_PANEL),
isDisabled: panel.panelValue.type !== P8P_CA_DEF_TYPE_VALUE.VARIABLE
}
]}
/>
</Box>
</Box>
);
};
//Контроль свойств - редактор действия "Открыть панель"
P8PCAPanelOpenOptions.propTypes = {
panel: P8P_CA_OPEN_PANEL_PARAMS_SHAPE,
valueTypes: PropTypes.array,
isValues: PropTypes.bool.isRequired,
onStateChange: PropTypes.func.isRequired,
onValueSourceMenuClick: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { P8PCAPanelOpenOptions };

View File

@ -1,285 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Редактор действия "Открыть раздел"
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { TextField, Box, InputAdornment, IconButton, Icon, FormControlLabel, Checkbox } from "@mui/material"; //Интерфейсные элементы
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
import { P8P_CA_OPEN_UNIT_PARAMS_SHAPE, P8P_CA_INPUT_PARAM_INITIAL, P8P_CA_DEF_TYPE_VALUE } from "./common"; //Общие ресурсы действий
import { P8PCAFieldWithType } from "./field_with_type"; //Поле с выбором типа значения
import { P8PCATablePropValues } from "./table_prop_values"; //Таблица значений свойств действия
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER_UNIT: { display: "flex", flexDirection: "column", gap: "10px" }
};
//-----------
//Тело модуля
//-----------
//Редактор действия "Открыть раздел"
const P8PCAUnitOpenOptions = ({ unit, valueTypes, isValues, onStateChange, onValueSourceMenuClick }) => {
//Подключение к контексту приложения
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
//При изменении параметра открытия
const handleModalChange = () => onStateChange({ modal: !unit.modal });
//При нажатии на очистку раздела
const handleClearUnitClick = () =>
onStateChange({
unitCode: "",
unitName: "",
showMethod: ""
});
//При нажатии на выбор раздела
const handleSelectUnitClick = () => {
pOnlineShowDictionary({
unitCode: "Units",
showMethod: "methods",
inputParameters: [
{ name: "pos_unit_name", value: unit.unitName },
{ name: "pos_method_name", value: unit.showMethodName }
],
callBack: res =>
res.success &&
onStateChange({
unitCode: res.outParameters.unit_code,
unitName: res.outParameters.unit_name,
showMethod: res.outParameters.method_code,
showMethodName: res.outParameters.method_name
})
});
};
//При добавлении связи входного параметра
const handleInputParameAdd = () => {
onStateChange({
inputParams: [...unit.inputParams, { ...P8P_CA_INPUT_PARAM_INITIAL }]
});
};
//При удалении связи входного параметра
const handleInputParamDelete = index => {
//Копируем связываемые переменные
let newInputParams = [...unit.inputParams];
//Удаляем по индексу
newInputParams.splice(index, 1);
//Устанавливаем новый массив
onStateChange({ inputParams: [...newInputParams] });
};
//При очистке входного параметра
const handleClearInputParameterClick = index => {
//Копируем связываемые переменные
let newInputParams = [...unit.inputParams];
//Очищаем значение по индексу
newInputParams[index]["parameter"] = "";
newInputParams[index]["inputParameter"] = "";
//Устанавливаем новый массив
onStateChange({ inputParams: [...newInputParams] });
};
//При нажатии на выбор параметра метода вызова
const handleSelectUnitParameterClick = index => {
unit.unitCode &&
unit.showMethod &&
pOnlineShowDictionary({
unitCode: "UnitParams",
showMethod: "main",
inputParameters: [
{ name: "in_UNITCODE", value: unit.unitCode },
{ name: "in_PARENT_METHOD_CODE", value: unit.showMethod },
{ name: "in_PARAMNAME", value: unit.inputParams[index].parameter }
],
callBack: res => {
if (res.success) {
//Копируем связываемые переменные
let newInputParams = [...unit.inputParams];
//Очищаем значение по индексу
newInputParams[index]["parameter"] = res.outParameters.out_PARAMNAME;
newInputParams[index]["inputParameter"] = res.outParameters.out_IN_CODE;
//Устанавливаем новый массив
onStateChange({ inputParams: [...newInputParams] });
}
}
});
};
//При изменении типа значения входного параметра
const handleInputParamValueTypeChange = (value, index) => {
//Копируем связываемые переменные
let newInputParams = [...unit.inputParams];
//Очищаем значение по индексу
newInputParams[index].resultValue = { ...newInputParams[index].resultValue, type: value, value: "" };
//Устанавливаем новый массив
onStateChange({ inputParams: [...newInputParams] });
};
//При очистке значения параметра
const handleClearInputParamValueClick = index => {
//Копируем связываемые переменные
let newInputParams = [...unit.inputParams];
//Очищаем значение по индексу
newInputParams[index].resultValue = { ...newInputParams[index].resultValue, value: "" };
//Устанавливаем новый массив
onStateChange({ inputParams: [...newInputParams] });
};
//При изменении значения входного параметра
const handleInputParamValueChange = (value, index) => {
//Копируем связываемые переменные
let newInputParams = [...unit.inputParams];
//Очищаем значение по индексу
newInputParams[index].resultValue = { ...newInputParams[index].resultValue, value: value };
//Устанавливаем новый массив
onStateChange({ inputParams: [...newInputParams] });
};
//При связывании значения входного параметра с переменной
const handleInputParamValueSourceChange = (valueSource, index) => {
//Копируем связываемые переменные
let newInputParams = [...unit.inputParams];
//Очищаем значение по индексу
newInputParams[index].resultValue = { ...newInputParams[index].resultValue, value: valueSource };
//Устанавливаем новый массив
onStateChange({ inputParams: [...newInputParams] });
};
//При нажатии на выбор переменной
const handleValueSourceSelect = (e, index) =>
isValues
? onValueSourceMenuClick &&
onValueSourceMenuClick(e, valueSource => {
handleInputParamValueSourceChange(valueSource, index);
})
: null;
//Рендер отображения колонки свойства
const handlePropertyCellRender = (item, index) => {
//Значение первой колонки
return (
<TextField
id={`parameter_${index}`}
InputLabelProps={{ shrink: true }}
type={"text"}
variant={"standard"}
value={item.parameter}
name={"parameter"}
fullWidth
InputProps={{
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => handleClearInputParameterClick(index)}>
<Icon>clear</Icon>
</IconButton>
<IconButton onClick={() => handleSelectUnitParameterClick(index)} disabled={!unit.showMethodName}>
<Icon>list</Icon>
</IconButton>
</InputAdornment>
)
}}
/>
);
};
//Рендер отображения колонки значения
const handleValueCellRender = (item, index) => {
//Формирование представления колонки значения
return (
<P8PCAFieldWithType
valueTypes={valueTypes}
name={`inputValue_${index}`}
item={item.resultValue}
isValueReadOnly={item.resultValue.type === P8P_CA_DEF_TYPE_VALUE.VARIABLE}
onTypeChange={e => handleInputParamValueTypeChange(e.target.value, index)}
onValueChange={e => handleInputParamValueChange(e.target.value, index)}
endAdornments={[
{ icon: "clear", onClick: () => handleClearInputParamValueClick(index), isDisabled: false },
{
icon: "settings_ethernet",
onClick: e => handleValueSourceSelect(e, index),
isDisabled: item.resultValue.type !== P8P_CA_DEF_TYPE_VALUE.VARIABLE
}
]}
/>
);
};
//Формирование представления
return (
<Box sx={STYLES.CONTAINER_UNIT}>
<TextField
type={"text"}
variant={"standard"}
value={unit.unitName}
label={"Раздел"}
InputLabelProps={{ shrink: unit.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>
)
}}
required={true}
/>
<TextField
type={"text"}
variant={"standard"}
value={unit.showMethodName}
label={"Метод вызова"}
InputLabelProps={{ shrink: unit.showMethodName ? true : false }}
InputProps={{
readOnly: true
}}
required={true}
/>
<P8PCATablePropValues
name={"Входные параметры"}
items={unit.inputParams}
propertyCellName={"Входной параметр"}
valueCellName={"Значение"}
onPropertyCellRender={(item, index) => handlePropertyCellRender(item, index)}
onValueCellRender={(item, index) => handleValueCellRender(item, index)}
onAddRow={handleInputParameAdd}
onRowDelete={index => handleInputParamDelete(index)}
/>
<FormControlLabel control={<Checkbox checked={unit.modal} onChange={handleModalChange} />} label="Открывать модально" />
</Box>
);
};
//Контроль свойств - редактор действия "Открыть раздел"
P8PCAUnitOpenOptions.propTypes = {
unit: P8P_CA_OPEN_UNIT_PARAMS_SHAPE,
valueTypes: PropTypes.array,
isValues: PropTypes.bool.isRequired,
onStateChange: PropTypes.func.isRequired,
onValueSourceMenuClick: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { P8PCAUnitOpenOptions };

View File

@ -1,202 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Редактор действия "Установить переменную"
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { TextField, InputAdornment, IconButton, Icon } from "@mui/material"; //Интерфейсные элементы
import { P8P_CA_VARIABLE_LINK_INITIAL, P8P_CA_VARIABLE_LINK_SHAPE, P8P_CA_DEF_TYPE_VALUE } from "./common"; //Общие ресурсы действий
import { P8PCAFieldWithType } from "./field_with_type"; //Поле с выбором типа значения
import { P8PCATablePropValues } from "./table_prop_values"; //Таблица значений свойств действия
//---------
//Константы
//---------
//-----------
//Тело модуля
//-----------
//Редактор действия "Установить переменную"
const P8PCAVarSetOptions = ({ variables, valueTypes, isValues, onStateChange, onValueSourceMenuClick }) => {
//При добавлении связи переменной
const handleVariableAdd = () => {
onStateChange([...variables, { ...P8P_CA_VARIABLE_LINK_INITIAL }]);
};
//При удалении связи переменной
const handleVariableDelete = index => {
//Копируем связываемые переменные
let newVariables = [...variables];
//Удаляем по индексу
newVariables.splice(index, 1);
//Устанавливаем новый массив
onStateChange([...newVariables]);
};
//При очистке значения переменной
const handleClearVariableClick = index => {
//Копируем текущее состояние связываемых переменных
let newParams = [...variables];
//Очищаем значения у элемента по индексу
newParams[index].variableSource = "";
//Устанавливаем новый массив
onStateChange([...newParams]);
};
//При связывании переменной
const handleVariablesSourceChange = (valueSource, index) => {
//Копируем связываемые переменные
let newVariables = [...variables];
//Очищаем значение по индексу
newVariables[index].variableSource = valueSource;
//Устанавливаем новый массив
onStateChange([...newVariables]);
};
//При изменении типа значения
const handleResultValueTypeChange = (value, index) => {
//Копируем связываемые переменные
let newVariables = [...variables];
//Очищаем значение по индексу
newVariables[index].resultValue = { ...newVariables[index].resultValue, type: value, value: "" };
//Устанавливаем новый массив
onStateChange([...newVariables]);
};
//При очистке значения
const handleClearResultValueClick = index => {
//Копируем текущее состояние связываемых переменных
let newVariables = [...variables];
//Очищаем значения у элемента по индексу
newVariables[index].resultValue = { ...newVariables[index].resultValue, value: "" };
//Устанавливаем новый массив
onStateChange([...newVariables]);
};
//При ручном изменении значения
const handleResultValueChange = (value, index) => {
//Копируем связываемые переменные
let newVariables = [...variables];
//Очищаем значение по индексу
newVariables[index].resultValue = { ...newVariables[index].resultValue, value: value };
//Устанавливаем новый массив
onStateChange([...newVariables]);
};
//При связывании значения
const handleResultValueSourceChange = (valueSource, index) => {
//Копируем связываемые переменные
let newVariables = [...variables];
//Очищаем значение по индексу
newVariables[index].resultValue = { ...newVariables[index].resultValue, value: valueSource };
//Устанавливаем новый массив
onStateChange([...newVariables]);
};
//При нажатии на выбор переменной (значение)
const handleValueSourceSelect = (e, index) =>
isValues
? onValueSourceMenuClick &&
onValueSourceMenuClick(e, valueSource => {
handleResultValueSourceChange(valueSource, index);
})
: null;
//При нажатии на выбор переменной (переменная)
const handleVariableSourceSelect = (e, index) =>
isValues
? onValueSourceMenuClick &&
onValueSourceMenuClick(e, valueSource => {
handleVariablesSourceChange(valueSource, index);
})
: null;
//Рендер отображения колонки свойства
const handlePropertyCellRender = (item, index) => {
//Формирование представления колонки свойства
return (
<TextField
id={`variableSource_${index}`}
InputLabelProps={{ shrink: true }}
type={"text"}
variant={"standard"}
value={item.variableSource}
name={"variableSource"}
fullWidth
InputProps={{
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => handleClearVariableClick(index)}>
<Icon>clear</Icon>
</IconButton>
{isValues && (
<IconButton onClick={e => handleVariableSourceSelect(e, index)}>
<Icon>settings_ethernet</Icon>
</IconButton>
)}
</InputAdornment>
)
}}
/>
);
};
//Рендер отображения колонки значения
const handleValueCellRender = (item, index) => {
//Формирование представления колонки значения
return (
<P8PCAFieldWithType
valueTypes={valueTypes}
name={`resultValue_${index}`}
item={item.resultValue}
isValueReadOnly={item.resultValue.type === P8P_CA_DEF_TYPE_VALUE.VARIABLE}
onTypeChange={e => handleResultValueTypeChange(e.target.value, index)}
onValueChange={e => handleResultValueChange(e.target.value, index)}
endAdornments={[
{ icon: "clear", onClick: () => handleClearResultValueClick(index), isDisabled: false },
{
icon: "settings_ethernet",
onClick: e => handleValueSourceSelect(e, index),
isDisabled: item.resultValue.type !== P8P_CA_DEF_TYPE_VALUE.VARIABLE
}
]}
/>
);
};
//Формирование представления
return (
<P8PCATablePropValues
name={"Список параметров"}
items={variables}
propertyCellName={"Переменная"}
valueCellName={"Значение"}
onPropertyCellRender={(item, index) => handlePropertyCellRender(item, index)}
onValueCellRender={(item, index) => handleValueCellRender(item, index)}
onAddRow={handleVariableAdd}
onRowDelete={index => handleVariableDelete(index)}
/>
);
};
//Контроль свойств - редактор действия "Установить переменную"
P8PCAVarSetOptions.propTypes = {
variables: PropTypes.arrayOf(P8P_CA_VARIABLE_LINK_SHAPE),
valueTypes: PropTypes.array,
isValues: PropTypes.bool.isRequired,
onStateChange: PropTypes.func.isRequired,
onValueSourceMenuClick: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { P8PCAVarSetOptions };

View File

@ -1,250 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Редактор действия
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { TextField, Select, Menu, MenuItem, FormControl, InputLabel, Stack, Box } from "@mui/material"; //Интерфейсные элементы
import { TITLES } from "../../../../app.text"; //Общие текстовые ресурсы
import { P8PConfigDialog } from "../p8p_config_dialog"; //Диалог настройки
import { deepCopyObject } from "../../../core/utils"; //Вспомогательные функции
import { P8P_CA_DEF_VALUE_TYPES, P8P_CA_TYPE, P8P_CA_SHAPE, P8P_CA_INPUT_PARAM_INITIAL, P8P_CA_OPEN_UNIT_INITIAL, P8P_CA_INITIAL } from "./common"; //Общие ресурсы действий
import { P8PCAUnitOpenOptions } from "./config_unit_open"; //Компонент редактора действия "Открыть раздел"
import { P8PCAPanelOpenOptions } from "./config_panel_open"; //Компонент редактора действия "Открыть панель"
import { P8PCAVarSetOptions } from "./config_var_set"; //Компонент редактора действия "Установить переменную"
import { isActionOkDisabled, getActionTypeParams } from "./util"; //Вспомогательные ресурсы действий
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER: { display: "flex", flexDirection: "column", gap: "10px", minWidth: "300px" }
};
//-----------
//Тело модуля
//-----------
//Редактор действия
const P8PCAEditor = ({ areas = [], valueTypes = [], action = null, onOk = null, onCancel = null, valueProviders = {} }) => {
//Собственное состояние - признак инициализиации
const [init, setInit] = useState(true);
//Собственное состояние - параметры действия
const [state, setState] = useState({});
//Собственное состояние - доступные типы значений компонента
const [availableValueTypes, setAvailableValueTypes] = useState([]);
//Собственное состояние - доступные области компонента
const [availableAreas, setAvailableAreas] = useState([]);
//Собственное состояние - доступность поля "Элемент"
const [hasElement, setHasElement] = useState(false);
//Собственное состояние - элемент привязки меню выбора источника
const [valueProvidersMenuAnchorEl, setValueProvidersMenuAnchorEl] = useState({
target: null,
onChange: null
});
//Открытие/сокрытие меню выбора источника
const toggleValueProvidersMenu = (target, onChange) =>
setValueProvidersMenuAnchorEl(target instanceof Element ? { target, onChange } : { target: null, onChange: null });
//При отображении меню связывания значения с поставщиком данных
const handleValueSourceLinkMenuClick = (e, onChange) => setValueProvidersMenuAnchorEl({ target: e.currentTarget, onChange });
//При закрытии редактора с сохранением
const handleOk = () => onOk && onOk({ ...state });
//При закрытии редактора с отменой
const handleCancel = () => onCancel && onCancel();
//При изменении типа действия
const handleTypeChange = e => {
//Иницилизируем параметры типа
let newParams = getActionTypeParams(e.target.value);
//Изменяем тип действия с изменение структуры параметров
setState(pv => ({
...pv,
type: e.target.value,
params: Array.isArray(newParams) ? [...newParams] : { ...newParams }
}));
};
//При ручном изменении общего параметра действия
const handleChange = e => setState(pv => ({ ...pv, [e.target.name]: e.target.value }));
//При изменении области действия
const handleAreaChange = e => {
//Устанавливаем значение
setState(pv => ({ ...pv, [e.target.name]: e.target.value, name: "" }));
//Устанавливаем доступность "Элемент"
setHasElement(availableAreas.find(item => item.area === e.target.value).hasElement);
};
//При изменении полей параметров действия
const handleParamsChange = params => {
setState(pv => ({ ...pv, params: { ...pv.params, ...params } }));
};
//При изменении переменных параметров действия "Установить переменную"
const handleVariablesParamsChange = variables => {
setState(pv => ({ ...pv, params: [...variables] }));
};
//Список значений
const values = Object.keys(valueProviders);
//Наличие значений
const isValues = values && values.length > 0 ? true : false;
//Меню привязки к поставщикам значений
const valueProvidersMenu = isValues && (
<Menu anchorEl={valueProvidersMenuAnchorEl.target} open={Boolean(valueProvidersMenuAnchorEl.target)} onClose={toggleValueProvidersMenu}>
{values.map((value, i) => (
<MenuItem
key={i}
onClick={() => {
//Выполняем выбор параметра
valueProvidersMenuAnchorEl.onChange(value);
//Закрываем меню выбора переменной
toggleValueProvidersMenu();
}}
>
{value}
</MenuItem>
))}
</Menu>
);
//При инициализации действия
useEffect(() => {
//Если это иницализация
if (init) {
//Если это открытие действия - берем его параметры, иначе - инициализируем изначальными
const initAction = action
? action
: {
...P8P_CA_INITIAL,
area: areas[0].area,
params: { ...P8P_CA_OPEN_UNIT_INITIAL, inputParams: [{ ...P8P_CA_INPUT_PARAM_INITIAL }] }
};
//Устанавливаем параметры действия
setState(deepCopyObject(initAction));
//Заполняем доступные области
setAvailableAreas([...areas]);
//Определяем доступность "Элемент"
setHasElement(areas.find(item => item.area === initAction.area)?.hasElement || false);
//Заполняем доступные типы значений
setAvailableValueTypes([...P8P_CA_DEF_VALUE_TYPES, ...valueTypes]);
//Сбрасываем признак инициализации
setInit(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [init]);
//Формирование представления
return (
<>
{!init ? (
<P8PConfigDialog
title={`${action ? TITLES.UPDATE : TITLES.INSERT} действия`}
onOk={handleOk}
onCancel={handleCancel}
width={"xl"}
okDisabled={isActionOkDisabled(state)}
>
<Stack direction={"column"} spacing={1}>
{valueProvidersMenu}
<Box sx={STYLES.CONTAINER}>
{availableAreas.length !== 0 ? (
<FormControl variant={"standard"} fullWidth>
<InputLabel id={"areaLabel-label"}>Область</InputLabel>
<Select name={"area"} value={state.area} labelId={"area-label"} label={"Область"} onChange={handleAreaChange}>
{availableAreas.map((item, index) => (
<MenuItem value={item.area} key={index}>
{item.name}
</MenuItem>
))}
</Select>
</FormControl>
) : null}
{hasElement && (
<TextField
type={"text"}
variant={"standard"}
value={state.element}
label={"Элемент"}
name={"element"}
onChange={handleChange}
/>
)}
<FormControl variant={"standard"} fullWidth>
<InputLabel id={"type-label"}>Тип</InputLabel>
<Select name={"type"} value={state.type} labelId={"type-label"} label={"Тип"} onChange={handleTypeChange}>
{Object.keys(P8P_CA_TYPE).map((item, index) => (
<MenuItem value={P8P_CA_TYPE[item].code} key={index}>
{P8P_CA_TYPE[item].name}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
{state.type === P8P_CA_TYPE.openUnit.code && (
<P8PCAUnitOpenOptions
unit={state.params}
valueTypes={availableValueTypes}
isValues={isValues}
onStateChange={handleParamsChange}
onValueSourceMenuClick={handleValueSourceLinkMenuClick}
/>
)}
{state.type === P8P_CA_TYPE.openPanel.code && (
<P8PCAPanelOpenOptions
panel={state.params}
valueTypes={availableValueTypes}
isValues={isValues}
onStateChange={handleParamsChange}
onValueSourceMenuClick={handleValueSourceLinkMenuClick}
/>
)}
{state.type === P8P_CA_TYPE.setVariable.code && (
<P8PCAVarSetOptions
variables={state.params}
valueTypes={availableValueTypes}
isValues={isValues}
onStateChange={handleVariablesParamsChange}
onValueSourceMenuClick={handleValueSourceLinkMenuClick}
/>
)}
</Stack>
</P8PConfigDialog>
) : null}
</>
);
};
//Контроль свойств - Редактор условия
P8PCAEditor.propTypes = {
areas: PropTypes.array,
valueTypes: PropTypes.array,
action: P8P_CA_SHAPE,
onOk: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
valueProviders: PropTypes.object
};
//----------------
//Интерфейс модуля
//----------------
export { P8PCAEditor };

View File

@ -1,105 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Поле с выбором типа значения
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { TextField, Select, MenuItem, FormControl, Box, InputAdornment, IconButton, Icon, FormLabel } from "@mui/material"; //Интерфейсные элементы
import { P8P_CA_OBJECT_VALUE_SHAPE } from "./common"; //Общие ресурсы действий
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER: { display: "flex", flexDirection: "column", gap: "10px", alignItems: "center" },
LEGEND_SETTINGS: { paddingTop: "10px", marginBottom: "-5px" }
};
//-----------
//Тело модуля
//-----------
//Поле с выбором типа значения
const P8PCAFieldWithType = ({
valueTypes,
item,
name,
valueLabel = null,
groupLabel = null,
isValueReadOnly = false,
onTypeChange,
onValueChange = null,
endAdornments = []
}) => {
//Формирование представления
return (
<Box sx={STYLES.CONTAINER}>
{groupLabel ? (
<FormLabel component="legend" sx={STYLES.LEGEND_SETTINGS}>
{groupLabel}
</FormLabel>
) : null}
<FormControl variant={"standard"} fullWidth>
<Select name={`${name}_type`} value={item.type} labelId={`${name}-type-label`} onChange={onTypeChange} fullWidth>
{valueTypes.map((type, index) => (
<MenuItem value={type} key={index}>
{type}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
id={`${name}_value`}
InputLabelProps={{ shrink: true }}
type={"text"}
variant={"standard"}
value={item.value}
name={`${name}`}
label={valueLabel}
onChange={e => {
onValueChange && onValueChange(e);
}}
fullWidth
InputProps={{
readOnly: isValueReadOnly,
endAdornment:
endAdornments.length !== 0 ? (
<InputAdornment position="end">
{endAdornments.map((item, index) => (
<IconButton onClick={e => item.onClick(e)} key={index} disabled={item.isDisabled}>
<Icon>{item.icon}</Icon>
</IconButton>
))}
</InputAdornment>
) : null
}}
/>
</Box>
);
};
//Контроль свойств - поле с выбором типа значения
P8PCAFieldWithType.propTypes = {
valueTypes: PropTypes.array.isRequired,
item: P8P_CA_OBJECT_VALUE_SHAPE.isRequired,
name: PropTypes.string.isRequired,
valueLabel: PropTypes.string,
groupLabel: PropTypes.string,
isValueReadOnly: PropTypes.bool,
onTypeChange: PropTypes.func.isRequired,
onValueChange: PropTypes.func,
endAdornments: PropTypes.arrayOf(PropTypes.object)
};
//----------------
//Интерфейс модуля
//----------------
export { P8PCAFieldWithType };

View File

@ -1,99 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Таблица значений свойств действия
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, IconButton, Icon, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Typography } from "@mui/material"; //Интерфейсные элементы
import { APP_STYLES } from "../../../../app.styles"; //Типовые стили
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER_VARIABLES: { maxHeight: "240px", overflow: "auto", ...APP_STYLES.SCROLL },
BOX_VARIABLES_INFO: { display: "flex", justifyContent: "space-between", alignItems: "center", paddingTop: "10px" },
TABLE_HEAD_CELL_PROPERTY: { width: "45%", padding: "8px" },
TABLE_HEAD_CELL_VALUE: { width: "45%", padding: "8px" },
TABLE_HEAD_CELL_DELETE: { width: "10%", padding: "8px" },
TABLE_VARIABLES: { maxWidth: "700px", minWidth: "700px" },
TABLE_ROW_VARIABLES: { "&:last-child td, &:last-child th": { border: 0 } },
TABLE_CELL_VARIABLES: { padding: "8px 16px" }
};
//-----------
//Тело модуля
//-----------
//Таблица значений свойств действия
const P8PCATablePropValues = ({ name, items, propertyCellName, valueCellName, onPropertyCellRender, onValueCellRender, onAddRow, onRowDelete }) => {
//Формирование представления
return (
<>
<Box sx={STYLES.BOX_VARIABLES_INFO}>
<Typography pl={1}>{name}</Typography>
<IconButton onClick={onAddRow}>
<Icon>add</Icon>
</IconButton>
</Box>
<TableContainer component={Paper} sx={STYLES.CONTAINER_VARIABLES}>
<Table sx={STYLES.TABLE_VARIABLES} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="center" sx={STYLES.TABLE_HEAD_CELL_PROPERTY}>
{propertyCellName}
</TableCell>
<TableCell align="center" sx={STYLES.TABLE_HEAD_CELL_VALUE}>
{valueCellName}
</TableCell>
<TableCell align="center" sx={STYLES.TABLE_HEAD_CELL_DELETE}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{Array.isArray(items) &&
items.map((item, i) => (
<TableRow key={i} sx={STYLES.TABLE_ROW_VARIABLES}>
<TableCell align="center" sx={STYLES.TABLE_CELL_VARIABLES}>
{onPropertyCellRender ? onPropertyCellRender(item, i) : null}
</TableCell>
<TableCell align="center" sx={STYLES.TABLE_CELL_VARIABLES}>
{onValueCellRender ? onValueCellRender(item, i) : null}
</TableCell>
<TableCell align="center" sx={STYLES.TABLE_CELL_VARIABLES}>
<IconButton onClick={() => onRowDelete(i)}>
<Icon>clear</Icon>
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
);
};
//Контроль свойств - таблица значений свойств действия
P8PCATablePropValues.propTypes = {
name: PropTypes.string,
items: PropTypes.array,
propertyCellName: PropTypes.string,
valueCellName: PropTypes.string,
onPropertyCellRender: PropTypes.func,
onValueCellRender: PropTypes.func,
onAddRow: PropTypes.func,
onRowDelete: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PCATablePropValues };

View File

@ -1,212 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Вспомогательные ресурсы компонента "Редактор действия"
*/
//---------------------
//Подключение библиотек
//---------------------
import { genUID } from "../../../core/utils"; //Вспомогательные функции
import {
P8P_CA_DEF_TYPE_VALUE,
P8P_CA_TYPE,
P8P_CA_SOURCE_FIELDS,
P8P_CA_OBJECT_SOURCE_FIELDS,
P8P_CA_OPEN_UNIT_INITIAL,
P8P_CA_INPUT_PARAM_INITIAL,
P8P_CA_OPEN_PANEL_INITIAL,
P8P_CA_VARIABLE_LINK_INITIAL
} from "./common"; //Общие ресурсы действий
//---------
//Константы
//---------
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Доступность сохранения настроек действия
const isActionOkDisabled = action => {
//Если область или тип не указаны
if (!action.area || !action.type) return true;
//Исходим от типа
switch (action.type) {
//Открыть раздел
case P8P_CA_TYPE.openUnit.code:
return !action.params.unitCode || !action.params.showMethod;
//Открыть панель
case P8P_CA_TYPE.openPanel.code:
return !action.params.tabValue.type || !action.params.tabValue.value || !action.params.panelValue.type || !action.params.panelValue.value;
//Установить переменную
case P8P_CA_TYPE.setVariable.code:
return action.params.length === 0 || action.params.some(item => !item.variableSource);
//Для всех остальных
default:
return false;
}
};
//Считывание параметров типа действия
const getActionTypeParams = type => {
//Определяем от типа
switch (type) {
//Открыть раздел
case P8P_CA_TYPE.openUnit.code:
return { ...P8P_CA_OPEN_UNIT_INITIAL, inputParams: [{ ...P8P_CA_INPUT_PARAM_INITIAL }] };
//Открыть панель
case P8P_CA_TYPE.openPanel.code:
return { ...P8P_CA_OPEN_PANEL_INITIAL };
//Установить переменную
case P8P_CA_TYPE.setVariable.code:
return [{ ...P8P_CA_VARIABLE_LINK_INITIAL }];
//Для всех остальных
default:
return {};
}
};
//Определение значения действия по типу
const getActionValueByType = (type, value, values, prms = {}, getCustomTypeValue = null) => {
//Исходим от типа действия
switch (type) {
//Значение (стандартное)
case P8P_CA_DEF_TYPE_VALUE.TEXT_VALUE:
return value;
//Переменная (стандартное)
case P8P_CA_DEF_TYPE_VALUE.VARIABLE:
return values[value];
//Кастомный тип
default:
return getCustomTypeValue ? getCustomTypeValue({ type, value, values, prms }) : null;
}
};
//Определения функции действия
const getActionFunction = (item, onOpenUnit, configUrlBase, onOpenPanel, onProviderChange, getCustomTypeValue = null) => {
//Выполняем исходя из типа действия
switch (item.type) {
//Открыть раздел
case P8P_CA_TYPE.openUnit.code: {
return ({ event, values, prms }) => {
onOpenUnit({
unitCode: item.params.unitCode,
showMethod: item.params.showMethod,
inputParameters: item.params.inputParams.reduce(
(prev, cur) =>
cur.inputParameter
? [
...prev,
{
name: cur.inputParameter,
value: getActionValueByType(cur.resultValue.type, cur.resultValue.value, values, prms, getCustomTypeValue)
}
]
: [...prev],
[]
),
modal: item.params.modal
});
event?.stopPropagation && event.stopPropagation();
};
}
//Открыть панель
case P8P_CA_TYPE.openPanel.code: {
return ({ event, values, prms }) => {
onOpenPanel({
id: genUID(),
url: `${configUrlBase}panels_editor?SCODE=${getActionValueByType(
item.params.panelValue.type,
item.params.panelValue.value,
values,
prms,
getCustomTypeValue
)}`,
caption:
getActionValueByType(item.params.tabValue.type, item.params.tabValue.value, values, prms, getCustomTypeValue) ||
"Редактор панелей"
});
event?.stopPropagation && event.stopPropagation();
};
}
//Установить переменную
case P8P_CA_TYPE.setVariable.code: {
//Устанавливаем новые значения проводников
return ({ event, values, prms }) => {
//Собираем объект новых значений проводников
let changedValues = item.params.reduce(
(prev, cur) => ({
...prev,
...{
[cur.variableSource]: getActionValueByType(cur.resultValue.type, cur.resultValue.value, values, prms, getCustomTypeValue)
}
}),
{}
);
onProviderChange({ ...changedValues });
event?.stopPropagation && event.stopPropagation();
};
}
}
};
//Выполнение действий компонента
const getHandlersByActions = (actions, onOpenUnit, configUrlBase, onOpenPanel, onProviderChange, getCustomTypeValue = null) => {
//Инициализируем обработчики
const handlers = actions.reduce((prev, cur) => {
//Ключ обработчика
let key = `${cur.area}.${cur.element}`;
//Если уже был добавлен - не добавляем, иначе добавляем
return !prev[key] ? { ...prev, [key]: { area: cur.area, element: cur.element, fn: null } } : { ...prev };
}, {});
//Обходим уникальные обработчики
for (const handler in handlers) {
//Считываем действия области
let areaActions = actions.filter(item => item.area === handlers[handler].area && item.element === handlers[handler].element);
//Собираем массив обработчиков области
let areaFunctions = areaActions.reduce(
(fns, action) => [...fns, getActionFunction(action, onOpenUnit, configUrlBase, onOpenPanel, onProviderChange, getCustomTypeValue)],
[]
);
//Устанавливаем области последовательный вызов обработчиков
handlers[handler].fn = ({ event, values, prms }) => {
areaFunctions.map(fn => fn({ event, values, prms }));
};
}
//Возвращаем обработчики
return handlers;
};
//Считывание всех связанных переменных рекурсивно
const getActionsVariablesRecursive = (objValue, propName = null) => {
//Если это значение
if (!(typeof objValue === "object") && !Array.isArray(objValue)) {
//Если поле из списка хранящих ссылку - берем его
return P8P_CA_SOURCE_FIELDS.includes(propName) && objValue ? [objValue] : [];
}
//Если это объект хранящий тип и значение - может хранить ссылку на переменную
if (P8P_CA_OBJECT_SOURCE_FIELDS.includes(propName)) {
//Если тип "Переменная" - указываем значение
return objValue?.type === P8P_CA_DEF_TYPE_VALUE.VARIABLE && objValue?.value ? [objValue.value] : [];
}
//Если это массив
if (Array.isArray(objValue)) {
return objValue.reduce((prev, cur) => [...prev, ...getActionsVariablesRecursive(cur)], []);
} else {
//Если это объект
return Object.keys(objValue).reduce((prev, cur) => [...prev, ...getActionsVariablesRecursive(objValue[cur], cur)], []);
}
};
//Считывание всех связанных переменных действий
const getActionsVariables = actions => {
//Считываем связанные переменные действий рекурсивно
return getActionsVariablesRecursive(actions);
};
//----------------
//Интерфейс модуля
//----------------
export { isActionOkDisabled, getActionTypeParams, getActionsVariables, getHandlersByActions };

View File

@ -1,82 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Общие ресурсы компонента "Редактор условия"
*/
//---------------------
//Подключение библиотек
//---------------------
import PropTypes from "prop-types"; //Контроль свойств компонента
//---------
//Константы
//---------
//Доступные операторы условия
const P8P_CC_OPERATORS = [
{ name: "==", value: "equal" },
{ name: "!=", value: "notEqual" },
{ name: "<=", value: "lessEqual" },
{ name: "<", value: "less" },
{ name: ">=", value: "greaterEqual" },
{ name: ">", value: "greater" },
{ name: "in", value: "in" }
];
//Структура параметра поля условия
const P8P_CC_FIELD_PRM_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
hasElement: PropTypes.bool,
icon: PropTypes.string
});
//Структура поля условия
const P8P_CC_FIELD_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
});
//Структура оператора условия
const P8P_CC_OPERATOR_SHAPE = PropTypes.shape({
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
});
//Структура условия
const P8P_CC_SHAPE = PropTypes.shape({
condField: P8P_CC_FIELD_SHAPE.isRequired,
condOperator: P8P_CC_OPERATOR_SHAPE.isRequired,
condElement: PropTypes.string, //Пока
condValue: PropTypes.string.isRequired,
resField: P8P_CC_FIELD_SHAPE.isRequired,
resElement: PropTypes.string, //Пока
resValue: PropTypes.string.isRequired
});
//Начальное состояние поля условия
const P8P_CC_FIELD_INITIAL = {
name: "",
value: ""
};
//Начальное состояние условия
const P8P_CC_INITIAL = {
condField: { ...P8P_CC_FIELD_INITIAL },
condOperator: { ...P8P_CC_OPERATORS[0] },
condElement: "",
condValue: "",
resField: { ...P8P_CC_FIELD_INITIAL },
resElement: "",
resValue: ""
};
//Начальное состояние условий
const P8P_CCS_INITIAL = [];
//----------------
//Интерфейс модуля
//----------------
export { P8P_CC_OPERATORS, P8P_CC_FIELD_PRM_SHAPE, P8P_CC_FIELD_SHAPE, P8P_CC_SHAPE, P8P_CC_INITIAL, P8P_CCS_INITIAL };

View File

@ -1,239 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Редактор условия
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { TextField, Select, MenuItem, FormControl, InputLabel, Stack, Box } from "@mui/material"; //Интерфейсные элементы
import { TITLES } from "../../../../app.text"; //Общие текстовые ресурсы
import { P8PConfigDialog } from "../p8p_config_dialog"; //Диалог настройки
import { P8P_CC_INITIAL, P8P_CC_SHAPE, P8P_CC_OPERATORS, P8P_CC_FIELD_PRM_SHAPE } from "./common"; //Общие ресурсы условий
import { deepCopyObject } from "../../../core/utils"; //Вспомогательные функции
import { isConditionOkDisabled } from "./util"; //Вспомогательные ресурсы условий
//---------
//Константы
//---------
//Стили
const STYLES = {
BOX_COND_GROUP: { display: "flex", flexDirection: "row", gap: "10px", paddingBottom: "10px" },
BOX_COND_ELEMENT: width => ({ minWidth: width })
};
//-----------
//Тело модуля
//-----------
//Редактор условия
const P8PCCEditor = ({ condition = null, onOk = null, onCancel = null, condFields = [], resFields = [] }) => {
//Собственное состояние - признак инициализиации
const [init, setInit] = useState(true);
//Собственное состояние - параметры элемента формы
const [state, setState] = useState({});
//При закрытии редактора с сохранением
const handleOk = () => onOk && onOk({ ...state });
//Собственное состояние - доступность поля "Элемент условия"
const [hasCondElement, setHasCondElement] = useState(false);
//Собственное состояние - доступность поля "Элемент результата"
const [hasResElement, setHasResElement] = useState(false);
//При закрытии редактора с отменой
const handleCancel = () => onCancel && onCancel();
//При изменении параметра элемента
const handleChange = e => setState(pv => ({ ...pv, [e.target.name]: e.target.value }));
//При изменении поля условия
const handleCondFieldChange = e => {
//Считываем нужный элемент
const newCondField = condFields.find(item => item.name === e.target.value);
//Обновляем поле (объект)
setState(pv => ({ ...pv, [e.target.name]: { name: newCondField.name, value: newCondField.value } }));
//Определяем доступность поля "Элемент условия"
setHasCondElement(newCondField?.hasElement || false);
};
//При изменении поля результата
const handleResFieldChange = e => {
//Считываем нужный элемент
const newResField = resFields.find(item => item.name === e.target.value);
//Обновляем поле результата
setState(pv => ({ ...pv, [e.target.name]: { name: newResField.name, value: newResField.value } }));
//Определяем доступность поля "Элемент результата"
setHasResElement(newResField?.hasElement || false);
};
//При изменении оператора условия
const handleOperatorChange = e => {
//Считываем нужный элемент
const newOperator = P8P_CC_OPERATORS.find(item => item.name === e.target.value);
//Обновляем оператор
setState(pv => ({ ...pv, [e.target.name]: { ...newOperator } }));
};
//При инициализации условия
useEffect(() => {
//Если это иницализация
if (init) {
//Если это открытие условия - берем его параметры, иначе - инициализируем изначальными
const initCondition = condition
? condition
: {
...P8P_CC_INITIAL,
condField: { name: condFields[0].name, value: condFields[0].value },
resField: { name: resFields[0].name, value: resFields[0].value }
};
//Устанавливаем параметры действия
setState(deepCopyObject(initCondition));
//Определяем доступность "Элемент условия"
setHasCondElement(condFields.find(item => item.name === initCondition.condField.name)?.hasElement || false);
//Определяем доступность "Элемент результата"
setHasResElement(resFields.find(item => item.name === initCondition.resField.name)?.hasElement || false);
//Сбрасываем признак инициализации
setInit(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [init]);
//Формирование представления
return (
<>
{!init ? (
<P8PConfigDialog
title={`${condition ? TITLES.UPDATE : TITLES.INSERT} условия`}
onOk={handleOk}
onCancel={handleCancel}
okDisabled={isConditionOkDisabled(state)}
>
<Stack direction={"column"} spacing={1}>
<Box>
<Box sx={STYLES.BOX_COND_GROUP}>
<FormControl variant={"standard"} sx={STYLES.BOX_COND_ELEMENT("160px")}>
<InputLabel id={"condField-label"}>Поле</InputLabel>
<Select
name={"condField"}
value={state.condField.name}
labelId={"condField-label"}
label={"Поле"}
onChange={handleCondFieldChange}
>
{condFields.map((item, index) => (
<MenuItem value={item.name} key={index}>
{item.name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl variant={"standard"} sx={STYLES.BOX_COND_ELEMENT("70px")}>
<InputLabel id={"condOperator-label"}>Оператор</InputLabel>
<Select
name={"condOperator"}
value={state.condOperator.name}
labelId={"condOperator-label"}
label={"Оператор"}
onChange={handleOperatorChange}
>
{P8P_CC_OPERATORS.map((item, index) => (
<MenuItem value={item.name} key={index}>
{item.name}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
InputLabelProps={{ shrink: true }}
type={"text"}
variant={"standard"}
value={state.condValue}
label={"Значение"}
name={"condValue"}
onChange={handleChange}
sx={STYLES.BOX_COND_ELEMENT("160px")}
/>
</Box>
{hasCondElement ? (
<TextField
InputLabelProps={{ shrink: true }}
sx={{ pb: 1.25 }}
type={"text"}
variant={"standard"}
value={state.condElement}
label={"Элемент условия"}
name={"condElement"}
onChange={handleChange}
fullWidth
/>
) : null}
<Box sx={STYLES.BOX_COND_GROUP}>
<FormControl variant={"standard"} sx={STYLES.BOX_COND_ELEMENT("160px")}>
<InputLabel id={"resField-label"}>Поле результата</InputLabel>
<Select
name={"resField"}
value={state.resField.name}
labelId={"resField-label"}
label={"Поле результата"}
onChange={handleResFieldChange}
>
{resFields.map((item, index) => (
<MenuItem value={item.name} key={index}>
{item.name}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
InputLabelProps={{ shrink: true }}
type={"text"}
variant={"standard"}
value={state.resValue}
label={"Результат"}
name={"resValue"}
onChange={handleChange}
fullWidth
/>
</Box>
{hasResElement ? (
<TextField
InputLabelProps={{ shrink: true }}
sx={{ pb: 1.25 }}
type={"text"}
variant={"standard"}
value={state.resElement}
label={"Элемент результата"}
name={"resElement"}
onChange={handleChange}
fullWidth
/>
) : null}
</Box>
</Stack>
</P8PConfigDialog>
) : null}
</>
);
};
//Контроль свойств - Редактор условия
P8PCCEditor.propTypes = {
condition: P8P_CC_SHAPE,
onOk: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
condFields: PropTypes.arrayOf(P8P_CC_FIELD_PRM_SHAPE).isRequired,
resFields: PropTypes.arrayOf(P8P_CC_FIELD_PRM_SHAPE).isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { P8PCCEditor };

View File

@ -1,89 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Вспомогательные ресурсы компонента "Редактор условия"
*/
//---------------------
//Подключение библиотек
//---------------------
//---------
//Константы
//---------
//Функции операторов условий
const P8P_CC_OPERATOR_FUNC = {
equal: (objValue, value) => objValue == value,
notEqual: (objValue, value) => objValue != value,
lessEqual: (objValue, value) => objValue <= value,
less: (objValue, value) => objValue < value,
greaterEqual: (objValue, value) => objValue >= value,
greater: (objValue, value) => objValue > value,
in: (objValue, value) =>
value
.split(",")
.map(item => item.trim())
.includes(objValue + "")
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Загрузка условий в объект
const loadConditionsToObject = (obj, conditions) => {
//Инициализируем новый объект
let newObj = { ...obj };
//Инициализируем функцию оператора
let operatorFunc;
//Если изначальный индикатор загружен
if (Object.keys(newObj).length !== 0) {
//Обходим условия
conditions.map(item => {
//Функция оператора
operatorFunc = P8P_CC_OPERATOR_FUNC[item.condOperator.value];
//Проверяем условие
if (operatorFunc && operatorFunc(newObj[item.condField.value], item.condValue)) {
//Устанавливаем поле в новое значение
newObj[item.resField.value] = item.resValue;
}
});
}
//Возвращаем новый объект
return newObj;
};
//Считывание результатов условия
const getConditionsValues = (obj, conditions, element = "") => {
//Инициализируем функцию оператора
let operatorFunc;
//Инициализируем значение поля
let condFieldValue = "";
//Инициализируем результат
let resObject = {};
//Обходим условия
conditions.map(item => {
//Определяем значение поля условия
condFieldValue = item.condElement ? obj[item.condElement] : obj[item.condField.value];
//Функция оператора
operatorFunc = P8P_CC_OPERATOR_FUNC[item.condOperator.value];
//Проверяем условие
if (operatorFunc && operatorFunc(condFieldValue, item.condValue)) {
//Если в условии нет элемента результата или он равен текущему элементу
if (!item.resElement || (element && item.resElement === element)) {
resObject[item.resField.value] = item.resValue;
}
}
});
//Возвращаем новый объект
return resObject;
};
//Доступность сохранения настроек условия
const isConditionOkDisabled = condition => (!condition.condField.value || !condition.condOperator.value || !condition.resField.value ? true : false);
//----------------
//Интерфейс модуля
//----------------
export { loadConditionsToObject, getConditionsValues, isConditionOkDisabled };

View File

@ -1,71 +0,0 @@
/*
Парус 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

@ -1,41 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Дополнительные настройки источников
*/
//---------------------
//Подключение библиотек
//---------------------
//---------
//Константы
//---------
//Наименование параметра данных компонента, полученных с сервера
const P8P_COMPONENT_SETTINGS_RESP_ARGS = {
INDICATOR: "XINDICATOR",
CHART: "XCHART",
TABLE: "XDATA_GRID"
};
//Наименование путей компонента
const P8P_COMPONENT_SETTINGS_PATHS = {
INDICATOR: "indicator",
CHART: "chart",
TABLE: "table",
FORM: "form"
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//-----------
//Тело модуля
//-----------
//----------------
//Интерфейс модуля
//----------------
export { P8P_COMPONENT_SETTINGS_RESP_ARGS, P8P_COMPONENT_SETTINGS_PATHS };

View File

@ -1,99 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Условия
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Icon, Button } from "@mui/material"; //Интерфейсные элементы
import { P8PCCEditor } from "./p8p_component_condition/editor"; //Редактор условия
import { P8PChipList } from "./p8p_chip_list"; //Дополнительные настройки редактора
import { P8P_CCS_INITIAL, P8P_CC_SHAPE, P8P_CC_FIELD_PRM_SHAPE } from "./p8p_component_condition/common"; //Общие ресурсы компонента "Редактор условия"
//---------
//Константы
//---------
//-----------
//Тело модуля
//-----------
//Условия
const P8PConditions = ({ conditions = P8P_CCS_INITIAL, condFields, resFields, onChange = null } = {}) => {
//Собственное состояние - редактор условий
const [conditionEditor, setConditionEditor] = useState({ display: false, index: null });
//При изменении условий
const handleConditionsChange = conditions => onChange && onChange(conditions);
//При добавлении нового условия
const handleConditionAdd = () => setConditionEditor({ display: true, index: null });
//При нажатии на условие
const handleConditionClick = i => setConditionEditor({ display: true, index: i });
//При удалении условия
const handleConditionDelete = i => {
const newConditions = [...conditions];
newConditions.splice(i, 1);
handleConditionsChange(newConditions);
};
//При отмене сохранения изменений условия
const handleConditionCancel = () => setConditionEditor({ display: false, index: null });
//При сохранении изменений условия
const handleConditionSave = condition => {
const newConditions = [...conditions];
conditionEditor.index == null ? newConditions.push({ ...condition }) : (newConditions[conditionEditor.index] = { ...condition });
handleConditionsChange(newConditions);
setConditionEditor({ display: false, index: null });
};
//Определяем структуру условий для отображения
const conditionChips = conditions.map(item => {
//Собираем текст условия
let text = `${item.condField.name} ${item.condOperator.name} ${item.condValue}`;
//Считываем поле результата
let resField = resFields.find(field => field.name === item.resField.name);
//Формируем структуру для отображения карточки действия
return { text: text, title: text, icon: resField?.icon, iconTitle: resField?.name };
});
//Формирование представления
return (
<>
{conditionEditor.display && (
<P8PCCEditor
condition={conditionEditor.index !== null ? { ...conditions[conditionEditor.index] } : null}
onCancel={handleConditionCancel}
onOk={handleConditionSave}
condFields={condFields}
resFields={resFields}
/>
)}
<P8PChipList items={conditionChips} onClick={handleConditionClick} onDelete={handleConditionDelete} />
<Button startIcon={<Icon>add</Icon>} onClick={handleConditionAdd}>
Добавить условие
</Button>
</>
);
};
//Контроль свойств компонента - условия
P8PConditions.propTypes = {
conditions: PropTypes.arrayOf(P8P_CC_SHAPE),
condFields: PropTypes.arrayOf(P8P_CC_FIELD_PRM_SHAPE).isRequired,
resFields: PropTypes.arrayOf(P8P_CC_FIELD_PRM_SHAPE).isRequired,
onChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PConditions };

View File

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

View File

@ -1,105 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Источник данных
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, IconButton, Icon, Typography, Button, Card, CardContent, CardActions, CardActionArea } from "@mui/material"; //Интерфейсные элементы
import { BUTTONS, TEXTS } from "../../../app.text"; //Общие текстовые ресурсы
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"; //Диалог настройки источника данных
import { P8PChipList } from "./p8p_chip_list"; //Дополнительные настройки редактора
//-----------
//Тело модуля
//-----------
//Источник данных
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 argChips =
configured && dataSource.arguments.map(argument => ({ text: `:${argument.name} = ${argument.valueSource || argument.value || "NULL"}` }));
//Формирование представления
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}>
<P8PChipList items={argChips} />
</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

@ -1,87 +0,0 @@
/*
Парус 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: "",
query: "",
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

@ -1,253 +0,0 @@
/*
Парус 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"; //Хуки источников данных
import { P8PDataSourceQuerySelector } from "./p8p_data_source_query_selector"; //Диалог выбора записи редактора запросов
import { hasValue } from "../../core/utils"; //Вспомогательные функции
//-----------
//Тело модуля
//-----------
//Диалог настройки источника данных
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 [openQuerySelector, setOpenQuerySelector] = useState(false);
//Собственное состояние - доступность полей выбора источника
const [disabledFields, setDisabledFields] = useState({ query: false, userProc: false });
//Подключение к контексту приложения
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, "");
//Открытие диалога выбора запроса
const handleOpenQuerySelector = () => setOpenQuerySelector(true);
//Закрытие диалога выбора запроса
const handleCancelQuerySelector = () => setOpenQuerySelector(false);
//При нажатии на очистку мнемокода запроса
const handleQueryClearClick = () => setState({ ...P8P_DATA_SOURCE_INITIAL });
//При нажатии на выбор запроса в качестве источника данных
const handleQuerySelectClick = query => {
setState(pv => ({ ...pv, type: P8P_DATA_SOURCE_TYPE.QUERY, query: query.code }));
handleCancelQuerySelector();
};
//При изменении описания пользовательской процедуры
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]);
//При изменении источника
useEffect(() => {
/*
Если выбран запрос - блокируем выбор процедуры
Если выбрана процедура - блокируем выбор запроса
Ничего не выбрано - доступны все варианты
*/
hasValue(state.query)
? setDisabledFields({ query: false, userProc: true })
: hasValue(state.userProc)
? setDisabledFields({ query: true, userProc: false })
: setDisabledFields({ query: false, userProc: false });
}, [state.query, state.userProc]);
//Список значений
const values = Object.keys(valueProviders);
//Наличие значений
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>
);
//Доступность сохранения настройки источника данных
const okDisabled = !state.query && !state.userProc ? true : false;
//Формирование представления
return (
<>
{openQuerySelector ? (
<P8PDataSourceQuerySelector current={state.query} onSelect={handleQuerySelectClick} onCancel={handleCancelQuerySelector} />
) : null}
<P8PConfigDialog title={TITLES.DATA_SOURCE_CONFIG} onOk={handleOk} onCancel={handleCancel} okDisabled={okDisabled}>
<Stack direction={"column"} spacing={1}>
{valueProvidersMenu}
{/* ДОРАБАТЫВАТЬ ПОСЛЕ РЕАЛИЗАЦИИ ЗАПРОСОВ */}
<TextField
type={"text"}
variant={"standard"}
value={state.query}
label={CAPTIONS.QUERY}
InputLabelProps={{ shrink: true }}
InputProps={{
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleQueryClearClick} disabled={disabledFields.query}>
<Icon>clear</Icon>
</IconButton>
<IconButton onClick={handleOpenQuerySelector} disabled={disabledFields.query}>
<Icon>list</Icon>
</IconButton>
</InputAdornment>
)
}}
disabled={disabledFields.query}
/>
<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} disabled={disabledFields.userProc}>
<Icon>clear</Icon>
</IconButton>
<IconButton onClick={handleUserProcSelectClick} disabled={disabledFields.userProc}>
<Icon>list</Icon>
</IconButton>
</InputAdornment>
)
}}
disabled={disabledFields.userProc}
/>
{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

@ -1,229 +0,0 @@
/*
Парус 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"; //Общие ресурсы источника данных
import { getConditionsValues } from "./p8p_component_condition/util"; //Вспомогательные ресурсы условий
import { getHandlersByActions } from "./p8p_component_action/util"; //Вспомогательные ресурсы действий
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
//-----------
//Тело модуля
//-----------
//Описание пользовательской процедуры
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, componentRespArg = "" }) => {
//Контроллер для прерывания запросов
const abortController = useRef(null);
//Собственное состояние - параметры исполнения
const [state, setState] = useState({ stored: null, storedArgs: [], respArg: null, reqSet: false });
//Собственное состояние - флаг загрузки
const [isLoading, setLoading] = useState(false);
//Собственное состояние - данные
const [data, setData] = useState({ componentData: {}, init: false });
//Собственное состояние - ошибка получения данных
const [error, setError] = useState(null);
//Собственное состояние - наличие настроек
const [haveConfing, setHaveConfig] = useState(false);
//Собственное состояние - наличие данных
const [haveData, setHaveData] = useState(false);
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При необходимости обновление информации о наличии данных
useEffect(() => {
setHaveData(data.init === true && !error ? true : false);
}, [data.init, error]);
//При необходимости обновить данные
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({ componentData: { ...data[componentRespArg] }, init: true });
} catch (e) {
if (e.message !== client.ERR_ABORTED) {
setError(formatErrorMessage(e.message).text);
setData({ componentData: {}, init: false });
}
} finally {
setLoading(false);
}
};
if (state.reqSet) {
if (state.stored) loadData();
} else setData({ componentData: {}, init: false });
return () => abortController.current?.abort?.();
}, [state.stored, state.storedArgs, state.respArg, state.reqSet, executeStored, componentRespArg]);
//При изменении свойств
useEffect(() => {
//Устанавливаем признак наличия настроек
setHaveConfig(dataSource?.stored ? true : false);
//Устанавливаем параметры исполнения
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({ componentData: {}, init: false });
}
return { stored, respArg, storedArgs, reqSet };
} else return pv;
} else return pv;
});
}, [dataSource, values]);
//Возвращаем интерфейс хука
return [data.componentData, error, haveConfing, haveData, isLoading];
};
//Изменение данных компонента с учетом условий
const useConditions = ({ componentData, conditions }) => {
//Собственное состояние - текущие условия компонента
const [currentConditions, setCurrentConditions] = useState([]);
//Собственное состояние - данные
const [data, setData] = useState();
//При обновлении условий компонента
useEffect(() => {
//Если условия изменились
if (JSON.stringify(currentConditions) != JSON.stringify(conditions)) {
//Устанавливаем новые условия
setCurrentConditions(conditions);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [conditions]);
//При обновлении данных или условий компонента
useEffect(() => {
//Если есть текущие условия
if (currentConditions.length !== 0) {
//Устанавливаем данные с учетом условий
setData({ ...componentData, ...getConditionsValues(componentData, currentConditions) });
} else {
//Оставляем данные компонента
setData({ ...componentData });
}
}, [currentConditions, componentData]);
//Возвращаем интерфейс хука
return [data];
};
//Получение обработчиков компонента
const useComponentHandlers = ({ actions = [], onValuesChange = null, getCustomTypeValue = null }) => {
//Контроллер для текущего состояния действий
const currentActions = useRef([]);
//Собственное состояние - обработчики компонента
const [handlers, setHandlers] = useState({});
//Подключение к контексту приложения
const { configUrlBase, pOnlineShowTab, pOnlineShowUnit } = useContext(ApplicationСtx);
//При необходимости обновления информации об обработчиках
useEffect(() => {
//Если изменились действия или параметры
if (JSON.stringify(currentActions.current) != JSON.stringify(actions)) {
//Считываем обработчики компонента
setHandlers(getHandlersByActions(actions, pOnlineShowUnit, configUrlBase, pOnlineShowTab, onValuesChange, getCustomTypeValue));
//Устанавливаем контроллер текущих действий
currentActions.current = actions;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actions]);
//Возвращаем интерфейс хука
return [handlers];
};
//----------------
//Интерфейс модуля
//----------------
export { useUserProcDesc, useDataSource, useConditions, useComponentHandlers };

View File

@ -1,75 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Диалог выбора записи редактора запросов
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, List, ListItem, ListItemButton, ListItemText, Typography } from "@mui/material"; //Интерфейсные элементы
import { P8PConfigDialog } from "./p8p_config_dialog"; //Типовой диалог настройки
//---------
//Константы
//---------
//Стили
const STYLES = {
LIST: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto" }
};
//-----------
//Тело модуля
//-----------
//Диалог выбора записи редактора запросов
const P8PDataSourceQuerySelector = ({ current = null, onSelect, onCancel }) => {
//!!! НА РЕАЛИЗАЦИИ
const test = [{ code: "TestCode", name: "TestName" }];
//При выборе элемента списка
const handleSelectClick = query => {
onSelect && onSelect(query);
};
//Формирование представления
return (
<P8PConfigDialog title={"Запросы"} onCancel={onCancel}>
<List sx={STYLES.LIST}>
{test.map((query, i) => {
const selected = query.code === current;
return (
<ListItem key={i}>
<ListItemButton onClick={() => handleSelectClick(query)} selected={selected}>
<ListItemText
primary={query.code}
secondaryTypographyProps={{ component: "div" }}
secondary={
<Stack direction={"column"}>
<Typography variant={"caption"}>{`${query.code}, ${query.name}`}</Typography>
</Stack>
}
/>
</ListItemButton>
</ListItem>
);
})}
</List>
</P8PConfigDialog>
);
};
//Контроль свойств компонента - Диалог выбора записи редактора запросов
P8PDataSourceQuerySelector.propTypes = {
current: PropTypes.string,
onSelect: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { P8PDataSourceQuerySelector };

View File

@ -1,62 +0,0 @@
/*
Парус 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, allowClose = true }) => {
//При нажатии на "Сохранить"
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>
{allowClose ? (
<IconButton onClick={() => handleSaveClick(true)} title={BUTTONS.SAVE}>
<Icon>done_all</Icon>
</IconButton>
) : null}
</Stack>
)}
</Box>
);
};
//Контроль свойств компонента - Контейнер редактора
P8PEditorBox.propTypes = {
title: PropTypes.string.isRequired,
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
onSave: PropTypes.func,
allowClose: PropTypes.bool
};
//----------------
//Интерфейс модуля
//----------------
export { P8PEditorBox };

View File

@ -1,49 +0,0 @@
/*
Парус 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

@ -1,62 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Компонент: Панель инструментов редактора
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { IconButton, Icon, Stack, Grid } 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,
customRenderer: PropTypes.func
});
//-----------
//Тело модуля
//-----------
//Панель инструментов редактора
const P8PEditorToolBar = ({ items = [] }) => {
//Формирование представления
return (
<Stack direction={"row"} p={1}>
<Grid container columns={items.length}>
{items.map((item, i) => (
<Grid item size={{ xs: 2, sm: 4, md: 4 }} key={i}>
{item.customRenderer ? (
item.customRenderer({ icon: item.icon, title: item.title, disabled: item?.disabled === true, onClick: item.onClick })
) : (
<IconButton onClick={item.onClick} title={item.title} disabled={item?.disabled === true}>
<Icon>{item.icon}</Icon>
</IconButton>
)}
</Grid>
))}
</Grid>
</Stack>
);
};
//Контроль свойств компонента - Панель инструментов редактора
P8PEditorToolBar.propTypes = {
items: PropTypes.arrayOf(P8P_EDITOR_TOOL_BAR_ITEM_SHAPE)
};
//----------------
//Интерфейс модуля
//----------------
export { P8PEditorToolBar };

View File

@ -1,39 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редакторы панелей
Общие ресурсы редакторов
*/
//---------
//Константы
//---------
//Стили
const STYLES = {
CHIP: (fullWidth = false, multiLine = false) => ({
...(multiLine ? { height: "auto" } : {}),
"& .MuiChip-label": {
...(multiLine
? {
display: "block",
whiteSpace: "normal"
}
: {}),
...(fullWidth ? { width: "100%" } : {})
}
})
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Проверка корректности наименования элемента формы
const isElementNameCorrect = elementName => {
return new RegExp(/^[\w\d_]*$/).test(elementName);
};
//----------------
//Интерфейс модуля
//----------------
export { STYLES, isElementNameCorrect };

View File

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

View File

@ -47,7 +47,7 @@ const STYLES = {
//-----------
//Рабочее пространство
const P8PAppWorkspace = ({ children, panels = [], selectedPanel, caption, closeCaption, homeCaption, onHomeNavigate, onItemNavigate } = {}) => {
const P8PAppWorkspace = ({ children, panels = [], selectedPanel, closeCaption, homeCaption, onHomeNavigate, onItemNavigate } = {}) => {
//Собственное состояния
const [open, setOpen] = useState(false);
@ -86,7 +86,7 @@ const P8PAppWorkspace = ({ children, panels = [], selectedPanel, caption, closeC
<Icon>{open ? "chevron_left" : "menu"}</Icon>
</IconButton>
<Typography variant="h6" noWrap component="div">
{caption || selectedPanel?.caption}
{selectedPanel?.caption}
</Typography>
</Toolbar>
</AppBar>
@ -120,7 +120,6 @@ 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

@ -9,16 +9,7 @@
import React, { useState, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
P8PTable,
P8P_TABLE_SIZE,
P8P_TABLE_DATA_TYPE,
P8P_TABLE_FILTER_SHAPE,
P8P_TABLE_MORE_HEIGHT,
P8P_TABLE_FILTERS_HEIGHT,
P8P_TABLE_PAGINATOR_ALIGN,
P8P_TABLE_PAGINATOR_POSITION
} from "./p8p_table"; //Таблица
import { P8PTable, P8P_TABLE_SIZE, P8P_TABLE_DATA_TYPE, P8P_TABLE_FILTER_SHAPE, P8P_TABLE_MORE_HEIGHT, P8P_TABLE_FILTERS_HEIGHT } from "./p8p_table"; //Таблица
//---------
//Константы
@ -39,29 +30,17 @@ const P8P_DATA_GRID_MORE_HEIGHT = P8P_TABLE_MORE_HEIGHT;
//Высота фильтров таблицы
const P8P_DATA_GRID_FILTERS_HEIGHT = P8P_TABLE_FILTERS_HEIGHT;
//Размещение области страниц по вертикали
const P8P_DATA_GRID_PAGINATOR_ALIGN = P8P_TABLE_PAGINATOR_ALIGN;
//Размещение области страниц по горизонтали
const P8P_DATA_GRID_PAGINATOR_POSITION = P8P_TABLE_PAGINATOR_POSITION;
//-----------
//Тело модуля
//-----------
//Таблица данных
const P8PDataGrid = ({
style = {},
tableStyle = {},
columnsDef = [],
filtersInitial,
groups = [],
rows = [],
size,
pageNumber = 1,
pagesCount = 0,
pagesAlign = P8P_DATA_GRID_PAGINATOR_ALIGN.RIGHT,
pagesPosition = P8P_DATA_GRID_PAGINATOR_POSITION.BOTTOM,
fixedHeader = false,
fixedColumns = 0,
morePages = false,
@ -89,7 +68,6 @@ const P8PDataGrid = ({
onOrderChanged,
onFilterChanged,
onPagesCountChanged,
onPageChanged,
objectsCopier
}) => {
//Собственное состояние - сортировки
@ -128,9 +106,6 @@ const P8PDataGrid = ({
if (onPagesCountChanged) onPagesCountChanged();
};
//При изменении номера страницы
const handlePageChange = ({ page }) => onPageChanged && onPageChanged({ page });
//При изменении списка установленных извне фильтров
useEffect(() => {
setFilters(filtersInitial || []);
@ -139,18 +114,12 @@ const P8PDataGrid = ({
//Генерация содержимого
return (
<P8PTable
style={style}
tableStyle={tableStyle}
columnsDef={columnsDef}
groups={groups}
rows={rows}
orders={orders}
filters={filters}
size={size || P8P_DATA_GRID_SIZE.MEDIUM}
pageNumber={pageNumber}
pagesCount={pagesCount}
pagesAlign={pagesAlign}
pagesPosition={pagesPosition}
fixedHeader={fixedHeader}
fixedColumns={fixedColumns}
morePages={morePages}
@ -179,24 +148,17 @@ const P8PDataGrid = ({
onOrderChanged={handleOrderChanged}
onFilterChanged={handleFilterChanged}
onPagesCountChanged={handlePagesCountChanged}
onPageChanged={handlePageChange}
/>
);
};
//Контроль свойств - Таблица данных
P8PDataGrid.propTypes = {
style: PropTypes.object,
tableStyle: PropTypes.object,
columnsDef: PropTypes.array,
filtersInitial: PropTypes.arrayOf(P8P_DATA_GRID_FILTER_SHAPE),
groups: PropTypes.array,
rows: PropTypes.array,
size: PropTypes.string,
pageNumber: PropTypes.number,
pagesCount: PropTypes.number,
pagesAlign: PropTypes.string,
pagesPosition: PropTypes.string,
fixedHeader: PropTypes.bool,
fixedColumns: PropTypes.number,
morePages: PropTypes.bool,
@ -224,7 +186,6 @@ P8PDataGrid.propTypes = {
onOrderChanged: PropTypes.func,
onFilterChanged: PropTypes.func,
onPagesCountChanged: PropTypes.func,
onPageChanged: PropTypes.func,
objectsCopier: PropTypes.func.isRequired
};
@ -238,7 +199,5 @@ export {
P8P_DATA_GRID_FILTER_SHAPE,
P8P_DATA_GRID_MORE_HEIGHT,
P8P_DATA_GRID_FILTERS_HEIGHT,
P8P_DATA_GRID_PAGINATOR_ALIGN,
P8P_DATA_GRID_PAGINATOR_POSITION,
P8PDataGrid
};

View File

@ -1,136 +0,0 @@
/*
Парус 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"; //Поле ввода
import { APP_STYLES } from "../../app.styles"; //Типовые стили
//---------
//Константы
//---------
//Типовая ширина диалога
const P8P_DIALOG_WIDTH = {
XS: "xs",
SM: "sm",
MD: "md",
LG: "lg",
XL: "xl"
};
//Стили
const STYLES = {
SCROLL: display =>
display === true ? { overflow: "auto", ...APP_STYLES.SCROLL } : { overflow: "hidden", display: "flex", flexDirection: "column" }
};
//-----------------------
//Вспомогательные функции
//-----------------------
//Формирование объекта вида {ключ: значение} из текущего состояния элементов ввода формы
const buildFormValues = inputsState =>
inputsState.reduce((res, input) => ({ ...res, [input.name]: input.value == undefined ? null : input.value }), {});
//-----------
//Тело модуля
//-----------
//Диалог
const P8PDialog = ({
title,
width,
fullWidth,
inputs,
children,
okDisabled = false,
scrollContent = true,
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 sx={STYLES.SCROLL(scrollContent)}>
{inputsState.map((input, i) => (
<P8PInput key={i} {...input} formValues={formValues} onChange={handleInputChange} />
))}
{children}
</DialogContent>
<DialogActions>
{onOk && (
<Button disabled={okDisabled} 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)]),
okDisabled: PropTypes.bool,
scrollContent: PropTypes.bool,
onOk: PropTypes.func,
onCancel: PropTypes.func,
onClose: PropTypes.func,
onInputChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { P8PDialog, P8P_DIALOG_WIDTH };

View File

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

@ -1,229 +0,0 @@
/*
Парус 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": { filter: "brightness(0.92) !important" },
"&: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: clickable => ({
width: "99cqw",
...(clickable
? {
cursor: "pointer"
}
: {})
}),
VALUE_TYPOGRAPHY: clickable => ({
...(clickable
? {
cursor: "pointer"
}
: {})
})
};
//-----------------------
//Вспомогательные функции
//-----------------------
//Подбор цвета заливки
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,
onValueClick = null,
onCaptionClick = 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 = e => {
setShowHint(false);
e.stopPropagation();
};
//При нажатии на заголовок
const handleCaptionClick = event => {
//Если есть функция нажатия на заголовок
onCaptionClick && onCaptionClick(event);
};
//При нажатии на значение
const handleValueClick = event => {
//Если есть функция нажатия на заголовок
onValueClick && onValueClick(event);
};
//Представление текста значения индикатора
const valueTextView = (
<Typography variant={"h4"} sx={STYLES.VALUE_TYPOGRAPHY(onValueClick ? true : false)} onClick={handleValueClick}>
{[undefined, null, ""].includes(value) ? TEXTS.NO_DATA_FOUND_SHORT : value}
</Typography>
);
//Представление текста подписи индикатора
const captionView = (
<Typography
align={"left"}
noWrap={true}
sx={STYLES.CAPTION_TYPOGRAPHY(onCaptionClick ? true : false)}
title={caption}
onClick={handleCaptionClick}
>
{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,
onValueClick: PropTypes.func,
onCaptionClick: PropTypes.func,
backgroundColor: PropTypes.string,
color: PropTypes.string
};
//----------------
//Интерфейс модуля
//----------------
export { P8P_INDICATOR_VARIANT, P8P_INDICATOR_STATE, P8PIndicator };

View File

@ -1,135 +0,0 @@
/*
Парус 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

@ -16,7 +16,6 @@ import {
TableContainer,
TableHead,
TableRow,
Pagination,
Paper,
IconButton,
Icon,
@ -35,7 +34,7 @@ import {
Link
} from "@mui/material"; //Интерфейсные компоненты
import { useTheme } from "@mui/material/styles"; //Взаимодействие со стилями MUI
import { P8PAppInlineError, P8PHintDialog } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
import { P8P_TABLE_AT, HEADER_INITIAL_STATE, hasValue, p8pTableReducer } from "./p8p_table_reducer"; //Редьюсер состояния
//---------
@ -82,20 +81,6 @@ const P8P_TABLE_FILTER_SHAPE = PropTypes.shape({
to: PropTypes.any
});
//Размещение области страниц по вертикали
const P8P_TABLE_PAGINATOR_ALIGN = {
LEFT: "left",
RIGHT: "right",
CENTER: "center"
};
//Размещение области страниц по горизонтали
const P8P_TABLE_PAGINATOR_POSITION = {
TOP: "top",
BOTTOM: "bottom",
BOTH: "both"
};
//Высота кнопки догрузки данных
const P8P_TABLE_MORE_HEIGHT = "49px";
@ -104,7 +89,9 @@ const P8P_TABLE_FILTERS_HEIGHT = "48px";
//Стили
const STYLES = {
TABLE: {},
TABLE: {
with: "100%"
},
TABLE_HEAD_STICKY: {
position: "sticky",
top: 0,
@ -151,16 +138,6 @@ const STYLES = {
FILTER_CHIP: {
alignItems: "center"
},
PAGINATION: (pagesAlign, position) => ({
display: "flex",
justifyContent:
pagesAlign === P8P_TABLE_PAGINATOR_ALIGN.LEFT
? "flex-start"
: pagesAlign === P8P_TABLE_PAGINATOR_ALIGN.CENTER
? "space-around"
: "flex-end",
...(position === P8P_TABLE_PAGINATOR_POSITION.TOP ? { paddingBottom: "10px" } : { paddingTop: "10px" })
}),
MORE_BUTTON_CONTAINER: {
with: "100%",
textAlign: "center",
@ -313,6 +290,28 @@ 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,
@ -489,18 +488,12 @@ P8PTableFiltersChips.propTypes = {
//Таблица
const P8PTable = ({
style = {},
tableStyle = {},
columnsDef = [],
groups = [],
rows = [],
orders,
filters,
size,
pageNumber = 1,
pagesCount = 0,
pagesAlign = P8P_TABLE_PAGINATOR_ALIGN.RIGHT,
pagesPosition = P8P_TABLE_PAGINATOR_POSITION.BOTTOM,
fixedHeader = false,
fixedColumns = 0,
morePages = false,
@ -526,7 +519,6 @@ const P8PTable = ({
onOrderChanged,
onFilterChanged,
onPagesCountChanged,
onPageChanged,
objectsCopier,
containerComponent,
containerComponentProps
@ -664,9 +656,6 @@ const P8PTable = ({
else setExpanded(pv => ({ ...pv, [rowIndex]: true }));
};
//Отработка изменения страницы
const handlePageChange = (e, page) => onPageChanged && onPageChanged({ page });
//При перезагрузке данных
useEffect(() => {
if (reloading) setExpanded({});
@ -711,35 +700,12 @@ const P8PTable = ({
));
};
//Генерация области страниц
const renderPagination = position => {
//Признак отображения в конкретной области
const isVisible = [
position === P8P_TABLE_PAGINATOR_POSITION.TOP ? P8P_TABLE_PAGINATOR_POSITION.TOP : P8P_TABLE_PAGINATOR_POSITION.BOTTOM,
P8P_TABLE_PAGINATOR_POSITION.BOTH
].includes(pagesPosition);
//Генерация содержимого
return (
<>
{pagesCount && pagesCount > 0 && isVisible ? (
<Pagination
sx={STYLES.PAGINATION(pagesAlign, position)}
count={pagesCount}
defaultPage={1}
page={pageNumber}
size="medium"
onChange={handlePageChange}
/>
) : null}
</>
);
};
//Генерация содержимого
return (
<div style={{ ...(style || {}) }}>
{displayHintColumn ? <P8PHintDialog title={displayHintColumnDef.caption} hint={displayHintColumnDef.hint} onOk={handleHintOk} /> : null}
<div>
{displayHintColumn ? (
<P8PTableColumnHintDialog columnDef={displayHintColumnDef} okBtnCaption={okFilterBtnCaption} onOk={handleHintOk} />
) : null}
{filterColumn ? (
<P8PTableColumnFilterDialog
columnDef={filterColumnDef}
@ -768,9 +734,8 @@ const P8PTable = ({
valueFormatter={valueFormatter}
/>
) : null}
{renderPagination(P8P_TABLE_PAGINATOR_POSITION.TOP)}
<TableContainer component={containerComponent ? containerComponent : Paper} {...(containerComponentProps ? containerComponentProps : {})}>
<Table stickyHeader={fixedHeader} sx={{ ...STYLES.TABLE, ...(tableStyle || {}) }} size={size || P8P_TABLE_SIZE.MEDIUM}>
<Table stickyHeader={fixedHeader} sx={STYLES.TABLE} size={size || P8P_TABLE_SIZE.MEDIUM}>
<TableHead sx={fixedHeader ? STYLES.TABLE_HEAD_STICKY : {}}>
{header.displayLevels.map((level, i) => (
<TableRow key={level}>
@ -922,8 +887,7 @@ const P8PTable = ({
</TableBody>
</Table>
</TableContainer>
{renderPagination(P8P_TABLE_PAGINATOR_POSITION.BOTTOM)}
{morePages && (!pagesCount || pagesCount <= 0) ? (
{morePages ? (
<Container style={STYLES.MORE_BUTTON_CONTAINER}>
<Button fullWidth onClick={handleMorePagesBtnClick} {...(morePagesBtnProps ? morePagesBtnProps : {})}>
{morePagesBtnCaption}
@ -936,8 +900,6 @@ const P8PTable = ({
//Контроль свойств - Таблица
P8PTable.propTypes = {
style: PropTypes.object,
tableStyle: PropTypes.object,
columnsDef: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
@ -970,10 +932,6 @@ P8PTable.propTypes = {
).isRequired,
filters: PropTypes.arrayOf(P8P_TABLE_FILTER_SHAPE).isRequired,
size: PropTypes.string,
pageNumber: PropTypes.number,
pagesCount: PropTypes.number,
pagesAlign: PropTypes.string,
pagesPosition: PropTypes.string,
fixedHeader: PropTypes.bool,
fixedColumns: PropTypes.number,
morePages: PropTypes.bool,
@ -999,7 +957,6 @@ P8PTable.propTypes = {
onOrderChanged: PropTypes.func,
onFilterChanged: PropTypes.func,
onPagesCountChanged: PropTypes.func,
onPageChanged: PropTypes.func,
objectsCopier: PropTypes.func.isRequired,
containerComponent: PropTypes.oneOfType([PropTypes.elementType, PropTypes.string]),
containerComponentProps: PropTypes.object
@ -1009,13 +966,4 @@ P8PTable.propTypes = {
//Интерфейс модуля
//----------------
export {
P8P_TABLE_DATA_TYPE,
P8P_TABLE_SIZE,
P8P_TABLE_FILTER_SHAPE,
P8P_TABLE_MORE_HEIGHT,
P8P_TABLE_FILTERS_HEIGHT,
P8P_TABLE_PAGINATOR_ALIGN,
P8P_TABLE_PAGINATOR_POSITION,
P8PTable
};
export { P8P_TABLE_DATA_TYPE, P8P_TABLE_SIZE, P8P_TABLE_FILTER_SHAPE, P8P_TABLE_MORE_HEIGHT, P8P_TABLE_FILTERS_HEIGHT, P8PTable };

View File

@ -56,9 +56,6 @@ 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);
@ -172,7 +169,6 @@ export const ApplicationContext = ({ errors, displaySizeGetter, guidGenerator, c
return (
<ApplicationСtx.Provider
value={{
setAppBarTitle,
findPanelByName,
pOnlineShowTab,
pOnlineShowUnit,

View File

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

View File

@ -33,9 +33,7 @@ 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,
DETAIL: PropTypes.string.isRequired,
HIDE: PropTypes.string.isRequired
CANCEL: PropTypes.string.isRequired
});
//----------------
@ -58,16 +56,12 @@ export const MessagingContext = ({ titles, texts, buttons, children }) => {
//Отображение сообщения
const showMsg = useCallback(
(type, text, msgOnOk = null, msgOnCancel = null, fullErrorText = null) =>
dispatch({ type: MSG_AT.SHOW_MSG, payload: { type, text, msgOnOk, msgOnCancel, fullErrorText } }),
(type, text, msgOnOk = null, msgOnCancel = null) => dispatch({ type: MSG_AT.SHOW_MSG, payload: { type, text, msgOnOk, msgOnCancel } }),
[]
);
//Отображение сообщения - ошибка
const showMsgErr = useCallback(
(text, msgOnOk = null, fullErrorText = null) => showMsg(MSG_TYPE.ERR, text, msgOnOk, null, fullErrorText),
[showMsg]
);
const showMsgErr = useCallback((text, msgOnOk = null) => showMsg(MSG_TYPE.ERR, text, msgOnOk), [showMsg]);
//Отображение сообщения - информация
const showMsgInfo = useCallback((text, msgOnOk = null) => showMsg(MSG_TYPE.INFO, text, msgOnOk), [showMsg]);
@ -132,7 +126,6 @@ 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}
@ -141,8 +134,6 @@ 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,7 +35,6 @@ const INITIAL_STATE = {
msg: false,
msgType: MSG_TYPE.ERR,
msgText: null,
msgFullErrorText: null,
msgOnOk: null,
msgOnCancel: null
};
@ -60,7 +59,6 @@ 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, setAppBarTitle } = useContext(ApplicationСtx);
const { findPanelByName } = useContext(ApplicationСtx);
//Проверка наличия параметров запроса
const isNavigationSearch = () => (location.search ? true : false);
@ -65,8 +65,6 @@ 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,7 +34,6 @@ const ERR_APPSERVER = "Ошибка сервера приложений"; //Об
const ERR_UNEXPECTED = "Неожиданный ответ сервера"; //Неожиданный ответ сервера
const ERR_NETWORK = "Ошибка соединения с сервером"; //Ошибка сети
const ERR_UNAUTH = "Сеанс завершен. Пройдите аутентификацию повторно."; //Ошибка аутентификации
const ERR_ABORTED = "Запрос прерван принудительно";
//-----------
//Тело модуля
@ -77,16 +76,7 @@ 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,
signal = null
} = {}) => {
const executeAction = async ({ serverURL, action, payload = {}, isArray, transformTagName, tagValueProcessor, attributeValueProcessor } = {}) => {
console.log(`EXECUTING ${action ? action : ""} ON ${serverURL} WITH PAYLOAD:`);
console.log(payload ? payload : "NO PAYLOAD");
let response = null;
@ -102,14 +92,11 @@ const executeAction = async ({
body: await buildXML(rqBody),
headers: {
"content-type": "application/xml"
},
...(signal ? { signal } : {})
}
});
} catch (e) {
//Прервано принудительно
if (signal?.aborted === true) throw new Error(ERR_ABORTED);
//Сетевая ошибка
else throw new Error(`${ERR_NETWORK}: ${e.message || "неопределённая ошибка"}`);
throw new Error(`${ERR_NETWORK}: ${e.message}`);
}
//Проверим на наличие ошибок HTTP - если есть вернём их
if (!response.ok) throw new Error(`${ERR_APPSERVER}: ${response.statusText}`);
@ -149,8 +136,7 @@ const executeStored = async ({
tagValueProcessor,
attributeValueProcessor,
throwError = true,
spreadOutArguments = false,
signal = null
spreadOutArguments = false
} = {}) => {
let res = null;
try {
@ -171,8 +157,7 @@ const executeStored = async ({
payload: { SSTORED: stored, XARGUMENTS: serverArgs, SRESP_ARG: respArg },
isArray,
tagValueProcessor,
attributeValueProcessor,
signal
attributeValueProcessor
});
if (spreadOutArguments === true && Array.isArray(res?.XPAYLOAD?.XOUT_ARGUMENTS)) {
let spreadArgs = {};
@ -208,11 +193,6 @@ 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,17 +102,14 @@ const getDisplaySize = () => {
};
//Глубокое копирование объекта
const deepCopyObject = obj => (structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj)));
const deepCopyObject = obj => JSON.parse(JSON.stringify(obj));
//Конвертация объекта в Base64 XML
const object2XML = (obj, builderOptions) => {
const object2Base64XML = (obj, builderOptions) => {
const builder = new XMLBuilder(builderOptions);
return builder.build(obj);
return btoa(unescape(encodeURIComponent(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) => {
@ -161,43 +158,12 @@ 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 =>
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
);
//Формироваеие уникального идентификатора по текущему времени
const genUID = prefix => (prefix ? `${prefix}_${Date.now()}` : `${Date.now()}`);
//----------------
//Интерфейс модуля
//----------------
@ -206,14 +172,11 @@ export {
hasValue,
getDisplaySize,
deepCopyObject,
object2XML,
object2Base64XML,
xml2JSON,
formatDateRF,
formatDateTimeRF,
formatDateJSONDateOnly,
formatNumberRFCurrency,
formatErrorMessage,
genGUID,
genUID
genGUID
};

View File

@ -1,297 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Панель мониторинга: Корневая панель доски задач
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState, useCallback } from "react"; //Классы React
import { DragDropContext, Droppable } from "react-beautiful-dnd"; //Работа с drag&drop
import { Stack, Box, IconButton, Icon } from "@mui/material"; //Интерфейсные компоненты
import { StatusCard } from "./components/status_card.js";
import { TaskDialog } from "./task_dialog.js"; //Компонент формы события
import { Filter } from "./filter.js"; //Компонент фильтров
import { useExtraData, 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

@ -1,174 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Кастомное поле ввода
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { FormControl, InputLabel, Input, InputAdornment, IconButton, Icon, FormHelperText, Select, MenuItem, 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

@ -1,336 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Диалог фильтра отбора
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext, useEffect, useCallback } from "react"; //Классы React
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
Dialog,
DialogTitle,
IconButton,
Icon,
DialogContent,
DialogActions,
Button,
Box,
Stack,
Checkbox,
FormControlLabel,
Radio,
RadioGroup
} from "@mui/material"; //Интерфейсные компоненты
import { 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

@ -1,99 +0,0 @@
/*
Парус 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

@ -1,149 +0,0 @@
/*
Парус 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

@ -1,166 +0,0 @@
/*
Парус 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

@ -1,109 +0,0 @@
/*
Парус 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

@ -1,415 +0,0 @@
/*
Парус 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: (task, colorRule) => {
const expiredColor = getTaskExpiredColor(task);
const backgroundColor = task.nClosed ? "#d3d3d3" : colorRule.SCOLOR ? getTaskBgColorByRule(task, colorRule) : null;
return {
...(expiredColor ? { borderLeft: `solid ${expiredColor}` } : {}),
...(backgroundColor ? { backgroundColor: backgroundColor } : {})
};
},
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", width: "100%" },
STACK_SENDER: { alignItems: "center", marginLeft: "auto", width: "50%", justifyContent: "flex-end", paddingLeft: "10px", gap: "5px" },
TYPOGRAPHY_TASK: { color: "text.secondary", fontSize: 14, width: "40%", overflow: "hidden" },
TYPOGRAPHY_SENDER: { color: "text.secondary", fontSize: 14, width: "80%", overflow: "hidden", textAlign: "end" },
ICON_COLOR: linked => {
return { color: theme => (linked ? TASK_COLORS.LINKED : theme.palette.grey[500]), width: "10%" };
}
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Действия карточки события
const CardActions = ({
taskRn,
taskClosed,
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 =>
action.visible ? (
<MenuItem
sx={action.delimiter ? STYLES.MENU_ITEM_DELIMITER : {}}
key={`${taskRn}_${action.method}`}
onClick={() => handleActionClick(action)}
disabled={taskClosed === 1 && action.disableClosed ? true : false}
>
<Icon>{action.icon}</Icon>
<Typography pl={1}>{action.name}</Typography>
</MenuItem>
) : null
)}
</Menu>
</Box>
);
};
//Контроль свойств - Действия карточки события
CardActions.propTypes = {
taskRn: PropTypes.number.isRequired,
taskClosed: PropTypes.oneOf([0, 1]).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, handleCatalogTreeOpen } = 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 handleTaskMove = useCallback(
async ({ nEvent, nCrn, onReload }) => {
await executeStored({
stored: "PKG_P8PANELS_CLNTTSKBRD.CLNEVENTS_MOVE",
args: { NCLNEVENTS: nEvent, NCRN: nCrn }
});
//Если требуется перезагрузить данные
onReload ? onReload() : null;
},
[executeStored]
);
//По нажатию действия "Направить"
const handleTaskSendAction = 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 handleTaskStateChangeAction = useCallback(
async ({ nEvent, onReload, onNoteOpen }) => {
//Выполняем изменения статуса события
handleTaskStateChange({ nEvent, onReload, onNoteOpen });
},
[handleTaskStateChange]
);
//По нажатию действия "Переместить"
const handleTaskMoveAction = useCallback(
async ({ nEvent, onReload }) => {
//Открываем выбор записи из раздела "Каталоги иерархии"
handleCatalogTreeOpen({
sUnitName: "ClientEvents",
nRn: task.nCrn,
callBack: res => {
//Выполняем перемещение события
handleTaskMove({
nEvent,
nCrn: res.outParameters.out_RN,
onReload
});
}
});
},
[handleCatalogTreeOpen, handleTaskMove, task.nCrn]
);
//При изменении ссылок в меню действий (для того, чтобы ссылка на объект менялась при реальной необходимости)
useEffect(() => {
//Устанавливаем список меню
setMenuItems(
makeCardActionsArray(
handleTaskEditAction,
handleTaskEditClientAction,
handleTaskDeleteAction,
handleTaskStateChangeAction,
handleTaskReturnAction,
handleTaskSendAction,
handleEventNotesOpenAction,
handleTaskFileLinksOpenAction,
handleTaskMoveAction
)
);
}, [
handleEventNotesOpenAction,
handleTaskFileLinksOpenAction,
handleTaskSendAction,
handleTaskStateChangeAction,
handleTaskDeleteAction,
handleTaskEditAction,
handleTaskEditClientAction,
handleTaskReturnAction,
handleTaskMoveAction
]);
//Генерация содержимого
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(task, colorRule)}>
<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
);
}}
title={task.sDescription}
>
{task.sDescription}
</Typography>
}
sx={STYLES.CARD_HEADER}
action={
<CardActions
taskRn={task.nRn}
taskClosed={task.nClosed}
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_TASK} noWrap title={task.name}>
{task.name}
</Typography>
{task.sSender ? (
<Stack direction="row" spacing={0.5} sx={STYLES.STACK_SENDER}>
<Typography sx={STYLES.TYPOGRAPHY_SENDER} title={task.sSender} noWrap>
{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

@ -1,151 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент панели: Форма события
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useCallback } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, 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 { COMMON_STYLES } from "../styles"; //Общие стили
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER: { height: "625px", textAlign: "center", overflow: "hidden", display: "flex", flexDirection: "column" },
BOX_TAB: { height: "575px", overflowY: "auto", ...COMMON_STYLES.SCROLL }
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Свойства вкладки
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}
sx={STYLES.BOX_TAB}
>
{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, editable, docProps, onTaskChange, onEventNextNumbGet }) => {
//Состояние вкладки
const [tab, setTab] = useState(0);
//При изменении вкладки
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]
);
//Генерация содержимого
return (
<Box sx={STYLES.CONTAINER}>
<Tabs value={tab} onChange={handleTabChange} aria-label="tabs of values">
<Tab label="Событие" {...a11yProps(0)} />
<Tab label="Исполнитель" {...a11yProps(1)} />
{docProps.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.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,
editable: PropTypes.bool.isRequired,
docProps: PropTypes.array,
onTaskChange: PropTypes.func.isRequired,
onEventNextNumbGet: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskForm };

View File

@ -1,155 +0,0 @@
/*
Парус 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

@ -1,192 +0,0 @@
/*
Парус 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, task.isUpdate || task.nClosed === 1)}
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 || task.nClosed === 1}
InputProps={getInputProps(() => handleClntClientsChange(), !task.sType || task.nClosed === 1)}
></TextField>
<TextField
sx={COMMON_STYLES.TASK_FORM_TEXT_FIELD()}
id="sClntClnperson"
label="Сотрудник"
value={task.sClntClnperson}
variant="standard"
onChange={onFieldEdit}
disabled={!task.sType || task.nClosed === 1}
InputProps={getInputProps(() => handleClntClnpersonChange(), !task.sType || task.nClosed === 1)}
></TextField>
</Box>
</Box>
);
};
//Контроль свойств - Вкладка основной информации
TaskFormTabInfo.propTypes = {
task: PropTypes.object.isRequired,
editable: PropTypes.bool.isRequired,
onFieldEdit: PropTypes.func.isRequired,
onEventNextNumbGet: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskFormTabInfo };

View File

@ -1,178 +0,0 @@
/*
Парус 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"; //Состояние открытия разделов
//---------
//Константы
//---------
//Стили
const STYLES = {
BOX_FEW_COLUMNS: { display: "flex", flexWrap: "wrap", justifyContent: "space-between" }
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Подключение настройки пользовательского формата даты
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">
<Box sx={STYLES.BOX_FEW_COLUMNS}>
{docProps.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>
</Box>
);
};
//Контроль свойств - Вкладка информации со свойствами
TaskFormTabProps.propTypes = {
task: PropTypes.object.isRequired,
docProps: PropTypes.array.isRequired,
onPropEdit: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskFormTabProps };

View File

@ -1,212 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент: Фильтр отбора
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Chip, Stack, Icon, IconButton, Box, Menu, MenuItem, Typography } from "@mui/material"; //Интерфейсные компоненты
import { 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

@ -1,256 +0,0 @@
/*
Парус 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 },
{ name: "in_RN", value: prms.nRn }
],
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

@ -1,122 +0,0 @@
/*
Парус 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

@ -1,243 +0,0 @@
/*
Парус 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

@ -1,193 +0,0 @@
/*
Парус 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,
nClosed: data.XEVENT.NCLOSED,
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

@ -1,453 +0,0 @@
/*
Парус 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: "",
nCrn: task.NCRN,
nClosed: task.NCLOSED,
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

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

View File

@ -1,303 +0,0 @@
/*
Парус 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, onMove) => {
//Формируем список действий карточки
return [
{
method: "EDIT",
name: "Исправить",
icon: "edit",
visible: false,
delimiter: false,
tasksReload: false,
needAccountsReload: false,
disableClosed: false,
func: onEdit
},
{
method: "EDIT_CLIENT",
name: "Исправить в разделе",
icon: "edit_note",
visible: true,
delimiter: false,
tasksReload: false,
needAccountsReload: false,
disableClosed: false,
func: onEditClient
},
{
method: "MOVE",
name: "Переместить",
icon: "drive_file_move",
visible: true,
delimiter: false,
tasksReload: true,
needAccountsReload: false,
disableClosed: false,
func: onMove
},
{
method: "DELETE",
name: "Удалить",
icon: "delete",
visible: true,
delimiter: true,
tasksReload: true,
needAccountsReload: false,
disableClosed: false,
func: onDelete
},
{
method: "TASK_STATE_CHANGE",
name: "Перейти",
icon: "turn_right",
visible: true,
delimiter: false,
tasksReload: true,
needAccountsReload: true,
disableClosed: true,
func: onStateChange
},
{
method: "TASK_RETURN",
name: "Выполнить возврат",
icon: "turn_left",
visible: true,
delimiter: false,
tasksReload: true,
needAccountsReload: true,
disableClosed: true,
func: onReturn
},
{
method: "TASK_SEND",
name: "Направить",
icon: "send",
visible: true,
delimiter: true,
tasksReload: true,
needAccountsReload: true,
disableClosed: true,
func: onSend
},
{
method: "NOTES",
name: "Примечания",
icon: "event_note",
visible: true,
delimiter: true,
tasksReload: false,
needAccountsReload: false,
disableClosed: false,
func: onNotesOpen
},
{
method: "FILE_LINKS",
name: "Присоединенные документы",
icon: "attach_file",
visible: true,
delimiter: false,
tasksReload: false,
needAccountsReload: false,
disableClosed: false,
func: onFileLinksOpen
}
];
};

View File

@ -1,48 +0,0 @@
/*
Парус 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", marginBottom: "1px" },
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

@ -1,176 +0,0 @@
/*
Парус 8 - Панели мониторинга - УДП - Доски задач
Компонент панели: Диалог формы события
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useCallback, useContext, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { useClientEvent } from "./hooks/task_dialog_hooks"; //Хук для события
import { useDocsProps } from "./hooks/task_dialog_hooks"; //Хук для получения доп. свойств раздела "События"
import { TaskForm } from "./components/task_form"; //Форма события
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { object2Base64XML } from "../../core/utils"; //Вспомогательные функции
import { hasValue } from "../../core/utils"; //Вспомогательные процедуры и функции
import { P8PDialog } from "../../components/p8p_dialog"; //Типовой диалог
//---------
//Константы
//---------
//-----------
//Тело модуля
//-----------
//Диалог формы события
const TaskDialog = ({ taskRn, taskType, editable, onTasksReload, onClose }) => {
//Собственное состояние
const [task, setTask] = useClientEvent(taskRn, taskType);
//Состояние допустимых дополнительных свойств
const [docProps] = useDocsProps(taskType);
//Состояние заполненности всех обязательных доп. свойств
const [docPropsReady, setDocPropsReady] = useState(false);
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//При изменении информации о задаче
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]);
//Проверка заполненности всех обязательных доп. свойств
useEffect(() => {
//Если доп. свойства загрузились
if (docProps.loaded) {
//Проверяем остались ли обязательные незаполненные свойства
let notFilled = docProps.props.some(docProp => docProp.BREQUIRE === true && !hasValue(task.docProps[docProp.SFORMATTED_ID]));
//Если незаполненных обязательных доп. свойств не осталось - доп. свойства готовы, иначе не готовы
setDocPropsReady(!notFilled);
} else {
//Доп. свойства не готовы
setDocPropsReady(false);
}
}, [docProps.loaded, docProps.props, task.docProps]);
//Генерация содержимого
return (
<>
{!task.init && docProps.loaded && (
<P8PDialog
title={task.nRn ? `Исправление события${task.nClosed ? " [аннулировано]" : ""}` : "Добавление события"}
fullWidth={true}
onOk={() => (taskRn ? handleUpdateEvent(onClose).then(onTasksReload) : handleInsertTask(onClose).then(onTasksReload))}
onClose={onClose ? onClose : null}
okDisabled={taskRn ? task.updateDisabled || !editable || !docPropsReady : task.insertDisabled || !docPropsReady}
scrollContent={false}
>
<TaskForm
task={task}
taskType={taskType}
editable={!taskRn || editable ? true : false}
docProps={docProps.props}
onTaskChange={handleTaskChange}
onEventNextNumbGet={handleEventNextNumbGet}
/>
</P8PDialog>
)}
</>
);
};
//Контроль свойств - Диалог формы события
TaskDialog.propTypes = {
taskRn: PropTypes.number,
taskType: PropTypes.string.isRequired,
editable: PropTypes.bool,
onTasksReload: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { TaskDialog };

View File

@ -1,281 +0,0 @@
/*
Парус 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

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

View File

@ -1,484 +0,0 @@
/*
Парус 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

@ -1,103 +0,0 @@
/*
Парус 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,8 +48,7 @@ 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) : ["SDOCPREF", "SDOCNUMB"].includes(name) ? undefined : val,
attributeValueProcessor: (name, val) => (["DEXEC_DATE", "DREL_DATE"].includes(name) ? formatDateRF(val) : val),
respArg: "COUT"
});
setCostRouteLists(pv => ({

View File

@ -36,8 +36,7 @@ import {
Card,
CardHeader,
CardContent,
CardActions,
Tooltip
CardActions
} from "@mui/material"; //Интерфейсные элементы
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
@ -56,34 +55,19 @@ import { IncomFromDepsDataGrid } from "./datagrids/incomefromdeps"; //Табли
//---------
//Склонения для документов
const PLANS_DECLINATIONS = ["план", "плана", "планов"];
const SPEC_DECLINATIONS = ["элемент", "элемента", "элементов"];
const 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",
@ -100,26 +84,7 @@ 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_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"
}
}
: {};
}
FILTERS_LEVEL: { display: "table-cell", verticalAlign: "middle", paddingLeft: "15px" }
};
//------------------------------------
@ -137,9 +102,7 @@ const parseProdPlanSpXML = async xmlDoc => {
};
//Форматирование для отображения количества документов
const formatCountDocs = (nCountDocs, nType = 0) => {
//Склонение документов
let DECLINATIONS = nType === 0 ? PLANS_DECLINATIONS : SPEC_DECLINATIONS;
const formatCountDocs = nCountDocs => {
//Получаем последнюю цифру в значении
let num = (nCountDocs % 100) % 10;
//Документов
@ -152,146 +115,55 @@ const formatCountDocs = (nCountDocs, nType = 0) => {
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 = [],
selectedPlans = [],
selectedPlanCtlg,
selectedPlansElements,
filter,
setFilter,
onCtlgClick,
onCtlgPlanClick,
onCtlgPlansOk,
onCtlgPlansCancel
} = {}) => {
const PlanCtlgsList = ({ planCtlgs = [], selectedPlanCtlg, filter, setFilter, onClick } = {}) => {
//Генерация содержимого
return (
<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>
<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>
);
};
//Контроль свойств - Список каталогов планов
PlanCtlgsList.propTypes = {
planCtlgs: PropTypes.array,
selectedPlans: PropTypes.array,
selectedPlanCtlg: PropTypes.number,
selectedPlansElements: PropTypes.number,
onCtlgClick: PropTypes.func,
onCtlgPlanClick: PropTypes.func,
onClick: PropTypes.func,
filter: PropTypes.object,
setFilter: PropTypes.func,
onCtlgPlansOk: PropTypes.func,
onCtlgPlansCancel: PropTypes.func
setFilter: PropTypes.func
};
//Генерация диалога задачи
@ -360,18 +232,12 @@ 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,
@ -387,14 +253,11 @@ const MechRecCostProdPlans = () => {
const { InlineMsgInfo } = useContext(MessagingСtx);
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
const { executeStored } = useContext(BackEndСtx);
//Подключение к контексту навигации
const { getNavigationSearch } = useContext(NavigationCtx);
//Подключение к контексту сообщений
const { showMsgInfo } = useContext(MessagingСtx);
//Инициализация каталогов планов
const initPlanCtlgs = useCallback(async () => {
if (!state.init) {
@ -402,101 +265,74 @@ const MechRecCostProdPlans = () => {
stored: "PKG_P8PANELS_MECHREC.FCPRODPLAN_PP_CTLG_INIT",
args: {},
respArg: "COUT",
isArray: name => ["XFCPRODPLAN_CRNS", "XCRN_PLANS"].includes(name)
isArray: name => name === "XFCPRODPLAN_CRNS"
});
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,
CFCPRODPLANS: {
VALUE: state.selectedPlans.length > 0 ? state.selectedPlans.join(";") : null,
SDATA_TYPE: SERV_DATA_TYPE_CLOB
},
NLEVEL: level,
SSORT_FIELD: sort,
NFCPRODPLANSP: state.planSpec
}
args: { NCRN: state.selectedPlanCtlg, 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: [...Array(data.NMAX_LEVEL).keys()].map(el => el + 1),
selectedPlanCtlgMenuItems: state.selectedPlanCtlgMenuItems
? state.selectedPlanCtlgMenuItems
: [...Array(data.NMAX_LEVEL).keys()].map(el => el + 1),
selectedPlanCtlgSpecsLoaded: true,
gantt: { ...doc, tasks: [...(doc?.tasks || [])] },
loadedCtlg: state.selectedPlanCtlg,
loadedPlans: [...state.selectedPlans],
loadedElements: state.selectedPlansElements,
showPlanList: false
gantt: { ...doc, tasks: [...(doc?.tasks || [])] }
}));
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[executeStored, state.selectedPlanCtlg, state.selectedPlans, state.planSpec]
[executeStored, state.ident, state.selectedPlanCtlg, state.planSpec]
);
//Обработка нажатия на элемент в списке каталогов планов
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
}));
const handleProjectClick = project => {
if (state.selectedPlanCtlg != project.NRN) selectPlan(project.NRN);
else unselectPlan();
};
//При подключении компонента к странице
@ -509,8 +345,8 @@ const MechRecCostProdPlans = () => {
//При смене выбранного каталога плана или при явном указании позиции спецификации плана
useEffect(() => {
if (state.planSpec) loadPlanCtglSpecs(null, SORT_REP_DATE_TO);
}, [state.planSpec, loadPlanCtglSpecs]);
if (state.selectedPlanCtlg || state.planSpec) loadPlanCtglSpecs(null, SORT_REP_DATE_TO);
}, [state.selectedPlanCtlg, state.planSpec, loadPlanCtglSpecs]);
//Выбор уровня
const handleChangeSelectLevel = selectedLevel => {
@ -534,17 +370,6 @@ const MechRecCostProdPlans = () => {
setState(pv => ({ ...pv, selectedTaskDetail: taskRn, selectedTaskDetailType: taskType }));
};
//При открытии окна информации об ограничении уровня
const handleLevelLimitInfoOpen = () => {
//Отображаем информацию
showMsgInfo(
`Размер производственной программы превышает предельно допустимый для одновременного отображения в виде диаграммы Ганта.
Доступные для просмотра уровни вложенности ограничены.
Вы можете просматривать производственную программу частями, используя действие "Открытие панели Производственная программа" в спецификации "Выпуск"
раздела "Планы и отчеты производства изделий".`
);
};
//Генерация содержимого
return (
<Box>
@ -553,18 +378,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={handleSelectedPlansCancel} sx={STYLES.PLANS_DRAWER}>
<Drawer
anchor={"left"}
open={state.showPlanList}
onClose={() => setState(pv => ({ ...pv, showPlanList: false }))}
sx={STYLES.PLANS_DRAWER}
>
<PlanCtlgsList
planCtlgs={filteredPlanCtgls}
selectedPlans={state.selectedPlans}
selectedPlanCtlg={state.selectedPlanCtlg}
selectedPlansElements={state.selectedPlansElements}
filter={filter}
setFilter={setFilter}
onCtlgClick={handleCtlgClick}
onCtlgPlanClick={handleCtlgPlanClick}
onCtlgPlansOk={handleSelectedPlansOk}
onCtlgPlansCancel={handleSelectedPlansCancel}
onClick={handleProjectClick}
/>
</Drawer>
</>
@ -609,16 +434,8 @@ const MechRecCostProdPlans = () => {
</Select>
</Box>
<Box sx={STYLES.FILTERS_LEVEL}>
<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>
<InputLabel id="select-label-level">До уровня</InputLabel>
<Select
sx={STYLES.FILTERS_LIMIT_SELECT(state.selectedPlanCtlgOutOfLimit)}
labelId="select-label-level"
id="select-level"
value={state.selectedPlanCtlgLevel}
@ -646,14 +463,14 @@ const MechRecCostProdPlans = () => {
/>
</Box>
)
) : !state.loadedCtlg ? (
) : !state.selectedPlanCtlg ? (
<Box pt={3}>
<InlineMsgInfo
okBtn={false}
text={
state.planSpec
? "Загружаю график для выбранной позиции плана..."
: "Укажите каталог планов или планы для отображения их спецификаций"
: "Укажите каталог планов для отображения их спецификаций"
}
/>
</Box>

View File

@ -7,7 +7,7 @@
//Подключение библиотек
//---------------------
import React, { useState, useEffect, useContext, useCallback } from "react"; //Классы React
import React, { useState, useEffect, useContext } from "react"; //Классы React
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { object2Base64XML, formatDateRF } from "../../core/utils"; //Вспомогательные функции
@ -32,60 +32,17 @@ export const useFilteredPlans = (plans, filter) => {
return filteredPlans;
};
//Хук для планов
const useDeptCostProdPlans = month => {
//Собственное состояние - таблица данных
const [state, setState] = useState({
init: false,
loaded: false,
showPlanList: false,
rows: [],
reload: true,
selected: {},
currentMonth: ""
});
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При подключении компонента к странице
useEffect(() => {
const initPlans = async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_MECHREC.FCPRODPLAN_DEPT_INIT",
args: { SMONTH: month },
respArg: "COUT",
isArray: name => name === "XFCPRODPLANS",
attributeValueProcessor: (name, val) => (name === "SPERIOD" ? undefined : val)
});
setState(pv => ({
...pv,
init: true,
rows: [...(data?.XFCPRODPLANS || [])],
loaded: true,
reload: false,
selected: {},
currentMonth: month
}));
};
//Если месяц указан и он не соответствует текущим данным
if (month && month !== state.currentMonth) {
initPlans();
}
}, [executeStored, month, state.currentMonth]);
//Возвращаем данные
return [state, setState];
};
//Хук для информации о плане
const useDeptCostProdPlanInfo = plan => {
//Хук для основной таблицы
const useDeptCostProdPlans = () => {
//Собственное состояние - таблица данных
const [state, setState] = useState({
init: false,
showPlanList: false,
showIncomeFromDeps: null,
showFcroutelst: null,
planList: [],
planListLoaded: false,
selectedPlan: {},
dataLoaded: false,
columnsDef: [],
orders: null,
@ -100,43 +57,14 @@ const useDeptCostProdPlanInfo = plan => {
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//При необходимости очистки данных о плане
const handleClear = useCallback(
() =>
setState(pv => ({
...pv,
init: false,
showPlanList: false,
showIncomeFromDeps: null,
showFcroutelst: null,
dataLoaded: false,
columnsDef: [],
orders: null,
rows: [],
reload: true,
pageNumber: 1,
morePages: true,
fixedHeader: false,
fixedColumns: 0
})),
[]
);
//При изменении состояния сортировки
const handleOrderChanged = ({ orders }) => setState(pv => ({ ...pv, orders: [...orders], pageNumber: 1, reload: true }));
//При изменении количества отображаемых страниц
const handlePagesCountChanged = () => setState(pv => ({ ...pv, pageNumber: pv.pageNumber + 1, reload: true }));
//При необходимости обновить данные таблицы
useEffect(() => {
//Если план выбран
if (plan.NRN) {
if (state.selectedPlan.NRN) {
const loadData = async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_MECHREC.FCPRODPLANSP_DEPT_DG_GET",
args: {
NFCPRODPLAN: plan.NRN,
NFCPRODPLAN: state.selectedPlan.NRN,
CORDERS: { VALUE: object2Base64XML(state.orders, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
NPAGE_NUMBER: state.pageNumber,
NPAGE_SIZE: DATA_GRID_PAGE_SIZE_LARGE,
@ -148,7 +76,6 @@ const useDeptCostProdPlanInfo = plan => {
setState(pv => ({
...pv,
...data.XDATA_GRID,
init: true,
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,
@ -156,20 +83,31 @@ const useDeptCostProdPlanInfo = plan => {
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE_LARGE
}));
};
//Если необходимо перезагрузить
if (state.reload) {
loadData();
}
}
//Если план не выбран и есть какие-то данные
if (!plan.NRN && state.dataLoaded) {
//Очищаем их
handleClear();
}
}, [plan.NRN, state.reload, state.orders, state.pageNumber, state.dataLoaded, executeStored, SERV_DATA_TYPE_CLOB, handleClear]);
}, [state.selectedPlan, state.reload, state.orders, state.pageNumber, state.dataLoaded, executeStored, SERV_DATA_TYPE_CLOB]);
//Возвращаем данные
return [state, setState, handleClear, handleOrderChanged, handlePagesCountChanged];
//При подключении компонента к странице
useEffect(() => {
const initPlans = async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_MECHREC.FCPRODPLAN_DEPT_INIT",
args: {},
respArg: "COUT",
isArray: name => name === "XFCPRODPLANS",
attributeValueProcessor: (name, val) => (name === "SPERIOD" ? undefined : val)
});
setState(pv => ({ ...pv, init: true, planList: [...(data?.XFCPRODPLANS || [])], planListLoaded: true }));
};
if (!state.init) {
initPlans();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return [state, setState];
};
//Хук для таблицы маршрутных листов
@ -226,7 +164,6 @@ const useCostRouteLists = task => {
task
]);
//Возвращаем данные
return [costRouteLists, setCostRouteLists];
};
@ -286,7 +223,6 @@ const useCostRouteListsSpecs = mainRowRN => {
mainRowRN
]);
//Возвращаем данные
return [costRouteListsSpecs, setCostRouteListsSpecs];
};
@ -336,8 +272,7 @@ const useIncomFromDeps = task => {
}
}, [SERV_DATA_TYPE_CLOB, executeStored, incomFromDeps.dataLoaded, incomFromDeps.orders, incomFromDeps.pageNumber, incomFromDeps.reload, task]);
//Возвращаем данные
return [incomFromDeps, setIncomFromDeps];
};
export { useDeptCostProdPlans, useDeptCostProdPlanInfo, useCostRouteLists, useCostRouteListsSpecs, useIncomFromDeps };
export { useDeptCostProdPlans, useCostRouteLists, useCostRouteListsSpecs, useIncomFromDeps };

View File

@ -12,7 +12,7 @@ import PropTypes from "prop-types"; //Контроль свойств компо
import { Drawer, Fab, Box, List, ListItemButton, ListItemText, Typography, TextField, Link, Grid } from "@mui/material"; //Интерфейсные элементы
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
import { useDeptCostProdPlans, useFilteredPlans, useDeptCostProdPlanInfo } from "./hooks"; //Вспомогательные хуки
import { useDeptCostProdPlans, useFilteredPlans } from "./hooks"; //Вспомогательные хуки
import { P8PDataGrid, P8P_DATA_GRID_SIZE } from "../../components/p8p_data_grid"; //Таблица данных
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
@ -34,8 +34,7 @@ const TITLE_PADDING_BOTTOM = "20px";
//Стили
const STYLES = {
PLANS_FILTER: { paddingTop: "20px", display: "flex", flexDirection: "column", alignItems: "center", gap: "5px" },
PLANS_FILTER_ITEM: { margin: "0px 10px", width: "93%" },
PLANS_FINDER: { marginTop: "10px", marginLeft: "10px", width: "93%" },
PLANS_LIST_ITEM_PRIMARY: { wordWrap: "break-word" },
PLANS_BUTTON: { position: "absolute" },
PLANS_DRAWER: {
@ -61,18 +60,10 @@ const STYLES = {
FACT_VALUE: { color: "blue" }
};
//Имена полей компонента
const SFIELD_MONTH = "month";
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Считывание текущего года и месяца в формате "YYYY-MM"
const getCurrentYearMonth = () => {
return `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`;
};
//Генерация представления ячейки заголовка группы
export const groupCellRender = ({ group }) => ({
cellStyle: STYLES.DATA_GRID_GROUP_CELL,
@ -97,7 +88,7 @@ const getRowBackgroudColor = row => {
};
//Генерация заливки строки исходя от значений
const dataCellRender = ({ row, columnDef, onProdOrderClick, onMatresCodeClick }) => {
const dataCellRender = ({ row, columnDef, handleProdOrderClick, handleMatresCodeClick }) => {
//Описываем общие свойства
let cellProps = { title: row[columnDef.name] };
//Описываем общий стиль
@ -136,7 +127,7 @@ const dataCellRender = ({ row, columnDef, onProdOrderClick, onMatresCodeClick })
cellProps,
cellStyle,
data: (
<Link component="button" variant="body2" align="left" underline="hover" onClick={() => onProdOrderClick(row["NRN"])}>
<Link component="button" variant="body2" align="left" underline="hover" onClick={() => handleProdOrderClick(row["NRN"])}>
{row[columnDef.name]}
</Link>
)
@ -148,7 +139,7 @@ const dataCellRender = ({ row, columnDef, onProdOrderClick, onMatresCodeClick })
cellProps,
cellStyle: STYLES.DATA_GRID_CELL_MATRES_CODE(cellStyle, row),
data: (
<Link component="button" variant="body2" align="left" underline="hover" onClick={() => onMatresCodeClick(row["NRN"])}>
<Link component="button" variant="body2" align="left" underline="hover" onClick={() => handleMatresCodeClick(row["NRN"])}>
{row[columnDef.name]}
</Link>
)
@ -158,38 +149,21 @@ const dataCellRender = ({ row, columnDef, onProdOrderClick, onMatresCodeClick })
};
//Список каталогов планов
const PlanList = ({ plans = [], selectedPlan, filter, onFilterChange, onClick } = {}) => {
//При изменении фильтра
const handleFilterChange = e => {
onFilterChange && onFilterChange(e);
};
const PlanList = ({ plans = [], selectedPlan, filter, setFilter, onClick } = {}) => {
//Генерация содержимого
return (
<div>
<Box sx={STYLES.PLANS_FILTER}>
<TextField
sx={STYLES.PLANS_FILTER_ITEM}
name={SFIELD_MONTH}
label="Месяц"
type="month"
value={filter.month}
InputLabelProps={{ shrink: true }}
variant="standard"
fullWidth
onChange={handleFilterChange}
required={true}
></TextField>
<TextField
sx={STYLES.PLANS_FILTER_ITEM}
name="planName"
label="План"
value={filter.planName}
variant="standard"
fullWidth
onChange={handleFilterChange}
></TextField>
</Box>
<TextField
sx={STYLES.PLANS_FINDER}
name="planFilter"
label="План"
value={filter.planName}
variant="standard"
fullWidth
onChange={event => {
setFilter(pv => ({ ...pv, planName: event.target.value }));
}}
></TextField>
<List>
{plans.map(p => (
<ListItemButton key={p.NRN} selected={p.NRN === selectedPlan.NRN} onClick={() => (onClick ? onClick(p) : null)}>
@ -207,7 +181,7 @@ PlanList.propTypes = {
selectedPlan: PropTypes.object,
onClick: PropTypes.func,
filter: PropTypes.object,
onFilterChange: PropTypes.func
setFilter: PropTypes.func
};
//-----------
@ -216,135 +190,135 @@ PlanList.propTypes = {
//Корневая панель производственного плана цеха
const MechRecDeptCostProdPlans = () => {
//Собственное состояние - таблица данных
const [state, setState] = useDeptCostProdPlans();
//Состояние для фильтра каталогов
const [filter, setFilter] = useState({ planName: "", month: getCurrentYearMonth() });
//Собственное состояние - таблица планов
const [plans, setPlans] = useDeptCostProdPlans(filter.month);
//Собственное состояние - таблица информации
const [planInfo, setPlanInfo, onPlanInfoClear, onPlanInfoOrderChanged, onPlanInfoPagesCountChanged] = useDeptCostProdPlanInfo(plans.selected);
const [filter, setFilter] = useState({ planName: "" });
//Массив отфильтрованных каталогов
const filteredPlanCtgls = useFilteredPlans(plans.rows, filter);
const filteredPlanCtgls = useFilteredPlans(state.planList, filter);
//Подключение к контексту сообщений
const { InlineMsgInfo } = useContext(MessagingСtx);
//Выбор плана
const selectPlan = plan => {
setPlans(pv => ({
setState(pv => ({
...pv,
selected: plan,
showPlanList: false
showIncomeFromDeps: null,
showFcroutelst: null,
selectedPlan: plan,
showPlanList: false,
dataLoaded: false,
columnsDef: [],
orders: null,
rows: [],
reload: true,
pageNumber: 1,
morePages: true
}));
onPlanInfoClear();
};
//Сброс выбора плана
const unselectPlan = () => {
setPlans(pv => ({
const unselectPlan = () =>
setState(pv => ({
...pv,
selected: {},
showPlanList: false
showIncomeFromDeps: null,
showFcroutelst: null,
selectedPlan: {},
showPlanList: false,
dataLoaded: false,
columnsDef: [],
orders: null,
rows: [],
reload: true,
pageNumber: 1,
morePages: true
}));
onPlanInfoClear();
};
//Обработка нажатия на элемент в списке планов
const handlePlanClick = plan => {
if (plans.selected.NRN != plan.NRN) selectPlan(plan);
if (state.selectedPlan.NRN != plan.NRN) selectPlan(plan);
else unselectPlan();
};
//При изменении состояния сортировки информации плана
const handlePlanInfoOrderChanged = ({ orders }) => onPlanInfoOrderChanged({ orders });
//При изменении состояния сортировки
const handleOrderChanged = ({ orders }) => setState(pv => ({ ...pv, orders: [...orders], pageNumber: 1, reload: true }));
//При изменении количества отображаемых страниц информации плана
const handlePlanInfoPagesCountChanged = () => onPlanInfoPagesCountChanged();
//При изменении количества отображаемых страниц
const handlePagesCountChanged = () => setState(pv => ({ ...pv, pageNumber: pv.pageNumber + 1, reload: true }));
//При нажатии на "Заказ"
const handleProdOrderClick = planSp => {
setPlanInfo(pv => ({ ...pv, showIncomeFromDeps: planSp }));
setState(pv => ({ ...pv, showIncomeFromDeps: planSp }));
};
//При нажатии на "Обозначение"
const handleMatresCodeClick = planSp => {
setPlanInfo(pv => ({ ...pv, showFcroutelst: planSp }));
setState(pv => ({ ...pv, showFcroutelst: planSp }));
};
//При изменении фильтров
const handleFilterChange = e =>
setFilter(pv => ({ ...pv, [e.target.name]: e.target.name === SFIELD_MONTH && !e.target.value ? getCurrentYearMonth() : e.target.value }));
//Генерация содержимого
return (
<Box p={2}>
<Fab variant="extended" sx={STYLES.PLANS_BUTTON} onClick={() => setPlans(pv => ({ ...pv, showPlanList: !pv.showPlanList }))}>
<Fab variant="extended" sx={STYLES.PLANS_BUTTON} onClick={() => setState(pv => ({ ...pv, showPlanList: !pv.showPlanList }))}>
Планы
</Fab>
<Drawer
anchor={"left"}
open={plans.showPlanList}
onClose={() => setPlans(pv => ({ ...pv, showPlanList: false }))}
open={state.showPlanList}
onClose={() => setState(pv => ({ ...pv, showPlanList: false }))}
sx={STYLES.PLANS_DRAWER}
>
<PlanList
plans={filteredPlanCtgls}
selectedPlan={plans.selected}
selectedPlan={state.selectedPlan}
filter={filter}
onFilterChange={handleFilterChange}
setFilter={setFilter}
onClick={handlePlanClick}
/>
</Drawer>
<Grid container>
<Grid item xs={12}>
<Box display="flex" justifyContent="center" alignItems="center">
{planInfo.dataLoaded ? (
planInfo.rows.length === 0 ? (
{state.dataLoaded ? (
state.rows.length === 0 ? (
<InlineMsgInfo okBtn={false} text={"В плане отсутствуют записи спецификации"} />
) : (
<Box sx={STYLES.CONTAINER}>
<Typography sx={STYLES.TITLE} variant={"h6"}>
{`Производственный план цеха №${plans.selected.SSUBDIV} на ${plans.selected.SPERIOD}`}
{`Производственный план цеха №${state.selectedPlan.SSUBDIV} на ${state.selectedPlan.SPERIOD}`}
</Typography>
<Box>
<P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS}
containerComponentProps={{ elevation: 6, sx: STYLES.DATA_GRID_CONTAINER }}
fixedHeader={planInfo.fixedHeader}
fixedColumns={planInfo.fixedColumns}
columnsDef={planInfo.columnsDef}
rows={planInfo.rows}
fixedHeader={state.fixedHeader}
fixedColumns={state.fixedColumns}
columnsDef={state.columnsDef}
rows={state.rows}
size={P8P_DATA_GRID_SIZE.MEDIUM}
morePages={planInfo.morePages}
reloading={planInfo.reload}
onOrderChanged={handlePlanInfoOrderChanged}
onPagesCountChanged={handlePlanInfoPagesCountChanged}
dataCellRender={prms =>
dataCellRender({
...prms,
onProdOrderClick: handleProdOrderClick,
onMatresCodeClick: handleMatresCodeClick
})
}
morePages={state.morePages}
reloading={state.reload}
onOrderChanged={handleOrderChanged}
onPagesCountChanged={handlePagesCountChanged}
dataCellRender={prms => dataCellRender({ ...prms, handleProdOrderClick, handleMatresCodeClick })}
groupCellRender={groupCellRender}
/>
</Box>
</Box>
)
) : !plans.selected.NRN ? (
) : !state.selectedPlan.NRN ? (
<InlineMsgInfo okBtn={false} text={"Укажите план для отображения спецификаций"} />
) : null}
</Box>
</Grid>
</Grid>
{planInfo.showIncomeFromDeps ? (
<IncomFromDepsDataGridDialog task={planInfo.showIncomeFromDeps} onClose={() => handleProdOrderClick(null)} />
) : null}
{planInfo.showFcroutelst ? (
<CostRouteListsDataGridDialog task={planInfo.showFcroutelst} onClose={() => handleMatresCodeClick(null)} />
{state.showIncomeFromDeps ? (
<IncomFromDepsDataGridDialog task={state.showIncomeFromDeps} onClose={() => handleProdOrderClick(null)} />
) : null}
{state.showFcroutelst ? <CostRouteListsDataGridDialog task={state.showFcroutelst} onClose={() => handleMatresCodeClick(null)} /> : null}
</Box>
);
};

View File

@ -1,51 +0,0 @@
/*
Парус 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

@ -1,83 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Представление компонента панели
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, Typography } from "@mui/material"; //Интерфейсные элементы
import { useComponentModule } from "./components/components_hooks"; //Хуки компонентов
//---------
//Константы
//---------
//Стили
const STYLES = {
COMPONENT_BOX: isDragging => {
return isDragging ? { pointerEvents: "none" } : {};
}
};
//-----------
//Тело модуля
//-----------
//Представление компонента панели
const ComponentView = ({ id, isDragging, path, settings = {}, values = {}, onValuesChange = null } = {}) => {
//Подгрузка модуля представления компонента (lazy здесь постоянно обновлялся при смене props, поэтому на хуке, от props независимого)
const [ComponentView, init] = useComponentModule({ path, module: "view" });
//При смене значений
const handleValuesChange = values => onValuesChange && onValuesChange(values);
//Расчёт флага наличия компонента
const haveComponent = path ? true : false;
//Формирование представления
return (
<Box className={"component-view__wrap"} sx={STYLES.COMPONENT_BOX(isDragging)}>
{haveComponent && init && <ComponentView.default id={id} {...settings} values={values} onValuesChange={handleValuesChange} />}
{!haveComponent && <Typography align={"center"}>Компонент не определён</Typography>}
</Box>
);
};
//Контроль свойств компонента - компонент панели
ComponentView.propTypes = {
id: PropTypes.string.isRequired,
isDragging: PropTypes.bool.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

@ -1,51 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: График (общие ресурсы действия)
*/
//---------------------
//Подключение библиотек
//---------------------
//---------
//Константы
//---------
//Собственные значения типа действия
const P8P_CA_CHART_TYPE_VALUE = {
GRPH_ELEMENT: "Элемент графика"
};
//Типы значений действий графика
const P8P_CA_CHART_VALUE_TYPES = [...Object.values(P8P_CA_CHART_TYPE_VALUE)];
//Доступные области действий графика
const P8P_CA_CHART_ACTION_AREAS = [
{ name: "Компонент", area: "component", hasElement: false },
{ name: "Элемент графика", area: "chart_item", hasElement: false }
];
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Получение значения собственного типа действия
const getChartCustomTypeValue = ({ type, value, prms }) => {
//Если это элемента графика - возвращаем нужное значение, иначе - null
return type === P8P_CA_CHART_TYPE_VALUE.GRPH_ELEMENT ? prms.item[value] : null;
};
//Считывание обработчиков графика
const getChartHandlers = handlers => {
//Возвращаем объект хэндлеров (имеют формат "[Область].[Сегмент]", где [Сегмент] - необязательный)
return {
onComponentClick: handlers["component."]?.fn,
onChartItemClick: handlers["chart_item."]?.fn
};
};
//----------------
//Интерфейс модуля
//----------------
export { P8P_CA_CHART_VALUE_TYPES, P8P_CA_CHART_ACTION_AREAS, getChartCustomTypeValue, getChartHandlers };

View File

@ -1,73 +0,0 @@
/*
Парус 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"; //Источник данных
import { P8P_CA_SHAPE, P8P_CAS_INITIAL } from "../../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий
import { P8P_CA_CHART_VALUE_TYPES, P8P_CA_CHART_ACTION_AREAS } from "./action"; //Общие ресурсы действий графика
import { P8PActions } from "../../../../components/editors/p8p_actions"; //Действия
//-----------
//Тело модуля
//-----------
//График (редактор настроек)
const ChartEditor = ({ id, dataSource = null, valueProviders = {}, actions = P8P_CAS_INITIAL, onSettingsChange = null } = {}) => {
//Собственное состояние - текущие настройки
const [settings, setSettings] = useState(null);
//При сохранении изменений элемента
const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } }));
//При изменении действий
const handleActionsChange = actions => setSettings(pv => ({ ...pv, actions }));
//При сохранении настроек
const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor });
//При изменении компонента
useEffect(() => {
settings?.id != id && setSettings({ id, dataSource, actions });
}, [settings, id, dataSource, actions]);
//Формирование представления
return (
<P8PEditorBox title={"Параметры графика"} onSave={handleSave}>
<P8PEditorSubHeader title={"Источник данных"} />
<P8PDataSource dataSource={settings?.dataSource} valueProviders={valueProviders} onChange={handleDataSourceChange} />
<P8PEditorSubHeader title={"Действия"} />
<P8PActions
actions={settings?.actions}
valueProviders={valueProviders}
areas={P8P_CA_CHART_ACTION_AREAS}
valueTypes={P8P_CA_CHART_VALUE_TYPES}
onChange={handleActionsChange}
/>
</P8PEditorBox>
);
};
//Контроль свойств компонента - График (редактор настроек)
ChartEditor.propTypes = {
id: PropTypes.string.isRequired,
dataSource: P8P_DATA_SOURCE_SHAPE,
valueProviders: PropTypes.object,
actions: PropTypes.arrayOf(P8P_CA_SHAPE),
onSettingsChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export default ChartEditor;

View File

@ -1,101 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: График (представление)
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Paper } from "@mui/material"; //Интерфейсные элементы
import { P8PChart } from "../../../../components/p8p_chart"; //График
import { useDataSource, useComponentHandlers } 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"; //Информационное сообщение внутри компонента
import { P8P_COMPONENT_SETTINGS_RESP_ARGS } from "../../../../components/editors/p8p_component_settings"; //Дополнительный функционал источников данных
import { P8P_CA_SHAPE } from "../../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий
import { getChartCustomTypeValue, getChartHandlers } from "./action"; //Общие ресурсы действий графика
//---------
//Константы
//---------
//Иконка компонента
const COMPONENT_ICON = "bar_chart";
//Наименование компонента
const COMPONENT_NAME = "График";
//Стили
const STYLES = {
CHART: { width: "100%", height: "100%", alignItems: "center", justifyContent: "center", display: "flex" },
CONTAINER: clickable => ({
...(clickable
? {
cursor: "pointer"
}
: {})
})
};
//-----------
//Тело модуля
//-----------
//График (представление)
const Chart = ({ dataSource = null, values = {}, actions = [], onValuesChange = null } = {}) => {
//Собственное состояние - данные
const [chart, error, haveConfing, haveData] = useDataSource({ dataSource, values, componentRespArg: P8P_COMPONENT_SETTINGS_RESP_ARGS.CHART });
//Собственное состояние - обработчики компонента
const [handlers] = useComponentHandlers({ actions, onValuesChange, getCustomTypeValue: getChartCustomTypeValue });
//Обработчики областей
const { onComponentClick, onChartItemClick } = getChartHandlers(handlers);
//Формирование представления
return (
<Paper
sx={STYLES.CONTAINER(onComponentClick)}
className={"component-view__container component-view__container__empty"}
elevation={6}
onClick={event => onComponentClick && onComponentClick({ event, values })}
>
{haveConfing && haveData ? (
<P8PChart
style={STYLES.CHART}
{...chart}
options={{ responsive: true, maintainAspectRatio: false }}
onClick={prms => onChartItemClick && onChartItemClick({ values, prms })}
/>
) : (
<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,
actions: PropTypes.arrayOf(P8P_CA_SHAPE),
onValuesChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export default Chart;

View File

@ -1,57 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Описание
*/
//---------
//Константы
//---------
const COMPONENTS = [
{
name: "Форма",
path: "form",
settings: {
id: "",
title: "",
autoApply: false,
orientation: "v",
items: []
}
},
{
name: "График",
path: "chart",
settings: {
id: "",
dataSource: {},
actions: []
}
},
{
name: "Таблица",
path: "table",
settings: {
id: "",
dataSource: {},
actions: [],
conditions: []
}
},
{
name: "Индикатор",
path: "indicator",
settings: {
id: "",
dataSource: {},
actions: [],
conditions: []
}
}
];
//----------------
//Интерфейс модуля
//----------------
export { COMPONENTS };

View File

@ -1,639 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Хуки компонентов
*/
//---------------------
//Подключение библиотек
//---------------------
import { useState, useEffect, useContext, useCallback, useLayoutEffect } from "react"; //Классы React
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
import { MessagingСtx } from "../../../context/messaging"; //Контекст сообщений
import { object2Base64XML, genUID, xml2JSON } from "../../../core/utils"; //Вспомогательные функции
import { exportXMLFile } from "../layouts"; //Дополнительная разметка и вёрстка клиентских элементов
import { P8P_COMPONENT_SETTINGS_PATHS } from "../../../components/editors/p8p_component_settings"; //Дополнительные настройки источников
import { getActionsVariables } from "../../../components/editors/p8p_component_action/util"; //Вспомогательный функционал действий компонентов
import { P8P_CA_TYPE } from "../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий
//---------
//Константы
//---------
//Начальное состояние размера макета
const INITIAL_BREAKPOINT = "lg";
//Начальное состояние макета
const INITIAL_LAYOUTS = {
[INITIAL_BREAKPOINT]: []
};
//---------
//Константы расчета высоты строки ResponsiveGridLayout
//---------
//Структура параметров для расчета высоты строки ResponsiveGridLayout
const GRID_LAYOUT_PARAMS = {
//Высота главного меню (px)
APP_BAR_HEIGHT: 64,
//Дополнительный отступ (px)
SAFE_OFFSET: 20,
//Максимальное количество строк (число)
MAX_ROWS: 68,
//Высота отступа (px)
MARGIN: 10
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Проверка на массив загружаемых данных панели
const isArrayPanelDesc = (name, jPath) =>
["items", "arguments", "conditions", "actions", "dependencies", "inputParams"].includes(name) || /(.*)XLAYOUTS\.[^.]*$/.test(jPath);
//Обработка значений тэгов загружаемых данных панели
const tagValueProcessorPanelDesc = (name, val, jPath) =>
["condValue", "resValue", "description"].includes(name)
? undefined
: /(.*)dataSource.arguments.value$/.test(jPath) || /(.*)XVALUE_PROVIDERS(.*)$/.test(jPath)
? undefined
: val;
//Конвертация серверного описания компонентов в данные для редактора панелей
const convertServerData2Components = components => {
//Корректировка информации о действия (Для типа "setVariable" значение ключа "params" - обязательно массив, в иных случаях - объект)
const correctionActions = actions => {
return actions.reduce(
(prevActions, action) => [
...prevActions,
{
...(action.type === P8P_CA_TYPE.setVariable.code && !Array.isArray(action.params)
? { ...action, params: [{ ...action.params }] }
: { ...action })
}
],
[]
);
};
//Форматируем устанавливая пустой объект для dataSource, если он пуст
return Object.keys(components).reduce(
(prev, cur) => ({
...prev,
[cur]: {
...components[cur],
settings: {
...components[cur].settings,
//dataSource - обязательно объект
...(components[cur].settings?.dataSource ? { dataSource: components[cur].settings.dataSource || {} } : {}),
//actions - требуют корректировки
...(components[cur].settings?.actions
? {
actions: correctionActions(components[cur].settings.actions)
}
: {})
}
}
}),
{}
);
};
//Конвертация серверного описания проводников значений в данные для редактора панелей
const convertServerData2ValueProviders = valueProviders => {
//Форматируем инициализируя dependencies, если требуется
return Object.keys(valueProviders).reduce(
(prev, cur) => ({ ...prev, [cur]: { ...valueProviders[cur], dependencies: valueProviders[cur].dependencies || [] } }),
{}
);
};
//Конвертация серверного описания панели в данные для редактора панелей
const serverPanelData2PanelDesc = (components, valueProviders, layouts, breakpoint) => {
//Возвращаем информацию о панеле с учетом конвертаций
return {
components: convertServerData2Components(components),
valueProviders: convertServerData2ValueProviders(valueProviders),
layouts: layouts[breakpoint] ? { [breakpoint]: [...layouts[breakpoint]] } : INITIAL_LAYOUTS,
breakpoint: breakpoint
};
};
//Считывание общего списка зависимостей от проводника значений
const getValueProvidersLinks = (componentPath, settings) => {
//Если это индикатор/график/таблица
if ([P8P_COMPONENT_SETTINGS_PATHS.INDICATOR, P8P_COMPONENT_SETTINGS_PATHS.CHART, P8P_COMPONENT_SETTINGS_PATHS.TABLE].includes(componentPath)) {
//Собираем зависимости из настройки источника
let argumentsSources = Array.isArray(settings?.dataSource?.arguments)
? settings.dataSource.arguments.reduce((prev, cur) => (cur.valueSource ? [...prev, cur.valueSource] : [...prev]), [])
: [];
//Собираем зависимости из параметров действий
let actionsSources = Array.isArray(settings?.actions) ? getActionsVariables(settings.actions) : [];
//Возвращаем зависимости компонента
return [...new Set([...argumentsSources, ...actionsSources])];
}
//Если это форма
if (P8P_COMPONENT_SETTINGS_PATHS.FORM === componentPath) {
//Собираем зависимости из элементов формы
let items = Array.isArray(settings?.items) ? settings.items.map(item => item.name) : [];
//Возвращаем зависимости компонента
return [...new Set(items)];
}
};
//-----------
//Тело модуля
//-----------
//Отложенная загрузка модуля компонента (как альтернативу можно применять 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];
};
//Работа с панелью
const usePanel = () => {
//Собственное состояние - рабочая панель
const [panel, setPanel] = useState();
//Собственное состояние - имя рабочей панели
const [panelName, setPanelName] = useState(null);
//Собственное состояние - наличие изменений рабочей панели
const [isPanelChanged, setIsPanelChanged] = useState(false);
//Собственное состояние - возможность редактирования
const [isEditAvaliable, setIsEditAvaliable] = useState(true);
//Собственное состояние - режим редактирования
const [editMode, setEditMode] = useState(true);
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При необходимости загрузки информации о панели
const loadPanel = async panel => {
//Считываем информацию с сервера
const res = await executeStored({
stored: "PKG_P8PANELS_PE.PANEL_ATTRS_GET_BY_CODE",
args: {
SCODE: panel
},
respArg: "COUT",
loader: true
});
//Устанавливаем рабочую панель
setPanel(res.XPANEL_ATTRS.rn);
//Устанавливаем наименование рабочей панели
setPanelName(res.XPANEL_ATTRS.name);
//Сбрасываем наличие изменений рабочей панели
setIsPanelChanged(false);
//Загружаемую панель запрещено редактировать
setIsEditAvaliable(false);
setEditMode(false);
};
//При выборе панели
const selectPanel = (panel, panelName, isPanelEditAvaliable) => {
//Обновляем информацию о панели
setPanel(panel);
setPanelName(panelName);
setIsEditAvaliable(isPanelEditAvaliable);
setEditMode(isPanelEditAvaliable);
setIsPanelChanged(false);
};
//При закрытии панели
const closePanel = () => {
setPanel(null);
setPanelName(null);
setIsPanelChanged(false);
};
//При установке признака изменений панели
const setPanelChanged = isChanged => setIsPanelChanged(isChanged);
//При установке признака режима редактирования
const changeEditMode = isEditMode => setEditMode(isEditMode);
//Возвращаем интерфейс хука
return [panel, panelName, editMode, isEditAvaliable, isPanelChanged, loadPanel, selectPanel, closePanel, changeEditMode, setPanelChanged];
};
//Работа с менеджером панелей
const usePanelManager = () => {
//Собственное состояние - флаг необходимости обновления
const [refresh, setRefresh] = useState(true);
//Собственное состояние - данные
const [data, setData] = useState(null);
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//Добавление панели
const insertPanel = useCallback(
async (code, name) => {
await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_INSERT", args: { SCODE: code, SNAME: name }, loader: false });
setRefresh(true);
},
[executeStored]
);
//Изменение панели
const updatePanel = useCallback(
async (query, code, name) => {
await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_UPDATE", args: { NRN: query, SCODE: code, SNAME: name }, loader: false });
setRefresh(true);
},
[executeStored]
);
//Удаление панели
const deletePanel = useCallback(
async query => {
await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_DELETE", args: { NRN: query }, loader: false });
setRefresh(true);
},
[executeStored]
);
//Установка флага готовности панели
const setPanelReady = useCallback(
async (query, ready) => {
await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_READY_SET", args: { NRN: query, NREADY: ready }, loader: false });
setRefresh(true);
},
[executeStored]
);
//Установка флага публичности панели
const setPanelPbl = useCallback(
async (query, pbl) => {
await executeStored({ stored: "PKG_P8PANELS_PE.PANEL_PBL_SET", args: { NRN: query, NPBL: pbl }, loader: false });
setRefresh(true);
},
[executeStored]
);
//Импорт новой панели
const importPanel = useCallback(
async fileData => {
await executeStored({
stored: "PKG_P8PANELS_PE.PANEL_IMPORT",
args: {
CPANEL: {
//Форматируем данные документа в base64
VALUE: btoa(unescape(encodeURIComponent(fileData))),
SDATA_TYPE: SERV_DATA_TYPE_CLOB
}
},
loader: true
});
setRefresh(true);
},
[SERV_DATA_TYPE_CLOB, executeStored]
);
//При необходимости получить/обновить данные
useEffect(() => {
//Загрузка данных с сервера
const loadData = async () => {
try {
const data = await executeStored({
stored: "PKG_P8PANELS_PE.PANEL_LIST",
respArg: "COUT",
isArray: name => ["XPANEL"].includes(name),
attributeValueProcessor: (name, val) => (["code", "name"].includes(name) ? undefined : val),
loader: true
});
setData(data?.XPANELS?.XPANEL || []);
} finally {
setRefresh(false);
}
};
//Если надо обновить
if (refresh)
//Получим данные
loadData();
}, [refresh, executeStored]);
//Возвращаем интерфейс хука
return [data, insertPanel, updatePanel, deletePanel, setPanelReady, setPanelPbl, importPanel];
};
//Работа с содержимым панели
const usePanelDesc = panel => {
//Собственное состояние - флаг инициализированности
const [isInit, setInit] = useState(false);
//Собственное состояние - флаг необходимости обновления
const [refresh, setRefresh] = useState(true);
//Собственное состояние - данные
const [data, setData] = useState({
components: {},
valueProviders: {},
layouts: INITIAL_LAYOUTS,
breakpoint: INITIAL_BREAKPOINT
});
//Подключение к контексту взаимодействия с сервером
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
//Подключение к контексту сообщений
const { showMsgErr } = useContext(MessagingСtx);
//Считывание базовой информации о панели
const getPanelInfo = useCallback(async () => {
//Считываем информацию с сервера
const res = await executeStored({
stored: "PKG_P8PANELS_PE.PANEL_ATTRS_GET",
args: {
NRN: panel
},
respArg: "COUT",
loader: true
});
//Забираем все без RN
// eslint-disable-next-line no-unused-vars
const { rn, ...panelAttrs } = res.XPANEL_ATTRS;
//Возвращаем результат
return panelAttrs;
}, [executeStored, panel]);
//Формирования данных панели для выгрузки
const makeXMLPanelDesc = useCallback(
async (includePanelInfo = false, isBase64 = true) => {
//Считываем информацию о выгружаемой панели
const panelInfo = includePanelInfo ? await getPanelInfo() : {};
//Сформируем данные в формат XML
const xmlData = object2Base64XML(
{
XPANEL: {
...(includePanelInfo ? { XPANEL_INFO: panelInfo } : {}),
XCOMPONENTS: data.components,
XVALUE_PROVIDERS: data.valueProviders,
XLAYOUTS: data.layouts,
XOPTIONS: {
breakpoint: data.breakpoint
}
}
},
{ suppressEmptyNode: false }
);
//Возвращаем данные в формате XML (формат исходит от признака)
return isBase64 ? xmlData : decodeURIComponent(escape(atob(xmlData)));
},
[data.breakpoint, data.components, data.layouts, data.valueProviders, getPanelInfo]
);
//Добавление компонента в макет
const addComponent = component => {
//Генерируем ID
const id = genUID(component.path);
//Добавляем компонент и его макет
setData(pv => ({
...pv,
components: { ...pv.components, [id]: { ...component, settings: { ...component.settings, id: id } } },
layouts: { ...pv.layouts, [data.breakpoint]: [...pv.layouts[data.breakpoint], { i: id, x: 0, y: 0, w: 4, h: 10 }] }
}));
};
//Удаление компонента из макета
const deleteComponent = id => {
//Удаляем все старые зависимости от компонента
const newValueProviders = Object.keys(data.valueProviders).reduce(
(prev, cur) => ({
...prev,
[cur]: { ...data.valueProviders[cur], dependencies: data.valueProviders[cur].dependencies.filter(el => el !== id) }
}),
{}
);
//Обновляем данные
setData(pv => ({
...pv,
layouts: { ...pv.layouts, [data.breakpoint]: data.layouts[data.breakpoint].filter(item => item.i !== id) },
components: { ...pv.components, [id]: { ...pv.components[id], deleted: true } },
valueProviders: { ...newValueProviders }
}));
};
//Изменение размера холста
const breakpointChange = breakpoint => setData(pv => ({ ...pv, breakpoint: breakpoint }));
//Изменение состояния макета
const layoutsChange = layouts => setData(pv => ({ ...pv, layouts: layouts }));
//Изменение значений в компоненте
const changeValueProviders = values => {
//Считываем проводники
const newValueProviders = { ...data.valueProviders };
//Переносим новые значения в проводники
Object.keys(values).map(el => (newValueProviders[el].value = values[el]));
//Обновляем проводники
setData(pv => ({ ...pv, valueProviders: { ...newValueProviders } }));
};
//Изменение настроек компонента
const changeComponentSettings = (id = null, settings = {}, onChanged = null) => {
//Считываем новые зависимости компонента
const providedValues = getValueProvidersLinks(data.components[id].path, settings);
//Удаляем все старые зависимости от компонента
const newValueProviders = Object.keys(data.valueProviders).reduce(
(prev, cur) => ({
...prev,
[cur]: { ...data.valueProviders[cur], dependencies: data.valueProviders[cur].dependencies.filter(el => el !== id) }
}),
{}
);
//Добавляем новые зависимости
providedValues.map(providerName => newValueProviders[providerName].dependencies.push(id));
//Обновляем данные
setData(pv => ({
...pv,
components: { ...pv.components, [id]: { ...pv.components[id], settings: { ...settings } } },
valueProviders: { ...newValueProviders }
}));
//Выполняем действия после изменения
onChanged && onChanged();
};
//Изменение настроек панели
const changePanelSettings = valueProviders => {
//Обновляем данные
setData(pv => ({
...pv,
valueProviders: { ...valueProviders }
}));
};
//Сохранение описания панели
const savePanelDesc = useCallback(
async (callBack = null) => {
try {
await executeStored({
stored: "PKG_P8PANELS_PE.PANEL_DESC_SET",
args: {
NRN: panel,
CPANEL: {
VALUE: await makeXMLPanelDesc(),
SDATA_TYPE: SERV_DATA_TYPE_CLOB
}
},
loader: true
});
callBack && callBack(false);
} catch (e) {
callBack && callBack(true);
}
},
[SERV_DATA_TYPE_CLOB, executeStored, makeXMLPanelDesc, panel]
);
//Загрузка описания панели из файла
const importPanelDesc = async (fileData, callBack = null) => {
//Формируем данные панели
const panelData = await xml2JSON({
xmlDoc: fileData,
isArray: isArrayPanelDesc,
tagValueProcessor: tagValueProcessorPanelDesc
});
//Если файл содержит тэг XPANEL
if (panelData.XPANEL) {
setData(
serverPanelData2PanelDesc(
panelData.XPANEL?.XCOMPONENTS || {},
panelData.XPANEL?.XVALUE_PROVIDERS || {},
panelData.XPANEL?.XLAYOUTS || {},
panelData.XPANEL?.XOPTIONS?.breakpoint || INITIAL_BREAKPOINT
)
);
callBack && callBack(true);
} else {
showMsgErr("Загружаемые данные не соответствуют формату настройки панели.");
callBack && callBack(false);
}
};
//Выгрузка панели в файл
const exportPanelDesc = async panelName => {
//Формируем XML-представление панели
const xmlPanelDesc = await makeXMLPanelDesc(true, false);
//Выгружаем в файл
exportXMLFile(xmlPanelDesc, panelName);
};
//При необходимости получить/обновить данные
useEffect(() => {
//Загрузка данных с сервера
const loadData = async () => {
try {
const data = await executeStored({
stored: "PKG_P8PANELS_PE.PANEL_DESC_GET",
args: { NRN: panel },
respArg: "COUT",
isArray: isArrayPanelDesc,
tagValueProcessor: tagValueProcessorPanelDesc,
loader: true
});
setData(
serverPanelData2PanelDesc(
data?.XCOMPONENTS || {},
data?.XVALUE_PROVIDERS || {},
data?.XLAYOUTS || {},
data?.XOPTIONS?.breakpoint || INITIAL_BREAKPOINT
)
);
setInit(true);
} finally {
setRefresh(false);
}
};
//Если надо обновить
if (refresh)
if (panel)
//Если есть для чего получать данные
loadData();
//Нет идентификатора запроса - нет данных
else
setData({
components: {},
valueProviders: {},
layouts: INITIAL_LAYOUTS,
breakpoint: INITIAL_BREAKPOINT
});
}, [refresh, panel, executeStored]);
//При изменении входных свойств - поднимаем флаг обновления
useEffect(() => {
setInit(false);
setRefresh(true);
}, [panel]);
//Возвращаем интерфейс хука
return [
data,
isInit,
addComponent,
deleteComponent,
breakpointChange,
layoutsChange,
changeValueProviders,
changeComponentSettings,
changePanelSettings,
savePanelDesc,
importPanelDesc,
exportPanelDesc
];
};
//Работа с соотношением размеров
const useWindowResize = () => {
//Состояние высоты строки ResponsiveGridLayout
const [rowHeight, setRowHeight] = useState(1);
//При изменении размера
useLayoutEffect(() => {
//Расчет высоты строки
const updateRowHeight = () => {
//Определяем доступную область
const availableHeight = window.innerHeight - GRID_LAYOUT_PARAMS.APP_BAR_HEIGHT - GRID_LAYOUT_PARAMS.SAFE_OFFSET;
//Промежутки между рядами
const totalMargins = (GRID_LAYOUT_PARAMS.MAX_ROWS - 1) * GRID_LAYOUT_PARAMS.MARGIN;
//Рассчитываем высоту строки
const result = (availableHeight - totalMargins) / GRID_LAYOUT_PARAMS.MAX_ROWS;
//Устанавливаем
setRowHeight(result);
};
//Запускаем при открытии панели
updateRowHeight();
//Устанавливаем обработчик на событие
window.addEventListener("resize", updateRowHeight);
//Удаляем обработчик на событие
return () => window.removeEventListener("resize", updateRowHeight);
}, []);
//Возвращаем интерфейс хука
return [rowHeight];
};
//----------------
//Интерфейс модуля
//----------------
export { useComponentModule, usePanel, usePanelManager, usePanelDesc, useWindowResize };

View File

@ -1,52 +0,0 @@
/*
Парус 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"
};
//Доступность сохранения настроек элемента
export const isItemOkDisabled = item => (!item.name ? true : false);

View File

@ -1,380 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Форма (редактор настроек)
*/
//TODO: Контроль уникальности имени элемента формы
//---------------------
//Подключение библиотек
//---------------------
import React, { useEffect, useState, useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
TextField,
Button,
Icon,
Select,
Menu,
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 { TITLES } from "../../../../../app.text"; //Общие текстовые ресурсы
import { ITEM_SHAPE, ITEM_INITIAL, ITEMS_INITIAL, ORIENTATION, isItemOkDisabled } from "./common"; //Общие ресурсы и константы формы
//---------
//Константы
//---------
//Стили
const STYLES = {
CHIP_ITEM: { ...COMMON_STYLES.CHIP(true, false) }
};
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Редактор элемента
const ItemEditor = ({ item = null, valueProviders = {}, onOk = null, onCancel = null } = {}) => {
//Собственное состояние - параметры элемента формы
const [state, setState] = useState({ ...ITEM_INITIAL, ...item });
//Собственное состояние - элемент привязки меню выбора источника
const [valueProvidersMenuAnchorEl, setValueProvidersMenuAnchorEl] = useState(null);
//Подключение к контексту приложения
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
}))
});
};
//Открытие/сокрытие меню выбора источника
const toggleValueProvidersMenu = target => setValueProvidersMenuAnchorEl(target instanceof Element ? target : null);
//При очистке значения/связывания параметра
const handleParamClearClick = () => setState(pv => ({ ...pv, name: "" }));
//При отображении меню связывания параметра с поставщиком данных
const handleParamLinkMenuClick = e => setValueProvidersMenuAnchorEl(e.currentTarget);
//При выборе элемента меню связывания аргумента с поставщиком данных
const handleParamLinkClick = valueSource => {
//Устанавливаем выбранный параметр
setState(pv => ({ ...pv, name: valueSource }));
//Закрываем меню
toggleValueProvidersMenu();
};
//Список значений
const values = Object.keys(valueProviders);
//Наличие значений
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={() => handleParamLinkClick(value)}>
{value}
</MenuItem>
))}
</Menu>
);
//Формирование представления
return (
<P8PConfigDialog
title={`${item ? TITLES.UPDATE : TITLES.INSERT} элемента`}
onOk={handleOk}
onCancel={handleCancel}
okDisabled={isItemOkDisabled(state)}
>
<Stack direction={"column"} spacing={1}>
{valueProvidersMenu}
<TextField
type={"text"}
variant={"standard"}
value={state.name}
label={"Имя"}
id={"name"}
required={true}
onChange={handleChange}
InputProps={{
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={handleParamClearClick}>
<Icon>clear</Icon>
</IconButton>
{isValues && (
<IconButton onClick={handleParamLinkMenuClick}>
<Icon>settings_ethernet</Icon>
</IconButton>
)}
</InputAdornment>
)
}}
/>
<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,
valueProviders: PropTypes.object,
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//-----------
//Тело модуля
//-----------
//Форма (редактор настроек)
const FormEditor = ({
id,
title = "",
orientation = ORIENTATION.V,
autoApply = false,
items = ITEMS_INITIAL,
valueProviders = {},
onSettingsChange = null
} = {}) => {
//Собственное состояние - текущие настройки
const [settings, setSettings] = useState(null);
//Собственное состояние - редактор элементов формы
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, closeEditor });
//При изменении компонента
useEffect(() => {
settings?.id != id && setSettings({ id, title, orientation, autoApply, items });
}, [settings, id, title, orientation, autoApply, items]);
//Формирование представления
return (
settings && (
<P8PEditorBox title={"Параметры формы"} onSave={handleSave}>
{itemEditor.display && (
<ItemEditor
item={itemEditor.index !== null ? { ...settings.items[itemEditor.index] } : null}
valueProviders={valueProviders}
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),
valueProviders: PropTypes.object,
onSettingsChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export default FormEditor;

View File

@ -1,174 +0,0 @@
/*
Парус 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"; //Общие ресурсы и константы формы
import { APP_STYLES } from "../../../../../app.styles";
//---------
//Константы
//---------
//Стили
const STYLES = {
CONTAINER: { overflow: "auto", ...APP_STYLES.SCROLL }
};
//Иконка компонента
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"}`} sx={STYLES.CONTAINER} 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

@ -1,39 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Индикатор (общие ресурсы действия)
*/
//---------------------
//Подключение библиотек
//---------------------
//---------
//Константы
//---------
//Доступные области действий индикатора
const P8P_CA_INDICATOR_ACTION_AREAS = [
{ name: "Компонент", area: "component", hasElement: false },
{ name: "Заголовок", area: "inner_caption", hasElement: false },
{ name: "Значение", area: "inner_value", hasElement: false }
];
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Считывание обработчиков индикатора
const getIndicatorHandlers = handlers => {
//Возвращаем объект хэндлеров (имеют формат "[Область].[Сегмент]", где [Сегмент] - необязательный)
return {
onComponentClick: handlers["component."]?.fn,
onCaptionClick: handlers["inner_caption."]?.fn,
onValueClick: handlers["inner_value."]?.fn
};
};
//----------------
//Интерфейс модуля
//----------------
export { P8P_CA_INDICATOR_ACTION_AREAS, getIndicatorHandlers };

View File

@ -1,33 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Индикатор (общие ресурсы условия)
*/
//---------------------
//Подключение библиотек
//---------------------
//---------
//Константы
//---------
//Доступные поля условий индикатора
const P8P_CC_INDICATOR_COND_FIELDS = [
{ name: "Состояние", value: "state", hasElement: false },
{ name: "Значение", value: "value", hasElement: false },
{ name: "Подсказка", value: "hint", hasElement: false }
];
//Доступные поля результата условия индикатора
const P8P_CC_INDICATOR_RES_FIELDS = [
{ name: "Подпись", value: "caption", hasElement: false, icon: "rtt" },
{ name: "Значение", value: "value", hasElement: false, icon: "pin" },
{ name: "Цвет заливки", value: "backgroundColor", hasElement: false, icon: "color_lens" },
{ name: "Цвет шрифта", value: "color", hasElement: false, icon: "format_color_text" }
];
//----------------
//Интерфейс модуля
//----------------
export { P8P_CC_INDICATOR_COND_FIELDS, P8P_CC_INDICATOR_RES_FIELDS };

View File

@ -1,97 +0,0 @@
/*
Парус 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"; //Источник данных
import { P8PActions } from "../../../../components/editors/p8p_actions"; //Действия
import { P8PConditions } from "../../../../components/editors/p8p_conditions"; //Условия
import { P8P_CA_SHAPE, P8P_CAS_INITIAL } from "../../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий
import { P8P_CCS_INITIAL, P8P_CC_SHAPE } from "../../../../components/editors/p8p_component_condition/common"; //Общие ресурсы компонента "Редактор условия"
import { P8P_CA_INDICATOR_ACTION_AREAS } from "./action"; //Общие ресурсы действий индикатора
import { P8P_CC_INDICATOR_COND_FIELDS, P8P_CC_INDICATOR_RES_FIELDS } from "./conditions"; //Общие ресурсы условий индикатора
//---------
//Константы
//---------
//-----------
//Тело модуля
//-----------
//Индикатор (редактор настроек)
const IndicatorEditor = ({
id,
dataSource = null,
valueProviders = {},
conditions = P8P_CCS_INITIAL,
actions = P8P_CAS_INITIAL,
onSettingsChange = null
} = {}) => {
//Собственное состояние - текущие настройки
const [settings, setSettings] = useState(null);
//При сохранении изменений элемента
const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } }));
//При изменении действий
const handleActionsChange = actions => setSettings(pv => ({ ...pv, actions }));
//При изменении условий
const handleConditionsChange = conditions => setSettings(pv => ({ ...pv, conditions }));
//При сохранении настроек
const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor });
//При изменении компонента
useEffect(() => {
settings?.id != id && setSettings({ id, dataSource, conditions, actions });
}, [settings, id, dataSource, conditions, actions]);
//Формирование представления
return (
<P8PEditorBox title={"Параметры индикатора"} onSave={handleSave}>
<P8PEditorSubHeader title={"Источник данных"} />
<P8PDataSource dataSource={settings?.dataSource} valueProviders={valueProviders} onChange={handleDataSourceChange} />
<P8PEditorSubHeader title={"Условия"} />
<P8PConditions
conditions={settings?.conditions}
condFields={P8P_CC_INDICATOR_COND_FIELDS}
resFields={P8P_CC_INDICATOR_RES_FIELDS}
onChange={handleConditionsChange}
/>
<P8PEditorSubHeader title={"Действия"} />
<P8PActions
actions={settings?.actions}
valueProviders={valueProviders}
areas={P8P_CA_INDICATOR_ACTION_AREAS}
onChange={handleActionsChange}
/>
</P8PEditorBox>
);
};
//Контроль свойств компонента - Индикатор (редактор настроек)
IndicatorEditor.propTypes = {
id: PropTypes.string.isRequired,
dataSource: P8P_DATA_SOURCE_SHAPE,
valueProviders: PropTypes.object,
conditions: PropTypes.arrayOf(P8P_CC_SHAPE),
actions: PropTypes.arrayOf(P8P_CA_SHAPE),
onSettingsChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export default IndicatorEditor;

View File

@ -1,111 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Индикатор (представление)
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Paper } from "@mui/material"; //Интерфейсные элементы
import { P8PIndicator } from "../../../../components/p8p_indicator"; //Компонент индикатора
import { useDataSource, useConditions, useComponentHandlers } 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"; //Информационное сообщение внутри компонента
import { P8P_CC_SHAPE } from "../../../../components/editors/p8p_component_condition/common"; //Общие ресурсы условий
import { P8P_CA_SHAPE } from "../../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий
import { P8P_COMPONENT_SETTINGS_RESP_ARGS } from "../../../../components/editors/p8p_component_settings"; //Дополнительный функционал источников данных
import { getIndicatorHandlers } from "./action"; //Общие ресурсы действий индикатора
//---------
//Константы
//---------
//Иконка компонента
const COMPONENT_ICON = "speed";
//Наименование компонента
const COMPONENT_NAME = "Индикатор";
//Стили
const STYLES = {
CONTAINER: clickable => ({
height: "100%",
...(clickable
? {
cursor: "pointer"
}
: {})
})
};
//-----------
//Тело модуля
//-----------
//Индикатор (представление)
const Indicator = ({ dataSource = null, values = {}, conditions = [], actions = [], onValuesChange = null }) => {
//Собственное состояние - данные
const [indicator, error, haveConfing, haveData] = useDataSource({
dataSource,
values,
componentRespArg: P8P_COMPONENT_SETTINGS_RESP_ARGS.INDICATOR
});
//Собственное состояние - данные с учетом условий
const [indicatorOrigin] = useConditions({ componentData: indicator, conditions });
//Собственное состояние - обработчики компонента
const [handlers] = useComponentHandlers({ actions, onValuesChange });
//Обработчики областей
const { onComponentClick, onCaptionClick, onValueClick } = getIndicatorHandlers(handlers);
//Формирование представления
return (
<Paper
{...(haveConfing && haveData
? { sx: { ...STYLES.CONTAINER(onComponentClick) } }
: { className: "component-view__container component-view__container__empty" })}
elevation={6}
onClick={event => onComponentClick && onComponentClick({ event, values })}
>
{haveConfing && haveData ? (
<P8PIndicator
{...indicatorOrigin}
onValueClick={onValueClick ? event => onValueClick({ event, values }) : null}
onCaptionClick={onCaptionClick ? event => onCaptionClick({ event, values }) : null}
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,
conditions: PropTypes.arrayOf(P8P_CC_SHAPE),
actions: PropTypes.arrayOf(P8P_CA_SHAPE),
onValuesChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export default Indicator;

View File

@ -1,54 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компонент: Диалог добавления/исправления панели
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог
import { TITLES } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
//-----------
//Тело модуля
//-----------
//Диалог добавления/исправления панели
const PanelIUDialog = ({ code = "", name = "", insert = true, onOk, onCancel }) => {
//Нажатие на кнопку "Ok"
const handleOk = values => onOk && onOk({ ...values });
//Нажатие на кнопку "Отмена"
const handleCancel = () => onCancel && onCancel();
//Генерация содержимого
return (
<P8PDialog
title={`${insert === true ? TITLES.INSERT : TITLES.UPDATE} панели`}
inputs={[
{ name: "code", value: code, label: "Мнемокод" },
{ name: "name", value: name, label: "Наименование" }
]}
onOk={handleOk}
onCancel={handleCancel}
/>
);
};
//Контроль свойств - Диалог добавления/исправления панели
PanelIUDialog.propTypes = {
code: PropTypes.string,
name: PropTypes.string,
insert: PropTypes.bool,
onOk: PropTypes.func,
onCancel: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { PanelIUDialog };

View File

@ -1,158 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компонент: Список панелей
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Stack, List, ListItem, IconButton, Icon, ListItemButton, ListItemText, Typography } from "@mui/material"; //Интерфейсные компоненты MUI
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
import { APP_STYLES } from "../../../../../app.styles"; //Типовые стили
//---------
//Константы
//---------
//Стили
const STYLES = {
LIST_PANELS: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL },
SMALL_TOOL_ICON: {
fontSize: 20
}
};
//---------
//Константы
//---------
//Структура данных о сущности панели
const PANELS_LIST_ITEM_SHAPE = PropTypes.shape({
rn: PropTypes.number.isRequired,
code: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
author: PropTypes.string.isRequired,
chDate: PropTypes.string.isRequired,
modify: PropTypes.oneOf([0, 1]).isRequired,
pbl: PropTypes.oneOf([0, 1]).isRequired,
ready: PropTypes.oneOf([0, 1]).isRequired
});
//-----------
//Тело модуля
//-----------
//Диалог открытия панели
const PanelsList = ({
panels = [],
isEditable = true,
current = null,
onSelect = null,
onPbl = null,
onReady = null,
onEdit = null,
onDelete = null
} = {}) => {
//При выборе элемента списка
const handleSelectClick = query => {
onSelect && onSelect(query);
};
//При нажатии на общедоступность
const handlePblClick = (e, query) => {
e.stopPropagation();
onPbl && onPbl(query);
};
//При нажатии на готовность
const handleReadyClick = (e, query) => {
e.stopPropagation();
onReady && onReady(query);
};
//При нажатии на исправлении
const handleEditClick = (e, query) => {
e.stopPropagation();
onEdit && onEdit(query);
};
//При нажатии на удаление
const handleDeleteClick = (e, query) => {
e.stopPropagation();
onDelete && onDelete(query);
};
//Формирование представления
return (
<List sx={STYLES.LIST_PANELS}>
{panels.map((panel, i) => {
const selected = panel.rn === current || panel.code === current;
const disabled = !panel.modify;
const pblTitle = `${panel.pbl === 1 ? "Общедоступный" : "Приватный"}${!disabled ? " - нажмите, чтобы изменить" : ""}`;
const pblIcon = panel.pbl === 1 ? "groups" : "lock_person";
const readyTitle = `${panel.ready === 1 ? "Готов" : "Не готов"}${!disabled ? " - нажмите, чтобы изменить" : ""}`;
const readyIcon = panel.ready === 1 ? "touch_app" : "do_not_touch";
return (
<ListItem key={i}>
<ListItemButton onClick={() => handleSelectClick(panel)} selected={selected}>
<ListItemText
primary={panel.name}
secondaryTypographyProps={{ component: "div" }}
secondary={
<Stack direction={"column"}>
<Typography variant={"caption"}>{`${panel.code}, ${panel.author}, ${panel.chDate}`}</Typography>
{isEditable ? (
<Stack direction={"row"}>
<div title={pblTitle}>
<IconButton disabled={disabled} onClick={e => handlePblClick(e, panel)}>
<Icon sx={STYLES.SMALL_TOOL_ICON}>{pblIcon}</Icon>
</IconButton>
</div>
<div title={readyTitle}>
<IconButton disabled={disabled} onClick={e => handleReadyClick(e, panel)}>
<Icon sx={STYLES.SMALL_TOOL_ICON}>{readyIcon}</Icon>
</IconButton>
</div>
</Stack>
) : null}
</Stack>
}
/>
{isEditable ? (
<Stack direction={"row"}>
<IconButton onClick={e => handleEditClick(e, panel)} disabled={disabled} title={BUTTONS.UPDATE}>
<Icon>edit</Icon>
</IconButton>
<IconButton onClick={e => handleDeleteClick(e, panel)} disabled={disabled} title={BUTTONS.DELETE}>
<Icon>delete</Icon>
</IconButton>
</Stack>
) : null}
</ListItemButton>
</ListItem>
);
})}
</List>
);
};
//Контроль свойств компонента - Список панелей
PanelsList.propTypes = {
panels: PropTypes.arrayOf(PANELS_LIST_ITEM_SHAPE),
current: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
isEditable: PropTypes.bool,
onSelect: PropTypes.func,
onPbl: PropTypes.func,
onReady: PropTypes.func,
onEdit: PropTypes.func,
onDelete: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { PanelsList };

View File

@ -1,118 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компонент: Менеджер панелей
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Box, Button, Icon } from "@mui/material"; //Интерфейсные компоненты MUI
import { MessagingСtx } from "../../../../context/messaging"; //Контекст сообщений
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
import { P8PConfigDialog } from "../../../../components/editors/p8p_config_dialog"; //Типовой диалог настройки
import { usePanelManager } from "../components_hooks"; //Пользовательские хуки
import { PanelsList } from "./panels_list"; //Список панелей
import { PanelIUDialog } from "./panel_iu_dialog"; //Диалог добавления/исправления панели
import { ImportPanelButton } from "../../layouts"; //Дополнительная разметка и вёрстка клиентских элементов
//-----------
//Тело модуля
//-----------
//Менеджер панелей
const PanelsManager = ({ current = null, isEditable = true, onPanelSelect = null, onCancel = null } = {}) => {
//Собственное состояние - изменяемая панель
const [modPanel, setModPanel] = useState(null);
//Работа со списком панелей
const [panels, insertPanel, updatePanel, deletePanel, setPanelReady, setPanelPbl, importPanel] = usePanelManager();
//Подключение к контексту сообщений
const { showMsgWarn } = useContext(MessagingСtx);
//При добавлении панели
const handlePanelAdd = () => setModPanel(true);
//При загрузке панели из файла
const handlePanelImport = fileData => {
importPanel(fileData);
};
//При выборе панели
const handlePanelSelect = panel => onPanelSelect && onPanelSelect(panel);
//При установке признака публичности
const handlePanelPblSet = panel => setPanelPbl(panel.rn, panel.pbl === 1 ? 0 : 1);
//При установке признака готовности
const handlePanelReadySet = panel => setPanelReady(panel.rn, panel.ready === 1 ? 0 : 1);
//При исправлении панели
const handlePanelEdit = panel => setModPanel({ ...panel });
//При удалении панели
const handlePanelDelete = panel => showMsgWarn("Удалить панель?", () => deletePanel(panel.rn));
//При закрытии диалога добавления/исправления по "ОК"
const handleIUDialogOk = async values => {
if (modPanel === true) await insertPanel(values.code, values.name);
else await updatePanel(modPanel.rn, values.code, values.name);
setModPanel(null);
};
//При закрытии диалога добавления/исправления по "Отмена"
const handleIUDialogCancel = () => setModPanel(null);
//При закрытии менеджера отменой
const handleCancel = () => onCancel && onCancel();
//Формирование представления
return (
<P8PConfigDialog title={"Панели"} onCancel={handleCancel}>
{modPanel && (
<PanelIUDialog
code={modPanel?.code}
name={modPanel?.name}
insert={modPanel === true}
onOk={handleIUDialogOk}
onCancel={handleIUDialogCancel}
/>
)}
{isEditable ? (
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Button startIcon={<Icon>add</Icon>} onClick={handlePanelAdd}>
{BUTTONS.INSERT}
</Button>
<ImportPanelButton id={"input-file-loader-manager"} startIcon={"file_upload"} title={"Загрузить"} onClick={handlePanelImport} />
</Box>
) : null}
<PanelsList
panels={panels || []}
current={current}
isEditable={isEditable}
onSelect={handlePanelSelect}
onPbl={handlePanelPblSet}
onReady={handlePanelReadySet}
onEdit={handlePanelEdit}
onDelete={handlePanelDelete}
/>
</P8PConfigDialog>
);
};
//Контроль свойств компонента - Менеджер панелей
PanelsManager.propTypes = {
current: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
isEditable: PropTypes.bool,
onPanelSelect: PropTypes.func,
onCancel: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export { PanelsManager };

View File

@ -1,58 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Таблица (общие ресурсы действия)
*/
//---------------------
//Подключение библиотек
//---------------------
//---------
//Константы
//---------
//Собственные значения типа действия
const P8P_CA_TABLE_TYPE_VALUE = {
COLUMN_VALUE: "Значение графы"
};
//Типы значений действий таблицы
const P8P_CA_TABLE_VALUE_TYPES = [...Object.values(P8P_CA_TABLE_TYPE_VALUE)];
//Доступные области действий таблицы
const P8P_CA_TABLE_ACTION_AREAS = [
{ name: "Компонент", area: "component", hasElement: false },
{ name: "Графа таблицы", area: "column", hasElement: true },
{ name: "Строка таблицы", area: "row", hasElement: false }
];
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Получение значения собственного типа действия
const getDataGridCustomTypeValue = ({ type, value, prms }) => {
//Если это значение графы - возвращаем нужное значение, иначе - null
return type === P8P_CA_TABLE_TYPE_VALUE.COLUMN_VALUE ? prms.row[value] : null;
};
//Считывание обработчиков таблицы
const getDataGridHandlers = (handlers, element = null) => {
//Возвращаем объект хэндлеров (имеют формат "[Область].[Сегмент]", где [Сегмент] - необязательный)
return {
onComponentClick: handlers["component."]?.fn,
onRowClick: handlers["row."]?.fn,
onColumnClick: element ? handlers[`column.${element}`]?.fn : null
};
};
//Проверка наличия обработчиков ячеек
const isHasCellRender = (handlers, conditions) => {
return Object.keys(handlers).some(handler => handler.startsWith("column.") || handler === "row.") || conditions.length !== 0;
};
//----------------
//Интерфейс модуля
//----------------
export { P8P_CA_TABLE_VALUE_TYPES, P8P_CA_TABLE_ACTION_AREAS, getDataGridCustomTypeValue, getDataGridHandlers, isHasCellRender };

View File

@ -1,38 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Индикатор (общие ресурсы условия)
*/
//---------------------
//Подключение библиотек
//---------------------
import { getConditionsValues } from "../../../../components/editors/p8p_component_condition/util"; //Вспомогательные ресурсы условий
//---------
//Константы
//---------
//Доступные поля условий таблицы
const P8P_CC_TABLE_COND_FIELDS = [{ name: "Графа", value: "column", hasElement: true }];
//Доступные поля результата условия таблицы
const P8P_CC_TABLE_RES_FIELDS = [
{ name: "Цвет заливки", value: "backgroundColor", hasElement: true, icon: "color_lens" },
{ name: "Цвет шрифта", value: "color", hasElement: true, icon: "format_color_text" }
];
//------------------------------------
//Вспомогательные функции и компоненты
//------------------------------------
//Считывание стилей колонки по условиям
const getColumnStylesByConditions = (conditions, row, columnName) => {
//Считываем стили строки
return getConditionsValues(row, conditions, columnName);
};
//----------------
//Интерфейс модуля
//----------------
export { P8P_CC_TABLE_COND_FIELDS, P8P_CC_TABLE_RES_FIELDS, getColumnStylesByConditions };

View File

@ -1,94 +0,0 @@
/*
Парус 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"; //Источник данных
import { P8PActions } from "../../../../components/editors/p8p_actions"; //Действия
import { P8PConditions } from "../../../../components/editors/p8p_conditions"; //Условия
import { P8P_CA_TABLE_VALUE_TYPES, P8P_CA_TABLE_ACTION_AREAS } from "./action"; //Общие ресурсы действий таблицы
import { P8P_CC_TABLE_COND_FIELDS, P8P_CC_TABLE_RES_FIELDS } from "./conditions"; //Общие ресурсы условий таблицы
import { P8P_CA_SHAPE, P8P_CAS_INITIAL } from "../../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий
import { P8P_CCS_INITIAL, P8P_CC_SHAPE } from "../../../../components/editors/p8p_component_condition/common"; //Общие ресурсы компонента "Редактор условия"
//-----------
//Тело модуля
//-----------
//Таблица (редактор настроек)
const TableEditor = ({
id,
dataSource = null,
valueProviders = {},
conditions = P8P_CCS_INITIAL,
actions = P8P_CAS_INITIAL,
onSettingsChange = null
} = {}) => {
//Собственное состояние - текущие настройки
const [settings, setSettings] = useState(null);
//При сохранении изменений элемента
const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } }));
//При изменении действий
const handleActionsChange = actions => setSettings(pv => ({ ...pv, actions }));
//При изменении условий
const handleConditionsChange = conditions => setSettings(pv => ({ ...pv, conditions }));
//При сохранении настроек
const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor });
//При изменении компонента
useEffect(() => {
settings?.id != id && setSettings({ id, dataSource, conditions, actions });
}, [settings, id, dataSource, conditions, actions]);
//Формирование представления
return (
<P8PEditorBox title={"Параметры таблицы"} onSave={handleSave}>
<P8PEditorSubHeader title={"Источник данных"} />
<P8PDataSource dataSource={settings?.dataSource} valueProviders={valueProviders} onChange={handleDataSourceChange} />
<P8PEditorSubHeader title={"Условия"} />
<P8PConditions
conditions={settings?.conditions}
condFields={P8P_CC_TABLE_COND_FIELDS}
resFields={P8P_CC_TABLE_RES_FIELDS}
onChange={handleConditionsChange}
/>
<P8PEditorSubHeader title={"Действия"} />
<P8PActions
actions={settings?.actions}
valueProviders={valueProviders}
areas={P8P_CA_TABLE_ACTION_AREAS}
valueTypes={P8P_CA_TABLE_VALUE_TYPES}
onChange={handleActionsChange}
/>
</P8PEditorBox>
);
};
//Контроль свойств компонента - Таблица (редактор настроек)
TableEditor.propTypes = {
id: PropTypes.string.isRequired,
dataSource: P8P_DATA_SOURCE_SHAPE,
valueProviders: PropTypes.object,
conditions: PropTypes.arrayOf(P8P_CC_SHAPE),
actions: PropTypes.arrayOf(P8P_CA_SHAPE),
onSettingsChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export default TableEditor;

View File

@ -1,161 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Компоненты: Таблица (представление)
*/
//---------------------
//Подключение библиотек
//---------------------
import React from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { Paper, Link } 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, useComponentHandlers } 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"; //Информационное сообщение внутри компонента
import { P8P_COMPONENT_SETTINGS_RESP_ARGS } from "../../../../components/editors/p8p_component_settings"; //Дополнительный функционал источников данных
import { P8P_CC_SHAPE, P8P_CCS_INITIAL } from "../../../../components/editors/p8p_component_condition/common"; //Общие ресурсы условий
import { P8P_CA_SHAPE, P8P_CAS_INITIAL } from "../../../../components/editors/p8p_component_action/common"; //Общие ресурсы действий
import { getDataGridCustomTypeValue, getDataGridHandlers, isHasCellRender } from "./action"; //Общие ресурсы действий таблицы
import { getColumnStylesByConditions } from "./conditions";
//---------
//Константы
//---------
//Иконка компонента
const COMPONENT_ICON = "table_view";
//Наименование компонента
const COMPONENT_NAME = "Таблица";
//Стили
const STYLES = {
CONTAINER: clickable => ({
display: "flex",
height: "100%",
overflow: "hidden",
...(clickable
? {
cursor: "pointer"
}
: {})
}),
DATA_GRID: { width: "100%" },
DATA_GRID_CONTAINER: {
height: `calc(100%)`,
...APP_STYLES.SCROLL
},
DATA_GRID_ROW_CLICABLE: { cursor: "pointer" }
};
//---------------------------------------------
//Вспомогательные функции и компоненты
//---------------------------------------------
//Форматирование значения ячейки
const dataCellRender = ({ row, columnDef, values, handlers, conditions }) => {
//Инициализируем обработчики строки
const { onRowClick, onColumnClick } = getDataGridHandlers(handlers, columnDef.name);
//Инициализируем стили по условию
const condStyles = getColumnStylesByConditions(conditions, row, columnDef.name);
//Накладываем нужные обработчики
return {
cellStyle: { ...(onRowClick ? { ...STYLES.DATA_GRID_ROW_CLICABLE } : {}), ...condStyles },
cellProps: {
...(onRowClick
? {
onClick: event => {
onRowClick({ event, values, prms: { row, columnDef } });
}
}
: {})
},
data: onColumnClick ? (
<Link component="button" variant="body2" underline="hover" onClick={event => onColumnClick({ event, values, prms: { row, columnDef } })}>
{row[columnDef.name]}
</Link>
) : (
row[columnDef.name]
)
};
};
//-----------
//Тело модуля
//-----------
//Таблица (представление)
const Table = ({ dataSource = null, values = {}, conditions = P8P_CCS_INITIAL, actions = P8P_CAS_INITIAL, onValuesChange = null } = {}) => {
//Собственное состояние - данные
const [dataGrid, error, haveConfing, haveData] = useDataSource({ dataSource, values, componentRespArg: P8P_COMPONENT_SETTINGS_RESP_ARGS.TABLE });
//Собственное состояние - обработчики компонента
const [handlers] = useComponentHandlers({ actions, onValuesChange, getCustomTypeValue: getDataGridCustomTypeValue });
//Признак необходимости рендера ячеек
const hasCellRender = isHasCellRender(handlers, conditions);
//Обработчики областей
const { onComponentClick } = getDataGridHandlers(handlers);
//Формирование представления
return (
<Paper
{...(haveConfing && haveData
? { sx: { ...STYLES.CONTAINER(onComponentClick) } }
: { className: "component-view__container component-view__container__empty" })}
elevation={6}
onClick={event => onComponentClick && onComponentClick({ event, values })}
>
{haveConfing && haveData ? (
<P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS}
{...dataGrid}
style={STYLES.DATA_GRID}
containerComponentProps={{ sx: STYLES.DATA_GRID_CONTAINER, elevation: 0 }}
dataCellRender={
hasCellRender
? prms =>
dataCellRender({
...prms,
values,
handlers,
conditions
})
: null
}
/>
) : (
<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,
conditions: PropTypes.arrayOf(P8P_CC_SHAPE),
actions: PropTypes.arrayOf(P8P_CA_SHAPE),
onValuesChange: PropTypes.func
};
//----------------
//Интерфейс модуля
//----------------
export default Table;

View File

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

View File

@ -1,87 +0,0 @@
/*
Парус 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,
isDragging: PropTypes.bool
};
//----------------
//Интерфейс модуля
//----------------
export { LayoutItem };

View File

@ -1,134 +0,0 @@
/*
Парус 8 - Панели мониторинга - Редактор панелей
Дополнительная разметка и вёрстка клиентских элементов
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import { InputLabel, Input, IconButton, Icon, Button } from "@mui/material"; //Интерфейсные компоненты
import { TEXTS } from "../../../app.text"; //Общие текстовые ресурсы приложения
import { P8PAppProgress } from "../../components/p8p_app_progress"; //Индикатор процесса
//---------
//Константы
//---------
//Стили
const STYLES = {
INPUT_FILE: { display: "none" },
INPUT_LABEL: { display: "contents" }
};
//-----------
//Тело модуля
//-----------
//Компонент "Загрузка из файла"
const ImportPanel = ({ id, buttonView, onClick, disabled = false }) => {
//Состояние загрузки файла
const [loading, setLoading] = useState(false);
//При выборе файла
const handleChange = event => {
//Если файл выбран
if (event.target.files[0]) {
//Инициализируем считыватель файла
const reader = new FileReader();
//Обработчик начала считывания данных
reader.onloadstart = () => setLoading(true);
//Обработчик считывания данных
reader.onload = async e => {
//Обрабатываем текст файла
onClick(e.target.result);
setLoading(false);
};
//Обработчик ошибки считывания данных
reader.onerror = () => setLoading(false);
//Загружаем данные из файла
reader.readAsText(event.target.files[0]);
}
//Очищаем значение
event.target.value = null;
};
//Генерация содержимого
return (
<>
{loading ? <P8PAppProgress open={true} text={TEXTS.LOADING} /> : null}
<InputLabel htmlFor={id} sx={STYLES.INPUT_LABEL}>
<Input
id={id}
type="file"
hidden
onChange={handleChange}
sx={STYLES.INPUT_FILE}
inputProps={{ accept: ".xml" }}
disabled={disabled}
/>
{buttonView}
</InputLabel>
</>
);
};
//Контроль свойств - Компонент "Загрузка из файла"
ImportPanel.propTypes = {
id: PropTypes.string.isRequired,
buttonView: PropTypes.object.isRequired,
onClick: PropTypes.func.isRequired,
disabled: PropTypes.bool
};
//Генерация действия "Загрузить из файла" меню действий
export const toolbarImportRenderer = ({ icon, title, disabled, onClick }) => {
//Представление кнопки загрузки
let importButtonView = (
<IconButton title={title} disabled={disabled}>
<Icon>{icon}</Icon>
</IconButton>
);
//Генерация содержимого
return <ImportPanel id={"input-file-loader-toolbar"} buttonView={importButtonView} onClick={onClick} disabled={disabled} />;
};
//Компонент кнопки импорта панели из файла
export const ImportPanelButton = ({ id, startIcon, title, onClick }) => {
//Представление кнопки загрузки
let importButtonView = (
<Button startIcon={startIcon ? <Icon>{startIcon}</Icon> : null} component="span">
{title}
</Button>
);
//Генерация содержимого
return <ImportPanel id={id} buttonView={importButtonView} onClick={onClick} />;
};
//Контроль свойств - Компонент кнопки импорта панели из файла
ImportPanelButton.propTypes = {
id: PropTypes.string.isRequired,
startIcon: PropTypes.string,
title: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired
};
//Выгрузка XML файла
export const exportXMLFile = (data, fileName) => {
//Переводим в данные для файла
const blobData = new Blob([data], { type: "text/plain" });
//Формируем URL
const url = URL.createObjectURL(blobData);
//Делаем линк и устанавливаем параметры, после добавляем в DOM
const a = document.createElement("a");
a.href = url;
a.download = `${fileName.replace(/[/\\?%*:|"<>.,;= ]/g, "_")}.xml`;
document.body.appendChild(a);
//Выполняем открытие ссылки
a.click();
//Очищаем от лишних данных
document.body.removeChild(a);
URL.revokeObjectURL(url);
};

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