Compare commits

..

50 Commits

Author SHA1 Message Date
426864a4b7 Merge pull request 'main' (#7) from CITKParus/P8-Panels:main into main
Reviewed-on: #7
2025-02-20 17:14:46 +03:00
Mikhail Chechnev
bdc032fa67 WEBAPP: Свежая сборка 2025-02-14 14:11:07 +03:00
Mikhail Chechnev
6d10f6258b WEBAPP: P8PDataGrid - необязательность для columnsDef, rows, значение по умолчанию для groups (так можно упрощать state в верхнеуровневых компонентах) 2025-02-14 14:09:36 +03:00
Mikhail Chechnev
852abd5482 WEBAPP: P8PChart - обновление onClick-функции, значения по умолчанию для options, labels, datasets, необязательность labels 2025-02-14 13:48:36 +03:00
Mikhail Chechnev
27bd43afb5 WEBAPP: подчистка лишних комментов 2025-02-14 13:30:35 +03:00
Mikhail Chechnev
83b0c75a3b БД: Серверный API - процедура подготовки поисковой строки от клиента для использования в SQL-запросе 2025-02-14 13:28:40 +03:00
Mikhail Chechnev
ba8b7a5ce0 БД: Панель "Работы проектов" - каскадная очистка буфера при удалении проекта 2025-02-14 13:27:23 +03:00
Mikhail Chechnev
cca42f0b38 WEBAPP: Свежая сборка 2025-02-07 14:02:05 +03:00
Mikhail Chechnev
9469b01295 Актуализация документации: P8PDataGrid (обязательность свойств и значения по умолчанию) 2025-02-07 14:01:23 +03:00
Mikhail Chechnev
40242dedd4 WEBAPP: P8PDataGrid - инициализация списка открытых групп при монтировании, необязательность "reloading" 2025-02-07 14:00:03 +03:00
Mikhail Chechnev
aca423f16e WEBAPP: Выдача карточки панели в главном меню с превью по умолчанию, в случае отсутствия такового в настройках 2025-02-07 13:51:26 +03:00
Mikhail Chechnev
ccad22f6a7 WEBAPP: Свежая сборка 2025-01-17 17:18:18 +03:00
Mikhail Chechnev
252ef9e263 Панель "Экономика проектов" - поддержан переход в разделы "Проекты" и "Договоры" в режиме "закладка" 2025-01-17 17:18:00 +03:00
Mikhail Chechnev
7683f1ad76 WEBAPP: Поддержка открытия раздела/документа на закладке 2025-01-17 14:42:06 +03:00
Mikhail Chechnev
5261253d36 WEBAPP: Свежая сборка 2025-01-16 16:04:19 +03:00
Mikhail Chechnev
b2cc839cba WEBAPP: Панель "Экономика проектов" - фиксированные заголовки таблиц и общесистемные стили скролов 2025-01-16 16:04:00 +03:00
Mikhail Chechnev
b06ae0d242 WEBAPP: Общесистемные стили скролов в примере "Таблица данных" 2025-01-16 15:44:41 +03:00
Mikhail Chechnev
dac9bc4ea2 WEBAPP: Общесистемные стили полос прокрутки в "выдвижных панелях" для панелей ПУПД 2025-01-16 15:41:12 +03:00
Mikhail Chechnev
efed4b8ea2 WEBAPP: стили скролов для теля панели и главного бокового меню панелей 2025-01-16 15:38:10 +03:00
Mikhail Chechnev
a8a76766f4 WEB APP: P8PFullScreenDialog - возможность передачи свойств контейнеру содержимого диалога (contentProps) 2025-01-16 15:36:45 +03:00
Mikhail Chechnev
6fa9d97da4 Исправление неверной ссылки на тип TDG_FILTERS в инструкции 2025-01-15 19:20:33 +03:00
Mim
014d3f2e9e ЦИТК-800 - Программный интерфейс P8PDataGrid и P8PGantt приведены к общесистемному 2025-01-15 16:38:16 +03:00
Dollerino
b617a27c48 ЦИТК-800 - Изменение программного интерфейса P8PDataGrid. P8PDataGrid и P8PGantt приведены к единому формату данных с сервера 2024-12-03 15:26:51 +03:00
686d8ee5aa Merge pull request 'main' (#6) from CITKParus/P8-Panels:main into main
Reviewed-on: Dollerok/P8-Panels#6
2024-12-03 14:33:27 +03:00
Mikhail Chechnev
252dd4174c WEB APP: Свежая сборка 2024-12-02 16:24:27 +03:00
Mim
e5215d29b5 ЦИТК-840 - Панель "Выполнение работ" - корректировка параметра метода вызова "Графики ТО и Ремонтов" 2024-12-02 16:22:23 +03:00
Mikhail Chechnev
0d9505834c WEB APP: Свежая сборка 2024-11-29 20:07:11 +03:00
Mim
3fa1c4b048 ЦИТК-930 - Панель "Мониторинг изделий", доработка по замечаниям Ловыгина 2024-11-29 20:04:20 +03:00
3b763ec0c4 Merge branch 'EqsPrfrm' of https://git.citpb.ru/davay-popozhe/P8-Panels into EqsPrfrm 2024-11-29 20:00:05 +03:00
69c3b101a6 ЦИТК-840 2024-11-29 19:57:18 +03:00
Dollerino
a45479bdca Merge branch 'main' of https://git.citpb.ru/Dollerok/P8-Panels 2024-11-28 18:19:10 +03:00
Dollerino
c84b96f21e ЦИТК-930 - Доработана панель "Мониторинг сборки изделий" 2024-11-28 18:18:44 +03:00
af9f9815c5 Merge pull request 'main' (#5) from CITKParus/P8-Panels:main into main
Reviewed-on: Dollerok/P8-Panels#5
2024-11-28 18:09:16 +03:00
Mikhail Chechnev
4b52cea683 WEB APP: Свежая сборка 2024-11-27 18:46:57 +03:00
Mim
147114a4ae ЦИТК-930 - Панель "Мониторинг сборки изделий" - реализована в "общем виде", удалены "авиационные термины" 2024-11-27 18:40:34 +03:00
Dollerino
49309e5c0e ЦИТК-930 - Доработка панели "Мониторинг сборки изделий" 2024-11-25 16:06:24 +03:00
2d1a07d071 Merge pull request 'main' (#4) from CITKParus/P8-Panels:main into main
Reviewed-on: Dollerok/P8-Panels#4
2024-11-25 15:38:25 +03:00
Mim
68a3e8b2be ЦИТК-928 - панель "Производственная программа" - исправление ошибки расчёта прогресса задачи (>100) 2024-11-21 15:18:00 +03:00
078550ad06 ЦИТК-928 2024-11-21 15:04:16 +03:00
1d6007c4b7 ЦИТК-928 2024-11-21 14:52:36 +03:00
Mikhail Chechnev
607e5d9ddc WEB APP: Свежая сборка 2024-11-20 16:05:44 +03:00
Mikhail Chechnev
2bfce51fed ЦИТК-904 - Code-review "P8PCyclogram" 2024-11-20 16:05:26 +03:00
Mim
db66f0c96b ЦИТК-904 - Компонент "P8PCyclogram" 2024-11-20 14:03:46 +03:00
Mikhail Chechnev
a8c9bf2fec WEB APP: Свежая сборка 2024-11-18 19:32:04 +03:00
Mikhail Chechnev
47532b9c19 WEB APP: Выравнивание кнопок на рабочем столе, масштаб "Год" для P8PGantt, функция форматирования даты со временем в формате РФ 2024-11-18 19:31:46 +03:00
Mikhail Chechnev
9f0c2aa946 Актуализация документации: особенности установки на разные релизы Парус-Онлайн 2024-11-08 19:48:28 +03:00
Dollerino
cf76c80519 ЦИТК-904 - Добавлен компонент "Циклограмма" 2024-10-23 16:30:05 +03:00
86b1dcd717 Merge pull request 'main' (#3) from CITKParus/P8-Panels:main into main
Reviewed-on: Dollerok/P8-Panels#3
2024-10-21 15:25:36 +03:00
3a02384346 Merge pull request 'main' (#6) from CITKParus/P8-Panels:main into EqsPrfrm
Reviewed-on: #6
2024-10-16 16:24:51 +03:00
0c8222ea7e Merge pull request 'main' (#2) from main into EqsPrfrm
Reviewed-on: #2
2024-09-20 16:11:31 +03:00
82 changed files with 7242 additions and 8140 deletions

730
README.md
View File

@ -68,13 +68,13 @@
> **Внимание:** > **Внимание:**
> >
> - **Проверьте версию "ПАРУС 8 Онлайн"**
>
> Перед копированием расширения из репозитория убедитесь, что в [релизах расширения](https://git.citpb.ru/CITKParus/P8-Panels-ParusOnlineExt/releases) нет специальной сборки для Вашей версии "ПАРУС 8 Онлайн". **Если таковая есть - необходимо устанавливать именно её, а не актуальную сборку из основной ветки репозитория!**
>
> - **Для релиза "ПАРУС 8 Онлайн" от 30.08.2024** > - **Для релиза "ПАРУС 8 Онлайн" от 30.08.2024**
> >
> Требуется [патч до промежуточной сборки 02.09.2024 или старше](https://cloud.mail.ru/public/nEZb/y4oQa1N6D). Установка расширения на данный релиз не рекомендуется, по возможности - пропустите его. > Требуется [патч для "ПАРУС 8 Онлайн" до промежуточной сборки 02.09.2024](https://cloud.mail.ru/public/nEZb/y4oQa1N6D). Установка расширения на данный релиз не рекомендуется, по возможности - пропустите его.
>
> - **Для релизов "ПАРУС 8 Онлайн" до 30.08.2024**
>
> Содержимое папки "bin" следует брать из специальной сборки расширения - [Для сборок Парус-Онлайн до 30.08.2024](https://git.citpb.ru/CITKParus/P8-Panels-ParusOnlineExt/releases/tag/FOR_PARUS_ONLINE_BEFORE_20240830)
3. Подключите библиотеку расширения к серверу приложений "ПАРУС 8 Онлайн". Для этого добавьте ссылку на библиотеку в файл "Config\extensions.config" сервера приложений: 3. Подключите библиотеку расширения к серверу приложений "ПАРУС 8 Онлайн". Для этого добавьте ссылку на библиотеку в файл "Config\extensions.config" сервера приложений:
@ -647,8 +647,8 @@ const MyPanel = () => {
В состав API входят: В состав API входят:
- `pOnlineShowTab` - функция, отображение типовой закладки "ПАРУС 8 Онлайн" - `pOnlineShowTab` - функция, отображение типовой закладки "ПАРУС 8 Онлайн"
- `pOnlineShowUnit` - функция, отображение раздела "ПАРУС 8 Онлайн" в модальном режиме - `pOnlineShowUnit` - функция, отображение раздела "ПАРУС 8 Онлайн" в модальном режиме или на закладке
- `pOnlineShowDocument` - функция, отображение раздела "ПАРУС 8 Онлайн" в модальном режиме с позиционированием/отбором по документу - `pOnlineShowDocument` - функция, отображение раздела "ПАРУС 8 Онлайн" в модальном режиме (или на закладке) с позиционированием/отбором по документу
- `pOnlineShowDictionary` - функция, отображение раздела "ПАРУС 8 Онлайн" в режиме словаря - `pOnlineShowDictionary` - функция, отображение раздела "ПАРУС 8 Онлайн" в режиме словаря
- `pOnlineUserProcedure` - функция, исполнение "Пользовательской процедуры" - `pOnlineUserProcedure` - функция, исполнение "Пользовательской процедуры"
- `pOnlineUserReport` - функция, печать "Пользовательского отчёта" - `pOnlineUserReport` - функция, печать "Пользовательского отчёта"
@ -684,13 +684,15 @@ const MyPanel = () => {
{ {
unitCode, unitCode,
showMethod = "main", showMethod = "main",
inputParameters inputParameters,
modal = true
} }
``` ```
`unitCode` - обязательный, строка, код раздела Системы\ `unitCode` - обязательный, строка, код раздела Системы\
`showMethod` - необязательный, строка, метод вызова раздела Системы (если не указан - будет использован метод вызова "main")\ `showMethod` - необязательный, строка, метод вызова раздела Системы (если не указан - будет использован метод вызова "main")\
`inputParameters` - необязательный, массив объектов вида `[{name: ИМЯ_ПАРАМЕТРА, value: ЗНАЧЕНИЕАРАМЕТРА},...]`, параметры метода вызова раздела Системы `inputParameters` - необязательный, массив объектов вида `[{name: ИМЯ_ПАРАМЕТРА, value: ЗНАЧЕНИЕАРАМЕТРА},...]`, параметры метода вызова раздела Системы\
`modal` - необязательный, логический, true (по умолчанию) - открыть в модальном окне, false - открыть в закладке (если закладка ранее была открыта с таким же набором параметров - она будет активирована), поддерживается для "ПАРУС 8 Онлайн" с версии 26.12.2024
**Результат:** функция не возвращает значимого результата **Результат:** функция не возвращает значимого результата
@ -705,14 +707,16 @@ const MyPanel = () => {
unitCode, unitCode,
document, document,
showMethod = "main", showMethod = "main",
inRnParameter = "in_RN" inRnParameter = "in_RN",
modal = true
} }
``` ```
`unitCode` - обязательный, строка, код раздела Системы\ `unitCode` - обязательный, строка, код раздела Системы\
`document` - обязательный, число, регистрационный номер документа или иной его идентификатор `document` - обязательный, число, регистрационный номер документа или иной его идентификатор
`showMethod` - необязательный, строка, метод вызова раздела Системы (если не указан - будет использован метод вызова "main")\ `showMethod` - необязательный, строка, метод вызова раздела Системы (если не указан - будет использован метод вызова "main")\
`inRnParameter` - необязательный, строка, имя параметра метода вызова для позиционирования/отбора (если не указан, будет применён параметр метода вызова с именем "in_RN") `inRnParameter` - необязательный, строка, имя параметра метода вызова для позиционирования/отбора (если не указан, будет применён параметр метода вызова с именем "in_RN")\
`modal` - необязательный, логический, true (по умолчанию) - открыть в модальном окне, false - открыть в закладке (если закладка ранее была открыта с таким же набором параметров - она будет активирована), поддерживается для "ПАРУС 8 Онлайн" с версии 26.12.2024
**Результат:** функция не возвращает значимого результата **Результат:** функция не возвращает значимого результата
@ -1287,15 +1291,15 @@ const MyPanel = () => {
**Свойства** **Свойства**
`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: <ШИРИНА_КОЛОНКИ>}`\ `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: <ОКОНЧАНИЕ_ДИАПАЗОНА_ЗНАЧЕНИЙ_ФИЛЬТРА>}`\ `filtersInitial` - необязательныей, массив, начальное состояние фильтров таблицы, содержит объекты вида `{name: <НАИМЕНОВАНИЕ_КОЛОНКИ>, from: <НАЧАЛО_ДИАПАЗОНА_ЗНАЧЕНИЙ_ФИЛЬТРА>, to: <ОКОНЧАНИЕ_ДИАПАЗОНА_ЗНАЧЕНИЙ_ФИЛЬТРА>}`\
`groups` - необязательный, массив групп данных, содержит объекты вида `{name: <ИМЯ_ГРУППЫ>, caption: <ЗАГОЛОВОКРУППЫ>, expandable: <ПРИЗНАК_РАЗВОРАЧИВАЕМОСТИ_ГРУППЫ - true|false>, expanded: <ПРИЗНАК_РАЗВЕРНУТОСТИ_ГРУППЫ - true|false>}`\ `groups` - необязательный, массив групп данных, содержит объекты вида `{name: <ИМЯ_ГРУППЫ>, caption: <ЗАГОЛОВОКРУППЫ>, expandable: <ПРИЗНАК_РАЗВОРАЧИВАЕМОСТИ_ГРУППЫ - true|false>, expanded: <ПРИЗНАК_РАЗВЕРНУТОСТИ_ГРУППЫ - true|false>}`\
`rows` - обязательный, массив, отображаемые таблицой строки данных, содержит объекты вида `{groupName: <ИМЯ_ГРУППЫ_СОДЕРЖАЩЕЙ_СТРОКУ>, <ИМЯ_КОЛОНКИ>: <ЗНАЧЕНИЕ>}`\ `rows` - необязательный, массив, отображаемые таблицой строки данных, содержит объекты вида `{groupName: <ИМЯ_ГРУППЫ_СОДЕРЖАЩЕЙ_СТРОКУ>, <ИМЯ_КОЛОНКИ>: <ЗНАЧЕНИЕ>}`\
`size` - необязательный, строка, размер отступов при вёрстке таблицы, `small|medium` (см. константу `P8P_DATA_GRID_SIZE` в исходном коде компонента)\ `size` - необязательный, строка, размер отступов при вёрстке таблицы, `small|medium` (см. константу `P8P_DATA_GRID_SIZE` в исходном коде компонента)\
`fixedHeader` - необязательный, логический, признак фиксации заголовка таблицы\ `fixedHeader` - необязательный, логический, признак фиксации заголовка таблицы, по умолчанию - `false`\
`fixedColumns` - необязательный, число, количество фиксированных колонок слева `fixedColumns` - необязательный, число, количество фиксированных колонок слева, по умолчанию - 0\
`morePages` - обязательный, логический, признак отображения кнопки догрузки данных\ `morePages` - необязательный, логический, признак отображения кнопки догрузки данных, по умолчанию - `false`\
`reloading` - обязательный, логический, признак выполнения обновления данных таблицы (служит для корректной выдачи сообщения об отсуствии данных и корректного отображения "разворачивающихся" строк)\ `reloading` - необязательный, логический, признак выполнения обновления данных таблицы (служит для корректной выдачи сообщения об отсуствии данных и корректного отображения "разворачивающихся" строк), по умолчанию - `false`\
`expandable` - необязательный, логический, признак необходимости формирования "разворачивающихся" строк, по умолчанию - `false`\ `expandable` - необязательный, логический, признак необходимости формирования "разворачивающихся" строк, по умолчанию - `false`\
`orderAscMenuItemCaption` - обязательный, строка, текст для пункта меню сортировки колонки по возрастанию\ `orderAscMenuItemCaption` - обязательный, строка, текст для пункта меню сортировки колонки по возрастанию\
`orderDescMenuItemCaption` - обязательный, строка, текст для пункта меню сортировки колонки по убыванию\ `orderDescMenuItemCaption` - обязательный, строка, текст для пункта меню сортировки колонки по убыванию\
@ -1341,18 +1345,18 @@ const MyPanel = () => {
Такие свойства как `columnsDef`, `groups`, `rows` компонента `P8PDataGrid` требуют от разработчика передачи данных в определённом формате. Это не обязательно должна быть информация из БД Системы, можно, например, просто объявить переменные в коде панели, задать им соответствующие значения и передать в компонент. Но изначально, таблица данных задумывалась для отображения сведений, полученных их учётных регистров Системы. Такие сведения, как правило, собираются хранимым объектом БД, исполняемым из панели посредством вызова `executeStored`. С целью снижения трудозатрат на приведение собранных хранимым объектом данных к форматам, потребляемым `P8PDataGrid`, реализован специальный API на стороне сервера БД. Такие свойства как `columnsDef`, `groups`, `rows` компонента `P8PDataGrid` требуют от разработчика передачи данных в определённом формате. Это не обязательно должна быть информация из БД Системы, можно, например, просто объявить переменные в коде панели, задать им соответствующие значения и передать в компонент. Но изначально, таблица данных задумывалась для отображения сведений, полученных их учётных регистров Системы. Такие сведения, как правило, собираются хранимым объектом БД, исполняемым из панели посредством вызова `executeStored`. С целью снижения трудозатрат на приведение собранных хранимым объектом данных к форматам, потребляемым `P8PDataGrid`, реализован специальный API на стороне сервера БД.
Для таблицы данных это (см. детальные описания программных интерфейсов в пакете `PKG_P8PANELS_VISUAL`): Для таблицы данных это (см. детальные описания программных интерфейсов в пакете `PKG_P8PANELS_VISUAL`):
`PKG_P8PANELS_VISUAL.TDATA_GRID_MAKE` - функция, инициализация таблицы данных, возвращает объект для хранения описания таблицы\ `PKG_P8PANELS_VISUAL.TDG_MAKE` - функция, инициализация таблицы данных, возвращает объект для хранения описания таблицы\
`PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF` - процедура, добавление описания колонки в таблицу, принимает на вход объект с описанием таблицы и параметры, описывающие добавляемую колонку (её имя, заголовок, тип данных, видимость, доступность отбора и сортировки, набор предопределённых значений и т.д.)\ `PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF` - процедура, добавление описания колонки в таблицу, принимает на вход объект с описанием таблицы и параметры, описывающие добавляемую колонку (её имя, заголовок, тип данных, видимость, доступность отбора и сортировки, набор предопределённых значений и т.д.)\
`PKG_P8PANELS_VISUAL.TCOL_VALS_ADD` - процедура, служит для формирования коллекции предопределённых значений колонки таблицы (подготовленная коллекция передаётся в `RCOL_VALS` вызова `TDATA_GRID_ADD_COL_DEF`, если необходимо)\ `PKG_P8PANELS_VISUAL.TDG_COL_VALS_ADD` - процедура, служит для формирования коллекции предопределённых значений колонки таблицы (подготовленная коллекция передаётся в `RCOL_VALS` вызова `TDG_ADD_COL_DEF`, если необходимо)\
`PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_GROUP` - процедура, служит для добавления описания группы в таблицу данных, на вход принимает объект для хранения описания таблицы и параметры добавляемой группы\ `PKG_P8PANELS_VISUAL.TDG_ADD_GROUP` - процедура, служит для добавления описания группы в таблицу данных, на вход принимает объект для хранения описания таблицы и параметры добавляемой группы\
`PKG_P8PANELS_VISUAL.TROW_ADD_COL` - процедура, добавляет значение колонки к строке таблицы (значение указывается явно в `[S|N|D]VALUE`)\ `PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL` - процедура, добавляет значение колонки к строке таблицы (значение указывается явно в `[S|N|D]VALUE`)\
`PKG_P8PANELS_VISUAL.TROW_ADD_CUR_COL[S|N|D]` - процедура, добавляет значение колонки к строке таблицы (значение указывается через ссылку на номер колонки `NPOSITION` в курсоре `ICURSOR` динамического SQL)\ `PKG_P8PANELS_VISUAL.TDG_ROW_ADD_CUR_COL[S|N|D]` - процедура, добавляет значение колонки к строке таблицы (значение указывается через ссылку на номер колонки `NPOSITION` в курсоре `ICURSOR` динамического SQL)\
`PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_ROW` - процедура, добавляет сформированную строку со значениями колонок в таблицу данных, на вход принимает объект для хранения описания таблицы и описание строки, сформированное вызовами `TROW_ADD_COL` и `TROW_ADD_CUR_COL[S|N|D]`, а так же год группы, в которую должна быть включена строка\ `PKG_P8PANELS_VISUAL.TDG_ADD_ROW` - процедура, добавляет сформированную строку со значениями колонок в таблицу данных, на вход принимает объект для хранения описания таблицы и описание строки, сформированное вызовами `TDG_ROW_ADD_COL` и `TDG_ROW_ADD_CUR_COL[S|N|D]`, а так же год группы, в которую должна быть включена строка\
`PKG_P8PANELS_VISUAL.TDATA_GRID_TO_XML` - функция, производит сериализацию объекта, описывающего таблицу данных, в специальный XML-формат, корректно интерпретируемый клиентским компонентом `P8PDataGrid` при передаче в WEB-приложение\ `PKG_P8PANELS_VISUAL.TDG_TO_XML` - функция, производит сериализацию объекта, описывающего таблицу данных, в специальный XML-формат, корректно интерпретируемый клиентским компонентом `P8PDataGrid` при передаче в WEB-приложение\
`PKG_P8PANELS_VISUAL.TORDERS_FROM_XML` - функция, служит для десериализации (как правило, полученного от клиентского приложения) состояния сортировок в коллекцию формата `TORDERS`, на вход принимает `CLOB` с сериализованным состоянием сортировок таблицы в виде `BASE64(<orders><name>ИМЯ</name><direction>ASC|DESC</direction></orders>...)` (клиентское приложение должно обеспечить передачу состояния сортировок в этом формате, см. пример ниже)\ `PKG_P8PANELS_VISUAL.TDG_ORDERS_FROM_XML` - функция, служит для десериализации (как правило, полученного от клиентского приложения) состояния сортировок в коллекцию формата `TORDERS`, на вход принимает `CLOB` с сериализованным состоянием сортировок таблицы в виде `BASE64(<orders><name>ИМЯ</name><direction>ASC|DESC</direction></orders>...)` (клиентское приложение должно обеспечить передачу состояния сортировок в этом формате, см. пример ниже)\
`PKG_P8PANELS_VISUAL.TORDERS_SET_QUERY` - процедура, вспомогательная утилита, производит в тексте SQL-запроса, поданного на вход, замену указанного шаблона на конструкцию `order by`, сформированную с учётом переданной коллекции `RORDERS`\ `PKG_P8PANELS_VISUAL.TDG_ORDERS_SET_QUERY` - процедура, вспомогательная утилита, производит в тексте SQL-запроса, поданного на вход, замену указанного шаблона на конструкцию `order by`, сформированную с учётом переданной коллекции `RORDERS`\
`PKG_P8PANELS_VISUAL.TFILTERS_FROM_XML` - функция, служит для десериализации (как правило, полученного от клиентского приложения) состояния фильтров в коллекцию формата `TFILTERS`, на вход принимает `CLOB` с сериализованным состоянием фильтров таблицы в виде `BASE64(<filters><name>ИМЯ</name><from>ЗНАЧЕНИЕ</from><to>ЗНАЧЕНИЕ</to></filters>...)` (клиентское приложение должно обеспечить передачу состояния фильтров в этом формате, см. пример ниже)\ `PKG_P8PANELS_VISUAL.TDG_FILTERS_FROM_XML` - функция, служит для десериализации (как правило, полученного от клиентского приложения) состояния фильтров в коллекцию формата `TDG_FILTERS`, на вход принимает `CLOB` с сериализованным состоянием фильтров таблицы в виде `BASE64(<filters><name>ИМЯ</name><from>ЗНАЧЕНИЕ</from><to>ЗНАЧЕНИЕ</to></filters>...)` (клиентское приложение должно обеспечить передачу состояния фильтров в этом формате, см. пример ниже)\
`PKG_P8PANELS_VISUAL.TFILTERS_SET_QUERY` - процедура, вспомогательная утилита, производит вызов указанной серверной процедуры отбора с учётом переданных переменных окружения и значений в `RFILTERS`\ `PKG_P8PANELS_VISUAL.TDG_FILTERS_SET_QUERY` - процедура, вспомогательная утилита, производит вызов указанной серверной процедуры отбора с учётом переданных переменных окружения и значений в `RFILTERS`\
`PKG_P8PANELS_VISUAL.UTL_ROWS_LIMITS_CALC` - процедура, вспомогательная утилита, служит для конвертации номера страницы данных и размера страницы данных в границы диапазона строк выборки (как правило, клиентскому приложению удобнее прислать на сервер текущий номер страницы и её размер, в то время к в запросах, для выборки, удобнее применять границы диапазонов строк) `PKG_P8PANELS_VISUAL.UTL_ROWS_LIMITS_CALC` - процедура, вспомогательная утилита, служит для конвертации номера страницы данных и размера страницы данных в границы диапазона строк выборки (как правило, клиентскому приложению удобнее прислать на сервер текущий номер страницы и её размер, в то время к в запросах, для выборки, удобнее применять границы диапазонов строк)
**Пример** **Пример**
@ -1372,11 +1376,11 @@ const MyPanel = () => {
is is
NCOMPANY PKG_STD.TREF := GET_SESSION_COMPANY(); -- Организация сеанса NCOMPANY PKG_STD.TREF := GET_SESSION_COMPANY(); -- Организация сеанса
NIDENT PKG_STD.TREF := GEN_IDENT(); -- Идентификатор отбора NIDENT PKG_STD.TREF := GEN_IDENT(); -- Идентификатор отбора
RF PKG_P8PANELS_VISUAL.TFILTERS; -- Фильтры RF PKG_P8PANELS_VISUAL.TDG_FILTERS; -- Фильтры
RO PKG_P8PANELS_VISUAL.TORDERS; -- Сортировки RO PKG_P8PANELS_VISUAL.TDG_ORDERS; -- Сортировки
RDG PKG_P8PANELS_VISUAL.TDATA_GRID; -- Описание таблицы RDG PKG_P8PANELS_VISUAL.TDG; -- Описание таблицы
RAGN_TYPES PKG_P8PANELS_VISUAL.TCOL_VALS; -- Предопределенные значения "Типа контрагентов" RAGN_TYPES PKG_P8PANELS_VISUAL.TDG_COL_VALS; -- Предопределенные значения "Типа контрагентов"
RDG_ROW PKG_P8PANELS_VISUAL.TROW; -- Строка таблицы RDG_ROW PKG_P8PANELS_VISUAL.TDG_ROW; -- Строка таблицы
NROW_FROM PKG_STD.TREF; -- Номер строки с NROW_FROM PKG_STD.TREF; -- Номер строки с
NROW_TO PKG_STD.TREF; -- Номер строки по NROW_TO PKG_STD.TREF; -- Номер строки по
CSQL clob; -- Буфер для запроса CSQL clob; -- Буфер для запроса
@ -1387,18 +1391,18 @@ const MyPanel = () => {
NAGNTYPE PKG_STD.TREF; -- Буфер для "Типа" NAGNTYPE PKG_STD.TREF; -- Буфер для "Типа"
begin begin
/* Читаем фильтры */ /* Читаем фильтры */
RF := PKG_P8PANELS_VISUAL.TFILTERS_FROM_XML(CFILTERS => CFILTERS); RF := PKG_P8PANELS_VISUAL.TDG_FILTERS_FROM_XML(CFILTERS => CFILTERS);
/* Читем сортировки */ /* Читем сортировки */
RO := PKG_P8PANELS_VISUAL.TORDERS_FROM_XML(CORDERS => CORDERS); RO := PKG_P8PANELS_VISUAL.TDG_ORDERS_FROM_XML(CORDERS => CORDERS);
/* Преобразуем номер и размер страницы в номер строк с и по */ /* Преобразуем номер и размер страницы в номер строк с и по */
PKG_P8PANELS_VISUAL.UTL_ROWS_LIMITS_CALC(NPAGE_NUMBER => NPAGE_NUMBER, PKG_P8PANELS_VISUAL.UTL_ROWS_LIMITS_CALC(NPAGE_NUMBER => NPAGE_NUMBER,
NPAGE_SIZE => NPAGE_SIZE, NPAGE_SIZE => NPAGE_SIZE,
NROW_FROM => NROW_FROM, NROW_FROM => NROW_FROM,
NROW_TO => NROW_TO); NROW_TO => NROW_TO);
/* Инициализируем таблицу данных */ /* Инициализируем таблицу данных */
RDG := PKG_P8PANELS_VISUAL.TDATA_GRID_MAKE(BFIXED_HEADER => true, NFIXED_COLUMNS => 2); RDG := PKG_P8PANELS_VISUAL.TDG_MAKE(BFIXED_HEADER => true, NFIXED_COLUMNS => 2);
/* Описываем колонки таблицы данных */ /* Описываем колонки таблицы данных */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SAGNABBR', SNAME => 'SAGNABBR',
SCAPTION => 'Мнемокод', SCAPTION => 'Мнемокод',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
@ -1407,7 +1411,7 @@ const MyPanel = () => {
BORDER => true, BORDER => true,
BFILTER => true, BFILTER => true,
NWIDTH => 150); NWIDTH => 150);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SAGNINFO', SNAME => 'SAGNINFO',
SCAPTION => 'Сведения', SCAPTION => 'Сведения',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
@ -1416,7 +1420,7 @@ const MyPanel = () => {
BFILTER => false, BFILTER => false,
BEXPANDABLE => true, BEXPANDABLE => true,
NWIDTH => 300); NWIDTH => 300);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SAGNNAME', SNAME => 'SAGNNAME',
SCAPTION => 'Наименование', SCAPTION => 'Наименование',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
@ -1426,9 +1430,9 @@ const MyPanel = () => {
BFILTER => true, BFILTER => true,
SPARENT => 'SAGNINFO', SPARENT => 'SAGNINFO',
NWIDTH => 200); NWIDTH => 200);
PKG_P8PANELS_VISUAL.TCOL_VALS_ADD(RCOL_VALS => RAGN_TYPES, NVALUE => 0); PKG_P8PANELS_VISUAL.TDG_COL_VALS_ADD(RCOL_VALS => RAGN_TYPES, NVALUE => 0);
PKG_P8PANELS_VISUAL.TCOL_VALS_ADD(RCOL_VALS => RAGN_TYPES, NVALUE => 1); PKG_P8PANELS_VISUAL.TDG_COL_VALS_ADD(RCOL_VALS => RAGN_TYPES, NVALUE => 1);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'NAGNTYPE', SNAME => 'NAGNTYPE',
SCAPTION => 'Тип', SCAPTION => 'Тип',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_NUMB, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_NUMB,
@ -1444,11 +1448,11 @@ const MyPanel = () => {
'или оперативном управлении обособленное имущество, отвечает по своим обязательствам этим имуществом, может от своего ' || 'или оперативном управлении обособленное имущество, отвечает по своим обязательствам этим имуществом, может от своего ' ||
'имени приобретать и осуществлять имущественные и личные неимущественные права, отвечать по своим обязанностям.<br>' || 'имени приобретать и осуществлять имущественные и личные неимущественные права, отвечать по своим обязанностям.<br>' ||
'<b style="color:green">Физическое лицо</b> - субъект правовых отношений, представляющий собой одного человека.'); '<b style="color:green">Физическое лицо</b> - субъект правовых отношений, представляющий собой одного человека.');
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SFULLNAME', SNAME => 'SFULLNAME',
SCAPTION => 'Полное наименование', SCAPTION => 'Полное наименование',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR); SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SAGNIDNUMB', SNAME => 'SAGNIDNUMB',
SCAPTION => 'ИНН', SCAPTION => 'ИНН',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR); SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR);
@ -1477,9 +1481,12 @@ const MyPanel = () => {
PKG_SQL_BUILD.APPEND(SSQL => CSQL, SELEMENT1 => ' and AG.RN in (select ID from COND_BROKER_IDSMART where IDENT = :NIDENT) %ORDER_BY%) D) F'); PKG_SQL_BUILD.APPEND(SSQL => CSQL, SELEMENT1 => ' and AG.RN in (select ID from COND_BROKER_IDSMART where IDENT = :NIDENT) %ORDER_BY%) D) F');
PKG_SQL_BUILD.APPEND(SSQL => CSQL, SELEMENT1 => ' where F.NROW between :NROW_FROM and :NROW_TO'); PKG_SQL_BUILD.APPEND(SSQL => CSQL, SELEMENT1 => ' where F.NROW between :NROW_FROM and :NROW_TO');
/* Учтём сортировки */ /* Учтём сортировки */
PKG_P8PANELS_VISUAL.TORDERS_SET_QUERY(RDATA_GRID => RDG, RORDERS => RO, SPATTERN => '%ORDER_BY%', CSQL => CSQL); PKG_P8PANELS_VISUAL.TDG_ORDERS_SET_QUERY(RDATA_GRID => RDG,
RORDERS => RO,
SPATTERN => '%ORDER_BY%',
CSQL => CSQL);
/* Учтём фильтры */ /* Учтём фильтры */
PKG_P8PANELS_VISUAL.TFILTERS_SET_QUERY(NIDENT => NIDENT, PKG_P8PANELS_VISUAL.TDG_FILTERS_SET_QUERY(NIDENT => NIDENT,
NCOMPANY => NCOMPANY, NCOMPANY => NCOMPANY,
SUNIT => 'AGNLIST', SUNIT => 'AGNLIST',
SPROCEDURE => 'P_AGNLIST_BASE_COND', SPROCEDURE => 'P_AGNLIST_BASE_COND',
@ -1512,7 +1519,7 @@ const MyPanel = () => {
if (NAGNTYPE = 0) then if (NAGNTYPE = 0) then
SGROUP := 'JUR'; SGROUP := 'JUR';
SAGNINFO := SAGNNAME || ', ЮЛ'; SAGNINFO := SAGNNAME || ', ЮЛ';
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_GROUP(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_GROUP(RDATA_GRID => RDG,
SNAME => SGROUP, SNAME => SGROUP,
SCAPTION => 'Юридические лица', SCAPTION => 'Юридические лица',
BEXPANDABLE => true, BEXPANDABLE => true,
@ -1520,21 +1527,30 @@ const MyPanel = () => {
else else
SGROUP := 'PERS'; SGROUP := 'PERS';
SAGNINFO := SAGNNAME || ', ФЛ'; SAGNINFO := SAGNNAME || ', ФЛ';
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_GROUP(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_GROUP(RDATA_GRID => RDG,
SNAME => SGROUP, SNAME => SGROUP,
SCAPTION => 'Физические лица', SCAPTION => 'Физические лица',
BEXPANDABLE => true, BEXPANDABLE => true,
BEXPANDED => false); BEXPANDED => false);
end if; end if;
RDG_ROW := PKG_P8PANELS_VISUAL.TROW_MAKE(SGROUP => SGROUP); RDG_ROW := PKG_P8PANELS_VISUAL.TDG_ROW_MAKE(SGROUP => SGROUP);
PKG_P8PANELS_VISUAL.TROW_ADD_CUR_COLS(RROW => RDG_ROW, SNAME => 'SAGNABBR', ICURSOR => ICURSOR, NPOSITION => 1); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_CUR_COLS(RROW => RDG_ROW,
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SAGNINFO', SVALUE => SAGNINFO); SNAME => 'SAGNABBR',
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SAGNNAME', SVALUE => SAGNNAME); ICURSOR => ICURSOR,
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, SNAME => 'NAGNTYPE', NVALUE => NAGNTYPE); NPOSITION => 1);
PKG_P8PANELS_VISUAL.TROW_ADD_CUR_COLS(RROW => RDG_ROW, SNAME => 'SFULLNAME', ICURSOR => ICURSOR, NPOSITION => 4); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SAGNINFO', SVALUE => SAGNINFO);
PKG_P8PANELS_VISUAL.TROW_ADD_CUR_COLS(RROW => RDG_ROW, SNAME => 'SAGNIDNUMB', ICURSOR => ICURSOR, NPOSITION => 5); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SAGNNAME', SVALUE => SAGNNAME);
PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW, SNAME => 'NAGNTYPE', NVALUE => NAGNTYPE);
PKG_P8PANELS_VISUAL.TDG_ROW_ADD_CUR_COLS(RROW => RDG_ROW,
SNAME => 'SFULLNAME',
ICURSOR => ICURSOR,
NPOSITION => 4);
PKG_P8PANELS_VISUAL.TDG_ROW_ADD_CUR_COLS(RROW => RDG_ROW,
SNAME => 'SAGNIDNUMB',
ICURSOR => ICURSOR,
NPOSITION => 5);
/* Добавляем строку в таблицу */ /* Добавляем строку в таблицу */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW); PKG_P8PANELS_VISUAL.TDG_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW);
end loop; end loop;
/* Освобождаем курсор */ /* Освобождаем курсор */
PKG_SQL_DML.CLOSE_CURSOR(ICURSOR => ICURSOR); PKG_SQL_DML.CLOSE_CURSOR(ICURSOR => ICURSOR);
@ -1544,7 +1560,7 @@ const MyPanel = () => {
raise; raise;
end; end;
/* Сериализуем описание */ /* Сериализуем описание */
COUT := PKG_P8PANELS_VISUAL.TDATA_GRID_TO_XML(RDATA_GRID => RDG, NINCLUDE_DEF => NINCLUDE_DEF); COUT := PKG_P8PANELS_VISUAL.TDG_TO_XML(RDATA_GRID => RDG, NINCLUDE_DEF => NINCLUDE_DEF);
end DATA_GRID; end DATA_GRID;
``` ```
@ -1617,18 +1633,14 @@ export const groupCellRender = () => ({ cellStyle: { padding: "2px" } });
//Пример: Таблица данных "P8PDataGrid" //Пример: Таблица данных "P8PDataGrid"
const DataGrid = ({ title }) => { const DataGrid = ({ title }) => {
//Собственное состояние - таблица данных //Собственное состояние - таблица данных
const [dataGrid, setdataGrid] = useState({ const [dataGrid, setDataGrid] = useState({
dataLoaded: false, dataLoaded: false,
columnsDef: [],
filters: null, filters: null,
orders: null, orders: null,
groups: [],
rows: [],
reload: true,
pageNumber: 1, pageNumber: 1,
morePages: true, morePages: true,
fixedHeader: false, expandable: true,
fixedColumns: 0 reloading: true
}); });
//Подключение к контексту взаимодействия с сервером //Подключение к контексту взаимодействия с сервером
@ -1639,7 +1651,7 @@ const DataGrid = ({ title }) => {
//Загрузка данных таблицы с сервера //Загрузка данных таблицы с сервера
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
if (dataGrid.reload) { if (dataGrid.reloading) {
const data = await executeStored({ const data = await executeStored({
stored: "PKG_P8PANELS_SAMPLES.DATA_GRID", stored: "PKG_P8PANELS_SAMPLES.DATA_GRID",
args: { args: {
@ -1651,32 +1663,31 @@ const DataGrid = ({ title }) => {
}, },
respArg: "COUT" respArg: "COUT"
}); });
setdataGrid(pv => ({ setDataGrid(pv => ({
...pv, ...pv,
fixedHeader: data.XDATA_GRID.fixedHeader, ...data.XDATA_GRID,
fixedColumns: data.XDATA_GRID.fixedColumns, columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef || [],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef, rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...(pv.rows || []), ...(data.XDATA_GRID.rows || [])],
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])], groups: data.XDATA_GRID.groups
groups: data.XGROUPS
? pv.pageNumber == 1 ? pv.pageNumber == 1
? [...data.XGROUPS] ? [...data.XDATA_GRID.groups]
: [...pv.groups, ...data.XGROUPS.filter(g => !pv.groups.find(pg => pg.name == g.name))] : [...(pv.groups || []), ...data.XDATA_GRID.groups.filter(g => !pv.groups.find(pg => pg.name == g.name))]
: [...pv.groups], : [...(pv.groups || [])],
dataLoaded: true, dataLoaded: true,
reload: false, reloading: false,
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
})); }));
} }
}, [dataGrid.reload, dataGrid.filters, dataGrid.orders, dataGrid.dataLoaded, dataGrid.pageNumber, executeStored, SERV_DATA_TYPE_CLOB]); }, [dataGrid.reloading, dataGrid.filters, dataGrid.orders, dataGrid.dataLoaded, dataGrid.pageNumber, executeStored, SERV_DATA_TYPE_CLOB]);
//При изменении состояния фильтра //При изменении состояния фильтра
const handleFilterChanged = ({ filters }) => setdataGrid(pv => ({ ...pv, filters: [...filters], pageNumber: 1, reload: true })); const handleFilterChanged = ({ filters }) => setDataGrid(pv => ({ ...pv, filters: [...filters], pageNumber: 1, reloading: true }));
//При изменении состояния сортировки //При изменении состояния сортировки
const handleOrderChanged = ({ orders }) => setdataGrid(pv => ({ ...pv, orders: [...orders], pageNumber: 1, reload: true })); const handleOrderChanged = ({ orders }) => setDataGrid(pv => ({ ...pv, orders: [...orders], pageNumber: 1, reloading: true }));
//При изменении количества отображаемых страниц //При изменении количества отображаемых страниц
const handlePagesCountChanged = () => setdataGrid(pv => ({ ...pv, pageNumber: pv.pageNumber + 1, reload: true })); const handlePagesCountChanged = () => setDataGrid(pv => ({ ...pv, pageNumber: pv.pageNumber + 1, reloading: true }));
//При нажатии на копку контрагента //При нажатии на копку контрагента
const handleAgnButtonClicked = agnCode => pOnlineShowDocument({ unitCode: "AGNLIST", document: agnCode, inRnParameter: "in_AGNABBR" }); const handleAgnButtonClicked = agnCode => pOnlineShowDocument({ unitCode: "AGNLIST", document: agnCode, inRnParameter: "in_AGNABBR" });
@ -1684,7 +1695,7 @@ const DataGrid = ({ title }) => {
//При необходимости обновить данные таблицы //При необходимости обновить данные таблицы
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, [dataGrid.reload, loadData]); }, [dataGrid.reloading, loadData]);
//Генерация содержимого //Генерация содержимого
return ( return (
@ -1698,16 +1709,9 @@ const DataGrid = ({ title }) => {
{dataGrid.dataLoaded ? ( {dataGrid.dataLoaded ? (
<P8PDataGrid <P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS} {...P8P_DATA_GRID_CONFIG_PROPS}
containerComponentProps={{ elevation: 6, style: STYLES.DATA_GRID_CONTAINER }} {...dataGrid}
columnsDef={dataGrid.columnsDef}
groups={dataGrid.groups}
rows={dataGrid.rows}
size={P8P_DATA_GRID_SIZE.LARGE} size={P8P_DATA_GRID_SIZE.LARGE}
fixedHeader={dataGrid.fixedHeader} containerComponentProps={{ elevation: 6, style: STYLES.DATA_GRID_CONTAINER }}
fixedColumns={dataGrid.fixedColumns}
filtersInitial={dataGrid.filters}
morePages={dataGrid.morePages}
reloading={dataGrid.reload}
valueFormatter={valueFormatter} valueFormatter={valueFormatter}
headCellRender={headCellRender} headCellRender={headCellRender}
dataCellRender={dataCellRender} dataCellRender={dataCellRender}
@ -1715,7 +1719,6 @@ const DataGrid = ({ title }) => {
onOrderChanged={handleOrderChanged} onOrderChanged={handleOrderChanged}
onFilterChanged={handleFilterChanged} onFilterChanged={handleFilterChanged}
onPagesCountChanged={handlePagesCountChanged} onPagesCountChanged={handlePagesCountChanged}
expandable={true}
rowExpandRender={({ row }) => ( rowExpandRender={({ row }) => (
<Button onClick={() => handleAgnButtonClicked(row.SAGNABBR)}>Показать в разделе</Button> <Button onClick={() => handleAgnButtonClicked(row.SAGNABBR)}>Показать в разделе</Button>
)} )}
@ -1759,7 +1762,7 @@ const MyPanel = () => {
`title` - необязательный, строка, заголовок графика, если не указано - заголовок не отображается\ `title` - необязательный, строка, заголовок графика, если не указано - заголовок не отображается\
`legendPosition` - необязательный, строка, расположение легенды, может принимать значения `left|right|top|bottom`, если не указано - легенда не отображается\ `legendPosition` - необязательный, строка, расположение легенды, может принимать значения `left|right|top|bottom`, если не указано - легенда не отображается\
`options` - необязательный, объект, дополнительные параметры графика, формат и допустимый состав атрибутов определены в документации к библиотеке [ChartJS](https://www.chartjs.org/docs/latest/), будет объединён с параметрами графика уже зафиксированными в компоненте `P8PChart` (см. `useEffect` при подключении компонента к старице в его исходном коде, параметры графика, зафиксированные в компоненте, имеют более высокий приоритет по сравнению с данным свойством) `options` - необязательный, объект, дополнительные параметры графика, формат и допустимый состав атрибутов определены в документации к библиотеке [ChartJS](https://www.chartjs.org/docs/latest/), будет объединён с параметрами графика уже зафиксированными в компоненте `P8PChart` (см. `useEffect` при подключении компонента к старице в его исходном коде, параметры графика, зафиксированные в компоненте, имеют более высокий приоритет по сравнению с данным свойством)
`labels` - обязательный, массив строк, список меток для значений графика\ `labels` - необязательный, массив строк, список меток для значений графика\
`datasets` - необязательный, массив объектов, данные для отображения на диаграмме, каждый элемент массива - серия данных для отображения, содержит объекты вида `{label: <ЗАГОЛОВОК_СЕРИИ>, borderColor: <ЦВЕТРАНИЦЫ_СЕРИИ_НАРАФИКЕ>, backgroundColor: <ЦВЕТ_ЗАЛИВКИ_СЕРИИ_НАРАФИКЕ>, data: <МАССИВ_ЗНАЧЕНИЙ_СЕРИИ_ДАННЫХ>, items: <МАССИВ_ОБЪЕКТОВРОИЗВОЛЬНОЙ_СТРУКТУРЫ_ДЛЯ_ОПИСАНИЯ_СЕРИИ_ДАННЫХ>}`\ `datasets` - необязательный, массив объектов, данные для отображения на диаграмме, каждый элемент массива - серия данных для отображения, содержит объекты вида `{label: <ЗАГОЛОВОК_СЕРИИ>, borderColor: <ЦВЕТРАНИЦЫ_СЕРИИ_НАРАФИКЕ>, backgroundColor: <ЦВЕТ_ЗАЛИВКИ_СЕРИИ_НАРАФИКЕ>, data: <МАССИВ_ЗНАЧЕНИЙ_СЕРИИ_ДАННЫХ>, items: <МАССИВ_ОБЪЕКТОВРОИЗВОЛЬНОЙ_СТРУКТУРЫ_ДЛЯ_ОПИСАНИЯ_СЕРИИ_ДАННЫХ>}`\
`onClick` - необязательный, функция, будет вызвана при нажатии на элемент графика, сигнатура функции `f({datasetIndex, itemIndex, item})`, результат функции не интерпретируется. Функции будет передан объект, поле `datasetIndex` которого, будет содержать индекс серии данных, `itemIndex` - индекс элемента серии данных, а `item` - описание элмента данных серии, на котором было зафиксировано нажатие.\ `onClick` - необязательный, функция, будет вызвана при нажатии на элемент графика, сигнатура функции `f({datasetIndex, itemIndex, item})`, результат функции не интерпретируется. Функции будет передан объект, поле `datasetIndex` которого, будет содержать индекс серии данных, `itemIndex` - индекс элемента серии данных, а `item` - описание элмента данных серии, на котором было зафиксировано нажатие.\
`style` - необязательный, объект, стили, которые будут применены к контейнеру `div` графика `style` - необязательный, объект, стили, которые будут применены к контейнеру `div` графика
@ -1851,7 +1854,7 @@ const STYLES = {
//Пример: Графики "P8PChart" //Пример: Графики "P8PChart"
const Chart = ({ title }) => { const Chart = ({ title }) => {
//Собственное состояние - график //Собственное состояние - график
const [chart, setChart] = useState({ loaded: false, labels: [], datasets: [] }); const [chart, setChart] = useState({ loaded: false });
//Подключение к контексту взаимодействия с сервером //Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx); const { executeStored } = useContext(BackEndСtx);
@ -2126,12 +2129,10 @@ const taskDialogRenderer = ({ task, close }) => {
//Пример: Диаграмма Ганта "P8Gantt" //Пример: Диаграмма Ганта "P8Gantt"
const Gantt = ({ title }) => { const Gantt = ({ title }) => {
//Собственное состояние //Собственное состояние
const [state, setState] = useState({ const [gantt, setGantt] = useState({
init: false, init: false,
dataLoaded: false, dataLoaded: false,
ident: null, ident: null,
ganttDef: {},
ganttTasks: [],
useCustomTaskDialog: false useCustomTaskDialog: false
}); });
@ -2142,21 +2143,21 @@ const Gantt = ({ title }) => {
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
const data = await executeStored({ const data = await executeStored({
stored: "PKG_P8PANELS_SAMPLES.GANTT", stored: "PKG_P8PANELS_SAMPLES.GANTT",
args: { NIDENT: state.ident }, args: { NIDENT: gantt.ident },
attributeValueProcessor: (name, val) => attributeValueProcessor: (name, val) =>
name == "numb" ? undefined : ["start", "end"].includes(name) ? formatDateJSONDateOnly(val) : val, name == "numb" ? undefined : ["start", "end"].includes(name) ? formatDateJSONDateOnly(val) : val,
respArg: "COUT" respArg: "COUT"
}); });
setState(pv => ({ ...pv, dataLoaded: true, ganttDef: { ...data.XGANTT_DEF }, ganttTasks: [...data.XGANTT_TASKS] })); setGantt(pv => ({ ...pv, dataLoaded: true, ...data.XGANTT }));
}, [state.ident, executeStored]); }, [gantt.ident, executeStored]);
//Инициализация данных диаграммы //Инициализация данных диаграммы
const initData = useCallback(async () => { const initData = useCallback(async () => {
if (!state.init) { if (!gantt.init) {
const data = await executeStored({ stored: "PKG_P8PANELS_SAMPLES.GANTT_INIT", args: { NIDENT: state.ident } }); const data = await executeStored({ stored: "PKG_P8PANELS_SAMPLES.GANTT_INIT", args: { NIDENT: gantt.ident } });
setState(pv => ({ ...pv, init: true, ident: data.NIDENT })); setGantt(pv => ({ ...pv, init: true, ident: data.NIDENT }));
} }
}, [state.init, state.ident, executeStored]); }, [gantt.init, gantt.ident, executeStored]);
//Изменение данных диаграммы //Изменение данных диаграммы
const modifyData = useCallback( const modifyData = useCallback(
@ -2164,13 +2165,13 @@ const Gantt = ({ title }) => {
try { try {
await executeStored({ await executeStored({
stored: "PKG_P8PANELS_SAMPLES.GANTT_MODIFY", stored: "PKG_P8PANELS_SAMPLES.GANTT_MODIFY",
args: { NIDENT: state.ident, NRN: rn, DDATE_FROM: new Date(start), DDATE_TO: new Date(end) } args: { NIDENT: gantt.ident, NRN: rn, DDATE_FROM: new Date(start), DDATE_TO: new Date(end) }
}); });
} finally { } finally {
loadData(); loadData();
} }
}, },
[state.ident, executeStored, loadData] [gantt.ident, executeStored, loadData]
); );
//Обработка измненения сроков задачи в диаграмме Гантта //Обработка измненения сроков задачи в диаграмме Гантта
@ -2180,8 +2181,8 @@ const Gantt = ({ title }) => {
//При необходимости обновить данные таблицы //При необходимости обновить данные таблицы
useEffect(() => { useEffect(() => {
if (state.ident) loadData(); if (gantt.ident) loadData();
}, [state.ident, loadData]); }, [gantt.ident, loadData]);
//При подключении компонента к странице //При подключении компонента к странице
useEffect(() => { useEffect(() => {
@ -2196,23 +2197,21 @@ const Gantt = ({ title }) => {
{title} {title}
</Typography> </Typography>
<FormControlLabel <FormControlLabel
control={<Checkbox onChange={() => setState(pv => ({ ...pv, useCustomTaskDialog: !pv.useCustomTaskDialog }))} />} sx={STYLES.CONTROL}
control={<Checkbox onChange={() => setGantt(pv => ({ ...pv, useCustomTaskDialog: !pv.useCustomTaskDialog }))} />}
label="Отображать пользовательский диалог задачи" label="Отображать пользовательский диалог задачи"
/> />
<Grid container spacing={0} direction="column" alignItems="center"> <Grid container direction="column" alignItems="center">
<Grid item xs={12}> <Grid item xs={12}>
{state.dataLoaded ? ( {gantt.dataLoaded ? (
<Box sx={STYLES.GANTT_CONTAINER} p={1}>
<P8PGantt <P8PGantt
{...P8P_GANTT_CONFIG_PROPS} {...P8P_GANTT_CONFIG_PROPS}
{...state.ganttDef} {...gantt}
height={GANTT_HEIGHT} containerStyle={STYLES.GANTT_CONTAINER}
tasks={state.ganttTasks}
onTaskDatesChange={handleTaskDatesChange} onTaskDatesChange={handleTaskDatesChange}
taskAttributeRenderer={taskAttributeRenderer} taskAttributeRenderer={taskAttributeRenderer}
taskDialogRenderer={state.useCustomTaskDialog ? taskDialogRenderer : null} taskDialogRenderer={gantt.useCustomTaskDialog ? taskDialogRenderer : null}
/> />
</Box>
) : null} ) : null}
</Grid> </Grid>
</Grid> </Grid>
@ -2366,6 +2365,503 @@ const Svg = ({ title }) => {
Полные актуальные исходные коды примера можно увидеть в "app/panels/samples/svg.js" данного репозитория соответственно. Полные актуальные исходные коды примера можно увидеть в "app/panels/samples/svg.js" данного репозитория соответственно.
##### Циклограмма "P8PCyclogram"
Компонент предназначен для отображения данных в виде циклограммы. Поддерживается:
- Группировка задач с отображением описания группы при наведении
- Форматирование цвета заливки задачи, текста задачи и цвета при наведении на задачу/группу
- Дополнение задачи произвольными учётными атрибутами
- Диалоговый редактор задачи, отображающий её дополнительные атрибуты с возможностью настройки их форматирования
- Отображение произвольного пользовательского диалога в качестве карточки задачи/редактора задачи
- Масштабирование визуального представления
![Пример P8PCyclogram](docs/img/72.png)
![Пример P8PCyclogram](docs/img/73.png)
**Подключение**
Клиентская часть циклограммы реализована в компоненте `P8PCyclogram`, объявленном в "app/components/p8p_cyclogram". Для использования компонента на панели его необходимо импортировать:
```
import { P8PCyclogram } from "../../components/p8p_cyclogram";
const MyPanel = () => {
return (
<div>
<P8PCyclogram .../>
</div>
);
}
```
**Свойства**
`containerStyle` - необязательный, объект, стили, которые будут применены к компонету `div`, являющемуся контейнером циклограммы\
`lineHeight` - необязательный, число, высота строки в пикселях (по умолчанию 20)\
`title` - необязательный, строка, заголовок циклограммы (если не указан - не отображается)\
`titleStyle` - необязательный, объект, стили, которые будут применены к компонету `Typography` заголовка циклограммы\
`onTitleClick` - необязательный, функция, будет вызвана при нажатии пользователем на заголовок (если указана - заголовок формируется в виде гиперссылки), сигнатура функции `f()`, результат функции не интерпретируется\
`zoomBar` - необязательный, логический, признак отображения панели управления масштабом (по умолчанию - не отображается)\
`zoom` - необязательный, число, масштаб циклограммы\
`columns` - обязательный, массив, колонки, отображаемые на циклограмме, должен состоять из объектов вида `{name: <НАИМЕНОВАНИЕ>, start: <ПОЗИЦИЯ_НАЧАЛА_КОЛОНКИ>, end: <ПОЗИЦИЯ_ОКОНЧАНИЯ_КОЛОНКИ>}` (см. константу `P8P_CYCLOGRAM_COLUMN_SHAPE` в коде компонента)\
`columnRenderer` - необязательный, функция формирования представления колонки (если не указана - отображение по умолчанию). Сигнатура функции: `f({column})`. Будет вызвана для каждой колонки циклограммы, в функцию будет передан объект, в поле `column` которого будет содержаться описание текущей генерируемой колонки (элемент массива `columns`, см. выше описание полей). Должна возвращать значение или React-компонент.\
`groups` - необязательный, массив, группы задач, которые отображаются на циклограмме, должен состоять из объектов вида `{name: <НАИМЕНОВАНИЕ>, height: <ВЫСОТА_ОТОБРАЖЕНИЯ_ГРУППЫ>, width: <ШИРИНА_ОТОБРАЖЕНИЯ_ГРУППЫ>, visible: <ПРИЗНАК_ОТОБРАЖЕНИЯ_ГРУППЫ>}` (см. константу `P8P_CYCLOGRAM_GROUP_SHAPE` в коде компонента). Группа отображается только при наведении на соответствующую задачу, которая относится к данной группе.\
`groupHeaderRenderer` - необязательный, функция формирования представления всплывающей информации о группе (если не указана - отображение по умолчанию). Сигнатура функции: `f({group})`. Будет вызвана для каждой группы циклограммы, в функцию будет передан объект, в поле `group` которого будет содержаться описание текущей генерируемой группы (элемент массив `groups`, см. выше описание полей). Должна возвращать значение или React-компонент.\
`tasks` - обязательный, массив, задачи, отображаемые на циклограмме, должен состоять из объектов вида `{id: <УНИКАЛЬНЫЙ_ИДЕНТИФИКАТОР>, rn: <ССЫЛКА_НА_ЗАПИСЬ_В_СИСТЕМЕ>, name: <НАИМЕНОВАНИЕ>, fullName: <ПОЛНОЕ_НАИМЕНОВАНИЕ>, lineNumb: <НОМЕР_СТРОКИ_ЗАДАЧИ>, start: <ПОЗИЦИЯ_НАЧАЛА_ЗАДАЧИ>, end: <ПОЗИЦИЯ_ОКОНЧАНИЯ_ЗАДАЧИ>, group: <НАИМЕНОВАНИЕРУППЫ>, bgColor: <ЦВЕТ_ЗАЛИВКИ>, textColor: <ЦВЕТ_ТЕКСТА>, highlightColor: <ЦВЕТ_НАВЕДЕНИЯ>, [<ИМЯ_ДОПОЛНИТЕЛЬНОГО_АТРИБУТА1>:<ЗНАЧЕНИЕ1>, <ИМЯ_ДОПОЛНИТЕЛЬНОГО_АТРИБУТА2>:<ЗНАЧЕНИЕ2>,...]}` (см. константу `P8P_CYCLOGRAM_TASK_SHAPE` в коде компонента).\
`taskRenderer` - необязательный, функция формирования представления задачи на циклограмме (если не указана - отображение по умолчанию). Сигнатура функции: `f({task, taskHeight, taskWidth})`. Будет вызвана для каждой задачи циклограммы, в функцию будет передан объект, в поле `task` которого будет содержаться описание текущей генерируемой задаче (элемент массив `tasks`, см. выше описание полей), в поле `taskHeight` описание высоты задачи, в поле `taskWidth` описание ширины задачи. Должна возвращать объект вида `{taskStyle: <СТИЛИ_ДЛЯ_ЭЛЕМЕНТА_ЗАДАЧИ>, taskProps: <СВОЙСТВА_ДЛЯ_ЭЛЕМЕНТА_ЗАДАЧИ>, data: <ЗНАЧЕНИЕ_ИЛИ_КОМПОНЕНТ_React_ДЛЯ_СОДЕРЖИМОГО_ЭЛЕМЕНТА_ЗАДАЧИ>}` или `undefined`, если для задачи не предполагается специального представления.\
`taskAttributes` - необязательный, массив, состав (не значения) дополнительных атрибутутов задач, должен состоять из объектов вида `{name: <ИМЯ_ДОПОЛНИТЕЛЬНОГО_АТРИБУТА>, caption: <ЗАГОЛОВОКОПОЛНИТЕЛЬНОГО_АТРИБУТА>, visible: <ПРИЗНАК_ОТОБРАЖЕНИЯ_ДОПОЛНИТЕЛЬНОГО_АТРИБУТА - true|false>}` (см. константу `P8P_CYCLOGRAM_TASK_ATTRIBUTE_SHAPE` в коде компонента)\
`taskAttributeRenderer` - необязательный, функция, если указана - будет вызвана при отображении диалога редактора здачи, результат функции будет применён для отображения области дополнительных атрибутов задачи в диалоге редактора, если не указана - дополнительные атрибуты будут отображены с форматированием по умолчанию. Сигнатура функции - `f({task, attribute})`, в функцию будет передан объект в поле `task`, которого, будет содержаться описание задачи для которой отображается редактор (элемент массива `tasks`, см. выше описание полей), в поле `attribute` - описание дополнительного атрибута формируемого в диалоге редактора (элемент массива `taskAttributes`, см. выше описание полей). Должна возвращать значение или React-компонент.\
`taskDialogRenderer` - необязательный, функция, если указана - будет вызвана до отображения диалога редактора задачи. Результат функции будет показан в качестве содержимого диалога редактора, вместо типовой формы. Сигнатура функции - `f({task, taskAttributes, close})`, в функцию будет передан объект в поле `task`, которого, будет содержаться описание задачи для которой отображается редактор (элемент массива `tasks`, см. выше описание полей), в поле `taskAttributes` - массив `taskAttributes` (см. выше описание полей), описывающий состав полей задачи, в поле `close` - функция закрытия диалога задачи, может быть вызвана возвращаемым Reac-компонентом для сокрытия диалога. Должна возвращать значение или React-компонент.\
`noDataFoundText` - обязательный, строка, текст для отображения ошибки об отсутствии данных\
`nameTaskEditorCaption` - обязательный, строка, подпись стандартного атрибута `name` в диалоге редактора задачи\
`okTaskEditorBtnCaption` - обязательный, строка, подпись кнопки "ОК" диалога редактора задачи\
`cancelTaskEditorBtnCaption` - обязательный, строка, подпись кнопки "ОТМЕНА" диалога редактора задачи
Некоторые параметры циклограммы вынесены в свойства компонента `P8PCyclogram` для минимизации его связи с фреймворком и поддержания возможности стороннего использования (например, свойства `noDataFoundText`, `okTaskEditorBtnCaption`, `cancelTaskEditorBtnCaption` и т.п.) . Тем не менее, в настройках фреймворка и его окружении уже есть реализации для данных свойств. Например, в "app.text.js" уже содержатся объявления типовых констант для текстов подписей кнопок и пунктов меню. Поэтому, в "app/config_wrapper.js" для привязки свойств `P8PCyclogram` к контексту фреймворка реализованы специальные декораторы и объекты-шаблоны, облегчающие подключение экземпляра `P8PCyclogram` к панели и снимающие с разработчика необходимость указывать некоторые из перечисленных выше обязательных свойств. В предложенном ниже примере, из модуля "config_wrapper" в панель импортируется объект `P8P_CYCLOGRAM_CONFIG_PROPS`, который уже содержт преднастроенное описание свойств `noDataFoundText`,`nameTaskEditorCaption`, `okTaskEditorBtnCaption` и `cancelTaskEditorBtnCaption`, полученное из окружения фреймворка. Таким образом, прикладной разработчик может не указывать их значения при использовании `P8PCyclogram` (если по каким-то причинам не хочет их переопределить, конечно).
```
import { P8PCyclogram } from "../../components/p8p_cyclogram";
import { P8P_CYCLOGRAM_CONFIG_PROPS } from "../../config_wrapper";
const MyPanel = () => {
return (
<div>
<P8PCyclogram {...P8P_CYCLOGRAM_CONFIG_PROPS} .../>
</div>
);
}
```
**API на сервере БД**
Компонент `P8PCyclogram` требует от разработчика передачи данных в определённом формате. С целью снижения трудозатрат на приведение собранных хранимым объектом данных Системы к форматам, потребляемым `P8PCyclogram`, реализован специальный API на стороне сервера БД.
Для циклограммы это (см. детальные описания программных интерфейсов в пакете `PKG_P8PANELS_VISUAL`):
`PKG_P8PANELS_VISUAL.TCYCLOGRAM_MAKE` - функция, инициализация циклограммы, возвращает объект для хранения её описания\
`PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK_ATTR` - процедура, добавляет, к указанному объекту описания циклограммы, описатель дополнительного атрибута задачи\
`PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_MAKE` - функция, инициализирует и возвращает объект для описания задачи в циклограмме (поставщик данных для `TCYCLOGRAM_ADD_TASK`)\
`PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL` - процедура, добавляет, к указанному объекту описания задачи, значение дополнительного атрибута\
`PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_COLUMN` - процедура, добавляет, к указанному объекту описания циклограммы, новую колонку\
`PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_GROUP` - процедура, добавляет, к указанному объекту описания циклограммы, новую группу\
`PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK` - процедура, добавляет, к указанному объекту описания циклограммы, новую задачу, ранее описанную через `TCYCLOGRAM_TASK_MAKE`\
`PKG_P8PANELS_VISUAL.TCYCLOGRAM_TO_XML` - функция, производит сериализацию объекта, описывающего циклограмму, в специальный XML-формат, корректно интерпретируемый клиентским компонентом `P8PCyclogram` при передаче в WEB-приложение
**Пример**
Код на стороне сервера БД (хранимая процедура в клиентском пакете `PKG_P8PANELS_SAMPLES`, требует наличия таблицы `P8PNL_SMPL_CYCLOGRAM`, см. "db/P8PNL_SMPL_CYCLOGRAM.sql"):
```
procedure CYCLOGRAM
(
NIDENT in number, -- Идентификатор процесса
COUT out clob -- Сериализованные данные для циклограммы
)
is
CG PKG_P8PANELS_VISUAL.TCYCLOGRAM; -- Описание циклограммы
RTASK PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK; -- Описание задачи циклограммы
NLINE_NUMB PKG_STD.TNUMBER := 0; -- Номер строки
NLINE_NUMB_TMP PKG_STD.TNUMBER := 0; -- Номер строки (буфер)
DTASK_DATE_START PKG_STD.TLDATE; -- Дата начала этапа
DTASK_DATE_END PKG_STD.TLDATE; -- Дата окончания этапа
NTASK_START PKG_STD.TNUMBER := 0; -- Позиция начала этапа
NTASK_END PKG_STD.TNUMBER := 0; -- Позиция окончания этапа
STASK_NAME PKG_STD.TSTRING; -- Наименование задачи
SCOLOR_WHITE PKG_STD.TSTRING := 'white'; -- Цвет - белый
SBG_TASK_COLOR_W_GRP PKG_STD.TSTRING := '#6bc982'; -- Цвет задачи с группой
SHL_TASK_COLOR_W_GRP PKG_STD.TSTRING := '#7dd592'; -- Цвет наведения задачи с группой
SBG_TASK_COLOR_WO_GRP PKG_STD.TSTRING := '#e36d6d'; -- Цвет задачи без группы
STEXT_COLOR_TASK_WO_GRP PKG_STD.TSTRING := '#e5e5e5'; -- Цвет текста задачи без группы
SBG_TASK_COLOR_GRP PKG_STD.TSTRING := 'cadetblue'; -- Цвет групповой задачи
SHL_TASK_COLOR_GRP PKG_STD.TSTRING := '#6fadaf'; -- Цвет наведения групповой задачи
/* Считывание значений группирующей задачи */
procedure GROUP_TASK_GET
(
NIDENT in number, -- Идентификатор процесса
NGROUP in number := null, -- Рег. номер группы
NFLAG_WO_GROUP in number := 0, -- Признак отбора задач без групп (0 - нет, 1 - да)
DTASK_DATE_START out date, -- Дата начала этапа
DTASK_DATE_END out date, -- Дата окончания этапа
NTASK_START out number, -- Позиция начала этапа
NTASK_END out number -- Позиция окончания этапа
)
is
begin
...
end GROUP_TASK_GET;
begin
/* Инициализируем циклограмму */
CG := PKG_P8PANELS_VISUAL.TCYCLOGRAM_MAKE(STITLE => 'Задачи на ' || TO_CHAR(EXTRACT(year from sysdate)) || ' год');
/* Добавляем атрибуты */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK_ATTR(RCYCLOGRAM => CG,
SNAME => 'ddate_start',
SCAPTION => 'Дата начала',
BVISIBLE => true,
BCLEAR => true);
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK_ATTR(RCYCLOGRAM => CG,
SNAME => 'ddate_end',
SCAPTION => 'Дата окончания',
BVISIBLE => true);
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK_ATTR(RCYCLOGRAM => CG,
SNAME => 'type',
SCAPTION => 'Тип',
BVISIBLE => false);
/* Обходим колонки */
for CLMN in (select T.NAME,
T.POS_START,
T.POS_END
from P8PNL_SMPL_CYCLOGRAM T
where T.IDENT = NIDENT
and T.TYPE = 0)
loop
/* Добавляем колонку */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_COLUMN(RCYCLOGRAM => CG,
SNAME => CLMN.NAME,
NSTART => CLMN.POS_START,
NEND => CLMN.POS_END);
end loop;
/* Считываем значения для задач проекта */
GROUP_TASK_GET(NIDENT => NIDENT,
NFLAG_WO_GROUP => 0,
DTASK_DATE_START => DTASK_DATE_START,
DTASK_DATE_END => DTASK_DATE_END,
NTASK_START => NTASK_START,
NTASK_END => NTASK_END);
/* Формируем задачу (этап) */
RTASK := PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_MAKE(NRN => 1,
SCAPTION => 'Задачи проекта',
SNAME => 'Задачи проекта',
NLINE_NUMB => NLINE_NUMB,
NSTART => NTASK_START,
NEND => NTASK_END,
SBG_COLOR => SCOLOR_WHITE);
/* Добавляем атрибуты */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_start',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => DTASK_DATE_START),
BCLEAR => true);
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_end',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => DTASK_DATE_END));
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG, RTASK => RTASK, SNAME => 'type', SVALUE => 0);
/* Добавляем задачу в циклограмму */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK(RCYCLOGRAM => CG, RTASK => RTASK);
/* Указываем следующую строку */
NLINE_NUMB := NLINE_NUMB + 1;
/* Обходим группы */
for GRP in (select T.RN,
T.NAME,
ROWNUM RNUM
from P8PNL_SMPL_CYCLOGRAM T
where T.IDENT = NIDENT
and T.TYPE = 1
order by T.RN asc)
loop
...
/* Добавляем группу */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_GROUP(RCYCLOGRAM => CG,
SNAME => GRP.NAME,
NHEADER_HEIGHT => 30,
NHEADER_WIDTH => 200);
/* Считываем значения этапа группы */
GROUP_TASK_GET(NIDENT => NIDENT,
NGROUP => GRP.RN,
DTASK_DATE_START => DTASK_DATE_START,
DTASK_DATE_END => DTASK_DATE_END,
NTASK_START => NTASK_START,
NTASK_END => NTASK_END);
/* Формируем задачу (этап) */
RTASK := PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_MAKE(NRN => GRP.RN,
SCAPTION => 'Этап ' || TO_CHAR(GRP.RNUM),
SNAME => 'Этап ' || TO_CHAR(GRP.RNUM),
NLINE_NUMB => NLINE_NUMB,
NSTART => NTASK_START,
NEND => NTASK_END,
SBG_COLOR => SBG_TASK_COLOR_GRP,
STEXT_COLOR => SCOLOR_WHITE,
SHIGHLIGHT_COLOR => SHL_TASK_COLOR_GRP);
/* Добавляем атрибуты */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_start',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => DTASK_DATE_START),
BCLEAR => true);
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_end',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => DTASK_DATE_END));
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG, RTASK => RTASK, SNAME => 'type', SVALUE => 1);
/* Добавляем задачу в циклограмму */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK(RCYCLOGRAM => CG, RTASK => RTASK);
/* Обходим задачи группы */
for TASK in (select T.RN,
T.NAME,
T.POS_START,
T.POS_END,
T.DATE_FROM,
T.DATE_TO,
ROWNUM RNUM
from P8PNL_SMPL_CYCLOGRAM T
where T.IDENT = NIDENT
and T.TYPE = 2
and T.TASK_GROUP = GRP.RN)
loop
...
end loop;
/* Указываем следующую строку */
NLINE_NUMB := NLINE_NUMB + 1;
end loop;
/* Указываем следующую строку */
NLINE_NUMB := NLINE_NUMB + 1;
/* Считываем значения для обособленных задач */
GROUP_TASK_GET(NIDENT => NIDENT,
NFLAG_WO_GROUP => 1,
DTASK_DATE_START => DTASK_DATE_START,
DTASK_DATE_END => DTASK_DATE_END,
NTASK_START => NTASK_START,
NTASK_END => NTASK_END);
/* Формируем задачу (этап) */
RTASK := PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_MAKE(NRN => 1,
SCAPTION => 'Обособленные задачи',
SNAME => 'Обособленные задачи',
NLINE_NUMB => NLINE_NUMB,
NSTART => NTASK_START,
NEND => NTASK_END,
SBG_COLOR => SCOLOR_WHITE);
/* Добавляем атрибуты */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_start',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => DTASK_DATE_START),
BCLEAR => true);
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_end',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => DTASK_DATE_END));
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG, RTASK => RTASK, SNAME => 'type', SVALUE => 0);
/* Добавляем задачу в циклограмму */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK(RCYCLOGRAM => CG, RTASK => RTASK);
/* Указываем следующую строку */
NLINE_NUMB := NLINE_NUMB + 1;
/* Цикл по обособленным задачам */
for REC in (select T.RN,
T.NAME,
T.POS_START,
T.POS_END,
T.DATE_FROM,
T.DATE_TO,
ROWNUM RNUM
from P8PNL_SMPL_CYCLOGRAM T
where T.IDENT = NIDENT
and T.TYPE = 2
and T.TASK_GROUP is null)
loop
...
end loop;
/* Формируем список */
COUT := PKG_P8PANELS_VISUAL.TCYCLOGRAM_TO_XML(RCYCLOGRAM => CG);
end CYCLOGRAM;
```
Код панели на стороне клиента (WEB-приложения):
```
/*
Парус 8 - Панели мониторинга - Примеры для разработчиков
Пример: Циклограмма "P8PCyclogram"
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext, useCallback, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
Typography,
Grid,
Button,
Box,
DialogContent,
List,
ListItem,
ListItemText,
Divider,
TextField,
DialogActions,
Stack,
Icon
} from "@mui/material"; //Интерфейсные элементы
import { formatDateJSONDateOnly, formatDateRF } from "../../core/utils"; //Вспомогательные функции
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
import { P8PCyclogram } from "../../components/p8p_cyclogram"; //Циклограмма
import { P8P_CYCLOGRAM_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
//---------
//Константы
//---------
//Отступ контейнера страницы от заголовка
const CONTAINER_PADDING_TOP = "20px";
//Высота заголовка страницы
const TITLE_HEIGHT = "47px";
//Высота строк
const LINE_HEIGHT = 30;
//Стили
const STYLES = {
CONTAINER: { textAlign: "center", paddingTop: CONTAINER_PADDING_TOP },
TITLE: { paddingBottom: "15px", height: TITLE_HEIGHT },
CYCLOGRAM_CONTAINER: {
height: `calc(100vh - ${APP_BAR_HEIGHT} - ${TITLE_HEIGHT} - ${CONTAINER_PADDING_TOP})`,
width: "100vw",
paddingTop: "5px"
},
TASK_EDITOR_CONTENT: { minWidth: 400, overflowX: "auto" },
TASK_EDITOR_LIST: { width: "100%", minWidth: 300, maxWidth: 700, bgcolor: "background.paper" },
GROUP_HEADER: height => ({
border: "1px solid",
backgroundColor: "#ecf8fb",
height: height,
borderRadius: "10px",
display: "flex",
alignItems: "center",
justifyContent: "space-around"
})
};
//---------------------------------------------
//Вспомогательные функции форматирования данных
//---------------------------------------------
//Диалог открытия задачи
const CustomTaskDialog = ({ task, ident, handleReload, close }) => {
...
};
//Контроль свойств - Диалог открытия задачи
CustomTaskDialog.propTypes = {
task: PropTypes.object.isRequired,
ident: PropTypes.number.isRequired,
handleReload: PropTypes.func.isRequired,
close: PropTypes.func.isRequired
};
//Заголовок группы
const CustomGroupHeader = ({ group }) => {
...
};
//Контроль свойств - Заголовок группы
CustomGroupHeader.propTypes = {
group: PropTypes.object.isRequired
};
//Отображение задачи
const taskRenderer = ({ task }) => {
...
};
//-----------
//Тело модуля
//-----------
//Пример: Циклограмма "P8PCyclogram"
const Cyclogram = ({ title }) => {
//Собственное состояние
const [state, setState] = useState({
init: false,
dataLoaded: false,
reload: true,
ident: null
});
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При необходимости перезагрузки
const handleReload = () => {
setState(pv => ({ ...pv, reload: true }));
};
//При необходимости обновить данные таблицы
useEffect(() => {
//Загрузка данных циклограммы с сервера
const loadData = async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_SAMPLES.CYCLOGRAM",
args: { NIDENT: state.ident },
attributeValueProcessor: (name, val) =>
name === "name" ? undefined : ["ddate_start", "ddate_end"].includes(name) ? formatDateJSONDateOnly(val) : val,
respArg: "COUT"
});
setState(pv => ({ ...pv, dataLoaded: true, ...data.XCYCLOGRAM, reload: false }));
};
//Если указан идентификатор и требуется перезагрузить
if (state.ident && state.reload) loadData();
}, [state.ident, state.reload, executeStored]);
//При подключении компонента к странице
useEffect(() => {
//Инициализация данных циклограммы
const initData = async () => {
const data = await executeStored({ stored: "PKG_P8PANELS_SAMPLES.CYCLOGRAM_INIT", args: { NIDENT: state.ident } });
setState(pv => ({ ...pv, init: true, ident: data.NIDENT, reload: true }));
};
//Если требуется проинициализировать
if (!state.init) {
initData();
}
}, [executeStored, state.ident, state.init]);
return (
<Box>
<div style={STYLES.CONTAINER}>
<Typography sx={STYLES.TITLE} variant={"h6"}>
{title}
</Typography>
<Grid container direction="column" alignItems="center">
<Grid item xs={12}>
{state.dataLoaded ? (
<P8PCyclogram
{...P8P_CYCLOGRAM_CONFIG_PROPS}
{...state}
containerStyle={STYLES.CYCLOGRAM_CONTAINER}
lineHeight={LINE_HEIGHT}
taskDialogRenderer={prms => (
<CustomTaskDialog task={prms.task} ident={state.ident} handleReload={handleReload} close={prms.close} />
)}
taskRenderer={prms => taskRenderer(prms)}
groupHeaderRenderer={prms => <CustomGroupHeader group={prms.group} />}
/>
) : null}
</Grid>
</Grid>
</div>
</Box>
);
};
//Контроль свойств - Пример: Циклограмма "P8PCyclogram"
Cyclogram.propTypes = {
title: PropTypes.string.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { Cyclogram };
```
Полные актуальные исходные коды примеров можно увидеть в "db/PKG_P8PANELS_SAMPLES.pck" и "app/panels/samples/cyclogram.js" данного репозитория соответственно.
### Ограничения дизайна пользовательского интерфейса ### Ограничения дизайна пользовательского интерфейса
Фреймворк позволяет реализовать любые пользовательские интерфейсы, вёрстка которых не противоречит возможностям современного HTML. Тем не менее, при разработке пользовательских интерфейсов панелей важно придерживаться предложенных ниже правил. Это позволит создавать их в едином ключе и упростит работу конечного пользователя при их освоении. Фреймворк позволяет реализовать любые пользовательские интерфейсы, вёрстка которых не противоречит возможностям современного HTML. Тем не менее, при разработке пользовательских интерфейсов панелей важно придерживаться предложенных ниже правил. Это позволит создавать их в едином ключе и упростит работу конечного пользователя при их освоении.

View File

@ -53,6 +53,7 @@ export const CAPTIONS = {
export const ERRORS = { export const ERRORS = {
UNDER_CONSTRUCTION: "Панель в разработке", UNDER_CONSTRUCTION: "Панель в разработке",
P8O_API_UNAVAILABLE: '"ПАРУС 8 Онлайн" недоступен', P8O_API_UNAVAILABLE: '"ПАРУС 8 Онлайн" недоступен',
P8O_API_UNSUPPORTED: 'Функция "ПАРУС 8 Онлайн" не поддерживается',
DEFAULT: "Неожиданная ошибка" DEFAULT: "Неожиданная ошибка"
}; };

View File

@ -23,7 +23,8 @@ import {
ListItemIcon, ListItemIcon,
ListItemText ListItemText
} from "@mui/material"; //Интерфейсные компоненты } from "@mui/material"; //Интерфейсные компоненты
import { P8PPanelsMenuDrawer, P8P_PANELS_MENU_PANEL_SHAPE } from "./p8p_panels_menu"; import { P8PPanelsMenuDrawer, P8P_PANELS_MENU_PANEL_SHAPE } from "./p8p_panels_menu"; //Меню
import { APP_STYLES } from "../../app.styles"; //Типовые стили
//--------- //---------
//Константы //Константы
@ -34,6 +35,7 @@ const APP_BAR_HEIGHT = "64px";
//Стили //Стили
const STYLES = { const STYLES = {
DRAWER: { [`& .MuiDrawer-paper`]: { ...APP_STYLES.SCROLL } },
ROOT_BOX: { display: "flex" }, ROOT_BOX: { display: "flex" },
APP_BAR: { position: "fixed" }, APP_BAR: { position: "fixed" },
APP_BAR_BUTTON: { mr: 2 }, APP_BAR_BUTTON: { mr: 2 },
@ -88,7 +90,7 @@ const P8PAppWorkspace = ({ children, panels = [], selectedPanel, closeCaption, h
</Typography> </Typography>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<Drawer anchor="left" open={open} onClose={handleDrawerClose}> <Drawer anchor="left" open={open} onClose={handleDrawerClose} sx={STYLES.DRAWER}>
<List> <List>
<ListItemButton onClick={handleDrawerClose}> <ListItemButton onClick={handleDrawerClose}>
<ListItemIcon> <ListItemIcon>

View File

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

View File

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

View File

@ -36,15 +36,15 @@ const P8P_DATA_GRID_FILTERS_HEIGHT = P8P_TABLE_FILTERS_HEIGHT;
//Таблица данных //Таблица данных
const P8PDataGrid = ({ const P8PDataGrid = ({
columnsDef, columnsDef = [],
filtersInitial, filtersInitial,
groups, groups = [],
rows, rows = [],
size, size,
fixedHeader = false, fixedHeader = false,
fixedColumns = 0, fixedColumns = 0,
morePages = false, morePages = false,
reloading, reloading = false,
expandable, expandable,
orderAscMenuItemCaption, orderAscMenuItemCaption,
orderDescMenuItemCaption, orderDescMenuItemCaption,
@ -154,15 +154,15 @@ const P8PDataGrid = ({
//Контроль свойств - Таблица данных //Контроль свойств - Таблица данных
P8PDataGrid.propTypes = { P8PDataGrid.propTypes = {
columnsDef: PropTypes.array.isRequired, columnsDef: PropTypes.array,
filtersInitial: PropTypes.arrayOf(P8P_DATA_GRID_FILTER_SHAPE), filtersInitial: PropTypes.arrayOf(P8P_DATA_GRID_FILTER_SHAPE),
groups: PropTypes.array, groups: PropTypes.array,
rows: PropTypes.array.isRequired, rows: PropTypes.array,
size: PropTypes.string, size: PropTypes.string,
fixedHeader: PropTypes.bool, fixedHeader: PropTypes.bool,
fixedColumns: PropTypes.number, fixedColumns: PropTypes.number,
morePages: PropTypes.bool, morePages: PropTypes.bool,
reloading: PropTypes.bool.isRequired, reloading: PropTypes.bool,
expandable: PropTypes.bool, expandable: PropTypes.bool,
orderAscMenuItemCaption: PropTypes.string.isRequired, orderAscMenuItemCaption: PropTypes.string.isRequired,
orderDescMenuItemCaption: PropTypes.string.isRequired, orderDescMenuItemCaption: PropTypes.string.isRequired,

View File

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

View File

@ -33,7 +33,7 @@ import { P8PAppInlineError } from "./p8p_app_message"; //Встраиваемо
//--------- //---------
//Уровни масштаба //Уровни масштаба
const P8P_GANTT_ZOOM = [0, 1, 2, 3, 4]; const P8P_GANTT_ZOOM = [0, 1, 2, 3, 4, 5];
//Уровни масштаба (строковые наименования в терминах библиотеки) //Уровни масштаба (строковые наименования в терминах библиотеки)
const P8P_GANTT_ZOOM_VIEW_MODES = { const P8P_GANTT_ZOOM_VIEW_MODES = {
@ -41,7 +41,8 @@ const P8P_GANTT_ZOOM_VIEW_MODES = {
1: "Half Day", 1: "Half Day",
2: "Day", 2: "Day",
3: "Week", 3: "Week",
4: "Month" 4: "Month",
5: "Year"
}; };
//Структура задачи //Структура задачи

View File

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

View File

@ -118,7 +118,9 @@ const STYLES = {
}, },
TABLE_CELL_EXPAND_CONTAINER: { TABLE_CELL_EXPAND_CONTAINER: {
paddingBottom: 0, paddingBottom: 0,
paddingTop: 0 paddingTop: 0,
paddingLeft: 0,
paddingRight: 0
}, },
TABLE_CELL_GROUP_HEADER: { TABLE_CELL_GROUP_HEADER: {
backgroundColor: "lightgray" backgroundColor: "lightgray"
@ -486,16 +488,16 @@ P8PTableFiltersChips.propTypes = {
//Таблица //Таблица
const P8PTable = ({ const P8PTable = ({
columnsDef, columnsDef = [],
groups, groups = [],
rows, rows = [],
orders, orders,
filters, filters,
size, size,
fixedHeader = false, fixedHeader = false,
fixedColumns = 0, fixedColumns = 0,
morePages = false, morePages = false,
reloading, reloading = false,
expandable, expandable,
orderAscMenuItemCaption, orderAscMenuItemCaption,
orderDescMenuItemCaption, orderDescMenuItemCaption,
@ -531,7 +533,9 @@ const P8PTable = ({
const [expanded, setExpanded] = useState({}); const [expanded, setExpanded] = useState({});
//Собственное состояния - развёрнутые группы //Собственное состояния - развёрнутые группы
const [expandedGroups, setExpandedGroups] = useState({}); const [expandedGroups, setExpandedGroups] = useState(
Array.isArray(groups) && groups.length > 0 ? Object.assign({}, ...groups.map(g => ({ [g.name]: g.expanded }))) : {}
);
//Собственное состояние - колонка с отображаемой подсказкой //Собственное состояние - колонка с отображаемой подсказкой
const [displayHintColumn, setDisplayHintColumn] = useState(null); const [displayHintColumn, setDisplayHintColumn] = useState(null);
@ -931,7 +935,7 @@ P8PTable.propTypes = {
fixedHeader: PropTypes.bool, fixedHeader: PropTypes.bool,
fixedColumns: PropTypes.number, fixedColumns: PropTypes.number,
morePages: PropTypes.bool, morePages: PropTypes.bool,
reloading: PropTypes.bool.isRequired, reloading: PropTypes.bool,
expandable: PropTypes.bool, expandable: PropTypes.bool,
orderAscMenuItemCaption: PropTypes.string.isRequired, orderAscMenuItemCaption: PropTypes.string.isRequired,
orderDescMenuItemCaption: PropTypes.string.isRequired, orderDescMenuItemCaption: PropTypes.string.isRequired,

View File

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

View File

@ -22,7 +22,8 @@ const P8O_API = window.top?.parus?.clientApi;
//Структура объекта с описанием ошибок //Структура объекта с описанием ошибок
const APPLICATION_CONTEXT_ERRORS_SHAPE = PropTypes.shape({ const APPLICATION_CONTEXT_ERRORS_SHAPE = PropTypes.shape({
P8O_API_UNAVAILABLE: PropTypes.string.isRequired P8O_API_UNAVAILABLE: PropTypes.string.isRequired,
P8O_API_UNSUPPORTED: PropTypes.string.isRequired
}); });
//---------------- //----------------
@ -72,21 +73,38 @@ export const ApplicationContext = ({ errors, displaySizeGetter, guidGenerator, c
//Отображение раздела "ПАРУС 8 Онлайн" //Отображение раздела "ПАРУС 8 Онлайн"
const pOnlineShowUnit = useCallback( const pOnlineShowUnit = useCallback(
({ unitCode, showMethod = "main", inputParameters }) => { ({ unitCode, showMethod = "main", inputParameters, modal = true }) => {
if (P8O_API) P8O_API.fn.openDocumentModal({ unitcode: unitCode, method: showMethod, inputParameters }); if (P8O_API)
modal
? P8O_API.fn.openDocumentModal({ unitcode: unitCode, method: showMethod, inputParameters })
: P8O_API.fn.openDocument
? P8O_API.fn.openDocument({ unitcode: unitCode, method: showMethod, inputParameters })
: showMsgErr(errors.P8O_API_UNSUPPORTED);
else showMsgErr(errors.P8O_API_UNAVAILABLE); else showMsgErr(errors.P8O_API_UNAVAILABLE);
}, },
[showMsgErr, errors.P8O_API_UNAVAILABLE] [showMsgErr, errors.P8O_API_UNAVAILABLE, errors.P8O_API_UNSUPPORTED]
); );
//Отображение документа "ПАРУС 8 Онлайн" //Отображение документа "ПАРУС 8 Онлайн"
const pOnlineShowDocument = useCallback( const pOnlineShowDocument = useCallback(
({ unitCode, document, showMethod = "main", inRnParameter = "in_RN" }) => { ({ unitCode, document, showMethod = "main", inRnParameter = "in_RN", modal = true }) => {
if (P8O_API) if (P8O_API)
P8O_API.fn.openDocumentModal({ unitcode: unitCode, method: showMethod, inputParameters: [{ name: inRnParameter, value: document }] }); modal
? P8O_API.fn.openDocumentModal({
unitcode: unitCode,
method: showMethod,
inputParameters: [{ name: inRnParameter, value: document }]
})
: P8O_API.fn.openDocument
? P8O_API.fn.openDocument({
unitcode: unitCode,
method: showMethod,
inputParameters: [{ name: inRnParameter, value: document }]
})
: showMsgErr(errors.P8O_API_UNSUPPORTED);
else showMsgErr(errors.P8O_API_UNAVAILABLE); else showMsgErr(errors.P8O_API_UNAVAILABLE);
}, },
[showMsgErr, errors.P8O_API_UNAVAILABLE] [showMsgErr, errors.P8O_API_UNAVAILABLE, errors.P8O_API_UNSUPPORTED]
); );
//Отображение словаря "ПАРУС 8 Онлайн" //Отображение словаря "ПАРУС 8 Онлайн"

View File

@ -33,34 +33,42 @@ const DISPLAY_SIZE = {
//Типовые пути конвертации в массив (при переводе XML -> JSON) //Типовые пути конвертации в массив (при переводе XML -> JSON)
const XML_ALWAYS_ARRAY_PATHS = [ const XML_ALWAYS_ARRAY_PATHS = [
"XRESPOND.XPAYLOAD.XOUT_ARGUMENTS", "XRESPOND.XPAYLOAD.XOUT_ARGUMENTS",
"XRESPOND.XPAYLOAD.XROWS", "XRESPOND.XPAYLOAD.XDATA_GRID.rows",
"XRESPOND.XPAYLOAD.XCOLUMNS_DEF", "XRESPOND.XPAYLOAD.XDATA_GRID.columnsDef",
"XRESPOND.XPAYLOAD.XCOLUMNS_DEF.values", "XRESPOND.XPAYLOAD.XDATA_GRID.columnsDef.values",
"XRESPOND.XPAYLOAD.XGROUPS", "XRESPOND.XPAYLOAD.XDATA_GRID.groups",
"XRESPOND.XPAYLOAD.XGANTT_DEF.taskAttributes", "XRESPOND.XPAYLOAD.XGANTT.taskAttributes",
"XRESPOND.XPAYLOAD.XGANTT_DEF.taskColors", "XRESPOND.XPAYLOAD.XGANTT.taskColors",
"XRESPOND.XPAYLOAD.XGANTT_TASKS", "XRESPOND.XPAYLOAD.XGANTT.tasks",
"XRESPOND.XPAYLOAD.XGANTT_TASKS.dependencies", "XRESPOND.XPAYLOAD.XGANTT.tasks.dependencies",
"XRESPOND.XPAYLOAD.XCHART.labels", "XRESPOND.XPAYLOAD.XCHART.labels",
"XRESPOND.XPAYLOAD.XCHART.datasets", "XRESPOND.XPAYLOAD.XCHART.datasets",
"XRESPOND.XPAYLOAD.XCHART.datasets.data", "XRESPOND.XPAYLOAD.XCHART.datasets.data",
"XRESPOND.XPAYLOAD.XCHART.datasets.items" "XRESPOND.XPAYLOAD.XCHART.datasets.items",
"XRESPOND.XPAYLOAD.XCYCLOGRAM.taskAttributes",
"XRESPOND.XPAYLOAD.XCYCLOGRAM.columns",
"XRESPOND.XPAYLOAD.XCYCLOGRAM.groups",
"XRESPOND.XPAYLOAD.XCYCLOGRAM.tasks"
]; ];
//Типовые шаблоны конвертации в массив (при переводе XML -> JSON) //Типовые шаблоны конвертации в массив (при переводе XML -> JSON)
const XML_ALWAYS_ARRAY_PATH_PATTERNS = [ const XML_ALWAYS_ARRAY_PATH_PATTERNS = [
/(.*)XROWS$/, /(.*)XDATA_GRID.rows$/,
/(.*)XCOLUMNS_DEF$/, /(.*)XDATA_GRID.columnsDef$/,
/(.*)XCOLUMNS_DEF.values$/, /(.*)XDATA_GRID.columnsDef.values$/,
/(.*)XGROUPS$/, /(.*)XDATA_GRID.groups$/,
/(.*)XGANTT_DEF.taskAttributes$/, /(.*)XGANTT.taskAttributes$/,
/(.*)XGANTT_DEF.taskColors$/, /(.*)XGANTT.taskColors$/,
/(.*)XGANTT_TASKS$/, /(.*)XGANTT.tasks$/,
/(.*)XGANTT_TASKS.dependencies$/, /(.*)XGANTT.tasks.dependencies$/,
/(.*)XCHART.labels$/, /(.*)XCHART.labels$/,
/(.*)XCHART.datasets$/, /(.*)XCHART.datasets$/,
/(.*)XCHART.datasets.data$/, /(.*)XCHART.datasets.data$/,
/(.*)XCHART.datasets.items$/ /(.*)XCHART.datasets.items$/,
/(.*)XCYCLOGRAM.taskAttributes$/,
/(.*)XCYCLOGRAM.columns$/,
/(.*)XCYCLOGRAM.groups$/,
/(.*)XCYCLOGRAM.tasks$/
]; ];
//Типовой постфикс тега для массива (при переводе XML -> JSON) //Типовой постфикс тега для массива (при переводе XML -> JSON)
@ -68,11 +76,13 @@ const XML_ALWAYS_ARRAY_POSTFIX = "__SYSTEM__ARRAY__";
//Типовые шаблоны конвертации значения атрибута в строку (при переводе XML -> JSON) //Типовые шаблоны конвертации значения атрибута в строку (при переводе XML -> JSON)
const XML_ATTR_ALWAYS_STR_PATH_PATTERNS = [ const XML_ATTR_ALWAYS_STR_PATH_PATTERNS = [
/(.*)XCOLUMNS_DEF.name$/, /(.*)XDATA_GRID.columnsDef.name$/,
/(.*)XCOLUMNS_DEF.caption$/, /(.*)XDATA_GRID.columnsDef.caption$/,
/(.*)XCOLUMNS_DEF.parent$/, /(.*)XDATA_GRID.columnsDef.parent$/,
/(.*)XGROUPS.name$/, /(.*)XDATA_GRID.groups.name$/,
/(.*)XGROUPS.caption$/ /(.*)XDATA_GRID.groups.caption$/,
/(.*)XCYCLOGRAM.columns.name$/,
/(.*)XCYCLOGRAM.groups.name$/
]; ];
//----------- //-----------
@ -97,7 +107,6 @@ const deepCopyObject = obj => JSON.parse(JSON.stringify(obj));
//Конвертация объекта в Base64 XML //Конвертация объекта в Base64 XML
const object2Base64XML = (obj, builderOptions) => { const object2Base64XML = (obj, builderOptions) => {
const builder = new XMLBuilder(builderOptions); const builder = new XMLBuilder(builderOptions);
//onOrderChanged({ orders: btoa(ordersBuilder.build(newOrders)) });
return btoa(unescape(encodeURIComponent(builder.build(obj)))); return btoa(unescape(encodeURIComponent(builder.build(obj))));
}; };
@ -140,6 +149,9 @@ const xml2JSON = ({ xmlDoc, isArray, transformTagName, tagValueProcessor, attrib
//Форматирование даты в формат РФ //Форматирование даты в формат РФ
const formatDateRF = value => (value ? dayjs(value).format("DD.MM.YYYY") : null); const formatDateRF = value => (value ? dayjs(value).format("DD.MM.YYYY") : null);
//Форматирование даты и времени в формат РФ
const formatDateTimeRF = value => (value ? dayjs(value).format("DD.MM.YYYY HH:mm:ss") : null);
//Форматирование даты в формат JSON (только дата, без времени) //Форматирование даты в формат JSON (только дата, без времени)
const formatDateJSONDateOnly = value => (value ? dayjs(value).format("YYYY-MM-DD") : null); const formatDateJSONDateOnly = value => (value ? dayjs(value).format("YYYY-MM-DD") : null);
@ -163,6 +175,7 @@ export {
object2Base64XML, object2Base64XML,
xml2JSON, xml2JSON,
formatDateRF, formatDateRF,
formatDateTimeRF,
formatDateJSONDateOnly, formatDateJSONDateOnly,
formatNumberRFCurrency, formatNumberRFCurrency,
genGUID genGUID

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -53,14 +53,15 @@ const useCostRouteLists = (task, taskType) => {
}); });
setCostRouteLists(pv => ({ setCostRouteLists(pv => ({
...pv, ...pv,
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef, ...data.XDATA_GRID,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])], columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
dataLoaded: true, dataLoaded: true,
reload: false, reload: false,
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE, morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE,
quantPlanSum: data.XROWS ? data.XROWS.reduce((a, b) => a + b["NQUANT_PLAN"], 0) : 0, quantPlanSum: data.XDATA_GRID.rows ? data.XDATA_GRID.rows.reduce((a, b) => a + b["NQUANT_PLAN"], 0) : 0,
uniqueNomns: data.XROWS uniqueNomns: data.XDATA_GRID.rows
? data.XROWS.reduce((accumulator, current) => { ? data.XDATA_GRID.rows.reduce((accumulator, current) => {
if (!accumulator.find(item => item.SMATRES_PLAN_NOMEN === current.SMATRES_PLAN_NOMEN)) { if (!accumulator.find(item => item.SMATRES_PLAN_NOMEN === current.SMATRES_PLAN_NOMEN)) {
accumulator.push(current); accumulator.push(current);
} }
@ -122,11 +123,12 @@ const useIncomFromDeps = (task, taskType) => {
}); });
setIncomFromDeps(pv => ({ setIncomFromDeps(pv => ({
...pv, ...pv,
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef, ...data.XDATA_GRID,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])], columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
dataLoaded: true, dataLoaded: true,
reload: false, reload: false,
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
})); }));
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -172,11 +174,12 @@ const useGoodsParties = mainRowRN => {
}); });
setGoodsParties(pv => ({ setGoodsParties(pv => ({
...pv, ...pv,
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef, ...data.XDATA_GRID,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])], columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
dataLoaded: true, dataLoaded: true,
reload: false, reload: false,
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
})); }));
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -223,11 +226,12 @@ const useCostDeliveryLists = mainRowRN => {
}); });
setCostDeliveryLists(pv => ({ setCostDeliveryLists(pv => ({
...pv, ...pv,
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef, ...data.XDATA_GRID,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])], columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
dataLoaded: true, dataLoaded: true,
reload: false, reload: false,
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
})); }));
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -43,11 +43,12 @@ import { MessagingСtx } from "../../context/messaging"; //Контекст со
import { NavigationCtx } from "../../context/navigation"; //Контекст навигации import { NavigationCtx } from "../../context/navigation"; //Контекст навигации
import { P8P_GANTT_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения import { P8P_GANTT_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
import { P8PGantt, taskLegendDesc } from "../../components/p8p_gantt"; //Диаграмма Ганта import { P8PGantt, taskLegendDesc } from "../../components/p8p_gantt"; //Диаграмма Ганта
import { xml2JSON, formatDateJSONDateOnly, formatDateRF, hasValue } from "../../core/utils"; //Вспомогательные функции import { xml2JSON, formatDateJSONDateOnly, formatDateRF, hasValue } from "../../core/utils"; //Вспомогательные функции
import { useFilteredPlanCtlgs } from "./hooks"; //Вспомогательные хуки import { useFilteredPlanCtlgs } from "./hooks"; //Вспомогательные хуки
import { CostRouteListsDataGrid } from "./datagrids/fcroutlst"; import { CostRouteListsDataGrid } from "./datagrids/fcroutlst"; //Таблица "Маршрутные листы"
import { IncomFromDepsDataGrid } from "./datagrids/incomefromdeps"; import { IncomFromDepsDataGrid } from "./datagrids/incomefromdeps"; //Таблица "Приходы из подразделений"
//--------- //---------
//Константы //Константы
@ -72,7 +73,7 @@ const STYLES = {
width: "350px", width: "350px",
display: "inline-block", display: "inline-block",
flexShrink: 0, flexShrink: 0,
[`& .MuiDrawer-paper`]: { width: "350px", display: "inline-block", boxSizing: "border-box" } [`& .MuiDrawer-paper`]: { width: "350px", display: "inline-block", boxSizing: "border-box", ...APP_STYLES.SCROLL }
}, },
GANTT_CONTAINER: { height: `calc(100vh - ${APP_BAR_HEIGHT})`, width: "100vw", paddingTop: "24px" }, GANTT_CONTAINER: { height: `calc(100vh - ${APP_BAR_HEIGHT})`, width: "100vw", paddingTop: "24px" },
GANTT_TITLE: { paddingLeft: "250px", paddingRight: "250px" }, GANTT_TITLE: { paddingLeft: "250px", paddingRight: "250px" },
@ -97,7 +98,7 @@ const parseProdPlanSpXML = async xmlDoc => {
attributeValueProcessor: (name, val) => attributeValueProcessor: (name, val) =>
["numb", "title"].includes(name) ? undefined : ["start", "end"].includes(name) ? formatDateJSONDateOnly(val) : val ["numb", "title"].includes(name) ? undefined : ["start", "end"].includes(name) ? formatDateJSONDateOnly(val) : val
}); });
return data.XDATA; return data.XDATA.XGANTT;
}; };
//Форматирование для отображения количества документов //Форматирование для отображения количества документов
@ -237,8 +238,7 @@ const MechRecCostProdPlans = () => {
selectedPlanCtlgLevel: null, selectedPlanCtlgLevel: null,
selectedPlanCtlgSort: null, selectedPlanCtlgSort: null,
selectedPlanCtlgMenuItems: null, selectedPlanCtlgMenuItems: null,
selectedPlanCtlgGanttDef: {}, gantt: {},
selectedPlanCtlgSpecs: [],
selectedTaskDetail: null, selectedTaskDetail: null,
selectedTaskDetailType: null, selectedTaskDetailType: null,
planSpec: null planSpec: null
@ -282,8 +282,7 @@ const MechRecCostProdPlans = () => {
selectedPlanCtlgLevel: null, selectedPlanCtlgLevel: null,
selectedPlanCtlgSort: null, selectedPlanCtlgSort: null,
selectedPlanCtlgMenuItems: null, selectedPlanCtlgMenuItems: null,
selectedPlanCtlgSpecs: [], gantt: {},
selectedPlanCtlgGanttDef: {},
showPlanList: false, showPlanList: false,
selectedTaskDetail: null, selectedTaskDetail: null,
selectedTaskDetailType: null selectedTaskDetailType: null
@ -300,8 +299,7 @@ const MechRecCostProdPlans = () => {
selectedPlanCtlgLevel: null, selectedPlanCtlgLevel: null,
selectedPlanCtlgSort: null, selectedPlanCtlgSort: null,
selectedPlanCtlgMenuItems: null, selectedPlanCtlgMenuItems: null,
selectedPlanCtlgSpecs: [], gantt: {},
selectedPlanCtlgGanttDef: {},
showPlanList: false, showPlanList: false,
selectedTaskDetail: null, selectedTaskDetail: null,
selectedTaskDetailType: null selectedTaskDetailType: null
@ -324,8 +322,7 @@ const MechRecCostProdPlans = () => {
? state.selectedPlanCtlgMenuItems ? state.selectedPlanCtlgMenuItems
: [...Array(data.NMAX_LEVEL).keys()].map(el => el + 1), : [...Array(data.NMAX_LEVEL).keys()].map(el => el + 1),
selectedPlanCtlgSpecsLoaded: true, selectedPlanCtlgSpecsLoaded: true,
selectedPlanCtlgGanttDef: doc.XGANTT_DEF ? { ...doc.XGANTT_DEF } : {}, gantt: { ...doc, tasks: [...(doc?.tasks || [])] }
selectedPlanCtlgSpecs: [...(doc?.XGANTT_TASKS || [])]
})); }));
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -401,7 +398,7 @@ const MechRecCostProdPlans = () => {
<Grid container> <Grid container>
<Grid item xs={12}> <Grid item xs={12}>
{state.selectedPlanCtlgSpecsLoaded ? ( {state.selectedPlanCtlgSpecsLoaded ? (
state.selectedPlanCtlgSpecs.length === 0 ? ( state.gantt.tasks.length === 0 ? (
<Box pt={3}> <Box pt={3}>
<InlineMsgInfo <InlineMsgInfo
okBtn={false} okBtn={false}
@ -459,10 +456,9 @@ const MechRecCostProdPlans = () => {
) : null} ) : null}
<P8PGantt <P8PGantt
{...P8P_GANTT_CONFIG_PROPS} {...P8P_GANTT_CONFIG_PROPS}
{...state.selectedPlanCtlgGanttDef} {...state.gantt}
containerStyle={STYLES.GANTT_CONTAINER} containerStyle={STYLES.GANTT_CONTAINER}
titleStyle={STYLES.GANTT_TITLE} titleStyle={STYLES.GANTT_TITLE}
tasks={state.selectedPlanCtlgSpecs}
taskDialogRenderer={prms => taskDialogRenderer({ ...prms, handleTaskDetailOpen })} taskDialogRenderer={prms => taskDialogRenderer({ ...prms, handleTaskDetailOpen })}
/> />
</Box> </Box>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -57,11 +57,12 @@ const LabFactRptDtl = ({ periodId, title, onHide }) => {
}); });
setFactRptDtl(pv => ({ setFactRptDtl(pv => ({
...pv, ...pv,
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef, ...data.XDATA_GRID,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])], columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
dataLoaded: true, dataLoaded: true,
reload: false, reload: false,
morePages: (data.XROWS || []).length >= configSystemPageSize morePages: (data.XDATA_GRID.rows || []).length >= configSystemPageSize
})); }));
} }
}, [ }, [

View File

@ -56,11 +56,12 @@ const LabPlanFOTDtl = ({ periodId, title, onHide }) => {
}); });
setPlanFOTDtl(pv => ({ setPlanFOTDtl(pv => ({
...pv, ...pv,
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef, ...data.XDATA_GRID,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])], columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
dataLoaded: true, dataLoaded: true,
reload: false, reload: false,
morePages: (data.XROWS || []).length >= configSystemPageSize morePages: (data.XDATA_GRID.rows || []).length >= configSystemPageSize
})); }));
} }
}, [ }, [

View File

@ -61,11 +61,12 @@ const LabPlanJobsDtl = ({ periodId, title, onHide, onProjectClick }) => {
}); });
setPlanJobsDtl(pv => ({ setPlanJobsDtl(pv => ({
...pv, ...pv,
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef, ...data.XDATA_GRID,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])], columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
dataLoaded: true, dataLoaded: true,
reload: false, reload: false,
morePages: (data.XROWS || []).length >= configSystemPageSize morePages: (data.XDATA_GRID.rows || []).length >= configSystemPageSize
})); }));
} }
}, [ }, [

View File

@ -265,8 +265,7 @@ const PrjJobs = () => {
selectedProjectJobsLoaded: false, selectedProjectJobsLoaded: false,
selectedProject: null, selectedProject: null,
selectedProjectDocRn: null, selectedProjectDocRn: null,
selectedProjectGanttDef: {}, gantt: {},
selectedProjectTasks: [],
showInitDialog: false showInitDialog: false
}); });
@ -308,8 +307,9 @@ const PrjJobs = () => {
setState(pv => ({ setState(pv => ({
...pv, ...pv,
selectedProjectJobsLoaded: true, selectedProjectJobsLoaded: true,
selectedProjectGanttDef: tasksOnly === true ? { ...pv.selectedProjectGanttDef } : data.XGANTT_DEF ? { ...data.XGANTT_DEF } : {}, gantt: {
selectedProjectTasks: [...data.XGANTT_TASKS] ...(tasksOnly === true ? { ...pv.gantt, tasks: [...data.XGANTT.tasks] } : data.XGANTT ? { ...data.XGANTT } : {})
}
})); }));
}, },
[executeStored, state.ident, state.selectedProject] [executeStored, state.ident, state.selectedProject]
@ -394,8 +394,7 @@ const PrjJobs = () => {
selectedProject: project, selectedProject: project,
selectedProjectDocRn: projectDocRn, selectedProjectDocRn: projectDocRn,
selectedProjectJobsLoaded: false, selectedProjectJobsLoaded: false,
selectedProjectTasks: [], gantt: {},
selectedProjectGanttDef: {},
showProjectsList: false showProjectsList: false
})); }));
}; };
@ -407,8 +406,7 @@ const PrjJobs = () => {
selectedProjectJobsLoaded: false, selectedProjectJobsLoaded: false,
selectedProject: null, selectedProject: null,
selectedProjectDocRn: null, selectedProjectDocRn: null,
selectedProjectTasks: [], gantt: {},
selectedProjectGanttDef: {},
showProjectsList: false showProjectsList: false
})); }));
@ -515,11 +513,10 @@ const PrjJobs = () => {
{state.selectedProjectJobsLoaded ? ( {state.selectedProjectJobsLoaded ? (
<P8PGantt <P8PGantt
{...P8P_GANTT_CONFIG_PROPS} {...P8P_GANTT_CONFIG_PROPS}
{...state.selectedProjectGanttDef} {...state.gantt}
containerStyle={STYLES.GANTT_CONTAINER} containerStyle={STYLES.GANTT_CONTAINER}
titleStyle={STYLES.GANTT_TITLE} titleStyle={STYLES.GANTT_TITLE}
onTitleClick={handleTitleClick} onTitleClick={handleTitleClick}
tasks={state.selectedProjectTasks}
onTaskDatesChange={handleTaskDatesChange} onTaskDatesChange={handleTaskDatesChange}
taskAttributeRenderer={taskAttributeRenderer} taskAttributeRenderer={taskAttributeRenderer}
/> />

View File

@ -79,11 +79,12 @@ const ResMon = ({ ident, onPlanJobsDtlProjectClick }) => {
}); });
setPeriods(pv => ({ setPeriods(pv => ({
...pv, ...pv,
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef, ...data.XDATA_GRID,
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])], columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef,
rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...pv.rows, ...(data.XDATA_GRID.rows || [])],
dataLoaded: true, dataLoaded: true,
reload: false, reload: false,
morePages: (data.XROWS || []).length >= configSystemPageSize morePages: (data.XDATA_GRID.rows || []).length >= configSystemPageSize
})); }));
} }
}, [ident, peridos.reload, peridos.orders, peridos.dataLoaded, peridos.pageNumber, executeStored, configSystemPageSize, SERV_DATA_TYPE_CLOB]); }, [ident, peridos.reload, peridos.orders, peridos.dataLoaded, peridos.pageNumber, executeStored, configSystemPageSize, SERV_DATA_TYPE_CLOB]);

