diff --git a/src/webparts/requestPortal/forms/Clearance/submit.ts b/src/webparts/requestPortal/forms/Clearance/submit.ts index 05df2e4..cac5365 100644 --- a/src/webparts/requestPortal/forms/Clearance/submit.ts +++ b/src/webparts/requestPortal/forms/Clearance/submit.ts @@ -1,21 +1,15 @@ -import { getSP } from '../../shared/pnp/pnpjs'; -import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; -import { buildRequestUniqueCode } from '../../shared/requestCode'; -import { approvalListTitle, approvalListTitleForListId, dataListTitle } from '../../shared/listTitles'; +import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval'; +import { ISaveItemResult, resolveDataList, saveRequestItem } from '../../shared/dataList'; +import { ensureEmployee } from '../../shared/employee'; import { FORM_ID, IFormData } from './model'; /** - * Экземпляр сервиса списка согласования этой формы. - * - * listId — партиция КОНКРЕТНОЙ заявки (см. shared/approval/listPartitions.ts). - * Не задан — текущая партиция, как раньше. Список согласования ищется только по - * имени (нет прямой ссылки на него, в отличие от списка данных), поэтому для - * архивной партиции имя приходится резолвить через реестр — если реестр не - * знает про такой listId (устаревшая ссылка), резолвер бросит понятную ошибку, - * а не тихо откатится на текущую партицию. + * Экземпляр сервиса списка согласования этой формы — тонкая обёртка над + * approvalServiceFor (см. shared/approval/approvalServiceFactory.ts), под + * именем формы для читаемости вызывающего кода. */ export function clearanceApprovalService(listId?: string): ApprovalListService { - return new ApprovalListService(listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)); + return approvalServiceFor(FORM_ID, listId); } /** Отметки, которые проставляет не инициатор, а профильный согласующий (см. approverRights.ts). */ @@ -47,16 +41,10 @@ export async function updateApproverFields( Number.isFinite(amount) && fields.TrainingAmount.trim() !== '' ? amount : null; } if (Object.keys(item).length === 0) return; - const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); + const list = resolveDataList(FORM_ID, listId); await list.items.getById(id).update(item); } -/** email / claims-логин → пользователь сайта (Id для Person-поля + Title для заголовка). */ -async function ensureEmployee(login: string): Promise<{ Id: number; Title: string }> { - const full = login.indexOf('|') >= 0 ? login : `i:0#.f|membership|${login}`; - return getSP().web.ensureUser(full); -} - function pad2(n: number): string { return n < 10 ? `0${n}` : `${n}`; } @@ -83,11 +71,7 @@ function buildItem(data: IFormData, status: string, employeeId: number, employee }; } -export interface ISaveResult { - id: number; - /** Задан только при создании новой заявки (существующий код не меняется). */ - code?: string; -} +export type ISaveResult = ISaveItemResult; /** * Сохранить обходной лист: содержимое + маршрут (согласование + исполнение). @@ -111,23 +95,10 @@ export async function saveClearanceRequest( status: string, listId?: string ): Promise { - const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const employee = await ensureEmployee(data.Employee); const item = buildItem(data, status, employee.Id, employee.Title || data.Employee); - let id: number; - let code: string | undefined; - if (existingId) { - await list.items.getById(existingId).update(item); - id = existingId; - } else { - const added = await list.items.add(item); - id = (added as { Id: number }).Id; - // Код известен только после создания элемента (нужен его Id). - code = buildRequestUniqueCode(requestTypeId, id, new Date()); - await list.items.getById(id).update({ RequestUniqueCode: code }); - } - - await writeRoute(clearanceApprovalService(listId), id, approvers, executors, status); - return { id, code }; + const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item); + await writeRoute(clearanceApprovalService(listId), result.id, approvers, executors, status); + return result; } diff --git a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts index 30245ae..9ff9fe1 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts @@ -1,24 +1,17 @@ -import { getSP } from '../../shared/pnp/pnpjs'; -import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; -import { buildRequestUniqueCode } from '../../shared/requestCode'; -import { approvalListTitle, approvalListTitleForListId, dataListTitle } from '../../shared/listTitles'; +import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval'; +import { ISaveItemResult, saveRequestItem } from '../../shared/dataList'; import { FORM_ID, IFormData } from './model'; // Числовые поля: пустая строка → null, иначе Number(). const NUMBER_FIELDS: (keyof IFormData)[] = ['PpeHeight', 'PpeShoeSize', 'SalaryProbation', 'SalaryAfterProbation']; /** - * Экземпляр сервиса списка согласования этой формы. - * - * listId — партиция КОНКРЕТНОЙ заявки (см. shared/approval/listPartitions.ts). - * Не задан — текущая партиция, как раньше. Список согласования ищется только по - * имени (нет прямой ссылки на него, в отличие от списка данных), поэтому для - * архивной партиции имя приходится резолвить через реестр — если реестр не - * знает про такой listId (устаревшая ссылка), резолвер бросит понятную ошибку, - * а не тихо откатится на текущую партицию. + * Экземпляр сервиса списка согласования этой формы — тонкая обёртка над + * approvalServiceFor (см. shared/approval/approvalServiceFactory.ts), под + * именем формы для читаемости вызывающего кода. */ export function hireApprovalService(listId?: string): ApprovalListService { - return new ApprovalListService(listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)); + return approvalServiceFor(FORM_ID, listId); } /** Осмысленный заголовок элемента списка (системная колонка Title). */ @@ -42,11 +35,7 @@ function buildItem(data: IFormData, status: string): Record { return item; } -export interface ISaveResult { - id: number; - /** Задан только при создании новой заявки (существующий код не меняется). */ - code?: string; -} +export type ISaveResult = ISaveItemResult; /** * Сохранить заявку «на прием»: контент + маршрут (согласующие + исполнители) со статусом. @@ -72,22 +61,9 @@ export async function saveHireRequest( status: string, listId?: string ): Promise { - const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildItem(data, status); - let id: number; - let code: string | undefined; - if (existingId) { - await list.items.getById(existingId).update(item); - id = existingId; - } else { - const added = await list.items.add(item); - id = (added as { Id: number }).Id; - // Код известен только после создания элемента (нужен его Id). - code = buildRequestUniqueCode(requestTypeId, id, new Date()); - await list.items.getById(id).update({ RequestUniqueCode: code }); - } - - await writeRoute(hireApprovalService(listId), id, approvers, executors, status); - return { id, code }; + const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item); + await writeRoute(hireApprovalService(listId), result.id, approvers, executors, status); + return result; } diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts b/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts index 959fb6b..f723de7 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts +++ b/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts @@ -1,7 +1,6 @@ import { getSP } from '../../shared/pnp/pnpjs'; -import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; -import { buildRequestUniqueCode } from '../../shared/requestCode'; -import { approvalListTitle, approvalListTitleForListId, dataListTitle } from '../../shared/listTitles'; +import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval'; +import { ISaveItemResult, saveRequestItem } from '../../shared/dataList'; import { FORM_ID, IFormData } from './model'; // Поля-люди (один человек): логин → ensureUser → Id в колонку Id. @@ -12,17 +11,12 @@ const MULTI_PERSON_FIELDS: (keyof IFormData)[] = ['HiringDecisionMaker']; const NUMBER_FIELDS: (keyof IFormData)[] = ['OpenVacanciesCount', 'RateProbation', 'RateAfterProbation']; /** - * Экземпляр сервиса списка согласования этой формы. - * - * listId — партиция КОНКРЕТНОЙ заявки (см. shared/approval/listPartitions.ts). - * Не задан — текущая партиция, как раньше. Список согласования ищется только по - * имени (нет прямой ссылки на него, в отличие от списка данных), поэтому для - * архивной партиции имя приходится резолвить через реестр — если реестр не - * знает про такой listId (устаревшая ссылка), резолвер бросит понятную ошибку, - * а не тихо откатится на текущую партицию. + * Экземпляр сервиса списка согласования этой формы — тонкая обёртка над + * approvalServiceFor (см. shared/approval/approvalServiceFactory.ts), под + * именем формы для читаемости вызывающего кода. */ export function selectionApprovalService(listId?: string): ApprovalListService { - return new ApprovalListService(listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)); + return approvalServiceFor(FORM_ID, listId); } /** Логин → числовой Id пользователя на текущем сайте. */ @@ -86,11 +80,7 @@ async function applyPeople(item: Record, data: IFormData): Prom } } -export interface ISaveResult { - id: number; - /** Задан только при создании новой заявки (существующий код не меняется). */ - code?: string; -} +export type ISaveResult = ISaveItemResult; /** * Сохранить заявку «на подбор»: содержимое + маршрут согласования. @@ -114,24 +104,11 @@ export async function saveSelectionRequest( status: string, listId?: string ): Promise { - const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildScalarBody(data); item.RequestStatus = status; await applyPeople(item, data); - let id: number; - let code: string | undefined; - if (existingId) { - await list.items.getById(existingId).update(item); - id = existingId; - } else { - const added = await list.items.add(item); - id = (added as { Id: number }).Id; - // Код известен только после создания элемента (нужен его Id). - code = buildRequestUniqueCode(requestTypeId, id, new Date()); - await list.items.getById(id).update({ RequestUniqueCode: code }); - } - - await writeRoute(selectionApprovalService(listId), id, approvers, [], status); - return { id, code }; + const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item); + await writeRoute(selectionApprovalService(listId), result.id, approvers, [], status); + return result; } diff --git a/src/webparts/requestPortal/forms/Ticket1C/submit.ts b/src/webparts/requestPortal/forms/Ticket1C/submit.ts index e96b831..b4e459a 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/submit.ts +++ b/src/webparts/requestPortal/forms/Ticket1C/submit.ts @@ -1,7 +1,5 @@ -import { getSP } from '../../shared/pnp/pnpjs'; -import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; -import { buildRequestUniqueCode } from '../../shared/requestCode'; -import { approvalListTitle, approvalListTitleForListId, dataListTitle } from '../../shared/listTitles'; +import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval'; +import { ISaveItemResult, resolveDataList, saveRequestItem } from '../../shared/dataList'; import { FORM_ID, IFormData } from './model'; // Префикс уникального кода заявки — буквенная метка «1С» вместо числового id @@ -10,17 +8,12 @@ import { FORM_ID, IFormData } from './model'; export const TICKET1C_CODE_PREFIX = '1С'; /** - * Экземпляр сервиса списка согласования этой формы. - * - * listId — партиция КОНКРЕТНОЙ заявки (см. shared/approval/listPartitions.ts). - * Не задан — текущая партиция, как раньше. Список согласования ищется только по - * имени (нет прямой ссылки на него, в отличие от списка данных), поэтому для - * архивной партиции имя приходится резолвить через реестр — если реестр не - * знает про такой listId (устаревшая ссылка), резолвер бросит понятную ошибку, - * а не тихо откатится на текущую партицию. + * Экземпляр сервиса списка согласования этой формы — тонкая обёртка над + * approvalServiceFor (см. shared/approval/approvalServiceFactory.ts), под + * именем формы для читаемости вызывающего кода. */ export function ticket1cApprovalService(listId?: string): ApprovalListService { - return new ApprovalListService(listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)); + return approvalServiceFor(FORM_ID, listId); } /** Поля элемента списка данных из модели формы. TaskName пишется в системную колонку Title. */ @@ -36,11 +29,7 @@ function buildItem(data: IFormData, status: string): Record { return item; } -export interface ISaveResult { - id: number; - /** Задан только при создании новой заявки (существующий код не меняется). */ - code?: string; -} +export type ISaveResult = ISaveItemResult; /** * Сохранить заявку в отдел 1С: содержимое + маршрут согласования. @@ -63,24 +52,11 @@ export async function saveTicketRequest( status: string, listId?: string ): Promise { - const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildItem(data, status); - let id: number; - let code: string | undefined; - if (existingId) { - await list.items.getById(existingId).update(item); - id = existingId; - } else { - const added = await list.items.add(item); - id = (added as { Id: number }).Id; - // Код известен только после создания элемента (нужен его Id). - code = buildRequestUniqueCode(TICKET1C_CODE_PREFIX, id, new Date()); - await list.items.getById(id).update({ RequestUniqueCode: code }); - } - - await writeRoute(ticket1cApprovalService(listId), id, approvers, [], status); - return { id, code }; + const result = await saveRequestItem(FORM_ID, existingId, listId, TICKET1C_CODE_PREFIX, item); + await writeRoute(ticket1cApprovalService(listId), result.id, approvers, [], status); + return result; } /** @@ -89,12 +65,12 @@ export async function saveTicketRequest( * решение согласующего применяется из нескольких вкладок/сессий одновременно. */ export async function getPlannerTaskId(id: number, listId?: string): Promise { - const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); + const list = resolveDataList(FORM_ID, listId); const item = (await list.items.getById(id).select('PlannerTaskId')()) as { PlannerTaskId?: string }; return item.PlannerTaskId || undefined; } export async function setPlannerTaskId(id: number, taskId: string, listId?: string): Promise { - const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); + const list = resolveDataList(FORM_ID, listId); await list.items.getById(id).update({ PlannerTaskId: taskId }); } diff --git a/src/webparts/requestPortal/forms/TmcTransfer/submit.ts b/src/webparts/requestPortal/forms/TmcTransfer/submit.ts index a29622d..8dce2ba 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/submit.ts +++ b/src/webparts/requestPortal/forms/TmcTransfer/submit.ts @@ -1,22 +1,15 @@ -import { getSP } from '../../shared/pnp/pnpjs'; -import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; -import { buildRequestUniqueCode } from '../../shared/requestCode'; +import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval'; +import { ISaveItemResult, saveRequestItem } from '../../shared/dataList'; import { ILineItem } from '../../shared/ui'; -import { approvalListTitle, approvalListTitleForListId, dataListTitle } from '../../shared/listTitles'; import { FORM_ID, IFormData } from './model'; /** - * Экземпляр сервиса списка согласования этой формы. - * - * listId — партиция КОНКРЕТНОЙ заявки (см. shared/approval/listPartitions.ts). - * Не задан — текущая партиция, как раньше. Список согласования ищется только по - * имени (нет прямой ссылки на него, в отличие от списка данных), поэтому для - * архивной партиции имя приходится резолвить через реестр — если реестр не - * знает про такой listId (устаревшая ссылка), резолвер бросит понятную ошибку, - * а не тихо откатится на текущую партицию. + * Экземпляр сервиса списка согласования этой формы — тонкая обёртка над + * approvalServiceFor (см. shared/approval/approvalServiceFactory.ts), под + * именем формы для читаемости вызывающего кода. */ export function tmcTransferApprovalService(listId?: string): ApprovalListService { - return new ApprovalListService(listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)); + return approvalServiceFor(FORM_ID, listId); } /** Осмысленный заголовок элемента (системная колонка Title). */ @@ -43,11 +36,7 @@ function buildItem(data: IFormData, status: string): Record { return item; } -export interface ISaveResult { - id: number; - /** Задан только при создании новой заявки (существующий код не меняется). */ - code?: string; -} +export type ISaveResult = ISaveItemResult; /** * Сохранить заявку «СЗ на перемещение ТМЦ»: контент + маршрут (согласующие + исполнители). @@ -69,22 +58,9 @@ export async function saveTmcTransferRequest( status: string, listId?: string ): Promise { - const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildItem(data, status); - let id: number; - let code: string | undefined; - if (existingId) { - await list.items.getById(existingId).update(item); - id = existingId; - } else { - const added = await list.items.add(item); - id = (added as { Id: number }).Id; - // Код известен только после создания элемента (нужен его Id). - code = buildRequestUniqueCode(requestTypeId, id, new Date()); - await list.items.getById(id).update({ RequestUniqueCode: code }); - } - - await writeRoute(tmcTransferApprovalService(listId), id, approvers, executors, status); - return { id, code }; + const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item); + await writeRoute(tmcTransferApprovalService(listId), result.id, approvers, executors, status); + return result; } diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/submit.ts b/src/webparts/requestPortal/forms/UnpaidLeave/submit.ts index 57a23da..1a9f704 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/submit.ts +++ b/src/webparts/requestPortal/forms/UnpaidLeave/submit.ts @@ -1,27 +1,15 @@ -import { getSP } from '../../shared/pnp/pnpjs'; -import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; -import { buildRequestUniqueCode } from '../../shared/requestCode'; -import { approvalListTitle, approvalListTitleForListId, dataListTitle } from '../../shared/listTitles'; +import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval'; +import { ISaveItemResult, saveRequestItem } from '../../shared/dataList'; +import { ensureEmployee } from '../../shared/employee'; import { FORM_ID, IFormData } from './model'; /** - * Экземпляр сервиса списка согласования этой формы. - * - * listId — партиция КОНКРЕТНОЙ заявки (см. shared/approval/listPartitions.ts). - * Не задан — текущая партиция, как раньше. Список согласования ищется только по - * имени (нет прямой ссылки на него, в отличие от списка данных), поэтому для - * архивной партиции имя приходится резолвить через реестр — если реестр не - * знает про такой listId (устаревшая ссылка), резолвер бросит понятную ошибку, - * а не тихо откатится на текущую партицию. + * Экземпляр сервиса списка согласования этой формы — тонкая обёртка над + * approvalServiceFor (см. shared/approval/approvalServiceFactory.ts), под + * именем формы для читаемости вызывающего кода. */ export function unpaidLeaveApprovalService(listId?: string): ApprovalListService { - return new ApprovalListService(listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)); -} - -/** email / claims-логин → пользователь сайта (Id для Person-поля + Title для заголовка). */ -async function ensureUser(login: string): Promise<{ Id: number; Title: string }> { - const full = login.indexOf('|') >= 0 ? login : `i:0#.f|membership|${login}`; - return getSP().web.ensureUser(full); + return approvalServiceFor(FORM_ID, listId); } function pad2(n: number): string { @@ -64,11 +52,7 @@ function buildItem( }; } -export interface ISaveResult { - id: number; - /** Задан только при создании новой заявки (существующий код не меняется). */ - code?: string; -} +export type ISaveResult = ISaveItemResult; /** * Сохранить заявку без содержания: содержимое + маршрут согласования. @@ -89,25 +73,12 @@ export async function saveUnpaidLeaveRequest( status: string, listId?: string ): Promise { - const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); - const employee = await ensureUser(data.Employee); + const employee = await ensureEmployee(data.Employee); // Замещающий необязателен: резолвим только если выбран. - const substituteId = data.Substitute ? (await ensureUser(data.Substitute)).Id : undefined; + const substituteId = data.Substitute ? (await ensureEmployee(data.Substitute)).Id : undefined; const item = buildItem(data, status, employee.Id, employee.Title || data.Employee, substituteId); - let id: number; - let code: string | undefined; - if (existingId) { - await list.items.getById(existingId).update(item); - id = existingId; - } else { - const added = await list.items.add(item); - id = (added as { Id: number }).Id; - // Код известен только после создания элемента (нужен его Id). - code = buildRequestUniqueCode(requestTypeId, id, new Date()); - await list.items.getById(id).update({ RequestUniqueCode: code }); - } - - await writeRoute(unpaidLeaveApprovalService(listId), id, approvers, [], status); - return { id, code }; + const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item); + await writeRoute(unpaidLeaveApprovalService(listId), result.id, approvers, [], status); + return result; } diff --git a/src/webparts/requestPortal/forms/Vacation/submit.ts b/src/webparts/requestPortal/forms/Vacation/submit.ts index 22338be..6df8fa2 100644 --- a/src/webparts/requestPortal/forms/Vacation/submit.ts +++ b/src/webparts/requestPortal/forms/Vacation/submit.ts @@ -1,27 +1,15 @@ -import { getSP } from '../../shared/pnp/pnpjs'; -import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; -import { buildRequestUniqueCode } from '../../shared/requestCode'; -import { approvalListTitle, approvalListTitleForListId, dataListTitle } from '../../shared/listTitles'; +import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval'; +import { ISaveItemResult, saveRequestItem } from '../../shared/dataList'; +import { ensureEmployee } from '../../shared/employee'; import { FORM_ID, IFormData } from './model'; /** - * Экземпляр сервиса списка согласования этой формы. - * - * listId — партиция КОНКРЕТНОЙ заявки (см. shared/approval/listPartitions.ts). - * Не задан — текущая партиция, как раньше. Список согласования ищется только по - * имени (нет прямой ссылки на него, в отличие от списка данных), поэтому для - * архивной партиции имя приходится резолвить через реестр — если реестр не - * знает про такой listId (устаревшая ссылка), резолвер бросит понятную ошибку, - * а не тихо откатится на текущую партицию. + * Экземпляр сервиса списка согласования этой формы — тонкая обёртка над + * approvalServiceFor (см. shared/approval/approvalServiceFactory.ts), под + * именем формы для читаемости вызывающего кода. */ export function vacationApprovalService(listId?: string): ApprovalListService { - return new ApprovalListService(listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)); -} - -/** email / claims-логин → пользователь сайта (Id для Person-поля + Title для заголовка). */ -async function ensureEmployee(login: string): Promise<{ Id: number; Title: string }> { - const full = login.indexOf('|') >= 0 ? login : `i:0#.f|membership|${login}`; - return getSP().web.ensureUser(full); + return approvalServiceFor(FORM_ID, listId); } function pad2(n: number): string { @@ -55,11 +43,7 @@ function buildItem(data: IFormData, status: string, employeeId: number, employee }; } -export interface ISaveResult { - id: number; - /** Задан только при создании новой заявки (существующий код не меняется). */ - code?: string; -} +export type ISaveResult = ISaveItemResult; /** * Сохранить заявку на отпуск: содержимое + маршрут согласования. @@ -83,23 +67,10 @@ export async function saveVacationRequest( status: string, listId?: string ): Promise { - const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const employee = await ensureEmployee(data.Employee); const item = buildItem(data, status, employee.Id, employee.Title || data.Employee); - let id: number; - let code: string | undefined; - if (existingId) { - await list.items.getById(existingId).update(item); - id = existingId; - } else { - const added = await list.items.add(item); - id = (added as { Id: number }).Id; - // Код известен только после создания элемента (нужен его Id). - code = buildRequestUniqueCode(requestTypeId, id, new Date()); - await list.items.getById(id).update({ RequestUniqueCode: code }); - } - - await writeRoute(vacationApprovalService(listId), id, approvers, [], status); - return { id, code }; + const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item); + await writeRoute(vacationApprovalService(listId), result.id, approvers, [], status); + return result; } diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts index 0c808e3..b2d0841 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts @@ -1,21 +1,14 @@ -import { getSP } from '../../shared/pnp/pnpjs'; -import { ApprovalListService, IApproverToWrite, writeRoute } from '../../shared/approval'; -import { buildRequestUniqueCode } from '../../shared/requestCode'; -import { approvalListTitle, approvalListTitleForListId, dataListTitle } from '../../shared/listTitles'; +import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval'; +import { ISaveItemResult, resolveDataList, saveRequestItem } from '../../shared/dataList'; import { FORM_ID, IFormData } from './model'; /** - * Экземпляр сервиса списка согласования этой формы. - * - * listId — партиция КОНКРЕТНОЙ заявки (см. shared/approval/listPartitions.ts). - * Не задан — текущая партиция, как раньше. Список согласования ищется только по - * имени (нет прямой ссылки на него, в отличие от списка данных), поэтому для - * архивной партиции имя приходится резолвить через реестр — если реестр не - * знает про такой listId (устаревшая ссылка), резолвер бросит понятную ошибку, - * а не тихо откатится на текущую партицию. + * Экземпляр сервиса списка согласования этой формы — тонкая обёртка над + * approvalServiceFor (см. shared/approval/approvalServiceFactory.ts), под + * именем формы для читаемости вызывающего кода. */ export function workplaceApprovalService(listId?: string): ApprovalListService { - return new ApprovalListService(listId ? approvalListTitleForListId(FORM_ID, listId) : approvalListTitle(FORM_ID)); + return approvalServiceFor(FORM_ID, listId); } /** @@ -30,7 +23,7 @@ export async function updateReviewFields( fields: { Software: string; AccessRights: string }, listId?: string ): Promise { - const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); + const list = resolveDataList(FORM_ID, listId); await list.items.getById(id).update({ Software: fields.Software === '' ? null : fields.Software, AccessRights: fields.AccessRights === '' ? null : fields.AccessRights, @@ -62,11 +55,7 @@ function buildItem(data: IFormData, status: string, sourceHireId?: number): Reco return item; } -export interface ISaveResult { - id: number; - /** Задан только при создании новой заявки (существующий код не меняется). */ - code?: string; -} +export type ISaveResult = ISaveItemResult; /** * Сохранить заявку «орг. рабочего места»: контент + маршрут (согласующие + исполнители). @@ -92,22 +81,9 @@ export async function saveWorkplaceRequest( sourceHireId?: number, listId?: string ): Promise { - const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildItem(data, status, sourceHireId); - let id: number; - let code: string | undefined; - if (existingId) { - await list.items.getById(existingId).update(item); - id = existingId; - } else { - const added = await list.items.add(item); - id = (added as { Id: number }).Id; - // Код известен только после создания элемента (нужен его Id). - code = buildRequestUniqueCode(requestTypeId, id, new Date()); - await list.items.getById(id).update({ RequestUniqueCode: code }); - } - - await writeRoute(workplaceApprovalService(listId), id, approvers, executors, status); - return { id, code }; + const result = await saveRequestItem(FORM_ID, existingId, listId, requestTypeId, item); + await writeRoute(workplaceApprovalService(listId), result.id, approvers, executors, status); + return result; } diff --git a/src/webparts/requestPortal/shared/approval/approvalServiceFactory.ts b/src/webparts/requestPortal/shared/approval/approvalServiceFactory.ts new file mode 100644 index 0000000..b169bdf --- /dev/null +++ b/src/webparts/requestPortal/shared/approval/approvalServiceFactory.ts @@ -0,0 +1,24 @@ +import { RequestTypeId } from '../../models/types'; +import { approvalListTitle, approvalListTitleForListId } from '../listTitles'; +import { ApprovalListService } from './approvalList.service'; + +/** + * Экземпляр сервиса списка согласования формы. + * + * listId — партиция КОНКРЕТНОЙ заявки (см. shared/approval/listPartitions.ts). + * Не задан — текущая партиция, как раньше. Список согласования ищется только по + * имени (нет прямой ссылки на него, в отличие от списка данных), поэтому для + * архивной партиции имя приходится резолвить через реестр — если реестр не + * знает про такой listId (устаревшая ссылка), резолвер бросит понятную ошибку, + * а не тихо откатится на текущую партицию. + * + * Раньше каждая форма объявляла свою «xApprovalService» с телом-копией этой + * функции (8 одинаковых реализаций) — оставлены только тонкие обёртки под + * именем формы (см. submit.ts каждой формы), чтобы вызывающий код не терял + * читаемость («vacationApprovalService» вместо безликого параметра formId). + */ +export function approvalServiceFor(formId: RequestTypeId, listId?: string): ApprovalListService { + return new ApprovalListService( + listId ? approvalListTitleForListId(formId, listId) : approvalListTitle(formId) + ); +} diff --git a/src/webparts/requestPortal/shared/approval/index.ts b/src/webparts/requestPortal/shared/approval/index.ts index 433dd59..fbaf754 100644 --- a/src/webparts/requestPortal/shared/approval/index.ts +++ b/src/webparts/requestPortal/shared/approval/index.ts @@ -42,6 +42,7 @@ export { useRequestTypeId } from './useRequestTypeId'; export type { IRequestTypeIdState } from './useRequestTypeId'; export { ApprovalListService } from './approvalList.service'; export type { IApproverToWrite } from './approvalList.service'; +export { approvalServiceFor } from './approvalServiceFactory'; export { ApproverService } from './approverDecision.service'; export type { ApproverDecision, IPendingRow } from './approverDecision.service'; export { getPlannedMatrix, writeApprovers, writeRoute } from './approvalSubmission.service'; diff --git a/src/webparts/requestPortal/shared/dataList.ts b/src/webparts/requestPortal/shared/dataList.ts new file mode 100644 index 0000000..97620e5 --- /dev/null +++ b/src/webparts/requestPortal/shared/dataList.ts @@ -0,0 +1,62 @@ +import '@pnp/sp/webs'; +import '@pnp/sp/lists'; +import '@pnp/sp/items'; +import { IList } from '@pnp/sp/lists'; +import { getSP } from './pnp/pnpjs'; +import { buildRequestUniqueCode } from './requestCode'; +import { dataListTitle } from './listTitles'; +import { RequestTypeId } from '../models/types'; + +/** + * Список данных, в который фактически пишем: listId (конкретная партиция — + * либо ОБНОВЛЯЕМОЙ заявки, либо только что резолвленная текущая, см. + * getCurrentListPartition) или, если он не задан, список по конвенции имён + * (см. dataListTitle) — тот же откат, что действовал до партиционирования. + * + * Раньше эта строка (`listId ? getSP().web.getList(listId) : ...`) была + * скопирована в save/update-функции каждой формы и в submit.ts отдельных + * форм (Ticket1C — Planner, Clearance — updateApproverFields) — 8+ мест с + * одинаковым кодом. Расхождение здесь означало бы, что часть операций тихо + * пишет не в ту партицию. + */ +export function resolveDataList(formId: RequestTypeId, listId?: string): IList { + return listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(formId)); +} + +export interface ISaveItemResult { + id: number; + /** Задан только при создании новой заявки (существующий код не меняется). */ + code?: string; +} + +/** + * Общее ядро сохранения элемента списка данных: резолв списка, создание/обновление, + * генерация уникального кода заявки. Вынесено из saveXRequest каждой формы — сам + * набор полей (buildItem) и работа с маршрутом (writeRoute) остаются у формы, + * форма отличается только ими. + * + * existingId задан → обновляем ту же запись, иначе создаём новую. + * codePrefix — обычно числовой RequestTypeId, но может быть строкой-меткой + * (см. buildRequestUniqueCode, TICKET1C_CODE_PREFIX). + */ +export async function saveRequestItem( + formId: RequestTypeId, + existingId: number | undefined, + listId: string | undefined, + codePrefix: number | string, + item: Record +): Promise { + const list = resolveDataList(formId, listId); + + if (existingId) { + await list.items.getById(existingId).update(item); + return { id: existingId }; + } + + const added = await list.items.add(item); + const id = (added as { Id: number }).Id; + // Код известен только после создания элемента (нужен его Id). + const code = buildRequestUniqueCode(codePrefix, id, new Date()); + await list.items.getById(id).update({ RequestUniqueCode: code }); + return { id, code }; +} diff --git a/src/webparts/requestPortal/shared/employee.ts b/src/webparts/requestPortal/shared/employee.ts new file mode 100644 index 0000000..10826a8 --- /dev/null +++ b/src/webparts/requestPortal/shared/employee.ts @@ -0,0 +1,13 @@ +import '@pnp/sp/webs'; +import '@pnp/sp/site-users/web'; +import { getSP } from './pnp/pnpjs'; + +/** + * email / claims-логин → пользователь сайта (Id для Person-поля + Title для заголовка). + * Было скопировано в submit.ts нескольких форм с сотрудником-инициатором + * (Vacation, Clearance) — вынесено сюда, чтобы не разойтись при правке. + */ +export async function ensureEmployee(login: string): Promise<{ Id: number; Title: string }> { + const full = login.indexOf('|') >= 0 ? login : `i:0#.f|membership|${login}`; + return getSP().web.ensureUser(full); +}