diff --git a/provisioning/Fix-ColumnType.ps1 b/provisioning/Fix-ColumnType.ps1 new file mode 100644 index 0000000..e69d20b --- /dev/null +++ b/provisioning/Fix-ColumnType.ps1 @@ -0,0 +1,55 @@ +<# +.SYNOPSIS + Пересоздаёт колонку с нужным типом, когда фактический тип на сайте разошёлся с + манифестом (провижинер сам тип существующей колонки не меняет). Колонка сначала + снимается со списка и удаляется как столбец сайта, затем создаётся заново нужного + типа и подключается к списку. + + ВНИМАНИЕ: значения этой колонки в существующих элементах при удалении теряются. + +.EXAMPLE + # Починить BusinessTrips: сделать Boolean вместо Note + ./Fix-ColumnType.ps1 -SiteUrl "https://tnsukz.sharepoint.com/sites/requests" ` + -ClientId "7459c965-5ed4-4673-8140-0ab8faf9db64" -Tenant "tnsukz.onmicrosoft.com" ` + -Thumbprint "224B64C8E0CAB1E09AF373E92B3AF4AC33C1233C" ` + -ListTitle "Заявка на подбор 1 — данные" ` + -InternalName "BusinessTrips" -DisplayName "Командировки" -NewType Boolean +#> +param( + [Parameter(Mandatory = $true)][string]$SiteUrl, + [Parameter(Mandatory = $true)][string]$ClientId, + [Parameter(Mandatory = $true)][string]$Tenant, + [Parameter(Mandatory = $true)][string]$Thumbprint, + [Parameter(Mandatory = $true)][string]$ListTitle, + [Parameter(Mandatory = $true)][string]$InternalName, + [Parameter(Mandatory = $true)][string]$DisplayName, + [Parameter(Mandatory = $true)][ValidateSet('Boolean', 'Number', 'Text', 'Note', 'DateTime')][string]$NewType +) + +$ErrorActionPreference = 'Stop' +Connect-PnPOnline -Url $SiteUrl -ClientId $ClientId -Tenant $Tenant -Thumbprint $Thumbprint + +$group = 'Портал заявок' + +# 1) Снять колонку со списка (если подключена). +try { + Remove-PnPField -List $ListTitle -Identity $InternalName -Force -ErrorAction Stop + Write-Host " [~] снята со списка: $InternalName" -ForegroundColor Yellow +} catch { Write-Host " [ ] на списке не было отдельной ссылки: $InternalName" -ForegroundColor DarkGray } + +# 2) Удалить столбец сайта (каскадно снимает и оставшиеся ссылки). +try { + Remove-PnPField -Identity $InternalName -Force -ErrorAction Stop + Write-Host " [~] удалён столбец сайта: $InternalName" -ForegroundColor Yellow +} catch { Write-Host " [ ] столбца сайта не было: $InternalName" -ForegroundColor DarkGray } + +# 3) Создать заново нужного типа как столбец сайта. +Add-PnPField -InternalName $InternalName -DisplayName $DisplayName -Type $NewType -Group $group | Out-Null +Write-Host " [+] создан заново: $InternalName [$NewType]" -ForegroundColor Green + +# 4) Подключить к списку. +Add-PnPField -List $ListTitle -Field $InternalName | Out-Null +Write-Host " [+] подключён к списку «$ListTitle»" -ForegroundColor Green + +Write-Host "`nГотово." -ForegroundColor Cyan +Disconnect-PnPOnline diff --git a/provisioning/Inspect-Fields.ps1 b/provisioning/Inspect-Fields.ps1 new file mode 100644 index 0000000..4748374 --- /dev/null +++ b/provisioning/Inspect-Fields.ps1 @@ -0,0 +1,31 @@ +<# +.SYNOPSIS + Диагностика: печатает фактический тип колонок списка данных в SharePoint — + чтобы поймать расхождение с манифестом (напр. Number-поле реально Text). + Только чтение, ничего не меняет. + +.EXAMPLE + ./Inspect-Fields.ps1 -SiteUrl "https://tnsukz.sharepoint.com/sites/requests" ` + -ClientId "7459c965-5ed4-4673-8140-0ab8faf9db64" -Tenant "tnsukz.onmicrosoft.com" ` + -Thumbprint "224B64C8E0CAB1E09AF373E92B3AF4AC33C1233C" ` + -ListTitle "Заявка на подбор 1 — данные" +#> +param( + [Parameter(Mandatory = $true)][string]$SiteUrl, + [Parameter(Mandatory = $true)][string]$ClientId, + [Parameter(Mandatory = $true)][string]$Tenant, + [Parameter(Mandatory = $true)][string]$Thumbprint, + [Parameter(Mandatory = $true)][string]$ListTitle +) + +$ErrorActionPreference = 'Stop' +Connect-PnPOnline -Url $SiteUrl -ClientId $ClientId -Tenant $Tenant -Thumbprint $Thumbprint + +Write-Host "`nКолонки списка «$ListTitle»:`n" -ForegroundColor Cyan +Get-PnPField -List $ListTitle | + Where-Object { -not $_.Hidden -and $_.Group -ne '_Hidden' } | + Select-Object InternalName, TypeAsString, @{N='Mult';E={$_.TypeAsString -like '*Multi*'}} | + Sort-Object InternalName | + Format-Table -AutoSize + +Disconnect-PnPOnline diff --git a/src/webparts/requestPortal/forms/RecruitingHire/README.md b/src/webparts/requestPortal/forms/RecruitingHire/README.md index 10b196a..ff657d4 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/README.md +++ b/src/webparts/requestPortal/forms/RecruitingHire/README.md @@ -13,16 +13,37 @@ ## Особое поведение - **Маска телефона** (PhoneInput) на поле «Телефон». - **Разделитель тысяч** (MoneyInput) на окладах (испытательный срок / после). +- **Мультивыбор компаний (Companies)** — справочник `GET /Dictionary/Companies` + (`CompanyPickerMulti`), хранится JSON-массивом `{Id,Name}` в Note-колонке `CompaniesJson`. +- **График работы (WorkSchedule)** — выпадающий список (набор совпадает с «Заявкой + на подбор»); при выборе «Другое» появляется ручной ввод `WorkScheduleOther`. +- **Испытательный срок (ProbationPeriod)** — выпадающий список частых сроков. +- **Рекрутинговое агентство (условный блок, последняя секция)** — `Toggle` + `RecruitingAgency`; при включении показываются «Название компании» (`AgencyName`) + и «Условия» (`AgencyTerms`), при выключении оба поля очищаются. - **Условное поле «Взамен кого» (ReplacedEmployee)** — показывается только когда тип приёма = замена уволенного (см. `HIRE_TYPE_REPLACEMENT` в модели). - Секция **СИЗ** (рост, размеры одежды/обуви). - **Является источником для «Организация рабочего места»** — из просмотра этой заявки кнопка «Организация рабочего места на основе этой» открывает форму it-workplace-setup с префиллом (rpsource = id этой заявки). +- **Связь с «Заявкой на подбор» (SelectionRequest\*)** — необязательное поле в + секции «Заявка»: поиск-выбор согласованной заявки на подбор (`SelectionRequestField` + + `selectionLookup.ts`, только статус «Согласовано», ищет по всем партициям серии). + Связь хранится тройкой полей `SelectionRequestId` / `SelectionRequestListId` / + `SelectionRequestCode`, а НЕ нативным Lookup — списки партиционированы. В форме + и в просмотре показывается ссылкой «№ код ↗», которая открывает + `RecruitingSelectionView` в **новой вкладке** (deep-link через `routeUrl`, + rptype=recruiting-selection, rpid/rplist из сохранённых полей). +- **Вложения по категориям** — две области: «Удостоверение» и «Резюме» (в каждую + можно несколько файлов). Вложения SP плоские, поэтому категория кодируется + префиксом в имени файла «<Категория> — <имя>» (`attachmentCategories.ts`); при + показе группируются и префикс срезается. Старые файлы без префикса показываются + блоком «Прочие вложения», чтобы не исчезали. - Печать — общий механизм (без «Вложений»). ## Секции формы -ApplicationSection · CandidateSection · PpeSection · ConditionsSection +ApplicationSection · CandidateSection · PpeSection · ConditionsSection · AgencySection ## Точки входа - rptype: `recruiting-hire` diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx index 3dbf93e..ca3c0bf 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx @@ -47,10 +47,19 @@ import { import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; import { saveHireRequest, hireApprovalService } from './submit'; import { loadHireRequest } from './load'; +import { + ATT_CATEGORY_ID, + ATT_CATEGORY_RESUME, + categoryPrefix, + prefixFiles, + attachmentsOfCategory, + uncategorizedAttachments +} from './attachmentCategories'; import { ApplicationSection } from './sections/ApplicationSection'; import { CandidateSection } from './sections/CandidateSection'; import { PpeSection } from './sections/PpeSection'; import { ConditionsSection } from './sections/ConditionsSection'; +import { AgencySection } from './sections/AgencySection'; const RecruitingHireForm: React.FC = ({ requestType, @@ -61,7 +70,9 @@ const RecruitingHireForm: React.FC = ({ editListId }) => { const [data, setData] = useState(EMPTY_DATA); - const [files, setFiles] = useState([]); + // Вложения — две категории: удостоверение и резюме (см. attachmentCategories.ts). + const [idFiles, setIdFiles] = useState([]); + const [resumeFiles, setResumeFiles] = useState([]); const [existingAttachments, setExistingAttachments] = useState([]); // При открытии на редактирование (editId) — сразу известен, иначе появится // после первого сохранения. @@ -210,9 +221,15 @@ const RecruitingHireForm: React.FC = ({ if (code) { setRequestCode(code); } - if (files.length > 0) { - await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, files); // файлы — после создания элемента - setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало + // Категорию кодируем префиксом в имени файла (SP-вложения плоские). + const filesToUpload = [ + ...prefixFiles(idFiles, ATT_CATEGORY_ID), + ...prefixFiles(resumeFiles, ATT_CATEGORY_RESUME) + ]; + if (filesToUpload.length > 0) { + await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, filesToUpload); // файлы — после создания элемента + setIdFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало + setResumeFiles([]); setExistingAttachments(await getAttachments(writeListId || dataListTitle(FORM_ID), id)); // показать добавленные } if (onSaved) { @@ -258,6 +275,11 @@ const RecruitingHireForm: React.FC = ({ const sectionProps = { data, set, context }; + // Уже прикреплённые файлы, разложенные по категориям (+ прочие — старые без префикса). + const idAttachments = attachmentsOfCategory(existingAttachments, ATT_CATEGORY_ID); + const resumeAttachments = attachmentsOfCategory(existingAttachments, ATT_CATEGORY_RESUME); + const otherAttachments = uncategorizedAttachments(existingAttachments); + return (