View File

@ -97,16 +97,15 @@ const useConf = (currentTab, handleSectionChange) => {
sections.map(s => { sections.map(s => {
let dg = {}; let dg = {};
Object.assign(dg, dataGrid, { Object.assign(dg, dataGrid, {
...s.XDATA.XDATA_GRID,
rn: s.NRN, rn: s.NRN,
code: s.SCODE, code: s.SCODE,
name: s.SNAME, name: s.SNAME,
delete_allow: s.NDELETE_ALLOW, delete_allow: s.NDELETE_ALLOW,
dataLoaded: true, dataLoaded: true,
columnsDef: [...(s.XDATA.XCOLUMNS_DEF || [])], columnsDef: [...(s.XDATA.XDATA_GRID.columnsDef || [])],
groups: [...(s.XDATA.XGROUPS || [])], groups: [...(s.XDATA.XDATA_GRID.groups || [])],
rows: [...(s.XDATA.XROWS || [])], rows: [...(s.XDATA.XDATA_GRID.rows || [])],
fixedHeader: s.XDATA.XDATA_GRID.fixedHeader,
fixedColumns: s.XDATA.XDATA_GRID.fixedColumns,
reload: false reload: false
}); });
//Если раздел имеет составы показателей //Если раздел имеет составы показателей

View File

@ -33,7 +33,7 @@ const STYLES = {
//Пример: Графики "P8PChart" //Пример: Графики "P8PChart"
const Chart = ({ title }) => { const Chart = ({ title }) => {
//Собственное состояние - график //Собственное состояние - график
const [chart, setChart] = useState({ loaded: false, labels: [], datasets: [] }); const [chart, setChart] = useState({ loaded: false });
//Подключение к контексту взаимодействия с сервером //Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx); const { executeStored } = useContext(BackEndСtx);

View File

@ -0,0 +1,301 @@
/*
Парус 8 - Панели мониторинга - Примеры для разработчиков
Пример: Циклограмма "P8PCyclogram"
*/
//---------------------
//Подключение библиотек
//---------------------
import React, { useState, useContext, useCallback, useEffect } from "react"; //Классы React
import PropTypes from "prop-types"; //Контроль свойств компонента
import {
Typography,
Grid,
Button,
Box,
DialogContent,
List,
ListItem,
ListItemText,
Divider,
TextField,
DialogActions,
Stack,
Icon
} from "@mui/material"; //Интерфейсные элементы
import { formatDateJSONDateOnly, formatDateRF } from "../../core/utils"; //Вспомогательные функции
import { APP_BAR_HEIGHT } from "../../components/p8p_app_workspace"; //Заголовок страницы
import { P8PCyclogram } from "../../components/p8p_cyclogram"; //Циклограмма
import { P8P_CYCLOGRAM_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
//---------
//Константы
//---------
//Отступ контейнера страницы от заголовка
const CONTAINER_PADDING_TOP = "20px";
//Высота заголовка страницы
const TITLE_HEIGHT = "47px";
//Высота строк
const LINE_HEIGHT = 30;
//Стили
const STYLES = {
CONTAINER: { textAlign: "center", paddingTop: CONTAINER_PADDING_TOP },
TITLE: { paddingBottom: "15px", height: TITLE_HEIGHT },
CYCLOGRAM_CONTAINER: {
height: `calc(100vh - ${APP_BAR_HEIGHT} - ${TITLE_HEIGHT} - ${CONTAINER_PADDING_TOP})`,
width: "100vw",
paddingTop: "5px"
},
TASK_EDITOR_CONTENT: { minWidth: 400, overflowX: "auto" },
TASK_EDITOR_LIST: { width: "100%", minWidth: 300, maxWidth: 700, bgcolor: "background.paper" },
GROUP_HEADER: height => ({
border: "1px solid",
backgroundColor: "#ecf8fb",
height: height,
borderRadius: "10px",
display: "flex",
alignItems: "center",
justifyContent: "space-around"
})
};
//---------------------------------------------
//Вспомогательные функции форматирования данных
//---------------------------------------------
//Диалог открытия задачи
const CustomTaskDialog = ({ task, ident, handleReload, close }) => {
//Собственное состояние
const [taskDates, setTaskDates] = useState({ start: task.ddate_start, end: task.ddate_end });
//Тип проекта
const textType = task.type === 0 ? "Задачи проекта" : task.type === 1 ? "Этап проекта" : "Работа проекта";
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//Изменение дат задачи
const changeDates = useCallback(async () => {
//Изменяем даты задачи
await executeStored({
stored: "PKG_P8PANELS_SAMPLES.CYCLOGRAM_TASK_MODIFY",
args: {
NIDENT: ident,
NRN: task.rn,
SDATE_FROM: formatDateRF(taskDates.start),
SDATE_TO: formatDateRF(taskDates.end)
}
});
handleReload();
close();
}, [close, executeStored, handleReload, ident, task.rn, taskDates.end, taskDates.start]);
//При нажатии OK
const handleOk = () => {
//Изменяем даты задачи
changeDates();
};
return (
<>
<DialogContent sx={STYLES.TASK_EDITOR_CONTENT}>
<List sx={STYLES.TASK_EDITOR_LIST}>
<ListItem alignItems="flex-start">
<ListItemText primary={"Наименование"} secondary={task.fullName} />
</ListItem>
<Divider component="li" />
<ListItem alignItems="flex-start">
<ListItemText
secondaryTypographyProps={{ component: "span" }}
primary={"Начало"}
secondary={
<TextField
error={!taskDates.start}
disabled={task.type !== 2}
name="start"
fullWidth
required
InputLabelProps={{ shrink: true }}
type={"date"}
value={taskDates.start}
onChange={e => setTaskDates(pv => ({ ...pv, start: e.target.value }))}
variant="standard"
size="small"
margin="normal"
></TextField>
}
/>
</ListItem>
<Divider component="li" />
<ListItem alignItems="flex-start">
<ListItemText
secondaryTypographyProps={{ component: "span" }}
primary={"Окончание"}
secondary={
<TextField
error={!taskDates.end}
disabled={task.type !== 2}
name="end"
fullWidth
required
InputLabelProps={{ shrink: true }}
type={"date"}
value={taskDates.end}
onChange={e => setTaskDates(pv => ({ ...pv, end: e.target.value }))}
variant="standard"
size="small"
margin="normal"
></TextField>
}
/>
</ListItem>
<Divider component="li" />
<ListItem alignItems="flex-start">
<ListItemText
primary={"Тип"}
secondaryTypographyProps={{ component: "span" }}
secondary={
<Stack direction="row" gap={0.5}>
<Icon title={textType}>{task.type === 0 ? "description" : task.type === 1 ? "check" : "work_outline"}</Icon>
{textType}
</Stack>
}
/>
</ListItem>
</List>
</DialogContent>
<DialogActions>
<Button onClick={handleOk}>ОК</Button>
<Button onClick={close}>Отмена</Button>
</DialogActions>
</>
);
};
//Контроль свойств - Диалог открытия задачи
CustomTaskDialog.propTypes = {
task: PropTypes.object.isRequired,
ident: PropTypes.number.isRequired,
handleReload: PropTypes.func.isRequired,
close: PropTypes.func.isRequired
};
//Заголовок группы
const CustomGroupHeader = ({ group }) => {
return (
<Box sx={STYLES.GROUP_HEADER(group.height)}>
<Typography variant="body2">{group.name}</Typography>
</Box>
);
};
//Контроль свойств - Заголовок группы
CustomGroupHeader.propTypes = {
group: PropTypes.object.isRequired
};
//Отображение задачи
const taskRenderer = ({ task }) => {
//Если это задачи проекта
if (task.type === 0) {
return {
taskStyle: { border: "3px solid #ebe058" }
};
}
};
//-----------
//Тело модуля
//-----------
//Пример: Циклограмма "P8PCyclogram"
const Cyclogram = ({ title }) => {
//Собственное состояние
const [state, setState] = useState({
init: false,
dataLoaded: false,
reload: true,
ident: null
});
//Подключение к контексту взаимодействия с сервером
const { executeStored } = useContext(BackEndСtx);
//При необходимости перезагрузки
const handleReload = () => {
setState(pv => ({ ...pv, reload: true }));
};
//При необходимости обновить данные таблицы
useEffect(() => {
//Загрузка данных циклограммы с сервера
const loadData = async () => {
const data = await executeStored({
stored: "PKG_P8PANELS_SAMPLES.CYCLOGRAM",
args: { NIDENT: state.ident },
attributeValueProcessor: (name, val) => (["ddate_start", "ddate_end"].includes(name) ? formatDateJSONDateOnly(val) : val),
respArg: "COUT"
});
setState(pv => ({ ...pv, dataLoaded: true, ...data.XCYCLOGRAM, reload: false }));
};
//Если указан идентификатор и требуется перезагрузить
if (state.ident && state.reload) loadData();
}, [state.ident, state.reload, executeStored]);
//При подключении компонента к странице
useEffect(() => {
//Инициализация данных циклограммы
const initData = async () => {
const data = await executeStored({ stored: "PKG_P8PANELS_SAMPLES.CYCLOGRAM_INIT", args: { NIDENT: state.ident } });
setState(pv => ({ ...pv, init: true, ident: data.NIDENT, reload: true }));
};
//Если требуется проинициализировать
if (!state.init) {
initData();
}
}, [executeStored, state.ident, state.init]);
return (
<Box>
<div style={STYLES.CONTAINER}>
<Typography sx={STYLES.TITLE} variant={"h6"}>
{title}
</Typography>
<Grid container direction="column" alignItems="center">
<Grid item xs={12}>
{state.dataLoaded ? (
<P8PCyclogram
{...P8P_CYCLOGRAM_CONFIG_PROPS}
{...state}
containerStyle={STYLES.CYCLOGRAM_CONTAINER}
lineHeight={LINE_HEIGHT}
taskDialogRenderer={prms => (
<CustomTaskDialog task={prms.task} ident={state.ident} handleReload={handleReload} close={prms.close} />
)}
taskRenderer={prms => taskRenderer(prms)}
groupHeaderRenderer={prms => <CustomGroupHeader group={prms.group} />}
/>
) : null}
</Grid>
</Grid>
</div>
</Box>
);
};
//Контроль свойств - Пример: Циклограмма "P8PCyclogram"
Cyclogram.propTypes = {
title: PropTypes.string.isRequired
};
//----------------
//Интерфейс модуля
//----------------
export { Cyclogram };

View File

@ -15,6 +15,7 @@ import { P8PDataGrid, P8P_DATA_GRID_SIZE } from "../../components/p8p_data_grid"
import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения import { P8P_DATA_GRID_CONFIG_PROPS } from "../../config_wrapper"; //Подключение компонентов к настройкам приложения
import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером import { BackEndСtx } from "../../context/backend"; //Контекст взаимодействия с сервером
import { ApplicationСtx } from "../../context/application"; //Контекст приложения import { ApplicationСtx } from "../../context/application"; //Контекст приложения
import { APP_STYLES } from "../../../app.styles"; //Типовые стили
//--------- //---------
//Константы //Константы
@ -27,7 +28,7 @@ const DATA_GRID_PAGE_SIZE = 5;
const STYLES = { const STYLES = {
CONTAINER: { textAlign: "center", paddingTop: "20px" }, CONTAINER: { textAlign: "center", paddingTop: "20px" },
TITLE: { paddingBottom: "15px" }, TITLE: { paddingBottom: "15px" },
DATA_GRID_CONTAINER: { maxWidth: 700, maxHeight: 500, minHeight: 500 } DATA_GRID_CONTAINER: { maxWidth: 700, maxHeight: 500, minHeight: 500, ...APP_STYLES.SCROLL }
}; };
//--------------------------------------------- //---------------------------------------------
@ -86,18 +87,14 @@ export const groupCellRender = () => ({ cellStyle: { padding: "2px" } });
//Пример: Таблица данных "P8PDataGrid" //Пример: Таблица данных "P8PDataGrid"
const DataGrid = ({ title }) => { const DataGrid = ({ title }) => {
//Собственное состояние - таблица данных //Собственное состояние - таблица данных
const [dataGrid, setdataGrid] = useState({ const [dataGrid, setDataGrid] = useState({
dataLoaded: false, dataLoaded: false,
columnsDef: [],
filters: null, filters: null,
orders: null, orders: null,
groups: [],
rows: [],
reload: true,
pageNumber: 1, pageNumber: 1,
morePages: true, morePages: true,
fixedHeader: false, expandable: true,
fixedColumns: 0 reloading: true
}); });
//Подключение к контексту взаимодействия с сервером //Подключение к контексту взаимодействия с сервером
@ -108,7 +105,7 @@ const DataGrid = ({ title }) => {
//Загрузка данных таблицы с сервера //Загрузка данных таблицы с сервера
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
if (dataGrid.reload) { if (dataGrid.reloading) {
const data = await executeStored({ const data = await executeStored({
stored: "PKG_P8PANELS_SAMPLES.DATA_GRID", stored: "PKG_P8PANELS_SAMPLES.DATA_GRID",
args: { args: {
@ -120,32 +117,31 @@ const DataGrid = ({ title }) => {
}, },
respArg: "COUT" respArg: "COUT"
}); });
setdataGrid(pv => ({ setDataGrid(pv => ({
...pv, ...pv,
fixedHeader: data.XDATA_GRID.fixedHeader, ...data.XDATA_GRID,
fixedColumns: data.XDATA_GRID.fixedColumns, columnsDef: data.XDATA_GRID.columnsDef ? [...data.XDATA_GRID.columnsDef] : pv.columnsDef || [],
columnsDef: data.XCOLUMNS_DEF ? [...data.XCOLUMNS_DEF] : pv.columnsDef, rows: pv.pageNumber == 1 ? [...(data.XDATA_GRID.rows || [])] : [...(pv.rows || []), ...(data.XDATA_GRID.rows || [])],
rows: pv.pageNumber == 1 ? [...(data.XROWS || [])] : [...pv.rows, ...(data.XROWS || [])], groups: data.XDATA_GRID.groups
groups: data.XGROUPS
? pv.pageNumber == 1 ? pv.pageNumber == 1
? [...data.XGROUPS] ? [...data.XDATA_GRID.groups]
: [...pv.groups, ...data.XGROUPS.filter(g => !pv.groups.find(pg => pg.name == g.name))] : [...(pv.groups || []), ...data.XDATA_GRID.groups.filter(g => !pv.groups.find(pg => pg.name == g.name))]
: [...pv.groups], : [...(pv.groups || [])],
dataLoaded: true, dataLoaded: true,
reload: false, reloading: false,
morePages: (data.XROWS || []).length >= DATA_GRID_PAGE_SIZE morePages: (data.XDATA_GRID.rows || []).length >= DATA_GRID_PAGE_SIZE
})); }));
} }
}, [dataGrid.reload, dataGrid.filters, dataGrid.orders, dataGrid.dataLoaded, dataGrid.pageNumber, executeStored, SERV_DATA_TYPE_CLOB]); }, [dataGrid.reloading, dataGrid.filters, dataGrid.orders, dataGrid.dataLoaded, dataGrid.pageNumber, executeStored, SERV_DATA_TYPE_CLOB]);
//При изменении состояния фильтра //При изменении состояния фильтра
const handleFilterChanged = ({ filters }) => setdataGrid(pv => ({ ...pv, filters: [...filters], pageNumber: 1, reload: true })); const handleFilterChanged = ({ filters }) => setDataGrid(pv => ({ ...pv, filters: [...filters], pageNumber: 1, reloading: true }));
//При изменении состояния сортировки //При изменении состояния сортировки
const handleOrderChanged = ({ orders }) => setdataGrid(pv => ({ ...pv, orders: [...orders], pageNumber: 1, reload: true })); const handleOrderChanged = ({ orders }) => setDataGrid(pv => ({ ...pv, orders: [...orders], pageNumber: 1, reloading: true }));
//При изменении количества отображаемых страниц //При изменении количества отображаемых страниц
const handlePagesCountChanged = () => setdataGrid(pv => ({ ...pv, pageNumber: pv.pageNumber + 1, reload: true })); const handlePagesCountChanged = () => setDataGrid(pv => ({ ...pv, pageNumber: pv.pageNumber + 1, reloading: true }));
//При нажатии на копку контрагента //При нажатии на копку контрагента
const handleAgnButtonClicked = agnCode => pOnlineShowDocument({ unitCode: "AGNLIST", document: agnCode, inRnParameter: "in_AGNABBR" }); const handleAgnButtonClicked = agnCode => pOnlineShowDocument({ unitCode: "AGNLIST", document: agnCode, inRnParameter: "in_AGNABBR" });
@ -153,7 +149,7 @@ const DataGrid = ({ title }) => {
//При необходимости обновить данные таблицы //При необходимости обновить данные таблицы
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, [dataGrid.reload, loadData]); }, [dataGrid.reloading, loadData]);
//Генерация содержимого //Генерация содержимого
return ( return (
@ -167,16 +163,9 @@ const DataGrid = ({ title }) => {
{dataGrid.dataLoaded ? ( {dataGrid.dataLoaded ? (
<P8PDataGrid <P8PDataGrid
{...P8P_DATA_GRID_CONFIG_PROPS} {...P8P_DATA_GRID_CONFIG_PROPS}
containerComponentProps={{ elevation: 6, style: STYLES.DATA_GRID_CONTAINER }} {...dataGrid}
columnsDef={dataGrid.columnsDef}
groups={dataGrid.groups}
rows={dataGrid.rows}
size={P8P_DATA_GRID_SIZE.LARGE} size={P8P_DATA_GRID_SIZE.LARGE}
fixedHeader={dataGrid.fixedHeader} containerComponentProps={{ elevation: 6, sx: STYLES.DATA_GRID_CONTAINER }}
fixedColumns={dataGrid.fixedColumns}
filtersInitial={dataGrid.filters}
morePages={dataGrid.morePages}
reloading={dataGrid.reload}
valueFormatter={valueFormatter} valueFormatter={valueFormatter}
headCellRender={headCellRender} headCellRender={headCellRender}
dataCellRender={dataCellRender} dataCellRender={dataCellRender}
@ -184,7 +173,6 @@ const DataGrid = ({ title }) => {
onOrderChanged={handleOrderChanged} onOrderChanged={handleOrderChanged}
onFilterChanged={handleFilterChanged} onFilterChanged={handleFilterChanged}
onPagesCountChanged={handlePagesCountChanged} onPagesCountChanged={handlePagesCountChanged}
expandable={true}
rowExpandRender={({ row }) => ( rowExpandRender={({ row }) => (
<Button onClick={() => handleAgnButtonClicked(row.SAGNABBR)}>Показать в разделе</Button> <Button onClick={() => handleAgnButtonClicked(row.SAGNABBR)}>Показать в разделе</Button>
)} )}

View File

@ -97,12 +97,10 @@ const taskDialogRenderer = ({ task, close }) => {
//Пример: Диаграмма Ганта "P8Gantt" //Пример: Диаграмма Ганта "P8Gantt"
const Gantt = ({ title }) => { const Gantt = ({ title }) => {
//Собственное состояние //Собственное состояние
const [state, setState] = useState({ const [gantt, setGantt] = useState({
init: false, init: false,
dataLoaded: false, dataLoaded: false,
ident: null, ident: null,
ganttDef: {},
ganttTasks: [],
useCustomTaskDialog: false useCustomTaskDialog: false
}); });
@ -113,21 +111,21 @@ const Gantt = ({ title }) => {
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
const data = await executeStored({ const data = await executeStored({
stored: "PKG_P8PANELS_SAMPLES.GANTT", stored: "PKG_P8PANELS_SAMPLES.GANTT",
args: { NIDENT: state.ident }, args: { NIDENT: gantt.ident },
attributeValueProcessor: (name, val) => attributeValueProcessor: (name, val) =>
name == "numb" ? undefined : ["start", "end"].includes(name) ? formatDateJSONDateOnly(val) : val, name == "numb" ? undefined : ["start", "end"].includes(name) ? formatDateJSONDateOnly(val) : val,
respArg: "COUT" respArg: "COUT"
}); });
setState(pv => ({ ...pv, dataLoaded: true, ganttDef: { ...data.XGANTT_DEF }, ganttTasks: [...data.XGANTT_TASKS] })); setGantt(pv => ({ ...pv, dataLoaded: true, ...data.XGANTT }));
}, [state.ident, executeStored]); }, [gantt.ident, executeStored]);
//Инициализация данных диаграммы //Инициализация данных диаграммы
const initData = useCallback(async () => { const initData = useCallback(async () => {
if (!state.init) { if (!gantt.init) {
const data = await executeStored({ stored: "PKG_P8PANELS_SAMPLES.GANTT_INIT", args: { NIDENT: state.ident } }); const data = await executeStored({ stored: "PKG_P8PANELS_SAMPLES.GANTT_INIT", args: { NIDENT: gantt.ident } });
setState(pv => ({ ...pv, init: true, ident: data.NIDENT })); setGantt(pv => ({ ...pv, init: true, ident: data.NIDENT }));
} }
}, [state.init, state.ident, executeStored]); }, [gantt.init, gantt.ident, executeStored]);
//Изменение данных диаграммы //Изменение данных диаграммы
const modifyData = useCallback( const modifyData = useCallback(
@ -135,13 +133,13 @@ const Gantt = ({ title }) => {
try { try {
await executeStored({ await executeStored({
stored: "PKG_P8PANELS_SAMPLES.GANTT_MODIFY", stored: "PKG_P8PANELS_SAMPLES.GANTT_MODIFY",
args: { NIDENT: state.ident, NRN: rn, DDATE_FROM: new Date(start), DDATE_TO: new Date(end) } args: { NIDENT: gantt.ident, NRN: rn, DDATE_FROM: new Date(start), DDATE_TO: new Date(end) }
}); });
} finally { } finally {
loadData(); loadData();
} }
}, },
[state.ident, executeStored, loadData] [gantt.ident, executeStored, loadData]
); );
//Обработка измненения сроков задачи в диаграмме Гантта //Обработка измненения сроков задачи в диаграмме Гантта
@ -151,8 +149,8 @@ const Gantt = ({ title }) => {
//При необходимости обновить данные таблицы //При необходимости обновить данные таблицы
useEffect(() => { useEffect(() => {
if (state.ident) loadData(); if (gantt.ident) loadData();
}, [state.ident, loadData]); }, [gantt.ident, loadData]);
//При подключении компонента к странице //При подключении компонента к странице
useEffect(() => { useEffect(() => {
@ -168,20 +166,19 @@ const Gantt = ({ title }) => {
</Typography> </Typography>
<FormControlLabel <FormControlLabel
sx={STYLES.CONTROL} sx={STYLES.CONTROL}
control={<Checkbox onChange={() => setState(pv => ({ ...pv, useCustomTaskDialog: !pv.useCustomTaskDialog }))} />} control={<Checkbox onChange={() => setGantt(pv => ({ ...pv, useCustomTaskDialog: !pv.useCustomTaskDialog }))} />}
label="Отображать пользовательский диалог задачи" label="Отображать пользовательский диалог задачи"
/> />
<Grid container direction="column" alignItems="center"> <Grid container direction="column" alignItems="center">
<Grid item xs={12}> <Grid item xs={12}>
{state.dataLoaded ? ( {gantt.dataLoaded ? (
<P8PGantt <P8PGantt
{...P8P_GANTT_CONFIG_PROPS} {...P8P_GANTT_CONFIG_PROPS}
{...state.ganttDef} {...gantt}
containerStyle={STYLES.GANTT_CONTAINER} containerStyle={STYLES.GANTT_CONTAINER}
tasks={state.ganttTasks}
onTaskDatesChange={handleTaskDatesChange} onTaskDatesChange={handleTaskDatesChange}
taskAttributeRenderer={taskAttributeRenderer} taskAttributeRenderer={taskAttributeRenderer}
taskDialogRenderer={state.useCustomTaskDialog ? taskDialogRenderer : null} taskDialogRenderer={gantt.useCustomTaskDialog ? taskDialogRenderer : null}
/> />
) : null} ) : null}
</Grid> </Grid>

View File

@ -18,6 +18,7 @@ import { DataGrid } from "./data_grid"; //Пример: Таблица данн
import { Chart } from "./chart"; //Пример: Графики "P8PChart" import { Chart } from "./chart"; //Пример: Графики "P8PChart"
import { Gantt } from "./gantt"; //Пример: Диаграмма Ганта "P8PGantt" import { Gantt } from "./gantt"; //Пример: Диаграмма Ганта "P8PGantt"
import { Svg } from "./svg"; //Пример: Интерактивные изображения "P8PSVG" import { Svg } from "./svg"; //Пример: Интерактивные изображения "P8PSVG"
import { Cyclogram } from "./cyclogram"; //Пример: Циклограмма "P8PCyclogram"
//--------- //---------
//Константы //Константы
@ -32,7 +33,8 @@ const MODES = {
DATAGRID: { name: "DATAGRID", caption: 'Таблица данных "P8PDataGrid"', component: DataGrid }, DATAGRID: { name: "DATAGRID", caption: 'Таблица данных "P8PDataGrid"', component: DataGrid },
CHART: { name: "CHART", caption: 'Графики "P8PChart"', component: Chart }, CHART: { name: "CHART", caption: 'Графики "P8PChart"', component: Chart },
GANTT: { name: "GANTT", caption: 'Диаграмма Ганта "P8PGantt"', component: Gantt }, GANTT: { name: "GANTT", caption: 'Диаграмма Ганта "P8PGantt"', component: Gantt },
SVG: { name: "SVG", caption: 'Интерактивные изображения "P8PSVG"', component: Svg } SVG: { name: "SVG", caption: 'Интерактивные изображения "P8PSVG"', component: Svg },
CYCLOGRAM: { name: "CYCLOGRAM", caption: 'Циклограмма "P8PCyclogram"', component: Cyclogram }
}; };
//Стили //Стили

View File

@ -19,8 +19,8 @@ create table P8PNL_JB_JOBS
EDITABLE number(1) default 0 not null, -- Признак возможности редактирования (0 - нет, 1 - да) EDITABLE number(1) default 0 not null, -- Признак возможности редактирования (0 - нет, 1 - да)
CHANGED number(1) default 0 not null, -- Признак наличия изменений, требующих сохранения (0 - нет, 1 - да) CHANGED number(1) default 0 not null, -- Признак наличия изменений, требующих сохранения (0 - нет, 1 - да)
constraint C_P8PNL_JB_JOBS_RN_PK primary key (RN), constraint C_P8PNL_JB_JOBS_RN_PK primary key (RN),
constraint C_P8PNL_JB_JOBS_PRN_FK foreign key (PRN) references P8PNL_JB_PRJCTS (RN), constraint C_P8PNL_JB_JOBS_PRN_FK foreign key (PRN) references P8PNL_JB_PRJCTS (RN) on delete cascade,
constraint C_P8PNL_JB_JOBS_HRN_FK foreign key (HRN) references P8PNL_JB_JOBS (RN), constraint C_P8PNL_JB_JOBS_HRN_FK foreign key (HRN) references P8PNL_JB_JOBS (RN) on delete cascade,
constraint C_P8PNL_JB_JOBS_STAGE_VAL check (STAGE in (0, 1)), constraint C_P8PNL_JB_JOBS_STAGE_VAL check (STAGE in (0, 1)),
constraint C_P8PNL_JB_JOBS_EDTBL_VAL check (EDITABLE in (0, 1)), constraint C_P8PNL_JB_JOBS_EDTBL_VAL check (EDITABLE in (0, 1)),
constraint C_P8PNL_JB_JOBS_CHNGD_VAL check (CHANGED in (0, 1)), constraint C_P8PNL_JB_JOBS_CHNGD_VAL check (CHANGED in (0, 1)),

View File

@ -9,7 +9,7 @@ create table P8PNL_JB_JOBSPREV
PRN number(17) not null, -- Рег. номер родителя PRN number(17) not null, -- Рег. номер родителя
JB_JOBS number(17) not null, -- Рег. номер предшествующей работы/этапа JB_JOBS number(17) not null, -- Рег. номер предшествующей работы/этапа
constraint C_P8PNL_JB_JOBSPREV_RN_PK primary key (RN), constraint C_P8PNL_JB_JOBSPREV_RN_PK primary key (RN),
constraint C_P8PNL_JB_JOBSPREV_PRN_FK foreign key (PRN) references P8PNL_JB_JOBS (RN), constraint C_P8PNL_JB_JOBSPREV_PRN_FK foreign key (PRN) references P8PNL_JB_JOBS (RN) on delete cascade,
constraint C_P8PNL_JB_JOBSPREV_JB_JOBS_FK foreign key (JB_JOBS) references P8PNL_JB_JOBS (RN), constraint C_P8PNL_JB_JOBSPREV_JB_JOBS_FK foreign key (JB_JOBS) references P8PNL_JB_JOBS (RN) on delete cascade,
constraint C_P8PNL_JB_JOBSPREV_UN unique (IDENT, PRN, JB_JOBS) constraint C_P8PNL_JB_JOBSPREV_UN unique (IDENT, PRN, JB_JOBS)
); );

View File

@ -15,7 +15,7 @@ create table P8PNL_JB_PERIODS
LAB_PLAN_JOBS number(17,3) default 0 not null, -- Трудоёмкость (план, по графику) LAB_PLAN_JOBS number(17,3) default 0 not null, -- Трудоёмкость (план, по графику)
constraint C_P8PNL_JB_PERIODS_RN_PK primary key (RN), constraint C_P8PNL_JB_PERIODS_RN_PK primary key (RN),
constraint C_P8PNL_JB_PERIODS_DATE_VAL check (DATE_FROM <= DATE_TO), constraint C_P8PNL_JB_PERIODS_DATE_VAL check (DATE_FROM <= DATE_TO),
constraint C_P8PNL_JB_PERIODS_INS_DEP_FK foreign key (INS_DEPARTMENT) references INS_DEPARTMENT (RN), constraint C_P8PNL_JB_PERIODS_INS_DEP_FK foreign key (INS_DEPARTMENT) references INS_DEPARTMENT (RN) on delete cascade,
constraint C_P8PNL_JB_PERIODS_FCMNPWR_FK foreign key (FCMANPOWER) references FCMANPOWER (RN), constraint C_P8PNL_JB_PERIODS_FCMNPWR_FK foreign key (FCMANPOWER) references FCMANPOWER (RN) on delete cascade,
constraint C_P8PNL_JB_PERIODS_UN unique (IDENT, DATE_FROM, INS_DEPARTMENT, FCMANPOWER) constraint C_P8PNL_JB_PERIODS_UN unique (IDENT, DATE_FROM, INS_DEPARTMENT, FCMANPOWER)
); );

View File

@ -11,7 +11,7 @@ create table P8PNL_JB_PRJCTS
EDITABLE number(1) default 0 not null, -- Признак возможности редактирования (0 - нет, 1 - да) EDITABLE number(1) default 0 not null, -- Признак возможности редактирования (0 - нет, 1 - да)
CHANGED number(1) default 0 not null, -- Признак наличия изменений, требующих сохранения (0 - нет, 1 - да) CHANGED number(1) default 0 not null, -- Признак наличия изменений, требующих сохранения (0 - нет, 1 - да)
constraint C_P8PNL_JB_PRJCTS_RN_PK primary key (RN), constraint C_P8PNL_JB_PRJCTS_RN_PK primary key (RN),
constraint C_P8PNL_JB_PRJCTS_PROJECT_FK foreign key (PROJECT) references PROJECT (RN), constraint C_P8PNL_JB_PRJCTS_PROJECT_FK foreign key (PROJECT) references PROJECT (RN) on delete cascade,
constraint C_P8PNL_JB_PRJCTS_JOBS_VAL check (JOBS in (0, 1)), constraint C_P8PNL_JB_PRJCTS_JOBS_VAL check (JOBS in (0, 1)),
constraint C_P8PNL_JB_PRJCTS_EDTBL_VAL check (EDITABLE in (0, 1)), constraint C_P8PNL_JB_PRJCTS_EDTBL_VAL check (EDITABLE in (0, 1)),
constraint C_P8PNL_JB_PRJCTS_CHNGD_VAL check (CHANGED in (0, 1)), constraint C_P8PNL_JB_PRJCTS_CHNGD_VAL check (CHANGED in (0, 1)),

View File

@ -0,0 +1,18 @@
/*
Парус 8 - Панели мониторинга - Примеры
Буфер для циклограммы
*/
create table P8PNL_SMPL_CYCLOGRAM
(
RN number(17) not null, -- Рег. номер записи
IDENT number(17) not null, -- Идентификатор процесса
TYPE number(1) not null, -- Тип (0 - колонка, 1 - группа, 2 - задача)
NAME varchar2(200) not null, -- Наименование
POS_START number(17) default null, -- Начальная позиция на циклограмме
POS_END number(17) default null, -- Конечная позиция на циклограмме
DATE_FROM date default null, -- Дата начала
DATE_TO date default null, -- Дата окончания
TASK_GROUP number(17) default null, -- Группа задач
constraint C_P8PNL_SMPL_CYCLOGRAM_RN_PK primary key (RN),
constraint C_P8PNL_SMPL_CYCLOGRAM_TP_VAL check (TYPE in (0, 1, 2))
);

View File

@ -27,6 +27,13 @@ create or replace package PKG_P8PANELS_BASE as
NDOCUMENT in number -- Рег. номер документа NDOCUMENT in number -- Рег. номер документа
) return number; -- Флаг доступности (см. константы NACCESS_*) ) return number; -- Флаг доступности (см. константы NACCESS_*)
/* Подготовка пользовательской строки поиска для вставки в запрос */
procedure UTL_SEARCH_PREPARE
(
SSEARCH in varchar2, -- Пользовательская строка поиска
SSEARCH_PREPARED out varchar2 -- Подготовленная строка поиска
);
/* Базовое исполнение действий */ /* Базовое исполнение действий */
procedure PROCESS procedure PROCESS
( (
@ -191,6 +198,36 @@ create or replace package body PKG_P8PANELS_BASE as
return NACCESS_NO; return NACCESS_NO;
end UTL_DOC_ACCESS_CHECK; end UTL_DOC_ACCESS_CHECK;
/* Подготовка пользовательской строки поиска для вставки в запрос */
procedure UTL_SEARCH_PREPARE
(
SSEARCH in varchar2, -- Пользовательская строка поиска
SSEARCH_PREPARED out varchar2 -- Подготовленная строка поиска
)
is
/* Локальные константы */
SANY_SYS constant char(1) := '%'; -- Маска "любое количество любых символов" Oracle
SONE_SYS constant char(1) := '_'; -- Маска "любой один символ" Oracle
SENT_WRD_DELIM constant varchar2(1) := ' '; -- Разделитель слов в предложении
SANY_PRS constant varchar2(240) := PKG_OPTIONS.STARSYMB; -- Маска "любое количество любых символов" Парус
SONE_PRS constant varchar2(240) := PKG_OPTIONS.QUESTSYMB; -- Маска "любой один символ" Парус
begin
/* Если пользовательская строка пустая - то это всё что угодно */
if (SSEARCH is null)
then
SSEARCH_PREPARED := SANY_SYS;
else
/* Подменим пользовательские маски на системные и соберем подготовленную строку поиска */
SSEARCH_PREPARED := '%' || replace(replace(replace(SSEARCH
,SANY_PRS
,SANY_SYS)
,SONE_PRS
,SONE_SYS)
,SENT_WRD_DELIM
,SANY_SYS) || '%';
end if;
end UTL_SEARCH_PREPARE;
/* Формирование сообщения об отсутствии значения */ /* Формирование сообщения об отсутствии значения */
function MSG_NO_DATA_MAKE function MSG_NO_DATA_MAKE
( (

View File

@ -227,10 +227,10 @@ create or replace package body PKG_P8PANELS_EQUIPSRV as
NCOMPANY PKG_STD.TREF := GET_SESSION_COMPANY(); -- Рег. номер организации NCOMPANY PKG_STD.TREF := GET_SESSION_COMPANY(); -- Рег. номер организации
SPRJ_GROUP_NAME PKG_STD.TSTRING; -- Наименование группы для проекта SPRJ_GROUP_NAME PKG_STD.TSTRING; -- Наименование группы для проекта
BEXPANDED boolean; -- Флаг раскрытости уровня BEXPANDED boolean; -- Флаг раскрытости уровня
RDG PKG_P8PANELS_VISUAL.TDATA_GRID; -- Описание таблицы RDG PKG_P8PANELS_VISUAL.TDG; -- Описание таблицы
RDG_ROW_INFO PKG_P8PANELS_VISUAL.TROW; -- Строка таблицы с информацией по объекту ремонта RDG_ROW_INFO PKG_P8PANELS_VISUAL.TDG_ROW; -- Строка таблицы с информацией по объекту ремонта
RDG_ROW_PLAN PKG_P8PANELS_VISUAL.TROW; -- Строка таблицы с планом по объекту ремонта RDG_ROW_PLAN PKG_P8PANELS_VISUAL.TDG_ROW; -- Строка таблицы с планом по объекту ремонта
RDG_ROW_FACT PKG_P8PANELS_VISUAL.TROW; -- Строка таблицы с фактом по объекту ремонта RDG_ROW_FACT PKG_P8PANELS_VISUAL.TDG_ROW; -- Строка таблицы с фактом по объекту ремонта
NCURYEAR PKG_STD.TNUMBER; -- Текущий год NCURYEAR PKG_STD.TNUMBER; -- Текущий год
NCURMONTH PKG_STD.TNUMBER; -- Текущий месяц NCURMONTH PKG_STD.TNUMBER; -- Текущий месяц
NTOTALDAYS PKG_STD.TNUMBER; -- Дней в текущем месяце NTOTALDAYS PKG_STD.TNUMBER; -- Дней в текущем месяце
@ -399,95 +399,95 @@ create or replace package body PKG_P8PANELS_EQUIPSRV as
/* Определим дату конца периода */ /* Определим дату конца периода */
NTODATE := LAST_DAY(TO_DATE('01.' || LPAD(TO_CHAR(NTOMONTH), 2, '0') || '.' || TO_CHAR(NTOYEAR), 'dd.mm.yyyy')); NTODATE := LAST_DAY(TO_DATE('01.' || LPAD(TO_CHAR(NTOMONTH), 2, '0') || '.' || TO_CHAR(NTOYEAR), 'dd.mm.yyyy'));
/* Инициализируем таблицу данных */ /* Инициализируем таблицу данных */
RDG := PKG_P8PANELS_VISUAL.TDATA_GRID_MAKE(BFIXED_HEADER => true, NFIXED_COLUMNS => 2); RDG := PKG_P8PANELS_VISUAL.TDG_MAKE(BFIXED_HEADER => true, NFIXED_COLUMNS => 2);
/* Формируем структуру заголовка */ /* Формируем структуру заголовка */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SOBJINFO', SNAME => 'SOBJINFO',
SCAPTION => 'Информация по объекту ремонта', SCAPTION => 'Информация по объекту ремонта',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
NWIDTH => 80); NWIDTH => 80);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SINFO', SNAME => 'SINFO',
SCAPTION => 'Объект ремонта', SCAPTION => 'Объект ремонта',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
NWIDTH => 80); NWIDTH => 80);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SWRKTYPE', SNAME => 'SWRKTYPE',
SCAPTION => 'Тип работ', SCAPTION => 'Тип работ',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
SPARENT => 'SINFO', SPARENT => 'SINFO',
NWIDTH => 80); NWIDTH => 80);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'NRN', SNAME => 'NRN',
SCAPTION => 'Рег. номер', SCAPTION => 'Рег. номер',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_NUMB, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_NUMB,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SWORKNAME', SNAME => 'SWORKNAME',
SCAPTION => 'Наименование работы', SCAPTION => 'Наименование работы',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'STECHOBJCODE', SNAME => 'STECHOBJCODE',
SCAPTION => 'Код тех. объекта', SCAPTION => 'Код тех. объекта',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'STECHOBJNAME', SNAME => 'STECHOBJNAME',
SCAPTION => 'Наименование тех. объекта', SCAPTION => 'Наименование тех. объекта',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SBELONG', SNAME => 'SBELONG',
SCAPTION => 'Принадлежность', SCAPTION => 'Принадлежность',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SPRODOBJ', SNAME => 'SPRODOBJ',
SCAPTION => 'Производственный объект', SCAPTION => 'Производственный объект',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'STECHSERV', SNAME => 'STECHSERV',
SCAPTION => 'Тех. служба', SCAPTION => 'Тех. служба',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SRESPDEP', SNAME => 'SRESPDEP',
SCAPTION => 'Отвественное подразделение', SCAPTION => 'Отвественное подразделение',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'STECSERVCODE', SNAME => 'STECSERVCODE',
SCAPTION => 'Вид ремонта', SCAPTION => 'Вид ремонта',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'DDATEPLANBEG', SNAME => 'DDATEPLANBEG',
SCAPTION => 'Начало работы (план)', SCAPTION => 'Начало работы (план)',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_DATE, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_DATE,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'DDATEPLANEND', SNAME => 'DDATEPLANEND',
SCAPTION => 'Окончание работы (план)', SCAPTION => 'Окончание работы (план)',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_DATE, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_DATE,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'DDATEFACTBEG', SNAME => 'DDATEFACTBEG',
SCAPTION => 'Начало работы (факт)', SCAPTION => 'Начало работы (факт)',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_DATE, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_DATE,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'DDATEFACTEND', SNAME => 'DDATEFACTEND',
SCAPTION => 'Окончание работы (факт)', SCAPTION => 'Окончание работы (факт)',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_DATE, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_DATE,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'STECSRVKINDCODE', SNAME => 'STECSRVKINDCODE',
SCAPTION => 'Код типа работы', SCAPTION => 'Код типа работы',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
BVISIBLE => false); BVISIBLE => false);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'STECSRVKINDNAME', SNAME => 'STECSRVKINDNAME',
SCAPTION => 'Наименование типа работы', SCAPTION => 'Наименование типа работы',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
@ -530,7 +530,7 @@ create or replace package body PKG_P8PANELS_EQUIPSRV as
else else
BEXPANDED := false; BEXPANDED := false;
end if; end if;
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => '_' || TO_CHAR(Y) || '_' || TO_CHAR(M), SNAME => '_' || TO_CHAR(Y) || '_' || TO_CHAR(M),
SCAPTION => LPAD(TO_CHAR(M), 2, '0') || ' ' || TO_CHAR(Y), SCAPTION => LPAD(TO_CHAR(M), 2, '0') || ' ' || TO_CHAR(Y),
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
@ -544,9 +544,8 @@ create or replace package body PKG_P8PANELS_EQUIPSRV as
/* Цикл по дням месяца */ /* Цикл по дням месяца */
for D in 1 .. NTOTALDAYS for D in 1 .. NTOTALDAYS
loop loop
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => '_' || TO_CHAR(Y) || '_' || TO_CHAR(M) || '_' || SNAME => '_' || TO_CHAR(Y) || '_' || TO_CHAR(M) || '_' || TO_CHAR(D),
TO_CHAR(D),
SCAPTION => TO_CHAR(D, '99'), SCAPTION => TO_CHAR(D, '99'),
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
SPARENT => '_' || TO_CHAR(Y) || '_' || TO_CHAR(M)); SPARENT => '_' || TO_CHAR(Y) || '_' || TO_CHAR(M));
@ -590,7 +589,7 @@ create or replace package body PKG_P8PANELS_EQUIPSRV as
for M in NMS .. NME for M in NMS .. NME
loop loop
SPERIODNAME := '_' || TO_CHAR(Y) || '_' || TO_CHAR(M); SPERIODNAME := '_' || TO_CHAR(Y) || '_' || TO_CHAR(M);
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW_INFO, PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW_INFO,
SNAME => SPERIODNAME, SNAME => SPERIODNAME,
SVALUE => 'план: ' || HOURS_STR(NHOURS => TRUNC(PKG_CONTVALLOC1S.GETN(RCONTAINER => YM, SVALUE => 'план: ' || HOURS_STR(NHOURS => TRUNC(PKG_CONTVALLOC1S.GETN(RCONTAINER => YM,
SROWID => SPERIODNAME || '_P'), SROWID => SPERIODNAME || '_P'),
@ -605,17 +604,17 @@ create or replace package body PKG_P8PANELS_EQUIPSRV as
end loop; end loop;
end loop; end loop;
/* Добавление строки с трудоёмкостью */ /* Добавление строки с трудоёмкостью */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW_INFO); PKG_P8PANELS_VISUAL.TDG_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW_INFO);
end if; end if;
/* Добавление группы с объектом ремонта */ /* Добавление группы с объектом ремонта */
SCURTECHOBJ := QQ.STECHOBJNAME; SCURTECHOBJ := QQ.STECHOBJNAME;
SPRJ_GROUP_NAME := SCURTECHOBJ; SPRJ_GROUP_NAME := SCURTECHOBJ;
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_GROUP(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_GROUP(RDATA_GRID => RDG,
SNAME => SPRJ_GROUP_NAME, SNAME => SPRJ_GROUP_NAME,
SCAPTION => QQ.STECHOBJNAME, SCAPTION => QQ.STECHOBJNAME,
BEXPANDABLE => false); BEXPANDABLE => false);
RDG_ROW_INFO := PKG_P8PANELS_VISUAL.TROW_MAKE(SGROUP => SPRJ_GROUP_NAME); RDG_ROW_INFO := PKG_P8PANELS_VISUAL.TDG_ROW_MAKE(SGROUP => SPRJ_GROUP_NAME);
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW_INFO, SNAME => 'SOBJINFO', SVALUE => SCURTECHOBJ); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW_INFO, SNAME => 'SOBJINFO', SVALUE => SCURTECHOBJ);
end if; end if;
/* Формируем имя группы для вида ремонта */ /* Формируем имя группы для вида ремонта */
SCURTSKCODE := SCURTECHOBJ || '_' || QQ.STECSRVKINDCODE; SCURTSKCODE := SCURTECHOBJ || '_' || QQ.STECSRVKINDCODE;
@ -623,7 +622,7 @@ create or replace package body PKG_P8PANELS_EQUIPSRV as
if (PKG_CONTVALLOC1S.EXISTS_(RCONTAINER => GF, SROWID => SCURTSKCODE) = false) then if (PKG_CONTVALLOC1S.EXISTS_(RCONTAINER => GF, SROWID => SCURTSKCODE) = false) then
/* Добавляем строку плана */ /* Добавляем строку плана */
if (RDG_ROW_PLAN.RCOLS is not null) then if (RDG_ROW_PLAN.RCOLS is not null) then
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW_PLAN); PKG_P8PANELS_VISUAL.TDG_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW_PLAN);
end if; end if;
/* Добавляем строку факта */ /* Добавляем строку факта */
if (RDG_ROW_FACT.RCOLS is not null) then if (RDG_ROW_FACT.RCOLS is not null) then
@ -631,27 +630,27 @@ create or replace package body PKG_P8PANELS_EQUIPSRV as
/* Цикл по коллекции для закрашивания месяцев */ /* Цикл по коллекции для закрашивания месяцев */
for Z in 1 .. PKG_CONTVALLOC1S.COUNT_(RCONTAINER => MCLR) for Z in 1 .. PKG_CONTVALLOC1S.COUNT_(RCONTAINER => MCLR)
loop loop
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW_FACT, PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW_FACT,
SNAME => CR, SNAME => CR,
SVALUE => PKG_CONTVALLOC1S.GETS(RCONTAINER => MCLR, SROWID => CR)); SVALUE => PKG_CONTVALLOC1S.GETS(RCONTAINER => MCLR, SROWID => CR));
CR := PKG_CONTVALLOC1S.NEXT_(RCONTAINER => MCLR, SROWID => CR); CR := PKG_CONTVALLOC1S.NEXT_(RCONTAINER => MCLR, SROWID => CR);
end loop; end loop;
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW_FACT); PKG_P8PANELS_VISUAL.TDG_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW_FACT);
end if; end if;
PKG_CONTVALLOC1S.PURGE(RCONTAINER => MCLR); PKG_CONTVALLOC1S.PURGE(RCONTAINER => MCLR);
/* Добвим группу для вида ремонта */ /* Добвим группу для вида ремонта */
SPRJ_GROUP_NAME := SCURTSKCODE; SPRJ_GROUP_NAME := SCURTSKCODE;
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_GROUP(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_GROUP(RDATA_GRID => RDG,
SNAME => SPRJ_GROUP_NAME, SNAME => SPRJ_GROUP_NAME,
SCAPTION => QQ.STECSRVKINDCODE, SCAPTION => QQ.STECSRVKINDCODE,
BEXPANDABLE => false); BEXPANDABLE => false);
/* Строка плана */ /* Строка плана */
RDG_ROW_PLAN := PKG_P8PANELS_VISUAL.TROW_MAKE(SGROUP => SPRJ_GROUP_NAME); RDG_ROW_PLAN := PKG_P8PANELS_VISUAL.TDG_ROW_MAKE(SGROUP => SPRJ_GROUP_NAME);
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW_PLAN, SNAME => 'SOBJINFO', SVALUE => QQ.STECSRVKINDCODE); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW_PLAN, SNAME => 'SOBJINFO', SVALUE => QQ.STECSRVKINDCODE);
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW_PLAN, SNAME => 'SWRKTYPE', SVALUE => 'План'); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW_PLAN, SNAME => 'SWRKTYPE', SVALUE => 'План');
/* Строка факта */ /* Строка факта */
RDG_ROW_FACT := PKG_P8PANELS_VISUAL.TROW_MAKE(SGROUP => SPRJ_GROUP_NAME); RDG_ROW_FACT := PKG_P8PANELS_VISUAL.TDG_ROW_MAKE(SGROUP => SPRJ_GROUP_NAME);
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW_FACT, SNAME => 'SWRKTYPE', SVALUE => 'Факт'); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW_FACT, SNAME => 'SWRKTYPE', SVALUE => 'Факт');
/* Добавляем в заполненные группы */ /* Добавляем в заполненные группы */
PKG_CONTVALLOC1S.PUTS(RCONTAINER => GF, SROWID => SPRJ_GROUP_NAME, SVALUE => ''); PKG_CONTVALLOC1S.PUTS(RCONTAINER => GF, SROWID => SPRJ_GROUP_NAME, SVALUE => '');
end if; end if;
@ -676,7 +675,7 @@ create or replace package body PKG_P8PANELS_EQUIPSRV as
/* Закрашивание месяца плана синим */ /* Закрашивание месяца плана синим */
if (PKG_CONTVALLOC1S.EXISTS_(RCONTAINER => COLS, SROWID => SPRJ_GROUP_NAME || ' ' || SPERIODNAME || ' PLAN') = if (PKG_CONTVALLOC1S.EXISTS_(RCONTAINER => COLS, SROWID => SPRJ_GROUP_NAME || ' ' || SPERIODNAME || ' PLAN') =
false) then false) then
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW_PLAN, SNAME => SPERIODNAME, SVALUE => 'blue'); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW_PLAN, SNAME => SPERIODNAME, SVALUE => 'blue');
PKG_CONTVALLOC1S.PUTS(RCONTAINER => COLS, PKG_CONTVALLOC1S.PUTS(RCONTAINER => COLS,
SROWID => SPRJ_GROUP_NAME || ' ' || SPERIODNAME || ' PLAN', SROWID => SPRJ_GROUP_NAME || ' ' || SPERIODNAME || ' PLAN',
SVALUE => ''); SVALUE => '');
@ -686,7 +685,7 @@ create or replace package body PKG_P8PANELS_EQUIPSRV as
/* Закрашивание дня плана синим */ /* Закрашивание дня плана синим */
if (PKG_CONTVALLOC1S.EXISTS_(RCONTAINER => COLS, SROWID => SPRJ_GROUP_NAME || ' ' || SPERIODNAME || ' PLAN') = if (PKG_CONTVALLOC1S.EXISTS_(RCONTAINER => COLS, SROWID => SPRJ_GROUP_NAME || ' ' || SPERIODNAME || ' PLAN') =
false) then false) then
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW_PLAN, SNAME => SPERIODNAME, SVALUE => 'blue'); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW_PLAN, SNAME => SPERIODNAME, SVALUE => 'blue');
PKG_CONTVALLOC1S.PUTS(RCONTAINER => COLS, PKG_CONTVALLOC1S.PUTS(RCONTAINER => COLS,
SROWID => SPRJ_GROUP_NAME || ' ' || SPERIODNAME || ' PLAN', SROWID => SPRJ_GROUP_NAME || ' ' || SPERIODNAME || ' PLAN',
SVALUE => ''); SVALUE => '');
@ -783,7 +782,7 @@ create or replace package body PKG_P8PANELS_EQUIPSRV as
for M in NMS .. NME for M in NMS .. NME
loop loop
SPERIODNAME := '_' || TO_CHAR(Y) || '_' || TO_CHAR(M); SPERIODNAME := '_' || TO_CHAR(Y) || '_' || TO_CHAR(M);
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW_INFO, PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW_INFO,
SNAME => SPERIODNAME, SNAME => SPERIODNAME,
SVALUE => 'план: ' || HOURS_STR(NHOURS => TRUNC(PKG_CONTVALLOC1S.GETN(RCONTAINER => YM, SVALUE => 'план: ' || HOURS_STR(NHOURS => TRUNC(PKG_CONTVALLOC1S.GETN(RCONTAINER => YM,
SROWID => SPERIODNAME || '_P'), SROWID => SPERIODNAME || '_P'),
@ -793,27 +792,27 @@ create or replace package body PKG_P8PANELS_EQUIPSRV as
1))); 1)));
end loop; end loop;
end loop; end loop;
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW_INFO); PKG_P8PANELS_VISUAL.TDG_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW_INFO);
end if; end if;
/* План для последней записи */ /* План для последней записи */
if ((RDG_ROW_PLAN.RCOLS is not null) and (NROWS = 0)) then if ((RDG_ROW_PLAN.RCOLS is not null) and (NROWS = 0)) then
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW_PLAN); PKG_P8PANELS_VISUAL.TDG_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW_PLAN);
end if; end if;
/* Факт для последней записи */ /* Факт для последней записи */
if ((RDG_ROW_FACT.RCOLS is not null) and (NROWS = 0)) then if ((RDG_ROW_FACT.RCOLS is not null) and (NROWS = 0)) then
CR := PKG_CONTVALLOC1S.FIRST_(RCONTAINER => MCLR); CR := PKG_CONTVALLOC1S.FIRST_(RCONTAINER => MCLR);
for Z in 1 .. PKG_CONTVALLOC1S.COUNT_(RCONTAINER => MCLR) for Z in 1 .. PKG_CONTVALLOC1S.COUNT_(RCONTAINER => MCLR)
loop loop
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW_FACT, PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW_FACT,
SNAME => CR, SNAME => CR,
SVALUE => PKG_CONTVALLOC1S.GETS(RCONTAINER => MCLR, SROWID => CR)); SVALUE => PKG_CONTVALLOC1S.GETS(RCONTAINER => MCLR, SROWID => CR));
CR := PKG_CONTVALLOC1S.NEXT_(RCONTAINER => MCLR, SROWID => CR); CR := PKG_CONTVALLOC1S.NEXT_(RCONTAINER => MCLR, SROWID => CR);
end loop; end loop;
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW_FACT); PKG_P8PANELS_VISUAL.TDG_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW_FACT);
end if; end if;
end loop; end loop;
/* Сериализуем описание */ /* Сериализуем описание */
COUT := PKG_P8PANELS_VISUAL.TDATA_GRID_TO_XML(RDATA_GRID => RDG, NINCLUDE_DEF => 1); COUT := PKG_P8PANELS_VISUAL.TDG_TO_XML(RDATA_GRID => RDG, NINCLUDE_DEF => 1);
/* Очищаем контейнеры */ /* Очищаем контейнеры */
PKG_CONTVALLOC1S.PURGE(RCONTAINER => YM); PKG_CONTVALLOC1S.PURGE(RCONTAINER => YM);
PKG_CONTVALLOC1S.PURGE(RCONTAINER => MCLR); PKG_CONTVALLOC1S.PURGE(RCONTAINER => MCLR);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -784,8 +784,8 @@ create or replace package body PKG_P8PANELS_RRPCONFED as
is is
NVERSION PKG_STD.TREF; -- Рег. номер версии словаря контрагентов NVERSION PKG_STD.TREF; -- Рег. номер версии словаря контрагентов
NCOMPANY PKG_STD.TREF := GET_SESSION_COMPANY(); -- Организация сеанса NCOMPANY PKG_STD.TREF := GET_SESSION_COMPANY(); -- Организация сеанса
RDG PKG_P8PANELS_VISUAL.TDATA_GRID; -- Описание таблицы RDG PKG_P8PANELS_VISUAL.TDG; -- Описание таблицы
RDG_ROW PKG_P8PANELS_VISUAL.TROW; -- Строка таблицы RDG_ROW PKG_P8PANELS_VISUAL.TDG_ROW; -- Строка таблицы
CDG clob; -- XML данных раздела CDG clob; -- XML данных раздела
CXML PKG_CONTVALLOC2NS.TCONTAINER; -- Контейнер для данных XML CXML PKG_CONTVALLOC2NS.TCONTAINER; -- Контейнер для данных XML
RRRPCONFSCTNMRK RRPCONFSCTNMRK%rowtype; -- Рег. номер показателя RRRPCONFSCTNMRK RRPCONFSCTNMRK%rowtype; -- Рег. номер показателя
@ -798,7 +798,7 @@ create or replace package body PKG_P8PANELS_RRPCONFED as
/* Инициализация колонок граф показателей */ /* Инициализация колонок граф показателей */
procedure MARKS_COLUMNS_INIT procedure MARKS_COLUMNS_INIT
( (
RDG in out nocopy PKG_P8PANELS_VISUAL.TDATA_GRID, -- Описание таблицы RDG in out nocopy PKG_P8PANELS_VISUAL.TDG, -- Описание таблицы
NRRPCONFSCTN in number -- Рег. номер раздела NRRPCONFSCTN in number -- Рег. номер раздела
) )
is is
@ -816,31 +816,31 @@ create or replace package body PKG_P8PANELS_RRPCONFED as
order by C.CODE) order by C.CODE)
loop loop
/* Наименование графы */ /* Наименование графы */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SCOL_' || REC.CODE, SNAME => 'SCOL_' || REC.CODE,
SCAPTION => REC.NAME, SCAPTION => REC.NAME,
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
NWIDTH => 200); NWIDTH => 200);
/* Рег. номер графы */ /* Рег. номер графы */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'NCOL_RN_' || REC.CODE, SNAME => 'NCOL_RN_' || REC.CODE,
SCAPTION => REC.NAME || ' рег. номер', SCAPTION => REC.NAME || ' рег. номер',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_NUMB, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_NUMB,
BVISIBLE => false); BVISIBLE => false);
/* Рег. номер показателя */ /* Рег. номер показателя */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'NMARK_RN_' || REC.CODE, SNAME => 'NMARK_RN_' || REC.CODE,
SCAPTION => REC.NAME || ' рег. номер показателя', SCAPTION => REC.NAME || ' рег. номер показателя',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_NUMB, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_NUMB,
BVISIBLE => false); BVISIBLE => false);
/* Мнемокод показателя */ /* Мнемокод показателя */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SMARK_CODE_' || REC.CODE, SNAME => 'SMARK_CODE_' || REC.CODE,
SCAPTION => REC.NAME || ' мнемокод показателя', SCAPTION => REC.NAME || ' мнемокод показателя',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
BVISIBLE => false); BVISIBLE => false);
/* Для составов показтелей */ /* Для составов показтелей */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'MARK_CNS_' || REC.CODE, SNAME => 'MARK_CNS_' || REC.CODE,
SCAPTION => REC.NAME || ' состав показателя', SCAPTION => REC.NAME || ' состав показателя',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
@ -897,21 +897,21 @@ create or replace package body PKG_P8PANELS_RRPCONFED as
order by T.CODE) order by T.CODE)
loop loop
/* Инициализируем таблицу данных */ /* Инициализируем таблицу данных */
RDG := PKG_P8PANELS_VISUAL.TDATA_GRID_MAKE(BFIXED_HEADER => true, NFIXED_COLUMNS => 1); RDG := PKG_P8PANELS_VISUAL.TDG_MAKE(BFIXED_HEADER => true, NFIXED_COLUMNS => 1);
/* Формируем структуру заголовка */ /* Формируем структуру заголовка */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SROW_NAME', SNAME => 'SROW_NAME',
SCAPTION => '', SCAPTION => '',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
NWIDTH => 150); NWIDTH => 150);
/* Формируем структуру заголовка */ /* Формируем структуру заголовка */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SROW_CODE', SNAME => 'SROW_CODE',
SCAPTION => 'Мнемокод строки', SCAPTION => 'Мнемокод строки',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
BVISIBLE => false); BVISIBLE => false);
/* Формируем структуру заголовка */ /* Формируем структуру заголовка */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'NROW_RN', SNAME => 'NROW_RN',
SCAPTION => 'Рег. номер строки', SCAPTION => 'Рег. номер строки',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_NUMB, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_NUMB,
@ -938,11 +938,11 @@ create or replace package body PKG_P8PANELS_RRPCONFED as
LPAD(R.CODE, 20, '0')) LPAD(R.CODE, 20, '0'))
loop loop
/* Заполняем наименование строки */ /* Заполняем наименование строки */
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SROW_NAME', SVALUE => R.NAME, BCLEAR => true); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SROW_NAME', SVALUE => R.NAME, BCLEAR => true);
/* Заполняем мнемокод строки */ /* Заполняем мнемокод строки */
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SROW_CODE', SVALUE => R.CODE); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SROW_CODE', SVALUE => R.CODE);
/* Заполняем рег. номер строки */ /* Заполняем рег. номер строки */
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, SNAME => 'NROW_RN', NVALUE => R.RN); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW, SNAME => 'NROW_RN', NVALUE => R.RN);
/* Обходим графы раздела */ /* Обходим графы раздела */
for C in (select C.RN, for C in (select C.RN,
C.CODE, C.CODE,
@ -963,29 +963,29 @@ create or replace package body PKG_P8PANELS_RRPCONFED as
/* Считываем показатель по строке/графе */ /* Считываем показатель по строке/графе */
RRRPCONFSCTNMRK := RRPCONFSCTNMRK_GET_ROWCOL(NRRPCONFSCTN => S.NRN, NRRPROW => R.RN, NRRPCOLUMN => C.RN); RRRPCONFSCTNMRK := RRPCONFSCTNMRK_GET_ROWCOL(NRRPCONFSCTN => S.NRN, NRRPROW => R.RN, NRRPCOLUMN => C.RN);
/* Заполняем рег. номер графы */ /* Заполняем рег. номер графы */
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SCOL_' || C.CODE, SVALUE => C.NAME); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SCOL_' || C.CODE, SVALUE => C.NAME);
/* Заполняем рег. номер графы */ /* Заполняем рег. номер графы */
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, SNAME => 'NCOL_RN_' || C.CODE, NVALUE => C.RN); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW, SNAME => 'NCOL_RN_' || C.CODE, NVALUE => C.RN);
/* Заполняем рег. номер показателя */ /* Заполняем рег. номер показателя */
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW,
SNAME => 'NMARK_RN_' || C.CODE, SNAME => 'NMARK_RN_' || C.CODE,
NVALUE => RRRPCONFSCTNMRK.RN); NVALUE => RRRPCONFSCTNMRK.RN);
/* Если ошибка считывания показателя */ /* Если ошибка считывания показателя */
if (RRRPCONFSCTNMRK.RN is not null) then if (RRRPCONFSCTNMRK.RN is not null) then
/* Заполняем мнемокод показателя */ /* Заполняем мнемокод показателя */
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW,
SNAME => 'SMARK_CODE_' || C.CODE, SNAME => 'SMARK_CODE_' || C.CODE,
SVALUE => RRRPCONFSCTNMRK.CODE); SVALUE => RRRPCONFSCTNMRK.CODE);
/* Добавляем атрибут состава показателей */ /* Добавляем атрибут состава показателей */
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, SNAME => 'MARK_CNS_' || C.CODE, SVALUE => null); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW, SNAME => 'MARK_CNS_' || C.CODE, SVALUE => null);
end if; end if;
end loop; end loop;
/* Добавим строку для раздела */ /* Добавим строку для раздела */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW); PKG_P8PANELS_VISUAL.TDG_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW);
end loop; end loop;
end if; end if;
/* Сериализуем описание */ /* Сериализуем описание */
CDG := PKG_P8PANELS_VISUAL.TDATA_GRID_TO_XML(RDATA_GRID => RDG, NINCLUDE_DEF => 1); CDG := PKG_P8PANELS_VISUAL.TDG_TO_XML(RDATA_GRID => RDG, NINCLUDE_DEF => 1);
/* Заполняем контейнер данными о разделе */ /* Заполняем контейнер данными о разделе */
PKG_CONTVALLOC2NS.PUTN(RCONTAINER => CXML, NTABID => S.RNUM, SROWID => 'RN', NVALUE => S.NRN); PKG_CONTVALLOC2NS.PUTN(RCONTAINER => CXML, NTABID => S.RNUM, SROWID => 'RN', NVALUE => S.NRN);
PKG_CONTVALLOC2NS.PUTS(RCONTAINER => CXML, NTABID => S.RNUM, SROWID => 'CODE', SVALUE => S.SCODE); PKG_CONTVALLOC2NS.PUTS(RCONTAINER => CXML, NTABID => S.RNUM, SROWID => 'CODE', SVALUE => S.SCODE);

