From dcf3f06121f1435bc4e39b4f20e0cde763843ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Mon, 10 Aug 2026 16:59:20 +0500 Subject: [PATCH] feat: added unique request form code to forms --- .../RecruitingHire/RecruitingHireForm.tsx | 9 +++++++-- .../RecruitingHire/RecruitingHireView.tsx | 4 +++- .../forms/RecruitingHire/load.ts | 6 ++++-- .../forms/RecruitingHire/submit.ts | 19 ++++++++++++++----- .../RecruitingSelectionForm.tsx | 8 ++++++-- .../forms/RecruitingSelection/submit.ts | 13 +++++++++---- .../WorkplaceSetup/WorkplaceSetupForm.tsx | 9 +++++++-- .../WorkplaceSetup/WorkplaceSetupView.tsx | 4 +++- .../forms/WorkplaceSetup/load.ts | 6 ++++-- .../forms/WorkplaceSetup/submit.ts | 19 ++++++++++++++----- .../requestPortal/shared/requestCode.ts | 11 +++++++++++ .../shared/ui/RequestNumber/RequestNumber.tsx | 6 +++--- 12 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 src/webparts/requestPortal/shared/requestCode.ts diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx index fa27c87..54e022f 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx @@ -55,6 +55,7 @@ const RecruitingHireForm: React.FC = ({ requestType, context, // после первого сохранения. const [requestId, setRequestId] = useState(editId); const [requestStatus, setRequestStatus] = useState(undefined); + const [requestCode, setRequestCode] = useState(undefined); const submit = useRequestSubmit(); const readOnly = !isEditableStatus(requestStatus); @@ -73,6 +74,7 @@ const RecruitingHireForm: React.FC = ({ requestType, context, if (alive) { setData(loaded.data); setRequestStatus(loaded.status); + setRequestCode(loaded.code || undefined); setExistingAttachments(atts); } }) @@ -152,7 +154,7 @@ const RecruitingHireForm: React.FC = ({ requestType, context, }; const save = (status: string): (() => Promise) => async (): Promise => { - const id = await saveHireRequest( + const { id, code } = await saveHireRequest( requestId, data, toApprovers(matrix.rows), @@ -161,6 +163,9 @@ const RecruitingHireForm: React.FC = ({ requestType, context, ); setRequestId(id); setRequestStatus(status); + if (code) { + setRequestCode(code); + } if (files.length > 0) { await uploadAttachments(DATA_LIST_TITLE, id, files); // файлы — после создания элемента setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало @@ -218,7 +223,7 @@ const RecruitingHireForm: React.FC = ({ requestType, context, requestId || requestStatus ? ( <> {requestStatus && } - {requestId && } + {requestCode && } ) : undefined } diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx index 32c7a3c..329ec9f 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx @@ -41,6 +41,7 @@ const NOOP_SET: SetField = () => undefined; const RecruitingHireView: React.FC = ({ context, itemId }) => { const [data, setData] = useState(EMPTY_DATA); const [status, setStatus] = useState(''); + const [code, setCode] = useState(''); const [attachments, setAttachments] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -89,6 +90,7 @@ const RecruitingHireView: React.FC = ({ context, itemI if (alive) { setData(loaded.data); setStatus(loaded.status); + setCode(loaded.code); setAttachments(files); setError(null); setLoading(false); @@ -140,7 +142,7 @@ const RecruitingHireView: React.FC = ({ context, itemI
: undefined} > diff --git a/src/webparts/requestPortal/forms/RecruitingHire/load.ts b/src/webparts/requestPortal/forms/RecruitingHire/load.ts index b8d21fb..5c284fd 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/load.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/load.ts @@ -13,6 +13,8 @@ const SCALAR_FIELDS: (keyof IFormData)[] = [ export interface ILoadedRequest { data: IFormData; status: string; + /** Уникальный код заявки; '' — если запись создана до появления поля. */ + code: string; } /** Прочитать заявку «на прием» по id: модель формы + статус (для режима чтения). */ @@ -21,7 +23,7 @@ export async function loadHireRequest(itemId: number): Promise { const item = (await sp.web.lists .getByTitle(LIST_TITLE) .items.getById(itemId) - .select(...SCALAR_FIELDS, 'Status')()) as Record; + .select(...SCALAR_FIELDS, 'Status', 'RequestUniqueCode')()) as Record; const data: IFormData = { ...EMPTY_DATA }; const writable = data as unknown as Record; @@ -37,5 +39,5 @@ export async function loadHireRequest(itemId: number): Promise { } }); - return { data, status: (item.Status as string) || '' }; + return { data, status: (item.Status as string) || '', code: (item.RequestUniqueCode as string) || '' }; } diff --git a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts index 0014161..98be5de 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts @@ -1,5 +1,6 @@ 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 = 'Прием на работу — данные'; @@ -37,12 +38,18 @@ function buildItem(data: IFormData, status: string): Record { return item; } +export interface ISaveResult { + id: number; + /** Задан только при создании новой заявки (существующий код не меняется). */ + code?: string; +} + /** * Сохранить заявку «на прием»: контент + маршрут (согласующие + исполнители) со статусом. * existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую. * status = «Черновик» (сохранение) или «На согласовании» (отправка). * executors может быть пустым — тогда после согласования заявка станет «Согласовано». - * Возвращает Id заявки. + * Возвращает Id заявки (+ код при создании). */ export async function saveHireRequest( existingId: number | undefined, @@ -50,21 +57,23 @@ export async function saveHireRequest( approvers: IApproverToWrite[], executors: IApproverToWrite[], status: string -): Promise { +): Promise { const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE); 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 (id известен только после создания). - await list.items.getById(id).update({ RequestUniqueCode: String(id) }); + // Код известен только после создания элемента (нужен его Id). + code = buildRequestUniqueCode(HIRE_REQUEST_TYPE_ID, id, new Date()); + await list.items.getById(id).update({ RequestUniqueCode: code }); } await writeRoute(hireApprovalService(), id, approvers, executors, status); - return id; + return { id, code }; } diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx index b2e9c63..131c783 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx @@ -15,6 +15,7 @@ const RecruitingSelectionForm: React.FC = ({ requestType, con const [data, setData] = useState(EMPTY_DATA); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); + const [savedCode, setSavedCode] = useState(undefined); const [error, setError] = useState(null); const set: SetField = (key, value) => { @@ -35,8 +36,9 @@ const RecruitingSelectionForm: React.FC = ({ requestType, con setError(null); setSaving(true); try { - await submitRecruitingSelection(data); + const code = await submitRecruitingSelection(data); setSaved(true); + setSavedCode(code); if (onSaved) { onSaved(); } @@ -57,7 +59,9 @@ const RecruitingSelectionForm: React.FC = ({ requestType, con - {saved && Заявка сохранена в SharePoint.} + {saved && ( + Заявка сохранена в SharePoint. Код заявки: №{savedCode}. + )} {error && {error}} diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts b/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts index 9e3fb9f..b8c475b 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts +++ b/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts @@ -1,7 +1,10 @@ import { getSP } from '../../shared/pnp/pnpjs'; +import { buildRequestUniqueCode } from '../../shared/requestCode'; import { IFormData } from './model'; const LIST_TITLE = 'Заявка на подбор — данные'; +// Тип заявки во внешнем approval-route API (GET /api/request-types → id 4). +const RECRUITING_SELECTION_REQUEST_TYPE_ID = 4; // Поля-люди (один человек): логин → ensureUser → Id в колонку Id. const PERSON_FIELDS: (keyof IFormData)[] = ['Responsible', 'ProposedMentor', 'DirectManager']; @@ -16,8 +19,8 @@ async function resolveUserId(login: string): Promise { return ensured.Id; } -/** Записать заявку «на подбор» в список данных SharePoint. */ -export async function submitRecruitingSelection(data: IFormData): Promise { +/** Записать заявку «на подбор» в список данных SharePoint. Возвращает уникальный код заявки. */ +export async function submitRecruitingSelection(data: IFormData): Promise { const sp = getSP(); const item: Record = {}; @@ -56,6 +59,8 @@ export async function submitRecruitingSelection(data: IFormData): Promise const added = await sp.web.lists.getByTitle(LIST_TITLE).items.add(item); const id = (added as { Id: number }).Id; - // Уникальный код заявки: пока = id (id известен только после создания). - await sp.web.lists.getByTitle(LIST_TITLE).items.getById(id).update({ RequestUniqueCode: String(id) }); + // Код известен только после создания элемента (нужен его Id). + const code = buildRequestUniqueCode(RECRUITING_SELECTION_REQUEST_TYPE_ID, id, new Date()); + await sp.web.lists.getByTitle(LIST_TITLE).items.getById(id).update({ RequestUniqueCode: code }); + return code; } diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx index 76e5a0d..58271d1 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx @@ -54,6 +54,7 @@ const WorkplaceSetupForm: React.FC = ({ requestType, context, // открытии на редактирование (editId) — сразу из загруженной записи. const [requestId, setRequestId] = useState(editId); const [requestStatus, setRequestStatus] = useState(undefined); + const [requestCode, setRequestCode] = useState(undefined); const submit = useRequestSubmit(); // Редактировать можно новую/черновик/возвращённую/отклонённую; иначе только чтение. @@ -74,6 +75,7 @@ const WorkplaceSetupForm: React.FC = ({ requestType, context, if (alive) { setData(loaded.data); setRequestStatus(loaded.status); + setRequestCode(loaded.code || undefined); setExistingAttachments(atts); } }) @@ -175,7 +177,7 @@ const WorkplaceSetupForm: React.FC = ({ requestType, context, }; const save = (status: string): (() => Promise) => async (): Promise => { - const id = await saveWorkplaceRequest( + const { id, code } = await saveWorkplaceRequest( requestId, data, toApprovers(matrix.rows), @@ -185,6 +187,9 @@ const WorkplaceSetupForm: React.FC = ({ requestType, context, ); setRequestId(id); setRequestStatus(status); + if (code) { + setRequestCode(code); + } if (files.length > 0) { await uploadAttachments(DATA_LIST_TITLE, id, files); setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало @@ -242,7 +247,7 @@ const WorkplaceSetupForm: React.FC = ({ requestType, context, requestId || requestStatus ? ( <> {requestStatus && } - {requestId && } + {requestCode && } ) : undefined } diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx index 5eb0a09..a5c56fc 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx @@ -50,6 +50,7 @@ type SaveState = 'idle' | 'saving' | 'saved' | 'error'; const WorkplaceSetupView: React.FC = ({ context, itemId }) => { const [data, setData] = useState(EMPTY_DATA); const [status, setStatus] = useState(''); + const [code, setCode] = useState(''); const [attachments, setAttachments] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -101,6 +102,7 @@ const WorkplaceSetupView: React.FC = ({ context, itemI if (alive) { setData(loaded.data); setStatus(loaded.status); + setCode(loaded.code); setAttachments(files); setError(null); setLoading(false); @@ -202,7 +204,7 @@ const WorkplaceSetupView: React.FC = ({ context, itemI
: undefined} > diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts index 90eb0c0..4b6e639 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts @@ -11,6 +11,8 @@ const SCALAR_FIELDS: (keyof IFormData)[] = [ export interface ILoadedRequest { data: IFormData; status: string; + /** Уникальный код заявки; '' — если запись создана до появления поля. */ + code: string; } /** Прочитать заявку по id: модель формы + статус (для режима чтения). */ @@ -18,7 +20,7 @@ export async function loadWorkplaceRequest(itemId: number): Promise; + .select(...SCALAR_FIELDS, 'NeedsEquipment', 'Status', 'RequestUniqueCode')()) as Record; const data: IFormData = { ...EMPTY_DATA }; const writable = data as unknown as Record; @@ -36,5 +38,5 @@ export async function loadWorkplaceRequest(itemId: number): Promise { +): Promise { const list = getSP().web.lists.getByTitle(DATA_LIST_TITLE); 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 (id известен только после создания). - await list.items.getById(id).update({ RequestUniqueCode: String(id) }); + // Код известен только после создания элемента (нужен его Id). + code = buildRequestUniqueCode(WORKPLACE_REQUEST_TYPE_ID, id, new Date()); + await list.items.getById(id).update({ RequestUniqueCode: code }); } await writeRoute(workplaceApprovalService(), id, approvers, executors, status); - return id; + return { id, code }; } diff --git a/src/webparts/requestPortal/shared/requestCode.ts b/src/webparts/requestPortal/shared/requestCode.ts new file mode 100644 index 0000000..5432026 --- /dev/null +++ b/src/webparts/requestPortal/shared/requestCode.ts @@ -0,0 +1,11 @@ +/** + * Уникальный код заявки: {requestTypeId}-{YY}{MM}-{itemId}. + * requestTypeId — id типа заявки во внешнем approval-route API (GET /api/request-types). + * YY/MM — год и месяц подачи, itemId — Id элемента в списке данных. Пример: 1-2608-12. + */ +export function buildRequestUniqueCode(requestTypeId: number, itemId: number, submittedAt: Date): string { + const yy = String(submittedAt.getFullYear()).slice(-2); + const month = submittedAt.getMonth() + 1; + const mm = month < 10 ? `0${month}` : String(month); + return `${requestTypeId}-${yy}${mm}-${itemId}`; +} diff --git a/src/webparts/requestPortal/shared/ui/RequestNumber/RequestNumber.tsx b/src/webparts/requestPortal/shared/ui/RequestNumber/RequestNumber.tsx index 67fcebc..1886ff1 100644 --- a/src/webparts/requestPortal/shared/ui/RequestNumber/RequestNumber.tsx +++ b/src/webparts/requestPortal/shared/ui/RequestNumber/RequestNumber.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import styles from './RequestNumber.module.scss'; -/** Номер заявки (Id элемента списка) — плашка в правом углу шапки формы. */ -export const RequestNumber: React.FC<{ id: number }> = ({ id }) => ( - №{id} +/** Уникальный код заявки ({requestTypeId}-{YY}{MM}-{itemId}) — плашка в правом углу шапки формы. */ +export const RequestNumber: React.FC<{ code: string }> = ({ code }) => ( + №{code} );