diff --git a/provisioning/Provision-Lists.ps1 b/provisioning/Provision-Lists.ps1 index db949e9..0809b4e 100644 --- a/provisioning/Provision-Lists.ps1 +++ b/provisioning/Provision-Lists.ps1 @@ -10,6 +10,17 @@ 2) при необходимости переименовывает колонку Title; 3) обеспечивает колонки. + Имена списков НУМЕРОВАННЫЕ: «<Форма> — данные» / «<Форма> — согласование». + Номер собирает сам скрипт из имени в манифесте (см. Get-NumberedListTitle) — в + манифестах имена остаются без номера, чтобы номер не дублировался по 14 манифестам + и ссылкам lookupList. Нумерация нужна под архивацию: когда список данных упирается + в порог элементов, рядом создаётся следующая пара с номером N+1. + + Перед основным проходом идёт разовая МИГРАЦИЯ имён (только при -ListNumber 1): + существующие списки со старым безномерным именем переименовываются в «… 1 — …». + Переименовывается только Title — server-relative URL списка при этом НЕ меняется, + поэтому ссылки на список (request_type.list_id в approval-route) остаются валидными. + Колонки — через СТОЛБЦЫ САЙТА (эталоны): - не-Lookup колонка сначала создаётся как столбец сайта (группа «Портал заявок»), один раз на весь сайт, затем ПОДКЛЮЧАЕТСЯ к списку по ссылке. Так одинаковые @@ -40,17 +51,31 @@ .PARAMETER FormsRoot Папка с формами. По умолчанию — src/webparts/requestPortal/forms. +.PARAMETER ListNumber + Номер пары списков. 1 — текущие (рабочие) списки; 2, 3 … — следующие пары под + архивацию. Миграция безномерных имён выполняется только при 1. + +.PARAMETER SkipRename + Не выполнять миграцию имён существующих списков (только создание/колонки). + .EXAMPLE ./Provision-Lists.ps1 -SiteUrl "https://tnsukz.sharepoint.com/sites/requests" ` -ClientId "7459c965-5ed4-4673-8140-0ab8faf9db64" -Tenant "tnsukz.onmicrosoft.com" ` -Thumbprint "AABBCC...." + +.EXAMPLE + # Следующая пара списков под архивацию, без миграции имён. + ./Provision-Lists.ps1 -SiteUrl "..." -ClientId "..." -Tenant "..." -Thumbprint "..." ` + -ListNumber 2 #> param( [Parameter(Mandatory = $true)][string]$SiteUrl, [Parameter(Mandatory = $true)][string]$ClientId, [Parameter(Mandatory = $true)][string]$Tenant, [Parameter(Mandatory = $true)][string]$Thumbprint, - [string]$FormsRoot = "$PSScriptRoot/../src/webparts/requestPortal/forms" + [string]$FormsRoot = "$PSScriptRoot/../src/webparts/requestPortal/forms", + [ValidateRange(1, 999)][int]$ListNumber = 1, + [switch]$SkipRename ) $ErrorActionPreference = 'Stop' @@ -61,6 +86,30 @@ function ConvertTo-XmlSafe { return $Value.Replace('&', '&').Replace('<', '<').Replace('>', '>').Replace("'", ''').Replace('"', '"') } +# Имя списка по конвенции: «<Форма> — данные» / «<Форма> — согласование». +$LIST_TITLE_PATTERN = '^(?.+?)\s*—\s*(?данные|согласование)\s*$' + +# Имя в манифесте → нумерованное «<Форма> — <суффикс>». +# +# Номер ставится ПЕРЕД суффиксом, и это не косметика: и портал +# (features/myRequests/myRequests.service.ts), и админка маршрутов +# (approval-route/frontend/.../requestManageService.ts) выводят имя списка +# согласования из имени списка данных регексом /\s*—\s*данные\s*$/, привязанным к +# КОНЦУ строки. «… — данные 1» этот регекс не матчит, и список согласования просто +# перестаёт находиться. +# +# Идемпотентно: уже нумерованное имя не получает второй номер, а перенумеровывается. +function Get-NumberedListTitle { + param([string]$Title, [int]$Number) + + $m = [regex]::Match($Title, $LIST_TITLE_PATTERN) + if (-not $m.Success) { + throw "Имя списка «$Title» не по конвенции «<Форма> — данные|согласование» — номер поставить некуда" + } + $base = $m.Groups['base'].Value -replace '\s+\d+$', '' + return "$base $Number — $($m.Groups['suffix'].Value)" +} + # Есть ли колонка НА СПИСКЕ (list-scope). function Test-FieldExists { param([string]$List, [string]$InternalName) @@ -175,7 +224,12 @@ function Add-ListColumn { } } 'Lookup' { - $src = Get-PnPList -Identity $Col.lookupList -ErrorAction Stop + # lookupList в манифесте — безномерное имя, целевой список тоже нумерован. + # Кросс-формные lookup'ы (WorkplaceSetup.SourceHire → «Прием на работу … — данные») + # при этом предполагают ОДИН И ТОТ ЖЕ номер у обеих форм — при появлении + # реальных архивных пар это место придётся пересмотреть. + $lookupTitle = Get-NumberedListTitle -Title $Col.lookupList -Number $ListNumber + $src = Get-PnPList -Identity $lookupTitle -ErrorAction Stop $guid = $src.Id.ToString() $show = if ($Col.lookupShowField) { $Col.lookupShowField } else { 'Title' } $xml = "" @@ -239,9 +293,41 @@ if (-not $manifests) { return } +# Проход 0: миграция безномерных имён на «… 1 — …». Отдельным проходом ДО основного — +# так порядок обхода манифестов не важен: к моменту разрешения Lookup'а (по имени +# списка-источника) все списки уже переименованы. +# +# Переименовывается только Title. URL списка при переименовании Title в SharePoint не +# меняется, поэтому request_type.list_id (server-relative URL) в approval-route +# трогать не нужно. +if ($ListNumber -eq 1 -and -not $SkipRename) { + Write-Host "`n=== Миграция имён списков на «… 1 — …» ===" -ForegroundColor Cyan + foreach ($manifest in $manifests) { + $def = Get-Content -Path $manifest.FullName -Raw -Encoding UTF8 | ConvertFrom-Json + $legacyTitle = $def.dataListTitle + $targetTitle = Get-NumberedListTitle -Title $legacyTitle -Number 1 + + if (Get-PnPList -Identity $targetTitle -ErrorAction SilentlyContinue) { + Write-Host " [=] уже нумерован: «$targetTitle»" -ForegroundColor DarkGray + continue + } + if (-not (Get-PnPList -Identity $legacyTitle -ErrorAction SilentlyContinue)) { + Write-Host " [ ] нет списка «$legacyTitle» — создастся ниже как «$targetTitle»" -ForegroundColor DarkGray + continue + } + try { + Set-PnPList -Identity $legacyTitle -Title $targetTitle | Out-Null + Write-Host " [~] «$legacyTitle» → «$targetTitle»" -ForegroundColor Yellow + } + catch { + Write-Warning " [!] не удалось переименовать «$legacyTitle»: $($_.Exception.Message)" + } + } +} + foreach ($manifest in $manifests) { $def = Get-Content -Path $manifest.FullName -Raw -Encoding UTF8 | ConvertFrom-Json - $listTitle = $def.dataListTitle + $listTitle = Get-NumberedListTitle -Title $def.dataListTitle -Number $ListNumber Write-Host "`n=== Форма: $($manifest.Directory.Name) → список «$listTitle» ===" -ForegroundColor Cyan # 1) Список (идемпотентно) diff --git a/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx b/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx index d0a4ce8..4417739 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx @@ -24,8 +24,9 @@ import { findUnassignedStep } from '../../shared/approval'; import { IRequestFormProps } from '../../models/types'; -import { IFormData, EMPTY_DATA, SetField } from './model'; -import { saveClearanceRequest, clearanceApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; +import { saveClearanceRequest, clearanceApprovalService } from './submit'; import { loadClearanceRequest } from './load'; import { RequestSection } from './sections/RequestSection'; @@ -160,8 +161,8 @@ const ClearanceForm: React.FC = ({ requestType, context, onCa if (status === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID) }); } }; @@ -196,7 +197,7 @@ const ClearanceForm: React.FC = ({ requestType, context, onCa } + belowSubtitle={} headerRight={ requestId || requestStatus ? ( <> diff --git a/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx b/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx index bb50f22..9771ed7 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx @@ -13,14 +13,13 @@ import { useRequestTypeId, APPROVAL_PHASE } from '../../shared/approval'; -import { IFormData, EMPTY_DATA, SetField } from './model'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; import { loadClearanceRequest } from './load'; import { clearanceApprovalService, updateApproverFields, - IApproverEditableFields, - DATA_LIST_TITLE, - APPROVAL_LIST_TITLE + IApproverEditableFields } from './submit'; import { canEditHrRecordsFields, canEditParkingField } from './approverRights'; import { RequestSection } from './sections/RequestSection'; @@ -51,7 +50,10 @@ const ClearanceView: React.FC = ({ context, itemId, formKey const approval = useMemo(() => clearanceApprovalService(), []); const { id: requestTypeId } = useRequestTypeId(context, formKey); - const approverService = useMemo(() => new ApproverService(approval, DATA_LIST_TITLE), [approval]); + const approverService = useMemo( + () => new ApproverService(approval, dataListTitle(FORM_ID)), + [approval] + ); // Матрица маршрута из фактических строк заявки (read-only). decisionSeq в // reloadKey: после решения строки на сервере меняются — перечитываем матрицу. @@ -155,8 +157,8 @@ const ClearanceView: React.FC = ({ context, itemId, formKey if (!requestTypeId) return; notifyActiveStep(context, approval, itemId, { requestTypeId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID) }).catch(() => undefined); }; @@ -185,7 +187,7 @@ const ClearanceView: React.FC = ({ context, itemId, formKey } + belowSubtitle={} headerRight={status ? : undefined} > diff --git a/src/webparts/requestPortal/forms/Clearance/load.ts b/src/webparts/requestPortal/forms/Clearance/load.ts index c7b8c14..1740103 100644 --- a/src/webparts/requestPortal/forms/Clearance/load.ts +++ b/src/webparts/requestPortal/forms/Clearance/load.ts @@ -1,7 +1,6 @@ import { getSP } from '../../shared/pnp/pnpjs'; -import { IFormData, EMPTY_DATA } from './model'; - -const LIST_TITLE = 'Обходной лист — данные'; +import { dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA } from './model'; export interface ILoadedRequest { data: IFormData; @@ -15,7 +14,7 @@ export interface ILoadedRequest { /** Прочитать обходной лист по id: модель формы + статус (правка / просмотр). */ export async function loadClearanceRequest(itemId: number): Promise { const item = (await getSP() - .web.lists.getByTitle(LIST_TITLE) + .web.lists.getByTitle(dataListTitle(FORM_ID)) .items.getById(itemId) .select( 'Id', diff --git a/src/webparts/requestPortal/forms/Clearance/model.ts b/src/webparts/requestPortal/forms/Clearance/model.ts index 68bbff0..b7400d3 100644 --- a/src/webparts/requestPortal/forms/Clearance/model.ts +++ b/src/webparts/requestPortal/forms/Clearance/model.ts @@ -1,8 +1,15 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; +import { RequestTypeId } from '../../models/types'; + +/** + * Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков + * SharePoint выводятся из него, см. shared/listTitles.ts. + */ +export const FORM_ID: RequestTypeId = 'clearance-form'; /** * Данные формы «Обходной лист». Ключи по возможности = internalName колонок - * SP-списка «Обходной лист — данные». Employee хранит e-mail УВОЛЬНЯЕМОГО + * списка данных этой формы (см. shared/listTitles.ts). Employee хранит e-mail УВОЛЬНЯЕМОГО * сотрудника (то, что отдаёт PeopleField) — заявку подаёт HR/руководитель, * а не сам увольняемый, поэтому и маршрут согласования резолвится по email * из этого поля, а не по email инициатора (как в «Заявке на отпуск»). diff --git a/src/webparts/requestPortal/forms/Clearance/submit.ts b/src/webparts/requestPortal/forms/Clearance/submit.ts index a8441ad..fe5c512 100644 --- a/src/webparts/requestPortal/forms/Clearance/submit.ts +++ b/src/webparts/requestPortal/forms/Clearance/submit.ts @@ -1,14 +1,12 @@ import { getSP } from '../../shared/pnp/pnpjs'; import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; import { buildRequestUniqueCode } from '../../shared/requestCode'; -import { IFormData } from './model'; - -export const DATA_LIST_TITLE = 'Обходной лист — данные'; -export const APPROVAL_LIST_TITLE = 'Обходной лист — согласование'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData } from './model'; /** Экземпляр сервиса списка согласования этой формы. */ export function clearanceApprovalService(): ApprovalListService { - return new ApprovalListService(APPROVAL_LIST_TITLE); + return new ApprovalListService(approvalListTitle(FORM_ID)); } /** Отметки, которые проставляет не инициатор, а профильный согласующий (см. approverRights.ts). */ @@ -36,7 +34,7 @@ export async function updateApproverFields( Number.isFinite(amount) && fields.TrainingAmount.trim() !== '' ? amount : null; } if (Object.keys(item).length === 0) return; - await getSP().web.lists.getByTitle(DATA_LIST_TITLE).items.getById(id).update(item); + await getSP().web.lists.getByTitle(dataListTitle(FORM_ID)).items.getById(id).update(item); } /** email / claims-логин → пользователь сайта (Id для Person-поля + Title для заголовка). */ @@ -90,7 +88,7 @@ export async function saveClearanceRequest( approvers: IApproverToWrite[], status: string ): Promise { - const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE); + const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const employee = await ensureEmployee(data.Employee); const item = buildItem(data, status, employee.Id, employee.Title || data.Employee); diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx index 720d4fb..a704b0d 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx @@ -36,8 +36,9 @@ import { } from '../../shared/attachments/attachments.service'; import { effectiveUserEmail } from '../../shared/impersonation'; import { IRequestFormProps } from '../../models/types'; -import { IFormData, EMPTY_DATA, SetField } from './model'; -import { saveHireRequest, hireApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; +import { saveHireRequest, hireApprovalService } from './submit'; import { loadHireRequest } from './load'; import { ApplicationSection } from './sections/ApplicationSection'; import { CandidateSection } from './sections/CandidateSection'; @@ -66,7 +67,7 @@ const RecruitingHireForm: React.FC = ({ requestType, context, return; } let alive = true; - Promise.all([loadHireRequest(editId), getAttachments(DATA_LIST_TITLE, editId)]) + Promise.all([loadHireRequest(editId), getAttachments(dataListTitle(FORM_ID), editId)]) .then(([loaded, atts]) => { if (alive) { setData(loaded.data); @@ -157,7 +158,7 @@ const RecruitingHireForm: React.FC = ({ requestType, context, if (requestId === undefined) { return; } - deleteAttachment(DATA_LIST_TITLE, requestId, fileName) + deleteAttachment(dataListTitle(FORM_ID), requestId, fileName) .then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName))) .catch(() => undefined); }; @@ -184,9 +185,9 @@ const RecruitingHireForm: React.FC = ({ requestType, context, setRequestCode(code); } if (files.length > 0) { - await uploadAttachments(DATA_LIST_TITLE, id, files); // файлы — после создания элемента + await uploadAttachments(dataListTitle(FORM_ID), id, files); // файлы — после создания элемента setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало - setExistingAttachments(await getAttachments(DATA_LIST_TITLE, id)); // показать добавленные + setExistingAttachments(await getAttachments(dataListTitle(FORM_ID), id)); // показать добавленные } if (onSaved) { onSaved(); @@ -195,8 +196,8 @@ const RecruitingHireForm: React.FC = ({ requestType, context, if (status === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE, + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID), }); } }; @@ -236,7 +237,7 @@ const RecruitingHireForm: React.FC = ({ requestType, context, } + belowSubtitle={} headerRight={ requestId || requestStatus ? ( <> diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx index eabf02f..43a8251 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx @@ -15,10 +15,11 @@ import { } from '../../shared/approval'; import { navigate } from '../../shared/routing'; import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service'; -import { IFormData, EMPTY_DATA, SetField } from './model'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; import { loadHireRequest } from './load'; import { confirmDiscardFormChanges } from '../../shared/unsavedChanges'; -import { hireApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit'; +import { hireApprovalService } from './submit'; import { ApplicationSection } from './sections/ApplicationSection'; import { CandidateSection } from './sections/CandidateSection'; import { PpeSection } from './sections/PpeSection'; @@ -53,7 +54,7 @@ const RecruitingHireView: React.FC = ({ context, itemI // Сервис решений согласующего: список согласования + список данных этой формы. const approverService = useMemo( - () => new ApproverService(hireApprovalService(), DATA_LIST_TITLE), + () => new ApproverService(hireApprovalService(), dataListTitle(FORM_ID)), [] ); @@ -90,7 +91,7 @@ const RecruitingHireView: React.FC = ({ context, itemI useEffect(() => { let alive = true; setLoading(true); - Promise.all([loadHireRequest(itemId), getAttachments(DATA_LIST_TITLE, itemId)]) + Promise.all([loadHireRequest(itemId), getAttachments(dataListTitle(FORM_ID), itemId)]) .then(([loaded, files]) => { if (alive) { setData(loaded.data); @@ -129,8 +130,8 @@ const RecruitingHireView: React.FC = ({ context, itemI if (!requestTypeId) return; notifyActiveStep(context, approval, itemId, { requestTypeId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE, + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID), }).catch(() => undefined); }; @@ -159,7 +160,7 @@ const RecruitingHireView: React.FC = ({ context, itemI } + belowSubtitle={} headerRight={status ? : undefined} > diff --git a/src/webparts/requestPortal/forms/RecruitingHire/load.ts b/src/webparts/requestPortal/forms/RecruitingHire/load.ts index 3c4640e..cb020db 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/load.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/load.ts @@ -1,7 +1,6 @@ import { getSP } from '../../shared/pnp/pnpjs'; -import { IFormData, EMPTY_DATA } from './model'; - -const LIST_TITLE = 'Прием на работу — данные'; +import { dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA } from './model'; // Скалярные поля (текст/выбор/число/дата) — читаем как есть, приводим к строке. const SCALAR_FIELDS: (keyof IFormData)[] = [ @@ -21,7 +20,7 @@ export interface ILoadedRequest { export async function loadHireRequest(itemId: number): Promise { const sp = getSP(); const item = (await sp.web.lists - .getByTitle(LIST_TITLE) + .getByTitle(dataListTitle(FORM_ID)) .items.getById(itemId) .select(...SCALAR_FIELDS, 'RequestStatus', 'RequestUniqueCode')()) as Record; diff --git a/src/webparts/requestPortal/forms/RecruitingHire/model.ts b/src/webparts/requestPortal/forms/RecruitingHire/model.ts index 5f78d87..357f5c0 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/model.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/model.ts @@ -1,9 +1,16 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; import { ISelectOption } from '../../shared/ui'; +import { RequestTypeId } from '../../models/types'; /** - * Данные формы «Заявка на прием». Ключи = internalName колонок SP-списка - * «Прием на работу — данные», чтобы при записи маппинг был 1:1. + * Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков + * SharePoint выводятся из него, см. shared/listTitles.ts. + */ +export const FORM_ID: RequestTypeId = 'recruiting-hire'; + +/** + * Данные формы «Заявка на прием». Ключи = internalName колонок списка данных + * этой формы (см. shared/listTitles.ts), чтобы при записи маппинг был 1:1. */ export interface IFormData { // Заявка (шапка) diff --git a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts index 79fdebc..91ab1d4 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts @@ -1,17 +1,15 @@ import { getSP } from '../../shared/pnp/pnpjs'; import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; import { buildRequestUniqueCode } from '../../shared/requestCode'; -import { IFormData } from './model'; - -export const DATA_LIST_TITLE = 'Прием на работу — данные'; -export const APPROVAL_LIST_TITLE = 'Прием на работу — согласование'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData } from './model'; // Числовые поля: пустая строка → null, иначе Number(). const NUMBER_FIELDS: (keyof IFormData)[] = ['PpeHeight', 'PpeShoeSize', 'SalaryProbation', 'SalaryAfterProbation']; /** Экземпляр сервиса списка согласования этой формы. */ export function hireApprovalService(): ApprovalListService { - return new ApprovalListService(APPROVAL_LIST_TITLE); + return new ApprovalListService(approvalListTitle(FORM_ID)); } /** Осмысленный заголовок элемента списка (системная колонка Title). */ @@ -56,7 +54,7 @@ export async function saveHireRequest( executors: IApproverToWrite[], status: string ): Promise { - const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE); + const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildItem(data, status); let id: number; diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx index 15f58e6..38757a4 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx @@ -35,8 +35,9 @@ import { } from '../../shared/attachments/attachments.service'; import { effectiveUserEmail } from '../../shared/impersonation'; import { IRequestFormProps } from '../../models/types'; -import { IFormData, EMPTY_DATA, SetField } from './model'; -import { saveSelectionRequest, selectionApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; +import { saveSelectionRequest, selectionApprovalService } from './submit'; import { loadSelectionRequest } from './load'; import { ApplicationSection } from './sections/ApplicationSection'; import { DepartmentSection } from './sections/DepartmentSection'; @@ -63,7 +64,7 @@ const RecruitingSelectionForm: React.FC = ({ requestType, con return; } let alive = true; - Promise.all([loadSelectionRequest(editId), getAttachments(DATA_LIST_TITLE, editId)]) + Promise.all([loadSelectionRequest(editId), getAttachments(dataListTitle(FORM_ID), editId)]) .then(([loaded, atts]) => { if (alive) { setData(loaded.data); @@ -131,7 +132,7 @@ const RecruitingSelectionForm: React.FC = ({ requestType, con if (requestId === undefined) { return; } - deleteAttachment(DATA_LIST_TITLE, requestId, fileName) + deleteAttachment(dataListTitle(FORM_ID), requestId, fileName) .then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName))) .catch(() => undefined); }; @@ -151,9 +152,9 @@ const RecruitingSelectionForm: React.FC = ({ requestType, con setRequestCode(code); } if (files.length > 0) { - await uploadAttachments(DATA_LIST_TITLE, id, files); // файлы — после создания элемента + await uploadAttachments(dataListTitle(FORM_ID), id, files); // файлы — после создания элемента setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало - setExistingAttachments(await getAttachments(DATA_LIST_TITLE, id)); + setExistingAttachments(await getAttachments(dataListTitle(FORM_ID), id)); } if (onSaved) { onSaved(); @@ -162,8 +163,8 @@ const RecruitingSelectionForm: React.FC = ({ requestType, con if (status === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID) }); } }; @@ -198,7 +199,7 @@ const RecruitingSelectionForm: React.FC = ({ requestType, con } + belowSubtitle={} headerRight={ requestId || requestStatus ? ( <> diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx index 81ca4e0..ac89b97 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionView.tsx @@ -13,9 +13,10 @@ import { useRequestTypeId } from '../../shared/approval'; import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service'; -import { IFormData, EMPTY_DATA, SetField } from './model'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; import { loadSelectionRequest } from './load'; -import { selectionApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit'; +import { selectionApprovalService } from './submit'; import { ApplicationSection } from './sections/ApplicationSection'; import { DepartmentSection } from './sections/DepartmentSection'; import { VacancySection } from './sections/VacancySection'; @@ -44,7 +45,7 @@ const RecruitingSelectionView: React.FC = ({ cont const approval = useMemo(() => selectionApprovalService(), []); const { id: requestTypeId } = useRequestTypeId(context, formKey); - const approverService = useMemo(() => new ApproverService(approval, DATA_LIST_TITLE), [approval]); + const approverService = useMemo(() => new ApproverService(approval, dataListTitle(FORM_ID)), [approval]); // Матрица маршрута из фактических строк заявки (read-only). decisionSeq в // reloadKey: после решения строки на сервере меняются — перечитываем матрицу. @@ -57,7 +58,7 @@ const RecruitingSelectionView: React.FC = ({ cont useEffect(() => { let alive = true; setLoading(true); - Promise.all([loadSelectionRequest(itemId), getAttachments(DATA_LIST_TITLE, itemId)]) + Promise.all([loadSelectionRequest(itemId), getAttachments(dataListTitle(FORM_ID), itemId)]) .then(([loaded, files]) => { if (alive) { setData(loaded.data); @@ -96,8 +97,8 @@ const RecruitingSelectionView: React.FC = ({ cont if (!requestTypeId) return; notifyActiveStep(context, approval, itemId, { requestTypeId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID) }).catch(() => undefined); }; @@ -111,7 +112,7 @@ const RecruitingSelectionView: React.FC = ({ cont } + belowSubtitle={} headerRight={status ? : undefined} > diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/load.ts b/src/webparts/requestPortal/forms/RecruitingSelection/load.ts index e3027e7..7769da4 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/load.ts +++ b/src/webparts/requestPortal/forms/RecruitingSelection/load.ts @@ -1,7 +1,6 @@ import { getSP } from '../../shared/pnp/pnpjs'; -import { IFormData, EMPTY_DATA } from './model'; - -const LIST_TITLE = 'Заявка на подбор — данные'; +import { dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA } from './model'; // Категории полей — определяют, как читать значение из элемента списка. const PERSON_FIELDS: (keyof IFormData)[] = ['Responsible', 'ProposedMentor', 'DirectManager']; @@ -30,7 +29,7 @@ export async function loadSelectionRequest(itemId: number): Promise; diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/model.ts b/src/webparts/requestPortal/forms/RecruitingSelection/model.ts index 757fe0b..35a1b92 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/model.ts +++ b/src/webparts/requestPortal/forms/RecruitingSelection/model.ts @@ -1,5 +1,12 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; import { ISelectOption } from '../../shared/ui'; +import { RequestTypeId } from '../../models/types'; + +/** + * Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков + * SharePoint выводятся из него, см. shared/listTitles.ts. + */ +export const FORM_ID: RequestTypeId = 'recruiting-selection'; /** * Данные формы «Заявка на подбор». Ключи = internalName колонок SP-списка diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts b/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts index 59dc753..939c4e3 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts +++ b/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts @@ -1,10 +1,8 @@ import { getSP } from '../../shared/pnp/pnpjs'; import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; import { buildRequestUniqueCode } from '../../shared/requestCode'; -import { IFormData } from './model'; - -export const DATA_LIST_TITLE = 'Заявка на подбор — данные'; -export const APPROVAL_LIST_TITLE = 'Заявка на подбор — согласование'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData } from './model'; // Поля-люди (один человек): логин → ensureUser → Id в колонку Id. const PERSON_FIELDS: (keyof IFormData)[] = ['Responsible', 'ProposedMentor', 'DirectManager']; @@ -15,7 +13,7 @@ const NUMBER_FIELDS: (keyof IFormData)[] = ['DepartmentHeadcount', 'RateProbatio /** Экземпляр сервиса списка согласования этой формы. */ export function selectionApprovalService(): ApprovalListService { - return new ApprovalListService(APPROVAL_LIST_TITLE); + return new ApprovalListService(approvalListTitle(FORM_ID)); } /** Логин → числовой Id пользователя на текущем сайте. */ @@ -78,7 +76,7 @@ export async function saveSelectionRequest( approvers: IApproverToWrite[], status: string ): Promise { - const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE); + const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildScalarBody(data); item.RequestStatus = status; await applyPeople(item, data); diff --git a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx index 2e369ac..b12d540 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx +++ b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx @@ -35,14 +35,9 @@ import { } from '../../shared/attachments/attachments.service'; import { effectiveUserEmail } from '../../shared/impersonation'; import { IRequestFormProps } from '../../models/types'; -import { IFormData, EMPTY_DATA, SetField } from './model'; -import { - saveTicketRequest, - setPlannerTaskId, - ticket1cApprovalService, - DATA_LIST_TITLE, - APPROVAL_LIST_TITLE -} from './submit'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; +import { saveTicketRequest, setPlannerTaskId, ticket1cApprovalService } from './submit'; import { loadTicketRequest } from './load'; import { PlannerTicketService } from './services/plannerTicket.service'; import { getApplicantProfile } from './services/profile.service'; @@ -86,7 +81,7 @@ const Ticket1CForm: React.FC = ({ requestType, context, onCan return; } let alive = true; - Promise.all([loadTicketRequest(editId), getAttachments(DATA_LIST_TITLE, editId)]) + Promise.all([loadTicketRequest(editId), getAttachments(dataListTitle(FORM_ID), editId)]) .then(([loaded, atts]) => { if (alive) { setData(loaded.data); @@ -193,7 +188,7 @@ const Ticket1CForm: React.FC = ({ requestType, context, onCan if (requestId === undefined) { return; } - deleteAttachment(DATA_LIST_TITLE, requestId, fileName) + deleteAttachment(dataListTitle(FORM_ID), requestId, fileName) .then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName))) .catch(() => undefined); }; @@ -224,9 +219,9 @@ const Ticket1CForm: React.FC = ({ requestType, context, onCan let attachments = existingAttachments; if (files.length > 0) { - await uploadAttachments(DATA_LIST_TITLE, id, files); + await uploadAttachments(dataListTitle(FORM_ID), id, files); setFiles([]); - attachments = await getAttachments(DATA_LIST_TITLE, id); + attachments = await getAttachments(dataListTitle(FORM_ID), id); setExistingAttachments(attachments); } @@ -237,8 +232,8 @@ const Ticket1CForm: React.FC = ({ requestType, context, onCan if (finalStatus === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID) }); } else if (finalStatus === REQUEST_STATUS.approved) { // Согласование не требуется для этого вида работ — сразу передаём в отдел 1С. @@ -289,7 +284,7 @@ const Ticket1CForm: React.FC = ({ requestType, context, onCan } + belowSubtitle={} headerRight={ requestId || requestStatus ? ( <> diff --git a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CView.tsx b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CView.tsx index 015be31..cec9b4c 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CView.tsx +++ b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CView.tsx @@ -27,15 +27,10 @@ import { REQUEST_STATUS } from '../../shared/approval'; import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service'; -import { IFormData, EMPTY_DATA, SetField } from './model'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; import { loadTicketRequest } from './load'; -import { - ticket1cApprovalService, - getPlannerTaskId, - setPlannerTaskId, - DATA_LIST_TITLE, - APPROVAL_LIST_TITLE -} from './submit'; +import { ticket1cApprovalService, getPlannerTaskId, setPlannerTaskId } from './submit'; import { PlannerTicketService } from './services/plannerTicket.service'; import { PLANNER_PLAN_ID, PLANNER_BUCKET_NAME, PLANNER_REJECTED_BUCKET_NAME } from './config'; import { RequestSection } from './sections/RequestSection'; @@ -89,7 +84,7 @@ const Ticket1CView: React.FC = ({ context, itemId, formKey } const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value })); const approverService = useMemo( - () => new ApproverService(ticket1cApprovalService(), DATA_LIST_TITLE), + () => new ApproverService(ticket1cApprovalService(), dataListTitle(FORM_ID)), [] ); const approval = useMemo(() => ticket1cApprovalService(), []); @@ -105,7 +100,7 @@ const Ticket1CView: React.FC = ({ context, itemId, formKey } useEffect(() => { let alive = true; setLoading(true); - Promise.all([loadTicketRequest(itemId), getAttachments(DATA_LIST_TITLE, itemId)]) + Promise.all([loadTicketRequest(itemId), getAttachments(dataListTitle(FORM_ID), itemId)]) .then(([loaded, files]) => { if (alive) { setData(loaded.data); @@ -148,7 +143,7 @@ const Ticket1CView: React.FC = ({ context, itemId, formKey } const bucketName = finalStatus === REQUEST_STATUS.rejected ? PLANNER_REJECTED_BUCKET_NAME : PLANNER_BUCKET_NAME; - const atts = await getAttachments(DATA_LIST_TITLE, itemId); + const atts = await getAttachments(dataListTitle(FORM_ID), itemId); const taskId = await new PlannerTicketService(context).createTask({ planId: PLANNER_PLAN_ID, bucketName, @@ -179,8 +174,8 @@ const Ticket1CView: React.FC = ({ context, itemId, formKey } if (!requestTypeId) return; notifyActiveStep(context, approval, itemId, { requestTypeId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID) }).catch(() => undefined); }; @@ -203,7 +198,7 @@ const Ticket1CView: React.FC = ({ context, itemId, formKey } } + belowSubtitle={} headerRight={status ? : undefined} > diff --git a/src/webparts/requestPortal/forms/Ticket1C/load.ts b/src/webparts/requestPortal/forms/Ticket1C/load.ts index a30a85b..2f61d9a 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/load.ts +++ b/src/webparts/requestPortal/forms/Ticket1C/load.ts @@ -1,7 +1,6 @@ import { getSP } from '../../shared/pnp/pnpjs'; -import { IFormData, EMPTY_DATA } from './model'; - -const LIST_TITLE = 'Заявка в отдел 1С — данные'; +import { dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA } from './model'; // TaskName пишется/читается через системную колонку Title (см. provisioning.json/titleColumnName). const SCALAR_FIELDS: (keyof IFormData)[] = [ @@ -18,7 +17,7 @@ export interface ILoadedRequest { /** Прочитать заявку по id: модель формы + статус (для режима чтения). */ export async function loadTicketRequest(itemId: number): Promise { const item = (await getSP() - .web.lists.getByTitle(LIST_TITLE) + .web.lists.getByTitle(dataListTitle(FORM_ID)) .items.getById(itemId) .select(...SCALAR_FIELDS, 'Title', 'RequestStatus', 'RequestUniqueCode')()) as Record; diff --git a/src/webparts/requestPortal/forms/Ticket1C/model.ts b/src/webparts/requestPortal/forms/Ticket1C/model.ts index fbeb6a6..63f5612 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/model.ts +++ b/src/webparts/requestPortal/forms/Ticket1C/model.ts @@ -1,8 +1,15 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; +import { RequestTypeId } from '../../models/types'; /** - * Данные формы «Заявка в отдел 1С». Ключи = internalName колонок SP-списка - * «Заявка в отдел 1С — данные». + * Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков + * SharePoint выводятся из него, см. shared/listTitles.ts. + */ +export const FORM_ID: RequestTypeId = 'it-1c-ticket'; + +/** + * Данные формы «Заявка в отдел 1С». Ключи = internalName колонок списка данных + * этой формы (см. shared/listTitles.ts). */ export interface IFormData { /** Пишется в системную колонку Title (см. provisioning.json/titleColumnName). */ diff --git a/src/webparts/requestPortal/forms/Ticket1C/submit.ts b/src/webparts/requestPortal/forms/Ticket1C/submit.ts index 73a06ff..5458b3c 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/submit.ts +++ b/src/webparts/requestPortal/forms/Ticket1C/submit.ts @@ -1,10 +1,9 @@ import { getSP } from '../../shared/pnp/pnpjs'; import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; import { buildRequestUniqueCode } from '../../shared/requestCode'; -import { IFormData } from './model'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData } from './model'; -export const DATA_LIST_TITLE = 'Заявка в отдел 1С — данные'; -export const APPROVAL_LIST_TITLE = 'Заявка в отдел 1С — согласование'; // Префикс уникального кода заявки — буквенная метка «1С» вместо числового id // типа заявки. Числовой RequestTypeId (для резолва маршрута/уведомлений) // больше не хардкодится — см. useRequestTypeId в Form.tsx/View.tsx. @@ -12,7 +11,7 @@ export const TICKET1C_CODE_PREFIX = '1С'; /** Экземпляр сервиса списка согласования этой формы. */ export function ticket1cApprovalService(): ApprovalListService { - return new ApprovalListService(APPROVAL_LIST_TITLE); + return new ApprovalListService(approvalListTitle(FORM_ID)); } /** Поля элемента списка данных из модели формы. TaskName пишется в системную колонку Title. */ @@ -46,7 +45,7 @@ export async function saveTicketRequest( approvers: IApproverToWrite[], status: string ): Promise { - const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE); + const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildItem(data, status); let id: number; @@ -73,12 +72,12 @@ export async function saveTicketRequest( */ export async function getPlannerTaskId(id: number): Promise { const item = (await getSP() - .web.lists.getByTitle(DATA_LIST_TITLE) + .web.lists.getByTitle(dataListTitle(FORM_ID)) .items.getById(id) .select('PlannerTaskId')()) as { PlannerTaskId?: string }; return item.PlannerTaskId || undefined; } export async function setPlannerTaskId(id: number, taskId: string): Promise { - await getSP().web.lists.getByTitle(DATA_LIST_TITLE).items.getById(id).update({ PlannerTaskId: taskId }); + await getSP().web.lists.getByTitle(dataListTitle(FORM_ID)).items.getById(id).update({ PlannerTaskId: taskId }); } diff --git a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx index c11a273..79a598c 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx +++ b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx @@ -36,8 +36,9 @@ import { } from '../../shared/attachments/attachments.service'; import { effectiveUserEmail } from '../../shared/impersonation'; import { IRequestFormProps } from '../../models/types'; -import { IFormData, EMPTY_DATA, SetField } from './model'; -import { saveTmcTransferRequest, tmcTransferApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; +import { saveTmcTransferRequest, tmcTransferApprovalService } from './submit'; import { loadTmcTransferRequest } from './load'; import { HeaderSection } from './sections/HeaderSection'; import { ItemsSection } from './sections/ItemsSection'; @@ -79,7 +80,7 @@ const TmcTransferForm: React.FC = ({ requestType, context, on return; } let alive = true; - Promise.all([loadTmcTransferRequest(editId), getAttachments(DATA_LIST_TITLE, editId)]) + Promise.all([loadTmcTransferRequest(editId), getAttachments(dataListTitle(FORM_ID), editId)]) .then(([loaded, atts]) => { if (alive) { setData(loaded.data); @@ -173,7 +174,7 @@ const TmcTransferForm: React.FC = ({ requestType, context, on if (requestId === undefined) { return; } - deleteAttachment(DATA_LIST_TITLE, requestId, fileName) + deleteAttachment(dataListTitle(FORM_ID), requestId, fileName) .then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName))) .catch(() => undefined); }; @@ -200,9 +201,9 @@ const TmcTransferForm: React.FC = ({ requestType, context, on setRequestCode(code); } if (files.length > 0) { - await uploadAttachments(DATA_LIST_TITLE, id, files); + await uploadAttachments(dataListTitle(FORM_ID), id, files); setFiles([]); - setExistingAttachments(await getAttachments(DATA_LIST_TITLE, id)); + setExistingAttachments(await getAttachments(dataListTitle(FORM_ID), id)); } if (onSaved) { onSaved(); @@ -210,8 +211,8 @@ const TmcTransferForm: React.FC = ({ requestType, context, on if (status === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID) }); } }; @@ -247,7 +248,7 @@ const TmcTransferForm: React.FC = ({ requestType, context, on } + belowSubtitle={} headerRight={ requestId || requestStatus ? ( <> diff --git a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferView.tsx b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferView.tsx index 3456e50..cf6ef56 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferView.tsx +++ b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferView.tsx @@ -27,9 +27,10 @@ import { APPROVAL_PHASE } from '../../shared/approval'; import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service'; -import { IFormData, EMPTY_DATA, SetField } from './model'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; import { loadTmcTransferRequest } from './load'; -import { tmcTransferApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit'; +import { tmcTransferApprovalService } from './submit'; import { HeaderSection } from './sections/HeaderSection'; import { ItemsSection } from './sections/ItemsSection'; @@ -54,7 +55,7 @@ const TmcTransferView: React.FC = ({ context, itemId, for const set: SetField = () => undefined; const approverService = useMemo( - () => new ApproverService(tmcTransferApprovalService(), DATA_LIST_TITLE), + () => new ApproverService(tmcTransferApprovalService(), dataListTitle(FORM_ID)), [] ); @@ -84,7 +85,7 @@ const TmcTransferView: React.FC = ({ context, itemId, for useEffect(() => { let alive = true; setLoading(true); - Promise.all([loadTmcTransferRequest(itemId), getAttachments(DATA_LIST_TITLE, itemId)]) + Promise.all([loadTmcTransferRequest(itemId), getAttachments(dataListTitle(FORM_ID), itemId)]) .then(([loaded, files]) => { if (alive) { setData(loaded.data); @@ -123,8 +124,8 @@ const TmcTransferView: React.FC = ({ context, itemId, for if (!requestTypeId) return; notifyActiveStep(context, approval, itemId, { requestTypeId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID) }).catch(() => undefined); }; @@ -147,7 +148,7 @@ const TmcTransferView: React.FC = ({ context, itemId, for } + belowSubtitle={} headerRight={status ? : undefined} > diff --git a/src/webparts/requestPortal/forms/TmcTransfer/load.ts b/src/webparts/requestPortal/forms/TmcTransfer/load.ts index 78131c6..5bcb5b6 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/load.ts +++ b/src/webparts/requestPortal/forms/TmcTransfer/load.ts @@ -1,7 +1,7 @@ import { getSP } from '../../shared/pnp/pnpjs'; import { ILineItem } from '../../shared/ui'; -import { IFormData, EMPTY_DATA } from './model'; -import { DATA_LIST_TITLE } from './submit'; +import { dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA } from './model'; export interface ILoadedRequest { data: IFormData; @@ -24,7 +24,7 @@ function parseItems(raw: unknown): ILineItem[] { /** Прочитать заявку «СЗ на перемещение ТМЦ» по id: модель формы + статус + код. */ export async function loadTmcTransferRequest(itemId: number): Promise { const item = (await getSP() - .web.lists.getByTitle(DATA_LIST_TITLE) + .web.lists.getByTitle(dataListTitle(FORM_ID)) .items.getById(itemId) .select( 'Id', diff --git a/src/webparts/requestPortal/forms/TmcTransfer/model.ts b/src/webparts/requestPortal/forms/TmcTransfer/model.ts index af1bfd3..5e7e40c 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/model.ts +++ b/src/webparts/requestPortal/forms/TmcTransfer/model.ts @@ -1,5 +1,12 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; import { ILineItem } from '../../shared/ui'; +import { RequestTypeId } from '../../models/types'; + +/** + * Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков + * SharePoint выводятся из него, см. shared/listTitles.ts. + */ +export const FORM_ID: RequestTypeId = 'wh-tmc-transfer'; /** * Данные формы «Служебная записка на перемещение ТМЦ». diff --git a/src/webparts/requestPortal/forms/TmcTransfer/submit.ts b/src/webparts/requestPortal/forms/TmcTransfer/submit.ts index 894c116..b413edb 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/submit.ts +++ b/src/webparts/requestPortal/forms/TmcTransfer/submit.ts @@ -2,14 +2,12 @@ import { getSP } from '../../shared/pnp/pnpjs'; import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; import { buildRequestUniqueCode } from '../../shared/requestCode'; import { ILineItem } from '../../shared/ui'; -import { IFormData } from './model'; - -export const DATA_LIST_TITLE = 'СЗ на перемещение ТМЦ — данные'; -export const APPROVAL_LIST_TITLE = 'СЗ на перемещение ТМЦ — согласование'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData } from './model'; /** Экземпляр сервиса списка согласования этой формы. */ export function tmcTransferApprovalService(): ApprovalListService { - return new ApprovalListService(APPROVAL_LIST_TITLE); + return new ApprovalListService(approvalListTitle(FORM_ID)); } /** Осмысленный заголовок элемента (системная колонка Title). */ @@ -55,7 +53,7 @@ export async function saveTmcTransferRequest( executors: IApproverToWrite[], status: string ): Promise { - const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE); + const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildItem(data, status); let id: number; diff --git a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx index 0c5dac7..ba39122 100644 --- a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx +++ b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx @@ -24,8 +24,9 @@ import { findUnassignedStep } from '../../shared/approval'; import { IRequestFormProps } from '../../models/types'; -import { IFormData, EMPTY_DATA, SetField } from './model'; -import { saveVacationRequest, vacationApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; +import { saveVacationRequest, vacationApprovalService } from './submit'; import { loadVacationRequest } from './load'; import { RequestSection } from './sections/RequestSection'; @@ -170,8 +171,8 @@ const VacationForm: React.FC = ({ requestType, context, onCan if (status === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID) }); } }; @@ -206,7 +207,7 @@ const VacationForm: React.FC = ({ requestType, context, onCan } + belowSubtitle={} headerRight={ requestId || requestStatus ? ( <> diff --git a/src/webparts/requestPortal/forms/Vacation/VacationView.tsx b/src/webparts/requestPortal/forms/Vacation/VacationView.tsx index d498638..aa9b22a 100644 --- a/src/webparts/requestPortal/forms/Vacation/VacationView.tsx +++ b/src/webparts/requestPortal/forms/Vacation/VacationView.tsx @@ -12,9 +12,10 @@ import { notifyActiveStep, useRequestTypeId } from '../../shared/approval'; -import { IFormData, EMPTY_DATA, SetField } from './model'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; import { loadVacationRequest } from './load'; -import { vacationApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit'; +import { vacationApprovalService } from './submit'; import { RequestSection } from './sections/RequestSection'; interface IVacationViewProps { @@ -39,7 +40,7 @@ const VacationView: React.FC = ({ context, itemId, formKey } const approval = useMemo(() => vacationApprovalService(), []); const { id: requestTypeId } = useRequestTypeId(context, formKey); - const approverService = useMemo(() => new ApproverService(approval, DATA_LIST_TITLE), [approval]); + const approverService = useMemo(() => new ApproverService(approval, dataListTitle(FORM_ID)), [approval]); // Матрица маршрута из фактических строк заявки (read-only). decisionSeq в // reloadKey: после решения строки на сервере меняются — перечитываем матрицу. @@ -93,8 +94,8 @@ const VacationView: React.FC = ({ context, itemId, formKey } if (!requestTypeId) return; notifyActiveStep(context, approval, itemId, { requestTypeId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID) }).catch(() => undefined); }; @@ -108,7 +109,7 @@ const VacationView: React.FC = ({ context, itemId, formKey } } + belowSubtitle={} headerRight={status ? : undefined} > diff --git a/src/webparts/requestPortal/forms/Vacation/load.ts b/src/webparts/requestPortal/forms/Vacation/load.ts index 64df1c4..812466e 100644 --- a/src/webparts/requestPortal/forms/Vacation/load.ts +++ b/src/webparts/requestPortal/forms/Vacation/load.ts @@ -1,7 +1,6 @@ import { getSP } from '../../shared/pnp/pnpjs'; -import { IFormData, EMPTY_DATA } from './model'; - -const LIST_TITLE = 'Заявка на отпуск — данные'; +import { dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA } from './model'; export interface ILoadedRequest { data: IFormData; @@ -15,7 +14,7 @@ export interface ILoadedRequest { /** Прочитать заявку на отпуск по id: модель формы + статус (правка / просмотр). */ export async function loadVacationRequest(itemId: number): Promise { const item = (await getSP() - .web.lists.getByTitle(LIST_TITLE) + .web.lists.getByTitle(dataListTitle(FORM_ID)) .items.getById(itemId) .select( 'Id', diff --git a/src/webparts/requestPortal/forms/Vacation/model.ts b/src/webparts/requestPortal/forms/Vacation/model.ts index 5305d3d..ec1f7ea 100644 --- a/src/webparts/requestPortal/forms/Vacation/model.ts +++ b/src/webparts/requestPortal/forms/Vacation/model.ts @@ -1,8 +1,15 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; +import { RequestTypeId } from '../../models/types'; + +/** + * Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков + * SharePoint выводятся из него, см. shared/listTitles.ts. + */ +export const FORM_ID: RequestTypeId = 'hr-vacation'; /** * Данные формы «Заявка на отпуск». Ключи по возможности = internalName колонок - * SP-списка «Заявка на отпуск — данные». Employee хранит e-mail выбранного + * списка данных этой формы (см. shared/listTitles.ts). Employee хранит e-mail выбранного * сотрудника (то, что отдаёт PeopleField) — по нему же резолвится маршрут * согласования, поэтому заявку можно подать и за другого сотрудника. * EndDate в форме не вводится: считается при сохранении как начало + (дни − 1). diff --git a/src/webparts/requestPortal/forms/Vacation/submit.ts b/src/webparts/requestPortal/forms/Vacation/submit.ts index a6a5fff..efd45bd 100644 --- a/src/webparts/requestPortal/forms/Vacation/submit.ts +++ b/src/webparts/requestPortal/forms/Vacation/submit.ts @@ -1,14 +1,12 @@ import { getSP } from '../../shared/pnp/pnpjs'; import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; import { buildRequestUniqueCode } from '../../shared/requestCode'; -import { IFormData } from './model'; - -export const DATA_LIST_TITLE = 'Заявка на отпуск — данные'; -export const APPROVAL_LIST_TITLE = 'Заявка на отпуск — согласование'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData } from './model'; /** Экземпляр сервиса списка согласования этой формы. */ export function vacationApprovalService(): ApprovalListService { - return new ApprovalListService(APPROVAL_LIST_TITLE); + return new ApprovalListService(approvalListTitle(FORM_ID)); } /** email / claims-логин → пользователь сайта (Id для Person-поля + Title для заголовка). */ @@ -67,7 +65,7 @@ export async function saveVacationRequest( approvers: IApproverToWrite[], status: string ): Promise { - const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE); + const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const employee = await ensureEmployee(data.Employee); const item = buildItem(data, status, employee.Id, employee.Title || data.Employee); diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx index 84cb067..90b845e 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx @@ -36,8 +36,9 @@ import { } from '../../shared/attachments/attachments.service'; import { effectiveUserEmail } from '../../shared/impersonation'; import { IRequestFormProps } from '../../models/types'; -import { IFormData, EMPTY_DATA, SetField } from './model'; -import { saveWorkplaceRequest, workplaceApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; +import { saveWorkplaceRequest, workplaceApprovalService } from './submit'; import { loadPrefillFromHire } from './prefill'; import { loadWorkplaceRequest } from './load'; import { EmployeeSection } from './sections/EmployeeSection'; @@ -67,7 +68,7 @@ const WorkplaceSetupForm: React.FC = ({ requestType, context, return; } let alive = true; - Promise.all([loadWorkplaceRequest(editId), getAttachments(DATA_LIST_TITLE, editId)]) + Promise.all([loadWorkplaceRequest(editId), getAttachments(dataListTitle(FORM_ID), editId)]) .then(([loaded, atts]) => { if (alive) { setData(loaded.data); @@ -180,7 +181,7 @@ const WorkplaceSetupForm: React.FC = ({ requestType, context, if (requestId === undefined) { return; } - deleteAttachment(DATA_LIST_TITLE, requestId, fileName) + deleteAttachment(dataListTitle(FORM_ID), requestId, fileName) .then(() => setExistingAttachments((prev) => prev.filter((a) => a.fileName !== fileName))) .catch(() => undefined); }; @@ -208,9 +209,9 @@ const WorkplaceSetupForm: React.FC = ({ requestType, context, setRequestCode(code); } if (files.length > 0) { - await uploadAttachments(DATA_LIST_TITLE, id, files); + await uploadAttachments(dataListTitle(FORM_ID), id, files); setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало - setExistingAttachments(await getAttachments(DATA_LIST_TITLE, id)); // показать добавленные + setExistingAttachments(await getAttachments(dataListTitle(FORM_ID), id)); // показать добавленные } if (onSaved) { onSaved(); @@ -219,8 +220,8 @@ const WorkplaceSetupForm: React.FC = ({ requestType, context, if (status === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE, + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID), }); } }; @@ -260,7 +261,7 @@ const WorkplaceSetupForm: React.FC = ({ requestType, context, } + belowSubtitle={} headerRight={ requestId || requestStatus ? ( <> diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx index 3b5a9a3..5d5d2e9 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx @@ -27,9 +27,10 @@ import { APPROVAL_PHASE } from '../../shared/approval'; import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service'; -import { IFormData, EMPTY_DATA, SetField } from './model'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; import { loadWorkplaceRequest } from './load'; -import { workplaceApprovalService, updateReviewFields, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit'; +import { workplaceApprovalService, updateReviewFields } from './submit'; import { EmployeeSection } from './sections/EmployeeSection'; import { EquipmentSection } from './sections/EquipmentSection'; @@ -59,7 +60,7 @@ const WorkplaceSetupView: React.FC = ({ context, itemI const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value })); const approverService = useMemo( - () => new ApproverService(workplaceApprovalService(), DATA_LIST_TITLE), + () => new ApproverService(workplaceApprovalService(), dataListTitle(FORM_ID)), [] ); @@ -96,7 +97,7 @@ const WorkplaceSetupView: React.FC = ({ context, itemI useEffect(() => { let alive = true; setLoading(true); - Promise.all([loadWorkplaceRequest(itemId), getAttachments(DATA_LIST_TITLE, itemId)]) + Promise.all([loadWorkplaceRequest(itemId), getAttachments(dataListTitle(FORM_ID), itemId)]) .then(([loaded, files]) => { if (alive) { setData(loaded.data); @@ -174,8 +175,8 @@ const WorkplaceSetupView: React.FC = ({ context, itemI if (!requestTypeId) return; notifyActiveStep(context, approval, itemId, { requestTypeId, - dataListName: DATA_LIST_TITLE, - approvalListName: APPROVAL_LIST_TITLE, + dataListName: dataListTitle(FORM_ID), + approvalListName: approvalListTitle(FORM_ID), }).catch(() => undefined); }; @@ -211,7 +212,7 @@ const WorkplaceSetupView: React.FC = ({ context, itemI } + belowSubtitle={} headerRight={status ? : undefined} > diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts index 2cf9af3..1bb9c28 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts @@ -1,7 +1,6 @@ import { getSP } from '../../shared/pnp/pnpjs'; -import { IFormData, EMPTY_DATA } from './model'; - -const LIST_TITLE = 'Организация рабочего места — данные'; +import { dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData, EMPTY_DATA } from './model'; const SCALAR_FIELDS: (keyof IFormData)[] = [ 'FullName', 'NameLatin', 'Phone', 'Position', 'Department', 'DepartmentId', 'WorkKind', 'Location', 'StartDate', @@ -18,7 +17,7 @@ export interface ILoadedRequest { /** Прочитать заявку по id: модель формы + статус (для режима чтения). */ export async function loadWorkplaceRequest(itemId: number): Promise { const item = (await getSP() - .web.lists.getByTitle(LIST_TITLE) + .web.lists.getByTitle(dataListTitle(FORM_ID)) .items.getById(itemId) .select(...SCALAR_FIELDS, 'NeedsEquipment', 'RequestStatus', 'RequestUniqueCode')()) as Record; diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/model.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/model.ts index 023b767..fd295b9 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/model.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/model.ts @@ -1,4 +1,11 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; +import { RequestTypeId } from '../../models/types'; + +/** + * Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков + * SharePoint выводятся из него, см. shared/listTitles.ts. + */ +export const FORM_ID: RequestTypeId = 'it-workplace-setup'; /** * Данные формы «Организация рабочего места». Ключи = internalName колонок SP-списка diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts index ec2f3f0..c6d07de 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts @@ -1,14 +1,12 @@ import { getSP } from '../../shared/pnp/pnpjs'; import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; import { buildRequestUniqueCode } from '../../shared/requestCode'; -import { IFormData } from './model'; - -export const DATA_LIST_TITLE = 'Организация рабочего места — данные'; -export const APPROVAL_LIST_TITLE = 'Организация рабочего места — согласование'; +import { approvalListTitle, dataListTitle } from '../../shared/listTitles'; +import { FORM_ID, IFormData } from './model'; /** Экземпляр сервиса списка согласования этой формы. */ export function workplaceApprovalService(): ApprovalListService { - return new ApprovalListService(APPROVAL_LIST_TITLE); + return new ApprovalListService(approvalListTitle(FORM_ID)); } /** @@ -19,7 +17,7 @@ export async function updateReviewFields( id: number, fields: { Software: string; AccessRights: string } ): Promise { - await getSP().web.lists.getByTitle(DATA_LIST_TITLE).items.getById(id).update({ + await getSP().web.lists.getByTitle(dataListTitle(FORM_ID)).items.getById(id).update({ Software: fields.Software === '' ? null : fields.Software, AccessRights: fields.AccessRights === '' ? null : fields.AccessRights, }); @@ -71,7 +69,7 @@ export async function saveWorkplaceRequest( status: string, sourceHireId?: number ): Promise { - const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE); + const list = getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildItem(data, status, sourceHireId); let id: number; diff --git a/src/webparts/requestPortal/shared/listTitles.ts b/src/webparts/requestPortal/shared/listTitles.ts new file mode 100644 index 0000000..bf2b3c0 --- /dev/null +++ b/src/webparts/requestPortal/shared/listTitles.ts @@ -0,0 +1,60 @@ +import { RequestTypeId } from '../models/types'; + +/** + * ЕДИНЫЙ источник имён списков SharePoint. + * + * Раньше каждая форма объявляла свои DATA_LIST_TITLE/APPROVAL_LIST_TITLE строковыми + * литералами — 20 копий имён по 7 формам. Имена стали нумерованными (под архивацию: + * когда список данных упирается в порог элементов, рядом появляется пара с номером + * N+1), и с литералами каждый номер пришлось бы править по всем формам руками. + * + * Конвенция — ровно та же, что у провижинера (provisioning/Provision-Lists.ps1, + * функция Get-NumberedListTitle): + * «<База> — данные» / «<База> — согласование» + * + * Номер СТРОГО перед суффиксом: имя списка согласования выводится из имени списка + * данных регексом /\s*—\s*данные\s*$/, привязанным к КОНЦУ строки (см. + * features/myRequests/myRequests.service.ts и approval-route + * frontend/.../approvalAdmin/services/requestManageService.ts). Имя вида + * «… — данные 1» этот регекс не матчит, и список согласования перестаёт находиться. + */ + +/** База имени по типу заявки. Ключи — те же, что в реестре форм (forms/registry.ts). */ +const LIST_TITLE_BASE: Record = { + 'clearance-form': 'Обходной лист', + 'hr-vacation': 'Заявка на отпуск', + 'it-1c-ticket': 'Заявка в отдел 1С', + 'it-workplace-setup': 'Организация рабочего места', + 'recruiting-hire': 'Прием на работу', + 'recruiting-selection': 'Заявка на подбор', + 'wh-tmc-transfer': 'СЗ на перемещение ТМЦ' +}; + +/** + * Номер текущей пары списков. Пока единый для всех типов заявок — архивных пар ещё + * нет, все формы работают с парой №1. + * + * Когда архивация заработает, номер будет приходить из approval-route (эндпоинт + * «текущий список по типу заявки») и заполняться один раз при старте web part, до + * загрузки чанка любой формы. Сигнатуры функций ниже при этом не меняются — поэтому + * вызовы по формам править второй раз не придётся. + */ +const CURRENT_LIST_NUMBER = 1; + +function listTitleBase(id: RequestTypeId): string { + const base = LIST_TITLE_BASE[id]; + if (!base) { + throw new Error(`Неизвестный тип заявки «${id}»: нет базы имени списка в LIST_TITLE_BASE`); + } + return base; +} + +/** Имя списка данных заявок этого типа. */ +export function dataListTitle(id: RequestTypeId): string { + return `${listTitleBase(id)} ${CURRENT_LIST_NUMBER} — данные`; +} + +/** Имя списка согласования заявок этого типа. */ +export function approvalListTitle(id: RequestTypeId): string { + return `${listTitleBase(id)} ${CURRENT_LIST_NUMBER} — согласование`; +}