View File

@ -59,10 +59,35 @@ create or replace package PKG_P8PANELS_SAMPLES as
DDATE_TO in date -- Дата окончания задачи DDATE_TO in date -- Дата окончания задачи
); );
/* Инициализация буфера данных для диаграммы Ганта */
procedure CYCLOGRAM_INIT
(
NIDENT in out number -- Идентификатор буфера сформированных данных (null - сгенерировать новый, !null - удалить старые данные и пересоздать с указанным идентификатором)
);
/* Сбор данных для отображения циклограммы */
procedure CYCLOGRAM
(
NIDENT in number, -- Идентификатор процесса
COUT out clob -- Сериализованные данные для циклограммы
);
/* Изменение задачи циклограммы */
procedure CYCLOGRAM_TASK_MODIFY
(
NIDENT in number, -- Идентификатор буфера
NRN in number, -- Рег. номер записи
SDATE_FROM in varchar2, -- Дата начала (в строковом представлении)
SDATE_TO in varchar2 -- Дата окончания (в строковом представлении)
);
end PKG_P8PANELS_SAMPLES; end PKG_P8PANELS_SAMPLES;
/ /
create or replace package body PKG_P8PANELS_SAMPLES as create or replace package body PKG_P8PANELS_SAMPLES as
/* Константы - циклограмма */
NCG_MULTIPLIER constant PKG_STD.TNUMBER := 5; -- Множитель для ширины отображения
/* Получение списка контрагентов */ /* Получение списка контрагентов */
procedure AGNLIST_GET procedure AGNLIST_GET
( (
@ -205,6 +230,7 @@ create or replace package body PKG_P8PANELS_SAMPLES as
NCONTACT_METHOD => null, NCONTACT_METHOD => null,
SMF_ID => null, SMF_ID => null,
SOKOGU => null, SOKOGU => null,
NJURPERS_SUBDIV => null,
NRN => NRN); NRN => NRN);
end AGNLIST_INSERT; end AGNLIST_INSERT;
@ -231,11 +257,11 @@ create or replace package body PKG_P8PANELS_SAMPLES as
is is
NCOMPANY PKG_STD.TREF := GET_SESSION_COMPANY(); -- Организация сеанса NCOMPANY PKG_STD.TREF := GET_SESSION_COMPANY(); -- Организация сеанса
NIDENT PKG_STD.TREF := GEN_IDENT(); -- Идентификатор отбора NIDENT PKG_STD.TREF := GEN_IDENT(); -- Идентификатор отбора
RF PKG_P8PANELS_VISUAL.TFILTERS; -- Фильтры RF PKG_P8PANELS_VISUAL.TDG_FILTERS; -- Фильтры
RO PKG_P8PANELS_VISUAL.TORDERS; -- Сортировки RO PKG_P8PANELS_VISUAL.TDG_ORDERS; -- Сортировки
RDG PKG_P8PANELS_VISUAL.TDATA_GRID; -- Описание таблицы RDG PKG_P8PANELS_VISUAL.TDG; -- Описание таблицы
RAGN_TYPES PKG_P8PANELS_VISUAL.TCOL_VALS; -- Предопределенные значения "Типа контрагентов" RAGN_TYPES PKG_P8PANELS_VISUAL.TDG_COL_VALS; -- Предопределенные значения "Типа контрагентов"
RDG_ROW PKG_P8PANELS_VISUAL.TROW; -- Строка таблицы RDG_ROW PKG_P8PANELS_VISUAL.TDG_ROW; -- Строка таблицы
NROW_FROM PKG_STD.TREF; -- Номер строки с NROW_FROM PKG_STD.TREF; -- Номер строки с
NROW_TO PKG_STD.TREF; -- Номер строки по NROW_TO PKG_STD.TREF; -- Номер строки по
CSQL clob; -- Буфер для запроса CSQL clob; -- Буфер для запроса
@ -246,18 +272,18 @@ create or replace package body PKG_P8PANELS_SAMPLES as
NAGNTYPE PKG_STD.TREF; -- Буфер для "Типа" NAGNTYPE PKG_STD.TREF; -- Буфер для "Типа"
begin begin
/* Читаем фильтры */ /* Читаем фильтры */
RF := PKG_P8PANELS_VISUAL.TFILTERS_FROM_XML(CFILTERS => CFILTERS); RF := PKG_P8PANELS_VISUAL.TDG_FILTERS_FROM_XML(CFILTERS => CFILTERS);
/* Читем сортировки */ /* Читем сортировки */
RO := PKG_P8PANELS_VISUAL.TORDERS_FROM_XML(CORDERS => CORDERS); RO := PKG_P8PANELS_VISUAL.TDG_ORDERS_FROM_XML(CORDERS => CORDERS);
/* Преобразуем номер и размер страницы в номер строк с и по */ /* Преобразуем номер и размер страницы в номер строк с и по */
PKG_P8PANELS_VISUAL.UTL_ROWS_LIMITS_CALC(NPAGE_NUMBER => NPAGE_NUMBER, PKG_P8PANELS_VISUAL.UTL_ROWS_LIMITS_CALC(NPAGE_NUMBER => NPAGE_NUMBER,
NPAGE_SIZE => NPAGE_SIZE, NPAGE_SIZE => NPAGE_SIZE,
NROW_FROM => NROW_FROM, NROW_FROM => NROW_FROM,
NROW_TO => NROW_TO); NROW_TO => NROW_TO);
/* Инициализируем таблицу данных */ /* Инициализируем таблицу данных */
RDG := PKG_P8PANELS_VISUAL.TDATA_GRID_MAKE(BFIXED_HEADER => true, NFIXED_COLUMNS => 2); RDG := PKG_P8PANELS_VISUAL.TDG_MAKE(BFIXED_HEADER => true, NFIXED_COLUMNS => 2);
/* Описываем колонки таблицы данных */ /* Описываем колонки таблицы данных */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SAGNABBR', SNAME => 'SAGNABBR',
SCAPTION => 'Мнемокод', SCAPTION => 'Мнемокод',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
@ -266,7 +292,7 @@ create or replace package body PKG_P8PANELS_SAMPLES as
BORDER => true, BORDER => true,
BFILTER => true, BFILTER => true,
NWIDTH => 150); NWIDTH => 150);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SAGNINFO', SNAME => 'SAGNINFO',
SCAPTION => 'Сведения', SCAPTION => 'Сведения',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
@ -275,7 +301,7 @@ create or replace package body PKG_P8PANELS_SAMPLES as
BFILTER => false, BFILTER => false,
BEXPANDABLE => true, BEXPANDABLE => true,
NWIDTH => 300); NWIDTH => 300);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SAGNNAME', SNAME => 'SAGNNAME',
SCAPTION => 'Наименование', SCAPTION => 'Наименование',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR,
@ -285,9 +311,9 @@ create or replace package body PKG_P8PANELS_SAMPLES as
BFILTER => true, BFILTER => true,
SPARENT => 'SAGNINFO', SPARENT => 'SAGNINFO',
NWIDTH => 200); NWIDTH => 200);
PKG_P8PANELS_VISUAL.TCOL_VALS_ADD(RCOL_VALS => RAGN_TYPES, NVALUE => 0); PKG_P8PANELS_VISUAL.TDG_COL_VALS_ADD(RCOL_VALS => RAGN_TYPES, NVALUE => 0);
PKG_P8PANELS_VISUAL.TCOL_VALS_ADD(RCOL_VALS => RAGN_TYPES, NVALUE => 1); PKG_P8PANELS_VISUAL.TDG_COL_VALS_ADD(RCOL_VALS => RAGN_TYPES, NVALUE => 1);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'NAGNTYPE', SNAME => 'NAGNTYPE',
SCAPTION => 'Тип', SCAPTION => 'Тип',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_NUMB, SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_NUMB,
@ -303,11 +329,11 @@ create or replace package body PKG_P8PANELS_SAMPLES as
'или оперативном управлении обособленное имущество, отвечает по своим обязательствам этим имуществом, может от своего ' || 'или оперативном управлении обособленное имущество, отвечает по своим обязательствам этим имуществом, может от своего ' ||
'имени приобретать и осуществлять имущественные и личные неимущественные права, отвечать по своим обязанностям.<br>' || 'имени приобретать и осуществлять имущественные и личные неимущественные права, отвечать по своим обязанностям.<br>' ||
'<b style="color:green">Физическое лицо</b> - субъект правовых отношений, представляющий собой одного человека.'); '<b style="color:green">Физическое лицо</b> - субъект правовых отношений, представляющий собой одного человека.');
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SFULLNAME', SNAME => 'SFULLNAME',
SCAPTION => 'Полное наименование', SCAPTION => 'Полное наименование',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR); SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR);
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_COL_DEF(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_COL_DEF(RDATA_GRID => RDG,
SNAME => 'SAGNIDNUMB', SNAME => 'SAGNIDNUMB',
SCAPTION => 'ИНН', SCAPTION => 'ИНН',
SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR); SDATA_TYPE => PKG_P8PANELS_VISUAL.SDATA_TYPE_STR);
@ -336,9 +362,12 @@ create or replace package body PKG_P8PANELS_SAMPLES as
PKG_SQL_BUILD.APPEND(SSQL => CSQL, SELEMENT1 => ' and AG.RN in (select ID from COND_BROKER_IDSMART where IDENT = :NIDENT) %ORDER_BY%) D) F'); PKG_SQL_BUILD.APPEND(SSQL => CSQL, SELEMENT1 => ' and AG.RN in (select ID from COND_BROKER_IDSMART where IDENT = :NIDENT) %ORDER_BY%) D) F');
PKG_SQL_BUILD.APPEND(SSQL => CSQL, SELEMENT1 => ' where F.NROW between :NROW_FROM and :NROW_TO'); PKG_SQL_BUILD.APPEND(SSQL => CSQL, SELEMENT1 => ' where F.NROW between :NROW_FROM and :NROW_TO');
/* Учтём сортировки */ /* Учтём сортировки */
PKG_P8PANELS_VISUAL.TORDERS_SET_QUERY(RDATA_GRID => RDG, RORDERS => RO, SPATTERN => '%ORDER_BY%', CSQL => CSQL); PKG_P8PANELS_VISUAL.TDG_ORDERS_SET_QUERY(RDATA_GRID => RDG,
RORDERS => RO,
SPATTERN => '%ORDER_BY%',
CSQL => CSQL);
/* Учтём фильтры */ /* Учтём фильтры */
PKG_P8PANELS_VISUAL.TFILTERS_SET_QUERY(NIDENT => NIDENT, PKG_P8PANELS_VISUAL.TDG_FILTERS_SET_QUERY(NIDENT => NIDENT,
NCOMPANY => NCOMPANY, NCOMPANY => NCOMPANY,
SUNIT => 'AGNLIST', SUNIT => 'AGNLIST',
SPROCEDURE => 'P_AGNLIST_BASE_COND', SPROCEDURE => 'P_AGNLIST_BASE_COND',
@ -371,7 +400,7 @@ create or replace package body PKG_P8PANELS_SAMPLES as
if (NAGNTYPE = 0) then if (NAGNTYPE = 0) then
SGROUP := 'JUR'; SGROUP := 'JUR';
SAGNINFO := SAGNNAME || ', ЮЛ'; SAGNINFO := SAGNNAME || ', ЮЛ';
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_GROUP(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_GROUP(RDATA_GRID => RDG,
SNAME => SGROUP, SNAME => SGROUP,
SCAPTION => 'Юридические лица', SCAPTION => 'Юридические лица',
BEXPANDABLE => true, BEXPANDABLE => true,
@ -379,21 +408,30 @@ create or replace package body PKG_P8PANELS_SAMPLES as
else else
SGROUP := 'PERS'; SGROUP := 'PERS';
SAGNINFO := SAGNNAME || ', ФЛ'; SAGNINFO := SAGNNAME || ', ФЛ';
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_GROUP(RDATA_GRID => RDG, PKG_P8PANELS_VISUAL.TDG_ADD_GROUP(RDATA_GRID => RDG,
SNAME => SGROUP, SNAME => SGROUP,
SCAPTION => 'Физические лица', SCAPTION => 'Физические лица',
BEXPANDABLE => true, BEXPANDABLE => true,
BEXPANDED => false); BEXPANDED => false);
end if; end if;
RDG_ROW := PKG_P8PANELS_VISUAL.TROW_MAKE(SGROUP => SGROUP); RDG_ROW := PKG_P8PANELS_VISUAL.TDG_ROW_MAKE(SGROUP => SGROUP);
PKG_P8PANELS_VISUAL.TROW_ADD_CUR_COLS(RROW => RDG_ROW, SNAME => 'SAGNABBR', ICURSOR => ICURSOR, NPOSITION => 1); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_CUR_COLS(RROW => RDG_ROW,
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SAGNINFO', SVALUE => SAGNINFO); SNAME => 'SAGNABBR',
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SAGNNAME', SVALUE => SAGNNAME); ICURSOR => ICURSOR,
PKG_P8PANELS_VISUAL.TROW_ADD_COL(RROW => RDG_ROW, SNAME => 'NAGNTYPE', NVALUE => NAGNTYPE); NPOSITION => 1);
PKG_P8PANELS_VISUAL.TROW_ADD_CUR_COLS(RROW => RDG_ROW, SNAME => 'SFULLNAME', ICURSOR => ICURSOR, NPOSITION => 4); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SAGNINFO', SVALUE => SAGNINFO);
PKG_P8PANELS_VISUAL.TROW_ADD_CUR_COLS(RROW => RDG_ROW, SNAME => 'SAGNIDNUMB', ICURSOR => ICURSOR, NPOSITION => 5); PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW, SNAME => 'SAGNNAME', SVALUE => SAGNNAME);
PKG_P8PANELS_VISUAL.TDG_ROW_ADD_COL(RROW => RDG_ROW, SNAME => 'NAGNTYPE', NVALUE => NAGNTYPE);
PKG_P8PANELS_VISUAL.TDG_ROW_ADD_CUR_COLS(RROW => RDG_ROW,
SNAME => 'SFULLNAME',
ICURSOR => ICURSOR,
NPOSITION => 4);
PKG_P8PANELS_VISUAL.TDG_ROW_ADD_CUR_COLS(RROW => RDG_ROW,
SNAME => 'SAGNIDNUMB',
ICURSOR => ICURSOR,
NPOSITION => 5);
/* Добавляем строку в таблицу */ /* Добавляем строку в таблицу */
PKG_P8PANELS_VISUAL.TDATA_GRID_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW); PKG_P8PANELS_VISUAL.TDG_ADD_ROW(RDATA_GRID => RDG, RROW => RDG_ROW);
end loop; end loop;
/* Освобождаем курсор */ /* Освобождаем курсор */
PKG_SQL_DML.CLOSE_CURSOR(ICURSOR => ICURSOR); PKG_SQL_DML.CLOSE_CURSOR(ICURSOR => ICURSOR);
@ -403,7 +441,7 @@ create or replace package body PKG_P8PANELS_SAMPLES as
raise; raise;
end; end;
/* Сериализуем описание */ /* Сериализуем описание */
COUT := PKG_P8PANELS_VISUAL.TDATA_GRID_TO_XML(RDATA_GRID => RDG, NINCLUDE_DEF => NINCLUDE_DEF); COUT := PKG_P8PANELS_VISUAL.TDG_TO_XML(RDATA_GRID => RDG, NINCLUDE_DEF => NINCLUDE_DEF);
end DATA_GRID; end DATA_GRID;
/* График */ /* График */
@ -667,5 +705,532 @@ create or replace package body PKG_P8PANELS_SAMPLES as
end loop; end loop;
end GANTT_MODIFY; end GANTT_MODIFY;
/* Очистка буфера данных для циклограммы */
procedure CYCLOGRAM_BASE_CLEAN
(
NIDENT in number -- Идентификатор буфера
)
is
begin
/* Удалим из буфера всё с указанным идентификатором */
delete from P8PNL_SMPL_CYCLOGRAM T where T.IDENT = NIDENT;
end CYCLOGRAM_BASE_CLEAN;
/* Добавление данных в буфер циклограммы */
procedure CYCLOGRAM_BASE_INSERT
(
NIDENT in number, -- Идентификатор буфера
NTYPE in number, -- Тип (0 - колонка, 1 - группа, 2 - задача)
SNAME in varchar2, -- Наименование
NPOS_START in number := null, -- Позиция начала элемента
NPOS_END in number := null, -- Позиция окончания элемента
DDATE_FROM in date := null, -- Дата начала
DDATE_TO in date := null, -- Дата окончания
NTASK_GROUP in number := null, -- Рег. номер группы
NRN out number -- Рег. номер записи
)
is
begin
/* Генерируем рег. номер записи */
NRN := GEN_ID();
/* Добавим запись */
insert into P8PNL_SMPL_CYCLOGRAM
(RN, IDENT, type, name, POS_START, POS_END, DATE_FROM, DATE_TO, TASK_GROUP)
values
(NRN, NIDENT, NTYPE, SNAME, NPOS_START, NPOS_END, DDATE_FROM, DDATE_TO, NTASK_GROUP);
end CYCLOGRAM_BASE_INSERT;
/* Исправление данных в буфере циклограммы */
procedure CYCLOGRAM_BASE_UPDATE
(
NIDENT in number, -- Идентификатор буфера
NRN in number, -- Рег. номер записи
NTYPE in number, -- Тип задачи (0 - этап/веха, 1 - работа)
SNAME in varchar2, -- Наименование
NPOS_START in number, -- Позиция начала
NPOS_END in number, -- Позиция окончания
DDATE_FROM in date, -- Дата начала
DDATE_TO in date, -- Дата окончания
NTASK_GROUP in number -- Рег. номер группы
)
is
begin
/* Изменим запись */
update P8PNL_SMPL_CYCLOGRAM T
set T.TYPE = NTYPE,
T.NAME = SNAME,
T.POS_START = NPOS_START,
T.POS_END = NPOS_END,
T.DATE_FROM = DDATE_FROM,
T.DATE_TO = DDATE_TO,
T.TASK_GROUP = NTASK_GROUP
where T.RN = NRN
and T.IDENT = NIDENT;
end CYCLOGRAM_BASE_UPDATE;
/* Инициализация буфера данных для циклограммы */
procedure CYCLOGRAM_INIT
(
NIDENT in out number -- Идентификатор буфера сформированных данных (null - сгенерировать новый, !null - удалить старые данные и пересоздать с указанным идентификатором)
)
is
NYEAR PKG_STD.TNUMBER; -- Текущий год
DCYCLOGRAM_START PKG_STD.TLDATE; -- Дата начала циклограммы
DMONTH_CUR PKG_STD.TLDATE; -- Текущий месяц (для расчетов)
DMONTH_START PKG_STD.TLDATE; -- Начало месяца (для расчетов)
DMONTH_END PKG_STD.TLDATE; -- Окончание месяца (для расчетов)
NMONTH_DAYS PKG_STD.TNUMBER; -- Количество дней месяца
NSTART PKG_STD.TNUMBER := 0; -- Позиция начала элемента
NEND PKG_STD.TNUMBER := 0; -- Позиция окончания элемента
NGROUP PKG_STD.TNUMBER; -- Рег. номер группы
NDUMMY PKG_STD.TNUMBER; -- Буфер для рег. номера
NMONTH PKG_STD.TNUMBER; -- Месяц даты
/* Инициализация группы */
procedure INIT_GROUP
(
NIDENT in number, -- Идентификатор буфера
DDATE in date, -- Текущая обрабатываемая дата
NRN in out number -- Рег. номер группы
)
is
NMONTH PKG_STD.TNUMBER; -- Месяц даты
begin
/* Считываем текущий месяц */
NMONTH := D_MONTH(DDATE => DDATE);
/* Исходим от даты (формируем группу на начало каждого квартала) */
case NMONTH
/* Первый квартал */
when 1 then
/* Добавляем группу */
CYCLOGRAM_BASE_INSERT(NIDENT => NIDENT, NTYPE => 1, SNAME => 'I группа', NRN => NRN);
/* Второй квартал */
when 4 then
/* Добавляем группу */
CYCLOGRAM_BASE_INSERT(NIDENT => NIDENT, NTYPE => 1, SNAME => 'II группа', NRN => NRN);
/* Третий квартал */
when 7 then
/* Добавляем группу */
CYCLOGRAM_BASE_INSERT(NIDENT => NIDENT, NTYPE => 1, SNAME => 'III группа', NRN => NRN);
/* Четвертый квартал */
when 10 then
/* ДОбавляем группу */
CYCLOGRAM_BASE_INSERT(NIDENT => NIDENT, NTYPE => 1, SNAME => 'IV группа', NRN => NRN);
else
null;
end case;
end INIT_GROUP;
begin
/* Удаляем старые данные из буфера */
if (NIDENT is not null) then
CYCLOGRAM_BASE_CLEAN(NIDENT => NIDENT);
else
/* Илиформируем новый идентификатор, если не задан */
NIDENT := GEN_IDENT();
end if;
/* Фиксируем текущий год */
NYEAR := EXTRACT(year from sysdate);
/* Фиксируем дату начала циклограммы */
DCYCLOGRAM_START := TO_DATE('01.01.' || NYEAR, 'dd.mm.yyyy');
/* Добавляем колонки и групповые задачи (месяцы года) */
for I in 0 .. 11
loop
/* Рассчитываем текущий месяц */
DMONTH_CUR := ADD_MONTHS(DCYCLOGRAM_START, I);
/* Считываем первый и последний день месяца */
P_FIRST_LAST_DAY(DCALCDATE => DMONTH_CUR, DBGNDATE => DMONTH_START, DENDDATE => DMONTH_END);
/* Рассчитываем количество дней месяца */
NMONTH_DAYS := DMONTH_END - DMONTH_START + 1;
/* Рассчитываем позицию окончания элемента */
NEND := NSTART + (NMONTH_DAYS * NCG_MULTIPLIER);
/* Определяем номер месяца */
NMONTH := D_MONTH(DDATE => DMONTH_CUR);
/* Добавляем колонку в таблицу */
CYCLOGRAM_BASE_INSERT(NIDENT => NIDENT,
NTYPE => 0,
SNAME => TO_CHAR(NMONTH),
NPOS_START => NSTART,
NPOS_END => NEND,
NRN => NDUMMY);
/* Инициализируем группу */
INIT_GROUP(NIDENT => NIDENT, DDATE => DMONTH_CUR, NRN => NGROUP);
/* Добавляем задачу */
CYCLOGRAM_BASE_INSERT(NIDENT => NIDENT,
NTYPE => 2,
SNAME => 'Работа ' || TO_CHAR(I + 1),
NPOS_START => NSTART,
NPOS_END => NEND,
DDATE_FROM => DMONTH_START,
DDATE_TO => DMONTH_END,
NTASK_GROUP => NGROUP,
NRN => NDUMMY);
/* Если это февраль, май, август или ноябрь - добавляем особосбленную задачу */
if (NMONTH in (2, 5, 8, 11)) then
/* Добавляем обособленную задачу */
CYCLOGRAM_BASE_INSERT(NIDENT => NIDENT,
NTYPE => 2,
SNAME => 'Обособленная работа ' || NMONTH,
NPOS_START => NSTART,
NPOS_END => NEND,
DDATE_FROM => DMONTH_START,
DDATE_TO => DMONTH_END,
NRN => NDUMMY);
end if;
/* Рассчитываем начало следующего месяца */
NSTART := NEND;
end loop;
end CYCLOGRAM_INIT;
/* Сбор данных для отображения циклограммы */
procedure CYCLOGRAM
(
NIDENT in number, -- Идентификатор процесса
COUT out clob -- Сериализованные данные для циклограммы
)
is
CG PKG_P8PANELS_VISUAL.TCYCLOGRAM; -- Описание циклограммы
RTASK PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK; -- Описание задачи циклограммы
NLINE_NUMB PKG_STD.TNUMBER := 0; -- Номер строки
NLINE_NUMB_TMP PKG_STD.TNUMBER := 0; -- Номер строки (буфер)
DTASK_DATE_START PKG_STD.TLDATE; -- Дата начала этапа
DTASK_DATE_END PKG_STD.TLDATE; -- Дата окончания этапа
NTASK_START PKG_STD.TNUMBER := 0; -- Позиция начала этапа
NTASK_END PKG_STD.TNUMBER := 0; -- Позиция окончания этапа
STASK_NAME PKG_STD.TSTRING; -- Наименование задачи
SCOLOR_WHITE PKG_STD.TSTRING := 'white'; -- Цвет - белый
SBG_TASK_COLOR_W_GRP PKG_STD.TSTRING := '#6bc982'; -- Цвет задачи с группой
SHL_TASK_COLOR_W_GRP PKG_STD.TSTRING := '#7dd592'; -- Цвет наведения задачи с группой
SBG_TASK_COLOR_WO_GRP PKG_STD.TSTRING := '#e36d6d'; -- Цвет задачи без группы
STEXT_COLOR_TASK_WO_GRP PKG_STD.TSTRING := '#e5e5e5'; -- Цвет текста задачи без группы
SBG_TASK_COLOR_GRP PKG_STD.TSTRING := 'cadetblue'; -- Цвет групповой задачи
SHL_TASK_COLOR_GRP PKG_STD.TSTRING := '#6fadaf'; -- Цвет наведения групповой задачи
/* Считывание значений группирующей задачи */
procedure GROUP_TASK_GET
(
NIDENT in number, -- Идентификатор процесса
NGROUP in number := null, -- Рег. номер группы
NFLAG_WO_GROUP in number := 0, -- Признак отбора задач без групп (0 - нет, 1 - да)
DTASK_DATE_START out date, -- Дата начала этапа
DTASK_DATE_END out date, -- Дата окончания этапа
NTASK_START out number, -- Позиция начала этапа
NTASK_END out number -- Позиция окончания этапа
)
is
begin
/* Считываем начало и окончание этапа */
begin
select min(T.DATE_FROM),
max(T.DATE_TO),
min(T.POS_START),
max(T.POS_END)
into DTASK_DATE_START,
DTASK_DATE_END,
NTASK_START,
NTASK_END
from P8PNL_SMPL_CYCLOGRAM T
where T.IDENT = NIDENT
and (((NFLAG_WO_GROUP = 1) and (T.TASK_GROUP is null)) or ((NFLAG_WO_GROUP = 0) and (T.TASK_GROUP is not null)))
and ((NGROUP is null) or ((NGROUP is not null) and (T.TASK_GROUP = NGROUP)))
and T.TYPE = 2;
end;
end GROUP_TASK_GET;
begin
/* Инициализируем циклограмму */
CG := PKG_P8PANELS_VISUAL.TCYCLOGRAM_MAKE(STITLE => 'Задачи на ' || TO_CHAR(EXTRACT(year from sysdate)) || ' год');
/* Добавляем атрибуты */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK_ATTR(RCYCLOGRAM => CG,
SNAME => 'ddate_start',
SCAPTION => 'Дата начала',
BVISIBLE => true,
BCLEAR => true);
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK_ATTR(RCYCLOGRAM => CG,
SNAME => 'ddate_end',
SCAPTION => 'Дата окончания',
BVISIBLE => true);
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK_ATTR(RCYCLOGRAM => CG,
SNAME => 'type',
SCAPTION => 'Тип',
BVISIBLE => false);
/* Обходим колонки */
for CLMN in (select T.NAME,
T.POS_START,
T.POS_END
from P8PNL_SMPL_CYCLOGRAM T
where T.IDENT = NIDENT
and T.TYPE = 0)
loop
/* Добавляем колонку */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_COLUMN(RCYCLOGRAM => CG,
SNAME => CLMN.NAME,
NSTART => CLMN.POS_START,
NEND => CLMN.POS_END);
end loop;
/* Считываем значения для задач проекта */
GROUP_TASK_GET(NIDENT => NIDENT,
NFLAG_WO_GROUP => 0,
DTASK_DATE_START => DTASK_DATE_START,
DTASK_DATE_END => DTASK_DATE_END,
NTASK_START => NTASK_START,
NTASK_END => NTASK_END);
/* Формируем задачу (этап) */
RTASK := PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_MAKE(NRN => 1,
SCAPTION => 'Задачи проекта',
SNAME => 'Задачи проекта',
NLINE_NUMB => NLINE_NUMB,
NSTART => NTASK_START,
NEND => NTASK_END,
SBG_COLOR => SCOLOR_WHITE);
/* Добавляем атрибуты */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_start',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => DTASK_DATE_START),
BCLEAR => true);
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_end',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => DTASK_DATE_END));
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG, RTASK => RTASK, SNAME => 'type', SVALUE => 0);
/* Добавляем задачу в циклограмму */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK(RCYCLOGRAM => CG, RTASK => RTASK);
/* Указываем следующую строку */
NLINE_NUMB := NLINE_NUMB + 1;
/* Обходим группы */
for GRP in (select T.RN,
T.NAME,
ROWNUM RNUM
from P8PNL_SMPL_CYCLOGRAM T
where T.IDENT = NIDENT
and T.TYPE = 1
order by T.RN asc)
loop
/* Если это вторая группа - сохраним номер строки */
if (GRP.RNUM = 2) then
NLINE_NUMB_TMP := NLINE_NUMB;
end if;
/* Если это третья группа - вернемся на уровень второй группы */
if (GRP.RNUM = 3) then
NLINE_NUMB := NLINE_NUMB_TMP;
end if;
/* Добавляем группу */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_GROUP(RCYCLOGRAM => CG,
SNAME => GRP.NAME,
NHEADER_HEIGHT => 30,
NHEADER_WIDTH => 200);
/* Считываем значения этапа группы */
GROUP_TASK_GET(NIDENT => NIDENT,
NGROUP => GRP.RN,
DTASK_DATE_START => DTASK_DATE_START,
DTASK_DATE_END => DTASK_DATE_END,
NTASK_START => NTASK_START,
NTASK_END => NTASK_END);
/* Формируем задачу (этап) */
RTASK := PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_MAKE(NRN => GRP.RN,
SCAPTION => 'Этап ' || TO_CHAR(GRP.RNUM),
SNAME => 'Этап ' || TO_CHAR(GRP.RNUM),
NLINE_NUMB => NLINE_NUMB,
NSTART => NTASK_START,
NEND => NTASK_END,
SBG_COLOR => SBG_TASK_COLOR_GRP,
STEXT_COLOR => SCOLOR_WHITE,
SHIGHLIGHT_COLOR => SHL_TASK_COLOR_GRP);
/* Добавляем атрибуты */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_start',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => DTASK_DATE_START),
BCLEAR => true);
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_end',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => DTASK_DATE_END));
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG, RTASK => RTASK, SNAME => 'type', SVALUE => 1);
/* Добавляем задачу в циклограмму */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK(RCYCLOGRAM => CG, RTASK => RTASK);
/* Обходим задачи группы */
for TASK in (select T.RN,
T.NAME,
T.POS_START,
T.POS_END,
T.DATE_FROM,
T.DATE_TO,
ROWNUM RNUM
from P8PNL_SMPL_CYCLOGRAM T
where T.IDENT = NIDENT
and T.TYPE = 2
and T.TASK_GROUP = GRP.RN)
loop
/* Указываем следующую строку */
NLINE_NUMB := NLINE_NUMB + 1;
/* Формируем наименование задачи */
STASK_NAME := 'Работа ' || TO_CHAR(TASK.RNUM) || ' этапа ' || TO_CHAR(GRP.RNUM);
/* Формируем задачу */
RTASK := PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_MAKE(NRN => TASK.RN,
SCAPTION => STASK_NAME,
SNAME => STASK_NAME,
NLINE_NUMB => NLINE_NUMB,
NSTART => TASK.POS_START,
NEND => TASK.POS_END,
SGROUP => GRP.NAME,
SBG_COLOR => SBG_TASK_COLOR_W_GRP,
STEXT_COLOR => SCOLOR_WHITE,
SHIGHLIGHT_COLOR => SHL_TASK_COLOR_W_GRP);
/* Добавляем атрибуты */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_start',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => TASK.DATE_FROM),
BCLEAR => true);
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_end',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => TASK.DATE_TO));
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'type',
SVALUE => 2);
/* Добавляем задачу в циклограмму */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK(RCYCLOGRAM => CG, RTASK => RTASK);
end loop;
/* Указываем следующую строку */
NLINE_NUMB := NLINE_NUMB + 1;
end loop;
/* Указываем следующую строку */
NLINE_NUMB := NLINE_NUMB + 1;
/* Считываем значения для обособленных задач */
GROUP_TASK_GET(NIDENT => NIDENT,
NFLAG_WO_GROUP => 1,
DTASK_DATE_START => DTASK_DATE_START,
DTASK_DATE_END => DTASK_DATE_END,
NTASK_START => NTASK_START,
NTASK_END => NTASK_END);
/* Формируем задачу (этап) */
RTASK := PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_MAKE(NRN => 1,
SCAPTION => 'Обособленные задачи',
SNAME => 'Обособленные задачи',
NLINE_NUMB => NLINE_NUMB,
NSTART => NTASK_START,
NEND => NTASK_END,
SBG_COLOR => SCOLOR_WHITE);
/* Добавляем атрибуты */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_start',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => DTASK_DATE_START),
BCLEAR => true);
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_end',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => DTASK_DATE_END));
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG, RTASK => RTASK, SNAME => 'type', SVALUE => 0);
/* Добавляем задачу в циклограмму */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK(RCYCLOGRAM => CG, RTASK => RTASK);
/* Указываем следующую строку */
NLINE_NUMB := NLINE_NUMB + 1;
/* Цикл по обособленным задачам */
for REC in (select T.RN,
T.NAME,
T.POS_START,
T.POS_END,
T.DATE_FROM,
T.DATE_TO,
ROWNUM RNUM
from P8PNL_SMPL_CYCLOGRAM T
where T.IDENT = NIDENT
and T.TYPE = 2
and T.TASK_GROUP is null)
loop
/* Формируем наименование задачи */
STASK_NAME := 'Работа ' || TO_CHAR(REC.RNUM) || ' без этапа ';
/* Формируем задачу */
RTASK := PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_MAKE(NRN => REC.RN,
SCAPTION => STASK_NAME,
SNAME => STASK_NAME,
NLINE_NUMB => NLINE_NUMB,
NSTART => REC.POS_START,
NEND => REC.POS_END,
SBG_COLOR => SBG_TASK_COLOR_WO_GRP,
STEXT_COLOR => STEXT_COLOR_TASK_WO_GRP);
/* Добавляем атрибуты */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_start',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => REC.DATE_FROM),
BCLEAR => true);
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG,
RTASK => RTASK,
SNAME => 'ddate_end',
SVALUE => PKG_XCONVERT.DATE_TO_XML(DVALUE => REC.DATE_TO));
PKG_P8PANELS_VISUAL.TCYCLOGRAM_TASK_ADD_ATTR_VAL(RCYCLOGRAM => CG, RTASK => RTASK, SNAME => 'type', SVALUE => 2);
/* Добавляем задачу в циклограмму */
PKG_P8PANELS_VISUAL.TCYCLOGRAM_ADD_TASK(RCYCLOGRAM => CG, RTASK => RTASK);
end loop;
/* Формируем список */
COUT := PKG_P8PANELS_VISUAL.TCYCLOGRAM_TO_XML(RCYCLOGRAM => CG);
end CYCLOGRAM;
/* Изменение задачи циклограммы */
procedure CYCLOGRAM_TASK_MODIFY
(
NIDENT in number, -- Идентификатор буфера
NRN in number, -- Рег. номер записи
SDATE_FROM in varchar2, -- Дата начала (в строковом представлении)
SDATE_TO in varchar2 -- Дата окончания (в строковом представлении)
)
is
DDATE_FROM PKG_STD.TLDATE; -- Дата начала
DDATE_TO PKG_STD.TLDATE; -- Дата окончания
NYEAR PKG_STD.TNUMBER; -- Текущий год
DCYCLOGRAM_START PKG_STD.TLDATE; -- Дата начала циклограммы
NSTART PKG_STD.TNUMBER; -- Позиция начала элемента
NEND PKG_STD.TNUMBER; -- Позиция окончания элемента
begin
/* Фиксируем текущий год */
NYEAR := EXTRACT(year from sysdate);
/* Переводим даты */
DDATE_FROM := TO_DATE(SDATE_FROM, 'dd.mm.yyyy');
DDATE_TO := TO_DATE(SDATE_TO, 'dd.mm.yyyy');
/* Если дата начала выходит за границы года */
if (D_YEAR(DDATE => DDATE_FROM) <> NYEAR) then
P_EXCEPTION(0,
'Дата начала задачи выходит за границы текущего года (%s).',
NYEAR);
end if;
/* Если дата окончания выходит за границы года */
if (D_YEAR(DDATE => DDATE_TO) <> NYEAR) then
P_EXCEPTION(0,
'Дата окончания задачи выходит за границы текущего года (%s).',
NYEAR);
end if;
/* Дата окончания не может быть меньше даты начала */
if (DDATE_TO < DDATE_FROM) then
P_EXCEPTION(0, 'Дата окончания не может быть меньше даты начала.');
end if;
/* Фиксируем дату начала циклограммы */
DCYCLOGRAM_START := TO_DATE('01.01.' || NYEAR, 'dd.mm.yyyy');
/* Рассчитываем новую позицию начала */
NSTART := (DDATE_FROM - DCYCLOGRAM_START) * NCG_MULTIPLIER;
/* Рассчитываем новую позицию окончания */
NEND := NSTART + ((DDATE_TO - DDATE_FROM + 1) * NCG_MULTIPLIER);
/* Считываем запись */
for REC in (select T.*
from P8PNL_SMPL_CYCLOGRAM T
where T.RN = NRN
and T.IDENT = NIDENT)
loop
/* Обновляем запись циклограммы */
CYCLOGRAM_BASE_UPDATE(NIDENT => REC.IDENT,
NRN => REC.RN,
NTYPE => REC.TYPE,
SNAME => REC.NAME,
NPOS_START => NSTART,
NPOS_END => NEND,
DDATE_FROM => DDATE_FROM,
DDATE_TO => DDATE_TO,
NTASK_GROUP => REC.TASK_GROUP);
end loop;
end CYCLOGRAM_TASK_MODIFY;
end PKG_P8PANELS_SAMPLES; end PKG_P8PANELS_SAMPLES;
/ /

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

102
dist/p8-panels.js vendored

File diff suppressed because one or more lines are too long

BIN
docs/img/72.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/img/73.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@ -16,7 +16,7 @@
<script src="./libs/frappe-gantt/frappe-gantt.js"></script> <script src="./libs/frappe-gantt/frappe-gantt.js"></script>
<link rel="stylesheet" href="./libs/frappe-gantt/frappe-gantt.css" /> <link rel="stylesheet" href="./libs/frappe-gantt/frappe-gantt.css" />
</head> </head>
<body style="display: block; margin: 0px"> <body style="display: block; margin: 0px" class="scroll">
<div id="app-content"></div> <div id="app-content"></div>
<script src="dist/p8-panels.js"></script> <script src="dist/p8-panels.js"></script>
</body> </body>