forked from CITKParus/P8-Panels
Compare commits
71 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a20de79a40 | ||
|
fa00940776 | ||
|
4115961742 | ||
|
f6b29d5339 | ||
|
b3e7d7bf7b | ||
|
f9b2f1ae34 | ||
|
836a3db5e2 | ||
|
6ff99591e2 | ||
|
c990cb2246 | ||
|
2830d8c5d9 | ||
|
87ca29f593 | ||
|
aad6bb2662 | ||
|
afbb33c90f | ||
|
759fc763e2 | ||
|
d792187ff9 | ||
|
a3fd089452 | ||
|
3f539065ba | ||
7c515f7ebb | |||
|
1a71cbdf1b | ||
|
077582bd4c | ||
|
6958cfd904 | ||
|
f36d46525e | ||
|
97072a9b60 | ||
|
47d6b0cdb1 | ||
11f29bcf0c | |||
|
5c7a3b16b2 | ||
|
2672bcd8be | ||
c6688bd451 | |||
|
fa71c76a7d | ||
|
418e77bf74 | ||
|
e4683cf991 | ||
|
416eae7d88 | ||
|
fbbbd7c247 | ||
|
f4c665a74b | ||
|
a639c6371c | ||
|
4f2a1d4034 | ||
|
5a08fdf605 | ||
|
be351f7920 | ||
|
4d59203604 | ||
|
c734b62ba0 | ||
|
939efc0733 | ||
|
b2888efd62 | ||
|
b1b1288e60 | ||
|
50e3970c93 | ||
|
f418951695 | ||
|
fe02011a25 | ||
|
6ebbd0f08f | ||
|
a81797f5ac | ||
|
1cd0177454 | ||
|
5cf9b5db85 | ||
|
1954b27a27 | ||
|
2ce1fc8db2 | ||
9f99c99643 | |||
|
6a41686a84 | ||
|
a62daa4407 | ||
|
ef397b1818 | ||
|
1e580f806d | ||
|
6c935623ec | ||
efc787d3a5 | |||
|
c8790f85d9 | ||
|
ff4a67f375 | ||
|
e70c6b8e84 | ||
d06f3a2db1 | |||
|
624b1bd7be | ||
|
bab08ba1d3 | ||
|
fcc254178f | ||
|
3eba0a52f0 | ||
|
72aa5bc89c | ||
|
dca71f5383 | ||
|
b3cfa176eb | ||
49c28750f6 |
161
README.md
161
README.md
@ -104,6 +104,9 @@
|
||||
git clone https://git.citpb.ru/CITKParus/P8-Panels.git
|
||||
```
|
||||
|
||||
> **Внимание:** если при клонировании репозитория возникает ошибка "Server certificate verification failed" - используйте ключ `http.sslVerify=false`:\
|
||||
> `git clone https://git.citpb.ru/CITKParus/P8-Panels.git -c http.sslVerify=false`
|
||||
|
||||
6. Проведите компиляцию хранимых объектов БД из каталога "db" клонированного репозитория (компиляцию проводить под пользователем-владельцем схемы серверной части Системы, с последующей перекомпиляцией зависимых инвалидных объектов), затем исполните скрипт "grants.sql", размещённый в этом же каталоге.
|
||||
|
||||
7. Перезапустите сервер приложений "ПАРУС 8 Онлайн"
|
||||
@ -337,6 +340,59 @@ const MyPanel = () => {
|
||||
|
||||
8. Выдайте права но новое действие в "Администраторе", при необходимости - начните новый сеанс в "ПАРУС 8 Онлайн" с очисткой системного кэша.
|
||||
|
||||
#### Настройка КОР-действия для вызова панели "Выдача сменного задания на участок" из раздела "Сменные задания"
|
||||
|
||||
Входящая в состав поставки фреймворка панель "Выдача сменного задания на участок" доступна для вызова из раздела "Сменные задания" (приложение "Планирование и учёт в дискретном производстве", главное меню > "Документы" > "Сменные задания").
|
||||
|
||||
Для настройки этой возможности:
|
||||
|
||||
1. Откройте раздел "Классы" приложения "Конструктор отраслевых расширений" (главное меню > "Учёт" > "Классы")
|
||||
2. В дереве классов выберите "Сменные задания", а в списке классов - класс с кодом "CostJobs"
|
||||
3. В спецификации "Методы", выбранного класса, зарегистрируйте новый метод со следующими атрибутами:
|
||||
|
||||
- `Мнемокод` - P8PANELS_OPEN
|
||||
- `Наименование` - P8PANELS_OPEN
|
||||
- `Тип метода` - Встроенный
|
||||
- `Доступность` - Клиентский
|
||||
|
||||
4. Для добавленного метода `P8PANELS_OPEN` в спецификации "Параметры" зарегистрируйте следующий набор параметров:
|
||||
|
||||
| Имя | Наименование | Тип | Домен | Обязательный | Тип привязки | Контекст | Параметр действия |
|
||||
| -------- | ------------------- | ------- | ------- | ------------ | ----------------- | -------------------- | ----------------- |
|
||||
| NRN | Рег. номер записи | Входной | TRN | Нет | Контекст | Идентификатор записи | |
|
||||
| SPANEL | Наименование панели | Входной | TSTRING | Да | Параметр действия | | SPANEL |
|
||||
| SCAPTION | Заголовок вкладки | Входной | TSTRING | Нет | Параметр действия | | SCAPTION |
|
||||
|
||||
5. В спецификации "Действия", выбранного класса, зарегистрируйте новое действие со следующими атрибутами:
|
||||
|
||||
- `Тип` - Нестандартное
|
||||
- `Код` - FCJOBS_OPEN_JOBS_MANAGE_MP
|
||||
- `Наименование` - Открытие панели "Выдача сменного задания на участок"
|
||||
- `Технология производства` - Конструктор
|
||||
- `Реализующий метод` - P8PANELS_OPEN
|
||||
- `Обработка записей` - Для одной текущей записи
|
||||
- `Завершение транзакции` - После каждого вызова действия
|
||||
- `Обновление выборки` - Не обновлять
|
||||
|
||||
6. Для добавленного действия `FCJOBS_OPEN_JOBS_MANAGE_MP` в спецификации "Параметры" зарегистрируйте следующий набор параметров:
|
||||
|
||||
| Имя | Домен | Тип привязки | Значение |
|
||||
| -------- | ------- | ------------ | ---------------------------------- |
|
||||
| SPANEL | TSTRING | Значение | MechRecCostJobsManageMP |
|
||||
| SCAPTION | TSTRING | Значение | Выдача сменного задания на участок |
|
||||
|
||||
7. Откройте редактор формы представления данных класса "CostJobs" ("Сменные задания").
|
||||
|
||||
Для этого отметьте в списке классов запись с кодом "CostJobs", перейдите на закладку "Методы вызова", укажите метод вызова "main", в его контекстном меню укажите "Формы", в появившемся списке форм выполните действие "Редактор" для формы с наименованием "Форма просмотра".
|
||||
|
||||
В открывшемся редакторе формы перейдите в режим редактирования всплывающего меню заголовка (закладка "Таблицы", таблица "CostJobs", затем кнопка "Редактор источника", установить фокус на форме представления данных щелчком мыши, затем пункт "Всплывающее меню" в "Инспекторе объектов"). Найдите в меню пункт, созданный Системой для действия, зарегистрированного на шаге 5 (как правило имеет метку, совпадающую с наименованием действия). Расположите (перетаскиванием) этот пункт меню сразу после пункта "Отработать исполнение по штрих-кодам". Укажите для этого пункта следующие параметры в "Инспекторе объектов":
|
||||
|
||||
- `Заголовок` - Выдать сменное задание на участок…
|
||||
|
||||
Закройте окна редакторов с сохранением изменений.
|
||||
|
||||
8. Выдайте права но новое действие в "Администраторе", при необходимости - начните новый сеанс в "ПАРУС 8 Онлайн" с очисткой системного кэша.
|
||||
|
||||
#### Настройка КОР-действия для вызова панели "Производственная программа" из раздела "Планы и отчеты производства изделий"
|
||||
|
||||
Входящая в состав поставки фреймворка панель "Производственная программа" доступна для вызова из спецификации "Выпуск" раздела "Планы и отчеты производства изделий" (приложение "Планирование и учёт в дискретном производстве", главное меню > "Документы" > "Планы и отчеты производства изделий").
|
||||
@ -460,7 +516,7 @@ c:\inetpub\p8web20\WebClient\Modules\P8-Panels>npm run build
|
||||
- `isRespErr` - функция, проверка результата исполнения серверного объекта на наличие ошибок
|
||||
- `getRespErrMessage` - функция, получение ошибки исполнения серверного объекта
|
||||
- `getRespPayload` - функция, получение выходных значений, полученных после успешного исполнения
|
||||
- `executeStored` -функция, асинхронное исполнение хранимой процедуры/функции БД Системы
|
||||
- `executeStored` - функция, асинхронное исполнение хранимой процедуры/функции БД Системы
|
||||
- `getConfig` - функция, асинхронное считывание параметров конфигурации, определённых в "p8panels.config" (возвращает их JSON-представление)
|
||||
|
||||
При формировании ответов, функции, получающие данные с сервера, возвращают типовые значения:
|
||||
@ -528,7 +584,8 @@ c:\inetpub\p8web20\WebClient\Modules\P8-Panels>npm run build
|
||||
throwError = true,
|
||||
showErrorMessage = true,
|
||||
fullResponse = false,
|
||||
spreadOutArguments = true
|
||||
spreadOutArguments = true,
|
||||
signal = null
|
||||
}
|
||||
```
|
||||
|
||||
@ -541,7 +598,8 @@ c:\inetpub\p8web20\WebClient\Modules\P8-Panels>npm run build
|
||||
`throwError` - необязательный, логический, признак генерации исключения, если `false` - возвращает ошибку в типовом формате\
|
||||
`showErrorMessage` - необязательный, логический, признак отображения типового клиентского сообщение об ошибке, в случае её возникновения (только если `throwError = true`)\
|
||||
`fullResponse` - необязательный, логический, признак возврата полного типового ответа сервера, если `false` - возвращается только содержимое `XPAYLOAD`\
|
||||
`spreadOutArguments` - необязательный, логический, признак "разделения" значений выходных параметров исполняемого обхекта (игнорируется при наличии `respArg`), если `true` - `XPAYLOAD` будет содержать ответ в виде `{"ВЫХОДНОЙ_ПАРАМЕТР1": "ЗНАЧЕНИЕ", "ВЫХОДНОЙ_ПАРАМЕТР2": "ЗНАЧЕНИЕ", ...}`, если `false` - `XPAYLOAD` будет содержать ответ в виде `{XOUT_ARGUMENTS: [{SNAME: "ВЫХОДНОЙ_ПАРАМЕТР1", VALUE: "ЗНАЧЕНИЕ"}, {SNAME: "ВЫХОДНОЙ_ПАРАМЕТР2", VALUE: "ЗНАЧЕНИЕ"}, ...]}`
|
||||
`spreadOutArguments` - необязательный, логический, признак "разделения" значений выходных параметров исполняемого обхекта (игнорируется при наличии `respArg`), если `true` - `XPAYLOAD` будет содержать ответ в виде `{"ВЫХОДНОЙ_ПАРАМЕТР1": "ЗНАЧЕНИЕ", "ВЫХОДНОЙ_ПАРАМЕТР2": "ЗНАЧЕНИЕ", ...}`, если `false` - `XPAYLOAD` будет содержать ответ в виде `{XOUT_ARGUMENTS: [{SNAME: "ВЫХОДНОЙ_ПАРАМЕТР1", VALUE: "ЗНАЧЕНИЕ"}, {SNAME: "ВЫХОДНОЙ_ПАРАМЕТР2", VALUE: "ЗНАЧЕНИЕ"}, ...]}`\
|
||||
`signal` - необязательный, объект, экземпляр `AbortSignal` (например, `AbortController.signal`) для управления прерыванием выполнения запроса
|
||||
|
||||
**Результат:** объект с данными, размещёнными в `XPAYLOAD` ответа сервера (если `fullResponse = false`) или полный типовой ответ (описан выше).
|
||||
|
||||
@ -1035,7 +1093,7 @@ const Mui = ({ title }) => {
|
||||
|
||||

|
||||
|
||||
###### `undefined showMsg(type, text, msgOnOk = null, msgOnCancel = null)`
|
||||
###### `undefined showMsg(type, text, msgOnOk = null, msgOnCancel = null, fullErrorText = null)`
|
||||
|
||||
Отображает модальное окно сообщения заданного типа.
|
||||
|
||||
@ -1043,16 +1101,21 @@ const Mui = ({ title }) => {
|
||||
|
||||
`type` - обязательный, строка, тип отображаемого сообщения, `information|warning|error` (см. константу `MSG_TYPE` в "app/context/messaging_reducer" и константу `P8P_APP_MESSAGE_VARIANT` в "app/components/p8p_app_message")\
|
||||
`text` - обязательный, строка, текст отображаемого сообщения\
|
||||
`msgOnOk` - необязательный, функция, будет вызвана при нажатии на "ОК"/"ЗАКРЫТЬ" в сообщении
|
||||
`msgOnCancel` - необязательный, функция, будет вызвана при нажатии на "ОТМЕНА" в сообщении (только для сообщений типа `warning`)
|
||||
`msgOnOk` - необязательный, функция, будет вызвана при нажатии на "ОК"/"ЗАКРЫТЬ" в сообщении\
|
||||
`msgOnCancel` - необязательный, функция, будет вызвана при нажатии на "ОТМЕНА" в сообщении (только для сообщений типа `warning`)\
|
||||
`fullErrorText` - необязательный, строка, полный текст ошибки, используется только при `type="error"`. Если параметр указан, то в окно ошибки выводится кнопка "Подробнее", по нажатию на которую будет отображаться текст, указанный в данном параметре
|
||||
|
||||
**Результат:** функция не возвращает значимого результата
|
||||
|
||||
###### `undefined showMsgErr(text, msgOnOk = null)`
|
||||
###### `undefined showMsgErr(text, msgOnOk = null, fullErrorText = null)`
|
||||
|
||||
Декоратор для `showMsg`, отображает модальное окно сообщения типа "Ошибка" (`type="error"`).
|
||||
|
||||
**Входные параметры:** аналогично `showMsg`
|
||||
**Входные параметры:**
|
||||
|
||||
`text` - обязательный, строка, текст отображаемого сообщения\
|
||||
`msgOnOk` - необязательный, функция, будет вызвана при нажатии на "ЗАКРЫТЬ" в сообщении\
|
||||
`fullErrorText` - необязательный, строка, полный текст ошибки. Если параметр указан, то в окно ошибки выводится кнопка "Подробнее", по нажатию на которую будет отображаться текст, указанный в данном параметре
|
||||
|
||||
**Результат:** аналогично `showMsg`
|
||||
|
||||
@ -1140,6 +1203,20 @@ const Messages = ({ title }) => {
|
||||
Ошибка
|
||||
</Button>
|
||||
<Divider sx={STYLES.DIVIDER} />
|
||||
{/* Сообщение об ошибке (диалог с подробностями) */}
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() =>
|
||||
showMsgErr(
|
||||
"Что-то пошло не так :( ...но мы точно знаем что ;)",
|
||||
null,
|
||||
"Здесь подробная информация об ошибке (стек вызова СУБД, например)"
|
||||
)
|
||||
}
|
||||
>
|
||||
Ошибка с подробностями
|
||||
</Button>
|
||||
<Divider sx={STYLES.DIVIDER} />
|
||||
{/* Предупреждение (диалог) */}
|
||||
<Button
|
||||
variant="contained"
|
||||
@ -1190,7 +1267,7 @@ const Messages = ({ title }) => {
|
||||
|
||||

|
||||
|
||||
###### `undefined showMsg(message)`
|
||||
###### `undefined showLoader(message)`
|
||||
|
||||
Отображает модальный индикатор процесса с указанным сообщением.
|
||||
|
||||
@ -1257,14 +1334,14 @@ const Loader = ({ title }) => {
|
||||
- состоят из значительного числа интерфейсных примитивов
|
||||
- имеют специальный API на стороне сервера БД Системы для управления их содержимым
|
||||
|
||||
Необходимо понимать, что с одной стороны, наличие серверного API в БД значительно упрощает взаимодействие с компонентом, с другой стороны - ограничивает возможности его примерения только теми прикладными задачами и функциональными возможностями, которые заложены в него. При этом "примитивы" HTML и MUI, хоть и сложнее в применении, но позволяют "собирать" практически любые интерфейсные решения на вкус разработчика.
|
||||
Необходимо понимать, что с одной стороны, наличие серверного API в БД значительно упрощает взаимодействие с компонентом, с другой стороны - ограничивает возможности его применения только теми прикладными задачами и функциональными возможностями, которые заложены в него. При этом "примитивы" HTML и MUI, хоть и сложнее в применении, но позволяют "собирать" практически любые интерфейсные решения на вкус разработчика.
|
||||
|
||||
##### Таблица данных "P8PDataGrid"
|
||||
|
||||
Предназначена для формирования табличных представлений данных с поддержкой:
|
||||
|
||||
- постраничного вывода данных
|
||||
- сортировки и отбора данных по колонкам на строне сервера БД
|
||||
- сортировки и отбора данных по колонкам на стороне сервера БД
|
||||
- сложных заголовков с возможностью отображения/сокрытия уровней
|
||||
- разворачивающихся строк (accordion)
|
||||
- группировки строк с возможностью отображения/сокрытия содержимого группы
|
||||
@ -1291,6 +1368,8 @@ const MyPanel = () => {
|
||||
|
||||
**Свойства**
|
||||
|
||||
`style` - необязательный, объект, если задан, то будет применён в качестве атрибута `style` коневого контейнера (`div`) компонента\
|
||||
`tableStyle` - необязательный, объект, если задан, то будет применён в качестве атрибута `sx` компонента `Table`\
|
||||
`columnsDef` - необязательный, массив, описание колонок таблицы, содержит объекты вида `{caption: <ЗАГОЛОВОК_КОЛОНКИ>, dataType: <ТИП_ДАННЫХ - NUMB|STR|DATE>, filter: <ПРИЗНАК_ВОЗМОЖНОСТИ_ОТБОРА - true|false>, hint: <ОПИСАНИЕ_КОЛОНКИ_МОЖЕТ_СОДЕРЖАТЬ_HTML_РАЗМЕТКУ>, name: <НАИМЕНОВАНИЕ_КОЛОНКИ>, order: <ПРИЗНАК_ВОЗМОЖНОСТИ_СОРТИРОВКИ - true|false>, values: <МАССИВ_ПРЕДОПРЕДЕЛЁННЫХ_ЗНАЧЕНИЙ>, visible: <ПРИЗНАК_ВИДИМОСТИ_КОЛОНКИ - true|false>,expandable: <ПРИЗНАК_РАЗВОРАЧИВАЕМОСТИ_ГРУППОВОГО_ЗАГОЛОВКА - true|false>, expanded: <ПРИЗНАК_РАЗВЕРНУТОСТИ_ГРУППОВОГО_ЗАГОЛОВКА - true|false>, parent: <НАИМЕНОВАНИЕ_РОДИТЕЛЬСКОЙ_КОЛОНКИ_В_ГРУППОВОМ_ЗАГОЛОВКЕ>, width: <ШИРИНА_КОЛОНКИ>}`\
|
||||
`filtersInitial` - необязательныей, массив, начальное состояние фильтров таблицы, содержит объекты вида `{name: <НАИМЕНОВАНИЕ_КОЛОНКИ>, from: <НАЧАЛО_ДИАПАЗОНА_ЗНАЧЕНИЙ_ФИЛЬТРА>, to: <ОКОНЧАНИЕ_ДИАПАЗОНА_ЗНАЧЕНИЙ_ФИЛЬТРА>}`\
|
||||
`groups` - необязательный, массив групп данных, содержит объекты вида `{name: <ИМЯ_ГРУППЫ>, caption: <ЗАГОЛОВОК_ГРУППЫ>, expandable: <ПРИЗНАК_РАЗВОРАЧИВАЕМОСТИ_ГРУППЫ - true|false>, expanded: <ПРИЗНАК_РАЗВЕРНУТОСТИ_ГРУППЫ - true|false>}`\
|
||||
@ -1319,7 +1398,7 @@ const MyPanel = () => {
|
||||
`rowExpandRender` - необязательный, функция формирования представления развёрнутой строки таблицы (если не указана - интерфейсный элемент для "разворачивания" строки не будет отображён, даже при `expandable=true`). Сигнатура функции `f({row, columnsDef})`. Будет вызвана в момент "развёртывания" строки таблицы пользователем, в функцию будет передан объект, поле `row` которого будет содержать данные текущей "разворачиваемой" строки таблицы, а поле `columnsDef` - описание колонок таблицы. Должна возвращать представление "развёрнутой" строки таблицы в виде значения или Rect-компонента.\
|
||||
`valueFormatter` - необязательный, функция форматирования значений колонки (если не указана - форматирование согласно `columnsDef`). Сигнатура функции `f({value, columnDef})`. Будет вызвана в момент формирования ячейки таблицы (если ранее для ячейки `dataCellRender` не вернул специального представления) и в моммент формирования фильтра для ячейки. Должна возвращать отформатированное значение ячейки или React-компонент для её представления.\
|
||||
`containerComponent` - необязательный, функциональный React-компонент или строка с именем HTML-тэга, будет применён для формирования в иерархии DOM элемента-обёртки (контейнера) таблицы (по умолчанию используется компонет библиотеки MUI - Paper)\
|
||||
`containerComponentProps` - необязательный, объект, содержит свойства, которые будут переданы компоненту-контейнеру таблицы\
|
||||
`containerComponentProps` - необязательный, объект, содержит свойства, которые будут переданы компоненту-контейнеру (`TableContainer`) таблицы (является дочерним для корневого `div`, контейнера всего компонента)\
|
||||
`onOrderChanged` - необязательный, функция, будет вызвана при изменении пользователем состояния сортировок таблицы. Сигнатура функции `f({orders})`, результат функции не интерпретируется. В функцию передаётся объект, поле `orders` которого, содержит текущее состояние сортировок таблицы. Объект `orders` - массив, содержащий элементы вида `{name: <НАИМЕНОВАНИЕ_КОЛОНКИ>, direction: <ASC|DESC>}`. Функция применяется для инициации обновления данных в таблице.\
|
||||
`onFilterChanged` - необязательный, функция, будет вызвана при изменении пользователем состояния фильтров таблицы. Сигнатура функции `f({filters})`, результат функции не интерпретируется. В функцию передаётся объект, поле `filters` которого, содержит текущее состояние фильтров таблицы. Объект `filters` - массив, содержащий элементы вида `{name: <НАИМЕНОВАНИЕ_КОЛОНКИ>, from: <ЗНАЧЕНИЕ_НАЧАЛА_ДИАПАЗОНА_ОТБОРА>, to: <ЗНАЧЕНИЕ_ОКОНЧАНИЯ_ДИАПАЗОНА_ОТБОРА>}`. Функция применяется для инициации обновления данных в таблице.\
|
||||
`onPagesCountChanged` - необязательный, функция, будет вызвана при изменении пользователем количества отображаемых страниц данных таблицы. Сигнатура функции `f()`, результат функции не интерпретируется. Функция применяется для инициации обновления данных в таблице.\
|
||||
@ -1900,7 +1979,7 @@ const Chart = ({ title }) => {
|
||||
};
|
||||
```
|
||||
|
||||
Полные актуальные исходные коды примеров можно увидеть в "db/PKG_P8PANELS_SAMPLES.pck" и "app/panels/samples/data_grid.js" данного репозитория соответственно.
|
||||
Полные актуальные исходные коды примеров можно увидеть в "db/PKG_P8PANELS_SAMPLES.pck" и "app/panels/samples/chart.js" данного репозитория соответственно.
|
||||
|
||||
##### Диаграмма ганта "P8PGantt"
|
||||
|
||||
@ -1953,6 +2032,7 @@ const MyPanel = () => {
|
||||
`onTaskProgressChange` - необязательный, функция, если указана - будет вызвана при изменении прогресса исполнения элемента диаграммы, сигнатура функции `f({task, progress})`, результат функции не интерпретируется. В функцию будет передан объект в поле `task`, которого, будет содержаться описание изменённой задачи (элемент массива `tasks`, см. выше описание полей), в поле `progress` - новое значение прогресса исполнения задачи.\
|
||||
`taskAttributeRenderer` - необязательный, функция, если указана - будет вызвана при отображении диалога редактора здачи, результат функции будет применён для отображения области дополнительных атрибутов задачи в диалоге редактора, если не указана - дополнительные атрибуты будут отображены с форматированием по умолчанию. Сигнатура функции - `f({task, attribute})`, в функцию будет передан объект в поле `task`, которого, будет содержаться описание задачи для которой отображается редактор (элемент массива `tasks`, см. выше описание полей), в поле `attribute` - описание дополнительного атрибута формируемого в диалоге редактора (элемент массива `taskAttributes`, см. выше описание полей). Должна возвращать значение или React-компонент.\
|
||||
`taskDialogRenderer` - необязательный, функция, если указана - будет вызвана до отображения диалога редактора задачи. Результат функции будет показан в качестве содержимого диалога редактора, вместо типовой формы. Сигнатура функции - `f({task, taskAttributes, taskColors, close})`, в функцию будет передан объект в поле `task`, которого, будет содержаться описание задачи для которой отображается редактор (элемент массива `tasks`, см. выше описание полей), в поле `taskAttributes` - массив `taskAttributes` (см. выше описание полей), описывающий состав полей задачи, в поле `taskColors` - массив `taskColors` (см. выше описание полей), описывающий цвета заливки, определённые для задачи, в поле `close` - функция закрытия диалога задачи, может быть вызвана возвращаемым Reac-компонентом для сокрытия диалога. Должна возвращать значение или React-компонент.\
|
||||
`taskDialogProps` - необязательный, объект, содержит свойства, которые будут переданы компоненту-контейнеру (`Dialog`) редактора задачи.\
|
||||
`noDataFoundText` - обязательный, строка, текст для отображения ошибки об отсутствии данных\
|
||||
`numbTaskEditorCaption` - обязательный, строка, подпись стандартного атрибута `numb` в диалоге редактора задачи\
|
||||
`nameTaskEditorCaption` - обязательный, строка, подпись стандартного атрибута `name` в диалоге редактора задачи\
|
||||
@ -2862,6 +2942,61 @@ export { Cyclogram };
|
||||
|
||||
Полные актуальные исходные коды примеров можно увидеть в "db/PKG_P8PANELS_SAMPLES.pck" и "app/panels/samples/cyclogram.js" данного репозитория соответственно.
|
||||
|
||||
##### Индикатор "P8PIndicator"
|
||||
|
||||
Компонент предназначен для отображения данных в виде индикатора. Поддерживается:
|
||||
|
||||
- Цветовая индикация предопределёнными цветами в зависимости от состояния (не определено, позитивное, негативное, пограничное)
|
||||
- Цветовая индикация пользовательскими цветами
|
||||
- Обработка нажатий
|
||||
- Отображение иконки
|
||||
- Упрвление внешним видом (парение, рамка)
|
||||
- Интерактивные подсказки
|
||||
|
||||

|
||||
|
||||
**Подключение**
|
||||
|
||||
Клиентская часть индикатора реализована в компоненте `P8PIndicator`, объявленном в "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. Тем не менее, при разработке пользовательских интерфейсов панелей важно придерживаться предложенных ниже правил. Это позволит создавать их в едином ключе и упростит работу конечного пользователя при их освоении.
|
||||
|
@ -3,10 +3,49 @@
|
||||
Типовые стили
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { STATE } from "./app.text"; //Текстовые ресурсы и константы
|
||||
import { red, green, orange, grey } from "@mui/material/colors";
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
//Цвета
|
||||
export const APP_COLORS = {
|
||||
[STATE.UNDEFINED]: {
|
||||
color: "#dcdcdca0",
|
||||
contrColor: "black"
|
||||
},
|
||||
[STATE.INFO]: {
|
||||
color: "white",
|
||||
contrColor: "black"
|
||||
},
|
||||
[STATE.OK]: {
|
||||
color: green[200],
|
||||
contrColor: green[900]
|
||||
},
|
||||
[STATE.ERR]: {
|
||||
color: red[200],
|
||||
contrColor: red[900]
|
||||
},
|
||||
[STATE.WARN]: {
|
||||
color: orange[200],
|
||||
contrColor: orange[900]
|
||||
},
|
||||
HOVER: {
|
||||
color: grey[200],
|
||||
contrColor: grey[900]
|
||||
},
|
||||
ACTIVE: {
|
||||
color: grey[400],
|
||||
contrColor: grey[900]
|
||||
}
|
||||
};
|
||||
|
||||
//Стили
|
||||
export const APP_STYLES = {
|
||||
SCROLL: {
|
||||
|
37
app.text.js
37
app.text.js
@ -12,13 +12,20 @@ export const TITLES = {
|
||||
INFO: "Информация", //Информационный блок
|
||||
WARN: "Предупреждение", //Блок предупреждения
|
||||
ERR: "Ошибка", //Информация об ошибке
|
||||
DEFAULT_PANELS_GROUP: "Без привязки к группе" //Заголовок группы панелей по умолчанию
|
||||
DEFAULT_PANELS_GROUP: "Без привязки к группе", //Заголовок группы панелей по умолчанию
|
||||
DATA_SOURCE_CONFIG: "Настройка источника данных", //Заголовок для настройки источника данных
|
||||
INSERT: "Добавление", //Заголовок для диалогов/форм добавления
|
||||
UPDATE: "Исправление" //Заголовок для диалогов/форм исправления
|
||||
};
|
||||
|
||||
//Текст
|
||||
export const TEXTS = {
|
||||
LOADING: "Ожидайте...", //Ожидание завершения процесса
|
||||
NO_DATA_FOUND: "Данных не найдено" //Отсутствие данных
|
||||
NO_DATA_FOUND: "Данных не найдено", //Отсутствие данных
|
||||
NO_DATA_FOUND_SHORT: "Н.Д.", //Отсутствие данных (кратко)
|
||||
NO_SETTINGS: "Настройки не определены", //Отстутсвие настроек
|
||||
UNKNOWN_SOURCE_TYPE: "Неизвестный тип источника", //Отсуствие типа источника
|
||||
UNNAMED_SOURCE: "Источник без наименования" //Отсутствие наименования источника
|
||||
};
|
||||
|
||||
//Текст кнопок
|
||||
@ -29,11 +36,19 @@ export const BUTTONS = {
|
||||
OK: "ОК", //Ок
|
||||
CANCEL: "Отмена", //Отмена
|
||||
CLOSE: "Закрыть", //Сокрытие
|
||||
DETAIL: "Подробнее", //Отображение подробностей/детализации
|
||||
HIDE: "Скрыть", //Скрытие информации
|
||||
CLEAR: "Очистить", //Очистка
|
||||
ORDER_ASC: "По возрастанию", //Сортировка по возрастанию
|
||||
ORDER_DESC: "По убыванию", //Сортировка по убыванию
|
||||
FILTER: "Фильтр", //Фильтрация
|
||||
MORE: "Ещё" //Догрузка данных
|
||||
MORE: "Ещё", //Догрузка данных
|
||||
APPLY: "Применить", //Сохранение без закрытия интерфейса ввода
|
||||
SAVE: "Сохранить", //Сохранение
|
||||
CONFIG: "Настроить", //Настройка
|
||||
INSERT: "Добавить", //Добавление
|
||||
UPDATE: "Исправить", //Исправление
|
||||
DELETE: "Удалить" //Удаление
|
||||
};
|
||||
|
||||
//Метки атрибутов, сопроводительные надписи
|
||||
@ -46,7 +61,9 @@ export const CAPTIONS = {
|
||||
START: "Начало",
|
||||
END: "Окончание",
|
||||
PROGRESS: "Прогресс",
|
||||
LEGEND: "Легенда"
|
||||
LEGEND: "Легенда",
|
||||
USER_PROC: "Пользовательская процедура",
|
||||
QUERY: "Запрос"
|
||||
};
|
||||
|
||||
//Типовые сообщения об ошибках
|
||||
@ -54,10 +71,20 @@ export const ERRORS = {
|
||||
UNDER_CONSTRUCTION: "Панель в разработке",
|
||||
P8O_API_UNAVAILABLE: '"ПАРУС 8 Онлайн" недоступен',
|
||||
P8O_API_UNSUPPORTED: 'Функция "ПАРУС 8 Онлайн" не поддерживается',
|
||||
DEFAULT: "Неожиданная ошибка"
|
||||
DEFAULT: "Неожиданная ошибка",
|
||||
DATA_SOURCE_NO_REQ_ARGS: "Не заданы обязательные параметры источника данных"
|
||||
};
|
||||
|
||||
//Типовые сообщения для ошибок HTTP
|
||||
export const ERRORS_HTTP = {
|
||||
404: "Адрес не найден"
|
||||
};
|
||||
|
||||
//Типовые статусы
|
||||
export const STATE = {
|
||||
UNDEFINED: "UNDEFINED",
|
||||
INFO: "INFORMATION",
|
||||
OK: "OK",
|
||||
ERR: "ERR",
|
||||
WARN: "WARN"
|
||||
};
|
||||
|
@ -86,6 +86,9 @@ const Workspace = ({ panels = [], selectedPanel, children } = {}) => {
|
||||
//Подключение к контексту навигации
|
||||
const { navigateRoot, navigatePanel } = useContext(NavigationCtx);
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { appState } = useContext(ApplicationСtx);
|
||||
|
||||
//Отработка действия навигации домой
|
||||
const handleHomeNavigate = () => navigateRoot();
|
||||
|
||||
@ -98,6 +101,7 @@ const Workspace = ({ panels = [], selectedPanel, children } = {}) => {
|
||||
{...P8P_APP_WORKSPACE_CONFIG_PROPS}
|
||||
panels={panels}
|
||||
selectedPanel={selectedPanel}
|
||||
caption={appState.appBarTitle}
|
||||
onHomeNavigate={handleHomeNavigate}
|
||||
onItemNavigate={handleItemNavigate}
|
||||
>
|
||||
|
71
app/components/editors/p8p_component_inline_message.js
Normal file
71
app/components/editors/p8p_component_inline_message.js
Normal file
@ -0,0 +1,71 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редакторы панелей
|
||||
Компонент: Информационное сообщение внутри компонента
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Stack, Icon, Typography } from "@mui/material"; //Интерфейсные элементы
|
||||
import { TEXTS } from "../../../app.text"; //Общие текстовые ресурсы
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Типы сообщений компонентов
|
||||
const P8P_COMPONENT_INLINE_MESSAGE_TYPE = {
|
||||
COMMON: "COMMON",
|
||||
ERROR: "ERROR"
|
||||
};
|
||||
|
||||
//Типовые сообщения компонентов
|
||||
const P8P_COMPONENT_INLINE_MESSAGE = {
|
||||
NO_DATA_FOUND: TEXTS.NO_DATA_FOUND,
|
||||
NO_SETTINGS: TEXTS.NO_SETTINGS
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Информационное сообщение внутри компонента
|
||||
const P8PComponentInlineMessage = ({ icon, name, message, type = P8P_COMPONENT_INLINE_MESSAGE_TYPE.COMMON }) => {
|
||||
//Формирование представления
|
||||
return (
|
||||
<Stack direction={"column"}>
|
||||
<Stack direction={"row"} justifyContent={"center"} alignItems={"center"}>
|
||||
{icon && <Icon color={"disabled"}>{icon}</Icon>}
|
||||
{name && (
|
||||
<Typography align={"center"} color={"text.secondary"} variant={"button"}>
|
||||
{name}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<Typography
|
||||
align={"center"}
|
||||
color={type != P8P_COMPONENT_INLINE_MESSAGE_TYPE.ERROR ? "text.secondary" : "error.dark"}
|
||||
variant={"caption"}
|
||||
>
|
||||
{message}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Информационное сообщение внутри компонента
|
||||
P8PComponentInlineMessage.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
message: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(Object.values(P8P_COMPONENT_INLINE_MESSAGE_TYPE))
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8P_COMPONENT_INLINE_MESSAGE_TYPE, P8P_COMPONENT_INLINE_MESSAGE, P8PComponentInlineMessage };
|
40
app/components/editors/p8p_config_dialog.js
Normal file
40
app/components/editors/p8p_config_dialog.js
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редакторы панелей
|
||||
Компонент: Диалог настройки
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { P8PDialog } from "../p8p_dialog"; //Типовой диалог
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Диалог настройки
|
||||
const P8PConfigDialog = ({ title, children, onOk, onCancel }) => {
|
||||
//Формирование представления
|
||||
return (
|
||||
<P8PDialog title={title} onOk={onOk} onCancel={onCancel}>
|
||||
{children}
|
||||
</P8PDialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Диалог настройки
|
||||
P8PConfigDialog.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
||||
onOk: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8PConfigDialog };
|
113
app/components/editors/p8p_data_source.js
Normal file
113
app/components/editors/p8p_data_source.js
Normal file
@ -0,0 +1,113 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редакторы панелей
|
||||
Компонент: Источник данных
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Stack, IconButton, Icon, Typography, Chip, Button, Card, CardContent, CardActions, CardActionArea } from "@mui/material"; //Интерфейсные элементы
|
||||
import { BUTTONS, TEXTS } from "../../../app.text"; //Общие текстовые ресурсы
|
||||
import { STYLES as COMMON_STYLES } from "./p8p_editors_common"; //Общие ресурсы редаторов
|
||||
import { P8P_DATA_SOURCE_SHAPE, P8P_DATA_SOURCE_TYPE, P8P_DATA_SOURCE_TYPE_NAME, P8P_DATA_SOURCE_INITIAL } from "./p8p_data_source_common"; //Общие ресурсы компонента "Источник данных"
|
||||
import { P8PDataSourceConfigDialog } from "./p8p_data_source_config_dialog"; //Диалог настройки источника данных
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Источник данных
|
||||
const P8PDataSource = ({ dataSource = null, valueProviders = {}, onChange = null } = {}) => {
|
||||
//Собственное состояние - отображение диалога настройки
|
||||
const [configDlg, setConfigDlg] = useState(false);
|
||||
|
||||
//Уведомление родителя о смене настроек источника данных
|
||||
const notifyChange = settings => onChange && onChange(settings);
|
||||
|
||||
//При нажатии на настройку источника данных
|
||||
const handleSetup = () => setConfigDlg(true);
|
||||
|
||||
//При нажатии на настройку источника данных
|
||||
const handleSetupOk = dataSource => {
|
||||
setConfigDlg(false);
|
||||
notifyChange(dataSource);
|
||||
};
|
||||
|
||||
//При нажатии на настройку источника данных
|
||||
const handleSetupCancel = () => setConfigDlg(false);
|
||||
|
||||
//При удалении настроек источника данных
|
||||
const handleDelete = () => notifyChange({ ...P8P_DATA_SOURCE_INITIAL });
|
||||
|
||||
//Расчет флага "настроенности"
|
||||
const configured = dataSource?.type ? true : false;
|
||||
|
||||
//Список аргументов
|
||||
const args =
|
||||
configured &&
|
||||
dataSource.arguments.map((argument, i) => (
|
||||
<Chip
|
||||
key={i}
|
||||
label={`:${argument.name} = ${argument.valueSource || argument.value || "NULL"}`}
|
||||
variant={"outlined"}
|
||||
sx={COMMON_STYLES.CHIP(true)}
|
||||
/>
|
||||
));
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<>
|
||||
{configDlg && (
|
||||
<P8PDataSourceConfigDialog
|
||||
dataSource={dataSource}
|
||||
valueProviders={valueProviders}
|
||||
onOk={handleSetupOk}
|
||||
onCancel={handleSetupCancel}
|
||||
/>
|
||||
)}
|
||||
{configured && (
|
||||
<Card variant={"outlined"}>
|
||||
<CardActionArea onClick={handleSetup}>
|
||||
<CardContent>
|
||||
<Typography variant={"subtitle1"} noWrap={true}>
|
||||
{dataSource.type === P8P_DATA_SOURCE_TYPE.USER_PROC ? dataSource.userProc : TEXTS.UNNAMED_SOURCE}
|
||||
</Typography>
|
||||
<Typography variant={"caption"} color={"text.secondary"} noWrap={true}>
|
||||
{P8P_DATA_SOURCE_TYPE_NAME[dataSource.type] || TEXTS.UNKNOWN_SOURCE_TYPE}
|
||||
</Typography>
|
||||
<Stack direction={"column"} spacing={1} pt={2}>
|
||||
{args}
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
<CardActions>
|
||||
<IconButton onClick={handleDelete}>
|
||||
<Icon>delete</Icon>
|
||||
</IconButton>
|
||||
</CardActions>
|
||||
</Card>
|
||||
)}
|
||||
{!configured && (
|
||||
<Button startIcon={<Icon>build</Icon>} onClick={handleSetup}>
|
||||
{BUTTONS.CONFIG}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Источник данных
|
||||
P8PDataSource.propTypes = {
|
||||
dataSource: P8P_DATA_SOURCE_SHAPE,
|
||||
valueProviders: PropTypes.object,
|
||||
onChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8PDataSource };
|
86
app/components/editors/p8p_data_source_common.js
Normal file
86
app/components/editors/p8p_data_source_common.js
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редакторы панелей
|
||||
Общие ресурсы компонента "Источник данных"
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import client from "../../core/client"; //Клиент БД
|
||||
import { CAPTIONS } from "../../../app.text"; //Общие текстовые ресурсы
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Типы даных аргументов
|
||||
const P8P_DATA_SOURCE_ARGUMENT_DATA_TYPE = {
|
||||
STR: client.SERV_DATA_TYPE_STR,
|
||||
NUMB: client.SERV_DATA_TYPE_NUMB,
|
||||
DATE: client.SERV_DATA_TYPE_DATE
|
||||
};
|
||||
|
||||
//Структура аргумента источника данных
|
||||
const P8P_DATA_SOURCE_ARGUMENT_SHAPE = PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
caption: PropTypes.string.isRequired,
|
||||
dataType: PropTypes.oneOf(Object.values(P8P_DATA_SOURCE_ARGUMENT_DATA_TYPE)),
|
||||
req: PropTypes.bool.isRequired,
|
||||
value: PropTypes.any,
|
||||
valueSource: PropTypes.string
|
||||
});
|
||||
|
||||
//Начальное состояние аргумента источника данных
|
||||
const P8P_DATA_SOURCE_ARGUMENT_INITIAL = {
|
||||
name: "",
|
||||
caption: "",
|
||||
dataType: "",
|
||||
req: false,
|
||||
value: "",
|
||||
valueSource: ""
|
||||
};
|
||||
|
||||
//Типы источников данных
|
||||
const P8P_DATA_SOURCE_TYPE = {
|
||||
USER_PROC: "USER_PROC",
|
||||
QUERY: "QUERY"
|
||||
};
|
||||
|
||||
//Типы источников данных (наименования)
|
||||
const P8P_DATA_SOURCE_TYPE_NAME = {
|
||||
[P8P_DATA_SOURCE_TYPE.USER_PROC]: CAPTIONS.USER_PROC,
|
||||
[P8P_DATA_SOURCE_TYPE.QUERY]: CAPTIONS.QUERY
|
||||
};
|
||||
|
||||
//Структура источника данных
|
||||
const P8P_DATA_SOURCE_SHAPE = PropTypes.shape({
|
||||
type: PropTypes.oneOf([...Object.values(P8P_DATA_SOURCE_TYPE), ""]),
|
||||
userProc: PropTypes.string,
|
||||
stored: PropTypes.string,
|
||||
respArg: PropTypes.string,
|
||||
arguments: PropTypes.arrayOf(P8P_DATA_SOURCE_ARGUMENT_SHAPE)
|
||||
});
|
||||
|
||||
//Начальное состояние истоника данных
|
||||
const P8P_DATA_SOURCE_INITIAL = {
|
||||
type: "",
|
||||
userProc: "",
|
||||
stored: "",
|
||||
respArg: "",
|
||||
arguments: []
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export {
|
||||
P8P_DATA_SOURCE_ARGUMENT_DATA_TYPE,
|
||||
P8P_DATA_SOURCE_ARGUMENT_INITIAL,
|
||||
P8P_DATA_SOURCE_SHAPE,
|
||||
P8P_DATA_SOURCE_TYPE,
|
||||
P8P_DATA_SOURCE_TYPE_NAME,
|
||||
P8P_DATA_SOURCE_INITIAL
|
||||
};
|
185
app/components/editors/p8p_data_source_config_dialog.js
Normal file
185
app/components/editors/p8p_data_source_config_dialog.js
Normal file
@ -0,0 +1,185 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редакторы панелей
|
||||
Компонент: Диалог настройки источника данных
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useEffect, useContext } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Stack, IconButton, Icon, TextField, InputAdornment, MenuItem, Menu } from "@mui/material"; //Интерфейсные элементы
|
||||
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
|
||||
import { TITLES, CAPTIONS } from "../../../app.text"; //Общие текстовые ресурсы
|
||||
import { P8PConfigDialog } from "./p8p_config_dialog"; //Типовой диалог настройки
|
||||
import { P8P_DATA_SOURCE_TYPE, P8P_DATA_SOURCE_SHAPE, P8P_DATA_SOURCE_ARGUMENT_INITIAL, P8P_DATA_SOURCE_INITIAL } from "./p8p_data_source_common"; //Общие ресурсы компонента "Источник данных"
|
||||
import { useUserProcDesc } from "./p8p_data_source_hooks"; //Хуки источников данных
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Диалог настройки источника данных
|
||||
const P8PDataSourceConfigDialog = ({ dataSource = null, valueProviders = {}, onOk = null, onCancel = null } = {}) => {
|
||||
//Собственное состояние - параметры элемента формы
|
||||
const [state, setState] = useState({ ...P8P_DATA_SOURCE_INITIAL, ...dataSource });
|
||||
|
||||
//Собственное состояние - флаги обновление данных
|
||||
const [refresh, setRefresh] = useState({ userProcDesc: 0 });
|
||||
|
||||
//Собственное состояние - элемент привязки меню выбора источника
|
||||
const [valueProvidersMenuAnchorEl, setValueProvidersMenuAnchorEl] = useState(null);
|
||||
|
||||
//Описание выбранной пользовательской процедуры
|
||||
const [userProcDesc] = useUserProcDesc({ code: state.userProc, refresh: refresh.userProcDesc });
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
|
||||
|
||||
//Установка значения/привязки аргумента
|
||||
const setArgumentValueSource = (index, value, valueSource) =>
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
arguments: pv.arguments.map((argument, i) => ({ ...argument, ...(i == index ? { value, valueSource } : {}) }))
|
||||
}));
|
||||
|
||||
//Открытие/сокрытие меню выбора источника
|
||||
const toggleValueProvidersMenu = target => setValueProvidersMenuAnchorEl(target instanceof Element ? target : null);
|
||||
|
||||
//При нажатии на очистку наименования пользовательской процедуры
|
||||
const handleUserProcClearClick = () => setState({ ...P8P_DATA_SOURCE_INITIAL });
|
||||
|
||||
//При нажатии на выбор пользовательской процедуры в качестве источника данных
|
||||
const handleUserProcSelectClick = () => {
|
||||
pOnlineShowDictionary({
|
||||
unitCode: "UserProcedures",
|
||||
showMethod: "main",
|
||||
inputParameters: [{ name: "in_CODE", value: state.userProc }],
|
||||
callBack: res => {
|
||||
if (res.success) {
|
||||
setState(pv => ({ ...pv, type: P8P_DATA_SOURCE_TYPE.USER_PROC, userProc: res.outParameters.out_CODE }));
|
||||
setRefresh(pv => ({ ...pv, userProcDesc: pv.userProcDesc + 1 }));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//При закрытии дилога с сохранением
|
||||
const handleOk = () => onOk && onOk({ ...state });
|
||||
|
||||
//При закртии диалога отменой
|
||||
const handleCancel = () => onCancel && onCancel();
|
||||
|
||||
//При очистке значения/связывания аргумента
|
||||
const handleArgumentClearClick = index => setArgumentValueSource(index, "", "");
|
||||
|
||||
//При отображении меню связывания аргумента с поставщиком данных
|
||||
const handleArgumentLinkMenuClick = e => setValueProvidersMenuAnchorEl(e.currentTarget);
|
||||
|
||||
//При выборе элемента меню связывания аргумента с поставщиком данных
|
||||
const handleArgumentLinkClick = valueSource => {
|
||||
setArgumentValueSource(valueProvidersMenuAnchorEl.id, "", valueSource);
|
||||
toggleValueProvidersMenu();
|
||||
};
|
||||
|
||||
//При вводе значения аргумента
|
||||
const handleArgumentChange = (index, value) => setArgumentValueSource(index, value, "");
|
||||
|
||||
//При изменении описания пользовательской процедуры
|
||||
useEffect(() => {
|
||||
if (userProcDesc)
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
stored: userProcDesc?.stored?.name,
|
||||
respArg: userProcDesc?.stored?.respArg,
|
||||
arguments: (userProcDesc?.arguments || []).map(argument => ({ ...P8P_DATA_SOURCE_ARGUMENT_INITIAL, ...argument }))
|
||||
}));
|
||||
}, [userProcDesc]);
|
||||
|
||||
//Список значений
|
||||
const values = Object.keys(valueProviders).reduce((res, key) => [...res, ...Object.keys(valueProviders[key])], []);
|
||||
|
||||
//Наличие значений
|
||||
const isValues = values && values.length > 0 ? true : false;
|
||||
|
||||
//Меню привязки к поставщикам значений
|
||||
const valueProvidersMenu = isValues && (
|
||||
<Menu anchorEl={valueProvidersMenuAnchorEl} open={Boolean(valueProvidersMenuAnchorEl)} onClose={toggleValueProvidersMenu}>
|
||||
{values.map((value, i) => (
|
||||
<MenuItem key={i} onClick={() => handleArgumentLinkClick(value)}>
|
||||
{value}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<P8PConfigDialog title={TITLES.DATA_SOURCE_CONFIG} onOk={handleOk} onCancel={handleCancel}>
|
||||
<Stack direction={"column"} spacing={1}>
|
||||
{valueProvidersMenu}
|
||||
<TextField
|
||||
type={"text"}
|
||||
variant={"standard"}
|
||||
value={state.userProc}
|
||||
label={CAPTIONS.USER_PROC}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleUserProcClearClick}>
|
||||
<Icon>clear</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={handleUserProcSelectClick}>
|
||||
<Icon>list</Icon>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{Array.isArray(state?.arguments) &&
|
||||
state.arguments.map((argument, i) => (
|
||||
<TextField
|
||||
key={i}
|
||||
type={"text"}
|
||||
variant={"standard"}
|
||||
value={argument.value || argument.valueSource}
|
||||
label={argument.caption}
|
||||
onChange={e => handleArgumentChange(i, e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => handleArgumentClearClick(i)}>
|
||||
<Icon>clear</Icon>
|
||||
</IconButton>
|
||||
{isValues && (
|
||||
<IconButton id={i} onClick={handleArgumentLinkMenuClick}>
|
||||
<Icon>settings_ethernet</Icon>
|
||||
</IconButton>
|
||||
)}
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</P8PConfigDialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Диалог настройки источника данных
|
||||
P8PDataSourceConfigDialog.propTypes = {
|
||||
dataSource: P8P_DATA_SOURCE_SHAPE,
|
||||
valueProviders: PropTypes.object,
|
||||
onOk: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8PDataSourceConfigDialog };
|
151
app/components/editors/p8p_data_source_hooks.js
Normal file
151
app/components/editors/p8p_data_source_hooks.js
Normal file
@ -0,0 +1,151 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редакторы панелей
|
||||
Пользовательские хуки компонента "Источник данных"
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { useState, useContext, useEffect, useRef } from "react"; //Классы React
|
||||
import client from "../../core/client"; //Клиент взаимодействия с сервером приложений
|
||||
import { ERRORS } from "../../../app.text"; //Общие текстовые ресурсы
|
||||
import { formatErrorMessage } from "../../core/utils"; //Общие вспомогательные функции
|
||||
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
|
||||
import { P8P_DATA_SOURCE_TYPE, P8P_DATA_SOURCE_ARGUMENT_DATA_TYPE } from "./p8p_data_source_common"; //Общие ресурсы источника данных
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Описание пользовательской процедуры
|
||||
const useUserProcDesc = ({ code, refresh }) => {
|
||||
//Собственное состояние - флаг загрузки
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
//Собственное состояние - данные
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored } = useContext(BackEndСtx);
|
||||
|
||||
//При необходимости обновить данные компонента
|
||||
useEffect(() => {
|
||||
//Загрузка данных с сервера
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PE.USERPROCS_DESC",
|
||||
args: { SCODE: code },
|
||||
respArg: "COUT",
|
||||
isArray: name => name === "arguments",
|
||||
loader: false
|
||||
});
|
||||
setData(data?.XUSERPROC || null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
//Если надо обновить и есть для чего получать данные
|
||||
if (refresh > 0)
|
||||
if (code) loadData();
|
||||
else setData(null);
|
||||
}, [refresh, code, executeStored]);
|
||||
|
||||
//Возвращаем интерфейс хука
|
||||
return [data, isLoading];
|
||||
};
|
||||
|
||||
//Получение данных из источника
|
||||
const useDataSource = ({ dataSource, values }) => {
|
||||
//Контроллер для прерывания запросов
|
||||
const abortController = useRef(null);
|
||||
|
||||
//Собственное состояние - параметры исполнения
|
||||
const [state, setState] = useState({ stored: null, storedArgs: [], respArg: null, reqSet: false });
|
||||
|
||||
//Собственное состояние - флаг загрузки
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
//Собственное состояние - данные
|
||||
const [data, setData] = useState({ init: false });
|
||||
|
||||
//Собственное состояние - ошибка получения данных
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored } = useContext(BackEndСtx);
|
||||
|
||||
//При необходимости обновить данные
|
||||
useEffect(() => {
|
||||
//Загрузка данных с сервера
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
abortController.current?.abort?.();
|
||||
abortController.current = new AbortController();
|
||||
const data = await executeStored({
|
||||
stored: state.stored,
|
||||
args: { ...(state.storedArgs ? state.storedArgs : {}) },
|
||||
respArg: state.respArg,
|
||||
loader: false,
|
||||
signal: abortController.current.signal,
|
||||
showErrorMessage: false
|
||||
});
|
||||
setError(null);
|
||||
setData({ ...data, init: true });
|
||||
} catch (e) {
|
||||
if (e.message !== client.ERR_ABORTED) {
|
||||
setError(formatErrorMessage(e.message).text);
|
||||
setData({ init: false });
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
if (state.reqSet) {
|
||||
if (state.stored) loadData();
|
||||
} else setData({ init: false });
|
||||
return () => abortController.current?.abort?.();
|
||||
}, [state.stored, state.storedArgs, state.respArg, state.reqSet, executeStored]);
|
||||
|
||||
//При изменении свойств
|
||||
useEffect(() => {
|
||||
setState(pv => {
|
||||
if (dataSource?.type == P8P_DATA_SOURCE_TYPE.USER_PROC) {
|
||||
const { stored, respArg } = dataSource;
|
||||
let reqSet = true;
|
||||
const storedArgs = {};
|
||||
dataSource.arguments.forEach(argument => {
|
||||
let v = argument.valueSource ? values[argument.valueSource] : argument.value;
|
||||
storedArgs[argument.name] =
|
||||
argument.dataType == P8P_DATA_SOURCE_ARGUMENT_DATA_TYPE.NUMB
|
||||
? isNaN(parseFloat(v))
|
||||
? null
|
||||
: parseFloat(v)
|
||||
: argument.dataType == P8P_DATA_SOURCE_ARGUMENT_DATA_TYPE.DATE
|
||||
? new Date(v)
|
||||
: String(v === undefined ? "" : v);
|
||||
if (argument.req === true && [undefined, null, ""].includes(storedArgs[argument.name])) reqSet = false;
|
||||
});
|
||||
if (pv.stored != stored || pv.respArg != respArg || JSON.stringify(pv.storedArgs) != JSON.stringify(storedArgs)) {
|
||||
if (!reqSet) {
|
||||
setError(ERRORS.DATA_SOURCE_NO_REQ_ARGS);
|
||||
setData({ init: false });
|
||||
}
|
||||
return { stored, respArg, storedArgs, reqSet };
|
||||
} else return pv;
|
||||
} else return pv;
|
||||
});
|
||||
}, [dataSource, values]);
|
||||
|
||||
//Возвращаем интерфейс хука
|
||||
return [data, error, isLoading];
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { useUserProcDesc, useDataSource };
|
59
app/components/editors/p8p_editor_box.js
Normal file
59
app/components/editors/p8p_editor_box.js
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редакторы панелей
|
||||
Компонент: Контейнер редактора
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Box, Divider, IconButton, Icon, Stack } from "@mui/material"; //Интерфейсные компоненты MUI
|
||||
import { BUTTONS } from "../../../app.text"; //Общие текстовые ресурсы
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Контейнер редактора
|
||||
const P8PEditorBox = ({ title, children, onSave }) => {
|
||||
//При нажатии на "Сохранить"
|
||||
const handleSaveClick = (closeEditor = false) => onSave && onSave(closeEditor);
|
||||
|
||||
//Флаг отображения кнопок сохранения
|
||||
const showSaveBar = onSave ? true : false;
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<Box p={2}>
|
||||
<Divider>{title}</Divider>
|
||||
<Stack direction={"column"} spacing={1}>
|
||||
{children}
|
||||
</Stack>
|
||||
{showSaveBar && (
|
||||
<Stack direction={"row"} justifyContent={"right"} p={1}>
|
||||
<IconButton onClick={() => handleSaveClick(false)} title={BUTTONS.APPLY}>
|
||||
<Icon>done</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={() => handleSaveClick(true)} title={BUTTONS.SAVE}>
|
||||
<Icon>done_all</Icon>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Контейнер редактора
|
||||
P8PEditorBox.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
||||
onSave: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8PEditorBox };
|
46
app/components/editors/p8p_editor_sub_header.js
Normal file
46
app/components/editors/p8p_editor_sub_header.js
Normal file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редакторы панелей
|
||||
Компонент: Заголовок раздела редактора
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Divider, Chip } from "@mui/material"; //Интерфейсные компоненты MUI
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
DIVIDER: { paddingTop: "20px" }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Заголовок раздела редактора
|
||||
const P8PEditorSubHeader = ({ title }) => {
|
||||
//Формирование представления
|
||||
return (
|
||||
<Divider sx={STYLES.DIVIDER}>
|
||||
<Chip label={title} size={"small"} />
|
||||
</Divider>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Заголовок раздела редактора
|
||||
P8PEditorSubHeader.propTypes = {
|
||||
title: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8PEditorSubHeader };
|
53
app/components/editors/p8p_editor_toolbar.js
Normal file
53
app/components/editors/p8p_editor_toolbar.js
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редакторы панелей
|
||||
Компонент: Панель инструментов редактора
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { IconButton, Icon, Stack } from "@mui/material"; //Интерфейсные компоненты MUI
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Структура элемента панели инструментов редактора
|
||||
const P8P_EDITOR_TOOL_BAR_ITEM_SHAPE = PropTypes.shape({
|
||||
icon: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired
|
||||
});
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Панель инструментов редактора
|
||||
const P8PEditorToolBar = ({ items = [] }) => {
|
||||
//Формирование представления
|
||||
return (
|
||||
<Stack direction={"row"} p={1}>
|
||||
{items.map((item, i) => (
|
||||
<IconButton key={i} onClick={item.onClick} title={item.title} disabled={item?.disabled === true}>
|
||||
<Icon>{item.icon}</Icon>
|
||||
</IconButton>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Панель инструментов редактора
|
||||
P8PEditorToolBar.propTypes = {
|
||||
items: PropTypes.arrayOf(P8P_EDITOR_TOOL_BAR_ITEM_SHAPE)
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8PEditorToolBar };
|
30
app/components/editors/p8p_editors_common.js
Normal file
30
app/components/editors/p8p_editors_common.js
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редакторы панелей
|
||||
Общие ресурсы редакторов
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CHIP: (fullWidth = false, multiLine = false) => ({
|
||||
...(multiLine ? { height: "auto" } : {}),
|
||||
"& .MuiChip-label": {
|
||||
...(multiLine
|
||||
? {
|
||||
display: "block",
|
||||
whiteSpace: "normal"
|
||||
}
|
||||
: {}),
|
||||
...(fullWidth ? { width: "100%" } : {})
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { STYLES };
|
@ -7,7 +7,7 @@
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import React, { useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import Dialog from "@mui/material/Dialog"; //базовый класс диалога Material UI
|
||||
import DialogTitle from "@mui/material/DialogTitle"; //Заголовок диалога
|
||||
@ -18,6 +18,8 @@ import Typography from "@mui/material/Typography"; //Текст
|
||||
import Button from "@mui/material/Button"; //Кнопки
|
||||
import Container from "@mui/material/Container"; //Контейнер
|
||||
import Box from "@mui/material/Box"; //Обёртка
|
||||
import { BUTTONS, STATE } from "../../app.text"; //Типовые текстовые ресурсы и константы
|
||||
import { APP_COLORS } from "../../app.styles"; //Типовые стили
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
@ -25,9 +27,9 @@ import Box from "@mui/material/Box"; //Обёртка
|
||||
|
||||
//Варианты исполнения
|
||||
const P8P_APP_MESSAGE_VARIANT = {
|
||||
INFO: "information",
|
||||
WARN: "warning",
|
||||
ERR: "error"
|
||||
INFO: STATE.INFO,
|
||||
WARN: STATE.WARN,
|
||||
ERR: STATE.ERR
|
||||
};
|
||||
|
||||
//Стили
|
||||
@ -36,28 +38,35 @@ const STYLES = {
|
||||
wordBreak: "break-word"
|
||||
},
|
||||
INFO: {
|
||||
titleText: {},
|
||||
bodyText: {}
|
||||
titleText: {
|
||||
color: APP_COLORS[STATE.INFO].contrColor
|
||||
},
|
||||
bodyText: {
|
||||
color: APP_COLORS[STATE.INFO].contrColor
|
||||
}
|
||||
},
|
||||
WARN: {
|
||||
titleText: {
|
||||
color: "orange"
|
||||
color: APP_COLORS[STATE.WARN].contrColor
|
||||
},
|
||||
bodyText: {
|
||||
color: "orange"
|
||||
color: APP_COLORS[STATE.WARN].contrColor
|
||||
}
|
||||
},
|
||||
ERR: {
|
||||
titleText: {
|
||||
color: "red"
|
||||
color: APP_COLORS[STATE.ERR].contrColor
|
||||
},
|
||||
bodyText: {
|
||||
color: "red"
|
||||
color: APP_COLORS[STATE.ERR].contrColor
|
||||
}
|
||||
},
|
||||
INLINE_MESSAGE: {
|
||||
with: "100%",
|
||||
textAlign: "center"
|
||||
},
|
||||
FULL_ERROR_TEXT_BUTTON: {
|
||||
color: APP_COLORS[STATE.WARN].contrColor
|
||||
}
|
||||
};
|
||||
|
||||
@ -66,7 +75,25 @@ const STYLES = {
|
||||
//-----------
|
||||
|
||||
//Сообщение
|
||||
const P8PAppMessage = ({ variant, title, titleText, cancelBtn, onCancel, cancelBtnCaption, okBtn, onOk, okBtnCaption, open, text }) => {
|
||||
const P8PAppMessage = ({
|
||||
variant,
|
||||
title,
|
||||
titleText,
|
||||
cancelBtn,
|
||||
onCancel,
|
||||
cancelBtnCaption,
|
||||
okBtn,
|
||||
onOk,
|
||||
okBtnCaption,
|
||||
open,
|
||||
text,
|
||||
fullErrorText,
|
||||
showErrMoreCaption,
|
||||
hideErrMoreCaption
|
||||
}) => {
|
||||
//Состояние подробной информации об ошибке
|
||||
const [showFullErrorText, setShowFullErrorText] = useState(false);
|
||||
|
||||
//Подбор стиля и ресурсов
|
||||
let style = STYLES.INFO;
|
||||
switch (variant) {
|
||||
@ -86,12 +113,7 @@ const P8PAppMessage = ({ variant, title, titleText, cancelBtn, onCancel, cancelB
|
||||
|
||||
//Заголовок
|
||||
let titlePart;
|
||||
if (title && titleText)
|
||||
titlePart = (
|
||||
<DialogTitle id="message-dialog-title" style={{ ...style.DEFAULT, ...style.titleText }}>
|
||||
{titleText}
|
||||
</DialogTitle>
|
||||
);
|
||||
if (title && titleText) titlePart = <DialogTitle style={{ ...style.DEFAULT, ...style.titleText }}>{titleText}</DialogTitle>;
|
||||
|
||||
//Кнопка Отмена
|
||||
let cancelBtnPart;
|
||||
@ -102,16 +124,26 @@ const P8PAppMessage = ({ variant, title, titleText, cancelBtn, onCancel, cancelB
|
||||
let okBtnPart;
|
||||
if (okBtn && okBtnCaption)
|
||||
okBtnPart = (
|
||||
<Button onClick={() => (onOk ? onOk() : null)} color="primary" autoFocus>
|
||||
<Button onClick={() => (onOk ? onOk() : null)} autoFocus>
|
||||
{okBtnCaption}
|
||||
</Button>
|
||||
);
|
||||
|
||||
//Кнопка Подробнее
|
||||
let fullErrorTextBtn;
|
||||
if (fullErrorText && showErrMoreCaption && hideErrMoreCaption && variant === P8P_APP_MESSAGE_VARIANT.ERR)
|
||||
fullErrorTextBtn = (
|
||||
<Button onClick={() => setShowFullErrorText(!showFullErrorText)} sx={STYLES.FULL_ERROR_TEXT_BUTTON} autoFocus>
|
||||
{!showFullErrorText ? showErrMoreCaption : hideErrMoreCaption}
|
||||
</Button>
|
||||
);
|
||||
|
||||
//Все действия
|
||||
let actionsPart;
|
||||
if (cancelBtnPart || okBtnPart)
|
||||
actionsPart = (
|
||||
<DialogActions>
|
||||
{fullErrorTextBtn}
|
||||
{okBtnPart}
|
||||
{cancelBtnPart}
|
||||
</DialogActions>
|
||||
@ -119,17 +151,10 @@ const P8PAppMessage = ({ variant, title, titleText, cancelBtn, onCancel, cancelB
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Dialog
|
||||
open={open || false}
|
||||
aria-labelledby="message-dialog-title"
|
||||
aria-describedby="message-dialog-description"
|
||||
onClose={() => (onCancel ? onCancel() : null)}
|
||||
>
|
||||
<Dialog open={open || false} onClose={() => (onCancel ? onCancel() : null)}>
|
||||
{titlePart}
|
||||
<DialogContent>
|
||||
<DialogContentText id="message-dialog-description" style={style.bodyText}>
|
||||
{text}
|
||||
</DialogContentText>
|
||||
<DialogContentText style={style.bodyText}>{!showFullErrorText ? text : fullErrorText}</DialogContentText>
|
||||
</DialogContent>
|
||||
{actionsPart}
|
||||
</Dialog>
|
||||
@ -148,7 +173,10 @@ P8PAppMessage.propTypes = {
|
||||
onOk: PropTypes.func,
|
||||
okBtnCaption: PropTypes.string,
|
||||
open: PropTypes.bool,
|
||||
text: PropTypes.string
|
||||
text: PropTypes.string,
|
||||
fullErrorText: PropTypes.string,
|
||||
showErrMoreCaption: PropTypes.string,
|
||||
hideErrMoreCaption: PropTypes.string
|
||||
};
|
||||
|
||||
//Встроенное сообщение
|
||||
@ -158,13 +186,19 @@ const P8PAppInlineMessage = ({ variant, text, okBtn, onOk, okBtnCaption }) => {
|
||||
<Container style={STYLES.INLINE_MESSAGE}>
|
||||
<Box p={1}>
|
||||
<Typography
|
||||
color={variant === P8P_APP_MESSAGE_VARIANT.ERR ? "error" : variant === P8P_APP_MESSAGE_VARIANT.WARN ? "primary" : "textSecondary"}
|
||||
color={
|
||||
variant === P8P_APP_MESSAGE_VARIANT.ERR
|
||||
? APP_COLORS[STATE.ERR].contrColor
|
||||
: variant === P8P_APP_MESSAGE_VARIANT.WARN
|
||||
? APP_COLORS[STATE.WARN].contrColor
|
||||
: APP_COLORS[STATE.INFO].contrColor
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
{okBtn && okBtnCaption ? (
|
||||
<Box pt={1}>
|
||||
<Button onClick={() => (onOk ? onOk() : null)} color="primary" autoFocus>
|
||||
<Button onClick={() => (onOk ? onOk() : null)} autoFocus>
|
||||
{okBtnCaption}
|
||||
</Button>
|
||||
</Box>
|
||||
@ -216,6 +250,28 @@ const P8PAppInlineWarn = props => buildVariantInlineMessage(props, P8P_APP_MESSA
|
||||
//Встраиваемое сообщение информации
|
||||
const P8PAppInlineInfo = props => buildVariantInlineMessage(props, P8P_APP_MESSAGE_VARIANT.INFO);
|
||||
|
||||
//Диалог подсказки
|
||||
const P8PHintDialog = ({ title, hint, onOk }) => {
|
||||
return (
|
||||
<Dialog open={true} onClose={() => (onOk ? onOk() : null)}>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<div dangerouslySetInnerHTML={{ __html: hint }}></div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => (onOk ? onOk() : null)}>{BUTTONS.OK}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог подсказки
|
||||
P8PHintDialog.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
hint: PropTypes.string.isRequired,
|
||||
onOk: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
@ -229,5 +285,6 @@ export {
|
||||
P8PAppInlineMessage,
|
||||
P8PAppInlineError,
|
||||
P8PAppInlineWarn,
|
||||
P8PAppInlineInfo
|
||||
P8PAppInlineInfo,
|
||||
P8PHintDialog
|
||||
};
|
||||
|
@ -47,7 +47,7 @@ const STYLES = {
|
||||
//-----------
|
||||
|
||||
//Рабочее пространство
|
||||
const P8PAppWorkspace = ({ children, panels = [], selectedPanel, closeCaption, homeCaption, onHomeNavigate, onItemNavigate } = {}) => {
|
||||
const P8PAppWorkspace = ({ children, panels = [], selectedPanel, caption, closeCaption, homeCaption, onHomeNavigate, onItemNavigate } = {}) => {
|
||||
//Собственное состояния
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@ -86,7 +86,7 @@ const P8PAppWorkspace = ({ children, panels = [], selectedPanel, closeCaption, h
|
||||
<Icon>{open ? "chevron_left" : "menu"}</Icon>
|
||||
</IconButton>
|
||||
<Typography variant="h6" noWrap component="div">
|
||||
{selectedPanel?.caption}
|
||||
{caption || selectedPanel?.caption}
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
@ -120,6 +120,7 @@ P8PAppWorkspace.propTypes = {
|
||||
children: PropTypes.element,
|
||||
panels: PropTypes.arrayOf(P8P_PANELS_MENU_PANEL_SHAPE).isRequired,
|
||||
selectedPanel: P8P_PANELS_MENU_PANEL_SHAPE,
|
||||
caption: PropTypes.string,
|
||||
closeCaption: PropTypes.string.isRequired,
|
||||
homeCaption: PropTypes.string.isRequired,
|
||||
onHomeNavigate: PropTypes.func,
|
||||
|
@ -36,6 +36,8 @@ const P8P_DATA_GRID_FILTERS_HEIGHT = P8P_TABLE_FILTERS_HEIGHT;
|
||||
|
||||
//Таблица данных
|
||||
const P8PDataGrid = ({
|
||||
style = {},
|
||||
tableStyle = {},
|
||||
columnsDef = [],
|
||||
filtersInitial,
|
||||
groups = [],
|
||||
@ -114,6 +116,8 @@ const P8PDataGrid = ({
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<P8PTable
|
||||
style={style}
|
||||
tableStyle={tableStyle}
|
||||
columnsDef={columnsDef}
|
||||
groups={groups}
|
||||
rows={rows}
|
||||
@ -154,6 +158,8 @@ const P8PDataGrid = ({
|
||||
|
||||
//Контроль свойств - Таблица данных
|
||||
P8PDataGrid.propTypes = {
|
||||
style: PropTypes.object,
|
||||
tableStyle: PropTypes.object,
|
||||
columnsDef: PropTypes.array,
|
||||
filtersInitial: PropTypes.arrayOf(P8P_DATA_GRID_FILTER_SHAPE),
|
||||
groups: PropTypes.array,
|
||||
|
76
app/components/p8p_dialog.js
Normal file
76
app/components/p8p_dialog.js
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Компонент: Диалог
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useEffect, useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from "@mui/material"; //Интерфейсные компоненты
|
||||
import { BUTTONS } from "../../app.text"; //Общие текстовые ресурсы
|
||||
import { P8P_INPUT, P8PInput } from "./p8p_input"; //Поле ввода
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Диалог
|
||||
const P8PDialog = ({ title, inputs = [], children, onOk, onCancel, onClose }) => {
|
||||
//Состояние диалога
|
||||
const [state, setState] = useState({});
|
||||
|
||||
//При изменении элемента ввода
|
||||
const handleInputChange = (name, value) => setState(pv => ({ ...pv, [name]: value }));
|
||||
|
||||
//При нажатии на "ОК" диалога
|
||||
const handleOk = () => onOk && onOk(state);
|
||||
|
||||
//При нажатии на "Отмена" диалога
|
||||
const handleCancel = () => onCancel && onCancel();
|
||||
|
||||
//При нажатии на "Закрыть" диалога
|
||||
const handleClose = () => (onClose ? onClose() : onCancel ? onCancel() : null);
|
||||
|
||||
//При подключении к старнице
|
||||
useEffect(() => {
|
||||
setState(inputs.reduce((res, input) => ({ ...res, [input.name]: input.value == undefined ? null : input.value }), {}));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<Dialog onClose={handleClose} open>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
{inputs.map((input, i) => (
|
||||
<P8PInput key={i} {...input} value={state[input.name]} formValues={state} onChange={handleInputChange} />
|
||||
))}
|
||||
{children}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{onOk && <Button onClick={handleOk}>{BUTTONS.OK}</Button>}
|
||||
{onCancel && <Button onClick={handleCancel}>{BUTTONS.CANCEL}</Button>}
|
||||
{onClose && <Button onClick={handleClose}>{BUTTONS.CLOSE}</Button>}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог
|
||||
P8PDialog.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
inputs: PropTypes.arrayOf(PropTypes.shape(P8P_INPUT)),
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
||||
onOk: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8PDialog };
|
@ -139,6 +139,7 @@ const P8PGanttTaskEditor = ({
|
||||
onCancel,
|
||||
taskAttributeRenderer,
|
||||
taskDialogRenderer,
|
||||
taskDialogProps,
|
||||
numbCaption,
|
||||
nameCaption,
|
||||
startCaption,
|
||||
@ -186,7 +187,7 @@ const P8PGanttTaskEditor = ({
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Dialog open onClose={handleCancel}>
|
||||
<Dialog open onClose={handleCancel} {...(taskDialogProps ? taskDialogProps : {})}>
|
||||
{taskDialogRenderer ? (
|
||||
taskDialogRenderer({ task, taskAttributes, taskColors, close: handleCancel })
|
||||
) : (
|
||||
@ -315,6 +316,7 @@ P8PGanttTaskEditor.propTypes = {
|
||||
onCancel: PropTypes.func,
|
||||
taskAttributeRenderer: PropTypes.func,
|
||||
taskDialogRenderer: PropTypes.func,
|
||||
taskDialogProps: PropTypes.object,
|
||||
numbCaption: PropTypes.string.isRequired,
|
||||
nameCaption: PropTypes.string.isRequired,
|
||||
startCaption: PropTypes.string.isRequired,
|
||||
@ -347,6 +349,7 @@ const P8PGantt = ({
|
||||
onTaskProgressChange,
|
||||
taskAttributeRenderer,
|
||||
taskDialogRenderer,
|
||||
taskDialogProps,
|
||||
noDataFoundText,
|
||||
numbTaskEditorCaption,
|
||||
nameTaskEditorCaption,
|
||||
@ -467,6 +470,7 @@ const P8PGantt = ({
|
||||
onCancel={handleTaskEditorCancel}
|
||||
taskAttributeRenderer={taskAttributeRenderer}
|
||||
taskDialogRenderer={taskDialogRenderer}
|
||||
taskDialogProps={taskDialogProps}
|
||||
numbCaption={numbTaskEditorCaption}
|
||||
nameCaption={nameTaskEditorCaption}
|
||||
startCaption={startTaskEditorCaption}
|
||||
@ -502,6 +506,7 @@ P8PGantt.propTypes = {
|
||||
onTaskProgressChange: PropTypes.func,
|
||||
taskAttributeRenderer: PropTypes.func,
|
||||
taskDialogRenderer: PropTypes.func,
|
||||
taskDialogProps: PropTypes.object,
|
||||
noDataFoundText: PropTypes.string.isRequired,
|
||||
numbTaskEditorCaption: PropTypes.string.isRequired,
|
||||
nameTaskEditorCaption: PropTypes.string.isRequired,
|
||||
|
186
app/components/p8p_indicator.js
Normal file
186
app/components/p8p_indicator.js
Normal file
@ -0,0 +1,186 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Компонент: Индикатор
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { IconButton, Icon, Typography, Paper, Stack } from "@mui/material"; //Интерфейсные компоненты MUI
|
||||
import { P8PHintDialog } from "./p8p_app_message"; //Диалог подсказки
|
||||
import { TEXTS, STATE } from "../../app.text"; //Типовые текстовые ресурсы и константы
|
||||
import { APP_COLORS } from "../../app.styles"; //Типовые стили
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Варианты исполнения
|
||||
|
||||
const P8P_INDICATOR_VARIANT = {
|
||||
ELEVATION: "elevation",
|
||||
OUTLINED: "outlined"
|
||||
};
|
||||
|
||||
//Состояния
|
||||
const P8P_INDICATOR_STATE = {
|
||||
UNDEFINED: STATE.UNDEFINED,
|
||||
OK: STATE.OK,
|
||||
WARN: STATE.WARN,
|
||||
ERR: STATE.ERR
|
||||
};
|
||||
//Цвета заливки
|
||||
const BG_COLOR = {
|
||||
[STATE.OK]: APP_COLORS[STATE.OK].color,
|
||||
[STATE.ERR]: APP_COLORS[STATE.ERR].color,
|
||||
[STATE.WARN]: APP_COLORS[STATE.WARN].color
|
||||
};
|
||||
|
||||
//Цвета текста и иконок
|
||||
const COLOR = {
|
||||
[STATE.OK]: APP_COLORS[STATE.OK].contrColor,
|
||||
[STATE.ERR]: APP_COLORS[STATE.ERR].contrColor,
|
||||
[STATE.WARN]: APP_COLORS[STATE.WARN].contrColor
|
||||
};
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONTAINER: (state, clickable, userColor, userBackgroundColor) => ({
|
||||
padding: "10px",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
...getBackgroundColor(state, userBackgroundColor),
|
||||
...getColor(state, userColor),
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
...(clickable
|
||||
? {
|
||||
cursor: "pointer",
|
||||
"&:hover": { backgroundColor: APP_COLORS.HOVER.color },
|
||||
"&:active": { backgroundColor: APP_COLORS.ACTIVE.color }
|
||||
}
|
||||
: {})
|
||||
}),
|
||||
ICON: (state, userColor) => ({ fontSize: "50px", ...getColor(state, userColor) }),
|
||||
HINT_ICON: (state, userColor) => ({ fontSize: "1rem", ...getColor(state, userColor) }),
|
||||
VALUE_CAPTION_STACK: { containerType: "inline-size", width: "100%", overflow: "hidden" },
|
||||
CAPTION_TYPOGRAPHY: { width: "99cqw" }
|
||||
};
|
||||
|
||||
//-----------------------
|
||||
//Вспомогательные функции
|
||||
//-----------------------
|
||||
|
||||
//Подбор цвета заливки
|
||||
const getBackgroundColor = (state, userColor) =>
|
||||
userColor ? { backgroundColor: userColor } : BG_COLOR[state] ? { backgroundColor: BG_COLOR[state] } : {};
|
||||
|
||||
//Подбор цвета текста
|
||||
const getColor = (state, userColor) => (userColor ? { color: userColor } : COLOR[state] ? { color: COLOR[state] } : {});
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Индикатор
|
||||
const P8PIndicator = ({
|
||||
caption,
|
||||
value,
|
||||
icon = null,
|
||||
state = STATE.UNDEFINED,
|
||||
square = false,
|
||||
elevation = 3,
|
||||
variant = P8P_INDICATOR_VARIANT.ELEVATION,
|
||||
hint = null,
|
||||
onClick = null,
|
||||
backgroundColor = null,
|
||||
color = null
|
||||
} = {}) => {
|
||||
//Собственное состояние - отображение окна подсказки
|
||||
const [showHint, setShowHint] = useState(false);
|
||||
|
||||
//При нажатии на индикатор
|
||||
const handleClick = () => (onClick && !showHint ? onClick() : null);
|
||||
|
||||
//При нажатии на кнопку получения подсказки
|
||||
const handleHintClick = e => {
|
||||
setShowHint(true);
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
//При нажатии на кнопку закрытия подсказки
|
||||
const handleHintClose = () => setShowHint(false);
|
||||
|
||||
//Представление текста значения индикатора
|
||||
const valueTextView = <Typography variant={"h4"}>{[undefined, null, ""].includes(value) ? TEXTS.NO_DATA_FOUND_SHORT : value}</Typography>;
|
||||
|
||||
//Представление текста подписи индикатора
|
||||
const captionView = (
|
||||
<Typography align={"left"} noWrap={true} sx={STYLES.CAPTION_TYPOGRAPHY} title={caption}>
|
||||
{caption}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
//Представление подписи индикатора
|
||||
const valueView = hint ? (
|
||||
<>
|
||||
{showHint && <P8PHintDialog title={caption} hint={hint} onOk={handleHintClose} />}
|
||||
<Stack direction={"row"} alignItems={"start"}>
|
||||
{valueTextView}
|
||||
<IconButton onClick={handleHintClick}>
|
||||
<Icon sx={STYLES.HINT_ICON(state, color)}>help_outline</Icon>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</>
|
||||
) : (
|
||||
valueTextView
|
||||
);
|
||||
|
||||
//Флаг активности индикатора
|
||||
const clickable = onClick ? true : false;
|
||||
|
||||
//Представление
|
||||
return (
|
||||
<Paper
|
||||
elevation={variant === P8P_INDICATOR_VARIANT.ELEVATION ? elevation : 0}
|
||||
sx={STYLES.CONTAINER(state, clickable, color, backgroundColor)}
|
||||
square={square}
|
||||
variant={variant}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Stack direction={"row"} alignItems={"center"} justifyContent={"space-between"}>
|
||||
<Stack direction={"column"} alignItems={"start"} pr={2} sx={STYLES.VALUE_CAPTION_STACK}>
|
||||
{valueView}
|
||||
{captionView}
|
||||
</Stack>
|
||||
{icon ? <Icon sx={STYLES.ICON(state, color)}>{icon}</Icon> : null}
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Индикатор
|
||||
P8PIndicator.propTypes = {
|
||||
caption: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
icon: PropTypes.string,
|
||||
state: PropTypes.oneOf(Object.values(P8P_INDICATOR_STATE)),
|
||||
square: PropTypes.bool,
|
||||
elevation: PropTypes.number,
|
||||
variant: PropTypes.oneOf(Object.values(P8P_INDICATOR_VARIANT)),
|
||||
hint: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
backgroundColor: PropTypes.string,
|
||||
color: PropTypes.string
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8P_INDICATOR_VARIANT, P8P_INDICATOR_STATE, P8PIndicator };
|
137
app/components/p8p_input.js
Normal file
137
app/components/p8p_input.js
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга
|
||||
Компонент: Поле ввода
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useEffect } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Box, Icon, Input, InputAdornment, FormControl, Select, InputLabel, MenuItem, IconButton, Autocomplete, TextField } from "@mui/material"; //Интерфейсные компоненты
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Формат свойств поля ввода
|
||||
const P8P_INPUT = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.instanceOf(Date)]),
|
||||
label: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
dictionary: PropTypes.func,
|
||||
list: PropTypes.array,
|
||||
type: PropTypes.string,
|
||||
freeSolo: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
formValues: PropTypes.object
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Поле ввода
|
||||
const P8PInput = ({ name, value, label, onChange, dictionary, list, type, freeSolo = false, disabled = false, formValues, ...other }) => {
|
||||
//Значение элемента
|
||||
const [currentValue, setCurrentValue] = useState(value);
|
||||
|
||||
//При получении нового значения из вне
|
||||
useEffect(() => {
|
||||
setCurrentValue(value);
|
||||
}, [value]);
|
||||
|
||||
//Выбор значения из словаря
|
||||
const handleDictionaryClick = () => dictionary && dictionary(formValues, res => (res ? res.map(i => handleChangeByName(i.name, i.value)) : null));
|
||||
|
||||
//Изменение значения элемента (по событию)
|
||||
const handleChange = e => {
|
||||
setCurrentValue(e.target.value);
|
||||
if (onChange) onChange(e.target.name, e.target.value);
|
||||
};
|
||||
|
||||
//Изменение значения элемента (по имени и значению)
|
||||
const handleChangeByName = (targetName, value) => {
|
||||
if (targetName === name) setCurrentValue(value);
|
||||
if (onChange) onChange(targetName, value);
|
||||
};
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box p={1}>
|
||||
<FormControl variant={"standard"} fullWidth {...other}>
|
||||
{list ? (
|
||||
freeSolo ? (
|
||||
<Autocomplete
|
||||
id={name}
|
||||
name={name}
|
||||
freeSolo
|
||||
disabled={disabled}
|
||||
inputValue={currentValue ? currentValue : ""}
|
||||
onChange={(event, newValue) => handleChangeByName(name, newValue)}
|
||||
onInputChange={(event, newInputValue) => handleChangeByName(name, newInputValue)}
|
||||
options={list}
|
||||
renderInput={params => <TextField {...params} label={label} name={name} variant={"standard"} />}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<InputLabel id={`${name}Lable`} shrink>
|
||||
{label}
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`${name}Lable`}
|
||||
id={name}
|
||||
name={name}
|
||||
label={label}
|
||||
value={[undefined, null].includes(currentValue) ? "" : currentValue}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
displayEmpty
|
||||
>
|
||||
{list.map((item, i) => (
|
||||
<MenuItem key={i} value={[undefined, null].includes(item.value) ? "" : item.value}>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<InputLabel {...(type == "date" ? { shrink: true } : {})} htmlFor={name}>
|
||||
{label}
|
||||
</InputLabel>
|
||||
<Input
|
||||
id={name}
|
||||
name={name}
|
||||
value={currentValue ? currentValue : ""}
|
||||
endAdornment={
|
||||
dictionary ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton aria-label={`${name} select`} onClick={handleDictionaryClick} edge="end">
|
||||
<Icon>list</Icon>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
) : null
|
||||
}
|
||||
{...(type ? { type } : {})}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Поле ввода
|
||||
P8PInput.propTypes = P8P_INPUT;
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { P8P_INPUT, P8PInput };
|
@ -34,7 +34,7 @@ import {
|
||||
Link
|
||||
} from "@mui/material"; //Интерфейсные компоненты
|
||||
import { useTheme } from "@mui/material/styles"; //Взаимодействие со стилями MUI
|
||||
import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
|
||||
import { P8PAppInlineError, P8PHintDialog } from "./p8p_app_message"; //Встраиваемое сообщение об ошибке
|
||||
import { P8P_TABLE_AT, HEADER_INITIAL_STATE, hasValue, p8pTableReducer } from "./p8p_table_reducer"; //Редьюсер состояния
|
||||
|
||||
//---------
|
||||
@ -89,9 +89,7 @@ const P8P_TABLE_FILTERS_HEIGHT = "48px";
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
TABLE: {
|
||||
with: "100%"
|
||||
},
|
||||
TABLE: {},
|
||||
TABLE_HEAD_STICKY: {
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
@ -290,28 +288,6 @@ P8PTableColumnMenu.propTypes = {
|
||||
onItemClick: PropTypes.func
|
||||
};
|
||||
|
||||
//Диалог подсказки
|
||||
const P8PTableColumnHintDialog = ({ columnDef, okBtnCaption, onOk }) => {
|
||||
return (
|
||||
<Dialog open={true} aria-labelledby="filter-dialog-title" aria-describedby="filter-dialog-description" onClose={() => (onOk ? onOk() : null)}>
|
||||
<DialogTitle id="filter-dialog-title">{columnDef.caption}</DialogTitle>
|
||||
<DialogContent>
|
||||
<div dangerouslySetInnerHTML={{ __html: columnDef.hint }}></div>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => (onOk ? onOk() : null)}>{okBtnCaption}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог подсказки
|
||||
P8PTableColumnHintDialog.propTypes = {
|
||||
columnDef: PropTypes.object.isRequired,
|
||||
okBtnCaption: PropTypes.string.isRequired,
|
||||
onOk: PropTypes.func
|
||||
};
|
||||
|
||||
//Диалог фильтра
|
||||
const P8PTableColumnFilterDialog = ({
|
||||
columnDef,
|
||||
@ -488,6 +464,8 @@ P8PTableFiltersChips.propTypes = {
|
||||
|
||||
//Таблица
|
||||
const P8PTable = ({
|
||||
style = {},
|
||||
tableStyle = {},
|
||||
columnsDef = [],
|
||||
groups = [],
|
||||
rows = [],
|
||||
@ -702,10 +680,8 @@ const P8PTable = ({
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<div>
|
||||
{displayHintColumn ? (
|
||||
<P8PTableColumnHintDialog columnDef={displayHintColumnDef} okBtnCaption={okFilterBtnCaption} onOk={handleHintOk} />
|
||||
) : null}
|
||||
<div style={{ ...(style || {}) }}>
|
||||
{displayHintColumn ? <P8PHintDialog title={displayHintColumnDef.caption} hint={displayHintColumnDef.hint} onOk={handleHintOk} /> : null}
|
||||
{filterColumn ? (
|
||||
<P8PTableColumnFilterDialog
|
||||
columnDef={filterColumnDef}
|
||||
@ -735,7 +711,7 @@ const P8PTable = ({
|
||||
/>
|
||||
) : null}
|
||||
<TableContainer component={containerComponent ? containerComponent : Paper} {...(containerComponentProps ? containerComponentProps : {})}>
|
||||
<Table stickyHeader={fixedHeader} sx={STYLES.TABLE} size={size || P8P_TABLE_SIZE.MEDIUM}>
|
||||
<Table stickyHeader={fixedHeader} sx={{ ...STYLES.TABLE, ...(tableStyle || {}) }} size={size || P8P_TABLE_SIZE.MEDIUM}>
|
||||
<TableHead sx={fixedHeader ? STYLES.TABLE_HEAD_STICKY : {}}>
|
||||
{header.displayLevels.map((level, i) => (
|
||||
<TableRow key={level}>
|
||||
@ -900,6 +876,8 @@ const P8PTable = ({
|
||||
|
||||
//Контроль свойств - Таблица
|
||||
P8PTable.propTypes = {
|
||||
style: PropTypes.object,
|
||||
tableStyle: PropTypes.object,
|
||||
columnsDef: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
|
@ -56,6 +56,9 @@ export const ApplicationContext = ({ errors, displaySizeGetter, guidGenerator, c
|
||||
//Установка списка панелей
|
||||
const setPanels = panels => dispatch({ type: APP_AT.LOAD_PANELS, payload: panels });
|
||||
|
||||
//Установка заголовка в шапке приложения
|
||||
const setAppBarTitle = useCallback(appBarTitle => dispatch({ type: APP_AT.SET_APP_BAR_TITLE, payload: appBarTitle }), []);
|
||||
|
||||
//Поиск раздела по имени
|
||||
const findPanelByName = name => state.panels.find(panel => panel.name == name);
|
||||
|
||||
@ -169,6 +172,7 @@ export const ApplicationContext = ({ errors, displaySizeGetter, guidGenerator, c
|
||||
return (
|
||||
<ApplicationСtx.Provider
|
||||
value={{
|
||||
setAppBarTitle,
|
||||
findPanelByName,
|
||||
pOnlineShowTab,
|
||||
pOnlineShowUnit,
|
||||
|
@ -12,12 +12,14 @@ const APP_AT = {
|
||||
SET_URL_BASE: "SET_URL_BASE", //Установка базового URL приложения
|
||||
LOAD_PANELS: "LOAD_PANELS", //Загрузка списка панелей
|
||||
SET_INITIALIZED: "SET_INITIALIZED", //Установка флага инициализированности приложения
|
||||
SET_DISPLAY_SIZE: "SET_DISPLAY_SIZE" //Установка текущего типового размера экрана
|
||||
SET_DISPLAY_SIZE: "SET_DISPLAY_SIZE", //Установка текущего типового размера экрана
|
||||
SET_APP_BAR_TITLE: "SET_APP_BAR_TITLE" //Установка заголовка в шапке приложения
|
||||
};
|
||||
|
||||
//Состояние приложения по умолчанию
|
||||
const INITIAL_STATE = displaySizeGetter => ({
|
||||
displaySize: displaySizeGetter(),
|
||||
appBarTitle: "",
|
||||
urlBase: "",
|
||||
panels: [],
|
||||
panelsLoaded: false,
|
||||
@ -46,6 +48,8 @@ const handlers = {
|
||||
[APP_AT.SET_INITIALIZED]: state => ({ ...state, initialized: true }),
|
||||
//Установка текущего типового размера экрана
|
||||
[APP_AT.SET_DISPLAY_SIZE]: (state, { payload }) => ({ ...state, displaySize: payload }),
|
||||
//Установка заголовка в шапке приложения
|
||||
[APP_AT.SET_APP_BAR_TITLE]: (state, { payload }) => ({ ...state, appBarTitle: payload }),
|
||||
//Обработчик по умолчанию
|
||||
DEFAULT: state => state
|
||||
};
|
||||
|
@ -10,6 +10,7 @@
|
||||
import React, { createContext, useContext, useCallback } from "react"; //ReactJS
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { MessagingСtx } from "./messaging"; //Контекст сообщений
|
||||
import { formatErrorMessage } from "../core/utils"; //Вспомогательные функции
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
@ -63,7 +64,8 @@ export const BackEndContext = ({ client, children }) => {
|
||||
throwError = true,
|
||||
showErrorMessage = true,
|
||||
fullResponse = false,
|
||||
spreadOutArguments = true
|
||||
spreadOutArguments = true,
|
||||
signal = null
|
||||
} = {}) => {
|
||||
try {
|
||||
if (loader !== false) showLoader(loaderMessage);
|
||||
@ -75,12 +77,18 @@ export const BackEndContext = ({ client, children }) => {
|
||||
tagValueProcessor,
|
||||
attributeValueProcessor,
|
||||
throwError,
|
||||
spreadOutArguments
|
||||
spreadOutArguments,
|
||||
signal
|
||||
});
|
||||
if (fullResponse === true || isRespErr(result)) return result;
|
||||
else return result.XPAYLOAD;
|
||||
} catch (e) {
|
||||
if (showErrorMessage) showMsgErr(e.message);
|
||||
if (showErrorMessage) {
|
||||
//Разбираем текст ошибки
|
||||
let errMsg = formatErrorMessage(e.message);
|
||||
//Отображаем ошибку
|
||||
showMsgErr(errMsg.text, null, errMsg.fullErrorText);
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
if (loader !== false) hideLoader();
|
||||
|
@ -33,7 +33,9 @@ const MESSAGING_CONTEXT_TEXTS_SHAPE = PropTypes.shape({
|
||||
const MESSAGING_CONTEXT_BUTTONS_SHAPE = PropTypes.shape({
|
||||
CLOSE: PropTypes.string.isRequired,
|
||||
OK: PropTypes.string.isRequired,
|
||||
CANCEL: PropTypes.string.isRequired
|
||||
CANCEL: PropTypes.string.isRequired,
|
||||
DETAIL: PropTypes.string.isRequired,
|
||||
HIDE: PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
//----------------
|
||||
@ -56,12 +58,16 @@ export const MessagingContext = ({ titles, texts, buttons, children }) => {
|
||||
|
||||
//Отображение сообщения
|
||||
const showMsg = useCallback(
|
||||
(type, text, msgOnOk = null, msgOnCancel = null) => dispatch({ type: MSG_AT.SHOW_MSG, payload: { type, text, msgOnOk, msgOnCancel } }),
|
||||
(type, text, msgOnOk = null, msgOnCancel = null, fullErrorText = null) =>
|
||||
dispatch({ type: MSG_AT.SHOW_MSG, payload: { type, text, msgOnOk, msgOnCancel, fullErrorText } }),
|
||||
[]
|
||||
);
|
||||
|
||||
//Отображение сообщения - ошибка
|
||||
const showMsgErr = useCallback((text, msgOnOk = null) => showMsg(MSG_TYPE.ERR, text, msgOnOk), [showMsg]);
|
||||
const showMsgErr = useCallback(
|
||||
(text, msgOnOk = null, fullErrorText = null) => showMsg(MSG_TYPE.ERR, text, msgOnOk, null, fullErrorText),
|
||||
[showMsg]
|
||||
);
|
||||
|
||||
//Отображение сообщения - информация
|
||||
const showMsgInfo = useCallback((text, msgOnOk = null) => showMsg(MSG_TYPE.INFO, text, msgOnOk), [showMsg]);
|
||||
@ -126,6 +132,7 @@ export const MessagingContext = ({ titles, texts, buttons, children }) => {
|
||||
open={true}
|
||||
variant={state.msgType}
|
||||
text={state.msgText}
|
||||
fullErrorText={state.msgFullErrorText}
|
||||
title
|
||||
titleText={state.msgType == MSG_TYPE.ERR ? titles.ERR : state.msgType == MSG_TYPE.WARN ? titles.WARN : titles.INFO}
|
||||
okBtn={true}
|
||||
@ -134,6 +141,8 @@ export const MessagingContext = ({ titles, texts, buttons, children }) => {
|
||||
cancelBtn={state.msgType == MSG_TYPE.WARN}
|
||||
onCancel={handleMessageCancelClick}
|
||||
cancelBtnCaption={buttons.CANCEL}
|
||||
showErrMoreCaption={buttons.DETAIL}
|
||||
hideErrMoreCaption={buttons.HIDE}
|
||||
/>
|
||||
) : null}
|
||||
{children}
|
||||
|
@ -35,6 +35,7 @@ const INITIAL_STATE = {
|
||||
msg: false,
|
||||
msgType: MSG_TYPE.ERR,
|
||||
msgText: null,
|
||||
msgFullErrorText: null,
|
||||
msgOnOk: null,
|
||||
msgOnCancel: null
|
||||
};
|
||||
@ -59,6 +60,7 @@ const handlers = {
|
||||
msg: true,
|
||||
msgType: payload.type || MSG_TYPE.APP_ERR,
|
||||
msgText: payload.text,
|
||||
msgFullErrorText: payload.fullErrorText,
|
||||
msgOnOk: payload.msgOnOk,
|
||||
msgOnCancel: payload.msgOnCancel
|
||||
}),
|
||||
|
@ -41,7 +41,7 @@ export const NavigationContext = ({ children }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { findPanelByName } = useContext(ApplicationСtx);
|
||||
const { findPanelByName, setAppBarTitle } = useContext(ApplicationСtx);
|
||||
|
||||
//Проверка наличия параметров запроса
|
||||
const isNavigationSearch = () => (location.search ? true : false);
|
||||
@ -65,6 +65,8 @@ export const NavigationContext = ({ children }) => {
|
||||
const navigateTo = ({ path, search, state, replace = false }) => {
|
||||
//Если указано куда переходить
|
||||
if (path) {
|
||||
//Сброс кастомного заголовка
|
||||
setAppBarTitle("");
|
||||
//Переходим к адресу
|
||||
if (state) navigate(path, { state: JSON.stringify(state), replace });
|
||||
else navigate({ pathname: path, search: queryString.stringify(search), replace });
|
||||
|
@ -34,6 +34,7 @@ const ERR_APPSERVER = "Ошибка сервера приложений"; //Об
|
||||
const ERR_UNEXPECTED = "Неожиданный ответ сервера"; //Неожиданный ответ сервера
|
||||
const ERR_NETWORK = "Ошибка соединения с сервером"; //Ошибка сети
|
||||
const ERR_UNAUTH = "Сеанс завершен. Пройдите аутентификацию повторно."; //Ошибка аутентификации
|
||||
const ERR_ABORTED = "Запрос прерван принудительно";
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
@ -76,7 +77,16 @@ const getRespErrMessage = resp => (isRespErr(resp) && resp.SMESSAGE ? resp.SMESS
|
||||
const getRespPayload = resp => (resp && resp.XPAYLOAD ? resp.XPAYLOAD : null);
|
||||
|
||||
//Исполнение действия на сервере
|
||||
const executeAction = async ({ serverURL, action, payload = {}, isArray, transformTagName, tagValueProcessor, attributeValueProcessor } = {}) => {
|
||||
const executeAction = async ({
|
||||
serverURL,
|
||||
action,
|
||||
payload = {},
|
||||
isArray,
|
||||
transformTagName,
|
||||
tagValueProcessor,
|
||||
attributeValueProcessor,
|
||||
signal = null
|
||||
} = {}) => {
|
||||
console.log(`EXECUTING ${action ? action : ""} ON ${serverURL} WITH PAYLOAD:`);
|
||||
console.log(payload ? payload : "NO PAYLOAD");
|
||||
let response = null;
|
||||
@ -92,11 +102,14 @@ const executeAction = async ({ serverURL, action, payload = {}, isArray, transfo
|
||||
body: await buildXML(rqBody),
|
||||
headers: {
|
||||
"content-type": "application/xml"
|
||||
}
|
||||
},
|
||||
...(signal ? { signal } : {})
|
||||
});
|
||||
} catch (e) {
|
||||
//Прервано принудительно
|
||||
if (signal?.aborted === true) throw new Error(ERR_ABORTED);
|
||||
//Сетевая ошибка
|
||||
throw new Error(`${ERR_NETWORK}: ${e.message}`);
|
||||
else throw new Error(`${ERR_NETWORK}: ${e.message || "неопределённая ошибка"}`);
|
||||
}
|
||||
//Проверим на наличие ошибок HTTP - если есть вернём их
|
||||
if (!response.ok) throw new Error(`${ERR_APPSERVER}: ${response.statusText}`);
|
||||
@ -136,7 +149,8 @@ const executeStored = async ({
|
||||
tagValueProcessor,
|
||||
attributeValueProcessor,
|
||||
throwError = true,
|
||||
spreadOutArguments = false
|
||||
spreadOutArguments = false,
|
||||
signal = null
|
||||
} = {}) => {
|
||||
let res = null;
|
||||
try {
|
||||
@ -157,7 +171,8 @@ const executeStored = async ({
|
||||
payload: { SSTORED: stored, XARGUMENTS: serverArgs, SRESP_ARG: respArg },
|
||||
isArray,
|
||||
tagValueProcessor,
|
||||
attributeValueProcessor
|
||||
attributeValueProcessor,
|
||||
signal
|
||||
});
|
||||
if (spreadOutArguments === true && Array.isArray(res?.XPAYLOAD?.XOUT_ARGUMENTS)) {
|
||||
let spreadArgs = {};
|
||||
@ -193,6 +208,11 @@ const getConfig = async ({ throwError = true } = {}) => {
|
||||
//----------------
|
||||
|
||||
export default {
|
||||
ERR_APPSERVER,
|
||||
ERR_UNEXPECTED,
|
||||
ERR_NETWORK,
|
||||
ERR_UNAUTH,
|
||||
ERR_ABORTED,
|
||||
SERV_DATA_TYPE_STR,
|
||||
SERV_DATA_TYPE_NUMB,
|
||||
SERV_DATA_TYPE_DATE,
|
||||
|
@ -102,14 +102,17 @@ const getDisplaySize = () => {
|
||||
};
|
||||
|
||||
//Глубокое копирование объекта
|
||||
const deepCopyObject = obj => JSON.parse(JSON.stringify(obj));
|
||||
const deepCopyObject = obj => (structuredClone ? structuredClone(obj) : JSON.parse(JSON.stringify(obj)));
|
||||
|
||||
//Конвертация объекта в Base64 XML
|
||||
const object2Base64XML = (obj, builderOptions) => {
|
||||
const object2XML = (obj, builderOptions) => {
|
||||
const builder = new XMLBuilder(builderOptions);
|
||||
return btoa(unescape(encodeURIComponent(builder.build(obj))));
|
||||
return builder.build(obj);
|
||||
};
|
||||
|
||||
//Конвертация объекта в Base64 XML
|
||||
const object2Base64XML = (obj, builderOptions) => btoa(unescape(encodeURIComponent(object2XML(obj, builderOptions))));
|
||||
|
||||
//Конвертация XML в JSON
|
||||
const xml2JSON = ({ xmlDoc, isArray, transformTagName, tagValueProcessor, attributeValueProcessor, useDefaultPatterns = true }) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -158,6 +161,34 @@ const formatDateJSONDateOnly = value => (value ? dayjs(value).format("YYYY-MM-DD
|
||||
//Форматирование числа в "Денежном" формате РФ
|
||||
const formatNumberRFCurrency = value => (hasValue(value) ? new Intl.NumberFormat("ru-RU", { minimumFractionDigits: 2 }).format(value) : null);
|
||||
|
||||
//Форматирование текста ошибки
|
||||
const formatErrorMessage = errorMsg => {
|
||||
//Инициализируем текст заголовка ошибки
|
||||
let text = "";
|
||||
//Пробуем извлечь заголовок текста ошибки
|
||||
try {
|
||||
//Если это ошибка Oracle
|
||||
if (errorMsg.match(/^ORA-/)) {
|
||||
//Считываем первую строку с заголовочным текстом ошибки
|
||||
text = errorMsg.match(/^.*(?=(\nORA-))/)[0];
|
||||
//Убираем лишнюю информацию и пробелы
|
||||
text = text.replace(/ORA-\d*:/g, "").trim();
|
||||
}
|
||||
//Если это ошибка PG
|
||||
if (errorMsg.match(/^SQL Error/)) {
|
||||
//Считываем первую строку с заголовочным текстом ошибки
|
||||
text = errorMsg.match(/.*(?=(\n.*Where)|(.*Where))/)[0];
|
||||
//Убираем лишнюю информацию и пробелы
|
||||
text = text.replace(/SQL Error \[\d*\]: ERROR:/g, "").trim();
|
||||
}
|
||||
} catch {
|
||||
//Если произошла ошибка - оставляем полный текст ошибки
|
||||
text = errorMsg;
|
||||
}
|
||||
//Возвращаем результат
|
||||
return { text: text || errorMsg, fullErrorText: text ? errorMsg : null };
|
||||
};
|
||||
|
||||
//Формирование уникального идентификатора
|
||||
const genGUID = () =>
|
||||
"10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
|
||||
@ -172,11 +203,13 @@ export {
|
||||
hasValue,
|
||||
getDisplaySize,
|
||||
deepCopyObject,
|
||||
object2XML,
|
||||
object2Base64XML,
|
||||
xml2JSON,
|
||||
formatDateRF,
|
||||
formatDateTimeRF,
|
||||
formatDateJSONDateOnly,
|
||||
formatNumberRFCurrency,
|
||||
formatErrorMessage,
|
||||
genGUID
|
||||
};
|
||||
|
281
app/panels/mech_rec_cost_jobs_manage_mp/hooks.js
Normal file
281
app/panels/mech_rec_cost_jobs_manage_mp/hooks.js
Normal file
@ -0,0 +1,281 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Выдача сменного задания на участок
|
||||
Кастомные хуки
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { useState, useCallback, useEffect, useContext } from "react"; //Классы React
|
||||
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
|
||||
import { NavigationCtx } from "../../context/navigation"; //Контекст навигации
|
||||
import { object2Base64XML } from "../../core/utils"; //Вспомогательные функции
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Размер страницы данных
|
||||
const DATA_GRID_PAGE_SIZE = 50;
|
||||
|
||||
//---------------------------------------------
|
||||
//Вспомогательные функции форматирования данных
|
||||
//---------------------------------------------
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Хук для основной таблицы
|
||||
const useCostJobs = () => {
|
||||
//Собственное состояние - таблица данных
|
||||
const [state, setState] = useState({
|
||||
init: false,
|
||||
loaded: false,
|
||||
jobInfo: {},
|
||||
haveNote: false,
|
||||
coeff: "1.0"
|
||||
});
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored } = useContext(BackEndСtx);
|
||||
//Подключение к контексту навигации
|
||||
const { getNavigationSearch } = useContext(NavigationCtx);
|
||||
|
||||
//При подключении компонента к странице
|
||||
useEffect(() => {
|
||||
const initJob = async fcJob => {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_MECHREC.FCJOBS_MP_INIT",
|
||||
args: { NFCJOBS: parseInt(fcJob) },
|
||||
respArg: "COUT",
|
||||
attributeValueProcessor: (name, val) => (["NHAVE_NOTE"].includes(name) ? val == 1 : val)
|
||||
});
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
init: true,
|
||||
jobInfo: data.XFCJOBS ? data.XFCJOBS : {},
|
||||
loaded: true
|
||||
}));
|
||||
};
|
||||
if (!state.init) {
|
||||
//Считаем параметры, переданные из действия
|
||||
const actionPrms = getNavigationSearch();
|
||||
//Иницализируем сменное задание
|
||||
initJob(actionPrms.NRN);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return [state, setState];
|
||||
};
|
||||
|
||||
//Хук для таблицы операций
|
||||
const useCostJobsSpecs = task => {
|
||||
//Собственное состояние - таблица данных
|
||||
const [costJobsSpecs, setCostJobsSpecs] = useState({
|
||||
task: null,
|
||||
dataLoaded: false,
|
||||
columnsDef: [],
|
||||
orders: null,
|
||||
rows: [],
|
||||
selectedRow: {},
|
||||
reload: true,
|
||||
pageNumber: 1,
|
||||
morePages: true,
|
||||
fixedHeader: false,
|
||||
fixedColumns: 0
|
||||
});
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
|
||||
|
||||
//Выдача задания
|
||||
const issueCostJobsSpecs = useCallback(
|
||||
async prms => {
|
||||
try {
|
||||
await executeStored({
|
||||
stored: "PKG_P8PANELS_MECHREC.FCJOBSSP_MP_ISSUE",
|
||||
args: { NFCJOBS: prms.NFCJOBS, NCOEFF: parseFloat(prms.NCOEFF) }
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
},
|
||||
[executeStored]
|
||||
);
|
||||
|
||||
//При необходимости обновить данные таблицы
|
||||
useEffect(() => {
|
||||
//Если изменилось сменное задание - обновляем состояние
|
||||
if (costJobsSpecs.dataLoaded && costJobsSpecs.task !== task) {
|
||||
setCostJobsSpecs(pv => ({
|
||||
...pv,
|
||||
dataLoaded: false,
|
||||
columnsDef: [],
|
||||
orders: null,
|
||||
rows: [],
|
||||
selectedRow: {},
|
||||
reload: true,
|
||||
pageNumber: 1,
|
||||
morePages: true
|
||||
}));
|
||||
}
|
||||
//Если необходимо перезагрузить
|
||||
if (costJobsSpecs.reload && task) {
|
||||
const loadData = async () => {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_MECHREC.FCJOBSSP_MP_DG_GET",
|
||||
args: {
|
||||
NFCJOBS: task,
|
||||
NPAGE_NUMBER: costJobsSpecs.pageNumber,
|
||||
NPAGE_SIZE: DATA_GRID_PAGE_SIZE,
|
||||
CORDERS: { VALUE: object2Base64XML(costJobsSpecs.orders, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
|
||||
NINCLUDE_DEF: costJobsSpecs.dataLoaded ? 0 : 1
|
||||
},
|
||||
respArg: "COUT",
|
||||
attributeValueProcessor: (name, val) =>
|
||||
name === "NSELECT" ? val === 1 : name === "SWORKERS_LIST" ? (val ? val.split(",").map(Number) : []) : val
|
||||
});
|
||||
setCostJobsSpecs(pv => ({
|
||||
...pv,
|
||||
...data.XDATA_GRID,
|
||||
task: task,
|
||||
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
|
||||
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
|
||||
dataLoaded: true,
|
||||
reload: false,
|
||||
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
|
||||
}));
|
||||
};
|
||||
loadData();
|
||||
}
|
||||
}, [
|
||||
SERV_DATA_TYPE_CLOB,
|
||||
costJobsSpecs.dataLoaded,
|
||||
costJobsSpecs.orders,
|
||||
costJobsSpecs.pageNumber,
|
||||
costJobsSpecs.reload,
|
||||
costJobsSpecs.task,
|
||||
executeStored,
|
||||
task
|
||||
]);
|
||||
|
||||
return [costJobsSpecs, setCostJobsSpecs, issueCostJobsSpecs];
|
||||
};
|
||||
|
||||
//Хук для рабочих
|
||||
const useCostJobsWorkers = task => {
|
||||
//Собственное состояние - таблица данных
|
||||
const [costJobsWorkers, setCostJobsWorkers] = useState({
|
||||
task: null,
|
||||
dataLoaded: false,
|
||||
columnsDef: [],
|
||||
orders: null,
|
||||
rows: [],
|
||||
selectedRows: [],
|
||||
reload: true,
|
||||
pageNumber: 1,
|
||||
morePages: true
|
||||
});
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
|
||||
|
||||
//Включение рабочего в строку сменного задания
|
||||
const includeWorker = useCallback(
|
||||
async prms => {
|
||||
try {
|
||||
await executeStored({
|
||||
stored: "PKG_P8PANELS_MECHREC.FCJOBSSP_MP_INC_PERFORM",
|
||||
args: {
|
||||
NFCJOBSSP: prms.NFCJOBSSP,
|
||||
SPERFORM_LIST: {
|
||||
VALUE: Array.isArray(prms.SELECTED_WORKERS) ? prms.SELECTED_WORKERS.join(";") : prms.SELECTED_WORKERS,
|
||||
SDATA_TYPE: SERV_DATA_TYPE_CLOB
|
||||
},
|
||||
NQUANT_PLAN: prms.NQUANT_PLAN
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
},
|
||||
[SERV_DATA_TYPE_CLOB, executeStored]
|
||||
);
|
||||
|
||||
//Исключение рабочего из строки сменного задания
|
||||
const excludeWorker = useCallback(
|
||||
async prms => {
|
||||
try {
|
||||
await executeStored({
|
||||
stored: "PKG_P8PANELS_MECHREC.FCJOBSSP_MP_EXC_PERFORM",
|
||||
args: { NFCJOBSSP: prms.NFCJOBSSP }
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
},
|
||||
[executeStored]
|
||||
);
|
||||
|
||||
//При необходимости обновить данные таблицы
|
||||
useEffect(() => {
|
||||
//Если изменилось сменное задание - обновляем состояние
|
||||
if (costJobsWorkers.dataLoaded && costJobsWorkers.task !== task) {
|
||||
setCostJobsWorkers(pv => ({
|
||||
...pv,
|
||||
dataLoaded: false,
|
||||
columnsDef: [],
|
||||
orders: null,
|
||||
rows: [],
|
||||
selectedRows: [],
|
||||
reload: true,
|
||||
pageNumber: 1,
|
||||
morePages: true
|
||||
}));
|
||||
}
|
||||
//Если необходимо перезагрузить
|
||||
if (costJobsWorkers.reload && task) {
|
||||
const loadData = async () => {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_MECHREC.WORKERS_MP_DG_GET",
|
||||
args: {
|
||||
NFCJOBS: task,
|
||||
NPAGE_NUMBER: costJobsWorkers.pageNumber,
|
||||
NPAGE_SIZE: DATA_GRID_PAGE_SIZE,
|
||||
CORDERS: { VALUE: object2Base64XML(costJobsWorkers.orders, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
|
||||
NINCLUDE_DEF: costJobsWorkers.dataLoaded ? 0 : 1
|
||||
},
|
||||
respArg: "COUT",
|
||||
attributeValueProcessor: (name, val) => (["NSELECT"].includes(name) ? val === 1 : val)
|
||||
});
|
||||
setCostJobsWorkers(pv => ({
|
||||
...pv,
|
||||
...data.XDATA_GRID,
|
||||
task: task,
|
||||
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
|
||||
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
|
||||
dataLoaded: true,
|
||||
reload: false,
|
||||
morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
|
||||
}));
|
||||
};
|
||||
loadData();
|
||||
}
|
||||
}, [
|
||||
SERV_DATA_TYPE_CLOB,
|
||||
costJobsWorkers.dataLoaded,
|
||||
costJobsWorkers.orders,
|
||||
costJobsWorkers.pageNumber,
|
||||
costJobsWorkers.reload,
|
||||
costJobsWorkers.task,
|
||||
executeStored,
|
||||
task
|
||||
]);
|
||||
|
||||
return [costJobsWorkers, setCostJobsWorkers, includeWorker, excludeWorker];
|
||||
};
|
||||
|
||||
export { useCostJobs, useCostJobsSpecs, useCostJobsWorkers };
|
16
app/panels/mech_rec_cost_jobs_manage_mp/index.js
Normal file
16
app/panels/mech_rec_cost_jobs_manage_mp/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Выдача сменного задания на участок
|
||||
Панель мониторинга: Точка входа
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { MechRecCostJobs } from "./mech_rec_cost_jobs_manage_mp"; //Корневая панель выдачи сменного задания на участок
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export const RootClass = MechRecCostJobs;
|
@ -0,0 +1,484 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Выдача сменного задания на участок
|
||||
Панель мониторинга: Корневая панель выдачи сменного задания на участок
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState } from "react"; //Классы React
|
||||
import { Grid, Box, Typography, Checkbox, Icon, Stack, Button, Tooltip, TextField } from "@mui/material"; //Интерфейсные элементы
|
||||
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
|
||||
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
|
||||
import { P8PDataGrid, P8P_DATA_GRID_SIZE, P8P_DATA_GRID_MORE_HEIGHT } from "../../components/p8p_data_grid"; //Таблица данных
|
||||
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
|
||||
import { useCostJobs, useCostJobsSpecs, useCostJobsWorkers } from "./hooks"; //Вспомогательные хуки
|
||||
import { CostJobsSpecsInclude } from "./worker_include_dialog"; //Компонент диалога включения в задание
|
||||
import { hasValue } from "../../core/utils"; //Вспомогательные функции
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Мнемокод раздела операций
|
||||
const UNIT_COST_JOBS_SPECS = "CostJobsSpecs";
|
||||
|
||||
//Мнемокод раздела исполнений должности
|
||||
const UNIT_WORKERS = "ClientPostPerform";
|
||||
|
||||
//Высота основного заголовка
|
||||
const MAIN_HEADER_HEIGHT = "35px";
|
||||
|
||||
//Высота подзаголовка
|
||||
const SUB_HEADER_HEIGHT = "35px";
|
||||
|
||||
//Высота заголовка таблицы
|
||||
const TABLE_HEADER_HEIGHT = "35px";
|
||||
|
||||
//Высота панели кнопок таблицы
|
||||
const TABLE_BUTTONS_HEIGHT = "35px";
|
||||
|
||||
//Отступ таблицы
|
||||
const TABLE_PADDING_TOP = "15px";
|
||||
|
||||
//Формат для коэффициент выполнения норм
|
||||
const issueCoeffFormat = /^(?!.*\..*\.)[0-9]{0,3}(\.[0-9]{0,1})?$/;
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
MAIN_HEADER: { height: MAIN_HEADER_HEIGHT, overflow: "hidden" },
|
||||
SUB_HEADER: { height: SUB_HEADER_HEIGHT, overflow: "hidden" },
|
||||
CONTAINER: { textAlign: "center" },
|
||||
TABLE: { paddingTop: TABLE_PADDING_TOP },
|
||||
TABLE_HEADER: { height: TABLE_HEADER_HEIGHT, overflow: "hidden" },
|
||||
TABLE_BUTTONS: { display: "flex", justifyContent: "flex-end", height: TABLE_BUTTONS_HEIGHT, overflow: "hidden", alignItems: "flex-end" },
|
||||
DATA_GRID_CONTAINER: morePages => ({
|
||||
height: `calc(100vh - ${APP_BAR_HEIGHT} - ${MAIN_HEADER_HEIGHT} - ${SUB_HEADER_HEIGHT} - ${TABLE_HEADER_HEIGHT} - ${TABLE_BUTTONS_HEIGHT} - ${TABLE_PADDING_TOP} - 32px - ${
|
||||
morePages ? P8P_DATA_GRID_MORE_HEIGHT : "0px"
|
||||
})`,
|
||||
...APP_STYLES.SCROLL
|
||||
})
|
||||
};
|
||||
|
||||
//Цвета
|
||||
const colors = {
|
||||
LINKED: "#bce0de",
|
||||
UNAVAILABLE: "#949494",
|
||||
WITH_WORKER: "#82df83"
|
||||
};
|
||||
|
||||
//------------------------------------
|
||||
//Вспомогательные функции и компоненты
|
||||
//------------------------------------
|
||||
|
||||
//Проверка правильности значения коэффициент выполнения норм
|
||||
const isValidIssueCoeff = value => {
|
||||
return issueCoeffFormat.test(value);
|
||||
};
|
||||
|
||||
//Форматирование значения ячейки
|
||||
const dataCellRender = ({ row, columnDef, handleSelectChange, sUnit, selectedWorkerRows = [], selectedJobSpec }) => {
|
||||
//Стиль
|
||||
let cellStyle = {};
|
||||
//Если это рабочие
|
||||
if (sUnit === UNIT_WORKERS) {
|
||||
//Признак недоступности
|
||||
let disabled = true;
|
||||
//Если в выбранной строке смены указан исполнитель факт
|
||||
if (selectedJobSpec.NPERFORM_FACT) {
|
||||
//Если это текущей исполнитель
|
||||
if (selectedJobSpec.SWORKERS_LIST.includes(row["NRN"])) {
|
||||
//Подсвечиваем строку рабочего
|
||||
cellStyle = { backgroundColor: colors.LINKED };
|
||||
}
|
||||
} else {
|
||||
//Если выбрана строка смены
|
||||
if (selectedJobSpec.NRN) {
|
||||
//Если текущий рабочий может принять задание
|
||||
if (row["NLOADING"] < 100) {
|
||||
//Подсвечиваем строку рабочего
|
||||
cellStyle = { backgroundColor: colors.LINKED };
|
||||
disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
//Если уже выбрано достаточное количество рабочих и текущий рабочий не отмечен
|
||||
if (selectedJobSpec.NRESOURCE_NUMB === selectedWorkerRows.length && !selectedWorkerRows.includes(row["NRN"])) {
|
||||
//Устанавливаем признак недоступности
|
||||
disabled = true;
|
||||
}
|
||||
//Если загрузка рабочего больше 100
|
||||
if (row["NLOADING"] >= 100) {
|
||||
//Если поле не поле выбора
|
||||
if (columnDef.name !== "NSELECT") {
|
||||
//Указываем, что рабочее место недоступно
|
||||
cellStyle = { ...cellStyle, color: colors.UNAVAILABLE };
|
||||
}
|
||||
}
|
||||
//Для колонки выбора
|
||||
if (columnDef.name === "NSELECT") {
|
||||
return {
|
||||
cellStyle,
|
||||
data: (
|
||||
<Box sx={STYLES.CONTAINER}>
|
||||
<Checkbox
|
||||
disabled={disabled}
|
||||
checked={selectedWorkerRows.includes(row["NRN"])}
|
||||
onChange={() => handleSelectChange({ NRN: row["NRN"], SUNIT: sUnit, BFULL_LOADED: row["NLOADING"] >= 100 })}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
};
|
||||
}
|
||||
//Отформатированная колонка
|
||||
return {
|
||||
cellStyle,
|
||||
data: row[columnDef.name]
|
||||
};
|
||||
}
|
||||
//Если это сменное задание
|
||||
if (sUnit === UNIT_COST_JOBS_SPECS) {
|
||||
//Если указан исполнитель факт
|
||||
if (row["NPERFORM_FACT"]) {
|
||||
//Подсвечиваем сменное задание зеленым
|
||||
cellStyle = { ...cellStyle, backgroundColor: colors.WITH_WORKER };
|
||||
}
|
||||
//Для колонки выбора
|
||||
if (columnDef.name === "NSELECT") {
|
||||
return {
|
||||
cellStyle,
|
||||
data: (
|
||||
<Box sx={STYLES.CONTAINER}>
|
||||
<Checkbox
|
||||
disabled={row["DBEG_FACT"] ? true : false}
|
||||
checked={row["NRN"] === selectedJobSpec.NRN}
|
||||
onChange={() =>
|
||||
handleSelectChange({
|
||||
NRN: row["NRN"],
|
||||
SUNIT: sUnit,
|
||||
NPERFORM_FACT: row["NPERFORM_FACT"],
|
||||
NRESOURCE_NUMB: row["NRESOURCE_NUMB"],
|
||||
NQUANT_PLAN: row["NQUANT_PLAN"],
|
||||
SWORKERS_LIST: row["SWORKERS_LIST"]
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
};
|
||||
}
|
||||
//Отформатированная колонка
|
||||
return {
|
||||
cellStyle,
|
||||
data: row[columnDef.name]
|
||||
};
|
||||
}
|
||||
//Необрабатываемый раздел
|
||||
return {
|
||||
data: row[columnDef.name]
|
||||
};
|
||||
};
|
||||
|
||||
//Генерация представления ячейки заголовка группы
|
||||
export const headCellRender = ({ columnDef }) => {
|
||||
if (columnDef.name === "NSELECT") {
|
||||
return {
|
||||
stackStyle: { padding: "2px", justifyContent: "space-around" },
|
||||
data: <Icon>done</Icon>
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
stackStyle: { padding: "2px" },
|
||||
data: columnDef.caption
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Корневая панель выдачи сменного задания на участок
|
||||
const MechRecCostJobs = () => {
|
||||
//Состояние диалога включения в задание
|
||||
const [showInclude, setShowInclude] = useState(false);
|
||||
|
||||
//Состояние информации о сменном задании
|
||||
const [state, setState] = useCostJobs();
|
||||
|
||||
//Состояние таблицы сменных заданий
|
||||
const [costJobsSpecs, setCostJobsSpecs, issueCostJobsSpecs] = useCostJobsSpecs(state.jobInfo.NRN);
|
||||
|
||||
//Состояние таблицы рабочих
|
||||
const [costJobsWorkers, setCostJobsWorkers, includeWorker, excludeWorker] = useCostJobsWorkers(state.jobInfo.NRN);
|
||||
|
||||
//При изменении состояния сортировки операций
|
||||
const handleCostJobsSpecOrderChanged = ({ orders }) => setCostJobsSpecs(pv => ({ ...pv, orders: [...orders], pageNumber: 1, reload: true }));
|
||||
|
||||
//При изменении количества отображаемых страниц операций
|
||||
const handleCostJobsSpecPagesCountChanged = () => setCostJobsSpecs(pv => ({ ...pv, pageNumber: pv.pageNumber + 1, reload: true }));
|
||||
|
||||
//При изменении состояния сортировки рабочих
|
||||
const handleCostJobsWorkersOrderChanged = ({ orders }) => setCostJobsWorkers(pv => ({ ...pv, orders: [...orders], pageNumber: 1, reload: true }));
|
||||
|
||||
//При изменении количества отображаемых страниц рабочих
|
||||
const handleCostJobsWorkersPagesCountChanged = () => setCostJobsWorkers(pv => ({ ...pv, pageNumber: pv.pageNumber + 1, reload: true }));
|
||||
|
||||
//При исключении рабочих из строки сменного задания
|
||||
const handleCostJobsSpecExcludeWorker = () => {
|
||||
//Делаем асинхронно, чтобы при ошибке ничего не обновлять
|
||||
const excludeAsync = async () => {
|
||||
//Исключаем рабочего из строки сменного задания
|
||||
try {
|
||||
await excludeWorker({
|
||||
NFCJOBSSP: costJobsSpecs.selectedRow.NRN
|
||||
});
|
||||
//Необходимо обновить данные
|
||||
setCostJobsSpecs(pv => ({ ...pv, selectedRow: {}, pageNumber: 1, reload: true }));
|
||||
setCostJobsWorkers(pv => ({ ...pv, selectedRows: [], pageNumber: 1, reload: true }));
|
||||
} catch (e) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
};
|
||||
//Исключаем рабочего асинхронно
|
||||
excludeAsync();
|
||||
};
|
||||
|
||||
//Выдача задания операции
|
||||
const handleCostJobsSpecIssue = () => {
|
||||
//Делаем асинхронно, чтобы при ошибке ничего не обновлять
|
||||
const issueAsync = async () => {
|
||||
//Включаем рабочих в операции
|
||||
try {
|
||||
await issueCostJobsSpecs({ NFCJOBS: state.jobInfo.NRN, NCOEFF: state.coeff });
|
||||
//Необходимо обновить данные
|
||||
setCostJobsSpecs(pv => ({ ...pv, selectedRow: {}, pageNumber: 1, reload: true }));
|
||||
setCostJobsWorkers(pv => ({ ...pv, selectedRows: [], pageNumber: 1, reload: true }));
|
||||
} catch (e) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
};
|
||||
//Выдаем задание асинхронно
|
||||
issueAsync();
|
||||
};
|
||||
|
||||
//При изменение состояния выбора
|
||||
const handleSelectChange = prms => {
|
||||
//Выбранный элемент
|
||||
let selectedRow = null;
|
||||
//Буфер для выбранных рабочих
|
||||
let selectedWorkers = [];
|
||||
//Индекс рабочего в списке выбранных
|
||||
let workerIndex = null;
|
||||
//Исходим от раздела
|
||||
switch (prms.SUNIT) {
|
||||
//Сменное задание
|
||||
case UNIT_COST_JOBS_SPECS:
|
||||
//Определяем это новое отмеченное сменное задание или сброс старого
|
||||
selectedRow = costJobsSpecs.selectedRow.NRN ? (costJobsSpecs.selectedRow.NRN === prms.NRN ? null : prms.NRN) : prms.NRN;
|
||||
//Актуализируем строки
|
||||
setCostJobsSpecs(pv => ({
|
||||
...pv,
|
||||
selectedRow: selectedRow
|
||||
? {
|
||||
NRN: selectedRow,
|
||||
NPERFORM_FACT: prms.NPERFORM_FACT,
|
||||
NRESOURCE_NUMB: prms.NRESOURCE_NUMB,
|
||||
NQUANT_PLAN: prms.NQUANT_PLAN,
|
||||
SWORKERS_LIST: prms.SWORKERS_LIST
|
||||
}
|
||||
: { NRN: null, NPERFORM_FACT: null, NRESOURCE_NUMB: null, NQUANT_PLAN: null, SWORKERS_LIST: [] }
|
||||
}));
|
||||
//Выходим
|
||||
break;
|
||||
//Рабочие центры
|
||||
case UNIT_WORKERS:
|
||||
//Инициализируем рабочими центрами
|
||||
selectedWorkers = costJobsWorkers.selectedRows || [];
|
||||
//Определяем индекс элемента в массиве
|
||||
workerIndex = selectedWorkers.indexOf(prms.NRN);
|
||||
//Если такого рег. номера нет в списке - добавляем, иначе удаляем
|
||||
workerIndex > -1 ? selectedWorkers.splice(workerIndex, 1) : selectedWorkers.push(prms.NRN);
|
||||
//Актуализируем строки
|
||||
setCostJobsWorkers(pv => ({ ...pv, selectedRows: selectedWorkers }));
|
||||
//Выходим
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
//При открытии/закрытии диалога добавления
|
||||
const handleShowIncludeChange = needShow => setShowInclude(needShow);
|
||||
|
||||
//При изменении коэффициент выполнения норм
|
||||
const handleIssueCoeffChange = e => {
|
||||
isValidIssueCoeff(e.target.value) ? setState(pv => ({ ...pv, coeff: e.target.value })) : null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box p={2}>
|
||||
{state.loaded ? (
|
||||
<Box sx={STYLES.CONTAINER}>
|
||||
<Typography
|
||||
sx={STYLES.MAIN_HEADER}
|
||||
variant={"h6"}
|
||||
>{`Сменное задание №${state.jobInfo.SDOC_NUMB} на ${state.jobInfo.SPERIOD}`}</Typography>
|
||||
<Typography sx={STYLES.SUB_HEADER} variant={"h6"}>{`${state.jobInfo.SSUBDIV}`}</Typography>
|
||||
<Box sx={STYLES.CONTAINER}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item sx={STYLES.CONTAINER} xs={6}>
|
||||
<Typography sx={STYLES.TABLE_HEADER} variant={"h6"} color={"text.secondary"}>
|
||||
Сменное задание
|
||||
</Typography>
|
||||
{costJobsWorkers.dataLoaded ? (
|
||||
<>
|
||||
<Box sx={STYLES.TABLE_BUTTONS}>
|
||||
<Stack direction={"row"} spacing={1}>
|
||||
<Tooltip
|
||||
title={
|
||||
state.jobInfo.NHAVE_NOTE
|
||||
? "Сменное задание имеет строку с примечанием"
|
||||
: "Коэффициент выполнения норм"
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
name="editIssueValue"
|
||||
variant="outlined"
|
||||
sx={{ width: "68px" }}
|
||||
inputProps={{ sx: { padding: "4.2px 14px" } }}
|
||||
size="small"
|
||||
value={state.coeff}
|
||||
onChange={handleIssueCoeffChange}
|
||||
disabled={state.jobInfo.NHAVE_NOTE}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={
|
||||
state.jobInfo.NHAVE_NOTE
|
||||
? "Сменное задание имеет строку с примечанием"
|
||||
: !hasValue(state.coeff)
|
||||
? "Не указано значение коэффициент выполнения норм"
|
||||
: null
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={state.jobInfo.NHAVE_NOTE || !hasValue(state.coeff)}
|
||||
onClick={handleCostJobsSpecIssue}
|
||||
>
|
||||
Выдать задания
|
||||
</Button>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box sx={STYLES.TABLE}>
|
||||
<P8PDataGrid
|
||||
{...P8P_DATA_GRID_CONFIG_PROPS}
|
||||
containerComponentProps={{ sx: STYLES.DATA_GRID_CONTAINER(costJobsSpecs.morePages), elevation: 4 }}
|
||||
columnsDef={costJobsSpecs.columnsDef}
|
||||
rows={costJobsSpecs.rows}
|
||||
size={P8P_DATA_GRID_SIZE.SMALL}
|
||||
morePages={costJobsSpecs.morePages}
|
||||
reloading={costJobsSpecs.reload}
|
||||
onOrderChanged={handleCostJobsSpecOrderChanged}
|
||||
onPagesCountChanged={handleCostJobsSpecPagesCountChanged}
|
||||
dataCellRender={prms =>
|
||||
dataCellRender({
|
||||
...prms,
|
||||
handleSelectChange,
|
||||
sUnit: UNIT_COST_JOBS_SPECS,
|
||||
selectedJobSpec: costJobsSpecs.selectedRow
|
||||
})
|
||||
}
|
||||
headCellRender={prms => headCellRender({ ...prms })}
|
||||
fixedHeader={costJobsSpecs.fixedHeader}
|
||||
fixedColumns={costJobsSpecs.fixedColumns}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
) : null}
|
||||
</Grid>
|
||||
<Grid item sx={STYLES.CONTAINER} xs={6}>
|
||||
<Typography sx={STYLES.TABLE_HEADER} variant={"h6"} color={"text.secondary"}>
|
||||
Рабочие
|
||||
</Typography>
|
||||
{costJobsWorkers.dataLoaded ? (
|
||||
<>
|
||||
<Box sx={STYLES.TABLE_BUTTONS}>
|
||||
<Stack direction={"row"} spacing={1}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={!(costJobsSpecs.selectedRow.NRESOURCE_NUMB === costJobsWorkers.selectedRows.length)}
|
||||
onClick={() => handleShowIncludeChange(true)}
|
||||
>
|
||||
Включить в задание
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={!costJobsSpecs.selectedRow.NRN || !costJobsSpecs.selectedRow.NPERFORM_FACT}
|
||||
onClick={handleCostJobsSpecExcludeWorker}
|
||||
>
|
||||
Исключить из задания
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box sx={STYLES.TABLE}>
|
||||
<P8PDataGrid
|
||||
{...P8P_DATA_GRID_CONFIG_PROPS}
|
||||
containerComponentProps={{ sx: STYLES.DATA_GRID_CONTAINER(costJobsWorkers.morePages), elevation: 4 }}
|
||||
columnsDef={costJobsWorkers.columnsDef}
|
||||
rows={costJobsWorkers.rows}
|
||||
size={P8P_DATA_GRID_SIZE.SMALL}
|
||||
morePages={costJobsWorkers.morePages}
|
||||
reloading={costJobsWorkers.reload}
|
||||
onOrderChanged={handleCostJobsWorkersOrderChanged}
|
||||
onPagesCountChanged={handleCostJobsWorkersPagesCountChanged}
|
||||
dataCellRender={prms =>
|
||||
dataCellRender({
|
||||
...prms,
|
||||
handleSelectChange,
|
||||
sUnit: UNIT_WORKERS,
|
||||
selectedWorkerRows: costJobsWorkers.selectedRows,
|
||||
selectedJobSpec: costJobsSpecs.selectedRow
|
||||
})
|
||||
}
|
||||
headCellRender={prms => headCellRender({ ...prms })}
|
||||
fixedHeader={true}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
) : null}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
{showInclude ? (
|
||||
<CostJobsSpecsInclude
|
||||
includePrms={{
|
||||
NFCJOBSSP: costJobsSpecs.selectedRow.NRN,
|
||||
SELECTED_WORKERS: costJobsWorkers.selectedRows,
|
||||
NQUANT_PLAN: costJobsSpecs.selectedRow.NQUANT_PLAN
|
||||
}}
|
||||
setShowInclude={setShowInclude}
|
||||
setCostJobsSpecs={setCostJobsSpecs}
|
||||
setCostJobsWorkers={setCostJobsWorkers}
|
||||
includeWorker={includeWorker}
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { MechRecCostJobs };
|
103
app/panels/mech_rec_cost_jobs_manage_mp/worker_include_dialog.js
Normal file
103
app/panels/mech_rec_cost_jobs_manage_mp/worker_include_dialog.js
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Выдача сменного задания на участок
|
||||
Панель мониторинга: Диалог включения рабочего в сменное задание
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Box, Button, Dialog, DialogTitle, DialogContent, TextField, DialogActions } from "@mui/material"; //Интерфейсные элементы
|
||||
import { BUTTONS } from "../../../app.text"; //Текстовые ресурсы
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Диалог включения рабочего в сменное задание
|
||||
const CostJobsSpecsInclude = ({ includePrms, setShowInclude, setCostJobsSpecs, setCostJobsWorkers, includeWorker }) => {
|
||||
//Собственное состояние - Значение приоритета
|
||||
const [state, setState] = useState(includePrms.NQUANT_PLAN);
|
||||
|
||||
//При закрытии включения рабочего
|
||||
const handlePriorEditClose = () => setShowInclude(false);
|
||||
|
||||
//При включении рабочего в строку сменного задания
|
||||
const costJobsSpecIncludeCostEquipment = () => {
|
||||
//Делаем асинхронно, чтобы при ошибке ничего не обновлять
|
||||
const includeAsync = async () => {
|
||||
//Включаем рабочего в строку сменного задания
|
||||
try {
|
||||
await includeWorker({
|
||||
NFCJOBSSP: includePrms.NFCJOBSSP,
|
||||
SELECTED_WORKERS: includePrms.SELECTED_WORKERS,
|
||||
NQUANT_PLAN: state
|
||||
});
|
||||
//Необходимо обновить все данные
|
||||
setCostJobsSpecs(pv => ({ ...pv, selectedRow: {}, pageNumber: 1, reload: true }));
|
||||
setCostJobsWorkers(pv => ({ ...pv, selectedRows: [], pageNumber: 1, reload: true }));
|
||||
handlePriorEditClose();
|
||||
} catch (e) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
};
|
||||
//Включаем рабочего асинхронно
|
||||
includeAsync();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open onClose={() => handlePriorEditClose()}>
|
||||
<DialogTitle>Включить в задание</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box>
|
||||
<TextField
|
||||
name="editInculdeValue"
|
||||
label="Количество"
|
||||
variant="standard"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
type: "number",
|
||||
inputProps: {
|
||||
max: includePrms.NQUANT_PLAN,
|
||||
min: 0
|
||||
}
|
||||
}}
|
||||
value={state}
|
||||
onChange={event => {
|
||||
var value = parseInt(event.target.value, 10);
|
||||
if (value > includePrms.NQUANT_PLAN) {
|
||||
value = includePrms.NQUANT_PLAN;
|
||||
}
|
||||
if (value < 0) {
|
||||
value = 0;
|
||||
}
|
||||
setState(value);
|
||||
}}
|
||||
/>
|
||||
<Box></Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={costJobsSpecIncludeCostEquipment}>{BUTTONS.OK}</Button>
|
||||
<Button onClick={() => handlePriorEditClose(null)}>{BUTTONS.CANCEL}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог включения рабочего в сменное задание
|
||||
CostJobsSpecsInclude.propTypes = {
|
||||
includePrms: PropTypes.object.isRequired,
|
||||
setShowInclude: PropTypes.func.isRequired,
|
||||
setCostJobsSpecs: PropTypes.func.isRequired,
|
||||
setCostJobsWorkers: PropTypes.func.isRequired,
|
||||
includeWorker: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { CostJobsSpecsInclude };
|
@ -48,7 +48,8 @@ const useCostRouteLists = (task, taskType) => {
|
||||
NPAGE_SIZE: DATA_GRID_PAGE_SIZE,
|
||||
NINCLUDE_DEF: costRouteLists.dataLoaded ? 0 : 1
|
||||
},
|
||||
attributeValueProcessor: (name, val) => (["DEXEC_DATE", "DREL_DATE"].includes(name) ? formatDateRF(val) : val),
|
||||
attributeValueProcessor: (name, val) =>
|
||||
["DEXEC_DATE", "DREL_DATE"].includes(name) ? formatDateRF(val) : ["SDOCPREF", "SDOCNUMB"].includes(name) ? undefined : val,
|
||||
respArg: "COUT"
|
||||
});
|
||||
setCostRouteLists(pv => ({
|
||||
|
@ -36,7 +36,8 @@ import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardContent,
|
||||
CardActions
|
||||
CardActions,
|
||||
Tooltip
|
||||
} from "@mui/material"; //Интерфейсные элементы
|
||||
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
|
||||
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
|
||||
@ -55,19 +56,34 @@ import { IncomFromDepsDataGrid } from "./datagrids/incomefromdeps"; //Табли
|
||||
//---------
|
||||
|
||||
//Склонения для документов
|
||||
const DECLINATIONS = ["план", "плана", "планов"];
|
||||
const PLANS_DECLINATIONS = ["план", "плана", "планов"];
|
||||
const SPEC_DECLINATIONS = ["элемент", "элемента", "элементов"];
|
||||
|
||||
//Поля сортировки
|
||||
const SORT_REP_DATE = "DREP_DATE";
|
||||
const SORT_REP_DATE_TO = "DREP_DATE_TO";
|
||||
|
||||
//Максимальное количество элементов
|
||||
const MAX_TASKS = 10000;
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
PLANS_FINDER: { marginTop: "10px", marginLeft: "10px", width: "93%" },
|
||||
PLANS_CHECKBOX_HAVEDOCS: { alignContent: "space-around" },
|
||||
PLANS_LIST_CONTAINER: { height: "100%", display: "flex", flexDirection: "column", justifyContent: "space-between" },
|
||||
PLANS_LIST_ITEM_ZERODOCS: { backgroundColor: "#ebecec" },
|
||||
PLANS_LIST_ITEM_PRIMARY: { wordWrap: "break-word" },
|
||||
PLANS_LIST_ITEM_SECONDARY: { wordWrap: "break-word", fontSize: "0.6rem", textTransform: "uppercase" },
|
||||
PLANS_LIST_ITEM_PLAN: {
|
||||
backgroundColor: "#c7e7f1",
|
||||
"&:hover": { backgroundColor: `#c7e7f1`, filter: "brightness(0.92) !important" }
|
||||
},
|
||||
PLANS_LIST_ITEM_PLAN_FIELD: {
|
||||
marginLeft: "15px"
|
||||
},
|
||||
PLANS_LIST_FILTER_CONTAINER: { height: "calc(100% - 55px)", overflowY: "auto" },
|
||||
PLANS_LIST_BUTTONS_CONTAINER: { display: "flex", justifyContent: "space-around", paddingBottom: "10px", height: "45px" },
|
||||
PLANS_LIST_BUTTON: { minWidth: "125px", height: "35px" },
|
||||
PLANS_BUTTON: { position: "absolute", top: `calc(${APP_BAR_HEIGHT} + 16px)`, left: "16px" },
|
||||
PLANS_DRAWER: {
|
||||
width: "350px",
|
||||
@ -84,7 +100,26 @@ const STYLES = {
|
||||
TASK_DIALOG_ACTION_CONTAINER: { border: 1, borderColor: "text.primary", borderRadius: "5px", width: "100%" },
|
||||
FILTERS: { display: "table", float: "right" },
|
||||
FILTERS_DATE: { display: "table-cell", verticalAlign: "middle" },
|
||||
FILTERS_LEVEL: { display: "table-cell", verticalAlign: "middle", paddingLeft: "15px" }
|
||||
FILTERS_LEVEL: { display: "table-cell", verticalAlign: "middle", paddingLeft: "15px" },
|
||||
FILTERS_LEVEL_CAPTION: { display: "flex", alignItems: "center" },
|
||||
FILTERS_LEVEL_LIMIT_ICON: { padding: "0px 8px", color: "#9f9c9c" },
|
||||
FILTERS_LIMIT_SELECT: nOutOfLimit => {
|
||||
return nOutOfLimit === 1
|
||||
? {
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: "#e9863c"
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: "#e9863c",
|
||||
borderWidth: "0.15rem"
|
||||
},
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: "#e9863c",
|
||||
borderWidth: "0.15rem"
|
||||
}
|
||||
}
|
||||
: {};
|
||||
}
|
||||
};
|
||||
|
||||
//------------------------------------
|
||||
@ -102,7 +137,9 @@ const parseProdPlanSpXML = async xmlDoc => {
|
||||
};
|
||||
|
||||
//Форматирование для отображения количества документов
|
||||
const formatCountDocs = nCountDocs => {
|
||||
const formatCountDocs = (nCountDocs, nType = 0) => {
|
||||
//Склонение документов
|
||||
let DECLINATIONS = nType === 0 ? PLANS_DECLINATIONS : SPEC_DECLINATIONS;
|
||||
//Получаем последнюю цифру в значении
|
||||
let num = (nCountDocs % 100) % 10;
|
||||
//Документов
|
||||
@ -115,55 +152,146 @@ const formatCountDocs = nCountDocs => {
|
||||
return `${nCountDocs} ${DECLINATIONS[2]}`;
|
||||
};
|
||||
|
||||
//Изменение информации об отмеченных планах
|
||||
const updateCtlgPlanInfo = (selectedPlans, plan) => {
|
||||
//Результат изменений
|
||||
let res = { selectedPlans: [...selectedPlans] || [], selectedPlansElements: plan.NCOUNT_DOCS };
|
||||
//Определяем наличие в отмеченных планах
|
||||
let selectedIndex = res.selectedPlans.indexOf(plan.NRN);
|
||||
//Если уже есть в отмеченных - удаляем, нет - добавляем
|
||||
if (selectedIndex > -1) {
|
||||
//Удаляем план из выбранных
|
||||
res.selectedPlans.splice(selectedIndex, 1);
|
||||
//Переворачиваем сумму документов
|
||||
res.selectedPlansElements = res.selectedPlansElements * -1;
|
||||
} else {
|
||||
//Добавляем план в выбранные
|
||||
res.selectedPlans.push(plan.NRN);
|
||||
}
|
||||
//Возвращаем результат
|
||||
return res;
|
||||
};
|
||||
|
||||
//Список каталогов планов
|
||||
const PlanCtlgsList = ({ planCtlgs = [], selectedPlanCtlg, filter, setFilter, onClick } = {}) => {
|
||||
const PlanCtlgsList = ({
|
||||
planCtlgs = [],
|
||||
selectedPlans = [],
|
||||
selectedPlanCtlg,
|
||||
selectedPlansElements,
|
||||
filter,
|
||||
setFilter,
|
||||
onCtlgClick,
|
||||
onCtlgPlanClick,
|
||||
onCtlgPlansOk,
|
||||
onCtlgPlansCancel
|
||||
} = {}) => {
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<div>
|
||||
<TextField
|
||||
sx={STYLES.PLANS_FINDER}
|
||||
name="planFilter"
|
||||
label="Каталог"
|
||||
value={filter.ctlgName}
|
||||
variant="standard"
|
||||
fullWidth
|
||||
onChange={event => {
|
||||
setFilter(pv => ({ ...pv, ctlgName: event.target.value }));
|
||||
}}
|
||||
></TextField>
|
||||
<FormGroup sx={STYLES.PLANS_CHECKBOX_HAVEDOCS}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={filter.haveDocs} onChange={event => setFilter(pv => ({ ...pv, haveDocs: event.target.checked }))} />}
|
||||
label="Только с планами"
|
||||
labelPlacement="end"
|
||||
/>
|
||||
</FormGroup>
|
||||
<List>
|
||||
{planCtlgs.map(p => (
|
||||
<ListItemButton
|
||||
sx={p.NCOUNT_DOCS == 0 ? STYLES.PLANS_LIST_ITEM_ZERODOCS : null}
|
||||
key={p.NRN}
|
||||
selected={p.NRN === selectedPlanCtlg}
|
||||
onClick={() => (onClick ? onClick(p) : null)}
|
||||
>
|
||||
<ListItemText
|
||||
primary={<Typography sx={STYLES.PLANS_LIST_ITEM_PRIMARY}>{p.SNAME}</Typography>}
|
||||
secondary={<Typography sx={STYLES.PLANS_LIST_ITEM_SECONDARY}>{formatCountDocs(p.NCOUNT_DOCS)}</Typography>}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
<Box sx={STYLES.PLANS_LIST_CONTAINER}>
|
||||
<Box sx={STYLES.PLANS_LIST_FILTER_CONTAINER}>
|
||||
<TextField
|
||||
sx={STYLES.PLANS_FINDER}
|
||||
name="planFilter"
|
||||
label="Каталог"
|
||||
value={filter.ctlgName}
|
||||
variant="standard"
|
||||
fullWidth
|
||||
onChange={event => {
|
||||
setFilter(pv => ({ ...pv, ctlgName: event.target.value }));
|
||||
}}
|
||||
></TextField>
|
||||
<FormGroup sx={STYLES.PLANS_CHECKBOX_HAVEDOCS}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox checked={filter.haveDocs} onChange={event => setFilter(pv => ({ ...pv, haveDocs: event.target.checked }))} />
|
||||
}
|
||||
label="Только с планами"
|
||||
labelPlacement="end"
|
||||
/>
|
||||
</FormGroup>
|
||||
<List>
|
||||
{planCtlgs.map(ctlg => (
|
||||
<Box key={ctlg.NRN}>
|
||||
<ListItemButton
|
||||
sx={ctlg.NCOUNT_DOCS == 0 ? STYLES.PLANS_LIST_ITEM_ZERODOCS : null}
|
||||
key={ctlg.NRN}
|
||||
selected={ctlg.NRN === selectedPlanCtlg}
|
||||
onClick={() => (onCtlgClick ? onCtlgClick(ctlg) : null)}
|
||||
disabled={ctlg.NCOUNT_DOCS == 0}
|
||||
>
|
||||
<ListItemText
|
||||
primary={<Typography sx={STYLES.PLANS_LIST_ITEM_PRIMARY}>{ctlg.SNAME}</Typography>}
|
||||
secondary={<Typography sx={STYLES.PLANS_LIST_ITEM_SECONDARY}>{formatCountDocs(ctlg.NCOUNT_DOCS, 0)}</Typography>}
|
||||
/>
|
||||
</ListItemButton>
|
||||
{ctlg.NRN === selectedPlanCtlg && ctlg.XCRN_PLANS.length > 1
|
||||
? ctlg.XCRN_PLANS.map(plan => (
|
||||
<ListItemButton
|
||||
sx={plan.NCOUNT_DOCS == 0 ? STYLES.PLANS_LIST_ITEM_ZERODOCS : STYLES.PLANS_LIST_ITEM_PLAN}
|
||||
key={plan.NRN}
|
||||
disabled={plan.NCOUNT_DOCS == 0}
|
||||
onClick={() => (onCtlgPlanClick ? onCtlgPlanClick(plan) : null)}
|
||||
>
|
||||
<ListItemText
|
||||
sx={STYLES.PLANS_LIST_ITEM_PLAN_FIELD}
|
||||
primary={<Typography sx={STYLES.PLANS_LIST_ITEM_PRIMARY}>{plan.SNAME}</Typography>}
|
||||
secondary={
|
||||
<Typography sx={STYLES.PLANS_LIST_ITEM_SECONDARY}>
|
||||
{formatCountDocs(plan.NCOUNT_DOCS, 1)}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
{plan.NCOUNT_DOCS !== 0 ? <Checkbox checked={selectedPlans.includes(plan.NRN)} /> : null}
|
||||
</ListItemButton>
|
||||
))
|
||||
: null}
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
<Box sx={STYLES.PLANS_LIST_BUTTONS_CONTAINER}>
|
||||
<Tooltip
|
||||
title={
|
||||
!selectedPlanCtlg
|
||||
? "Не выбран каталог планов"
|
||||
: selectedPlans.length === 0
|
||||
? "Не выбраны планы каталога"
|
||||
: selectedPlansElements > MAX_TASKS
|
||||
? `Выбранные планы превышают максимум элементов (выбрано: ${selectedPlansElements}, максимум: ${MAX_TASKS})`
|
||||
: null
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Button
|
||||
sx={STYLES.PLANS_LIST_BUTTON}
|
||||
variant="contained"
|
||||
disabled={selectedPlans.length === 0 || selectedPlansElements > MAX_TASKS}
|
||||
onClick={onCtlgPlansOk}
|
||||
>
|
||||
Применить
|
||||
</Button>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Button sx={STYLES.PLANS_LIST_BUTTON} variant="contained" onClick={onCtlgPlansCancel}>
|
||||
Отмена
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Список каталогов планов
|
||||
PlanCtlgsList.propTypes = {
|
||||
planCtlgs: PropTypes.array,
|
||||
selectedPlans: PropTypes.array,
|
||||
selectedPlanCtlg: PropTypes.number,
|
||||
onClick: PropTypes.func,
|
||||
selectedPlansElements: PropTypes.number,
|
||||
onCtlgClick: PropTypes.func,
|
||||
onCtlgPlanClick: PropTypes.func,
|
||||
filter: PropTypes.object,
|
||||
setFilter: PropTypes.func
|
||||
setFilter: PropTypes.func,
|
||||
onCtlgPlansOk: PropTypes.func,
|
||||
onCtlgPlansCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//Генерация диалога задачи
|
||||
@ -232,12 +360,18 @@ const MechRecCostProdPlans = () => {
|
||||
showPlanList: false,
|
||||
planCtlgs: [],
|
||||
planCtlgsLoaded: false,
|
||||
selectedPlans: [],
|
||||
selectedPlansElements: 0,
|
||||
selectedPlanCtlgSpecsLoaded: false,
|
||||
selectedPlanCtlg: null,
|
||||
selectedPlanCtlgMaxLevel: null,
|
||||
selectedPlanCtlgLevel: null,
|
||||
selectedPlanCtlgOutOfLimit: 0,
|
||||
selectedPlanCtlgSort: null,
|
||||
selectedPlanCtlgMenuItems: null,
|
||||
loadedCtlg: null,
|
||||
loadedPlans: [],
|
||||
loadedElements: 0,
|
||||
gantt: {},
|
||||
selectedTaskDetail: null,
|
||||
selectedTaskDetailType: null,
|
||||
@ -253,11 +387,14 @@ const MechRecCostProdPlans = () => {
|
||||
const { InlineMsgInfo } = useContext(MessagingСtx);
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored } = useContext(BackEndСtx);
|
||||
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
|
||||
|
||||
//Подключение к контексту навигации
|
||||
const { getNavigationSearch } = useContext(NavigationCtx);
|
||||
|
||||
//Подключение к контексту сообщений
|
||||
const { showMsgInfo } = useContext(MessagingСtx);
|
||||
|
||||
//Инициализация каталогов планов
|
||||
const initPlanCtlgs = useCallback(async () => {
|
||||
if (!state.init) {
|
||||
@ -265,74 +402,101 @@ const MechRecCostProdPlans = () => {
|
||||
stored: "PKG_P8PANELS_MECHREC.FCPRODPLAN_PP_CTLG_INIT",
|
||||
args: {},
|
||||
respArg: "COUT",
|
||||
isArray: name => name === "XFCPRODPLAN_CRNS"
|
||||
isArray: name => ["XFCPRODPLAN_CRNS", "XCRN_PLANS"].includes(name)
|
||||
});
|
||||
setState(pv => ({ ...pv, init: true, planCtlgs: [...(data?.XFCPRODPLAN_CRNS || [])], planCtlgsLoaded: true }));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state.init, executeStored]);
|
||||
|
||||
//Выбор каталога планов
|
||||
const selectPlan = project => {
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
selectedPlanCtlg: project,
|
||||
selectedPlanCtlgSpecsLoaded: false,
|
||||
selectedPlanCtlgMaxLevel: null,
|
||||
selectedPlanCtlgLevel: null,
|
||||
selectedPlanCtlgSort: null,
|
||||
selectedPlanCtlgMenuItems: null,
|
||||
gantt: {},
|
||||
showPlanList: false,
|
||||
selectedTaskDetail: null,
|
||||
selectedTaskDetailType: null
|
||||
}));
|
||||
};
|
||||
|
||||
//Сброс выбора каталога планов
|
||||
const unselectPlan = () =>
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
selectedPlanCtlgSpecsLoaded: false,
|
||||
selectedPlanCtlg: null,
|
||||
selectedPlanCtlgMaxLevel: null,
|
||||
selectedPlanCtlgLevel: null,
|
||||
selectedPlanCtlgSort: null,
|
||||
selectedPlanCtlgMenuItems: null,
|
||||
gantt: {},
|
||||
showPlanList: false,
|
||||
selectedTaskDetail: null,
|
||||
selectedTaskDetailType: null
|
||||
}));
|
||||
|
||||
//Загрузка списка спецификаций каталога планов
|
||||
const loadPlanCtglSpecs = useCallback(
|
||||
async (level = null, sort = null) => {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_MECHREC.FCPRODPLANSP_GET",
|
||||
args: { NCRN: state.selectedPlanCtlg, NLEVEL: level, SSORT_FIELD: sort, NFCPRODPLANSP: state.planSpec }
|
||||
args: {
|
||||
NCRN: state.selectedPlanCtlg,
|
||||
CFCPRODPLANS: {
|
||||
VALUE: state.selectedPlans.length > 0 ? state.selectedPlans.join(";") : null,
|
||||
SDATA_TYPE: SERV_DATA_TYPE_CLOB
|
||||
},
|
||||
NLEVEL: level,
|
||||
SSORT_FIELD: sort,
|
||||
NFCPRODPLANSP: state.planSpec
|
||||
}
|
||||
});
|
||||
let doc = await parseProdPlanSpXML(data.COUT);
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
selectedPlanCtlgMaxLevel: data.NMAX_LEVEL,
|
||||
selectedPlanCtlgLevel: level || level === 0 ? level : data.NMAX_LEVEL,
|
||||
selectedPlanCtlgOutOfLimit: data.NOUT_OF_LIMIT,
|
||||
selectedPlanCtlgSort: sort,
|
||||
selectedPlanCtlgMenuItems: state.selectedPlanCtlgMenuItems
|
||||
? state.selectedPlanCtlgMenuItems
|
||||
: [...Array(data.NMAX_LEVEL).keys()].map(el => el + 1),
|
||||
selectedPlanCtlgMenuItems: [...Array(data.NMAX_LEVEL).keys()].map(el => el + 1),
|
||||
selectedPlanCtlgSpecsLoaded: true,
|
||||
gantt: { ...doc, tasks: [...(doc?.tasks || [])] }
|
||||
gantt: { ...doc, tasks: [...(doc?.tasks || [])] },
|
||||
loadedCtlg: state.selectedPlanCtlg,
|
||||
loadedPlans: [...state.selectedPlans],
|
||||
loadedElements: state.selectedPlansElements,
|
||||
showPlanList: false
|
||||
}));
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[executeStored, state.ident, state.selectedPlanCtlg, state.planSpec]
|
||||
[executeStored, state.selectedPlanCtlg, state.selectedPlans, state.planSpec]
|
||||
);
|
||||
|
||||
//Обработка нажатия на элемент в списке каталогов планов
|
||||
const handleProjectClick = project => {
|
||||
if (state.selectedPlanCtlg != project.NRN) selectPlan(project.NRN);
|
||||
else unselectPlan();
|
||||
const handleCtlgClick = project => {
|
||||
//Если этот каталог не был выбран
|
||||
if (state.selectedPlanCtlg != project.NRN) {
|
||||
//Если выбран уже загруженный - укажем информацию о том, как он загружен
|
||||
if (project.NRN === state.loadedCtlg) {
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
selectedPlanCtlg: project.NRN,
|
||||
selectedPlans: [...pv.loadedPlans],
|
||||
selectedPlansElements: pv.loadedElements
|
||||
}));
|
||||
} else {
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
selectedPlanCtlg: project.NRN,
|
||||
selectedPlans: project.XCRN_PLANS.length === 1 ? [project.XCRN_PLANS[0].NRN] : [],
|
||||
selectedPlansElements: 0
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
setState(pv => ({ ...pv, selectedPlanCtlg: null, selectedPlans: [], selectedPlansElements: 0 }));
|
||||
}
|
||||
};
|
||||
|
||||
//Обработка нажатия на элемент в списке планов каталога
|
||||
const handleCtlgPlanClick = plan => {
|
||||
//Считываем обновленную информацию об отмеченных планах
|
||||
let newPlansInfo = updateCtlgPlanInfo(state.selectedPlans, plan);
|
||||
//Обновляем список отмеченных планов
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
selectedPlans: [...newPlansInfo.selectedPlans],
|
||||
selectedPlansElements: pv.selectedPlansElements + newPlansInfo.selectedPlansElements
|
||||
}));
|
||||
};
|
||||
|
||||
//Обработка нажатия "ОК" при отборе планов
|
||||
const handleSelectedPlansOk = () => {
|
||||
//Загружаем диаграмму
|
||||
loadPlanCtglSpecs(null, SORT_REP_DATE_TO);
|
||||
};
|
||||
|
||||
//Обработка нажатия "Отмена" при отборе планов
|
||||
const handleSelectedPlansCancel = () => {
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
selectedPlanCtlg: pv.loadedCtlg,
|
||||
selectedPlans: [...pv.loadedPlans] || [],
|
||||
selectedPlansElements: pv.loadedElements,
|
||||
showPlanList: false
|
||||
}));
|
||||
};
|
||||
|
||||
//При подключении компонента к странице
|
||||
@ -345,8 +509,8 @@ const MechRecCostProdPlans = () => {
|
||||
|
||||
//При смене выбранного каталога плана или при явном указании позиции спецификации плана
|
||||
useEffect(() => {
|
||||
if (state.selectedPlanCtlg || state.planSpec) loadPlanCtglSpecs(null, SORT_REP_DATE_TO);
|
||||
}, [state.selectedPlanCtlg, state.planSpec, loadPlanCtglSpecs]);
|
||||
if (state.planSpec) loadPlanCtglSpecs(null, SORT_REP_DATE_TO);
|
||||
}, [state.planSpec, loadPlanCtglSpecs]);
|
||||
|
||||
//Выбор уровня
|
||||
const handleChangeSelectLevel = selectedLevel => {
|
||||
@ -370,6 +534,17 @@ const MechRecCostProdPlans = () => {
|
||||
setState(pv => ({ ...pv, selectedTaskDetail: taskRn, selectedTaskDetailType: taskType }));
|
||||
};
|
||||
|
||||
//При открытии окна информации об ограничении уровня
|
||||
const handleLevelLimitInfoOpen = () => {
|
||||
//Отображаем информацию
|
||||
showMsgInfo(
|
||||
`Размер производственной программы превышает предельно допустимый для одновременного отображения в виде диаграммы Ганта.
|
||||
Доступные для просмотра уровни вложенности ограничены.
|
||||
Вы можете просматривать производственную программу частями, используя действие "Открытие панели Производственная программа" в спецификации "Выпуск"
|
||||
раздела "Планы и отчеты производства изделий".`
|
||||
);
|
||||
};
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box>
|
||||
@ -378,18 +553,18 @@ const MechRecCostProdPlans = () => {
|
||||
<Fab variant="extended" sx={STYLES.PLANS_BUTTON} onClick={() => setState(pv => ({ ...pv, showPlanList: !pv.showPlanList }))}>
|
||||
Каталоги планов
|
||||
</Fab>
|
||||
<Drawer
|
||||
anchor={"left"}
|
||||
open={state.showPlanList}
|
||||
onClose={() => setState(pv => ({ ...pv, showPlanList: false }))}
|
||||
sx={STYLES.PLANS_DRAWER}
|
||||
>
|
||||
<Drawer anchor={"left"} open={state.showPlanList} onClose={handleSelectedPlansCancel} sx={STYLES.PLANS_DRAWER}>
|
||||
<PlanCtlgsList
|
||||
planCtlgs={filteredPlanCtgls}
|
||||
selectedPlans={state.selectedPlans}
|
||||
selectedPlanCtlg={state.selectedPlanCtlg}
|
||||
selectedPlansElements={state.selectedPlansElements}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
onClick={handleProjectClick}
|
||||
onCtlgClick={handleCtlgClick}
|
||||
onCtlgPlanClick={handleCtlgPlanClick}
|
||||
onCtlgPlansOk={handleSelectedPlansOk}
|
||||
onCtlgPlansCancel={handleSelectedPlansCancel}
|
||||
/>
|
||||
</Drawer>
|
||||
</>
|
||||
@ -434,8 +609,16 @@ const MechRecCostProdPlans = () => {
|
||||
</Select>
|
||||
</Box>
|
||||
<Box sx={STYLES.FILTERS_LEVEL}>
|
||||
<InputLabel id="select-label-level">До уровня</InputLabel>
|
||||
<Box sx={STYLES.FILTERS_LEVEL_CAPTION}>
|
||||
<InputLabel id="select-label-level">До уровня</InputLabel>
|
||||
{state.selectedPlanCtlgOutOfLimit === 1 ? (
|
||||
<IconButton sx={STYLES.FILTERS_LEVEL_LIMIT_ICON} onClick={handleLevelLimitInfoOpen}>
|
||||
<Icon>info</Icon>
|
||||
</IconButton>
|
||||
) : null}
|
||||
</Box>
|
||||
<Select
|
||||
sx={STYLES.FILTERS_LIMIT_SELECT(state.selectedPlanCtlgOutOfLimit)}
|
||||
labelId="select-label-level"
|
||||
id="select-level"
|
||||
value={state.selectedPlanCtlgLevel}
|
||||
@ -463,14 +646,14 @@ const MechRecCostProdPlans = () => {
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
) : !state.selectedPlanCtlg ? (
|
||||
) : !state.loadedCtlg ? (
|
||||
<Box pt={3}>
|
||||
<InlineMsgInfo
|
||||
okBtn={false}
|
||||
text={
|
||||
state.planSpec
|
||||
? "Загружаю график для выбранной позиции плана..."
|
||||
: "Укажите каталог планов для отображения их спецификаций"
|
||||
: "Укажите каталог планов или планы для отображения их спецификаций"
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
51
app/panels/panels_editor/component_editor.js
Normal file
51
app/panels/panels_editor/component_editor.js
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Редактор свойств компонента панели
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Box, Typography } from "@mui/material"; //Интерфейсные элементы
|
||||
import { useComponentModule } from "./components/components_hooks"; //Хуки компонентов
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Редактор свойств компонента панели
|
||||
const ComponentEditor = ({ id, path, settings = {}, valueProviders = {}, onSettingsChange = null } = {}) => {
|
||||
//Подгрузка модуля редактора компонента (lazy здесь постоянно обновлялся при смене props, поэтому на хуке, от props независимого)
|
||||
const [ComponentEditor, init] = useComponentModule({ path, module: "editor" });
|
||||
|
||||
//Расчёт флага наличия компонента
|
||||
const haveComponent = path ? true : false;
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<Box>
|
||||
{haveComponent && init && (
|
||||
<ComponentEditor.default id={id} {...settings} valueProviders={valueProviders} onSettingsChange={onSettingsChange} />
|
||||
)}
|
||||
{!haveComponent && <Typography align={"center"}>Компонент не определён</Typography>}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - редактор свойств компонента панели
|
||||
ComponentEditor.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
settings: PropTypes.object,
|
||||
valueProviders: PropTypes.object,
|
||||
onSettingsChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { ComponentEditor };
|
71
app/panels/panels_editor/component_view.js
Normal file
71
app/panels/panels_editor/component_view.js
Normal file
@ -0,0 +1,71 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Представление компонента панели
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Box, Typography } from "@mui/material"; //Интерфейсные элементы
|
||||
import { useComponentModule } from "./components/components_hooks"; //Хуки компонентов
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Представление компонента панели
|
||||
const ComponentView = ({ id, path, settings = {}, values = {}, onValuesChange = null } = {}) => {
|
||||
//Подгрузка модуля представления компонента (lazy здесь постоянно обновлялся при смене props, поэтому на хуке, от props независимого)
|
||||
const [ComponentView, init] = useComponentModule({ path, module: "view" });
|
||||
|
||||
//При смене значений
|
||||
const handleValuesChange = values => onValuesChange && onValuesChange(id, values);
|
||||
|
||||
//Расчёт флага наличия компонента
|
||||
const haveComponent = path ? true : false;
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<Box className={"component-view__wrap"}>
|
||||
{haveComponent && init && <ComponentView.default id={id} {...settings} values={values} onValuesChange={handleValuesChange} />}
|
||||
{!haveComponent && <Typography align={"center"}>Компонент не определён</Typography>}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - компонент панели
|
||||
ComponentView.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
settings: PropTypes.object,
|
||||
values: PropTypes.object,
|
||||
onValuesChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { ComponentView };
|
||||
|
||||
//--------------------------
|
||||
//ВАЖНО: Можно на React.lazy
|
||||
//--------------------------
|
||||
|
||||
//ПРИМЕР:
|
||||
/*
|
||||
import React, { Suspense, lazy } from "react"; //Классы React
|
||||
const ComponentView = ({ path = null, props = {} } = {}) => {
|
||||
const haveComponent = path ? true : false;
|
||||
const ComponentView = haveComponent ? lazy(() => import(`./components/${path}/view`)) : null;
|
||||
return (
|
||||
<Paper sx={STYLES.CONTAINER}>
|
||||
{haveComponent && (<Suspense fallback={null}><ComponentView {...props}/></Suspense>)}
|
||||
{!haveComponent && <Typography align={"center"}>Компонент не определён</Typography>}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
*/
|
58
app/panels/panels_editor/components/chart/editor.js
Normal file
58
app/panels/panels_editor/components/chart/editor.js
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Компоненты: График (редактор настроек)
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useEffect, useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { P8PEditorBox } from "../../../../components/editors/p8p_editor_box"; //Контейнер редактора
|
||||
import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Заголовок раздела редактора
|
||||
import { P8P_DATA_SOURCE_SHAPE } from "../../../../components/editors/p8p_data_source_common"; //Общие ресурсы источника данных
|
||||
import { P8PDataSource } from "../../../../components/editors/p8p_data_source"; //Источник данных
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//График (редактор настроек)
|
||||
const ChartEditor = ({ id, dataSource = null, valueProviders = {}, onSettingsChange = null } = {}) => {
|
||||
//Собственное состояние - текущие настройки
|
||||
const [settings, setSettings] = useState(null);
|
||||
|
||||
//При изменении компонента
|
||||
useEffect(() => {
|
||||
settings?.id != id && setSettings({ id, dataSource });
|
||||
}, [settings, id, dataSource]);
|
||||
|
||||
//При сохранении изменений элемента
|
||||
const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } }));
|
||||
|
||||
//При сохранении настроек
|
||||
const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor });
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<P8PEditorBox title={"Параметры графика"} onSave={handleSave}>
|
||||
<P8PEditorSubHeader title={"Источник данных"} />
|
||||
<P8PDataSource dataSource={settings?.dataSource} valueProviders={valueProviders} onChange={handleDataSourceChange} />
|
||||
</P8PEditorBox>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - График (редактор настроек)
|
||||
ChartEditor.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
dataSource: P8P_DATA_SOURCE_SHAPE,
|
||||
valueProviders: PropTypes.object,
|
||||
onSettingsChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default ChartEditor;
|
82
app/panels/panels_editor/components/chart/view.js
Normal file
82
app/panels/panels_editor/components/chart/view.js
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Компоненты: График (представление)
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Paper } from "@mui/material"; //Интерфейсные элементы
|
||||
import { P8PChart } from "../../../../components/p8p_chart"; //График
|
||||
import { useDataSource } from "../../../../components/editors/p8p_data_source_hooks"; //Хуки для данных
|
||||
import { P8P_DATA_SOURCE_SHAPE } from "../../../../components/editors/p8p_data_source_common"; //Общие ресурсы источника данных
|
||||
import {
|
||||
P8P_COMPONENT_INLINE_MESSAGE_TYPE,
|
||||
P8P_COMPONENT_INLINE_MESSAGE,
|
||||
P8PComponentInlineMessage
|
||||
} from "../../../../components/editors/p8p_component_inline_message"; //Информационное сообщение внутри компонента
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Иконка компонента
|
||||
const COMPONENT_ICON = "bar_chart";
|
||||
|
||||
//Наименование компонента
|
||||
const COMPONENT_NAME = "График";
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CHART: { width: "100%", height: "100%", alignItems: "center", justifyContent: "center", display: "flex" }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//График (представление)
|
||||
const Chart = ({ dataSource = null, values = {} } = {}) => {
|
||||
//Собственное состояние - данные
|
||||
const [data, error] = useDataSource({ dataSource, values });
|
||||
|
||||
//Флаг настроенности графика
|
||||
const haveConfing = dataSource?.stored ? true : false;
|
||||
|
||||
//Флаг наличия данных
|
||||
const haveData = data?.init === true && !error ? true : false;
|
||||
|
||||
//Данные графика
|
||||
const chart = data?.XCHART || {};
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<Paper className={"component-view__container component-view__container__empty"} elevation={6}>
|
||||
{haveConfing && haveData ? (
|
||||
<P8PChart style={STYLES.CHART} {...chart} />
|
||||
) : (
|
||||
<P8PComponentInlineMessage
|
||||
icon={COMPONENT_ICON}
|
||||
name={COMPONENT_NAME}
|
||||
message={!haveConfing ? P8P_COMPONENT_INLINE_MESSAGE.NO_SETTINGS : error ? error : P8P_COMPONENT_INLINE_MESSAGE.NO_DATA_FOUND}
|
||||
type={error ? P8P_COMPONENT_INLINE_MESSAGE_TYPE.ERROR : P8P_COMPONENT_INLINE_MESSAGE_TYPE.COMMON}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - График (представление)
|
||||
Chart.propTypes = {
|
||||
dataSource: P8P_DATA_SOURCE_SHAPE,
|
||||
values: PropTypes.object
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default Chart;
|
129
app/panels/panels_editor/components/components.js
Normal file
129
app/panels/panels_editor/components/components.js
Normal file
@ -0,0 +1,129 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Компоненты: Описание
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
const COMPONETNS = [
|
||||
{
|
||||
name: "Форма",
|
||||
path: "form",
|
||||
settings2: {
|
||||
title: "Параметры формирования",
|
||||
autoApply: true,
|
||||
items: [
|
||||
{
|
||||
name: "AGENT",
|
||||
caption: "Контрагент",
|
||||
unitCode2: "AGNLIST",
|
||||
unitName: "Контрагенты",
|
||||
showMethod: "main",
|
||||
showMethodName: "main",
|
||||
parameter: "Мнемокод",
|
||||
inputParameter: "in_AGNABBR",
|
||||
outputParameter: "out_AGNABBR"
|
||||
},
|
||||
{
|
||||
name: "DOC_TYPE",
|
||||
caption: "Тип документа",
|
||||
unitCode2: "DOCTYPES",
|
||||
unitName: "Типы документов",
|
||||
showMethod: "main",
|
||||
showMethodName: "main",
|
||||
parameter: "Мнемокод",
|
||||
inputParameter: "in_DOCCODE",
|
||||
outputParameter: "out_DOCCODE"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "График",
|
||||
path: "chart",
|
||||
settings2: {
|
||||
dataSource: {
|
||||
type: "USER_PROC",
|
||||
userProc: "ГрафТоп5ДогКонтрТип",
|
||||
stored: "UDO_P_P8P_AGNCONTR_CHART",
|
||||
respArg: "COUT",
|
||||
arguments: [
|
||||
{ name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" },
|
||||
{ name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Таблица",
|
||||
path: "table",
|
||||
settings2: {
|
||||
dataSource: {
|
||||
type: "USER_PROC",
|
||||
userProc: "ТаблицаДогКонтрТип",
|
||||
stored: "UDO_P_P8P_AGNCONTR_TABLE",
|
||||
respArg: "COUT",
|
||||
arguments: [
|
||||
{ name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" },
|
||||
{ name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "Индикатор",
|
||||
path: "indicator",
|
||||
settings2: {
|
||||
dataSource: {
|
||||
type: "USER_PROC",
|
||||
userProc: "ИндКолДогКонтрТип",
|
||||
stored: "UDO_P_P8P_AGNCONTR_IND",
|
||||
respArg: "COUT",
|
||||
arguments: [
|
||||
{ name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" },
|
||||
{ name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" },
|
||||
{
|
||||
name: "NIND_TYPE",
|
||||
caption: "Тип индикатора (0 - все, 1 - неутвержденные)",
|
||||
dataType: "NUMB",
|
||||
req: true,
|
||||
value: "0",
|
||||
valueSource: ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
} /*,
|
||||
{
|
||||
name: "Индикатор2",
|
||||
path: "indicator",
|
||||
settings: {
|
||||
dataSource: {
|
||||
type: "USER_PROC",
|
||||
userProc: "ИндКолДогКонтрТип",
|
||||
stored: "UDO_P_P8P_AGNCONTR_IND",
|
||||
respArg: "COUT",
|
||||
arguments: [
|
||||
{ name: "SAGENT", caption: "Контрагент", dataType: "STR", req: false, value: "", valueSource: "AGENT" },
|
||||
{ name: "SDOC_TYPE", caption: "Тип документа", dataType: "STR", req: false, value: "", valueSource: "DOC_TYPE" },
|
||||
{
|
||||
name: "NIND_TYPE",
|
||||
caption: "Тип индикатора (0 - все, 1 - неутвержденные)",
|
||||
dataType: "NUMB",
|
||||
req: true,
|
||||
value: "1",
|
||||
valueSource: ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}*/
|
||||
];
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { COMPONETNS };
|
44
app/panels/panels_editor/components/components_hooks.js
Normal file
44
app/panels/panels_editor/components/components_hooks.js
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Компоненты: Хуки компонентов
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { useState, useEffect } from "react"; //Классы React
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Отложенная загрузка модуля компонента (как альтернативу можно применять React.lazy)
|
||||
const useComponentModule = ({ path = null, module = "view" } = {}) => {
|
||||
//Собственное состояние - импортированный модуль компонента
|
||||
const [componentModule, setComponentModule] = useState(null);
|
||||
|
||||
//Собственное состояние - флаг готовности
|
||||
const [init, setInit] = useState(false);
|
||||
|
||||
//При подмонтировании к странице
|
||||
useEffect(() => {
|
||||
//Динамическая загрузка модуля компонента из библиотеки
|
||||
const importComponentModule = async () => {
|
||||
setInit(false);
|
||||
const moduleContent = await import(`./${path}/${module}`);
|
||||
setComponentModule(moduleContent);
|
||||
setInit(true);
|
||||
};
|
||||
if (path) importComponentModule();
|
||||
}, [path, module]);
|
||||
|
||||
//Возвращаем интерфейс хука
|
||||
return [componentModule, init];
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { useComponentModule };
|
49
app/panels/panels_editor/components/form/common.js
Normal file
49
app/panels/panels_editor/components/form/common.js
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Компоненты: Форма (общие константы)
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
//Структура элемента формы
|
||||
export const ITEM_SHAPE = PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
caption: PropTypes.string.isRequired,
|
||||
unitCode: PropTypes.string,
|
||||
unitName: PropTypes.string,
|
||||
showMethod: PropTypes.string,
|
||||
showMethodName: PropTypes.string,
|
||||
parameter: PropTypes.string,
|
||||
inputParameter: PropTypes.string,
|
||||
outputParameter: PropTypes.string
|
||||
});
|
||||
|
||||
//Начальное состояние элемента формы
|
||||
export const ITEM_INITIAL = {
|
||||
name: "",
|
||||
caption: "",
|
||||
unitCode: "",
|
||||
unitName: "",
|
||||
showMethod: "",
|
||||
showMethodName: "",
|
||||
parameter: "",
|
||||
inputParameter: "",
|
||||
outputParameter: ""
|
||||
};
|
||||
|
||||
//Начальное состояние элементов формы
|
||||
export const ITEMS_INITIAL = [];
|
||||
|
||||
//Ориентация элементов формы
|
||||
export const ORIENTATION = {
|
||||
H: "H",
|
||||
V: "v"
|
||||
};
|
309
app/panels/panels_editor/components/form/editor.js
Normal file
309
app/panels/panels_editor/components/form/editor.js
Normal file
@ -0,0 +1,309 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Компоненты: Форма (редактор настроек)
|
||||
*/
|
||||
|
||||
//TODO: Контроль уникальности имени элемента формы
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useEffect, useState, useContext } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import {
|
||||
TextField,
|
||||
Button,
|
||||
Icon,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
Chip,
|
||||
Stack,
|
||||
InputAdornment,
|
||||
IconButton
|
||||
} from "@mui/material"; //Интерфейсные элементы
|
||||
import { ApplicationСtx } from "../../../../context/application"; //Контекст приложения
|
||||
import { P8PEditorBox } from "../../../../components/editors/p8p_editor_box"; //Контейнер редактора
|
||||
import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Заголовок раздела редактора
|
||||
import { P8PConfigDialog } from "../../../../components/editors/p8p_config_dialog"; //Диалог настройки
|
||||
import { STYLES as COMMON_STYLES } from "../../../../components/editors/p8p_editors_common"; //Общие ресурсы редакторов
|
||||
import { ITEM_SHAPE, ITEM_INITIAL, ITEMS_INITIAL, ORIENTATION } from "./common"; //Общие ресурсы и константы формы
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CHIP_ITEM: { ...COMMON_STYLES.CHIP(true, false) }
|
||||
};
|
||||
|
||||
//------------------------------------
|
||||
//Вспомогательные функции и компоненты
|
||||
//------------------------------------
|
||||
|
||||
//Редактор элемента
|
||||
const ItemEditor = ({ item = null, onOk = null, onCancel = null } = {}) => {
|
||||
//Собственное состояние - параметры элемента формы
|
||||
const [state, setState] = useState({ ...ITEM_INITIAL, ...item });
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
|
||||
|
||||
//При закрытии редактора с сохранением
|
||||
const handleOk = () => onOk && onOk({ ...state });
|
||||
|
||||
//При закрытии редактора с отменой
|
||||
const handleCancel = () => onCancel && onCancel();
|
||||
|
||||
//При изменении параметра элемента
|
||||
const handleChange = e => setState(pv => ({ ...pv, [e.target.id]: e.target.value }));
|
||||
|
||||
//При нажатии на очистку раздела
|
||||
const handleClearUnitClick = () =>
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
unitCode: "",
|
||||
unitName: "",
|
||||
showMethod: "",
|
||||
showMethodName: "",
|
||||
parameter: "",
|
||||
inputParameter: "",
|
||||
outputParameter: ""
|
||||
}));
|
||||
|
||||
//При нажатии на выбор раздела
|
||||
const handleSelectUnitClick = () => {
|
||||
pOnlineShowDictionary({
|
||||
unitCode: "Units",
|
||||
showMethod: "methods",
|
||||
inputParameters: [
|
||||
{ name: "pos_unit_name", value: state.unitName },
|
||||
{ name: "pos_method_name", value: state.showMethodName }
|
||||
],
|
||||
callBack: res =>
|
||||
res.success &&
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
unitCode: res.outParameters.unit_code,
|
||||
unitName: res.outParameters.unit_name,
|
||||
showMethod: res.outParameters.method_code,
|
||||
showMethodName: res.outParameters.method_name,
|
||||
parameter: "",
|
||||
inputParameter: "",
|
||||
outputParameter: ""
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
//При нажатии на выбор параметра метода вызова
|
||||
const handleSelectUnitParameterClick = () => {
|
||||
state.unitCode &&
|
||||
state.showMethod &&
|
||||
pOnlineShowDictionary({
|
||||
unitCode: "UnitParams",
|
||||
showMethod: "main",
|
||||
inputParameters: [
|
||||
{ name: "in_UNITCODE", value: state.unitCode },
|
||||
{ name: "in_PARENT_METHOD_CODE", value: state.showMethod },
|
||||
{ name: "in_PARAMNAME", value: state.parameter }
|
||||
],
|
||||
callBack: res =>
|
||||
res.success &&
|
||||
setState(pv => ({
|
||||
...pv,
|
||||
parameter: res.outParameters.out_PARAMNAME,
|
||||
inputParameter: res.outParameters.out_IN_CODE,
|
||||
outputParameter: res.outParameters.out_OUT_CODE
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<P8PConfigDialog title={`${item ? "Изменение" : "Добавление"} элемента`} onOk={handleOk} onCancel={handleCancel}>
|
||||
<Stack direction={"column"} spacing={1}>
|
||||
<TextField type={"text"} variant={"standard"} value={state.name} label={"Имя"} id={"name"} onChange={handleChange} />
|
||||
<TextField type={"text"} variant={"standard"} value={state.caption} label={"Приглашение"} id={"caption"} onChange={handleChange} />
|
||||
<TextField
|
||||
type={"text"}
|
||||
variant={"standard"}
|
||||
value={state.unitName}
|
||||
label={"Раздел"}
|
||||
InputLabelProps={{ shrink: state.unitName ? true : false }}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleClearUnitClick}>
|
||||
<Icon>clear</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={handleSelectUnitClick}>
|
||||
<Icon>list</Icon>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
type={"text"}
|
||||
variant={"standard"}
|
||||
value={state.showMethodName}
|
||||
label={"Метод вызова"}
|
||||
InputLabelProps={{ shrink: state.showMethodName ? true : false }}
|
||||
InputProps={{ readOnly: true }}
|
||||
/>
|
||||
<TextField
|
||||
type={"text"}
|
||||
variant={"standard"}
|
||||
value={state.parameter}
|
||||
label={"Параметр"}
|
||||
InputLabelProps={{ shrink: state.parameter ? true : false }}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleSelectUnitParameterClick}>
|
||||
<Icon>list</Icon>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</P8PConfigDialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - редактор элемента
|
||||
ItemEditor.propTypes = {
|
||||
item: ITEM_SHAPE,
|
||||
onOk: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Форма (редактор настроек)
|
||||
const FormEditor = ({ id, title = "", orientation = ORIENTATION.V, autoApply = false, items = ITEMS_INITIAL, onSettingsChange = null } = {}) => {
|
||||
//Собственное состояние - текущие настройки
|
||||
const [settings, setSettings] = useState(null);
|
||||
|
||||
//Собственное состояние - предоставляемые в панель значения
|
||||
const [providedValues, setProvidedValues] = useState([]);
|
||||
|
||||
//Собственное состояние - редактор элементов формы
|
||||
const [itemEditor, setItemEditor] = useState({ display: false, index: null });
|
||||
|
||||
//При изменении значения настройки
|
||||
const handleChange = e => setSettings({ ...settings, [e.target.name]: e.target.type === "checkbox" ? e.target.checked : e.target.value });
|
||||
|
||||
//При добавлении нового элемента
|
||||
const handleItemAdd = () => setItemEditor({ display: true, index: null });
|
||||
|
||||
//При нажатии на элемент
|
||||
const handleItemClick = i => setItemEditor({ display: true, index: i });
|
||||
|
||||
//При удалении элемента
|
||||
const handleItemDelete = i => {
|
||||
const items = [...settings.items];
|
||||
items.splice(i, 1);
|
||||
setSettings(pv => ({ ...pv, items }));
|
||||
};
|
||||
|
||||
//При сохранении изменений элемента
|
||||
const handleItemSave = item => {
|
||||
const items = [...settings.items];
|
||||
itemEditor.index == null ? items.push({ ...item }) : (items[itemEditor.index] = { ...item });
|
||||
setSettings(pv => ({ ...pv, items }));
|
||||
setItemEditor({ display: false, index: null });
|
||||
};
|
||||
|
||||
//При отмене сохранения изменений элемента
|
||||
const handleItemCancel = () => setItemEditor({ display: false, index: null });
|
||||
|
||||
//При сохранении настроек
|
||||
const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, providedValues, closeEditor });
|
||||
|
||||
//При изменении компонента
|
||||
useEffect(() => {
|
||||
settings?.id != id && setSettings({ id, title, orientation, autoApply, items });
|
||||
}, [settings, id, title, orientation, autoApply, items]);
|
||||
|
||||
//При изменении состава элементов формы
|
||||
useEffect(() => {
|
||||
Array.isArray(settings?.items) && setProvidedValues(settings.items.map(item => item.name));
|
||||
}, [settings?.items]);
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
settings && (
|
||||
<P8PEditorBox title={"Параметры формы"} onSave={handleSave}>
|
||||
{itemEditor.display && (
|
||||
<ItemEditor
|
||||
item={itemEditor.index !== null ? { ...settings.items[itemEditor.index] } : null}
|
||||
onCancel={handleItemCancel}
|
||||
onOk={handleItemSave}
|
||||
/>
|
||||
)}
|
||||
<P8PEditorSubHeader title={"Общие"} />
|
||||
<TextField type={"text"} variant={"standard"} value={settings.title} label={"Заголовок"} name={"title"} onChange={handleChange} />
|
||||
<FormControl variant={"standard"}>
|
||||
<InputLabel id={"orientation-label"}>Ориентация</InputLabel>
|
||||
<Select
|
||||
name={"orientation"}
|
||||
value={settings.orientation}
|
||||
labelId={"orientation-label"}
|
||||
label={"Ориентация"}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={ORIENTATION.V}>Вертикально</MenuItem>
|
||||
<MenuItem value={ORIENTATION.H}>Горизонтально</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControlLabel
|
||||
control={<Switch name={"autoApply"} checked={settings.autoApply} onChange={handleChange} />}
|
||||
label={"Автоподтверждение"}
|
||||
/>
|
||||
<P8PEditorSubHeader title={"Элементы"} />
|
||||
{Array.isArray(settings?.items) &&
|
||||
settings.items.length > 0 &&
|
||||
settings.items.map((item, i) => (
|
||||
<Chip
|
||||
key={i}
|
||||
label={item.caption}
|
||||
variant={"outlined"}
|
||||
onClick={() => handleItemClick(i)}
|
||||
onDelete={() => handleItemDelete(i)}
|
||||
sx={STYLES.CHIP_ITEM}
|
||||
/>
|
||||
))}
|
||||
<Button startIcon={<Icon>add</Icon>} onClick={handleItemAdd}>
|
||||
Добавить элемент
|
||||
</Button>
|
||||
</P8PEditorBox>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Форма (редактор настроек)
|
||||
FormEditor.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
orientation: PropTypes.oneOf(Object.values(ORIENTATION)),
|
||||
autoApply: PropTypes.bool,
|
||||
items: PropTypes.arrayOf(ITEM_SHAPE),
|
||||
onSettingsChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default FormEditor;
|
168
app/panels/panels_editor/components/form/view.js
Normal file
168
app/panels/panels_editor/components/form/view.js
Normal file
@ -0,0 +1,168 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Компоненты: Форма (представление)
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useEffect, useState, useContext } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Paper, Stack, Typography, Icon, TextField, IconButton, InputAdornment } from "@mui/material"; //Интерфейсные элементы
|
||||
import { ApplicationСtx } from "../../../../context/application"; //Контекст приложения
|
||||
import { P8P_COMPONENT_INLINE_MESSAGE, P8PComponentInlineMessage } from "../../../../components/editors/p8p_component_inline_message"; //Информационное сообщение внутри компонента
|
||||
import { ITEM_SHAPE, ITEMS_INITIAL, ORIENTATION } from "./common"; //Общие ресурсы и константы формы
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Иконка компонента
|
||||
const COMPONENT_ICON = "fact_check";
|
||||
|
||||
//Наименование компонента
|
||||
const COMPONENT_NAME = "Форма";
|
||||
|
||||
//------------------------------------
|
||||
//Вспомогательные функции и компоненты
|
||||
//------------------------------------
|
||||
|
||||
//Элемент формы
|
||||
const FormItem = ({ item = null, fullWidth = false, value = "", onChange = null } = {}) => {
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
|
||||
|
||||
//При изменении значения элемента
|
||||
const handleChange = e => onChange && onChange(e.target.id, e.target.value);
|
||||
|
||||
//При очистке значения элемента
|
||||
const handleClear = () => onChange(item.name, "");
|
||||
|
||||
//При выборе значения из словаря
|
||||
const handleDictionary = () =>
|
||||
item.unitCode &&
|
||||
item.showMethod &&
|
||||
pOnlineShowDictionary({
|
||||
unitCode: item.unitCode,
|
||||
showMethod: item.showMethod,
|
||||
inputParameters: [{ name: item.inputParameter, value }],
|
||||
callBack: res => res.success && onChange && onChange(item.name, res.outParameters[item.outputParameter])
|
||||
});
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
item && (
|
||||
<TextField
|
||||
fullWidth={fullWidth}
|
||||
type={"text"}
|
||||
variant={"standard"}
|
||||
value={value}
|
||||
label={item.caption}
|
||||
id={item.name}
|
||||
onChange={handleChange}
|
||||
{...(item.unitCode && {
|
||||
InputLabelProps: { shrink: true },
|
||||
InputProps: {
|
||||
readOnly: true,
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleClear}>
|
||||
<Icon>clear</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={handleDictionary}>
|
||||
<Icon>list</Icon>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
})}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - элемент формы
|
||||
FormItem.propTypes = {
|
||||
item: ITEM_SHAPE,
|
||||
fullWidth: PropTypes.bool,
|
||||
value: PropTypes.any,
|
||||
onChange: PropTypes.func
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Форма (представление)
|
||||
const Form = ({ title = null, orientation = ORIENTATION.V, autoApply = false, items = ITEMS_INITIAL, values = {}, onValuesChange = null } = {}) => {
|
||||
//Собственное состояние - значения элементов
|
||||
const [selfValues, setSelfValues] = useState({});
|
||||
|
||||
//При изменении состава элементов или значений
|
||||
useEffect(() => setSelfValues(items.reduce((sV, item) => ({ ...sV, [item.name]: values[item.name] }), {})), [items, values]);
|
||||
|
||||
//При изменении значения элемента формы
|
||||
const handleItemChange = (name, value) => {
|
||||
setSelfValues(pv => ({ ...pv, [name]: value }));
|
||||
autoApply && onValuesChange && onValuesChange({ ...selfValues, [name]: value });
|
||||
};
|
||||
|
||||
//При подтверждении изменений формы
|
||||
const handleOkClick = () => onValuesChange && onValuesChange({ ...selfValues });
|
||||
|
||||
//Флаг настроенности формы
|
||||
const haveConfing = items && Array.isArray(items) && items.length > 0;
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<Paper className={`component-view__container ${!haveConfing && "component-view__container__empty"}`} elevation={6}>
|
||||
{haveConfing ? (
|
||||
<Stack direction={"column"}>
|
||||
<Stack direction={"row"} justifyContent={"space-between"} alignItems={"center"}>
|
||||
{title && (
|
||||
<Typography align={"left"} color={"text.primary"} variant={"subtitle2"} noWrap={true}>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
{!autoApply && (
|
||||
<IconButton onClick={handleOkClick}>
|
||||
<Icon>done</Icon>
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack direction={orientation == ORIENTATION.V ? "column" : "row"} spacing={1} pt={1} pb={1}>
|
||||
{items.map((item, i) => (
|
||||
<FormItem
|
||||
key={i}
|
||||
item={item}
|
||||
value={selfValues?.[item.name] || ""}
|
||||
onChange={handleItemChange}
|
||||
fullWidth={orientation == ORIENTATION.V}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
<P8PComponentInlineMessage icon={COMPONENT_ICON} name={COMPONENT_NAME} message={P8P_COMPONENT_INLINE_MESSAGE.NO_SETTINGS} />
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Форма (представление)
|
||||
Form.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
orientation: PropTypes.oneOf(Object.values(ORIENTATION)),
|
||||
autoApply: PropTypes.bool,
|
||||
items: PropTypes.arrayOf(ITEM_SHAPE),
|
||||
values: PropTypes.object,
|
||||
onValuesChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default Form;
|
58
app/panels/panels_editor/components/indicator/editor.js
Normal file
58
app/panels/panels_editor/components/indicator/editor.js
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Компоненты: Индикатор (редактор настроек)
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useEffect, useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { P8PEditorBox } from "../../../../components/editors/p8p_editor_box"; //Контейнер редактора
|
||||
import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Заголовок раздела редактора
|
||||
import { P8P_DATA_SOURCE_SHAPE } from "../../../../components/editors/p8p_data_source_common"; //Общие ресурсы источника данных
|
||||
import { P8PDataSource } from "../../../../components/editors/p8p_data_source"; //Источник данных
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Индикатор (редактор настроек)
|
||||
const IndicatorEditor = ({ id, dataSource = null, valueProviders = {}, onSettingsChange = null } = {}) => {
|
||||
//Собственное состояние - текущие настройки
|
||||
const [settings, setSettings] = useState(null);
|
||||
|
||||
//При изменении компонента
|
||||
useEffect(() => {
|
||||
settings?.id != id && setSettings({ id, dataSource });
|
||||
}, [settings, id, dataSource]);
|
||||
|
||||
//При сохранении изменений элемента
|
||||
const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } }));
|
||||
|
||||
//При сохранении настроек
|
||||
const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor });
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<P8PEditorBox title={"Параметры индикатора"} onSave={handleSave}>
|
||||
<P8PEditorSubHeader title={"Источник данных"} />
|
||||
<P8PDataSource dataSource={settings?.dataSource} valueProviders={valueProviders} onChange={handleDataSourceChange} />
|
||||
</P8PEditorBox>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Индикатор (редактор настроек)
|
||||
IndicatorEditor.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
dataSource: P8P_DATA_SOURCE_SHAPE,
|
||||
valueProviders: PropTypes.object,
|
||||
onSettingsChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default IndicatorEditor;
|
87
app/panels/panels_editor/components/indicator/view.js
Normal file
87
app/panels/panels_editor/components/indicator/view.js
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Компоненты: Индикатор (представление)
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Paper } from "@mui/material"; //Интерфейсные элементы
|
||||
import { P8PIndicator } from "../../../../components/p8p_indicator"; //Компонент индикатора
|
||||
import { useDataSource } from "../../../../components/editors/p8p_data_source_hooks"; //Хуки для данных
|
||||
import { P8P_DATA_SOURCE_SHAPE } from "../../../../components/editors/p8p_data_source_common"; //Общие ресурсы источника данных
|
||||
import {
|
||||
P8P_COMPONENT_INLINE_MESSAGE_TYPE,
|
||||
P8P_COMPONENT_INLINE_MESSAGE,
|
||||
P8PComponentInlineMessage
|
||||
} from "../../../../components/editors/p8p_component_inline_message"; //Информационное сообщение внутри компонента
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Иконка компонента
|
||||
const COMPONENT_ICON = "speed";
|
||||
|
||||
//Наименование компонента
|
||||
const COMPONENT_NAME = "Индикатор";
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONTAINER: { height: "100%" }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Индикатор (представление)
|
||||
const Indicator = ({ dataSource = null, values = {} } = {}) => {
|
||||
//Собственное состояние - данные
|
||||
const [data, error] = useDataSource({ dataSource, values });
|
||||
|
||||
//Флаг настроенности индикатора
|
||||
const haveConfing = dataSource?.stored ? true : false;
|
||||
|
||||
//Флаг наличия данных
|
||||
const haveData = data?.init === true && !error ? true : false;
|
||||
|
||||
//Данные индикатора
|
||||
const indicator = data?.XINDICATOR || {};
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<Paper
|
||||
{...(haveConfing && haveData
|
||||
? { sx: { ...STYLES.CONTAINER } }
|
||||
: { className: "component-view__container component-view__container__empty" })}
|
||||
elevation={6}
|
||||
>
|
||||
{haveConfing && haveData ? (
|
||||
<P8PIndicator {...indicator} elevation={0} />
|
||||
) : (
|
||||
<P8PComponentInlineMessage
|
||||
icon={COMPONENT_ICON}
|
||||
name={COMPONENT_NAME}
|
||||
message={!haveConfing ? P8P_COMPONENT_INLINE_MESSAGE.NO_SETTINGS : error ? error : P8P_COMPONENT_INLINE_MESSAGE.NO_DATA_FOUND}
|
||||
type={error ? P8P_COMPONENT_INLINE_MESSAGE_TYPE.ERROR : P8P_COMPONENT_INLINE_MESSAGE_TYPE.COMMON}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Индикатор (представление)
|
||||
Indicator.propTypes = {
|
||||
dataSource: P8P_DATA_SOURCE_SHAPE,
|
||||
values: PropTypes.object
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default Indicator;
|
58
app/panels/panels_editor/components/table/editor.js
Normal file
58
app/panels/panels_editor/components/table/editor.js
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Компоненты: Таблица (редактор настроек)
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useEffect, useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { P8PEditorBox } from "../../../../components/editors/p8p_editor_box"; //Контейнер редактора
|
||||
import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Заголовок раздела редактора
|
||||
import { P8P_DATA_SOURCE_SHAPE } from "../../../../components/editors/p8p_data_source_common"; //Общие ресурсы источника данных
|
||||
import { P8PDataSource } from "../../../../components/editors/p8p_data_source"; //Источник данных
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Таблица (редактор настроек)
|
||||
const TableEditor = ({ id, dataSource = null, valueProviders = {}, onSettingsChange = null } = {}) => {
|
||||
//Собственное состояние - текущие настройки
|
||||
const [settings, setSettings] = useState(null);
|
||||
|
||||
//При изменении компонента
|
||||
useEffect(() => {
|
||||
settings?.id != id && setSettings({ id, dataSource });
|
||||
}, [settings, id, dataSource]);
|
||||
|
||||
//При сохранении изменений элемента
|
||||
const handleDataSourceChange = dataSource => setSettings(pv => ({ ...pv, dataSource: { ...dataSource } }));
|
||||
|
||||
//При сохранении настроек
|
||||
const handleSave = (closeEditor = false) => onSettingsChange && onSettingsChange({ id, settings, closeEditor });
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<P8PEditorBox title={"Параметры таблицы"} onSave={handleSave}>
|
||||
<P8PEditorSubHeader title={"Источник данных"} />
|
||||
<P8PDataSource dataSource={settings?.dataSource} valueProviders={valueProviders} onChange={handleDataSourceChange} />
|
||||
</P8PEditorBox>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Таблица (редактор настроек)
|
||||
TableEditor.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
dataSource: P8P_DATA_SOURCE_SHAPE,
|
||||
valueProviders: PropTypes.object,
|
||||
onSettingsChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default TableEditor;
|
99
app/panels/panels_editor/components/table/view.js
Normal file
99
app/panels/panels_editor/components/table/view.js
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Компоненты: Таблица (представление)
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Paper } from "@mui/material"; //Интерфейсные элементы
|
||||
import { APP_STYLES } from "../../../../../app.styles"; //Типовые стили
|
||||
import { P8PDataGrid } from "../../../../components/p8p_data_grid"; //Таблица данных
|
||||
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../../../config_wrapper"; //Подключение компонентов к настройкам приложения
|
||||
import { useDataSource } from "../../../../components/editors/p8p_data_source_hooks"; //Хуки для данных
|
||||
import { P8P_DATA_SOURCE_SHAPE } from "../../../../components/editors/p8p_data_source_common"; //Общие ресурсы источника данных
|
||||
import {
|
||||
P8P_COMPONENT_INLINE_MESSAGE_TYPE,
|
||||
P8P_COMPONENT_INLINE_MESSAGE,
|
||||
P8PComponentInlineMessage
|
||||
} from "../../../../components/editors/p8p_component_inline_message"; //Информационное сообщение внутри компонента
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Иконка компонента
|
||||
const COMPONENT_ICON = "table_view";
|
||||
|
||||
//Наименование компонента
|
||||
const COMPONENT_NAME = "Таблица";
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONTAINER: { display: "flex", height: "100%", overflow: "hidden" },
|
||||
DATA_GRID: { width: "100%" },
|
||||
DATA_GRID_CONTAINER: {
|
||||
height: `calc(100%)`,
|
||||
...APP_STYLES.SCROLL
|
||||
}
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Таблица (представление)
|
||||
const Table = ({ dataSource = null, values = {} } = {}) => {
|
||||
//Собственное состояние - данные
|
||||
const [data, error] = useDataSource({ dataSource, values });
|
||||
|
||||
//Флаг настроенности таблицы
|
||||
const haveConfing = dataSource?.stored ? true : false;
|
||||
|
||||
//Флаг наличия данных
|
||||
const haveData = data?.init === true && !error ? true : false;
|
||||
|
||||
//Данные таблицы
|
||||
const dataGrid = data?.XDATA_GRID || {};
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<Paper
|
||||
{...(haveConfing && haveData
|
||||
? { sx: { ...STYLES.CONTAINER } }
|
||||
: { className: "component-view__container component-view__container__empty" })}
|
||||
elevation={6}
|
||||
>
|
||||
{haveConfing && haveData ? (
|
||||
<P8PDataGrid
|
||||
{...P8P_DATA_GRID_CONFIG_PROPS}
|
||||
{...dataGrid}
|
||||
style={STYLES.DATA_GRID}
|
||||
containerComponentProps={{ sx: STYLES.DATA_GRID_CONTAINER, elevation: 0 }}
|
||||
/>
|
||||
) : (
|
||||
<P8PComponentInlineMessage
|
||||
icon={COMPONENT_ICON}
|
||||
name={COMPONENT_NAME}
|
||||
message={!haveConfing ? P8P_COMPONENT_INLINE_MESSAGE.NO_SETTINGS : error ? error : P8P_COMPONENT_INLINE_MESSAGE.NO_DATA_FOUND}
|
||||
type={error ? P8P_COMPONENT_INLINE_MESSAGE_TYPE.ERROR : P8P_COMPONENT_INLINE_MESSAGE_TYPE.COMMON}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Таблица (представление)
|
||||
Table.propTypes = {
|
||||
dataSource: P8P_DATA_SOURCE_SHAPE,
|
||||
values: PropTypes.object
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export default Table;
|
16
app/panels/panels_editor/index.js
Normal file
16
app/panels/panels_editor/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Редактор панелей: точка входа
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { PanelsEditor } from "./panels_editor"; //Корневая панель редактора
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export const RootClass = PanelsEditor;
|
86
app/panels/panels_editor/layout_item.js
Normal file
86
app/panels/panels_editor/layout_item.js
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Элемент макета
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { IconButton, Icon, Stack } from "@mui/material"; //Интерфейсные элементы
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONTAINER: selected => ({ zIndex: 1100, ...(selected ? { border: "2px dotted green" } : {}) }),
|
||||
STACK_TOOLS: { position: "absolute", zIndex: 1200, height: "100%", backgroundColor: "#c0c0c07f" }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Элемент макета
|
||||
// eslint-disable-next-line react/display-name
|
||||
const LayoutItem = React.forwardRef(
|
||||
(
|
||||
{ style, className, onMouseDown, onMouseUp, onTouchEnd, children, onSettingsClick, onDeleteClick, item, editMode = false, selected = false },
|
||||
ref
|
||||
) => {
|
||||
//При нажатии на настройки
|
||||
const handleSettingsClick = () => onSettingsClick && onSettingsClick(item.i);
|
||||
|
||||
//При нажатии на удаление
|
||||
const handleDeleteClick = () => onDeleteClick && onDeleteClick(item.i);
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<div
|
||||
style={{ ...style, ...STYLES.CONTAINER(selected) }}
|
||||
className={`${className} layout-item__container`}
|
||||
ref={ref}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onTouchEnd={onTouchEnd}
|
||||
>
|
||||
{editMode && (
|
||||
<Stack direction={"column"} sx={STYLES.STACK_TOOLS}>
|
||||
<IconButton onClick={handleSettingsClick}>
|
||||
<Icon>settings</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={handleDeleteClick}>
|
||||
<Icon>delete</Icon>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
//Контроль свойств компонента - элемент макета
|
||||
LayoutItem.propTypes = {
|
||||
style: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
onMouseDown: PropTypes.func,
|
||||
onMouseUp: PropTypes.func,
|
||||
onTouchEnd: PropTypes.func,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
||||
onSettingsClick: PropTypes.func,
|
||||
onDeleteClick: PropTypes.func,
|
||||
item: PropTypes.object.isRequired,
|
||||
editMode: PropTypes.bool,
|
||||
selected: PropTypes.bool
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { LayoutItem };
|
29
app/panels/panels_editor/panels_editor.css
Normal file
29
app/panels/panels_editor/panels_editor.css
Normal file
@ -0,0 +1,29 @@
|
||||
:root {
|
||||
--border-color: #dee2e6;
|
||||
--layout-bg: #ffffff;
|
||||
}
|
||||
|
||||
.layout {
|
||||
background-color: var(--layout-bg);
|
||||
}
|
||||
|
||||
.layout-item__container {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.component-view__wrap {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.component-view__container {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.component-view__container__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
253
app/panels/panels_editor/panels_editor.js
Normal file
253
app/panels/panels_editor/panels_editor.js
Normal file
@ -0,0 +1,253 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор панелей
|
||||
Корневой компонент
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useEffect, useState, useContext } from "react"; //Классы React
|
||||
import { Responsive, WidthProvider } from "react-grid-layout"; //Адаптивный макет
|
||||
import { Box, Grid, Menu, MenuItem, Icon, Fab } from "@mui/material"; //Интерфейсные элементы
|
||||
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
|
||||
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Рабочая область приложения
|
||||
import { P8PEditorToolBar } from "../../components/editors/p8p_editor_toolbar"; //Панель инструментов редактора
|
||||
import { genGUID } from "../../core/utils"; //Общие вспомогательные функции
|
||||
import { LayoutItem } from "./layout_item"; //Элемент макета
|
||||
import { ComponentView } from "./component_view"; //Представление компонента панели
|
||||
import { ComponentEditor } from "./component_editor"; //Редактор свойств компонента панели
|
||||
import { COMPONETNS } from "./components/components"; //Описание доступных компонентов
|
||||
import "react-grid-layout/css/styles.css"; //Стили для адаптивного макета
|
||||
import "react-resizable/css/styles.css"; //Стили для адаптивного макета
|
||||
import "./panels_editor.css"; //Стили редактора панелей
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONTAINER: { display: "flex" },
|
||||
GRID_CONTAINER: { height: `calc(100vh - ${APP_BAR_HEIGHT})` },
|
||||
GRID_ITEM_INSPECTOR: { backgroundColor: "#e9ecef" },
|
||||
FAB_EDIT: { position: "absolute", top: 12, right: 12, zIndex: 2000 }
|
||||
};
|
||||
|
||||
//Заголовоки по умолчанию
|
||||
const PANEL_CAPTION_EDIT_MODE = "Редактор панелей";
|
||||
const PANEL_CAPTION_EXECUTE_MODE = "Исполнение панели";
|
||||
|
||||
//Начальное состояние размера макета
|
||||
const INITIAL_BREAKPOINT = "lg";
|
||||
|
||||
//Начальное состояние макета
|
||||
const INITIAL_LAYOUTS = {
|
||||
[INITIAL_BREAKPOINT]: []
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Обёрдка для динамического макета
|
||||
const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||
|
||||
//Корневой компонент редактора панелей
|
||||
const PanelsEditor = () => {
|
||||
//Собственное состояние
|
||||
const [components, setComponents] = useState({});
|
||||
const [valueProviders, setValueProviders] = useState({});
|
||||
const [layouts, setLayouts] = useState(INITIAL_LAYOUTS);
|
||||
const [breakpoint, setBreakpoint] = useState(INITIAL_BREAKPOINT);
|
||||
const [editMode, setEditMode] = useState(true);
|
||||
const [editComponent, setEditComponent] = useState(null);
|
||||
const [addMenuAnchorEl, setAddMenuAnchorEl] = useState(null);
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { setAppBarTitle } = useContext(ApplicationСtx);
|
||||
|
||||
//Добвление компонента в макет
|
||||
const addComponent = component => {
|
||||
const id = genGUID();
|
||||
setLayouts(pv => ({ ...pv, [breakpoint]: [...pv[breakpoint], { i: id, x: 0, y: 0, w: 4, h: 10 }] }));
|
||||
setComponents(pv => ({ ...pv, [id]: { ...component } }));
|
||||
};
|
||||
|
||||
//Удаление компонента из макета
|
||||
const deleteComponent = id => {
|
||||
setLayouts(pv => ({ ...pv, [breakpoint]: layouts[breakpoint].filter(item => item.i !== id) }));
|
||||
setComponents(pv => ({ ...pv, [id]: { ...pv[id], deleted: true } }));
|
||||
if (valueProviders[id]) {
|
||||
const vPTmp = { ...valueProviders };
|
||||
delete vPTmp[id];
|
||||
setValueProviders(vPTmp);
|
||||
}
|
||||
editComponent === id && closeComponentSettingsEditor();
|
||||
};
|
||||
|
||||
//Включение/выключение режима редиктирования
|
||||
const toggleEditMode = () => {
|
||||
if (!editMode) setAppBarTitle(PANEL_CAPTION_EDIT_MODE);
|
||||
else setAppBarTitle(PANEL_CAPTION_EXECUTE_MODE);
|
||||
setEditMode(!editMode);
|
||||
};
|
||||
|
||||
//Открытие редактора настроек компонента
|
||||
const openComponentSettingsEditor = id => setEditComponent(id);
|
||||
|
||||
//Закрытие реактора настроек компонента
|
||||
const closeComponentSettingsEditor = () => setEditComponent(null);
|
||||
|
||||
//Открытие/сокрытие меню добавления
|
||||
const toggleAddMenu = target => setAddMenuAnchorEl(target instanceof Element ? target : null);
|
||||
|
||||
//При изменении размера холста
|
||||
const handleBreakpointChange = breakpoint => setBreakpoint(breakpoint);
|
||||
|
||||
//При изменении состояния макета
|
||||
const handleLayoutChange = (currentLayout, layouts) => setLayouts(layouts);
|
||||
|
||||
//При нажатии на кнопку добалвения
|
||||
const handleAddClick = e => toggleAddMenu(e.currentTarget);
|
||||
|
||||
//При выборе элемента меню добавления
|
||||
const handleAddMenuItemClick = component => {
|
||||
toggleAddMenu();
|
||||
addComponent(component);
|
||||
};
|
||||
|
||||
//При изменении значений в компоненте
|
||||
const handleComponentValuesChange = (id, values) => setValueProviders(pv => ({ ...pv, [id]: { ...values } }));
|
||||
|
||||
//При нажатии на настройки компонента
|
||||
const handleComponentSettingsClick = id => (editComponent === id ? closeComponentSettingsEditor() : openComponentSettingsEditor(id));
|
||||
|
||||
//При изменении настроек компонента
|
||||
const handleComponentSettingsChange = ({ id = null, settings = {}, providedValues = [], closeEditor = false } = {}) => {
|
||||
if (id && components[id]) {
|
||||
const providedValuesInit = providedValues.reduce((res, providedValue) => ({ ...res, [providedValue]: undefined }), {});
|
||||
if (valueProviders[id]) {
|
||||
const vPTmp = { ...valueProviders[id] };
|
||||
Object.keys(valueProviders[id]).forEach(key => !providedValues.includes(key) && delete vPTmp[key]);
|
||||
setValueProviders(pv => ({ ...pv, [id]: { ...providedValuesInit, ...vPTmp } }));
|
||||
} else setValueProviders(pv => ({ ...pv, [id]: providedValuesInit }));
|
||||
setComponents(pv => ({ ...pv, [editComponent]: { ...pv[editComponent], settings: { ...settings } } }));
|
||||
if (closeEditor === true) closeComponentSettingsEditor();
|
||||
}
|
||||
};
|
||||
|
||||
//При удалении компоненета
|
||||
const handleComponentDeleteClick = id => deleteComponent(id);
|
||||
|
||||
//При подключении к странице
|
||||
useEffect(() => {
|
||||
//addComponent(COMPONETNS[0]);
|
||||
//addComponent(COMPONETNS[3]);
|
||||
//addComponent(COMPONETNS[4]);
|
||||
//addComponent(COMPONETNS[1]);
|
||||
//addComponent(COMPONETNS[2]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
//Текущие значения панели
|
||||
const values = Object.keys(valueProviders).reduce((res, key) => ({ ...res, ...valueProviders[key] }), {});
|
||||
|
||||
//Меню добавления
|
||||
const addMenu = (
|
||||
<Menu anchorEl={addMenuAnchorEl} open={Boolean(addMenuAnchorEl)} onClose={toggleAddMenu}>
|
||||
{COMPONETNS.map((comp, i) => (
|
||||
<MenuItem key={i} onClick={() => handleAddMenuItemClick(comp)}>
|
||||
{comp.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
//Кнопка редактирования
|
||||
const editButton = !editMode && (
|
||||
<Fab sx={STYLES.FAB_EDIT} size={"small"} color={"grey.700"} title={"Редактировать"} onClick={toggleEditMode}>
|
||||
<Icon>edit</Icon>
|
||||
</Fab>
|
||||
);
|
||||
|
||||
//Панель инструмментов
|
||||
const toolBar = (
|
||||
<P8PEditorToolBar
|
||||
items={[
|
||||
{ icon: "play_arrow", title: "Запустить", onClick: toggleEditMode },
|
||||
{
|
||||
icon: "add",
|
||||
title: "Добавить элемент",
|
||||
onClick: handleAddClick
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box sx={STYLES.CONTAINER}>
|
||||
{editButton}
|
||||
{addMenu}
|
||||
<Grid container sx={STYLES.GRID_CONTAINER} columns={25}>
|
||||
<Grid item xs={editMode ? 20 : 25}>
|
||||
<ResponsiveGridLayout
|
||||
rowHeight={5}
|
||||
className={"layout"}
|
||||
layouts={layouts}
|
||||
breakpoints={{ lg: 1200 }}
|
||||
cols={{ lg: 12 }}
|
||||
onBreakpointChange={handleBreakpointChange}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
useCSSTransforms={true}
|
||||
compactType={"vertical"}
|
||||
isDraggable={editMode}
|
||||
isResizable={editMode}
|
||||
>
|
||||
{layouts[breakpoint].map(item => (
|
||||
<LayoutItem
|
||||
key={item.i}
|
||||
onSettingsClick={handleComponentSettingsClick}
|
||||
onDeleteClick={handleComponentDeleteClick}
|
||||
item={item}
|
||||
editMode={editMode}
|
||||
selected={editMode && editComponent === item.i}
|
||||
>
|
||||
<ComponentView
|
||||
id={item.i}
|
||||
path={components[item.i]?.path}
|
||||
settings={components[item.i]?.settings}
|
||||
values={values}
|
||||
onValuesChange={handleComponentValuesChange}
|
||||
/>
|
||||
</LayoutItem>
|
||||
))}
|
||||
</ResponsiveGridLayout>
|
||||
</Grid>
|
||||
{editMode && (
|
||||
<Grid item xs={5} sx={STYLES.GRID_ITEM_INSPECTOR}>
|
||||
{toolBar}
|
||||
{editComponent && (
|
||||
<>
|
||||
<ComponentEditor
|
||||
id={editComponent}
|
||||
path={components[editComponent].path}
|
||||
settings={components[editComponent].settings}
|
||||
valueProviders={valueProviders}
|
||||
onSettingsChange={handleComponentSettingsChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { PanelsEditor };
|
170
app/panels/prj_info/filter.js
Normal file
170
app/panels/prj_info/filter.js
Normal file
@ -0,0 +1,170 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Фильтр
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Grid, Chip, Stack, Input, InputAdornment, IconButton, Icon } from "@mui/material"; //Интерфейсные элементы
|
||||
import { FILTER_INITIAL, FILTER_ITEMS, PRICE_STRUCT_STATUS, PROJECT_STATE, FilterDialog } from "./filter_dialog"; //Компонент "Диалог фильтра"
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONTAINER: { paddingTop: "10px" },
|
||||
FILTER: { maxWidth: "99vw" },
|
||||
SEARCH_GRID_ITEM: { paddingRight: "15px" }
|
||||
};
|
||||
|
||||
//------------------------------------
|
||||
//Вспомогательные функции и компоненты
|
||||
//------------------------------------
|
||||
|
||||
//Элемент фильтра
|
||||
const FilterItem = ({ caption, value, defaultValue, onClick, onDelete }) => {
|
||||
//При нажатии на элемент
|
||||
const handleClick = () => (onClick ? onClick() : null);
|
||||
|
||||
//При нажатии на удаление элемента
|
||||
const handleDelete = () => (onDelete ? onDelete() : null);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Chip
|
||||
label={
|
||||
<Stack direction={"row"} alignItems={"center"}>
|
||||
<strong>{caption}</strong>: {value || defaultValue}
|
||||
</Stack>
|
||||
}
|
||||
variant="outlined"
|
||||
onClick={handleClick}
|
||||
onDelete={onDelete ? handleDelete : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Элемент фильтра
|
||||
FilterItem.propTypes = {
|
||||
caption: PropTypes.string.isRequired,
|
||||
value: PropTypes.any,
|
||||
defaultValue: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
onDelete: PropTypes.func
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Фильтр
|
||||
const Filter = ({ values, onChange }) => {
|
||||
//Собственное состояние - отображение диалога ввода значений фильтра
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
//Собственное состояние - строка поиска
|
||||
const [search, setSearch] = useState(values.search);
|
||||
|
||||
//Передача сообщения об измении фильтра родителю
|
||||
const notifyChange = values => (onChange ? onChange(values) : null);
|
||||
|
||||
//При закрытии диалога с сохранением значений
|
||||
const handleFilterDialogOk = values => {
|
||||
setIsOpen(false);
|
||||
notifyChange(values);
|
||||
};
|
||||
|
||||
//При закрытии диалога без сохранения значений
|
||||
const handleFilterDialogCancel = () => setIsOpen(false);
|
||||
|
||||
//При нажатии на фильтр
|
||||
const handleClick = () => setIsOpen(true);
|
||||
|
||||
//При выполнении поиска
|
||||
const handleDoSearch = (clear = false) => {
|
||||
if (clear === true) setSearch("");
|
||||
notifyChange({ ...values, search: clear === true ? "" : search });
|
||||
};
|
||||
|
||||
//При изменении значения в строке поиска
|
||||
const handleSearchChange = e => setSearch(e.target.value);
|
||||
|
||||
//При нажатии клавиши в строке поиска
|
||||
const handleSearchKeyPress = e => ([13, 27].includes(e.keyCode) ? handleDoSearch(e.keyCode == 27) : null);
|
||||
|
||||
//Формирование функции обработки очистки элемента фильтар
|
||||
const buildFilterItemClearHandler = сode =>
|
||||
values[сode] != FILTER_INITIAL[сode] ? () => notifyChange({ ...values, [сode]: FILTER_INITIAL[сode] }) : null;
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Grid container sx={STYLES.CONTAINER}>
|
||||
<Grid xs={10} item>
|
||||
{isOpen ? <FilterDialog valuesInitial={values} onOk={handleFilterDialogOk} onCancel={handleFilterDialogCancel} /> : null}
|
||||
<Stack direction="row" spacing={1} p={1} alignItems={"center"} sx={STYLES.FILTER} onClick={handleClick}>
|
||||
<FilterItem
|
||||
caption={"Тип заказа"}
|
||||
value={values.prjType}
|
||||
defaultValue={"Любой"}
|
||||
onDelete={buildFilterItemClearHandler("prjType")}
|
||||
/>
|
||||
<FilterItem
|
||||
caption={"Подразделение-ответственный"}
|
||||
defaultValue={"Любое"}
|
||||
value={values.insDep}
|
||||
onDelete={buildFilterItemClearHandler("insDep")}
|
||||
/>
|
||||
<FilterItem
|
||||
caption={"Статус структуры цены"}
|
||||
defaultValue={"Неподдерживаемое значение"}
|
||||
value={PRICE_STRUCT_STATUS.find(item => item.value == values.priceStructStatus)?.name}
|
||||
onDelete={buildFilterItemClearHandler("priceStructStatus")}
|
||||
/>
|
||||
<FilterItem
|
||||
caption={"Состояние проекта"}
|
||||
defaultValue={"Неподдерживаемое значение"}
|
||||
value={PROJECT_STATE.find(item => item.value == values.prjState)?.name}
|
||||
onDelete={buildFilterItemClearHandler("prjState")}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid xs={2} item sx={STYLES.SEARCH_GRID_ITEM}>
|
||||
<Input
|
||||
fullWidth
|
||||
placeholder="Поиск..."
|
||||
value={search}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => handleDoSearch(true)}>
|
||||
<Icon>clear</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={handleDoSearch}>
|
||||
<Icon>search</Icon>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
onKeyDown={handleSearchKeyPress}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Фильтр
|
||||
Filter.propTypes = {
|
||||
values: FILTER_ITEMS.isRequired,
|
||||
onChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { FILTER_INITIAL, Filter };
|
147
app/panels/prj_info/filter_dialog.js
Normal file
147
app/panels/prj_info/filter_dialog.js
Normal file
@ -0,0 +1,147 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Диалог фильтра
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useContext } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Button, Dialog, DialogTitle, DialogContent, DialogActions } from "@mui/material"; //Интерфейсные элементы
|
||||
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
|
||||
import { BUTTONS } from "../../../app.text"; //Типовые тексты
|
||||
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
|
||||
import { FormField } from "./layouts"; //Общие компоненты панели
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
DIALOG_CONTENT: { overflowY: "auto", ...APP_STYLES.SCROLL }
|
||||
};
|
||||
|
||||
//Структура фильтра
|
||||
const FILTER_ITEMS = PropTypes.shape({
|
||||
prjType: PropTypes.string,
|
||||
insDep: PropTypes.string,
|
||||
priceStructStatus: PropTypes.number.isRequired,
|
||||
prjState: PropTypes.number.isRequired,
|
||||
search: PropTypes.string
|
||||
});
|
||||
|
||||
//Начальное состояние фильтра
|
||||
const FILTER_INITIAL = { prjType: "", insDep: "", priceStructStatus: 0, prjState: 0, search: "" };
|
||||
|
||||
//Статусы структуры цены
|
||||
const PRICE_STRUCT_STATUS = [
|
||||
{ value: 0, name: "Все" },
|
||||
{ value: 1, name: "Есть статьи с расходом больше 90%" },
|
||||
{ value: 2, name: "Есть статьи с перерасходом" }
|
||||
];
|
||||
|
||||
//Состояния проекта
|
||||
const PROJECT_STATE = [
|
||||
{ value: 0, name: "Все" },
|
||||
{ value: 1, name: "Открытые" },
|
||||
{ value: 2, name: "Неоткрытые" }
|
||||
];
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Диалог фильтра
|
||||
const FilterDialog = ({ valuesInitial, onOk, onCancel }) => {
|
||||
//Собственное состояние элементов фильтра
|
||||
const [values, setValues] = useState({ ...valuesInitial });
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
|
||||
|
||||
//Изменение элемента формы фильтра
|
||||
const handleValueChanged = (name, value) => setValues(pv => ({ ...pv, [name]: value }));
|
||||
|
||||
//Сброс настроек фильтра
|
||||
const handleResetClick = () => setValues({ ...FILTER_INITIAL });
|
||||
|
||||
//Сохранение фильтра
|
||||
const handleOkClick = () => (onOk ? onOk(values) : null);
|
||||
|
||||
//Отмена фильтра
|
||||
const handleCancelClick = () => (onCancel ? onCancel() : null);
|
||||
|
||||
//Выбор значения элемента формы из словаря
|
||||
const selectFromDictionary = (unitCode, name, applyValue) => {
|
||||
pOnlineShowDictionary({
|
||||
unitCode,
|
||||
showMethod: "main",
|
||||
inputParameters: [{ name: "in_CODE", value: values[name] }],
|
||||
callBack: res => applyValue(res.success ? [{ name, value: res.outParameters.out_CODE }] : null)
|
||||
});
|
||||
};
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Dialog open onClose={handleCancelClick} fullWidth maxWidth={"md"}>
|
||||
<DialogTitle>Фильтр отбора</DialogTitle>
|
||||
<DialogContent sx={STYLES.DIALOG_CONTENT}>
|
||||
<FormField
|
||||
elementCode={"prjType"}
|
||||
elementValue={values.prjType}
|
||||
labelText={"Тип заказа"}
|
||||
onChange={handleValueChanged}
|
||||
dictionary={applyValue => selectFromDictionary("ProjectTypes", "prjType", applyValue)}
|
||||
/>
|
||||
<FormField
|
||||
elementCode={"insDep"}
|
||||
elementValue={values.insDep}
|
||||
labelText={"Подразделение-ответственный"}
|
||||
onChange={handleValueChanged}
|
||||
dictionary={applyValue => selectFromDictionary("INS_DEPARTMENT", "insDep", applyValue)}
|
||||
/>
|
||||
<FormField
|
||||
elementCode={"priceStructStatus"}
|
||||
elementValue={values.priceStructStatus}
|
||||
labelText={"Статус структуры цены"}
|
||||
onChange={handleValueChanged}
|
||||
list={PRICE_STRUCT_STATUS}
|
||||
/>
|
||||
<FormField
|
||||
elementCode={"prjState"}
|
||||
elementValue={values.prjState}
|
||||
labelText={"Состояние проекта"}
|
||||
onChange={handleValueChanged}
|
||||
list={PROJECT_STATE}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={STYLES.DIALOG_ACTIONS}>
|
||||
<Button variant="text" onClick={handleOkClick}>
|
||||
{BUTTONS.OK}
|
||||
</Button>
|
||||
<Button variant="text" onClick={handleResetClick}>
|
||||
{BUTTONS.CLEAR}
|
||||
</Button>
|
||||
<Button variant="text" onClick={handleCancelClick}>
|
||||
{BUTTONS.CANCEL}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Диалог фильтра
|
||||
FilterDialog.propTypes = {
|
||||
valuesInitial: FILTER_ITEMS.isRequired,
|
||||
onOk: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { FILTER_ITEMS, FILTER_INITIAL, PRICE_STRUCT_STATUS, PROJECT_STATE, FilterDialog };
|
16
app/panels/prj_info/index.js
Normal file
16
app/panels/prj_info/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Панель мониторинга: точка входа
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { PrjInfo } from "./prj_info"; //Корневая панель информации о проектах
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export const RootClass = PrjInfo;
|
168
app/panels/prj_info/layouts.js
Normal file
168
app/panels/prj_info/layouts.js
Normal file
@ -0,0 +1,168 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Общие дополнительная разметка и вёрстка клиентских элементов
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useEffect } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Box, Icon, Input, InputAdornment, FormControl, Select, InputLabel, MenuItem, IconButton, Typography, Switch, Stack } from "@mui/material"; //Интерфейсные компоненты
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
STATE: value => ({ color: value === 1 ? "green" : "black" }),
|
||||
COST_STATUS: color => ({ color, verticalAlign: "middle" }),
|
||||
COST_READY: value => ({ color: value <= 30 ? "red" : value >= 80 ? "green" : "#e2af00" }),
|
||||
TOGGLE_COLOR: checked => ({ color: checked ? "#006dd9" : "lightgrey" })
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Поле ввода формы
|
||||
const FormField = ({ elementCode, elementValue, labelText, onChange, dictionary, list, type, ...other }) => {
|
||||
//Значение элемента
|
||||
const [value, setValue] = useState(elementValue);
|
||||
|
||||
//При получении нового значения из вне
|
||||
useEffect(() => {
|
||||
setValue(elementValue);
|
||||
}, [elementValue]);
|
||||
|
||||
//Выбор значения из словаря
|
||||
const handleDictionaryClick = () =>
|
||||
dictionary ? dictionary(res => (res ? res.map(i => handleChange({ target: { name: i.name, value: i.value } })) : null)) : null;
|
||||
|
||||
//Изменение значения элемента (по событию)
|
||||
const handleChange = e => {
|
||||
setValue(e.target.value);
|
||||
if (onChange) onChange(e.target.name, e.target.value);
|
||||
};
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box p={1}>
|
||||
<FormControl variant="standard" fullWidth {...other}>
|
||||
{list ? (
|
||||
<>
|
||||
<InputLabel id={`${elementCode}Lable`} shrink>
|
||||
{labelText}
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`${elementCode}Lable`}
|
||||
id={elementCode}
|
||||
name={elementCode}
|
||||
label={labelText}
|
||||
value={value || value == 0 ? value : ""}
|
||||
onChange={handleChange}
|
||||
displayEmpty
|
||||
>
|
||||
{list.map((item, i) => (
|
||||
<MenuItem key={i} value={item.value || item.value == 0 ? item.value : ""}>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<InputLabel {...(type == "date" ? { shrink: true } : {})} htmlFor={elementCode}>
|
||||
{labelText}
|
||||
</InputLabel>
|
||||
<Input
|
||||
id={elementCode}
|
||||
name={elementCode}
|
||||
value={value || value == 0 ? value : ""}
|
||||
endAdornment={
|
||||
dictionary ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton aria-label={`${elementCode} select`} onClick={handleDictionaryClick} edge="end">
|
||||
<Icon>list</Icon>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
) : null
|
||||
}
|
||||
{...(type ? { type } : {})}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Поле ввода формы
|
||||
FormField.propTypes = {
|
||||
elementCode: PropTypes.string.isRequired,
|
||||
elementValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.instanceOf(Date)]),
|
||||
labelText: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
dictionary: PropTypes.func,
|
||||
list: PropTypes.array,
|
||||
type: PropTypes.string
|
||||
};
|
||||
|
||||
//Переключатель
|
||||
const Toggle = ({ labels, checked, onChange }) => {
|
||||
//Обработка переключения
|
||||
const handleChange = event => (onChange ? onChange(event.target.checked) : null);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Stack direction={"row"} spacing={1} alignItems={"center"} justifyContent={"center"}>
|
||||
<Typography sx={STYLES.TOGGLE_COLOR(!checked)}>{labels[0]}</Typography>
|
||||
<Switch checked={checked} size="small" onChange={handleChange} />
|
||||
<Typography sx={STYLES.TOGGLE_COLOR(checked)}>{labels[1]}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Переключатель
|
||||
Toggle.propTypes = {
|
||||
labels: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
checked: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func
|
||||
};
|
||||
|
||||
//Формирование значения для колонки "Статус структуры цены"
|
||||
const formatCostStatusValue = ({ value, onClick, type = 1 }) => {
|
||||
const [text, color] =
|
||||
value == 0
|
||||
? ["Без отклонений", "lightgreen"]
|
||||
: value == 1
|
||||
? [type == 1 ? "Есть статьи с расходом более 90%" : "Расход более 90%", "#ffdf71"]
|
||||
: value == 2
|
||||
? [type == 1 ? "Есть статьи с перерасходом" : "Перерасход", "#eb6b6b"]
|
||||
: ["Не определено", "lightgray"];
|
||||
return onClick ? (
|
||||
<IconButton onClick={onClick}>
|
||||
<Icon sx={STYLES.COST_STATUS(color)} title={`${text}\nНажмите для детальной информации`}>
|
||||
circle
|
||||
</Icon>
|
||||
</IconButton>
|
||||
) : (
|
||||
<Icon sx={STYLES.COST_STATUS(color)} title={text}>
|
||||
circle
|
||||
</Icon>
|
||||
);
|
||||
};
|
||||
|
||||
//Формирование значения для колонки "Готов (%, зтраты)"
|
||||
const formatCostReadyValue = value => {
|
||||
return <span style={STYLES.COST_READY(value)}>{value}</span>;
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { STYLES as COMMON_STYLES, FormField, Toggle, formatCostStatusValue, formatCostReadyValue };
|
27
app/panels/prj_info/prj_info.js
Normal file
27
app/panels/prj_info/prj_info.js
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Корневой компонент панели
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import { Projects } from "./projects"; //Список проектов
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Корневой компонент панели "Информация о проектах"
|
||||
const PrjInfo = () => {
|
||||
//Генерация содержимого
|
||||
return <Projects />;
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { PrjInfo };
|
70
app/panels/prj_info/projects.js
Normal file
70
app/panels/prj_info/projects.js
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Список проектов
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useContext } from "react"; //Классы React
|
||||
import { P8PDataGrid } from "../../components/p8p_data_grid"; //Таблица данных
|
||||
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
|
||||
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
|
||||
import { useProjectsDataGrid } from "./projects_hooks"; //Хуки списка проектов
|
||||
import { FILTER_INITIAL, Filter } from "./filter"; //Компонент "Фильтр"
|
||||
import { PROJECTS_STYLES, projectDataCellRender, projectRowExpandRender } from "./projects_layouts"; //Дополнительная разметка и вёрстка клиентских элементов
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Список проектов
|
||||
const Projects = () => {
|
||||
//Собственное состояние
|
||||
const [projects, setProjects] = useState({ pageNumber: 1, orders: [], filter: { ...FILTER_INITIAL } });
|
||||
|
||||
//Состояние таблицы проектов
|
||||
const [projectsDataGrid] = useProjectsDataGrid({ ...projects.filter, pageNumber: projects.pageNumber, orders: projects.orders });
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowDocument } = useContext(ApplicationСtx);
|
||||
|
||||
//Отображение записи проекта в штатном разделе
|
||||
const showProject = async rn => pOnlineShowDocument({ unitCode: "Projects", document: rn, modal: false });
|
||||
|
||||
//При изменении количества отображаемых страниц
|
||||
const handlePagesCountChanged = () => setProjects(pv => ({ ...pv, pageNumber: pv.pageNumber + 1 }));
|
||||
|
||||
//При изменении состояния сортировки
|
||||
const handleOrderChanged = ({ orders }) => setProjects(pv => ({ ...pv, orders: [...orders], pageNumber: 1 }));
|
||||
|
||||
//При изменении фильтра
|
||||
const handleFilterChanged = values => setProjects(pv => ({ ...pv, filter: { ...values }, pageNumber: 1 }));
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<>
|
||||
<Filter values={projects.filter} onChange={handleFilterChanged} />
|
||||
{projectsDataGrid.init ? (
|
||||
<P8PDataGrid
|
||||
{...P8P_DATA_GRID_CONFIG_PROPS}
|
||||
{...projectsDataGrid}
|
||||
containerComponentProps={{ sx: PROJECTS_STYLES.DATA_GRID_CONTAINER(projectsDataGrid.morePages), elevation: 0 }}
|
||||
expandable={true}
|
||||
fixedHeader={true}
|
||||
onPagesCountChanged={handlePagesCountChanged}
|
||||
onOrderChanged={handleOrderChanged}
|
||||
dataCellRender={prms => projectDataCellRender({ ...prms, showProject })}
|
||||
rowExpandRender={projectRowExpandRender}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { Projects };
|
82
app/panels/prj_info/projects_hooks.js
Normal file
82
app/panels/prj_info/projects_hooks.js
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Список проектов: пользовательские хуки для взаимодействия с сервером
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { useState, useContext, useEffect } from "react"; //Классы React
|
||||
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
|
||||
import { object2Base64XML, formatDateRF } from "../../core/utils"; //Вспомогательные функции
|
||||
import config from "../../../app.config"; //Настройки приложения
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Размер страницы данных
|
||||
const DATA_GRID_PAGE_SIZE = config.SYSTEM.PAGE_SIZE;
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Получение данных проектов с сервера
|
||||
const useProjectsDataGrid = ({ prjType, insDep, priceStructStatus, prjState, search, pageNumber, orders }) => {
|
||||
//Собственное состояние - флаг загрузки
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
//Собственное состояние - таблица данных
|
||||
const [data, setData] = useState({ init: false, morePages: true });
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
|
||||
|
||||
//При необходимости обновить данные таблицы
|
||||
useEffect(() => {
|
||||
//Загрузка данных таблицы с сервера
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.INFO_PROJECTS_DG",
|
||||
args: {
|
||||
SPRJ_TYPE: prjType,
|
||||
SINS_DEPARTMENT: insDep,
|
||||
NCOST_STATUS: priceStructStatus,
|
||||
NSTATE: prjState,
|
||||
SSEARCH: search,
|
||||
CORDERS: { VALUE: object2Base64XML(orders, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
|
||||
NPAGE_NUMBER: pageNumber,
|
||||
NPAGE_SIZE: DATA_GRID_PAGE_SIZE,
|
||||
NINCLUDE_DEF: pageNumber == 1 ? 1 : 0
|
||||
},
|
||||
respArg: "COUT",
|
||||
loader: true,
|
||||
attributeValueProcessor: (name, val) => (["DBEGPLAN", "DENDPLAN"].includes(name) ? formatDateRF(val) : val)
|
||||
});
|
||||
setData(pv => ({
|
||||
...pv,
|
||||
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef || [],
|
||||
rows: pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...(pv.rows || []), ...(data.XDATA_GRID.rows || [])],
|
||||
morePages: DATA_GRID_PAGE_SIZE == 0 ? false : (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE,
|
||||
init: true
|
||||
}));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
loadData();
|
||||
}, [prjType, insDep, priceStructStatus, prjState, search, pageNumber, orders, executeStored, SERV_DATA_TYPE_CLOB]);
|
||||
|
||||
//Возвращаем интерфейс хука
|
||||
return [data, isLoading];
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { useProjectsDataGrid };
|
96
app/panels/prj_info/projects_layouts.js
Normal file
96
app/panels/prj_info/projects_layouts.js
Normal file
@ -0,0 +1,96 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Список проектов: дополнительная разметка и вёрстка клиентских элементов
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import { Icon, Stack, Paper, Link } from "@mui/material"; //Интерфейсные элементы
|
||||
import { P8P_DATA_GRID_MORE_HEIGHT } from "../../components/p8p_data_grid"; //Таблица данных
|
||||
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
|
||||
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
|
||||
import { COMMON_STYLES, formatCostStatusValue, formatCostReadyValue } from "./layouts"; //Общие стили и разметка панели
|
||||
import { Stages } from "./stages"; //Компонент "Этапы проекта"
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Высота фильтра (пиксели)
|
||||
const FILTER_HEIGHT = "60px";
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
DATA_GRID_CONTAINER: morePages => ({
|
||||
height: `calc(100vh - ${APP_BAR_HEIGHT} - ${FILTER_HEIGHT} - ${morePages ? P8P_DATA_GRID_MORE_HEIGHT : "0px"} - 8px)`,
|
||||
...APP_STYLES.SCROLL
|
||||
})
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Формирование значения для колонки "Состояние" проекта
|
||||
const formatPrjStateValue = value => {
|
||||
const [text, icon] =
|
||||
value == 0
|
||||
? ["Зарегистрирован", "app_registration"]
|
||||
: value == 1
|
||||
? ["Открыт", "lock_open"]
|
||||
: value == 2
|
||||
? ["Остановлен", "do_not_disturb_on"]
|
||||
: value == 3
|
||||
? ["Закрыт", "lock_outline"]
|
||||
: value == 4
|
||||
? ["Согласован", "thumb_up_alt"]
|
||||
: ["Исполнение прекращено", "block"];
|
||||
return (
|
||||
<Stack direction="row" gap={0.5} alignItems="center" justifyContent="center">
|
||||
<Icon title={text} sx={COMMON_STYLES.STATE(value)}>
|
||||
{icon}
|
||||
</Icon>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
//Форматирование ячеек таблицы "Проекты"
|
||||
const projectDataCellRender = ({ row, columnDef, showProject }) => {
|
||||
//Формирование представлений
|
||||
switch (columnDef.name) {
|
||||
case "NCOST_STATUS":
|
||||
return { cellProps: { align: "center" }, data: formatCostStatusValue({ value: row[columnDef.name] }) };
|
||||
case "NCOST_READY":
|
||||
return { cellProps: { align: "center" }, data: formatCostReadyValue(row[columnDef.name]) };
|
||||
case "NSTATE":
|
||||
return { cellProps: { align: "center" }, data: formatPrjStateValue(row[columnDef.name]) };
|
||||
case "SCODE":
|
||||
return {
|
||||
data: (
|
||||
<Link component="button" align="left" underline="hover" onClick={() => showProject(row["NRN"])}>
|
||||
{row[columnDef.name]}
|
||||
</Link>
|
||||
)
|
||||
};
|
||||
default:
|
||||
return { data: row[columnDef.name] };
|
||||
}
|
||||
};
|
||||
|
||||
//Генерация представления расширения строки таблицы "Проектов"
|
||||
const projectRowExpandRender = ({ row }) => {
|
||||
return (
|
||||
<Paper elevation={6}>
|
||||
<Stages projectRn={row.NRN} projectCode={row.SCODE} />
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { STYLES as PROJECTS_STYLES, projectDataCellRender, projectRowExpandRender };
|
173
app/panels/prj_info/stage_detail.js
Normal file
173
app/panels/prj_info/stage_detail.js
Normal file
@ -0,0 +1,173 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Детальная информация об этапе проекта
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useContext } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Grid, Box, Typography, Paper, Drawer, IconButton, Icon } from "@mui/material"; //Интерфейсные элементы
|
||||
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
|
||||
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
|
||||
import { MessagingСtx } from "../../context/messaging"; //Контекст сообщений
|
||||
import { TEXTS } from "../../../app.text"; //Тектовые ресурсы и константы
|
||||
import { P8PDataGrid, P8P_DATA_GRID_SIZE } from "../../components/p8p_data_grid"; //Таблица данных
|
||||
import { P8PChart } from "../../components/p8p_chart"; //График
|
||||
import { P8PAppInlineError } from "../../components/p8p_app_message"; //Встраиваемое сообщение об ошибке
|
||||
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
|
||||
import { useStageDetailInfoDataGrid, useStageDetailArtsDataGrid, useStageDetailArtsChart } from "./stage_detail_hooks"; //Хуки детализации этапов проекта
|
||||
import { Toggle } from "./layouts"; //Общая разметка и компоненты панели
|
||||
import {
|
||||
STAGE_DETAIL_STYLES,
|
||||
stageDetailInfoHeadCellRender,
|
||||
stageDetailInfoDataCellRender,
|
||||
stageDetailArtsHeadCellRender,
|
||||
stageDetailArtsDataCellRender
|
||||
} from "./stage_detail_layouts"; //Дополнительная разметка и вёрстка клиентских элементов
|
||||
|
||||
//------------------------------------
|
||||
//Вспомогательные функции и компоненты
|
||||
//------------------------------------
|
||||
|
||||
//Данные этапа
|
||||
const StageDetailData = ({ stageRn }) => {
|
||||
//Собственное состояние
|
||||
const [state, setState] = useState({ artsDisplayType: 0, artsChartType: 0 });
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored } = useContext(BackEndСtx);
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowUnit } = useContext(ApplicationСtx);
|
||||
|
||||
//Подключение к контексту сообщений
|
||||
const { showMsgErr } = useContext(MessagingСtx);
|
||||
|
||||
//Отображение журнала затрат (фактического, по рег. номеру ЛС и статьи затрат)
|
||||
const showCostNotesFact = async ({ faceAccRn, artclRn }) => {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.INFO_FCCOSTNOTES_FACT_SELECT",
|
||||
args: { NFACEACC: faceAccRn, NFPDARTCL: artclRn }
|
||||
});
|
||||
if (data.NIDENT) pOnlineShowUnit({ unitCode: "CostNotes", inputParameters: [{ name: "in_IDENT", value: data.NIDENT }] });
|
||||
else showMsgErr(TEXTS.NO_DATA_FOUND);
|
||||
};
|
||||
|
||||
//Состояние таблицы с информацией об этапе
|
||||
const [stageDeatilInfoDataGrid] = useStageDetailInfoDataGrid({ stageRn });
|
||||
|
||||
//Состояние таблицы с данными структуры цены
|
||||
const [stageDeatilArtsDataGrid] = useStageDetailArtsDataGrid({ stageRn });
|
||||
|
||||
//Состояние графика с данными структуры цены
|
||||
const [stageDeatilArtsChart] = useStageDetailArtsChart({ stageRn, display: state.artsDisplayType == 1, type: state.artsChartType });
|
||||
|
||||
//При изменении способа отображения структуры цены
|
||||
const handleArtsDisplayTypeChange = checked => setState(pv => ({ ...pv, artsDisplayType: checked ? 1 : 0 }));
|
||||
|
||||
//При изменении типа данных графика структуры цены
|
||||
const handleArtsChartTypeChange = checked => setState(pv => ({ ...pv, artsChartType: checked ? 1 : 0 }));
|
||||
|
||||
//Отработка нажатия на график
|
||||
const handleChartClick = ({ item }) =>
|
||||
state.artsChartType === 1 && item.NFACEACC && item.NFPDARTCL
|
||||
? showCostNotesFact({ faceAccRn: item.NFACEACC, artclRn: item.NFPDARTCL })
|
||||
: null;
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Grid container spacing={2} sx={STAGE_DETAIL_STYLES.DATA_AREA_CONTAINER}>
|
||||
<Grid item xs={5}>
|
||||
<Typography variant={"h6"} sx={STAGE_DETAIL_STYLES.DATA_AREA_HEADER}>
|
||||
Сведения
|
||||
</Typography>
|
||||
{stageDeatilInfoDataGrid.init ? (
|
||||
<P8PDataGrid
|
||||
{...P8P_DATA_GRID_CONFIG_PROPS}
|
||||
containerComponentProps={{ sx: STAGE_DETAIL_STYLES.DATA_AREA, elevation: 6 }}
|
||||
{...stageDeatilInfoDataGrid}
|
||||
size={P8P_DATA_GRID_SIZE.SMALL}
|
||||
fixedHeader={true}
|
||||
headCellRender={stageDetailInfoHeadCellRender}
|
||||
dataCellRender={stageDetailInfoDataCellRender}
|
||||
/>
|
||||
) : null}
|
||||
</Grid>
|
||||
<Grid item xs={7}>
|
||||
<Box sx={STAGE_DETAIL_STYLES.DATA_AREA_HEADER_CONTAINER}>
|
||||
<Typography variant={"h6"} sx={STAGE_DETAIL_STYLES.DATA_AREA_HEADER}>
|
||||
Структура цены
|
||||
</Typography>
|
||||
<Toggle labels={["Таблица", "График"]} checked={state.artsDisplayType === 1} onChange={handleArtsDisplayTypeChange} />
|
||||
</Box>
|
||||
{state.artsDisplayType === 0 ? (
|
||||
stageDeatilArtsDataGrid.init ? (
|
||||
<P8PDataGrid
|
||||
{...P8P_DATA_GRID_CONFIG_PROPS}
|
||||
containerComponentProps={{ sx: STAGE_DETAIL_STYLES.DATA_AREA, elevation: 6 }}
|
||||
{...stageDeatilArtsDataGrid}
|
||||
size={P8P_DATA_GRID_SIZE.SMALL}
|
||||
fixedHeader={true}
|
||||
headCellRender={stageDetailArtsHeadCellRender}
|
||||
dataCellRender={prms => stageDetailArtsDataCellRender({ ...prms, showCostNotesFact })}
|
||||
/>
|
||||
) : null
|
||||
) : (
|
||||
<Paper elevation={6} sx={STAGE_DETAIL_STYLES.DATA_AREA}>
|
||||
<Box sx={STAGE_DETAIL_STYLES.CHART_CONTAINER}>
|
||||
<Toggle labels={["План", "Факт"]} checked={state.artsChartType === 1} onChange={handleArtsChartTypeChange} />
|
||||
{stageDeatilArtsDataGrid?.rows?.length > 0 ? (
|
||||
stageDeatilArtsChart.init ? (
|
||||
<P8PChart style={STAGE_DETAIL_STYLES.CHART} {...stageDeatilArtsChart} onClick={handleChartClick} />
|
||||
) : null
|
||||
) : (
|
||||
<P8PAppInlineError text={TEXTS.NO_DATA_FOUND} />
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Данные этапа
|
||||
StageDetailData.propTypes = {
|
||||
stageRn: PropTypes.number
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Детальная информация об этапе проекта
|
||||
const StageDetail = ({ stageRn, stageName, isOpen, onClose }) => {
|
||||
return (
|
||||
<Drawer anchor={"right"} open={isOpen} onClose={onClose} sx={STAGE_DETAIL_STYLES.STAGE_DETAIL_DRAWER}>
|
||||
<Box sx={STAGE_DETAIL_STYLES.STAGE_DETAIL_HEADER}>
|
||||
<IconButton sx={STAGE_DETAIL_STYLES.STAGE_DETAIL_CLOSE_BUTTON} size={"small"} onClick={onClose}>
|
||||
<Icon>close</Icon>
|
||||
</IconButton>
|
||||
<Typography variant={"h6"} color={"white"} pl={2}>{`Этап: ${stageName}`}</Typography>
|
||||
</Box>
|
||||
<StageDetailData stageRn={stageRn} />
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Детальная информация об этапе проекта
|
||||
StageDetail.propTypes = {
|
||||
stageRn: PropTypes.number,
|
||||
stageName: PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { StageDetail };
|
126
app/panels/prj_info/stage_detail_hooks.js
Normal file
126
app/panels/prj_info/stage_detail_hooks.js
Normal file
@ -0,0 +1,126 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Детальная информация об этапе проекта: пользовательские хуки для взаимодействия с сервером
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { useState, useContext, useEffect } from "react"; //Классы React
|
||||
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Детали этапа проекта - информация об этапе
|
||||
const useStageDetailInfoDataGrid = ({ stageRn }) => {
|
||||
//Собственное состояние - флаг загрузки
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
//Собственное состояние - таблица данных
|
||||
const [data, setData] = useState({ init: false });
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored } = useContext(BackEndСtx);
|
||||
|
||||
//При необходимости обновить данные таблицы
|
||||
useEffect(() => {
|
||||
//Загрузка данных таблицы с сервера
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.INFO_STAGE_DTL_DG",
|
||||
args: { NPROJECTSTAGE: stageRn },
|
||||
respArg: "COUT",
|
||||
loader: true
|
||||
});
|
||||
setData(pv => ({ ...pv, ...data.XDATA_GRID, init: true }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
if (stageRn) loadData();
|
||||
}, [stageRn, executeStored]);
|
||||
|
||||
//Возвращаем интерфейс хука
|
||||
return [data, isLoading];
|
||||
};
|
||||
|
||||
//Детали этапа проекта - структура цены - таблица данных
|
||||
const useStageDetailArtsDataGrid = ({ stageRn }) => {
|
||||
//Собственное состояние - флаг загрузки
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
//Собственное состояние - таблица данных
|
||||
const [data, setData] = useState({ init: false });
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
|
||||
|
||||
//При необходимости обновить данные таблицы
|
||||
useEffect(() => {
|
||||
//Загрузка данных таблицы с сервера
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const artsData = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.INFO_STAGE_ARTS_DG",
|
||||
args: { NPROJECTSTAGE: stageRn },
|
||||
respArg: "COUT",
|
||||
loader: true
|
||||
});
|
||||
setData(pv => ({ ...pv, ...artsData.XDATA_GRID, init: true }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
if (stageRn) loadData();
|
||||
}, [stageRn, executeStored, SERV_DATA_TYPE_CLOB]);
|
||||
|
||||
//Возвращаем интерфейс хука
|
||||
return [data, isLoading];
|
||||
};
|
||||
|
||||
//Детали этапа проекта - структура цены - график
|
||||
const useStageDetailArtsChart = ({ stageRn, display, type }) => {
|
||||
//Собственное состояние - флаг загрузки
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
//Собственное состояние - график
|
||||
const [data, setData] = useState({ init: false, currentType: null });
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored } = useContext(BackEndСtx);
|
||||
|
||||
//При необходимости обновить данные таблицы
|
||||
useEffect(() => {
|
||||
//Загрузка данных таблицы с сервера
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.INFO_STAGE_ARTS_CHART",
|
||||
args: { NPROJECTSTAGE: stageRn, NTYPE: type },
|
||||
respArg: "COUT",
|
||||
loader: true
|
||||
});
|
||||
setData(pv => ({ ...pv, ...data.XCHART, currentType: type, init: true }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
if (stageRn && display && data.currentType != type) loadData();
|
||||
}, [stageRn, display, type, data.currentType, executeStored]);
|
||||
|
||||
//Возвращаем интерфейс хука
|
||||
return [data, isLoading];
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { useStageDetailInfoDataGrid, useStageDetailArtsDataGrid, useStageDetailArtsChart };
|
148
app/panels/prj_info/stage_detail_layouts.js
Normal file
148
app/panels/prj_info/stage_detail_layouts.js
Normal file
@ -0,0 +1,148 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Детальная информация об этапе проекта: дополнительная разметка и вёрстка клиентских элементов
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import { Link } from "@mui/material"; //Интерфейсные элементы
|
||||
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
|
||||
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
|
||||
import { formatNumberRFCurrency } from "../../core/utils"; //Вспомогательные функции
|
||||
import { formatCostStatusValue } from "./layouts"; //Общие стили и разметка панели
|
||||
import { formatStageStatusValue } from "./stages_layouts"; //Cтили и разметка списка этапов проекта
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Высота заголовка информационного блока
|
||||
const DATA_AREA_HEADER_HEIGHT = "52px";
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
STAGE_DETAIL_DRAWER: { flexShrink: 0, [`& .MuiDrawer-paper`]: { width: "70%", boxSizing: "border-box", ...APP_STYLES.SCROLL } },
|
||||
STAGE_DETAIL_HEADER: {
|
||||
height: APP_BAR_HEIGHT,
|
||||
paddingLeft: "24px",
|
||||
backgroundColor: "#1976d2",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start"
|
||||
},
|
||||
STAGE_DETAIL_CLOSE_BUTTON: { color: "white", marginBottom: "3px" },
|
||||
DATA_AREA_CONTAINER: { paddingLeft: "10px", paddingRight: "10px" },
|
||||
DATA_AREA: { height: `calc(100vh - ${APP_BAR_HEIGHT} - ${DATA_AREA_HEADER_HEIGHT} - 10px)`, overflowY: "auto", ...APP_STYLES.SCROLL },
|
||||
DATA_AREA_HEADER_CONTAINER: { display: "flex", justifyContent: "space-between" },
|
||||
DATA_AREA_HEADER: { paddingTop: "10px", paddingBottom: "10px" },
|
||||
DATA_GRID_HEADER: { fontSize: "10pt", padding: "6px 10px" },
|
||||
DATA_GRID_CELL: value => ({ fontSize: "9pt", padding: "6px 10px", ...(value ? { color: value > 0 ? "green" : "red" } : {}) }),
|
||||
CHART_CONTAINER: { paddingTop: "20px" },
|
||||
CHART: { maxHeight: "60vh", display: "flex", justifyContent: "center" }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Форматирование заголовков колонок таблицы "Сведения"
|
||||
const stageDetailInfoHeadCellRender = ({ columnDef }) => {
|
||||
//Инициализируем общий стиль ячеек
|
||||
let cellStyle = STYLES.DATA_GRID_HEADER;
|
||||
//Формирование представлений
|
||||
switch (columnDef.name) {
|
||||
case "SATTR":
|
||||
return { cellStyle, stackProps: { justifyContent: "left" } };
|
||||
case "SVALUE":
|
||||
return { cellStyle, stackProps: { justifyContent: "right" } };
|
||||
default:
|
||||
return { cellStyle: cellStyle };
|
||||
}
|
||||
};
|
||||
|
||||
//Форматирование ячеек строк таблицы "Сведения"
|
||||
const stageDetailInfoDataCellRender = ({ row, columnDef }) => {
|
||||
//Инициализируем общий стиль ячеек
|
||||
let cellStyle = STYLES.DATA_GRID_CELL();
|
||||
//Формирование представлений
|
||||
switch (columnDef.name) {
|
||||
case "SATTR":
|
||||
return { cellStyle: { ...cellStyle, color: "#1976d2" }, cellProps: { align: "left" } };
|
||||
case "SVALUE": {
|
||||
const res = { cellStyle, cellProps: { align: "right" } };
|
||||
if (["NCOST_SUM", "NSTAGE_COST_SUM"].includes(row["SCODE"]))
|
||||
res.data = row["SVALUE"] || row["SVALUE"] === 0 ? formatNumberRFCurrency(row["SVALUE"]) : "-";
|
||||
if (row["SCODE"] == "NSTATE")
|
||||
res.data = formatStageStatusValue({ value: parseInt(row["SVALUE"]), addText: true, justifyContent: "right" });
|
||||
return res;
|
||||
}
|
||||
default:
|
||||
return { cellStyle };
|
||||
}
|
||||
};
|
||||
|
||||
//Форматирование заголовков колонок таблицы "Структура затрат"
|
||||
const stageDetailArtsHeadCellRender = ({ columnDef }) => {
|
||||
//Инициализируем общий стиль ячеек
|
||||
let cellStyle = STYLES.DATA_GRID_HEADER;
|
||||
//Формирование представлений
|
||||
switch (columnDef.name) {
|
||||
case "NSTATE":
|
||||
return { cellStyle: { ...cellStyle, justifyContent: "center" }, stackStyle: { justifyContent: "center" } };
|
||||
default:
|
||||
return { cellStyle: cellStyle };
|
||||
}
|
||||
};
|
||||
|
||||
//Форматирование ячеек строк таблицы "Структура затрат"
|
||||
const stageDetailArtsDataCellRender = ({ row, columnDef, showCostNotesFact }) => {
|
||||
//Инициализируем общий стиль ячеек
|
||||
let cellStyle = STYLES.DATA_GRID_CELL;
|
||||
//Формирование представлений
|
||||
switch (columnDef.name) {
|
||||
case "NCOST_STATUS":
|
||||
return {
|
||||
cellProps: { align: "center" },
|
||||
data: formatCostStatusValue({ value: row[columnDef.name], type: 0 })
|
||||
};
|
||||
case "NPLAN_SUM":
|
||||
case "NPLAN_FACT_SUM":
|
||||
return {
|
||||
cellStyle: cellStyle(columnDef.name == "NPLAN_FACT_SUM" ? row[columnDef.name] : null),
|
||||
data: row[columnDef.name] || row[columnDef.name] === 0 ? formatNumberRFCurrency(row[columnDef.name]) : "-"
|
||||
};
|
||||
case "NFACT_SUM":
|
||||
return {
|
||||
cellStyle: cellStyle(),
|
||||
data:
|
||||
row[columnDef.name] || row[columnDef.name] === 0 ? (
|
||||
row[columnDef.name] > 0 ? (
|
||||
<Link component="button" onClick={() => showCostNotesFact({ faceAccRn: row["NFACEACC"], artclRn: row["NFPDARTCL"] })}>
|
||||
{formatNumberRFCurrency(row[columnDef.name])}
|
||||
</Link>
|
||||
) : (
|
||||
formatNumberRFCurrency(row[columnDef.name])
|
||||
)
|
||||
) : (
|
||||
"-"
|
||||
)
|
||||
};
|
||||
default:
|
||||
return { cellStyle: cellStyle(), data: row[columnDef.name] };
|
||||
}
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export {
|
||||
STYLES as STAGE_DETAIL_STYLES,
|
||||
stageDetailInfoHeadCellRender,
|
||||
stageDetailInfoDataCellRender,
|
||||
stageDetailArtsHeadCellRender,
|
||||
stageDetailArtsDataCellRender
|
||||
};
|
95
app/panels/prj_info/stages.js
Normal file
95
app/panels/prj_info/stages.js
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Список этапов проекта
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useContext } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Typography } from "@mui/material"; //Интерфейсные элементы
|
||||
import { ApplicationСtx } from "../../context/application"; //Контекст приложения
|
||||
import { P8PDataGrid } from "../../components/p8p_data_grid"; //Таблица данных
|
||||
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
|
||||
import { useStagesDataGrid } from "./stages_hooks"; //Хуки списка этапов проекта
|
||||
import { STAGES_STYLES, projectStageDataCellRender } from "./stages_layouts"; //Дополнительная разметка и вёрстка клиентских элементов
|
||||
import { StageDetail } from "./stage_detail"; //Компонент "Информация об этапе проекта"
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Список этапов проекта
|
||||
const Stages = ({ projectRn, projectCode }) => {
|
||||
//Собственное состояние
|
||||
const [stages, setStages] = useState({ pageNumber: 1, orders: [] });
|
||||
|
||||
//Состояние таблицы этапов
|
||||
const [stagesDataGrid] = useStagesDataGrid({ ...stages, projectRn });
|
||||
|
||||
//Состояние информации о этапе
|
||||
const [stageInfo, setStageInfo] = useState({ showInfo: false, stage: null, sFaceAcc: null });
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowUnit } = useContext(ApplicationСtx);
|
||||
|
||||
//Отображение записи этапа проекта в штатном разделе
|
||||
const showProjectStage = (prn, rn) => {
|
||||
pOnlineShowUnit({
|
||||
unitCode: "Projects",
|
||||
inputParameters: [
|
||||
{ name: "in_RN", value: prn },
|
||||
{ name: "in_STAGE_RN", value: rn }
|
||||
],
|
||||
modal: false
|
||||
});
|
||||
};
|
||||
|
||||
//Отображение деталей этапа
|
||||
const showStageDetails = stage => setStageInfo(pv => ({ ...pv, showInfo: true, stage: stage["NRN"], sFaceAcc: stage["SFACEACC"] }));
|
||||
|
||||
//При изменении количества отображаемых страниц
|
||||
const handlePagesCountChanged = () => setStages(pv => ({ ...pv, pageNumber: pv.pageNumber + 1 }));
|
||||
|
||||
//При изменении состояния сортировки
|
||||
const handleOrderChanged = ({ orders }) => setStages(pv => ({ ...pv, orders: [...orders], pageNumber: 1 }));
|
||||
|
||||
//Генерация содержимого
|
||||
return stagesDataGrid.init ? (
|
||||
<>
|
||||
<div style={STAGES_STYLES.CONTAINER}>
|
||||
<Typography variant={"subtitle2"} sx={STAGES_STYLES.TITLE}>
|
||||
{`Этапы проекта "${projectCode}"`}
|
||||
</Typography>
|
||||
<P8PDataGrid
|
||||
{...P8P_DATA_GRID_CONFIG_PROPS}
|
||||
{...stagesDataGrid}
|
||||
containerComponentProps={{ sx: STAGES_STYLES.DATA_GRID_CONTAINER, elevation: 0 }}
|
||||
onPagesCountChanged={handlePagesCountChanged}
|
||||
onOrderChanged={handleOrderChanged}
|
||||
dataCellRender={prms => projectStageDataCellRender({ ...prms, showProjectStage, showStageDetails })}
|
||||
/>
|
||||
</div>
|
||||
<StageDetail
|
||||
stageRn={stageInfo.stage}
|
||||
stageName={stageInfo.sFaceAcc}
|
||||
isOpen={stageInfo.showInfo}
|
||||
onClose={() => setStageInfo(pv => ({ ...pv, showInfo: false, stage: null, sFaceAcc: null }))}
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Список этапов проекта
|
||||
Stages.propTypes = {
|
||||
projectRn: PropTypes.number.isRequired,
|
||||
projectCode: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { Stages };
|
78
app/panels/prj_info/stages_hooks.js
Normal file
78
app/panels/prj_info/stages_hooks.js
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Список этапов проекта: пользовательские хуки для взаимодействия с сервером
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { useState, useContext, useEffect } from "react"; //Классы React
|
||||
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
|
||||
import { object2Base64XML, formatDateRF } from "../../core/utils"; //Вспомогательные функции
|
||||
import config from "../../../app.config"; //Настройки приложения
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Размер страницы данных
|
||||
const DATA_GRID_PAGE_SIZE = config.SYSTEM.PAGE_SIZE;
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Этапы проекта
|
||||
const useStagesDataGrid = ({ projectRn, pageNumber, orders }) => {
|
||||
//Собственное состояние - флаг загрузки
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
//Собственное состояние - таблица данных
|
||||
const [data, setData] = useState({ init: false, morePages: true });
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
|
||||
|
||||
//При необходимости обновить данные таблицы
|
||||
useEffect(() => {
|
||||
//Загрузка данных таблицы с сервера
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_PROJECTS.INFO_STAGES_DG",
|
||||
args: {
|
||||
NPROJECT: projectRn,
|
||||
CORDERS: { VALUE: object2Base64XML(orders, { arrayNodeName: "orders" }), SDATA_TYPE: SERV_DATA_TYPE_CLOB },
|
||||
NPAGE_NUMBER: pageNumber,
|
||||
NPAGE_SIZE: DATA_GRID_PAGE_SIZE,
|
||||
NINCLUDE_DEF: pageNumber == 1 ? 1 : 0
|
||||
},
|
||||
respArg: "COUT",
|
||||
loader: true,
|
||||
attributeValueProcessor: (name, val) => (["DBEGPLAN", "DENDPLAN"].includes(name) ? formatDateRF(val) : val)
|
||||
});
|
||||
setData(pv => ({
|
||||
...pv,
|
||||
columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef || [],
|
||||
rows: pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...(pv.rows || []), ...(data.XDATA_GRID.rows || [])],
|
||||
morePages: DATA_GRID_PAGE_SIZE == 0 ? false : (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE,
|
||||
init: true
|
||||
}));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
if (projectRn) loadData();
|
||||
}, [projectRn, orders, pageNumber, executeStored, SERV_DATA_TYPE_CLOB]);
|
||||
|
||||
//Возвращаем интерфейс хука
|
||||
return [data, isLoading];
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { useStagesDataGrid };
|
86
app/panels/prj_info/stages_layouts.js
Normal file
86
app/panels/prj_info/stages_layouts.js
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - ПУП - Информация о проектах
|
||||
Список этапов проекта: дополнительная разметка и вёрстка клиентских элементов
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import { Icon, Stack, Link } from "@mui/material"; //Интерфейсные элементы
|
||||
import { formatNumberRFCurrency } from "../../core/utils"; //Спомогательные функции
|
||||
import { COMMON_STYLES, formatCostStatusValue, formatCostReadyValue } from "./layouts"; //Общие стили и разметка панели
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONTAINER: { textAlign: "center", paddingTop: "10px", backgroundColor: "lightcyan" },
|
||||
TITLE: { fontSize: "13pt", paddingBottom: "10px" },
|
||||
DATA_GRID_CONTAINER: { backgroundColor: "lightcyan" }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Формирование значения для колонки "Состояние" этапа
|
||||
const formatStageStatusValue = ({ value, addText = false, justifyContent = "center" }) => {
|
||||
const [text, icon] =
|
||||
value == 0
|
||||
? ["Зарегистрирован", "app_registration"]
|
||||
: value == 1
|
||||
? ["Открыт", "lock_open"]
|
||||
: value == 2
|
||||
? ["Закрыт", "lock_outline"]
|
||||
: value == 3
|
||||
? ["Согласован", "thumb_up_alt"]
|
||||
: value == 4
|
||||
? ["Исполнение прекращено", "block"]
|
||||
: ["Остановлен", "do_not_disturb_on"];
|
||||
return (
|
||||
<Stack direction="row" gap={0.5} alignItems={"center"} justifyContent={justifyContent || "center"}>
|
||||
<Icon title={text} sx={COMMON_STYLES.STATE(value)}>
|
||||
{icon}
|
||||
</Icon>
|
||||
{addText == true ? text : null}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
//Форматирование ячеек таблицы "Этапы проекта"
|
||||
const projectStageDataCellRender = ({ row, columnDef, showProjectStage, showStageDetails }) => {
|
||||
//Формирование представлений
|
||||
switch (columnDef.name) {
|
||||
case "NCOST_STATUS":
|
||||
return {
|
||||
cellProps: { align: "center" },
|
||||
data: formatCostStatusValue({ value: row[columnDef.name], onClick: () => showStageDetails(row) })
|
||||
};
|
||||
case "NCOST_READY":
|
||||
return { cellProps: { align: "center" }, data: formatCostReadyValue(row[columnDef.name]) };
|
||||
case "NSTATE":
|
||||
return { cellProps: { align: "center" }, data: formatStageStatusValue({ value: row[columnDef.name] }) };
|
||||
case "SFACEACC":
|
||||
return {
|
||||
data: (
|
||||
<Link component="button" align="left" underline="hover" onClick={() => showProjectStage(row["NPRN"], row["NRN"])}>
|
||||
{row[columnDef.name]}
|
||||
</Link>
|
||||
)
|
||||
};
|
||||
case "NCOST_SUM":
|
||||
return { data: formatNumberRFCurrency(row[columnDef.name]) };
|
||||
default:
|
||||
return { data: row[columnDef.name] };
|
||||
}
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { STYLES as STAGES_STYLES, projectStageDataCellRender, formatStageStatusValue };
|
23
app/panels/query_editor/common.js
Normal file
23
app/panels/query_editor/common.js
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Обще ресурсы и константы
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Типы данных
|
||||
const DATA_TYPE = { STR: 0, NUMB: 1, DATE: 2 };
|
||||
|
||||
//Типы элементов диаграммы
|
||||
const NODE_TYPE = {
|
||||
ENTITY: "entity",
|
||||
ATTRIBUTE: "attribute"
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { DATA_TYPE, NODE_TYPE };
|
151
app/panels/query_editor/components/attribute/attribute.js
Normal file
151
app/panels/query_editor/components/attribute/attribute.js
Normal file
@ -0,0 +1,151 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Компоненты: Атрибут сущности
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Handle, Position, useStore } from "reactflow"; //Библиотека редактора диаграмм
|
||||
import { Box, Stack, Icon, Typography } from "@mui/material"; //Компоненты UI
|
||||
import { DATA_TYPE } from "../../common"; //Общие ресурсы и константы редактора
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Типовые цвета точек привязки
|
||||
const HANDLE_BORDER_COLOR = "#69db7c";
|
||||
const HANDLE_BORDER_COLOR_INVALID = "#ff0000";
|
||||
const HANDLE_BORDER_COLOR_DISABLED = "#adb5bd";
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONTAINER: { display: "flex", width: "100%", height: "100%", cursor: "default" },
|
||||
HANDLE_SOURCE: isConnecting => ({
|
||||
width: 14,
|
||||
height: 14,
|
||||
right: -10,
|
||||
border: `2px solid ${isConnecting ? HANDLE_BORDER_COLOR_DISABLED : HANDLE_BORDER_COLOR}`,
|
||||
borderRadius: 7,
|
||||
background: "white"
|
||||
}),
|
||||
HANDLE_TARGET: (isConnecting, isValidConnection) => ({
|
||||
width: isConnecting ? 14 : 0,
|
||||
height: 14,
|
||||
left: isConnecting ? -7 : 0,
|
||||
border: `2px solid ${isValidConnection ? HANDLE_BORDER_COLOR : HANDLE_BORDER_COLOR_INVALID}`,
|
||||
borderRadius: 7,
|
||||
background: "white",
|
||||
visibility: isConnecting ? "visible" : "hidden"
|
||||
}),
|
||||
CONTENT_STACK: { width: "100%" },
|
||||
TITLE_NAME_STACK: { width: "100%", containerType: "inline-size" },
|
||||
ATTR_PROP_ICON: { fontSize: "0.9rem" }
|
||||
};
|
||||
|
||||
//Иконки
|
||||
const ICONS = {
|
||||
[DATA_TYPE.STR]: "format_align_left",
|
||||
[DATA_TYPE.NUMB]: "pin",
|
||||
[DATA_TYPE.DATE]: "calendar_month",
|
||||
DEFAULT: "category"
|
||||
};
|
||||
|
||||
//Структура данных об атрибуте сущности
|
||||
const ATTRIBUTE_DATA_SHAPE = PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
parentEntity: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
dataType: PropTypes.oneOf(Object.values(DATA_TYPE)),
|
||||
agg: PropTypes.string,
|
||||
alias: PropTypes.string,
|
||||
use: PropTypes.oneOf([0, 1]).isRequired,
|
||||
show: PropTypes.oneOf([0, 1]).isRequired
|
||||
});
|
||||
|
||||
//-----------------------
|
||||
//Вспомогательные функции
|
||||
//-----------------------
|
||||
|
||||
//Получение атрибутики состояния включения атрибута в запрос
|
||||
const attrGetUse = (attr, callToAction = false) => {
|
||||
return [attr.use === 1, `${attr.use === 1 ? "Включен в запрос" : "Не включен в запрос"}${callToAction ? "- нажмите, чтобы изменить" : ""}`];
|
||||
};
|
||||
|
||||
//Получение атрибутики состояния отображения атрибута в результатах запроса
|
||||
const attrGetShow = (attr, callToAction = false) => {
|
||||
return [
|
||||
`${attr.show == 1 ? "Отображается в результатах запроса" : "Не отображается в результатах запроса"}${
|
||||
callToAction ? "- нажмите, чтобы изменить" : ""
|
||||
}`,
|
||||
attr.show == 1 ? "visibility" : "visibility_off"
|
||||
];
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Атрибут сущности
|
||||
const Attribute = ({ data }) => {
|
||||
//Поиск идентификатора соединяемого элемента
|
||||
const [connectionNodeId, targetConnectionNode, connectionStatus] = useStore(state => [
|
||||
state.connectionNodeId,
|
||||
state?.connectionEndHandle?.nodeId,
|
||||
state.connectionStatus
|
||||
]);
|
||||
|
||||
//Флаг выполнения соединения сущностей
|
||||
const isConnecting = Boolean(connectionNodeId);
|
||||
|
||||
//Флаг корректности соединения сущностей
|
||||
const isValidConnection = !(data.id == targetConnectionNode && connectionStatus == "invalid");
|
||||
|
||||
//Получим атрибуты состояния отображения
|
||||
const [showTitle, showIcon] = attrGetShow(data);
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<Box p={1} sx={STYLES.CONTAINER}>
|
||||
<Handle type={"source"} position={Position.Right} style={STYLES.HANDLE_SOURCE(isConnecting)} />
|
||||
<Handle
|
||||
type={"target"}
|
||||
position={Position.Left}
|
||||
isConnectableStart={false}
|
||||
style={STYLES.HANDLE_TARGET(isConnecting, isValidConnection)}
|
||||
/>
|
||||
<Stack direction={"row"} alignItems={"center"} spacing={1} sx={STYLES.CONTENT_STACK}>
|
||||
<Icon color={"action"}>{ICONS[data.dataType] || ICONS.DEFAULT}</Icon>
|
||||
<Stack direction={"column"} alignItems={"left"} sx={STYLES.TITLE_NAME_STACK}>
|
||||
<Typography variant={"body2"} noWrap title={data.title}>
|
||||
{data.title}
|
||||
</Typography>
|
||||
<Stack direction={"row"} alignItems={"center"} spacing={0.5}>
|
||||
<Typography component={"div"} variant={"caption"} color={"text.secondary"} noWrap title={data.name}>
|
||||
{`${data.name},`}
|
||||
</Typography>
|
||||
<Icon color={"action"} sx={STYLES.ATTR_PROP_ICON} title={showTitle}>
|
||||
{showIcon}
|
||||
</Icon>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Атрибут сущности
|
||||
Attribute.propTypes = {
|
||||
data: ATTRIBUTE_DATA_SHAPE
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { Attribute, ATTRIBUTE_DATA_SHAPE, attrGetUse, attrGetShow };
|
33
app/panels/query_editor/components/entity/entity.css
Normal file
33
app/panels/query_editor/components/entity/entity.css
Normal file
@ -0,0 +1,33 @@
|
||||
.entity__wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid var(--border-color-dark);
|
||||
border-radius: 6px;
|
||||
box-shadow: var(--shadow-entity);
|
||||
overflow: hidden;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.entity__wrapper[data-selected="true"] {
|
||||
outline: 1px solid var(--outline-color);
|
||||
border-color: var(--outline-color);
|
||||
}
|
||||
|
||||
.entity__title {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
align-content: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
background-color: var(--entity-title-bg);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.entity__name {
|
||||
width: 100%;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
font-size: 0.8rem;
|
||||
color: gray;
|
||||
}
|
51
app/panels/query_editor/components/entity/entity.js
Normal file
51
app/panels/query_editor/components/entity/entity.js
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Компоненты: Сущность запроса
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import "./entity.css"; //Стили компомнента
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Структура данных о сущности запроса
|
||||
const ENTITY_DATA_SHAPE = PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Сущность запроса
|
||||
const Entity = ({ data, selected = false }) => {
|
||||
return (
|
||||
<div className="entity__wrapper" data-selected={selected}>
|
||||
<div className="entity__title">
|
||||
<span>{data.title}</span>
|
||||
<div className="entity__name">{data.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Сущность запроса
|
||||
Entity.propTypes = {
|
||||
data: ENTITY_DATA_SHAPE,
|
||||
selected: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { Entity, ENTITY_DATA_SHAPE };
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Компонент: Диалог добавления сущности запроса
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог
|
||||
import { TITLES } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Диалог добавления сущности запроса
|
||||
const EntityAddDialog = ({ onOk, onCancel }) => {
|
||||
//Нажатие на кнопку "Ok"
|
||||
const handleOk = values => onOk && onOk({ ...values });
|
||||
|
||||
//Нажатие на кнопку "Отмена"
|
||||
const handleCancel = () => onCancel && onCancel();
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<P8PDialog title={`${TITLES.INSERT} сущности`} inputs={[{ name: "name", value: "", label: "Имя" }]} onOk={handleOk} onCancel={handleCancel} />
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог добавления сущности запроса
|
||||
EntityAddDialog.propTypes = {
|
||||
onOk: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { EntityAddDialog };
|
@ -0,0 +1,97 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Компонент: Список атрибутов сущности
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Stack, List, ListItem, IconButton, Icon, ListItemButton, ListItemText, ListItemIcon, Checkbox, Typography } from "@mui/material"; //Интерфейсные компоненты MUI
|
||||
import { ATTRIBUTE_DATA_SHAPE, attrGetUse, attrGetShow } from "../attribute/attribute"; //Атрибут сущности
|
||||
import { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
SMALL_TOOL_ICON: {
|
||||
fontSize: 20
|
||||
},
|
||||
LIST: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Список атрибутов сущности
|
||||
const AttrsList = ({ attrs = [], filter, onSelect = null, onShow = null } = {}) => {
|
||||
//При выборе элемента списка
|
||||
const handleSelectClick = attr => {
|
||||
onSelect && onSelect(attr);
|
||||
};
|
||||
|
||||
//При нажатии на исправлении
|
||||
const handleShowClick = (e, attr) => {
|
||||
e.stopPropagation();
|
||||
onShow && onShow(attr);
|
||||
};
|
||||
|
||||
//Рег. выражение для фильтра
|
||||
const filterRegExp = filter ? new RegExp(filter, "i") : null;
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<List sx={STYLES.LIST}>
|
||||
{attrs &&
|
||||
attrs
|
||||
.filter(attr => (filterRegExp ? filterRegExp.test(attr.name) || filterRegExp.test(attr.title) : true))
|
||||
.map((attr, i) => {
|
||||
const [selected, selectedTitle] = attrGetUse(attr, true);
|
||||
const [showTitle, showIcon] = attrGetShow(attr, true);
|
||||
return (
|
||||
<ListItem key={i} disablePadding>
|
||||
<ListItemButton onClick={() => handleSelectClick(attr)} selected={selected} dense>
|
||||
<ListItemIcon>
|
||||
<Checkbox edge="start" checked={selected} tabIndex={-1} disableRipple title={selectedTitle} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={attr.title}
|
||||
secondaryTypographyProps={{ component: "div" }}
|
||||
secondary={
|
||||
<Stack direction={"column"}>
|
||||
<Typography variant={"caption"}>{`${attr.alias || attr.name}`}</Typography>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<Stack direction={"row"}>
|
||||
<IconButton onClick={e => handleShowClick(e, attr)} title={showTitle}>
|
||||
<Icon>{showIcon}</Icon>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Список атрибутов сущности
|
||||
AttrsList.propTypes = {
|
||||
attrs: PropTypes.arrayOf(ATTRIBUTE_DATA_SHAPE),
|
||||
filter: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
onShow: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { AttrsList };
|
@ -0,0 +1,111 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Компонент: Диалог настройки атрибутов сущности
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useEffect } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { TextField, InputAdornment, Icon, IconButton } from "@mui/material"; //Интерфейсные элементы MUI
|
||||
import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог
|
||||
import { AttrsList } from "./attrs_list"; //Список атрибутов сущности
|
||||
import { useEntityAttrs } from "./hooks"; //Хуки диалога настройки атрибутов сущности
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Диалог настройки атрибутов сущности
|
||||
const EntityAttrsDialog = ({ query, id, title, onOk, onCancel }) => {
|
||||
//Собственное состояние - фильтр атрибутов
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
//Собственное состояние - список атрибутов
|
||||
const [attrs, setAttrs] = useState([]);
|
||||
|
||||
//Хук для взаимодействия с сервером
|
||||
const [srvAttrs, saveAttrs] = useEntityAttrs(query, id);
|
||||
|
||||
//Нажатие на кнопку "Ok"
|
||||
const handleOk = async () => {
|
||||
await saveAttrs(attrs);
|
||||
onOk && onOk();
|
||||
};
|
||||
|
||||
//Нажатие на кнопку "Отмена"
|
||||
const handleCancel = () => onCancel && onCancel();
|
||||
|
||||
//Выбор/исключение атрибута из запроса
|
||||
const handleAttrSelect = attr =>
|
||||
setAttrs(
|
||||
attrs.map(a => ({
|
||||
...(a.id === attr.id ? { ...a, use: a.use === 1 ? 0 : 1, show: a.use === 1 ? 0 : a.show } : a)
|
||||
}))
|
||||
);
|
||||
|
||||
//Отображение/сокрытие атрибута в запросе
|
||||
const handleAttrShow = attr =>
|
||||
setAttrs(
|
||||
attrs.map(a => ({
|
||||
...(a.id === attr.id ? { ...a, show: a.show === 1 ? 0 : 1, use: a.show === 0 ? 1 : a.use } : a)
|
||||
}))
|
||||
);
|
||||
|
||||
//При изменении значения фильтра
|
||||
const handleFilterChange = e => setFilter(e.target.value);
|
||||
|
||||
//При очистке фильтра
|
||||
const handleFilterClear = () => setFilter("");
|
||||
|
||||
//При загрузке данных с сервера
|
||||
useEffect(() => {
|
||||
if (srvAttrs) setAttrs(srvAttrs.map(srvAttr => ({ ...srvAttr })));
|
||||
}, [srvAttrs]);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<P8PDialog title={`Атрибуты сущности "${title}"`} onOk={handleOk} onCancel={handleCancel}>
|
||||
<TextField
|
||||
margin={"normal"}
|
||||
variant={"standard"}
|
||||
fullWidth
|
||||
placeholder={"Поиск атрибута..."}
|
||||
value={filter}
|
||||
onChange={handleFilterChange}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position={"start"}>
|
||||
<Icon>search</Icon>
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position={"end"}>
|
||||
<IconButton onClick={handleFilterClear}>
|
||||
<Icon>clear</Icon>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<AttrsList attrs={attrs} filter={filter} onSelect={handleAttrSelect} onShow={handleAttrShow} />
|
||||
</P8PDialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог настройки атрибутов сущности
|
||||
EntityAttrsDialog.propTypes = {
|
||||
query: PropTypes.number.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onOk: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { EntityAttrsDialog };
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Пользовательские хуки диалога настройки атрибутов сущности
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { useState, useContext, useEffect, useCallback } from "react"; //Классы React
|
||||
import { BackEndСtx } from "../../../../context/backend"; //Контекст взаимодействия с сервером
|
||||
import { object2Base64XML } from "../../../../core/utils"; //Вспомогательные функции
|
||||
|
||||
//------------------------------------
|
||||
//Вспомогательные функции и компоненты
|
||||
//------------------------------------
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Работа с атрибутами сущности
|
||||
const useEntityAttrs = (query, entity) => {
|
||||
//Собственное состояние - флаг загрузки
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
//Собственное состояние - флаг необходимости обновления
|
||||
const [refresh, setRefresh] = useState(true);
|
||||
|
||||
//Собственное состояние - данные
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored, SERV_DATA_TYPE_CLOB } = useContext(BackEndСtx);
|
||||
|
||||
//Обновление данных
|
||||
const doRefresh = () => setRefresh(true);
|
||||
|
||||
//Установка атрибутов сущности
|
||||
const setAttrs = useCallback(
|
||||
async attrs => {
|
||||
await executeStored({
|
||||
stored: "PKG_P8PANELS_QE.QUERY_ENT_ATTRS_SET",
|
||||
args: {
|
||||
NRN: query,
|
||||
SID: entity,
|
||||
CATTRS: {
|
||||
VALUE: object2Base64XML(attrs, { arrayNodeName: "XATTR", ignoreAttributes: false, attributeNamePrefix: "" }),
|
||||
SDATA_TYPE: SERV_DATA_TYPE_CLOB
|
||||
}
|
||||
},
|
||||
loader: false
|
||||
});
|
||||
},
|
||||
[query, entity, executeStored, SERV_DATA_TYPE_CLOB]
|
||||
);
|
||||
|
||||
//При необходимости получить/обновить данные
|
||||
useEffect(() => {
|
||||
//Загрузка данных с сервера
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_QE.QUERY_ENT_ATTRS_GET",
|
||||
args: { NRN: query, SID: entity },
|
||||
respArg: "COUT",
|
||||
isArray: name => ["XATTR"].includes(name),
|
||||
attributeValueProcessor: (name, val) => (["name", "title", "agg"].includes(name) ? undefined : val),
|
||||
loader: true
|
||||
});
|
||||
setData(data?.XATTRS?.XATTR || []);
|
||||
} finally {
|
||||
setRefresh(false);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
//Если надо обновить
|
||||
if (refresh)
|
||||
//Получим данные
|
||||
loadData();
|
||||
}, [refresh, query, entity, executeStored]);
|
||||
|
||||
//Возвращаем интерфейс хука
|
||||
return [data, setAttrs, doRefresh, isLoading];
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { useEntityAttrs };
|
@ -0,0 +1,144 @@
|
||||
/*
|
||||
Парус 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 { APP_STYLES } from "../../../../../app.styles"; //Общие стили приложения
|
||||
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
SMALL_TOOL_ICON: {
|
||||
fontSize: 20
|
||||
},
|
||||
LIST: { height: "500px", width: "360px", bgcolor: "background.paper", overflowY: "auto", ...APP_STYLES.SCROLL }
|
||||
};
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Структура элемента списка запросов
|
||||
const QUERIES_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 QueriesList = ({ queries = [], 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}>
|
||||
{queries.map((query, i) => {
|
||||
const selected = query.rn === current;
|
||||
const disabled = !query.modify;
|
||||
const pblTitle = `${query.pbl === 1 ? "Общедоступный" : "Приватный"}${!disabled ? " - нажмите, чтобы изменить" : ""}`;
|
||||
const pblIcon = query.pbl === 1 ? "groups" : "lock_person";
|
||||
const readyTitle = `${query.ready === 1 ? "Готов" : "Не готов"}${!disabled ? " - нажмите, чтобы изменить" : ""}`;
|
||||
const readyIcon = query.ready === 1 ? "touch_app" : "do_not_touch";
|
||||
return (
|
||||
<ListItem key={i} disablePadding>
|
||||
<ListItemButton onClick={() => handleSelectClick(query)} selected={selected} dense>
|
||||
<ListItemText
|
||||
primary={query.name}
|
||||
secondaryTypographyProps={{ component: "div" }}
|
||||
secondary={
|
||||
<Stack direction={"column"}>
|
||||
<Typography variant={"caption"}>{`${query.code}, ${query.author}, ${query.chDate}`}</Typography>
|
||||
<Stack direction={"row"}>
|
||||
<div title={pblTitle}>
|
||||
<IconButton disabled={disabled} onClick={e => handlePblClick(e, query)}>
|
||||
<Icon sx={STYLES.SMALL_TOOL_ICON}>{pblIcon}</Icon>
|
||||
</IconButton>
|
||||
</div>
|
||||
<div title={readyTitle}>
|
||||
<IconButton disabled={disabled} onClick={e => handleReadyClick(e, query)}>
|
||||
<Icon sx={STYLES.SMALL_TOOL_ICON}>{readyIcon}</Icon>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<Stack direction={"row"}>
|
||||
<IconButton onClick={e => handleEditClick(e, query)} disabled={disabled} title={BUTTONS.UPDATE}>
|
||||
<Icon>edit</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={e => handleDeleteClick(e, query)} disabled={disabled} title={BUTTONS.DELETE}>
|
||||
<Icon>delete</Icon>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Список запросов
|
||||
QueriesList.propTypes = {
|
||||
queries: PropTypes.arrayOf(QUERIES_LIST_ITEM_SHAPE),
|
||||
current: PropTypes.number,
|
||||
onSelect: PropTypes.func,
|
||||
onPbl: PropTypes.func,
|
||||
onReady: PropTypes.func,
|
||||
onEdit: PropTypes.func,
|
||||
onDelete: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { QueriesList };
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Компонент: Менеджер запросов
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useContext } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { 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 { useQuery } from "../../hooks"; //Пользовательские хуки
|
||||
import { QueriesList } from "./queries_list"; //Список запросов
|
||||
import { QueryIUDialog } from "./query_iu_dialog"; //Диалог добавления/исправления запроса
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Менеджер запросов
|
||||
const QueriesManager = ({ current = null, onQuerySelect = null, onCancel = null } = {}) => {
|
||||
//Собственное состояние - изменяемый запрос
|
||||
const [modQuery, setModQuery] = useState(null);
|
||||
|
||||
//Работа со списком запросов
|
||||
const [queries, insertQuery, updateQuery, deleteQuery, setQueryReady, setQueryPbl] = useQuery();
|
||||
|
||||
//Подключение к контексту сообщений
|
||||
const { showMsgWarn } = useContext(MessagingСtx);
|
||||
|
||||
//При добавлении запроса
|
||||
const handleQueryAdd = () => setModQuery(true);
|
||||
|
||||
//При выборе запроса
|
||||
const handleQuerySelect = query => onQuerySelect && onQuerySelect({ ...query });
|
||||
|
||||
//При установке признака публичности
|
||||
const handleQueryPblSet = query => setQueryPbl(query.rn, query.pbl === 1 ? 0 : 1);
|
||||
|
||||
//При установке признака готовности
|
||||
const handleQueryReadySet = query => setQueryReady(query.rn, query.ready === 1 ? 0 : 1);
|
||||
|
||||
//При исправлении запроса
|
||||
const handleQueryEdit = query => setModQuery({ ...query });
|
||||
|
||||
//При удалении запроса
|
||||
const handleQueryDelete = query => showMsgWarn("Удалить запрос?", () => deleteQuery(query.rn));
|
||||
|
||||
//При закрытии диалога добавления/исправления по "ОК"
|
||||
const handleIUDialogOk = async values => {
|
||||
if (modQuery === true) await insertQuery(values.code, values.name);
|
||||
else await updateQuery(modQuery.rn, values.code, values.name);
|
||||
setModQuery(null);
|
||||
};
|
||||
|
||||
//При закрытии диалога добавления/исправления по "Отмена"
|
||||
const handleIUDialogCancel = () => setModQuery(null);
|
||||
|
||||
//При закртии менеджера отменой
|
||||
const handleCancel = () => onCancel && onCancel();
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<P8PConfigDialog title={"Запросы"} onCancel={handleCancel}>
|
||||
{modQuery && (
|
||||
<QueryIUDialog
|
||||
code={modQuery?.code}
|
||||
name={modQuery?.name}
|
||||
insert={modQuery === true}
|
||||
onOk={handleIUDialogOk}
|
||||
onCancel={handleIUDialogCancel}
|
||||
/>
|
||||
)}
|
||||
<Button startIcon={<Icon>add</Icon>} onClick={handleQueryAdd}>
|
||||
{BUTTONS.INSERT}
|
||||
</Button>
|
||||
<QueriesList
|
||||
queries={queries || []}
|
||||
current={current}
|
||||
onSelect={handleQuerySelect}
|
||||
onPbl={handleQueryPblSet}
|
||||
onReady={handleQueryReadySet}
|
||||
onEdit={handleQueryEdit}
|
||||
onDelete={handleQueryDelete}
|
||||
/>
|
||||
</P8PConfigDialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Менеджер запросов
|
||||
QueriesManager.propTypes = {
|
||||
current: PropTypes.number,
|
||||
onQuerySelect: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { QueriesManager };
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Компонент: Диалог добавления/исправления запроса
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { P8PDialog } from "../../../../components/p8p_dialog"; //Типовой диалог
|
||||
import { TITLES } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Диалог добавления/исправления запроса
|
||||
const QueryIUDialog = ({ 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог добавления/исправления запроса
|
||||
QueryIUDialog.propTypes = {
|
||||
code: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
insert: PropTypes.bool,
|
||||
onOk: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { QueryIUDialog };
|
@ -0,0 +1,9 @@
|
||||
.query_diagram {
|
||||
--border-color: #dee2e6;
|
||||
--border-color-dark: #adb5bd;
|
||||
--outline-color: #74c0fc;
|
||||
--entity-title-bg: #f1f3f5;
|
||||
--shadow-entity: 0 -2px 5px 0 hsl(220 3% 15% / calc(1% + 2%)), 0 1px 1px -2px hsl(220 3% 15% / calc(1% + 3%)),
|
||||
0 2px 2px -2px hsl(220 3% 15% / calc(1% + 3%)), 0 5px 5px -2px hsl(220 3% 15% / calc(1% + 4%)), 0 9px 9px -2px hsl(220 3% 15% / calc(1% + 5%)),
|
||||
0 16px 16px -2px hsl(220 3% 15% / calc(1% + 6%));
|
||||
}
|
@ -0,0 +1,229 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Диаграмма запроса
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import ReactFlow, { addEdge, Controls, getOutgoers, applyNodeChanges, applyEdgeChanges } from "reactflow"; //Библиотека редактора диаграмм
|
||||
import { NODE_TYPE } from "../../common"; //Общие ресурсы и константы редактора
|
||||
import { Entity, ENTITY_DATA_SHAPE } from "../entity/entity"; //Сущность запроса
|
||||
import { Attribute, ATTRIBUTE_DATA_SHAPE } from "../attribute/attribute"; //Атрибут сущности
|
||||
import "reactflow/dist/style.css"; //Типовые стили библиотеки редактора диаграмм
|
||||
import "./query_diagram.css"; //Стили компонента
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONNECTION_LINE: {
|
||||
strokeWidth: 2,
|
||||
stroke: "gray"
|
||||
},
|
||||
EDGE: {
|
||||
strokeWidth: 2
|
||||
}
|
||||
};
|
||||
|
||||
//Привязка компонтов диаграммы к типам
|
||||
const NODE_TYPES_COMPONENTS = {
|
||||
[NODE_TYPE.ENTITY]: Entity,
|
||||
[NODE_TYPE.ATTRIBUTE]: Attribute
|
||||
};
|
||||
|
||||
//Структура сущности запроса
|
||||
const ENTITY_SHAPE = PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf([NODE_TYPE.ENTITY, NODE_TYPE.ATTRIBUTE]).isRequired,
|
||||
style: PropTypes.object,
|
||||
position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }),
|
||||
draggable: PropTypes.bool.isRequired,
|
||||
data: PropTypes.oneOfType([ENTITY_DATA_SHAPE, ATTRIBUTE_DATA_SHAPE])
|
||||
});
|
||||
|
||||
//Структура связи запроса
|
||||
const RELATION_SHAPE = PropTypes.shape({ id: PropTypes.string.isRequired, source: PropTypes.string.isRequired, target: PropTypes.string.isRequired });
|
||||
|
||||
//------------------------------------
|
||||
//Вспомогательные функции и компоненты
|
||||
//------------------------------------
|
||||
|
||||
//Проверка зацикленности связи
|
||||
const hasCycle = (connection, target, nodes, edges, visited = new Set()) => {
|
||||
if (visited.has(target.id)) return false;
|
||||
visited.add(target.id);
|
||||
for (const outgoer of getOutgoers(target, nodes, edges))
|
||||
if (outgoer.id === connection.source || hasCycle(connection, outgoer, nodes, edges, visited)) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
//Проверка корректности связи
|
||||
const isValidConnection = (connection, nodes, edges) => {
|
||||
//Должны быть заданы источник и приёмник
|
||||
if (!connection.source || !connection.target) return false;
|
||||
//Найдем источник и приёмник
|
||||
const source = nodes.find(node => node.id === connection.source);
|
||||
const target = nodes.find(node => node.id === connection.target);
|
||||
//Приёмник и источник должны существовать
|
||||
if (!target || !source) return false;
|
||||
//Нельзя ссылаться на самого себя
|
||||
if (source?.data?.parentEntity == target?.data?.parentEntity) return false;
|
||||
//Типы данны источника и приёмника должны совпадать
|
||||
if (source?.data?.dataType != target?.data?.dataType) return false;
|
||||
//Приёмник должен не должен быть источником
|
||||
if (target.id === connection.source) return false;
|
||||
//Нельзя зацикливаться
|
||||
return !hasCycle(connection, target, nodes, edges);
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Диаграмма запроса
|
||||
const QueryDiagram = ({
|
||||
entities = [],
|
||||
relations = [],
|
||||
onEntityClick,
|
||||
onEntityAttrClick,
|
||||
onEntityPositionChange,
|
||||
onEntityRemove,
|
||||
onRelactionClick,
|
||||
onRelationAdd,
|
||||
onRelationRemove,
|
||||
onPaneClick
|
||||
}) => {
|
||||
//Собственное состояние - элементы
|
||||
const [nodes, setNodes] = useState(entities);
|
||||
|
||||
//Собственное состояние - связи
|
||||
const [edges, setEdges] = useState(relations);
|
||||
|
||||
//Собственное состояние - перемещённый элемент
|
||||
const [movedNode, setMovedNode] = useState(null);
|
||||
|
||||
//При изменении элементов на диаграмме
|
||||
const handleNodesChange = useCallback(
|
||||
changes => {
|
||||
//При выборе атрибута подсветим всю сущность
|
||||
const tmpChanges = changes.reduce((prevChanges, curChanges) => {
|
||||
const tmp = { ...curChanges };
|
||||
if (tmp.type == "select") {
|
||||
const chEnt = entities.find(e => e.id === tmp.id);
|
||||
if (chEnt && chEnt?.data?.parentEntity) {
|
||||
prevChanges.push({ ...curChanges, id: chEnt.data.parentEntity });
|
||||
tmp.selected = false;
|
||||
}
|
||||
}
|
||||
prevChanges.push(tmp);
|
||||
return prevChanges;
|
||||
}, []);
|
||||
//Применим изменения в диаграмме
|
||||
setNodes(nodesSnapshot => applyNodeChanges(tmpChanges, nodesSnapshot));
|
||||
//Если двигали сущность - запомним начало движения
|
||||
if (changes.length == 1 && changes[0].type == "position" && changes[0].dragging)
|
||||
setMovedNode({ id: changes[0].id, position: { ...changes[0].position } });
|
||||
//Если закончили двигать сущность и ранее запомнили начало движения - вызываем колбэки для нажатия и двиения сущности
|
||||
if (changes.length == 1 && changes[0].type == "position" && !changes[0].dragging && movedNode) {
|
||||
if (onEntityPositionChange) onEntityPositionChange(movedNode.id, movedNode.position);
|
||||
if (onEntityClick) onEntityClick(movedNode.id);
|
||||
setMovedNode(null);
|
||||
}
|
||||
//Если удалили сущность - вызываем колбэк для удаления
|
||||
if (changes[0].type == "remove" && entities.find(e => e.id == changes[0].id && e.type == NODE_TYPE.ENTITY) && onEntityRemove)
|
||||
onEntityRemove(changes[0].id);
|
||||
},
|
||||
[movedNode, entities, onEntityClick, onEntityPositionChange, onEntityRemove]
|
||||
);
|
||||
|
||||
//При выборе элемента диаграммы
|
||||
const handleNodeClick = useCallback(
|
||||
(e, node) =>
|
||||
node?.type == NODE_TYPE.ENTITY
|
||||
? onEntityClick && onEntityClick(node?.id)
|
||||
: onEntityAttrClick && onEntityAttrClick(node?.parentId, node?.id),
|
||||
[onEntityClick, onEntityAttrClick]
|
||||
);
|
||||
|
||||
//При связывании элементов на диаграмме
|
||||
const handleConnect = connection => {
|
||||
setEdges(state => addEdge({ ...connection, id: `${connection.source}-${connection.target}` }, state));
|
||||
onRelationAdd && onRelationAdd(connection.source, connection.target);
|
||||
};
|
||||
|
||||
//При выборе связи диаграммы
|
||||
const handleEdgeClick = useCallback((e, edge) => onRelactionClick && onRelactionClick(edge.id), [onRelactionClick]);
|
||||
|
||||
//При изменении связей на диаграмме
|
||||
const handleEdgesChange = useCallback(
|
||||
changes => {
|
||||
setEdges(edgesSnapshot => applyEdgeChanges(changes, edgesSnapshot));
|
||||
if (changes.length == 1 && changes[0].type == "remove" && onRelationRemove) onRelationRemove(changes[0].id);
|
||||
},
|
||||
[onRelationRemove]
|
||||
);
|
||||
|
||||
//При нажатии на холст диаграммы
|
||||
const handlePaneClick = () => onPaneClick && onPaneClick();
|
||||
|
||||
//Валидация связи
|
||||
const validateConnection = connection => isValidConnection(connection, nodes, edges);
|
||||
|
||||
//Подсветка выбранной сущности
|
||||
|
||||
//При изменении состава сущностей
|
||||
useEffect(() => setNodes(entities), [entities]);
|
||||
|
||||
//При изменении состава связей
|
||||
useEffect(() => setEdges(relations), [relations]);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
nodeTypes={NODE_TYPES_COMPONENTS}
|
||||
edges={edges}
|
||||
onNodeClick={handleNodeClick}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgeClick={handleEdgeClick}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
onPaneClick={handlePaneClick}
|
||||
defaultEdgeOptions={{
|
||||
animated: true,
|
||||
style: STYLES.EDGE
|
||||
}}
|
||||
connectionLineStyle={STYLES.CONNECTION_LINE}
|
||||
onConnect={handleConnect}
|
||||
isValidConnection={validateConnection}
|
||||
className={"query_diagram"}
|
||||
>
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Диаграмма запроса
|
||||
QueryDiagram.propTypes = {
|
||||
entities: PropTypes.arrayOf(ENTITY_SHAPE),
|
||||
relations: PropTypes.arrayOf(RELATION_SHAPE),
|
||||
onEntityClick: PropTypes.func,
|
||||
onEntityAttrClick: PropTypes.func,
|
||||
onEntityPositionChange: PropTypes.func,
|
||||
onEntityRemove: PropTypes.func,
|
||||
onRelactionClick: PropTypes.func,
|
||||
onRelationAdd: PropTypes.func,
|
||||
onRelationRemove: PropTypes.func,
|
||||
onPaneClick: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { QueryDiagram };
|
@ -0,0 +1,122 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Свойства запроса
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useContext } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Button, Icon } from "@mui/material"; //Интерфейсные компоненты MUI
|
||||
import { MessagingСtx } from "../../../../context/messaging"; //Контекст сообщений приложения
|
||||
import { BUTTONS } from "../../../../../app.text"; //Общие текстовые ресурсы приложения
|
||||
import { EntityAddDialog } from "../entity_add_dialog/entity_add_dialog"; //Диалог добавления сущности
|
||||
import { EntityAttrsDialog } from "../entity_attrs_dialog/entity_attrs_dialog"; //Диалог настройки атрибутов сущности
|
||||
import { P8PEditorBox } from "../../../../components/editors/p8p_editor_box"; //Контейнер параметров редактора
|
||||
import { P8PEditorSubHeader } from "../../../../components/editors/p8p_editor_sub_header"; //Подзаголовок группы параметров редактора
|
||||
import { ENTITY_DATA_SHAPE } from "../entity/entity"; //Описание сущности
|
||||
import { RELATION_DATA_SHAPE } from "../relation/relation"; //Описание связи
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Свойства запроса
|
||||
const QueryOptions = ({ query, entity, relation, onEntityAdd, onEntityRemove, onRelationRemove, onQueryOptionsChanged }) => {
|
||||
//Отображение диалога добавления сущности
|
||||
const [openEntityAddDialog, setOpenEntityAddDialog] = useState(false);
|
||||
|
||||
//Отображение диалога настройки атрибутов сущности
|
||||
const [openEntityAttrsDialog, setOpenEntityAttrsDialog] = useState(false);
|
||||
|
||||
//Подключение к контексту сообщений
|
||||
const { showMsgWarn } = useContext(MessagingСtx);
|
||||
|
||||
//При нажатии на кнопку добавлении сущности в запрос
|
||||
const handleEntityAddClick = () => setOpenEntityAddDialog(true);
|
||||
|
||||
//При нажатии на кнопку настройки атрибутов сущности
|
||||
const handleEntityAttrsClick = () => setOpenEntityAttrsDialog(true);
|
||||
|
||||
//При нажатии на кнопку даления сущности из запроса
|
||||
const handleEntityRemoveClick = () =>
|
||||
showMsgWarn(`Удалить сущность "${entity.title}"?`, () => entity?.id && onEntityRemove && onEntityRemove(entity.id));
|
||||
|
||||
//При нажатии на кнопку даления связи из запроса
|
||||
const handleRelationRemoveClick = () =>
|
||||
showMsgWarn(
|
||||
`Удалить связь "${relation.source}" - "${relation.target}"?`,
|
||||
() => relation?.id && onRelationRemove && onRelationRemove(relation.id)
|
||||
);
|
||||
|
||||
//Закрытие диалога добавления сущности по "Отмена"
|
||||
const handleEntityAddDialogCancel = () => setOpenEntityAddDialog(false);
|
||||
|
||||
//Закрытие диалога добавления сущности по "ОК"
|
||||
const handleEntityAddDialogOk = values => onEntityAdd && onEntityAdd(values.name, res => res && setOpenEntityAddDialog(false));
|
||||
|
||||
//Закрытие диалога настройки атрибутов сущности по "Отмена"
|
||||
const handleEntityAttrsDialogCancel = () => setOpenEntityAttrsDialog(false);
|
||||
|
||||
//Закрытие диалога настройки атрибутов сущности по "ОК"
|
||||
const handleEntityAttrsDialogOk = () => {
|
||||
onQueryOptionsChanged && onQueryOptionsChanged();
|
||||
setOpenEntityAttrsDialog();
|
||||
};
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<>
|
||||
{openEntityAddDialog && <EntityAddDialog onOk={handleEntityAddDialogOk} onCancel={handleEntityAddDialogCancel} />}
|
||||
{openEntityAttrsDialog && (
|
||||
<EntityAttrsDialog query={query} {...entity} onOk={handleEntityAttrsDialogOk} onCancel={handleEntityAttrsDialogCancel} />
|
||||
)}
|
||||
<P8PEditorBox title={"Настройки запроса"}>
|
||||
<P8PEditorSubHeader title={"Параметры"} />
|
||||
<P8PEditorSubHeader title={"Условия отбора"} />
|
||||
<P8PEditorSubHeader title={"Сущности"} />
|
||||
<Button startIcon={<Icon>add</Icon>} onClick={handleEntityAddClick}>
|
||||
{BUTTONS.INSERT}
|
||||
</Button>
|
||||
{entity && (
|
||||
<>
|
||||
<P8PEditorSubHeader title={entity.title} />
|
||||
<Button startIcon={<Icon>edit_attributes</Icon>} onClick={handleEntityAttrsClick}>
|
||||
Атрибуты
|
||||
</Button>
|
||||
<Button startIcon={<Icon>delete</Icon>} onClick={handleEntityRemoveClick}>
|
||||
{BUTTONS.DELETE}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{relation && (
|
||||
<>
|
||||
<P8PEditorSubHeader title={"Связь"} />
|
||||
<Button startIcon={<Icon>delete</Icon>} onClick={handleRelationRemoveClick}>
|
||||
{BUTTONS.DELETE}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</P8PEditorBox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств компонента - Свойства запроса
|
||||
QueryOptions.propTypes = {
|
||||
query: PropTypes.number.isRequired,
|
||||
entity: ENTITY_DATA_SHAPE,
|
||||
relation: RELATION_DATA_SHAPE,
|
||||
onEntityAdd: PropTypes.func,
|
||||
onEntityRemove: PropTypes.func,
|
||||
onRelationRemove: PropTypes.func,
|
||||
onQueryOptionsChanged: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { QueryOptions };
|
27
app/panels/query_editor/components/relation/relation.js
Normal file
27
app/panels/query_editor/components/relation/relation.js
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Компоненты: Связь сущностей запроса
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Структура данных о связи сущностей запроса
|
||||
const RELATION_DATA_SHAPE = PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
source: PropTypes.string.isRequired,
|
||||
target: PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { RELATION_DATA_SHAPE };
|
290
app/panels/query_editor/hooks.js
Normal file
290
app/panels/query_editor/hooks.js
Normal file
@ -0,0 +1,290 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Пользовательские хуки
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { useState, useContext, useEffect, useCallback } from "react"; //Классы React
|
||||
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
|
||||
import { NODE_TYPE } from "./common"; //Общие ресурсы и константы редактора
|
||||
|
||||
//------------------------------------
|
||||
//Вспомогательные функции и компоненты
|
||||
//------------------------------------
|
||||
|
||||
//Конвертация серверного описания сущностей запроса в элементы диаграммы
|
||||
const serverEntity2QueryDiagramNodes = entity => {
|
||||
const groupWidth = 250;
|
||||
const nameColumnHeight = 50;
|
||||
const columns = entity?.XATTRS?.XATTR || [];
|
||||
const columnsCount = columns.length;
|
||||
const groupHeight = nameColumnHeight + columnsCount * 50;
|
||||
|
||||
const groupNode = {
|
||||
id: entity.id,
|
||||
type: NODE_TYPE.ENTITY,
|
||||
data: { ...entity },
|
||||
position: { x: entity.x, y: entity.y },
|
||||
style: {
|
||||
width: groupWidth,
|
||||
height: groupHeight
|
||||
},
|
||||
draggable: true
|
||||
};
|
||||
|
||||
const columnNodes = columns.map((column, index, columns) => {
|
||||
const x = 1;
|
||||
const y = 50 * (index + 1);
|
||||
const width = groupWidth - 2;
|
||||
const height = 50;
|
||||
|
||||
const isLast = index === columns.length - 1;
|
||||
const defaultColumnStyles = {
|
||||
borderBottom: "1px solid #dee2e6"
|
||||
};
|
||||
const lastColumnStyles = {
|
||||
borderBottom: "none"
|
||||
};
|
||||
const otherStyles = isLast ? lastColumnStyles : defaultColumnStyles;
|
||||
|
||||
return {
|
||||
id: column.id,
|
||||
type: NODE_TYPE.ATTRIBUTE,
|
||||
data: {
|
||||
...column,
|
||||
included: false,
|
||||
parentEntity: entity.id
|
||||
},
|
||||
position: { x, y },
|
||||
parentId: entity.id,
|
||||
extent: "parent",
|
||||
style: {
|
||||
width,
|
||||
height,
|
||||
...otherStyles
|
||||
},
|
||||
draggable: false,
|
||||
selectable: true
|
||||
};
|
||||
});
|
||||
|
||||
return [groupNode, ...columnNodes];
|
||||
};
|
||||
|
||||
//Конвертация серверного описания запроса в данные для редактора диаграмм
|
||||
const serverQueryData2QueryDiagram = (entities, relations) => {
|
||||
const result = { entities: [], relations: [...relations] };
|
||||
entities.forEach(entity => {
|
||||
const nodes = serverEntity2QueryDiagramNodes(entity);
|
||||
result.entities = [...result.entities, ...nodes];
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Работа с запросами
|
||||
const useQuery = () => {
|
||||
//Собственное состояние - флаг инициализированности
|
||||
const [isInit, setInit] = useState(false);
|
||||
|
||||
//Собственное состояние - флаг загрузки
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
//Собственное состояние - флаг необходимости обновления
|
||||
const [refresh, setRefresh] = useState(true);
|
||||
|
||||
//Собственное состояние - данные
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored } = useContext(BackEndСtx);
|
||||
|
||||
//Обновление данных
|
||||
const doRefresh = () => setRefresh(true);
|
||||
|
||||
//Добавление запроса
|
||||
const insertQuery = useCallback(
|
||||
async (code, name) => {
|
||||
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_INSERT", args: { SCODE: code, SNAME: name }, loader: false });
|
||||
setRefresh(true);
|
||||
},
|
||||
[executeStored]
|
||||
);
|
||||
|
||||
//Изменение запроса
|
||||
const updateQuery = useCallback(
|
||||
async (query, code, name) => {
|
||||
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_UPDATE", args: { NRN: query, SCODE: code, SNAME: name }, loader: false });
|
||||
setRefresh(true);
|
||||
},
|
||||
[executeStored]
|
||||
);
|
||||
|
||||
//Удаление запроса
|
||||
const deleteQuery = useCallback(
|
||||
async query => {
|
||||
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_DELETE", args: { NRN: query }, loader: false });
|
||||
setRefresh(true);
|
||||
},
|
||||
[executeStored]
|
||||
);
|
||||
|
||||
//Установка флага готовности запроса
|
||||
const setQueryReady = useCallback(
|
||||
async (query, ready) => {
|
||||
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_READY_SET", args: { NRN: query, NREADY: ready }, loader: false });
|
||||
setRefresh(true);
|
||||
},
|
||||
[executeStored]
|
||||
);
|
||||
|
||||
//Установка флага публичности запроса
|
||||
const setQueryPbl = useCallback(
|
||||
async (query, pbl) => {
|
||||
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_PBL_SET", args: { NRN: query, NPBL: pbl }, loader: false });
|
||||
setRefresh(true);
|
||||
},
|
||||
[executeStored]
|
||||
);
|
||||
|
||||
//При необходимости получить/обновить данные
|
||||
useEffect(() => {
|
||||
//Загрузка данных с сервера
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_QE.QUERY_LIST",
|
||||
respArg: "COUT",
|
||||
isArray: name => ["XQUERY"].includes(name),
|
||||
attributeValueProcessor: (name, val) => (["code", "name"].includes(name) ? undefined : val),
|
||||
loader: true
|
||||
});
|
||||
setData(data?.XQUERIES?.XQUERY || []);
|
||||
setInit(true);
|
||||
} finally {
|
||||
setRefresh(false);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
//Если надо обновить
|
||||
if (refresh)
|
||||
//Получим данные
|
||||
loadData();
|
||||
}, [refresh, executeStored]);
|
||||
|
||||
//Возвращаем интерфейс хука
|
||||
return [data, insertQuery, updateQuery, deleteQuery, setQueryReady, setQueryPbl, doRefresh, isLoading, isInit];
|
||||
};
|
||||
|
||||
//Работа с содержимым запроса
|
||||
const useQueryDesc = query => {
|
||||
//Собственное состояние - флаг инициализированности
|
||||
const [isInit, setInit] = useState(false);
|
||||
|
||||
//Собственное состояние - флаг загрузки
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
//Собственное состояние - флаг необходимости обновления
|
||||
const [refresh, setRefresh] = useState(true);
|
||||
|
||||
//Собственное состояние - данные
|
||||
const [data, setData] = useState(null);
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored } = useContext(BackEndСtx);
|
||||
|
||||
//Обновление данных
|
||||
const doRefresh = () => setRefresh(true);
|
||||
|
||||
//Добавление сущности в запрос
|
||||
const addEnt = useCallback(
|
||||
async (name, type) => {
|
||||
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_ENT_ADD", args: { NRN: query, SNAME: name, STYPE: type }, loader: false });
|
||||
setRefresh(true);
|
||||
},
|
||||
[query, executeStored]
|
||||
);
|
||||
|
||||
//Удаление сущности из запроса
|
||||
const removeEnt = useCallback(
|
||||
async ent => {
|
||||
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_ENT_REMOVE", args: { NRN: query, SID: ent }, loader: false });
|
||||
setRefresh(true);
|
||||
},
|
||||
[query, executeStored]
|
||||
);
|
||||
|
||||
//Сохранение координат сущности на диаграммем
|
||||
const setEntPosition = useCallback(
|
||||
async (ent, x, y) => {
|
||||
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_ENT_POSITION_SET", args: { NRN: query, SID: ent, NX: x, NY: y }, loader: false });
|
||||
},
|
||||
[query, executeStored]
|
||||
);
|
||||
|
||||
//Добавление отношения сущностей в запрос
|
||||
const addRl = useCallback(
|
||||
async (source, target) => {
|
||||
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_RL_ADD", args: { NRN: query, SSOURCE: source, STARGET: target }, loader: false });
|
||||
setRefresh(true);
|
||||
},
|
||||
[query, executeStored]
|
||||
);
|
||||
|
||||
//Удаление отношения сущностей из запроса
|
||||
const removeRl = useCallback(
|
||||
async rl => {
|
||||
await executeStored({ stored: "PKG_P8PANELS_QE.QUERY_RL_REMOVE", args: { NRN: query, SID: rl }, loader: false });
|
||||
setRefresh(true);
|
||||
},
|
||||
[query, executeStored]
|
||||
);
|
||||
|
||||
//При необходимости получить/обновить данные
|
||||
useEffect(() => {
|
||||
//Загрузка данных с сервера
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_QE.QUERY_DESC",
|
||||
args: { NRN: query },
|
||||
respArg: "COUT",
|
||||
isArray: name => ["XENT", "XATTR", "XRL"].includes(name),
|
||||
loader: true
|
||||
});
|
||||
setData(serverQueryData2QueryDiagram(data?.XENTS?.XENT || [], data?.XRLS?.XRL || []));
|
||||
setInit(true);
|
||||
} finally {
|
||||
setRefresh(false);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
//Если надо обновить
|
||||
if (refresh)
|
||||
if (query)
|
||||
//Если есть для чего получать данные
|
||||
loadData();
|
||||
//Нет идентификатора запроса - нет данных
|
||||
else setData(null);
|
||||
}, [refresh, query, executeStored]);
|
||||
|
||||
//При изменении входных свойств - поднимаем флаг обновления
|
||||
useEffect(() => setRefresh(true), [query]);
|
||||
|
||||
//Возвращаем интерфейс хука
|
||||
return [data, addEnt, removeEnt, setEntPosition, addRl, removeRl, doRefresh, isLoading, isInit];
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { useQuery, useQueryDesc };
|
16
app/panels/query_editor/index.js
Normal file
16
app/panels/query_editor/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Редактор запросов: точка входа
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import { QueryEditor } from "./query_editor"; //Корневая панель редактора
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export const RootClass = QueryEditor;
|
196
app/panels/query_editor/query_editor.js
Normal file
196
app/panels/query_editor/query_editor.js
Normal file
@ -0,0 +1,196 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - Редактор запросов
|
||||
Корневой компонент
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useContext } from "react"; //Классы React
|
||||
import { Box, Grid } from "@mui/material"; //Интерфейсные компоненты MUI
|
||||
import { ApplicationСtx } from "../../context/application"; //Контекст взаимодействия с приложением
|
||||
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Компоненты рабочего стола
|
||||
import { P8PEditorToolBar } from "../../components/editors/p8p_editor_toolbar"; //Панель инструментов редактора
|
||||
import { QueryDiagram } from "./components/query_diagram/query_diagram"; //Диаграмма запроса
|
||||
import { QueryOptions } from "./components/query_options/query_options"; //Свойства запроса
|
||||
import { QueriesManager } from "./components/queries_manager/queries_manager"; //Менеджер запросов
|
||||
import { useQueryDesc } from "./hooks"; //Пользовательские хуки
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Заголовок панели по умолчанию
|
||||
const APP_BAR_TITLE_DEFAULT = "Редактор запросов";
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONTAINER: { display: "flex" },
|
||||
GRID_CONTAINER: { height: `calc(100vh - ${APP_BAR_HEIGHT})` },
|
||||
GRID_ITEM_INSPECTOR: { backgroundColor: "#e9ecef" }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Корневой компонент редактора запросов
|
||||
const QueryEditor = () => {
|
||||
//Текущий запрос
|
||||
const [query, setQuery] = useState(null);
|
||||
|
||||
//Текущая сущность
|
||||
const [entity, setEntity] = useState(null);
|
||||
|
||||
//Текущая связь
|
||||
const [relation, setRelation] = useState(null);
|
||||
|
||||
//Отображения менеджера запросов
|
||||
const [openQueriesManager, setOpenQueriesManager] = useState(true);
|
||||
|
||||
//Получение данных запроса
|
||||
const [queryDiagram, addEnt, removeEnt, setEntPosition, addRl, removeRl, doRefresh] = useQueryDesc(query);
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { setAppBarTitle } = useContext(ApplicationСtx);
|
||||
|
||||
//Выбор сущности
|
||||
const selectEntity = ent => {
|
||||
setRelation(null);
|
||||
const queryEnt = queryDiagram.entities.find(e => e.id === ent);
|
||||
if (queryEnt) setEntity({ ...queryEnt.data });
|
||||
};
|
||||
|
||||
//Выбор связи
|
||||
const selectRelation = rl => {
|
||||
setEntity(null);
|
||||
const queryRl = queryDiagram.relations.find(r => r.id === rl);
|
||||
if (queryRl) setRelation({ ...queryRl });
|
||||
};
|
||||
|
||||
//Сброс выбора связи и сущности
|
||||
const cleanupEnRlSelection = () => {
|
||||
setRelation(null);
|
||||
setEntity(null);
|
||||
};
|
||||
|
||||
//Обработка изменения положения сущности на диаграмме
|
||||
const handleEntityPositionChange = (ent, position) => setEntPosition(ent, position.x, position.y);
|
||||
|
||||
//Обработка добавления сущности в запрос
|
||||
const handleEntityAdd = async (entName, cb) => {
|
||||
await addEnt(entName, "VIEW");
|
||||
cb(true);
|
||||
};
|
||||
|
||||
//Обработка удаления сущности из запроса
|
||||
const handleEntityRemove = async ent => {
|
||||
await removeEnt(ent);
|
||||
if (entity && entity?.id === ent) cleanupEnRlSelection();
|
||||
};
|
||||
|
||||
//Обработка выделения сущности
|
||||
const handleEntityClick = ent => selectEntity(ent);
|
||||
|
||||
//Обработка выделения тарибута сущности
|
||||
const handleEntityAttrClick = ent => selectEntity(ent);
|
||||
|
||||
//Обработка выделения связи
|
||||
const handleRelationClick = rl => selectRelation(rl);
|
||||
|
||||
//Обработка добавления отношения cущностей
|
||||
const handleRelationAdd = (source, target) => {
|
||||
cleanupEnRlSelection();
|
||||
addRl(source, target);
|
||||
};
|
||||
|
||||
//Обработка удаления отношения cущностей
|
||||
const handleRelationRemove = async rl => {
|
||||
await removeRl(rl);
|
||||
if (relation && relation?.id === rl) cleanupEnRlSelection();
|
||||
};
|
||||
|
||||
//При нажатии на панели (пустом месте) диаграммы запроса
|
||||
const handlePaneClick = () => cleanupEnRlSelection();
|
||||
|
||||
//Открытие менеджера запросов
|
||||
const handleOpenQueriesManager = () => setOpenQueriesManager(true);
|
||||
|
||||
//Закрытие менеджера запросов
|
||||
const handleCancelQueriesManager = () => setOpenQueriesManager(false);
|
||||
|
||||
//Закрытие запроса
|
||||
const handleQueryClose = () => {
|
||||
setAppBarTitle(APP_BAR_TITLE_DEFAULT);
|
||||
cleanupEnRlSelection();
|
||||
setQuery(null);
|
||||
};
|
||||
|
||||
//При выборе запроса
|
||||
const handleQuerySelect = ({ rn, name }) => {
|
||||
setAppBarTitle(`Запрос [${name}]`);
|
||||
setQuery(rn);
|
||||
setOpenQueriesManager(false);
|
||||
};
|
||||
|
||||
//При изменении свойств запроса
|
||||
const handleQueryOptionsChanged = () => {
|
||||
cleanupEnRlSelection();
|
||||
doRefresh();
|
||||
};
|
||||
|
||||
//Панель инструмментов
|
||||
const toolBar = (
|
||||
<P8PEditorToolBar
|
||||
items={[
|
||||
{ icon: "file_open", title: "Менеджер запросов", onClick: handleOpenQueriesManager },
|
||||
{ icon: "close", title: "Закрыть запрос", onClick: handleQueryClose, disabled: !query }
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box sx={STYLES.CONTAINER}>
|
||||
{openQueriesManager && <QueriesManager current={query} onQuerySelect={handleQuerySelect} onCancel={handleCancelQueriesManager} />}
|
||||
<Grid container sx={STYLES.GRID_CONTAINER} columns={25}>
|
||||
<Grid item xs={20}>
|
||||
{queryDiagram && (
|
||||
<QueryDiagram
|
||||
{...queryDiagram}
|
||||
onEntityClick={handleEntityClick}
|
||||
onEntityAttrClick={handleEntityAttrClick}
|
||||
onEntityPositionChange={handleEntityPositionChange}
|
||||
onEntityRemove={handleEntityRemove}
|
||||
onRelactionClick={handleRelationClick}
|
||||
onRelationAdd={handleRelationAdd}
|
||||
onRelationRemove={handleRelationRemove}
|
||||
onPaneClick={handlePaneClick}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={5} sx={STYLES.GRID_ITEM_INSPECTOR}>
|
||||
{toolBar}
|
||||
{query && (
|
||||
<QueryOptions
|
||||
query={query}
|
||||
entity={entity}
|
||||
relation={relation}
|
||||
onEntityAdd={handleEntityAdd}
|
||||
onEntityRemove={handleEntityRemove}
|
||||
onRelationRemove={handleRelationRemove}
|
||||
onQueryOptionsChanged={handleQueryOptionsChanged}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { QueryEditor };
|
@ -1,358 +0,0 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Панель мониторинга: Диалог добавления/исправления/удаления компонентов настройки регламентированного отчёта
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useContext, useCallback, useEffect } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Dialog, DialogTitle, IconButton, Icon, DialogContent, Typography, DialogActions, Button } from "@mui/material"; //Интерфейсные компоненты
|
||||
import { ApplicationСtx } from "../../../context/application"; //Контекст приложения
|
||||
import { BackEndСtx } from "../../../context/backend"; //Контекст взаимодействия с сервером
|
||||
import { IUDFormTextField } from "./iud_form_text_field"; //Компонент поля ввода
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CLOSE_BUTTON: {
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
top: 8,
|
||||
color: theme => theme.palette.grey[500]
|
||||
},
|
||||
PADDING_DIALOG_BUTTONS_RIGHT: { paddingRight: "32px" }
|
||||
};
|
||||
|
||||
//Статусы диалогового окна
|
||||
export const STATUSES = { CREATE: 0, EDIT: 1, DELETE: 2, RRPCONFSCTNMRK_CREATE: 3, RRPCONFSCTNMRK_EDIT: 4, RRPCONFSCTNMRK_DELETE: 5 };
|
||||
|
||||
//---------------
|
||||
//Тело компонента
|
||||
//---------------
|
||||
|
||||
const IUDFormDialog = ({ initial, onClose, onReload }) => {
|
||||
//Собственное состояние
|
||||
const [formData, setFormData] = useState({ ...initial });
|
||||
|
||||
//Подключение к контексту приложения
|
||||
const { pOnlineShowDictionary } = useContext(ApplicationСtx);
|
||||
|
||||
//Подключение к контексту взаимодействия с сервером
|
||||
const { executeStored } = useContext(BackEndСtx);
|
||||
|
||||
//При закрытии диалога без изменений
|
||||
const handleCancel = () => (onClose ? onClose() : null);
|
||||
|
||||
//При закрытии диалога с изменениями
|
||||
const handleOK = () => {
|
||||
if (onClose) {
|
||||
changeSections();
|
||||
onClose();
|
||||
} else null;
|
||||
};
|
||||
|
||||
//Отработка добавления/изсправления/удаления элемента
|
||||
const handleReload = () => {
|
||||
if (onReload) {
|
||||
onReload();
|
||||
} else null;
|
||||
};
|
||||
|
||||
//При изменении значения элемента
|
||||
const handleDialogItemChange = (item, value) => {
|
||||
setFormData(pv => ({ ...pv, [item]: value }));
|
||||
};
|
||||
|
||||
//Отработка изменений в разделе или показателе раздела
|
||||
const changeSections = useCallback(async () => {
|
||||
switch (formData.status) {
|
||||
case STATUSES.CREATE:
|
||||
await insertSections();
|
||||
break;
|
||||
case STATUSES.EDIT:
|
||||
await updateSections();
|
||||
break;
|
||||
case STATUSES.DELETE:
|
||||
await deleteSections();
|
||||
break;
|
||||
case STATUSES.RRPCONFSCTNMRK_CREATE:
|
||||
await addRRPCONFSCTNMRK();
|
||||
break;
|
||||
case STATUSES.RRPCONFSCTNMRK_EDIT:
|
||||
await editRRPCONFSCTNMRK();
|
||||
break;
|
||||
case STATUSES.RRPCONFSCTNMRK_DELETE:
|
||||
await deleteRRPCONFSCTNMRK();
|
||||
break;
|
||||
}
|
||||
handleReload();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [formData]);
|
||||
|
||||
//Добавление раздела
|
||||
const insertSections = useCallback(async () => {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_RRPCONFED.RRPCONFSCTN_INSERT",
|
||||
args: {
|
||||
NPRN: formData.prn,
|
||||
SCODE: formData.code,
|
||||
SNAME: formData.name
|
||||
}
|
||||
});
|
||||
setFormData(pv => ({
|
||||
...pv,
|
||||
rn: Number(data.NRN)
|
||||
}));
|
||||
}, [formData.prn, formData.code, formData.name, executeStored]);
|
||||
|
||||
//Исправление раздела
|
||||
const updateSections = useCallback(async () => {
|
||||
await executeStored({
|
||||
stored: "PKG_P8PANELS_RRPCONFED.RRPCONFSCTN_UPDATE",
|
||||
args: {
|
||||
NRN: formData.rn,
|
||||
SCODE: formData.code,
|
||||
SNAME: formData.name
|
||||
}
|
||||
});
|
||||
}, [formData.name, formData.code, formData.rn, executeStored]);
|
||||
|
||||
//Удаление раздела
|
||||
const deleteSections = useCallback(async () => {
|
||||
await executeStored({
|
||||
stored: "PKG_P8PANELS_RRPCONFED.RRPCONFSCTN_DELETE",
|
||||
args: {
|
||||
NRN: formData.rn
|
||||
}
|
||||
});
|
||||
}, [formData.rn, executeStored]);
|
||||
|
||||
//Добавление показателя раздела
|
||||
const addRRPCONFSCTNMRK = useCallback(async () => {
|
||||
await executeStored({
|
||||
stored: "PKG_P8PANELS_RRPCONFED.RRPCONFSCTNMRK_INSERT",
|
||||
args: {
|
||||
NPRN: formData.prn,
|
||||
SCODE: formData.code,
|
||||
SNAME: formData.name,
|
||||
NRRPROW: formData.rowRn,
|
||||
NRRPCOLUMN: formData.colRn
|
||||
}
|
||||
});
|
||||
}, [executeStored, formData.code, formData.colRn, formData.name, formData.prn, formData.rowRn]);
|
||||
|
||||
//Исправление показателя раздела
|
||||
const editRRPCONFSCTNMRK = useCallback(async () => {
|
||||
await executeStored({
|
||||
stored: "PKG_P8PANELS_RRPCONFED.RRPCONFSCTNMRK_UPDATE",
|
||||
args: { NRN: formData.rn, SNAME: formData.name }
|
||||
});
|
||||
}, [executeStored, formData.name, formData.rn]);
|
||||
|
||||
//Удаление показателя раздела
|
||||
const deleteRRPCONFSCTNMRK = useCallback(async () => {
|
||||
await executeStored({ stored: "PKG_P8PANELS_RRPCONFED.RRPCONFSCTNMRK_DELETE", args: { NRN: formData.rn } });
|
||||
}, [executeStored, formData.rn]);
|
||||
|
||||
//Формирование заголовка диалогового окна
|
||||
const formTitle = () => {
|
||||
switch (formData.status) {
|
||||
case STATUSES.CREATE:
|
||||
return "Добавление раздела";
|
||||
case STATUSES.EDIT:
|
||||
return "Исправление раздела";
|
||||
case STATUSES.DELETE:
|
||||
return "Удаление раздела";
|
||||
case STATUSES.RRPCONFSCTNMRK_CREATE:
|
||||
return "Добавление показателя раздела";
|
||||
case STATUSES.RRPCONFSCTNMRK_EDIT:
|
||||
return "Исправление показателя раздела";
|
||||
case STATUSES.RRPCONFSCTNMRK_DELETE:
|
||||
return "Удаление показателя раздела";
|
||||
}
|
||||
};
|
||||
|
||||
//Отрисовка диалогового окна
|
||||
const renderSwitch = () => {
|
||||
let btnText = "";
|
||||
switch (formData.status) {
|
||||
case STATUSES.CREATE:
|
||||
case STATUSES.RRPCONFSCTNMRK_CREATE:
|
||||
btnText = "Добавить";
|
||||
break;
|
||||
case STATUSES.EDIT:
|
||||
case STATUSES.RRPCONFSCTNMRK_EDIT:
|
||||
btnText = "Исправить";
|
||||
break;
|
||||
case STATUSES.DELETE:
|
||||
case STATUSES.RRPCONFSCTNMRK_DELETE:
|
||||
btnText = "Удалить";
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleOK({ formData });
|
||||
}}
|
||||
>
|
||||
{btnText}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
//Выбор строки
|
||||
const selectRow = (showDictionary, callBack) => {
|
||||
showDictionary({
|
||||
unitCode: "RRPRow",
|
||||
inputParameters: [{ name: "in_RN", value: formData.rowRn }],
|
||||
callBack: res => {
|
||||
if (res.success === true) {
|
||||
callBack(res.outParameters.out_CODE, res.outParameters.out_RN);
|
||||
setFormData(pv => ({
|
||||
...pv,
|
||||
reload: true,
|
||||
rowCode: res.outParameters.out_CODE,
|
||||
rowRn: res.outParameters.out_RN
|
||||
}));
|
||||
} else callBack(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//Выбор графы
|
||||
const selectColumn = (showDictionary, callBack) => {
|
||||
showDictionary({
|
||||
unitCode: "RRPColumn",
|
||||
inputParameters: [{ name: "in_RN", value: formData.colRn }],
|
||||
callBack: res => {
|
||||
if (res.success === true) {
|
||||
callBack(res.outParameters.out_CODE, res.outParameters.out_RN);
|
||||
setFormData(pv => ({
|
||||
...pv,
|
||||
reload: true,
|
||||
colCode: res.outParameters.out_CODE,
|
||||
colRn: res.outParameters.out_RN
|
||||
}));
|
||||
} else callBack(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//Получение мнемокода и наименования показателя раздела
|
||||
const getSctnMrkCodeName = useCallback(async () => {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_RRPCONFED.RRPCONFSCTNMRK_GET_CODE_NAME",
|
||||
args: {
|
||||
NRRPCONFSCTN: formData.prn,
|
||||
NRRPROW: formData.rowRn,
|
||||
NRRPCOLUMN: formData.colRn
|
||||
}
|
||||
});
|
||||
setFormData(pv => ({
|
||||
...pv,
|
||||
reload: false,
|
||||
code: data.SCODE,
|
||||
name: data.SNAME
|
||||
}));
|
||||
}, [executeStored, formData.colRn, formData.prn, formData.rowRn]);
|
||||
|
||||
//Считывание наименования показателя
|
||||
const getMarkName = useCallback(async () => {
|
||||
const data = await executeStored({
|
||||
stored: "PKG_P8PANELS_RRPCONFED.RRPCONFSCTNMRK_GET_NAME",
|
||||
args: {
|
||||
NRRPCONFSCTNMRK: formData.rn
|
||||
}
|
||||
});
|
||||
setFormData(pv => ({
|
||||
...pv,
|
||||
reload: false,
|
||||
name: data.SNAME
|
||||
}));
|
||||
}, [executeStored, formData.rn]);
|
||||
|
||||
//Получение наименования и мнемокода показателя раздела при заполнении необходимых полей
|
||||
useEffect(() => {
|
||||
//Если это добавление показателя и требуется сформировать мнемокод и наименование
|
||||
formData.status == STATUSES.RRPCONFSCTNMRK_CREATE && formData.reload && formData.rowRn && formData.colRn ? getSctnMrkCodeName() : null;
|
||||
//Если это исправление и требуется инициализировать наименование показателя
|
||||
formData.status == STATUSES.RRPCONFSCTNMRK_EDIT && formData.reload ? getMarkName() : null;
|
||||
}, [formData.status, formData.reload, formData.rowRn, formData.colRn, getSctnMrkCodeName, getMarkName]);
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Dialog open onClose={handleCancel}>
|
||||
<DialogTitle>{formTitle()}</DialogTitle>
|
||||
<IconButton aria-label="close" onClick={handleCancel} sx={STYLES.CLOSE_BUTTON}>
|
||||
<Icon>close</Icon>
|
||||
</IconButton>
|
||||
<DialogContent>
|
||||
{formData.status == STATUSES.DELETE || formData.status == STATUSES.RRPCONFSCTNMRK_DELETE ? (
|
||||
formData.status == STATUSES.DELETE ? (
|
||||
<Typography>Вы хотите удалить раздел {formData.name}?</Typography>
|
||||
) : (
|
||||
<Typography>Вы хотите удалить показатель раздела {formData.name}?</Typography>
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
{formData.status != STATUSES.RRPCONFSCTNMRK_EDIT ? (
|
||||
<IUDFormTextField
|
||||
elementCode="code"
|
||||
elementValue={formData.code}
|
||||
labelText="Мнемокод"
|
||||
onChange={handleDialogItemChange}
|
||||
/>
|
||||
) : null}
|
||||
<IUDFormTextField
|
||||
elementCode="name"
|
||||
elementValue={formData.name}
|
||||
labelText="Наименование"
|
||||
onChange={handleDialogItemChange}
|
||||
/>
|
||||
{formData.status == STATUSES.RRPCONFSCTNMRK_CREATE ? (
|
||||
<div>
|
||||
<IUDFormTextField
|
||||
elementCode="row"
|
||||
elementValue={formData.rowCode}
|
||||
labelText="Строка"
|
||||
onChange={handleDialogItemChange}
|
||||
dictionary={callBack => selectRow(pOnlineShowDictionary, callBack)}
|
||||
/>
|
||||
<IUDFormTextField
|
||||
elementCode="column"
|
||||
elementValue={formData.colCode}
|
||||
labelText="Графа"
|
||||
onChange={handleDialogItemChange}
|
||||
dictionary={callBack => selectColumn(pOnlineShowDictionary, callBack)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions sx={STYLES.PADDING_DIALOG_BUTTONS_RIGHT}>
|
||||
{renderSwitch()}
|
||||
<Button onClick={handleCancel}>Отмена</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог
|
||||
IUDFormDialog.propTypes = {
|
||||
initial: PropTypes.object.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
onReload: PropTypes.func
|
||||
};
|
||||
|
||||
//--------------------
|
||||
//Интерфейс компонента
|
||||
//--------------------
|
||||
|
||||
export { IUDFormDialog };
|
@ -1,87 +0,0 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Панель мониторинга: Компонент поля ввода
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useEffect } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Box, FormControl, InputLabel, Input, InputAdornment, IconButton, Icon } from "@mui/material"; //Интерфейсные компоненты
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
DIALOG_WINDOW_WIDTH: { width: 400 }
|
||||
};
|
||||
|
||||
//---------------
|
||||
//Тело компонента
|
||||
//---------------
|
||||
|
||||
//Поле ввода
|
||||
const IUDFormTextField = ({ elementCode, elementValue, labelText, onChange, dictionary, ...other }) => {
|
||||
//Значение элемента
|
||||
const [value, setValue] = useState(elementValue);
|
||||
|
||||
//При получении нового значения из вне
|
||||
useEffect(() => {
|
||||
setValue(elementValue);
|
||||
}, [elementValue]);
|
||||
|
||||
//Выбор значения из словаря
|
||||
const handleDictionaryClick = () =>
|
||||
dictionary ? dictionary(res => (res ? handleChange({ target: { name: elementCode, value: res } }) : null)) : null;
|
||||
|
||||
//Изменение значения элемента
|
||||
const handleChange = e => {
|
||||
setValue(e.target.value);
|
||||
if (onChange) onChange(e.target.name, e.target.value);
|
||||
};
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Box p={1}>
|
||||
<FormControl variant="standard" sx={STYLES.DIALOG_WINDOW_WIDTH} {...other}>
|
||||
<InputLabel htmlFor={elementCode}>{labelText}</InputLabel>
|
||||
<Input
|
||||
id={elementCode}
|
||||
name={elementCode}
|
||||
value={value ? value : ""}
|
||||
endAdornment={
|
||||
dictionary ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton aria-label={`${elementCode} select`} onClick={handleDictionaryClick} edge="end">
|
||||
<Icon>list</Icon>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
) : null
|
||||
}
|
||||
onChange={handleChange}
|
||||
multiline
|
||||
maxRows={4}
|
||||
/>
|
||||
</FormControl>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Поле ввода
|
||||
IUDFormTextField.propTypes = {
|
||||
elementCode: PropTypes.string.isRequired,
|
||||
elementValue: PropTypes.string,
|
||||
labelText: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
dictionary: PropTypes.func
|
||||
};
|
||||
|
||||
//--------------------
|
||||
//Интерфейс компонента
|
||||
//--------------------
|
||||
|
||||
export { IUDFormTextField };
|
43
app/panels/rrp_conf_editor/common.js
Normal file
43
app/panels/rrp_conf_editor/common.js
Normal file
@ -0,0 +1,43 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Обще стили и константы
|
||||
*/
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Допустимые варианты сортировки строк/граф
|
||||
const COL_ROW_ORDER = [
|
||||
{ name: "Номер", value: 0 },
|
||||
{ name: "Код", value: 1 },
|
||||
{ name: "Мнемокод", value: 2 }
|
||||
];
|
||||
|
||||
//Типовые цвета
|
||||
const BG_GRAY = "#e3e3e3";
|
||||
const BG_BLUE = "#1976d210";
|
||||
|
||||
//Типовые размеры шрифтов
|
||||
const FONT_SIZE_LARGE = "0.85rem";
|
||||
const FONT_SIZE_SMALL = "0.75rem";
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
TOOLBAR: { position: "absolute", right: 0, backgroundColor: BG_BLUE },
|
||||
LINK: { component: "button", cursor: "pointer", width: "-webkit-fill-available" },
|
||||
FONT_DATA_GRID: { fontSize: FONT_SIZE_SMALL },
|
||||
FONT_MARK_CARD_HEAD: { fontSize: FONT_SIZE_LARGE },
|
||||
FONT_MARK_CARD_BODY: { fontSize: FONT_SIZE_SMALL },
|
||||
BG_DATA_GRID_HEAD_CELL: { backgroundColor: BG_GRAY },
|
||||
BG_DATA_GRID_DATA_CELL: { backgroundColor: BG_GRAY },
|
||||
BG_MARK_CARD: { backgroundColor: BG_BLUE },
|
||||
BORDER_DATA_GRID_HEAD_CELL: { border: "1px solid white" },
|
||||
BORDER_DATA_GRID_DATA_CELL: { borderBottom: `1px solid ${BG_GRAY}` }
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { COL_ROW_ORDER, STYLES };
|
59
app/panels/rrp_conf_editor/components/action_message.js
Normal file
59
app/panels/rrp_conf_editor/components/action_message.js
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Компонент панели: Сообщение с действиями
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Icon, Stack, Typography } from "@mui/material"; //Интерфейсные элементы
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONTAINER: { height: "100%", width: "100%" }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Сообщение с действиями
|
||||
const ActionMessage = ({ icon, title, desc, children }) => {
|
||||
return (
|
||||
<Stack direction={"column"} justifyContent={"center"} sx={STYLES.CONTAINER}>
|
||||
<Stack direction={"row"} justifyContent={"center"} alignItems={"center"}>
|
||||
<Icon color={"disabled"}>{icon}</Icon>
|
||||
<Typography align={"center"} color={"text.secondary"} variant={"button"}>
|
||||
{title}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography align={"center"} variant={"caption"}>
|
||||
{desc}
|
||||
</Typography>
|
||||
<Stack direction={"row"} justifyContent={"center"} alignItems={"center"}>
|
||||
{children}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Сообщение с действиями
|
||||
ActionMessage.propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
desc: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)])
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { ActionMessage };
|
87
app/panels/rrp_conf_editor/components/dialog_help.js
Normal file
87
app/panels/rrp_conf_editor/components/dialog_help.js
Normal file
@ -0,0 +1,87 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Компонент панели: Диалог дополнительной информации
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Typography, List, ListItem } from "@mui/material"; //Интерфейсные элементы
|
||||
import { P8PDialog } from "../../../components/p8p_dialog"; //Типовой диалог
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
HELP_LIST_ITEM: { padding: "0px 0px 0px 5px", whiteSpace: "pre" },
|
||||
HELP_LIST_ITEM_NAME: { fontWeight: "bold", minWidth: "45px" }
|
||||
};
|
||||
|
||||
//------------------------------------
|
||||
//Вспомогательные функции и компоненты
|
||||
//------------------------------------
|
||||
|
||||
//Элемент списка расшифровки состава
|
||||
const HelpListItem = ({ name, desc }) => {
|
||||
return (
|
||||
<ListItem sx={STYLES.HELP_LIST_ITEM}>
|
||||
<Typography sx={STYLES.HELP_LIST_ITEM_NAME}>{name}</Typography>
|
||||
<Typography>{` - ${desc}`}</Typography>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Элемент списка расшифровки состава
|
||||
HelpListItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
desc: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Диалог дополнительной информации
|
||||
const DialogHelp = ({ onClose }) => {
|
||||
//При закрытии диалога
|
||||
const handleClose = () => onClose && onClose();
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<P8PDialog title={"Информация"} onClose={handleClose}>
|
||||
<Typography>Карточки показателей содержат сокращенную информацию о типе состава показателя. Список сокращений:</Typography>
|
||||
<List disablePadding={true}>
|
||||
<HelpListItem name={"fx"} desc={"формула"} />
|
||||
<HelpListItem name={"СЗ"} desc={"статическое значение"} />
|
||||
<HelpListItem name={"ХП"} desc={"хранимая процедура"} />
|
||||
<HelpListItem name={"РП"} desc={"расчетный показатель"} />
|
||||
<HelpListItem name={"ХО"} desc={"хозяйственные операции"} />
|
||||
<HelpListItem name={"РСДК"} desc={"расчёты с дебиторами/кредиторами"} />
|
||||
<HelpListItem name={"ОС"} desc={"остатки средств по счетам"} />
|
||||
<HelpListItem name={"ТМЦ"} desc={"остатки товарно-материальных ценностей"} />
|
||||
<HelpListItem name={"ДКЗ"} desc={"дебиторская/кредиторская задолженность"} />
|
||||
<HelpListItem name={"ИК"} desc={"инвентарная картотека"} />
|
||||
<HelpListItem name={"МБП"} desc={"картотека МБП"} />
|
||||
<HelpListItem name={"КОБП"} desc={"картотека операций будущих периодов"} />
|
||||
<HelpListItem name={"ДПНП"} desc={"декларация по налогу на прибыль"} />
|
||||
<HelpListItem name={"РО"} desc={"регламентированный отчет"} />
|
||||
</List>
|
||||
</P8PDialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог дополнительной информации
|
||||
DialogHelp.propTypes = {
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { DialogHelp };
|
103
app/panels/rrp_conf_editor/components/dialog_mark_iu.js
Normal file
103
app/panels/rrp_conf_editor/components/dialog_mark_iu.js
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Компонент панели: Диалог добавления/исправления показателя
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { useDictionary } from "../hooks"; //Кастомные хуки
|
||||
import { P8PDialog } from "../../../components/p8p_dialog"; //Типовой диалог
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Диалог добавления/исправления показателя
|
||||
const DialogMarkIU = ({
|
||||
code = "",
|
||||
name = "",
|
||||
rowCode = "",
|
||||
rowVersion = "",
|
||||
columnCode = "",
|
||||
columnVersion = "",
|
||||
insert = true,
|
||||
onOk,
|
||||
onCancel
|
||||
}) => {
|
||||
//Нажатие на кнопку "Ok"
|
||||
const handleOk = values => onOk && onOk({ ...values });
|
||||
|
||||
//Нажатие на кнопку "Отмена"
|
||||
const handleCancel = () => onCancel && onCancel();
|
||||
|
||||
//Хуки для работы со словарями
|
||||
const { selectRRPRow, selectRRPColumn } = useDictionary();
|
||||
|
||||
//Выбор строки из словаря
|
||||
const selectRow = (currentFormValues, setFormValues) => {
|
||||
selectRRPRow(
|
||||
currentFormValues.rowCode,
|
||||
currentFormValues.rowVersion,
|
||||
selectResult =>
|
||||
selectResult &&
|
||||
setFormValues([
|
||||
{ name: "rowCode", value: selectResult.code },
|
||||
{ name: "rowVersion", value: selectResult.version }
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
//Выбор графы из словаря
|
||||
const selectColumn = (currentFormValues, setFormValues) => {
|
||||
selectRRPColumn(
|
||||
currentFormValues.columnCode,
|
||||
currentFormValues.columnVersion,
|
||||
selectResult =>
|
||||
selectResult &&
|
||||
setFormValues([
|
||||
{ name: "columnCode", value: selectResult.code },
|
||||
{ name: "columnVersion", value: selectResult.version }
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<P8PDialog
|
||||
title={`${insert === true ? "Добавление" : "Исправление"} показателя`}
|
||||
inputs={[
|
||||
{ name: "code", value: code, label: "Мнемокод" },
|
||||
{ name: "name", value: name, label: "Наименование" },
|
||||
{ name: "rowCode", value: rowCode, label: "Строка", dictionary: selectRow },
|
||||
{ name: "rowVersion", value: rowVersion, label: "Редакция строки", disabled: true },
|
||||
{ name: "columnCode", value: columnCode, label: "Графа", dictionary: selectColumn },
|
||||
{ name: "columnVersion", value: columnVersion, label: "Редакция графы", disabled: true }
|
||||
]}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог добавления/исправления показателя
|
||||
DialogMarkIU.propTypes = {
|
||||
code: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
rowCode: PropTypes.string,
|
||||
rowVersion: PropTypes.string,
|
||||
columnCode: PropTypes.string,
|
||||
columnVersion: PropTypes.string,
|
||||
insert: PropTypes.bool,
|
||||
onOk: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { DialogMarkIU };
|
53
app/panels/rrp_conf_editor/components/dialog_order.js
Normal file
53
app/panels/rrp_conf_editor/components/dialog_order.js
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Компонент панели: Диалог сортировки
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { P8PDialog } from "../../../components/p8p_dialog"; //Типовой диалог
|
||||
import { COL_ROW_ORDER } from "../common"; //Обще стили и константы
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Диалог сортировки
|
||||
const DialogOrder = ({ rowOrder = 0, columnOrder = 0, onOk, onCancel }) => {
|
||||
//Нажатие на кнопку "Ok"
|
||||
const handleOk = values => onOk && onOk({ ...values });
|
||||
|
||||
//Нажатие на кнопку "Отмена"
|
||||
const handleCancel = () => onCancel && onCancel();
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<P8PDialog
|
||||
title={"Сортировка"}
|
||||
inputs={[
|
||||
{ name: "rowOrder", value: rowOrder, label: "Строки", list: COL_ROW_ORDER },
|
||||
{ name: "columnOrder", value: columnOrder, label: "Графы", list: COL_ROW_ORDER }
|
||||
]}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог сортировки
|
||||
DialogOrder.propTypes = {
|
||||
rowOrder: PropTypes.number,
|
||||
columnOrder: PropTypes.number,
|
||||
onOk: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { DialogOrder };
|
53
app/panels/rrp_conf_editor/components/dialog_section_iu.js
Normal file
53
app/panels/rrp_conf_editor/components/dialog_section_iu.js
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Компонент панели: Диалог добавления/исправления раздела
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { P8PDialog } from "../../../components/p8p_dialog"; //Типовой диалог
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Диалог добавления/исправления раздела
|
||||
const DialogSectionIU = ({ code = "", name = "", insert = true, onOk, onCancel }) => {
|
||||
//Нажатие на кнопку "Ok"
|
||||
const handleOk = values => onOk && onOk({ ...values });
|
||||
|
||||
//Нажатие на кнопку "Отмена"
|
||||
const handleCancel = () => onCancel && onCancel();
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<P8PDialog
|
||||
title={`${insert === true ? "Добавление" : "Исправление"} раздела`}
|
||||
inputs={[
|
||||
{ name: "code", value: code, label: "Мнемокод" },
|
||||
{ name: "name", value: name, label: "Наименование" }
|
||||
]}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог добавления/исправления раздела
|
||||
DialogSectionIU.propTypes = {
|
||||
code: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
insert: PropTypes.bool,
|
||||
onOk: PropTypes.func,
|
||||
onCancel: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { DialogSectionIU };
|
@ -1,343 +0,0 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Дополнительная разметка и вёрстка клиентских элементов
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import {
|
||||
IconButton,
|
||||
Icon,
|
||||
Link,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Box,
|
||||
Typography
|
||||
} from "@mui/material"; //Интерфейсные компоненты
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
export const STYLES = {
|
||||
BOX_ROW: { display: "flex", justifyContent: "center", alignItems: "center" },
|
||||
LINK_STYLE: { component: "button", cursor: "pointer", width: "-webkit-fill-available" },
|
||||
DATA_CELL: columnDef => ({
|
||||
padding: "5px 5px",
|
||||
fontSize: "0.775rem",
|
||||
letterSpacing: "0.005em",
|
||||
textAlign: "center",
|
||||
wordBreak: "break-all",
|
||||
backgroundColor: columnDef.name === "SROW_NAME" ? "#b4b4b4" : "trasparent"
|
||||
}),
|
||||
DATA_CELL_CARD: {
|
||||
padding: "0px 3px 3px 0px",
|
||||
border: "1px solid lightgrey"
|
||||
},
|
||||
DATA_CELL_CARD_HEADER: {
|
||||
padding: "0px"
|
||||
},
|
||||
DATA_CELL_CARD_SUBHEADER: {
|
||||
textAlign: "left",
|
||||
paddingLeft: "10px",
|
||||
fontSize: "1rem",
|
||||
fontWeight: "450"
|
||||
},
|
||||
DATA_CELL_CARD_CONTENT: listLength => {
|
||||
return {
|
||||
fontSize: "0.75rem",
|
||||
padding: "5px 0px",
|
||||
minHeight: "105px",
|
||||
maxHeight: "105px",
|
||||
overflowY: "auto",
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "8px"
|
||||
},
|
||||
"&::-webkit-scrollbar-track": {
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "#EBEBEB"
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
borderRadius: "8px",
|
||||
backgroundColor: "#b4b4b4"
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb:hover": {
|
||||
backgroundColor: "#808080"
|
||||
},
|
||||
"&:last-child": {
|
||||
paddingBottom: "0px"
|
||||
},
|
||||
...(listLength === 0 ? { display: "flex", justifyContent: "center", alignItems: "center" } : null)
|
||||
};
|
||||
},
|
||||
DATA_CELL_CARD_CONTEXT_FONT: {
|
||||
fontSize: "0.75rem"
|
||||
},
|
||||
DATA_CELL_CARD_CONTEXT_MARK: {
|
||||
padding: "0px 0px 0px 10px",
|
||||
borderBottom: "1px solid #EBEBEB"
|
||||
},
|
||||
DATA_CELL_CN: {
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "pre",
|
||||
padding: "0px 5px",
|
||||
maxWidth: "100px",
|
||||
border: "none"
|
||||
},
|
||||
GRID_PANEL_CARD: { maxWidth: 400, flexDirection: "column", display: "flex" },
|
||||
MARK_INFO: {
|
||||
fontSize: "0.8rem",
|
||||
textAlign: "left",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "pre",
|
||||
maxWidth: "max-content",
|
||||
width: "-webkit-fill-available"
|
||||
},
|
||||
BUTTON_CN_INSERT: {
|
||||
padding: "0px 8px",
|
||||
marginBottom: "2px",
|
||||
"& .MuiIcon-root": {
|
||||
fontWeight: "bold",
|
||||
fontSize: "1rem"
|
||||
}
|
||||
},
|
||||
HEAD_CELL: {
|
||||
backgroundColor: "#b4b4b4",
|
||||
textAlign: "center"
|
||||
},
|
||||
HEAD_CELL_STACK: {
|
||||
justifyContent: "space-around"
|
||||
}
|
||||
};
|
||||
|
||||
//------------------------------------
|
||||
//Вспомогательные функции и компоненты
|
||||
//------------------------------------
|
||||
|
||||
//Действия карты показателя
|
||||
const DataCellCardActions = ({ columnDef, menuItems, cellData, markRn }) => {
|
||||
//Собственное состояние
|
||||
const [cardActions, setCardActions] = useState({ anchorMenuMethods: null, openMethods: false });
|
||||
|
||||
//По нажатию на открытие меню действий
|
||||
const handleMethodsMenuButtonClick = event => {
|
||||
setCardActions(pv => ({ ...pv, anchorMenuMethods: event.currentTarget, openMethods: true }));
|
||||
};
|
||||
|
||||
//При закрытии меню
|
||||
const handleMethodsMenuClose = () => {
|
||||
setCardActions(pv => ({ ...pv, anchorMenuMethods: null, openMethods: false }));
|
||||
};
|
||||
return (
|
||||
<Box sx={STYLES.BOX_ROW}>
|
||||
<IconButton id={`${columnDef.name}_menu_button`} aria-haspopup="true" onClick={handleMethodsMenuButtonClick}>
|
||||
<Icon>more_vert</Icon>
|
||||
</IconButton>
|
||||
<Menu
|
||||
id={`${columnDef.name}_menu`}
|
||||
anchorEl={cardActions.anchorMenuMethods}
|
||||
open={cardActions.openMethods}
|
||||
onClose={handleMethodsMenuClose}
|
||||
>
|
||||
{menuItems.map(el => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={`${cellData}_${el.method}`}
|
||||
onClick={() => {
|
||||
el.func(markRn);
|
||||
handleMethodsMenuClose();
|
||||
}}
|
||||
>
|
||||
<Icon>{el.icon}</Icon>
|
||||
{el.name}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Действия карты показателя
|
||||
DataCellCardActions.propTypes = {
|
||||
columnDef: PropTypes.object.isRequired,
|
||||
menuItems: PropTypes.array,
|
||||
cellData: PropTypes.any,
|
||||
markRn: PropTypes.number
|
||||
};
|
||||
|
||||
//Таблица составов показателя
|
||||
const MarkCnList = ({ markRn, list, handleMarkCnOpen }) => {
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
{list.map((el, index) => {
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell
|
||||
sx={{ ...STYLES.DATA_CELL_CN, ...STYLES.DATA_CELL_CARD_CONTEXT_FONT }}
|
||||
title={el.SDESC}
|
||||
align="left"
|
||||
size="small"
|
||||
>
|
||||
<Link sx={STYLES.LINK_STYLE} onClick={() => handleMarkCnOpen(markRn, el.NRN)}>
|
||||
{el.SDESC}
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Таблица составов показателя
|
||||
MarkCnList.propTypes = {
|
||||
markRn: PropTypes.number.isRequired,
|
||||
list: PropTypes.array.isRequired,
|
||||
handleMarkCnOpen: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
//Ячейка таблицы строки
|
||||
const DataCellContent = ({ row, columnDef, menuItems, sectionRn, handleMarkAdd, handleMarkOpen, handleMarkCnOpen, handleMarkCnInsert }) => {
|
||||
//Считываем информацию о показателе
|
||||
let mark = {
|
||||
sectionRn: sectionRn,
|
||||
data: row[columnDef.name],
|
||||
nRn: row["NMARK_RN_" + columnDef.name.substring(5)],
|
||||
sCode: row["SMARK_CODE_" + columnDef.name.substring(5)],
|
||||
sRowCode: row["SROW_CODE"],
|
||||
nRowRn: row["NROW_RN"],
|
||||
sColCode: columnDef.name.substring(5),
|
||||
nColRn: row["NCOL_RN_" + columnDef.name.substring(5)],
|
||||
rCnList: row["MARK_CNS_" + columnDef.name.substring(5)] ? [...row["MARK_CNS_" + columnDef.name.substring(5)]] : []
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{mark.nRn ? (
|
||||
<Card variant={"plain"} sx={STYLES.DATA_CELL_CARD}>
|
||||
<CardHeader
|
||||
sx={STYLES.DATA_CELL_CARD_HEADER}
|
||||
subheader={
|
||||
<Box>
|
||||
<Link sx={STYLES.LINK_STYLE} onClick={() => (handleMarkOpen ? handleMarkOpen(mark.nRn) : null)}>
|
||||
Состав
|
||||
</Link>
|
||||
{mark.rCnList.length !== 0 ? (
|
||||
<IconButton sx={STYLES.BUTTON_CN_INSERT} aria-haspopup="true" onClick={() => handleMarkCnInsert(mark.nRn)}>
|
||||
<Icon>add</Icon>
|
||||
</IconButton>
|
||||
) : null}
|
||||
</Box>
|
||||
}
|
||||
subheaderTypographyProps={STYLES.DATA_CELL_CARD_SUBHEADER}
|
||||
action={<DataCellCardActions columnDef={columnDef} menuItems={menuItems} cellData={mark.data} markRn={mark.nRn} />}
|
||||
></CardHeader>
|
||||
<CardContent sx={STYLES.DATA_CELL_CARD_CONTEXT_MARK}>
|
||||
<Typography sx={STYLES.MARK_INFO} title={mark.sCode}>
|
||||
{mark.sCode}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardContent sx={STYLES.DATA_CELL_CARD_CONTENT(mark.rCnList.length)}>
|
||||
{mark.rCnList.length !== 0 ? (
|
||||
<MarkCnList markRn={mark.nRn} list={mark.rCnList} handleMarkCnOpen={handleMarkCnOpen} />
|
||||
) : (
|
||||
<Box>
|
||||
<Typography sx={STYLES.DATA_CELL_CARD_CONTEXT_FONT}>Показатель не имеет состава</Typography>
|
||||
<Link sx={STYLES.LINK_STYLE} onClick={() => (handleMarkCnInsert ? handleMarkCnInsert(mark.nRn) : null)}>
|
||||
Добавить
|
||||
</Link>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Box>
|
||||
<Typography sx={STYLES.DATA_CELL_CARD_CONTEXT_FONT}>Показатель отсутствует</Typography>
|
||||
<Link
|
||||
sx={STYLES.LINK_STYLE}
|
||||
onClick={() =>
|
||||
handleMarkOpen ? handleMarkAdd(mark.sectionRn, mark.nRowRn, mark.sRowCode, mark.nColRn, mark.sColCode) : null
|
||||
}
|
||||
>
|
||||
Добавить
|
||||
</Link>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Ячейка таблицы строки
|
||||
DataCellContent.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
columnDef: PropTypes.object.isRequired,
|
||||
menuItems: PropTypes.array,
|
||||
sectionRn: PropTypes.number.isRequired,
|
||||
handleMarkAdd: PropTypes.func,
|
||||
handleMarkOpen: PropTypes.func,
|
||||
handleMarkCnOpen: PropTypes.func,
|
||||
handleMarkCnInsert: PropTypes.func
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Генерация представления ячейки c данными показателя раздела регламентированного отчета
|
||||
export const confSctnMrkCellRender = ({
|
||||
row,
|
||||
columnDef,
|
||||
sectionRn,
|
||||
handleMarkAdd,
|
||||
handleMarkOpen,
|
||||
handleMarkCnOpen,
|
||||
handleMarkCnInsert,
|
||||
menuItems
|
||||
}) => {
|
||||
//Иницализируем стили
|
||||
let cellStyle = STYLES.DATA_CELL(columnDef);
|
||||
//Считываем значение
|
||||
let data = row[columnDef.name];
|
||||
//Если это не наименование строки и есть значение
|
||||
columnDef.name != "SROW_NAME" && data != undefined && columnDef.visible == true
|
||||
? (data = (
|
||||
<DataCellContent
|
||||
row={row}
|
||||
columnDef={columnDef}
|
||||
menuItems={menuItems}
|
||||
sectionRn={sectionRn}
|
||||
handleMarkAdd={handleMarkAdd}
|
||||
handleMarkOpen={handleMarkOpen}
|
||||
handleMarkCnOpen={handleMarkCnOpen}
|
||||
handleMarkCnInsert={handleMarkCnInsert}
|
||||
/>
|
||||
))
|
||||
: null;
|
||||
return { cellStyle: { ...cellStyle }, data: data };
|
||||
};
|
||||
|
||||
//Генерация представления ячейки заголовка группы c данными показателя раздела регламентированного отчета
|
||||
export const confSctnMrkHeadCellRender = ({ columnDef }) => {
|
||||
return {
|
||||
cellStyle: STYLES.HEAD_CELL,
|
||||
stackStyle: STYLES.HEAD_CELL_STACK,
|
||||
data: columnDef.caption
|
||||
};
|
||||
};
|
122
app/panels/rrp_conf_editor/components/mark_card.js
Normal file
122
app/panels/rrp_conf_editor/components/mark_card.js
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Компонент панели: Карточка показателя
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Box, Card, CardContent, Typography, Link, Divider } from "@mui/material"; //Интерфейсные компоненты
|
||||
import { APP_STYLES } from "../../../../app.styles"; //Общие стили приложения
|
||||
import { MarkCnList } from "./mark_cn_list"; //Состав показателя
|
||||
import { MarkCardToolbar } from "./mark_card_toolbar"; //Панель инструментов карточки
|
||||
import { STYLES as COMMON_STYLES } from "../common"; //Общие стили и константы
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CARD: hovered => ({
|
||||
padding: "0px 0px 0px 0px",
|
||||
border: "1px solid lightgrey",
|
||||
position: "relative",
|
||||
...(hovered ? COMMON_STYLES.BG_MARK_CARD : null)
|
||||
}),
|
||||
CARD_CONTENT_MARK_TITLE: { padding: "0px 0px 0px 0px" },
|
||||
CARD_CONTENT_MARK_CONSTITUTION: constitutionExists => ({
|
||||
padding: "8px",
|
||||
height: "105px",
|
||||
"&:last-child": { paddingBottom: "10px" },
|
||||
...COMMON_STYLES.FONT_MARK_CARD_BODY,
|
||||
...(!constitutionExists ? { display: "flex", justifyContent: "center", alignItems: "center" } : null)
|
||||
}),
|
||||
BOX_MARK_CONSTITUTION: {
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
overflowY: "auto",
|
||||
...APP_STYLES.SCROLL
|
||||
}
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Карточка показателя
|
||||
const MarkCard = ({ mark, code, name, constitution = [], onMarkAdd, onMarkUpdate, onMarkDelete, onMarkOpen, onMarkCnOpen, onMarkCnAdd }) => {
|
||||
//Флаг нахождения указателя мыши в карточке
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
//При попадании мыши на закладку раздела
|
||||
const handleCardMouseIn = () => setHovered(true);
|
||||
|
||||
//При выходе мыши из закладки раздела
|
||||
const handleCardMouseOut = () => setHovered(false);
|
||||
|
||||
//Флаг наличия данных в составе показателя
|
||||
const constitutionExists = constitution?.length > 0;
|
||||
|
||||
//Формирование представления
|
||||
return mark ? (
|
||||
<Card variant={"plain"} sx={STYLES.CARD(hovered)} onMouseEnter={handleCardMouseIn} onMouseLeave={handleCardMouseOut}>
|
||||
<CardContent sx={STYLES.CARD_CONTENT_MARK_TITLE}>
|
||||
<MarkCardToolbar
|
||||
title={code}
|
||||
desc={name}
|
||||
onOpen={onMarkOpen}
|
||||
onEdit={onMarkUpdate}
|
||||
onDelete={onMarkDelete}
|
||||
onCnAdd={onMarkCnAdd}
|
||||
showButtons={hovered}
|
||||
/>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardContent sx={STYLES.CARD_CONTENT_MARK_CONSTITUTION(constitutionExists)}>
|
||||
{constitutionExists ? (
|
||||
<Box sx={STYLES.BOX_MARK_CONSTITUTION}>
|
||||
<MarkCnList constitution={constitution} onMarkCnOpen={constitutionRn => onMarkCnOpen && onMarkCnOpen(constitutionRn)} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box>
|
||||
<Typography sx={COMMON_STYLES.FONT_MARK_CARD_BODY}>Показатель не имеет состава</Typography>
|
||||
<Link sx={COMMON_STYLES.LINK} onClick={onMarkCnAdd}>
|
||||
Добавить
|
||||
</Link>
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Box>
|
||||
<Typography sx={COMMON_STYLES.FONT_MARK_CARD_BODY}>Показатель отсутствует</Typography>
|
||||
<Link sx={COMMON_STYLES.LINK} onClick={onMarkAdd}>
|
||||
Добавить
|
||||
</Link>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Карточка показателя
|
||||
MarkCard.propTypes = {
|
||||
mark: PropTypes.number,
|
||||
code: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
constitution: PropTypes.arrayOf(PropTypes.object),
|
||||
onMarkAdd: PropTypes.func.isRequired,
|
||||
onMarkUpdate: PropTypes.func.isRequired,
|
||||
onMarkDelete: PropTypes.func.isRequired,
|
||||
onMarkOpen: PropTypes.func.isRequired,
|
||||
onMarkCnOpen: PropTypes.func.isRequired,
|
||||
onMarkCnAdd: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { MarkCard };
|
73
app/panels/rrp_conf_editor/components/mark_card_toolbar.js
Normal file
73
app/panels/rrp_conf_editor/components/mark_card_toolbar.js
Normal file
@ -0,0 +1,73 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Компонент панели: Панель инструментов карточки показателя
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Stack, IconButton, Icon, Typography, Link } from "@mui/material"; //Интерфейсные компоненты
|
||||
import { STYLES as COMMON_STYLES } from "../common"; //Общие стили и константы
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONTAINER: { ...COMMON_STYLES.TOOLBAR, width: "100%", position: "unset", right: "unset", height: "40px" }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Панель инструментов карточки показателя
|
||||
const MarkCardToolbar = ({ title, desc, onOpen, onEdit, onDelete, onCnAdd, showButtons = false }) => {
|
||||
return (
|
||||
<Stack direction={"row"} alignItems={"center"} justifyContent={"space-between"} sx={STYLES.CONTAINER} title={desc}>
|
||||
<Typography noWrap p={1} textAlign={"left"} variant={"subtitle1"} component={"div"} sx={COMMON_STYLES.FONT_MARK_CARD_HEAD}>
|
||||
{showButtons ? (
|
||||
<Link sx={COMMON_STYLES.LINK} onClick={onOpen}>
|
||||
{title}
|
||||
</Link>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</Typography>
|
||||
{showButtons && (
|
||||
<Stack direction={"row"}>
|
||||
<IconButton title={"Исправить показатель"} onClick={onEdit}>
|
||||
<Icon>edit</Icon>
|
||||
</IconButton>
|
||||
<IconButton title={"Добавить элемент состава"} onClick={onCnAdd}>
|
||||
<Icon>library_add</Icon>
|
||||
</IconButton>
|
||||
<IconButton title={"Удалить показатель"} onClick={onDelete}>
|
||||
<Icon>delete</Icon>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Панель инструментов карточки показателя
|
||||
MarkCardToolbar.propTypes = {
|
||||
title: PropTypes.string,
|
||||
desc: PropTypes.string,
|
||||
onOpen: PropTypes.func.isRequired,
|
||||
onEdit: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onCnAdd: PropTypes.func.isRequired,
|
||||
showButtons: PropTypes.bool
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { MarkCardToolbar };
|
67
app/panels/rrp_conf_editor/components/mark_cn_list.js
Normal file
67
app/panels/rrp_conf_editor/components/mark_cn_list.js
Normal file
@ -0,0 +1,67 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Компонент панели: Состав показателя
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Table, TableBody, TableRow, TableCell, Link } from "@mui/material"; //Интерфейсные компоненты
|
||||
import { STYLES as COMMON_STYLES } from "../common"; //Общие стили и константы
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
DATA_CELL_CN: {
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "pre",
|
||||
padding: "0px",
|
||||
maxWidth: "100px",
|
||||
border: "none",
|
||||
...COMMON_STYLES.FONT_MARK_CARD_BODY
|
||||
}
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Состав показателя
|
||||
const MarkCnList = ({ constitution, onMarkCnOpen }) => {
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
{constitution.map((el, index) => {
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell sx={STYLES.DATA_CELL_CN} title={el.SDESC} align={"left"} size={"small"}>
|
||||
<Link sx={COMMON_STYLES.LINK} onClick={() => onMarkCnOpen && onMarkCnOpen(el.NRN)}>
|
||||
{el.SDESC}
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Состав показателя
|
||||
MarkCnList.propTypes = {
|
||||
constitution: PropTypes.array.isRequired,
|
||||
onMarkCnOpen: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { MarkCnList };
|
187
app/panels/rrp_conf_editor/components/marks.js
Normal file
187
app/panels/rrp_conf_editor/components/marks.js
Normal file
@ -0,0 +1,187 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Компонент панели: Показатели раздела
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState, useContext } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Icon, Button } from "@mui/material"; //Интерфейсные элементы
|
||||
import { MessagingСtx } from "../../../context/messaging"; //Контекст сообщений
|
||||
import { P8PDataGrid, P8P_DATA_GRID_SIZE } from "../../../components/p8p_data_grid"; //Таблица данных
|
||||
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../../config_wrapper"; //Подключение компонентов к настройкам приложения
|
||||
import { APP_STYLES } from "../../../../app.styles"; //Типовые стили
|
||||
import { confSctnMrkCellRender, confSctnMrkHeadCellRender } from "../layouts"; //Дополнительная разметка и вёрстка клиентских элементов
|
||||
import { ActionMessage } from "./action_message"; //Сообщение с действиями
|
||||
import { DialogMarkIU } from "./dialog_mark_iu"; //Диалог добавления/исправления показателя
|
||||
import { DialogHelp } from "./dialog_help"; //Диалог помощи
|
||||
import { DialogOrder } from "./dialog_order"; //Диалог сортировки
|
||||
import { useDictionary } from "../hooks"; //Кастомные хуки
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
MARKS_DG_CONTAINER: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "auto",
|
||||
border: "unset",
|
||||
...APP_STYLES.SCROLL
|
||||
},
|
||||
MARKS_DG_TABLE: { tableLayout: "fixed", width: "auto" }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Показатели раздела
|
||||
const Marks = ({ marks, order, marksLoading, marksInit, onRefresh, onMarkInsert, onMarkUpdate, onMarkDelete, onOrderChange }) => {
|
||||
//Состояние - диалог сортировки
|
||||
const [dialogOrder, setDialogOrder] = useState(false);
|
||||
|
||||
//Состояние - диалог помощи
|
||||
const [dialogHelp, setDialogHelp] = useState(false);
|
||||
|
||||
//Состояние - Редактируемый показатель
|
||||
const [modMark, setModMark] = useState(null);
|
||||
|
||||
//Подключение к контексту сообщений
|
||||
const { showMsgWarn } = useContext(MessagingСtx);
|
||||
|
||||
//Подключение к словарям
|
||||
const { showMark, showMarkCn, showMarkCnAdd } = useDictionary();
|
||||
|
||||
//Изменение состояния диалога информации
|
||||
const toggleHelpDialog = () => setDialogHelp(pv => !pv);
|
||||
|
||||
//Изменение состояния диалога сортировки
|
||||
const toggleOrderDialog = () => setDialogOrder(pv => !pv);
|
||||
|
||||
//При необходимости обновления
|
||||
const handleRefresh = () => onRefresh && onRefresh();
|
||||
|
||||
//При вызове сортировки
|
||||
const handleOrder = () => toggleOrderDialog();
|
||||
|
||||
//При вызове помощи
|
||||
const handleHelp = () => toggleHelpDialog();
|
||||
|
||||
//Изменение состояния сортировки строк и граф
|
||||
const handleOrderChange = order => {
|
||||
onOrderChange && onOrderChange(order);
|
||||
toggleOrderDialog();
|
||||
};
|
||||
|
||||
//При добавлении показателя
|
||||
const handleMarkAdd = () => setModMark(true);
|
||||
|
||||
//При добавлении показателя по указанным строке/графе
|
||||
const handleMarkAddByRowCol = (row, column) => onMarkInsert({ row, column });
|
||||
|
||||
//При исправлении показателя
|
||||
const handleMarkUpdate = markDesc => setModMark({ ...markDesc });
|
||||
|
||||
//При удалении показателя
|
||||
const handleMarkDelete = mark => showMsgWarn("Удалить показатель?", () => onMarkDelete && onMarkDelete(mark));
|
||||
|
||||
//При переходе к показателю
|
||||
const handleMarkOpen = mark => showMark(mark, res => res.success && handleRefresh());
|
||||
|
||||
//При добавлении состава показателя
|
||||
const handleMarkCnAdd = mark => showMarkCnAdd(mark, res => res.success && handleRefresh());
|
||||
|
||||
//При переходе к составу показателя
|
||||
const handleMarkCnOpen = (mark, constitution) => showMarkCn(mark, constitution, res => res.success && handleRefresh());
|
||||
|
||||
//При закрытии диалога добавления/исправления по "ОК"
|
||||
const handleIUDialogOk = values => {
|
||||
if (modMark === true) onMarkInsert && onMarkInsert(values, res => res && setModMark(null));
|
||||
else onMarkUpdate && onMarkUpdate({ ...modMark, ...values }, res => res && setModMark(null));
|
||||
};
|
||||
|
||||
//При закрытии диалога добавления/исправления по "Отмена"
|
||||
const handleIUDialogCancel = () => setModMark(null);
|
||||
|
||||
//Формирование представления
|
||||
return (
|
||||
<>
|
||||
{dialogOrder && <DialogOrder {...order} onOk={handleOrderChange} onCancel={toggleOrderDialog} />}
|
||||
{dialogHelp && <DialogHelp onClose={toggleHelpDialog} />}
|
||||
{modMark && (
|
||||
<DialogMarkIU
|
||||
{...(modMark === true ? {} : modMark)}
|
||||
insert={modMark === true}
|
||||
onOk={handleIUDialogOk}
|
||||
onCancel={handleIUDialogCancel}
|
||||
/>
|
||||
)}
|
||||
{marksInit &&
|
||||
(marks ? (
|
||||
<P8PDataGrid
|
||||
{...P8P_DATA_GRID_CONFIG_PROPS}
|
||||
{...marks}
|
||||
tableStyle={STYLES.MARKS_DG_TABLE}
|
||||
containerComponentProps={{ elevation: 0, square: true, variant: "outlined", sx: STYLES.MARKS_DG_CONTAINER }}
|
||||
size={P8P_DATA_GRID_SIZE.SMALL}
|
||||
dataCellRender={prms =>
|
||||
confSctnMrkCellRender({
|
||||
...prms,
|
||||
onMarkAdd: handleMarkAddByRowCol,
|
||||
onMarkUpdate: handleMarkUpdate,
|
||||
onMarkDelete: handleMarkDelete,
|
||||
onMarkOpen: handleMarkOpen,
|
||||
onMarkCnOpen: handleMarkCnOpen,
|
||||
onMarkCnAdd: handleMarkCnAdd
|
||||
})
|
||||
}
|
||||
headCellRender={prms =>
|
||||
confSctnMrkHeadCellRender({
|
||||
...prms,
|
||||
onAdd: handleMarkAdd,
|
||||
onRefresh: handleRefresh,
|
||||
onOrder: handleOrder,
|
||||
onHelp: handleHelp
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
!marksLoading && (
|
||||
<ActionMessage icon={"info"} title={"В разделе нет показателей"} desc={"Добавьте новый"}>
|
||||
<Button startIcon={<Icon>add</Icon>} onClick={handleMarkAdd}>
|
||||
Показатель
|
||||
</Button>
|
||||
</ActionMessage>
|
||||
)
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Показатели раздела
|
||||
Marks.propTypes = {
|
||||
marks: PropTypes.object,
|
||||
order: PropTypes.object.isRequired,
|
||||
marksLoading: PropTypes.bool.isRequired,
|
||||
marksInit: PropTypes.bool.isRequired,
|
||||
onRefresh: PropTypes.func,
|
||||
onMarkInsert: PropTypes.func,
|
||||
onMarkUpdate: PropTypes.func,
|
||||
onMarkDelete: PropTypes.func,
|
||||
onOrderChange: PropTypes.func
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { Marks };
|
60
app/panels/rrp_conf_editor/components/marks_toolbar.js
Normal file
60
app/panels/rrp_conf_editor/components/marks_toolbar.js
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Компонент панели: Панель инструментов показателей
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Stack, Icon, IconButton } from "@mui/material"; //Интерфейсные компоненты
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
CONTAINER: { width: "100%" }
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Панель инструментов показателей
|
||||
const MarksToolbar = ({ onAdd, onRefresh, onOrder, onHelp }) => {
|
||||
//Формирование представления
|
||||
return (
|
||||
<Stack direction={"row"} alignItems={"center"} justifyContent={"center"} sx={STYLES.CONTAINER}>
|
||||
<IconButton title={"Добавить показатель"} onClick={onAdd}>
|
||||
<Icon>add</Icon>
|
||||
</IconButton>
|
||||
<IconButton title={"Обновить показатели"} onClick={onRefresh}>
|
||||
<Icon>refresh</Icon>
|
||||
</IconButton>
|
||||
<IconButton title={"Порядок сортировки показателей"} onClick={onOrder}>
|
||||
<Icon>sort</Icon>
|
||||
</IconButton>
|
||||
<IconButton title={"Легенда"} onClick={onHelp}>
|
||||
<Icon>help</Icon>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Панель инструментов показателей
|
||||
MarksToolbar.propTypes = {
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
onRefresh: PropTypes.func.isRequired,
|
||||
onOrder: PropTypes.func.isRequired,
|
||||
onHelp: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { MarksToolbar };
|
@ -1,243 +0,0 @@
|
||||
/*
|
||||
Парус 8 - Панели мониторинга - РО - Редактор настройки регламентированного отчёта
|
||||
Компонент панели: Раздел настройки
|
||||
*/
|
||||
|
||||
//---------------------
|
||||
//Подключение библиотек
|
||||
//---------------------
|
||||
|
||||
import React, { useState } from "react"; //Классы React
|
||||
import PropTypes from "prop-types"; //Контроль свойств компонента
|
||||
import { Box, IconButton, Icon, Dialog, DialogTitle, DialogContent, Typography, List, ListItem } from "@mui/material"; //Интерфейсные элементы
|
||||
import { P8PDataGrid, P8P_DATA_GRID_SIZE } from "../../../components/p8p_data_grid"; //Таблица данных
|
||||
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../../config_wrapper"; //Подключение компонентов к настройкам приложения
|
||||
import { SectionTabPanel } from "./section_tab_panel"; //Компонент вкладки раздела
|
||||
import { confSctnMrkCellRender, confSctnMrkHeadCellRender } from "./layouts"; //Дополнительная разметка и вёрстка клиентских элементов
|
||||
|
||||
//---------
|
||||
//Константы
|
||||
//---------
|
||||
|
||||
//Стили
|
||||
const STYLES = {
|
||||
GRID_SIZES: (height, pxOuterMenuH, pxPanelHeaderH, pxTabsH) => ({
|
||||
padding: 0,
|
||||
minWidth: "98vw",
|
||||
minHeight: (height - pxOuterMenuH - pxPanelHeaderH - pxTabsH) * 0.93,
|
||||
maxWidth: "98vw",
|
||||
maxHeight: (height - pxOuterMenuH - pxPanelHeaderH - pxTabsH) * 0.93
|
||||
}),
|
||||
TABLE_CONTAINER: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingTop: 1,
|
||||
paddingBottom: 1
|
||||
},
|
||||
SECTION_ACTIONS: { display: "flex", justifyContent: "space-between", padding: "0px 5px" },
|
||||
TABLE_SCROLL: {
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "12px",
|
||||
height: "12px"
|
||||
},
|
||||
"&::-webkit-scrollbar-track": {
|
||||
borderRadius: "88px",
|
||||
backgroundColor: "#EBEBEB"
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
borderRadius: "88px",
|
||||
backgroundColor: "#b4b4b4",
|
||||
backgroundClip: "padding-box",
|
||||
border: "3px solid #EBEBEB"
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb:hover": {
|
||||
backgroundColor: "#808080"
|
||||
}
|
||||
},
|
||||
HELP_LIST_ITEM: {
|
||||
padding: "0px 0px 0px 5px",
|
||||
whiteSpace: "pre",
|
||||
fontSize: "0.95rem"
|
||||
},
|
||||
HELP_LIST_ITEM_NAME: {
|
||||
fontWeight: "bold",
|
||||
fontSize: "inherit",
|
||||
minWidth: "45px"
|
||||
},
|
||||
HELP_LIST_ITEM_DESC: {
|
||||
fontSize: "inherit"
|
||||
}
|
||||
};
|
||||
|
||||
//------------------------------------
|
||||
//Вспомогательные функции и компоненты
|
||||
//------------------------------------
|
||||
|
||||
//Элемент списка расшифровки состава
|
||||
const HelpListItem = ({ name, desc }) => {
|
||||
return (
|
||||
<ListItem sx={STYLES.HELP_LIST_ITEM}>
|
||||
<Typography sx={STYLES.HELP_LIST_ITEM_NAME}>{name}</Typography>
|
||||
<Typography sx={STYLES.HELP_LIST_ITEM_DESC}>{` - ${desc}`}</Typography>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Элемент списка расшифровки состава
|
||||
HelpListItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
desc: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
//Диалог дополнительной информации
|
||||
const HelpDialog = ({ handleOpenHelpChange }) => {
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<Dialog open onClose={handleOpenHelpChange}>
|
||||
<DialogTitle>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Box flexGrow={1} textAlign="center">
|
||||
Информация
|
||||
</Box>
|
||||
<Box>
|
||||
<IconButton aria-label="close" onClick={handleOpenHelpChange}>
|
||||
<Icon>close</Icon>
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>Карточки показателей содержат сокращенную информацию о типе состава показателя.</Typography>
|
||||
<Typography>Список сокращений:</Typography>
|
||||
<List disablePadding={true}>
|
||||
<HelpListItem name="fx" desc="формула" />
|
||||
<HelpListItem name="СЗ" desc="статическое значение" />
|
||||
<HelpListItem name="ХП" desc="хранимая процедура" />
|
||||
<HelpListItem name="РП" desc="расчетный показатель" />
|
||||
<HelpListItem name="ХО" desc="хозяйственные операции" />
|
||||
<HelpListItem name="РСДК" desc="расчёты с дебиторами/кредиторами" />
|
||||
<HelpListItem name="ОС" desc="остатки средств по счетам" />
|
||||
<HelpListItem name="ТМЦ" desc="остатки товарно-материальных ценностей" />
|
||||
<HelpListItem name="ДКЗ" desc="дебиторская/кредиторская задолженность" />
|
||||
<HelpListItem name="ИК" desc="инвентарная картотека" />
|
||||
<HelpListItem name="МБП" desc="картотека МБП" />
|
||||
<HelpListItem name="КОБП" desc="картотека операций будущих периодов" />
|
||||
<HelpListItem name="ДПНП" desc="декларация по налогу на прибыль" />
|
||||
<HelpListItem name="РО" desc="регламентированный отчет" />
|
||||
</List>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Диалог дополнительной информации
|
||||
HelpDialog.propTypes = {
|
||||
handleOpenHelpChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
//-----------
|
||||
//Тело модуля
|
||||
//-----------
|
||||
|
||||
//Раздел настройки
|
||||
const SectionTab = ({
|
||||
section,
|
||||
tabValue,
|
||||
index,
|
||||
containerProps,
|
||||
handleMarkAdd,
|
||||
handleReload,
|
||||
handleMarkOpen,
|
||||
handleMarkCnOpen,
|
||||
handleMarkCnInsert,
|
||||
menuItems
|
||||
}) => {
|
||||
//Состояние - диалог информации
|
||||
const [openHelp, setOpenHelp] = useState(false);
|
||||
|
||||
//Изменение состояния диалога информации
|
||||
const handleOpenHelpChange = () => {
|
||||
setOpenHelp(!openHelp);
|
||||
};
|
||||
|
||||
//Генерация содержимого
|
||||
return (
|
||||
<>
|
||||
<SectionTabPanel key={section.rn} value={tabValue} index={index}>
|
||||
<Box sx={STYLES.SECTION_ACTIONS}>
|
||||
<Box>
|
||||
<IconButton onClick={() => handleMarkAdd(section.rn)}>
|
||||
<Icon>add</Icon>
|
||||
</IconButton>
|
||||
<IconButton onClick={() => handleReload()}>
|
||||
<Icon>refresh</Icon>
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box>
|
||||
<IconButton onClick={() => handleOpenHelpChange()}>
|
||||
<Icon>help</Icon>
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{section.dataLoaded && section.columnsDef.length > 3 ? (
|
||||
<Box sx={STYLES.TABLE_CONTAINER}>
|
||||
<P8PDataGrid
|
||||
{...P8P_DATA_GRID_CONFIG_PROPS}
|
||||
containerComponentProps={{
|
||||
elevation: 6,
|
||||
sx: { ...STYLES.TABLE_SCROLL },
|
||||
style: STYLES.GRID_SIZES(
|
||||
containerProps.height,
|
||||
containerProps.pxOuterMenuH,
|
||||
containerProps.pxPanelHeaderH,
|
||||
containerProps.pxTabsH
|
||||
)
|
||||
}}
|
||||
columnsDef={section.columnsDef}
|
||||
groups={section.groups}
|
||||
rows={section.rows}
|
||||
fixedHeader={section.fixedHeader}
|
||||
fixedColumns={section.fixedColumns}
|
||||
size={P8P_DATA_GRID_SIZE.LARGE}
|
||||
reloading={section.reload}
|
||||
dataCellRender={prms =>
|
||||
confSctnMrkCellRender({
|
||||
...prms,
|
||||
sectionRn: section.rn,
|
||||
handleMarkAdd: handleMarkAdd,
|
||||
handleMarkOpen: handleMarkOpen,
|
||||
handleMarkCnOpen: handleMarkCnOpen,
|
||||
handleMarkCnInsert: handleMarkCnInsert,
|
||||
menuItems: menuItems
|
||||
})
|
||||
}
|
||||
headCellRender={confSctnMrkHeadCellRender}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
</SectionTabPanel>
|
||||
{openHelp ? <HelpDialog handleOpenHelpChange={handleOpenHelpChange} /> : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
//Контроль свойств - Раздел настройки
|
||||
SectionTab.propTypes = {
|
||||
section: PropTypes.object.isRequired,
|
||||
tabValue: PropTypes.number,
|
||||
index: PropTypes.number,
|
||||
containerProps: PropTypes.object,
|
||||
handleMarkAdd: PropTypes.func,
|
||||
handleReload: PropTypes.func,
|
||||
handleMarkOpen: PropTypes.func,
|
||||
handleMarkCnOpen: PropTypes.func,
|
||||
handleMarkCnInsert: PropTypes.func,
|
||||
menuItems: PropTypes.array
|
||||
};
|
||||
|
||||
//----------------
|
||||
//Интерфейс модуля
|
||||
//----------------
|
||||
|
||||
export { SectionTab };
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user