diff --git a/docs/form-readme-template.md b/docs/form-readme-template.md new file mode 100644 index 0000000..b684001 --- /dev/null +++ b/docs/form-readme-template.md @@ -0,0 +1,27 @@ +# <Название формы> (<ключ-rptype>) + +**Назначение:** <одна строка — что это за заявка> +**Категория:** <Рекрутинг / IT / Бухгалтерия / Отдел кадров> +**Списки SP:** «<...> — данные»<, «<...> — согласование» если есть> +**RequestTypeId (внешний API маршрутов):** <число или — если согласования нет> + +## Маршрут +- Фазы: <нет / согласование / согласование + исполнение> +- Особенности: <напр. шаг 1 согласования — непосредственный руководитель; или «—»> + +## Особое поведение +<Только то, чем ЭТА форма отличается. Примеры пунктов:> +- Префилл «на основе» <какой заявки>: <какие поля копируются>. +- Поля, заполняемые согласующим: <какие, кем, когда>. +- Условные поля: <поле появляется при условии>. +- Маски/форматирование: . +- Нюансы печати: <если отличается от общего>. +- Является источником для «<другая форма>» (кнопка «создать на основе»). + +## Секции формы + + +## Точки входа +- rptype: `<ключ>` +- Режим согласующего (просмотр): <есть / нет> +- «Создать на основе»: <откуда → сюда, или —> diff --git a/docs/forms.md b/docs/forms.md new file mode 100644 index 0000000..ee795bc --- /dev/null +++ b/docs/forms.md @@ -0,0 +1,35 @@ +# Формы портала заявок — индекс + +Каждая форма живёт в своей папке `src/webparts/requestPortal/forms/<Форма>/` и имеет +рядом **`README.md`** — карточку с её **бизнес-поведением** (то, что не видно из кода +с первого взгляда). Здесь — общий индекс. + +## Правило (при добавлении формы) +1. Папка `forms/<Форма>/` + `README.md` по [шаблону](./form-readme-template.md). +2. Строка в индексе ниже. +3. Строка в `forms/registry.ts` (ленивая форма) и, если есть режим согласующего, + в `forms/viewRegistry.ts`. +4. Плитка в `data/requests.ts`. + +## Что НЕ дублируем в карточке (лежит в других местах) +- Колонки списков → `forms/<Форма>/provisioning.json`. +- Параметры ссылок (rptype/rpmode/rpid/rpsource) → [`routing.md`](./routing.md). +- Как обращаться к спискам SP из кода → [`sp-lists-frontend.md`](./sp-lists-frontend.md). +- Общий жизненный цикл (черновик/upsert/блокировка/статусы), согласование, исполнение, + печать — **одинаковы для всех форм** (общие примитивы `shared/ui` и `shared/approval`). + +Карточка — только про то, чем **эта** форма отличается. + +--- + +## Индекс форм + +| Ключ (rptype) | Форма | Категория | Согл. | Исп. | Просмотр | Фишки | Готово | +| --- | --- | --- | :---: | :---: | :---: | --- | :---: | +| [`recruiting-selection`](../src/webparts/requestPortal/forms/RecruitingSelection/README.md) | Заявка на подбор | Рекрутинг | нет | нет | нет | много полей (~42), поля-люди (в т.ч. мульти) | ✅ | +| [`recruiting-hire`](../src/webparts/requestPortal/forms/RecruitingHire/README.md) | Заявка на прием | Рекрутинг | да | да | да | маска телефона, деньги, условное «взамен кого», источник для орг. места | ✅ | +| [`it-workplace-setup`](../src/webparts/requestPortal/forms/WorkplaceSetup/README.md) | Организация рабочего места | IT | да | да | да | префилл из «прием», поля ПО/доступы правит руководитель | ✅ | +| `hr-transfer` | Перемещение сотрудников | Отдел кадров | — | — | — | — | ⏳ заглушка (плитка есть, форма не сделана) | + +Легенда: **Согл.** — есть фаза согласования; **Исп.** — есть фаза исполнения; +**Просмотр** — есть режим согласующего (`<Форма>View`). diff --git a/provisioning/Provision-Lists.ps1 b/provisioning/Provision-Lists.ps1 index b60d1f0..a76dfaa 100644 --- a/provisioning/Provision-Lists.ps1 +++ b/provisioning/Provision-Lists.ps1 @@ -4,18 +4,27 @@ по манифестам provisioning.json (единый источник правды по колонкам). .DESCRIPTION - Для каждого файла forms/<Форма>/provisioning.json скрипт: - 1) создаёт список данных (если его ещё нет); + Обходит ВСЕ манифесты forms/**/provisioning.json (в т.ч. подпапки approval/) и для + каждого: + 1) создаёт список (если его ещё нет); 2) при необходимости переименовывает колонку Title; - 3) добавляет недостающие колонки нужных типов. - Повторный запуск безопасен: существующие список и колонки не трогаются - (перед созданием колонки проверяется, что её ещё нет). + 3) обеспечивает колонки. + + Колонки — через СТОЛБЦЫ САЙТА (эталоны): + - не-Lookup колонка сначала создаётся как столбец сайта (группа «Портал заявок»), + один раз на весь сайт, затем ПОДКЛЮЧАЕТСЯ к списку по ссылке. Так одинаковые + колонки (Status, Phase, StepOrder …) определены в одном месте и единообразны; + менять их (напр. варианты Choice) можно в эталоне. Choice при повторе объединяет + варианты (union) — наборы разных форм не затирают друг друга. + - Lookup остаётся столбцом СПИСКА: эталон-lookup привязан к одному списку, а у нас + Ticket у каждого списка согласования ссылается на свой список данных. + + Повторный запуск безопасен (идемпотентно): существующие эталоны/колонки не + пересоздаются. Имена колонок (internalName) — как в манифестах, к ним обращается код. Вход — по сертификату «от имени приложения» (app-only, право Sites.Selected). Сертификат берётся из личного хранилища по отпечатку (Thumbprint). - Списки согласования пока НЕ создаются — только данные (по договорённости). - .PARAMETER SiteUrl URL сайта, например https://tnsukz.sharepoint.com/sites/requests @@ -52,90 +61,124 @@ function ConvertTo-XmlSafe { return $Value.Replace('&', '&').Replace('<', '<').Replace('>', '>').Replace("'", ''').Replace('"', '"') } +# Есть ли колонка НА СПИСКЕ (list-scope). function Test-FieldExists { param([string]$List, [string]$InternalName) return $null -ne (Get-PnPField -List $List -Identity $InternalName -ErrorAction SilentlyContinue) } -# Создаёт одну колонку по описанию из манифеста (идемпотентно). -function Add-Column { - param([string]$List, $Col) +# Есть ли СТОЛБЕЦ САЙТА (site-scope, галерея столбцов сайта). +function Test-SiteFieldExists { + param([string]$InternalName) + return $null -ne (Get-PnPField -Identity $InternalName -ErrorAction SilentlyContinue) +} - if (Test-FieldExists -List $List -InternalName $Col.internalName) { - # Колонка уже есть. Для Choice — досетапливаем варианты (могли добавиться новые - # статусы): существующие значения сохраняются, добавляются недостающие. +$SITE_COLUMN_GROUP = 'Портал заявок' + +# Создаёт/обновляет СТОЛБЕЦ САЙТА (эталон) — один раз на весь сайт. Для Choice при +# повторе объединяет варианты (union), чтобы наборы разных форм не затирали друг друга. +# Lookup сюда НЕ попадает (эталоном быть не может — привязка к конкретному списку). +function Ensure-SiteColumn { + param($Col) + + if (Test-SiteFieldExists -InternalName $Col.internalName) { if ($Col.type -eq 'Choice' -and $Col.choices) { - try { - Set-PnPField -List $List -Identity $Col.internalName -Values @{ Choices = [string[]]$Col.choices } | Out-Null - Write-Host " [~] варианты обновлены: $($Col.internalName)" -ForegroundColor DarkGray - } - catch { - Write-Warning " [!] $($Col.internalName): не удалось обновить варианты — $($_.Exception.Message)" - } - } - else { - Write-Host " [=] колонка есть: $($Col.internalName)" -ForegroundColor DarkGray + $existing = (Get-PnPField -Identity $Col.internalName).Choices + $merged = @($existing) + @($Col.choices) | Select-Object -Unique + Set-PnPField -Identity $Col.internalName -Values @{ Choices = [string[]]$merged } | Out-Null } return } + $g = $SITE_COLUMN_GROUP + switch ($Col.type) { + 'Text' { Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type Text -Group $g | Out-Null } + 'Note' { Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type Note -Group $g | Out-Null } + 'DateTime' { + Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type DateTime -Group $g | Out-Null + if ($Col.dateOnly) { + Set-PnPField -Identity $Col.internalName -Values @{ DisplayFormat = 0; FriendlyDisplayFormat = 1 } | Out-Null + } + } + 'Boolean' { Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type Boolean -Group $g | Out-Null } + 'User' { + if ($Col.allowMultiple) { + $xml = "" + Add-PnPFieldFromXml -FieldXml $xml | Out-Null + } + else { + Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type User -Group $g | Out-Null + } + if ($Col.userSelectionMode) { + $mode = if ($Col.userSelectionMode -eq 'PeopleAndGroups') { 1 } else { 0 } + Set-PnPField -Identity $Col.internalName -Values @{ SelectionMode = $mode } | Out-Null + } + } + 'Choice' { Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type Choice -Choices $Col.choices -Group $g | Out-Null } + 'Number' { + if ($null -ne $Col.decimals) { + $xml = "" + Add-PnPFieldFromXml -FieldXml $xml | Out-Null + } + else { + Add-PnPField -InternalName $Col.internalName -DisplayName $Col.title -Type Number -Group $g | Out-Null + } + } + default { throw "Неизвестный тип столбца сайта '$($Col.type)'" } + } + Write-Host " [+эталон] $($Col.internalName) — «$($Col.title)» [$($Col.type)]" -ForegroundColor Green +} + +# Обеспечивает колонку НА СПИСКЕ. Не-Lookup — из эталона (столбец сайта → подключить). +# Lookup — обычным столбцом списка (эталоном быть не может). +function Add-ColumnToList { + param([string]$List, $Col) + + if ($Col.type -eq 'Lookup') { + if (Test-FieldExists -List $List -InternalName $Col.internalName) { + Write-Host " [=] lookup есть: $($Col.internalName)" -ForegroundColor DarkGray + return + } + try { + $src = Get-PnPList -Identity $Col.lookupList -ErrorAction Stop + $guid = $src.Id.ToString() + $show = if ($Col.lookupShowField) { $Col.lookupShowField } else { 'Title' } + $xml = "" + Add-PnPFieldFromXml -List $List -FieldXml $xml | Out-Null + Write-Host " [+] lookup (столбец списка): $($Col.internalName)" -ForegroundColor Green + } + catch { + Write-Warning " [!] $($Col.internalName): $($_.Exception.Message)" + } + return + } + + # 1) эталон на сайте (создать/обновить) try { - switch ($Col.type) { - 'Text' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Text -AddToDefaultView | Out-Null } - 'Note' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Note -AddToDefaultView | Out-Null } - 'DateTime' { - Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type DateTime -AddToDefaultView | Out-Null - if ($Col.dateOnly) { - # 0 = только дата; 1 = отключить относительный формат («Завтра», «вчера») - Set-PnPField -List $List -Identity $Col.internalName -Values @{ DisplayFormat = 0; FriendlyDisplayFormat = 1 } | Out-Null - } - } - 'Boolean' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Boolean -AddToDefaultView | Out-Null } - 'User' { - if ($Col.allowMultiple) { - # Мульти-пользователь: только через XML (UserMulti нет в enum FieldType). - $xml = "" - Add-PnPFieldFromXml -List $List -FieldXml $xml | Out-Null - } - else { - Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type User -AddToDefaultView | Out-Null - } - if ($Col.userSelectionMode) { - # 0 = только люди, 1 = люди и группы - $mode = if ($Col.userSelectionMode -eq 'PeopleAndGroups') { 1 } else { 0 } - Set-PnPField -List $List -Identity $Col.internalName -Values @{ SelectionMode = $mode } | Out-Null - } - } - 'Choice' { Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Choice -Choices $Col.choices -AddToDefaultView | Out-Null } - 'Number' { - if ($null -ne $Col.decimals) { - # Точность задаётся только через XML. - $xml = "" - Add-PnPFieldFromXml -List $List -FieldXml $xml | Out-Null - } - else { - Add-PnPField -List $List -InternalName $Col.internalName -DisplayName $Col.title -Type Number -AddToDefaultView | Out-Null - } - } - 'Lookup' { - # Список-источник должен уже существовать (см. про порядок в README). - $src = Get-PnPList -Identity $Col.lookupList -ErrorAction Stop - $guid = $src.Id.ToString() - $show = if ($Col.lookupShowField) { $Col.lookupShowField } else { 'Title' } - $xml = "" - Add-PnPFieldFromXml -List $List -FieldXml $xml | Out-Null - } - default { throw "Неизвестный тип колонки '$($Col.type)'" } - } - - if ($Col.required) { - Set-PnPField -List $List -Identity $Col.internalName -Values @{ Required = $true } | Out-Null - } - - Write-Host " [+] $($Col.internalName) — «$($Col.title)» [$($Col.type)]" -ForegroundColor Green + Ensure-SiteColumn -Col $Col } catch { - Write-Warning " [!] $($Col.internalName): $($_.Exception.Message)" + Write-Warning " [!] эталон $($Col.internalName): $($_.Exception.Message)" + return + } + + # 2) подключить эталон к списку (если ещё не подключён) + if (Test-FieldExists -List $List -InternalName $Col.internalName) { + Write-Host " [=] уже на списке: $($Col.internalName)" -ForegroundColor DarkGray + } + else { + try { + Add-PnPField -List $List -Field $Col.internalName -AddToDefaultView | Out-Null + Write-Host " [+] подключён эталон: $($Col.internalName)" -ForegroundColor Green + } + catch { + Write-Warning " [!] подключение $($Col.internalName): $($_.Exception.Message)" + } + } + + # 3) обязательность — настройка уровня списка (у поля может отличаться по спискам) + if ($Col.required) { + try { Set-PnPField -List $List -Identity $Col.internalName -Values @{ Required = $true } | Out-Null } catch {} } } @@ -174,9 +217,9 @@ foreach ($manifest in $manifests) { } } - # 3) Колонки (идемпотентно) + # 3) Колонки (идемпотентно): не-Lookup — через эталоны (столбцы сайта), Lookup — на список. foreach ($col in $def.columns) { - Add-Column -List $listTitle -Col $col + Add-ColumnToList -List $listTitle -Col $col } } diff --git a/src/webparts/requestPortal/forms/RecruitingHire/README.md b/src/webparts/requestPortal/forms/RecruitingHire/README.md new file mode 100644 index 0000000..10b196a --- /dev/null +++ b/src/webparts/requestPortal/forms/RecruitingHire/README.md @@ -0,0 +1,30 @@ +# Заявка на прием (recruiting-hire) + +**Назначение:** приём сотрудника на работу. +**Категория:** Отдел рекрутинга +**Списки SP:** «Прием на работу — данные» / «Прием на работу — согласование» +**RequestTypeId (внешний API маршрутов):** 5 + +## Маршрут +- Фазы: согласование + исполнение. +- Исполнители подтягиваются из Execution-маршрута; если их нет — заявка финиширует + на «Согласовано» (блок «Исполнение» не показывается). + +## Особое поведение +- **Маска телефона** (PhoneInput) на поле «Телефон». +- **Разделитель тысяч** (MoneyInput) на окладах (испытательный срок / после). +- **Условное поле «Взамен кого» (ReplacedEmployee)** — показывается только когда + тип приёма = замена уволенного (см. `HIRE_TYPE_REPLACEMENT` в модели). +- Секция **СИЗ** (рост, размеры одежды/обуви). +- **Является источником для «Организация рабочего места»** — из просмотра этой + заявки кнопка «Организация рабочего места на основе этой» открывает форму + it-workplace-setup с префиллом (rpsource = id этой заявки). +- Печать — общий механизм (без «Вложений»). + +## Секции формы +ApplicationSection · CandidateSection · PpeSection · ConditionsSection + +## Точки входа +- rptype: `recruiting-hire` +- Режим согласующего (просмотр): есть (`RecruitingHireView`) +- «Создать на основе»: эта заявка → источник для `it-workplace-setup` diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/README.md b/src/webparts/requestPortal/forms/RecruitingSelection/README.md new file mode 100644 index 0000000..762838c --- /dev/null +++ b/src/webparts/requestPortal/forms/RecruitingSelection/README.md @@ -0,0 +1,30 @@ +# Заявка на подбор (recruiting-selection) + +**Назначение:** заявка на подбор персонала / открытие вакансии. +**Категория:** Отдел рекрутинга +**Списки SP:** «Заявка на подбор — данные» (только данные — согласования нет) +**RequestTypeId (внешний API маршрутов):** — (маршрут не используется) + +## Маршрут +- Фаз нет: отправка = запись в список данных, без согласования и исполнения. + +## Особое поведение +- **Самая большая форма (~42 поля).** +- **Поля-люди:** `Responsible`, `ProposedMentor`, `DirectManager` — один человек; + `HiringDecisionMaker` — несколько (мульти). Пишутся как `Id` + (см. `submit.ts` → `resolveUserId`). +- **Нет режима согласующего** (в `viewRegistry` не зарегистрирована) и **нет общего + жизненного цикла** (черновик/статус/upsert) — это ранняя форма, простая отправка + через `submitRecruitingSelection`. + +## Секции формы +См. `sections/` формы. + +## Точки входа +- rptype: `recruiting-selection` +- Режим согласующего (просмотр): нет +- «Создать на основе»: — + +> Замечание: сделана до введения общего жизненного цикла и согласования. При +> необходимости приводится к общему шаблону (`useRequestSubmit` + `SubmitActions` +> + маршрут согласования), как в остальных формах. diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/README.md b/src/webparts/requestPortal/forms/WorkplaceSetup/README.md new file mode 100644 index 0000000..38392dc --- /dev/null +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/README.md @@ -0,0 +1,32 @@ +# Организация рабочего места (it-workplace-setup) + +**Назначение:** подготовка рабочего места сотрудника (техника, ПО, доступы). +**Категория:** IT +**Списки SP:** «Организация рабочего места — данные» (+ lookup `SourceHire` → +«Прием на работу — данные») / «Организация рабочего места — согласование» +**RequestTypeId (внешний API маршрутов):** 7 + +## Маршрут +- Фазы: согласование + исполнение. +- **Шаг 1 согласования — непосредственный руководитель** (роль в маршруте). + +## Особое поведение +- **Префилл «на основе» заявки на прием** (открывается по `rpsource`): копируются + ФИО, телефон, должность, отдел/участок, вид работ, местоположение, дата приёма + (`loadPrefillFromHire`). Это снимок на момент создания; ссылка на источник + сохраняется в колонке-lookup `SourceHire`. +- **Поля «Необходимое ПО» и «Доступы» заполняет согласующий-руководитель** на своём + этапе (инициатор их обычно не знает). Доступны для правки, когда очередь дошла до + **первого** шага согласования и это текущий пользователь; сохраняются кнопкой + «Сохранить» и автоматически при «Согласовать» (`updateReviewFields`, + `DecisionPanel.onBeforeDecide`). Остальная форма при этом read-only. +- **Маска телефона** (PhoneInput). +- Печать — общий механизм (без «Вложений»). + +## Секции формы +EmployeeSection · EquipmentSection + +## Точки входа +- rptype: `it-workplace-setup` +- Режим согласующего (просмотр): есть (`WorkplaceSetupView`) +- «Создать на основе»: создаётся из просмотра заявки `recruiting-hire` diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.module.scss b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.module.scss index 6a27763..e1f27e3 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.module.scss +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.module.scss @@ -12,3 +12,33 @@ justify-content: flex-end; margin-bottom: $sp-4; } + +.reviewBar { + display: flex; + align-items: center; + gap: $sp-3; + margin-bottom: $sp-4; + padding: $sp-3 $sp-4; + border: 1px solid $gold-300; + border-radius: $r-md; + background: $gold-50; +} + +.reviewHint { + flex: 1; + min-width: 0; + font-size: $fs-sm; + color: $navy-800; +} + +.savedNote { + font-size: $fs-sm; + color: $green-600; + font-weight: 600; +} + +.saveError { + font-size: $fs-sm; + color: $red-500; + font-weight: 600; +} diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx index db40ca3..a6c5839 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx @@ -2,7 +2,16 @@ import * as React from 'react'; import { useState, useEffect, useMemo, useCallback } from 'react'; import type { WebPartContext } from '@microsoft/sp-webpart-base'; import styles from './WorkplaceSetupView.module.scss'; -import { Alert, FormCard, FormModeProvider, CollapseSection, FormField, AttachmentList, PrintButton } from '../../shared/ui'; +import { + Alert, + Button, + FormCard, + FormModeProvider, + CollapseSection, + FormField, + AttachmentList, + PrintButton +} from '../../shared/ui'; import { ApproverService, DecisionPanel, @@ -14,7 +23,7 @@ import { import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service'; import { IFormData, EMPTY_DATA, SetField } from './model'; import { loadWorkplaceRequest } from './load'; -import { workplaceApprovalService, DATA_LIST_TITLE } from './submit'; +import { workplaceApprovalService, updateReviewFields, DATA_LIST_TITLE } from './submit'; import { EmployeeSection } from './sections/EmployeeSection'; import { EquipmentSection } from './sections/EquipmentSection'; @@ -23,15 +32,21 @@ interface IWorkplaceSetupViewProps { itemId: number; } -const NOOP_SET: SetField = () => undefined; +type SaveState = 'idle' | 'saving' | 'saved' | 'error'; // Заявка «орг. рабочего места» только для просмотра (режим согласующего). +// Исключение: первый согласующий шаг (по маршруту — непосредственный руководитель) +// на своём этапе может заполнить «Необходимое ПО» и «Доступы» (инициатор их не знает). const WorkplaceSetupView: React.FC = ({ context, itemId }) => { const [data, setData] = useState(EMPTY_DATA); const [status, setStatus] = useState(''); const [attachments, setAttachments] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [canEditFields, setCanEditFields] = useState(false); + const [saveState, setSaveState] = useState('idle'); + + const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value })); const approverService = useMemo( () => new ApproverService(workplaceApprovalService(), DATA_LIST_TITLE), @@ -85,6 +100,45 @@ const WorkplaceSetupView: React.FC = ({ context, itemI }; }, [itemId]); + // Правка «ПО»/«Доступы» доступна, когда сейчас очередь ПЕРВОГО шага согласования + // (по маршруту — непосредственного руководителя) и это текущий пользователь. + useEffect(() => { + let alive = true; + Promise.all([ + approval.getForRequest(itemId), + approverService.getPendingRowForCurrentUser(itemId) + ]) + .then(([rows, pending]) => { + if (!alive) return; + const orders = rows.filter((r) => r.phase !== APPROVAL_PHASE.execution).map((r) => r.order); + const firstOrder = orders.length > 0 ? Math.min(...orders) : undefined; + setCanEditFields( + !!pending && pending.phase !== APPROVAL_PHASE.execution && pending.order === firstOrder + ); + }) + .catch(() => { + if (alive) setCanEditFields(false); + }); + return () => { + alive = false; + }; + }, [itemId, approval, approverService]); + + const saveReviewFields = useCallback( + () => updateReviewFields(itemId, { Software: data.Software, AccessRights: data.AccessRights }), + [itemId, data.Software, data.AccessRights] + ); + + const handleSave = async (): Promise => { + setSaveState('saving'); + try { + await saveReviewFields(); + setSaveState('saved'); + } catch { + setSaveState('error'); + } + }; + if (loading) { return
Загрузка заявки…
; } @@ -96,7 +150,7 @@ const WorkplaceSetupView: React.FC = ({ context, itemI ); } - const sectionProps = { data, set: NOOP_SET, context }; + const sectionProps = { data, set, context }; return (
@@ -104,6 +158,19 @@ const WorkplaceSetupView: React.FC = ({ context, itemI
+ {canEditFields && ( +
+ + Как согласующий вы можете заполнить «Необходимое ПО» и «Доступы». + + + {saveState === 'saved' && Сохранено} + {saveState === 'error' && Ошибка сохранения} +
+ )} +
= ({ context, itemI > - +
@@ -138,7 +205,11 @@ const WorkplaceSetupView: React.FC = ({ context, itemI
- +
); diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/sections/EquipmentSection.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/sections/EquipmentSection.tsx index 9fbf954..4fe6bd6 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/sections/EquipmentSection.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/sections/EquipmentSection.tsx @@ -2,16 +2,28 @@ import * as React from 'react'; import { CollapseSection, FormField, TextArea, Toggle } from '../../../shared/ui'; import { ISectionProps } from '../model'; -export const EquipmentSection: React.FC = ({ data, set }) => ( - - - set('NeedsEquipment', v)} onLabel="Да" /> - - -