diff --git a/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx b/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx index 1bd3345..24b775e 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceForm.tsx @@ -16,6 +16,7 @@ import { StatusBadge, useMatrixEditor, getPlannedMatrix, + getCurrentListPartition, notifyActiveStep, useRequestTypeId, REQUEST_STATUS, @@ -189,6 +190,21 @@ const ClearanceForm: React.FC = ({ if (!rtId) { throw new Error('Тип заявки не определён — обновите страницу и повторите.'); } + // requestId уже задан → редактируем существующую заявку, пишем в ту же + // партицию (editListId). Иначе — создаём новую, и партицию для записи + // спрашиваем у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit): + // это единственный запрос, который проверяет порог элементов текущего + // списка и ставит в очередь создание следующей партиции при достижении — + // без него архивация никогда не сработает. Сбой запроса не должен + // блокировать подачу заявки — откатываемся на список по конвенции имён. + let writeListId = editListId; + if (!requestId) { + try { + writeListId = (await getCurrentListPartition(context, rtId)).listId; + } catch (e) { + console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e); + } + } const { id, code } = await saveClearanceRequest( requestId, rtId, @@ -196,7 +212,7 @@ const ClearanceForm: React.FC = ({ toApprovers(matrix.rows), toApprovers(executors.rows), status, - editListId + writeListId ); setRequestId(id); setRequestStatus(status); @@ -207,13 +223,14 @@ const ClearanceForm: React.FC = ({ onSaved(); } // Отправка на согласование — уведомляем первый активный шаг (поток оповещения). - // Имена списков — той же партиции, что и сама заявка (editListId), не текущей: - // поток ищет строку маршрута именно в НЕЙ. + // Имена списков — той же партиции, куда фактически записали (writeListId), + // не обязательно текущей на момент рендера формы: поток ищет строку + // маршрута именно в НЕЙ. if (status === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), - approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID) + dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID), + approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID) }); } }; diff --git a/src/webparts/requestPortal/forms/Clearance/submit.ts b/src/webparts/requestPortal/forms/Clearance/submit.ts index aa3da4f..05df2e4 100644 --- a/src/webparts/requestPortal/forms/Clearance/submit.ts +++ b/src/webparts/requestPortal/forms/Clearance/submit.ts @@ -94,10 +94,13 @@ export interface ISaveResult { * existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую. * Возвращает Id заявки (+ код при создании). * - * listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadClearanceRequest). Имеет смысл - * только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — - * писать новые заявки в архив незачем и нельзя (approval-route переключает - * текущую партицию сам, по своему порогу элементов). + * listId — партиция, в которую фактически пишем: при редактировании — партиция + * ОБНОВЛЯЕМОЙ заявки (см. loadClearanceRequest), при создании — партиция, + * полученная от approval-route API непосредственно перед вызовом (см. + * getCurrentListPartition в ClearanceForm.tsx) — именно этот вызов, а не кеш из + * onInit, проверяет порог элементов и запускает архивацию следующей партиции. + * Не задан (сбой запроса за текущей партицией) → откат на список по конвенции + * имён, как до появления партиционирования. */ export async function saveClearanceRequest( existingId: number | undefined, @@ -108,8 +111,7 @@ export async function saveClearanceRequest( status: string, listId?: string ): Promise { - const list = - existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); + 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); @@ -126,6 +128,6 @@ export async function saveClearanceRequest( await list.items.getById(id).update({ RequestUniqueCode: code }); } - await writeRoute(clearanceApprovalService(existingId ? listId : undefined), id, approvers, executors, status); + await writeRoute(clearanceApprovalService(listId), id, approvers, executors, status); return { id, code }; } diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx index 9cf78ef..7706afd 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx @@ -20,6 +20,7 @@ import { StatusBadge, useMatrixEditor, getPlannedMatrix, + getCurrentListPartition, notifyActiveStep, useRequestTypeId, REQUEST_STATUS, @@ -186,6 +187,21 @@ const RecruitingHireForm: React.FC = ({ if (!rtId) { throw new Error('Тип заявки не определён — обновите страницу и повторите.'); } + // requestId уже задан → редактируем существующую заявку, пишем в ту же + // партицию (editListId). Иначе — создаём новую, и партицию для записи + // спрашиваем у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit): + // это единственный запрос, который проверяет порог элементов текущего + // списка и ставит в очередь создание следующей партиции при достижении — + // без него архивация никогда не сработает. Сбой запроса не должен + // блокировать подачу заявки — откатываемся на список по конвенции имён. + let writeListId = editListId; + if (!requestId) { + try { + writeListId = (await getCurrentListPartition(context, rtId)).listId; + } catch (e) { + console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e); + } + } const { id, code } = await saveHireRequest( requestId, rtId, @@ -193,7 +209,7 @@ const RecruitingHireForm: React.FC = ({ toApprovers(matrix.rows), toApprovers(executors.rows), status, - editListId + writeListId ); setRequestId(id); setRequestStatus(status); @@ -201,21 +217,22 @@ const RecruitingHireForm: React.FC = ({ setRequestCode(code); } if (files.length > 0) { - await uploadAttachments(editListId || dataListTitle(FORM_ID), id, files); // файлы — после создания элемента + await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, files); // файлы — после создания элемента setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало - setExistingAttachments(await getAttachments(editListId || dataListTitle(FORM_ID), id)); // показать добавленные + setExistingAttachments(await getAttachments(writeListId || dataListTitle(FORM_ID), id)); // показать добавленные } if (onSaved) { onSaved(); } // Отправка на согласование — уведомляем первый активный шаг (поток оповещения). - // Имена списков — той же партиции, что и сама заявка (editListId), не текущей: - // поток ищет строку маршрута именно в НЕЙ. + // Имена списков — той же партиции, куда фактически записали (writeListId), + // не обязательно текущей на момент рендера формы: поток ищет строку + // маршрута именно в НЕЙ. if (status === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), - approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID), + dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID), + approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID), }); } }; diff --git a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts index af3f216..30245ae 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts @@ -55,10 +55,13 @@ export interface ISaveResult { * executors может быть пустым — тогда после согласования заявка станет «Согласовано». * Возвращает Id заявки (+ код при создании). * - * listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadHireRequest). Имеет смысл - * только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — - * писать новые заявки в архив незачем и нельзя (approval-route переключает - * текущую партицию сам, по своему порогу элементов). + * listId — партиция, в которую фактически пишем: при редактировании — партиция + * ОБНОВЛЯЕМОЙ заявки (см. loadHireRequest), при создании — партиция, полученная + * от approval-route API непосредственно перед вызовом (см. getCurrentListPartition + * в RecruitingHireForm.tsx) — именно этот вызов, а не кеш из onInit, проверяет + * порог элементов и запускает архивацию следующей партиции. + * Не задан (сбой запроса за текущей партицией) → откат на список по конвенции + * имён, как до появления партиционирования. */ export async function saveHireRequest( existingId: number | undefined, @@ -69,8 +72,7 @@ export async function saveHireRequest( status: string, listId?: string ): Promise { - const list = - existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); + const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildItem(data, status); let id: number; @@ -86,6 +88,6 @@ export async function saveHireRequest( await list.items.getById(id).update({ RequestUniqueCode: code }); } - await writeRoute(hireApprovalService(existingId ? listId : undefined), id, approvers, executors, status); + await writeRoute(hireApprovalService(listId), id, approvers, executors, status); return { id, code }; } diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx index 3f1ae9d..47284df 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx @@ -20,6 +20,7 @@ import { StatusBadge, useMatrixEditor, getPlannedMatrix, + getCurrentListPartition, notifyActiveStep, useRequestTypeId, REQUEST_STATUS, @@ -160,13 +161,28 @@ const RecruitingSelectionForm: React.FC = ({ if (!rtId) { throw new Error('Тип заявки не определён — обновите страницу и повторите.'); } + // requestId уже задан → редактируем существующую заявку, пишем в ту же + // партицию (editListId). Иначе — создаём новую, и партицию для записи + // спрашиваем у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit): + // это единственный запрос, который проверяет порог элементов текущего + // списка и ставит в очередь создание следующей партиции при достижении — + // без него архивация никогда не сработает. Сбой запроса не должен + // блокировать подачу заявки — откатываемся на список по конвенции имён. + let writeListId = editListId; + if (!requestId) { + try { + writeListId = (await getCurrentListPartition(context, rtId)).listId; + } catch (e) { + console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e); + } + } const { id, code } = await saveSelectionRequest( requestId, rtId, data, toApprovers(matrix.rows), status, - editListId + writeListId ); setRequestId(id); setRequestStatus(status); @@ -174,21 +190,22 @@ const RecruitingSelectionForm: React.FC = ({ setRequestCode(code); } if (files.length > 0) { - await uploadAttachments(editListId || dataListTitle(FORM_ID), id, files); // файлы — после создания элемента + await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, files); // файлы — после создания элемента setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало - setExistingAttachments(await getAttachments(editListId || dataListTitle(FORM_ID), id)); + setExistingAttachments(await getAttachments(writeListId || dataListTitle(FORM_ID), id)); } if (onSaved) { onSaved(); } // Отправка на согласование — уведомляем первый активный шаг (поток оповещения). - // Имена списков — той же партиции, что и сама заявка (editListId), не текущей: - // поток ищет строку маршрута именно в НЕЙ. + // Имена списков — той же партиции, куда фактически записали (writeListId), + // не обязательно текущей на момент рендера формы: поток ищет строку + // маршрута именно в НЕЙ. if (status === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), - approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID) + dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID), + approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID) }); } }; diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts b/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts index 15d6be6..959fb6b 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts +++ b/src/webparts/requestPortal/forms/RecruitingSelection/submit.ts @@ -98,10 +98,13 @@ export interface ISaveResult { * status = «Черновик» (сохранение) или «На согласовании» (отправка). Фазы исполнения * нет — writeRoute вызывается с пустым списком исполнителей. Возвращает Id (+ код при создании). * - * listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadSelectionRequest). Имеет смысл - * только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — - * писать новые заявки в архив незачем и нельзя (approval-route переключает - * текущую партицию сам, по своему порогу элементов). + * listId — партиция, в которую фактически пишем: при редактировании — партиция + * ОБНОВЛЯЕМОЙ заявки (см. loadSelectionRequest), при создании — партиция, + * полученная от approval-route API непосредственно перед вызовом (см. + * getCurrentListPartition в RecruitingSelectionForm.tsx) — именно этот вызов, + * а не кеш из onInit, проверяет порог элементов и запускает архивацию + * следующей партиции. Не задан (сбой запроса за текущей партицией) → откат + * на список по конвенции имён, как до появления партиционирования. */ export async function saveSelectionRequest( existingId: number | undefined, @@ -111,8 +114,7 @@ export async function saveSelectionRequest( status: string, listId?: string ): Promise { - const list = - existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); + 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); @@ -130,6 +132,6 @@ export async function saveSelectionRequest( await list.items.getById(id).update({ RequestUniqueCode: code }); } - await writeRoute(selectionApprovalService(existingId ? listId : undefined), id, approvers, [], status); + await writeRoute(selectionApprovalService(listId), id, approvers, [], status); return { id, code }; } diff --git a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx index 2095c43..6714c90 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx +++ b/src/webparts/requestPortal/forms/Ticket1C/Ticket1CForm.tsx @@ -20,6 +20,7 @@ import { StatusBadge, useMatrixEditor, getPlannedMatrix, + getCurrentListPartition, notifyActiveStep, useRequestTypeId, REQUEST_STATUS, @@ -225,12 +226,28 @@ const Ticket1CForm: React.FC = ({ ? REQUEST_STATUS.onApproval : REQUEST_STATUS.approved; + // requestId уже задан → редактируем существующую заявку, пишем в ту же + // партицию (editListId). Иначе — создаём новую, и партицию для записи + // спрашиваем у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit): + // это единственный запрос, который проверяет порог элементов текущего + // списка и ставит в очередь создание следующей партиции при достижении — + // без него архивация никогда не сработает. Сбой запроса не должен + // блокировать подачу заявки — откатываемся на список по конвенции имён. + let writeListId = editListId; + if (!requestId) { + try { + writeListId = (await getCurrentListPartition(context, rtId)).listId; + } catch (e) { + console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e); + } + } + const { id, code } = await saveTicketRequest( requestId, data, toApprovers(matrix.rows), finalStatus, - editListId + writeListId ); setRequestId(id); setRequestStatus(finalStatus); @@ -240,9 +257,9 @@ const Ticket1CForm: React.FC = ({ let attachments = existingAttachments; if (files.length > 0) { - await uploadAttachments(editListId || dataListTitle(FORM_ID), id, files); + await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, files); setFiles([]); - attachments = await getAttachments(editListId || dataListTitle(FORM_ID), id); + attachments = await getAttachments(writeListId || dataListTitle(FORM_ID), id); setExistingAttachments(attachments); } @@ -251,12 +268,13 @@ const Ticket1CForm: React.FC = ({ } if (finalStatus === REQUEST_STATUS.onApproval) { - // Имена списков — той же партиции, что и сама заявка (editListId), не - // текущей: поток ищет строку маршрута именно в НЕЙ. + // Имена списков — той же партиции, куда фактически записали (writeListId), + // не обязательно текущей на момент рендера формы: поток ищет строку + // маршрута именно в НЕЙ. await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), - approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID) + dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID), + approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID) }); } else if (finalStatus === REQUEST_STATUS.approved) { // Согласование не требуется для этого вида работ — сразу передаём в отдел 1С. @@ -268,7 +286,7 @@ const Ticket1CForm: React.FC = ({ attachments, webAbsoluteUrl: context.pageContext.web.absoluteUrl }); - await setPlannerTaskId(id, taskId, editListId); + await setPlannerTaskId(id, taskId, writeListId); } }; diff --git a/src/webparts/requestPortal/forms/Ticket1C/submit.ts b/src/webparts/requestPortal/forms/Ticket1C/submit.ts index 935944f..e96b831 100644 --- a/src/webparts/requestPortal/forms/Ticket1C/submit.ts +++ b/src/webparts/requestPortal/forms/Ticket1C/submit.ts @@ -48,10 +48,13 @@ export interface ISaveResult { * Фазы исполнения нет: после согласования заявка уходит в Planner, а не исполнителю * из матрицы. Возвращает Id заявки (+ код при создании). * - * listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadTicketRequest). Имеет смысл - * только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — - * писать новые заявки в архив незачем и нельзя (approval-route переключает - * текущую партицию сам, по своему порогу элементов). + * listId — партиция, в которую фактически пишем: при редактировании — партиция + * ОБНОВЛЯЕМОЙ заявки (см. loadTicketRequest), при создании — партиция, + * полученная от approval-route API непосредственно перед вызовом (см. + * getCurrentListPartition в Ticket1CForm.tsx) — именно этот вызов, а не кеш из + * onInit, проверяет порог элементов и запускает архивацию следующей партиции. + * Не задан (сбой запроса за текущей партицией) → откат на список по конвенции + * имён, как до появления партиционирования. */ export async function saveTicketRequest( existingId: number | undefined, @@ -60,8 +63,7 @@ export async function saveTicketRequest( status: string, listId?: string ): Promise { - const list = - existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); + const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildItem(data, status); let id: number; @@ -77,7 +79,7 @@ export async function saveTicketRequest( await list.items.getById(id).update({ RequestUniqueCode: code }); } - await writeRoute(ticket1cApprovalService(existingId ? listId : undefined), id, approvers, [], status); + await writeRoute(ticket1cApprovalService(listId), id, approvers, [], status); return { id, code }; } diff --git a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx index 0a085ee..3ab89a0 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx +++ b/src/webparts/requestPortal/forms/TmcTransfer/TmcTransferForm.tsx @@ -26,7 +26,8 @@ import { isEditableStatus, IApproverToWrite, APPROVAL_PHASE, - findUnassignedStep + findUnassignedStep, + getCurrentListPartition } from '../../shared/approval'; import { uploadAttachments, @@ -202,6 +203,17 @@ const TmcTransferForm: React.FC = ({ if (!rtId) { throw new Error('Тип заявки не определён — обновите страницу и повторите.'); } + // Новая заявка — резолвим ТЕКУЩУЮ партицию через approval-route (заодно + // выполняется проверка порога и создание следующей архивной партиции при + // необходимости); при редактировании остаёмся в партиции самой заявки. + let writeListId = editListId; + if (!requestId) { + try { + writeListId = (await getCurrentListPartition(context, rtId)).listId; + } catch (e) { + console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e); + } + } const { id, code } = await saveTmcTransferRequest( requestId, rtId, @@ -209,7 +221,7 @@ const TmcTransferForm: React.FC = ({ toApprovers(matrix.rows), toApprovers(executors.rows), status, - editListId + writeListId ); setRequestId(id); setRequestStatus(status); @@ -217,21 +229,22 @@ const TmcTransferForm: React.FC = ({ setRequestCode(code); } if (files.length > 0) { - await uploadAttachments(editListId || dataListTitle(FORM_ID), id, files); + await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, files); setFiles([]); - setExistingAttachments(await getAttachments(editListId || dataListTitle(FORM_ID), id)); + setExistingAttachments(await getAttachments(writeListId || dataListTitle(FORM_ID), id)); } if (onSaved) { onSaved(); } // Отправка на согласование — уведомляем первый активный шаг (поток оповещения). - // Имена списков — той же партиции, что и сама заявка (editListId), не текущей: - // поток ищет строку маршрута именно в НЕЙ. + // Имена списков — той же партиции, куда только что записали (writeListId), + // а не editListId: для новой заявки это может быть только что созданная + // текущая партиция, отличная от editListId (который тут undefined). if (status === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), - approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID) + dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID), + approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID) }); } }; diff --git a/src/webparts/requestPortal/forms/TmcTransfer/submit.ts b/src/webparts/requestPortal/forms/TmcTransfer/submit.ts index 2e584c3..a29622d 100644 --- a/src/webparts/requestPortal/forms/TmcTransfer/submit.ts +++ b/src/webparts/requestPortal/forms/TmcTransfer/submit.ts @@ -54,10 +54,11 @@ export interface ISaveResult { * existingId задан → обновляем ту же запись (черновик/доработка), иначе создаём новую. * Возвращает Id заявки (+ уникальный код при создании). * - * listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadTmcTransferRequest). Имеет смысл - * только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — - * писать новые заявки в архив незачем и нельзя (approval-route переключает - * текущую партицию сам, по своему порогу элементов). + * listId — партиция, в которую пишем: для обновления существующей заявки — + * партиция ЭТОЙ заявки (см. loadTmcTransferRequest), для новой — ТЕКУЩАЯ + * партиция (см. getCurrentListPartition), которую вызывающая сторона резолвит + * заранее и передаёт сюда, а не текущая-по-умолчанию: именно на этом резолве + * держится проверка порога и создание следующей архивной партиции. */ export async function saveTmcTransferRequest( existingId: number | undefined, @@ -68,8 +69,7 @@ export async function saveTmcTransferRequest( status: string, listId?: string ): Promise { - const list = - existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); + const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildItem(data, status); let id: number; @@ -85,6 +85,6 @@ export async function saveTmcTransferRequest( await list.items.getById(id).update({ RequestUniqueCode: code }); } - await writeRoute(tmcTransferApprovalService(existingId ? listId : undefined), id, approvers, executors, status); + await writeRoute(tmcTransferApprovalService(listId), id, approvers, executors, status); return { id, code }; } diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx index f30629d..d277663 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx +++ b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx @@ -21,7 +21,8 @@ import { REQUEST_STATUS, isEditableStatus, IApproverToWrite, - findUnassignedStep + findUnassignedStep, + getCurrentListPartition } from '../../shared/approval'; import { IRequestFormProps } from '../../models/types'; import { @@ -176,13 +177,24 @@ const UnpaidLeaveForm: React.FC = ({ if (!rtId) { throw new Error('Тип заявки не определён — обновите страницу и повторите.'); } + // Новая заявка пишется в ТЕКУЩУЮ партицию — запрашиваем её у approval-route + // API непосредственно перед записью (не кеш из onInit): именно этот вызов + // проверяет порог элементов и запускает архивацию следующей партиции. + let writeListId = editListId; + if (!requestId) { + try { + writeListId = (await getCurrentListPartition(context, rtId)).listId; + } catch (e) { + console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e); + } + } const { id, code } = await saveUnpaidLeaveRequest( requestId, rtId, data, toApprovers(matrix.rows), status, - editListId + writeListId ); setRequestId(id); setRequestStatus(status); @@ -193,13 +205,13 @@ const UnpaidLeaveForm: React.FC = ({ onSaved(); } // Отправка на согласование — уведомляем первый активный шаг (поток оповещения). - // Имена списков — той же партиции, что и сама заявка (editListId), не текущей: - // поток ищет строку маршрута именно в НЕЙ. + // Имена списков — той же партиции, куда фактически записана заявка (writeListId), + // не текущей: поток ищет строку маршрута именно в НЕЙ. if (status === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), - approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID) + dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID), + approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID) }); } }; diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/submit.ts b/src/webparts/requestPortal/forms/UnpaidLeave/submit.ts index a016268..57a23da 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/submit.ts +++ b/src/webparts/requestPortal/forms/UnpaidLeave/submit.ts @@ -89,8 +89,7 @@ export async function saveUnpaidLeaveRequest( status: string, listId?: string ): Promise { - const list = - existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); + const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const employee = await ensureUser(data.Employee); // Замещающий необязателен: резолвим только если выбран. const substituteId = data.Substitute ? (await ensureUser(data.Substitute)).Id : undefined; @@ -109,6 +108,6 @@ export async function saveUnpaidLeaveRequest( await list.items.getById(id).update({ RequestUniqueCode: code }); } - await writeRoute(unpaidLeaveApprovalService(existingId ? listId : undefined), id, approvers, [], status); + await writeRoute(unpaidLeaveApprovalService(listId), id, approvers, [], status); return { id, code }; } diff --git a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx index 154efaa..e93626d 100644 --- a/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx +++ b/src/webparts/requestPortal/forms/Vacation/VacationForm.tsx @@ -16,6 +16,7 @@ import { StatusBadge, useMatrixEditor, getPlannedMatrix, + getCurrentListPartition, notifyActiveStep, useRequestTypeId, REQUEST_STATUS, @@ -173,7 +174,29 @@ const VacationForm: React.FC = ({ if (!rtId) { throw new Error('Тип заявки не определён — обновите страницу и повторите.'); } - const { id, code } = await saveVacationRequest(requestId, rtId, data, toApprovers(matrix.rows), status, editListId); + // requestId уже задан → редактируем существующую заявку, пишем в ту же + // партицию (editListId). Иначе — создаём новую, и партицию для записи + // спрашиваем у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit): + // это единственный запрос, который проверяет порог элементов текущего + // списка и ставит в очередь создание следующей партиции при достижении — + // без него архивация никогда не сработает. Сбой запроса не должен + // блокировать подачу заявки — откатываемся на список по конвенции имён. + let writeListId = editListId; + if (!requestId) { + try { + writeListId = (await getCurrentListPartition(context, rtId)).listId; + } catch (e) { + console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e); + } + } + const { id, code } = await saveVacationRequest( + requestId, + rtId, + data, + toApprovers(matrix.rows), + status, + writeListId + ); setRequestId(id); setRequestStatus(status); if (code) { @@ -183,13 +206,14 @@ const VacationForm: React.FC = ({ onSaved(); } // Отправка на согласование — уведомляем первый активный шаг (поток оповещения). - // Имена списков — той же партиции, что и сама заявка (editListId), не текущей: - // поток ищет строку маршрута именно в НЕЙ. + // Имена списков — той же партиции, куда фактически записали (writeListId), + // не обязательно текущей на момент рендера формы: поток ищет строку + // маршрута именно в НЕЙ. if (status === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), - approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID) + dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID), + approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID) }); } }; diff --git a/src/webparts/requestPortal/forms/Vacation/submit.ts b/src/webparts/requestPortal/forms/Vacation/submit.ts index 1827ced..22338be 100644 --- a/src/webparts/requestPortal/forms/Vacation/submit.ts +++ b/src/webparts/requestPortal/forms/Vacation/submit.ts @@ -67,10 +67,13 @@ export interface ISaveResult { * Фазы исполнения у отпуска нет — writeRoute вызывается с пустым списком исполнителей. * Возвращает Id заявки (+ код при создании). * - * listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadVacationRequest). Имеет смысл - * только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — - * писать новые заявки в архив незачем и нельзя (approval-route переключает - * текущую партицию сам, по своему порогу элементов). + * listId — партиция, в которую фактически пишем: при редактировании — партиция + * ОБНОВЛЯЕМОЙ заявки (см. loadVacationRequest), при создании — партиция, + * полученная от approval-route API непосредственно перед вызовом (см. + * getCurrentListPartition в VacationForm.tsx) — именно этот вызов, а не кеш из + * onInit, проверяет порог элементов и запускает архивацию следующей партиции. + * Не задан (сбой запроса за текущей партицией) → откат на список по конвенции + * имён, как до появления партиционирования. */ export async function saveVacationRequest( existingId: number | undefined, @@ -80,8 +83,7 @@ export async function saveVacationRequest( status: string, listId?: string ): Promise { - const list = - existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); + 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); @@ -98,6 +100,6 @@ export async function saveVacationRequest( await list.items.getById(id).update({ RequestUniqueCode: code }); } - await writeRoute(vacationApprovalService(existingId ? listId : undefined), id, approvers, [], status); + await writeRoute(vacationApprovalService(listId), id, approvers, [], status); return { id, code }; } diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx index be54c31..7e6e4e6 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx @@ -20,6 +20,7 @@ import { StatusBadge, useMatrixEditor, getPlannedMatrix, + getCurrentListPartition, notifyActiveStep, useRequestTypeId, REQUEST_STATUS, @@ -210,6 +211,21 @@ const WorkplaceSetupForm: React.FC = ({ if (!rtId) { throw new Error('Тип заявки не определён — обновите страницу и повторите.'); } + // requestId уже задан → редактируем существующую заявку, пишем в ту же + // партицию (editListId). Иначе — создаём новую, и партицию для записи + // спрашиваем у approval-route ПРЯМО СЕЙЧАС (а не берём из кеша onInit): + // это единственный запрос, который проверяет порог элементов текущего + // списка и ставит в очередь создание следующей партиции при достижении — + // без него архивация никогда не сработает. Сбой запроса не должен + // блокировать подачу заявки — откатываемся на список по конвенции имён. + let writeListId = editListId; + if (!requestId) { + try { + writeListId = (await getCurrentListPartition(context, rtId)).listId; + } catch (e) { + console.error('Не удалось получить текущую партицию списка — используется список по конвенции имён', e); + } + } const { id, code } = await saveWorkplaceRequest( requestId, rtId, @@ -218,7 +234,7 @@ const WorkplaceSetupForm: React.FC = ({ toApprovers(executors.rows), status, sourceId, - editListId + writeListId ); setRequestId(id); setRequestStatus(status); @@ -226,21 +242,22 @@ const WorkplaceSetupForm: React.FC = ({ setRequestCode(code); } if (files.length > 0) { - await uploadAttachments(editListId || dataListTitle(FORM_ID), id, files); + await uploadAttachments(writeListId || dataListTitle(FORM_ID), id, files); setFiles([]); // выгруженные файлы убираем, чтобы пересохранение не дублировало - setExistingAttachments(await getAttachments(editListId || dataListTitle(FORM_ID), id)); // показать добавленные + setExistingAttachments(await getAttachments(writeListId || dataListTitle(FORM_ID), id)); // показать добавленные } if (onSaved) { onSaved(); } // Отправка на согласование — уведомляем первый активный шаг (поток оповещения). - // Имена списков — той же партиции, что и сама заявка (editListId), не текущей: - // поток ищет строку маршрута именно в НЕЙ. + // Имена списков — той же партиции, куда фактически записали (writeListId), + // не обязательно текущей на момент рендера формы: поток ищет строку + // маршрута именно в НЕЙ. if (status === REQUEST_STATUS.onApproval) { await notifyActiveStep(context, approval, id, { requestTypeId: rtId, - dataListName: editListId ? dataListTitleForListId(FORM_ID, editListId) : dataListTitle(FORM_ID), - approvalListName: editListId ? approvalListTitleForListId(FORM_ID, editListId) : approvalListTitle(FORM_ID), + dataListName: writeListId ? dataListTitleForListId(FORM_ID, writeListId) : dataListTitle(FORM_ID), + approvalListName: writeListId ? approvalListTitleForListId(FORM_ID, writeListId) : approvalListTitle(FORM_ID), }); } }; diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts index bf6a187..0c808e3 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts @@ -74,10 +74,13 @@ export interface ISaveResult { * Исполнители пишутся сразу — фаза исполнения стартует автоматически после согласования. * sourceHireId — если заявка создана на основе «прием». Возвращает Id заявки (+ код при создании). * - * listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadWorkplaceRequest). Имеет смысл - * только вместе с existingId: новая заявка всегда создаётся в ТЕКУЩЕЙ партиции — - * писать новые заявки в архив незачем и нельзя (approval-route переключает - * текущую партицию сам, по своему порогу элементов). + * listId — партиция, в которую фактически пишем: при редактировании — партиция + * ОБНОВЛЯЕМОЙ заявки (см. loadWorkplaceRequest), при создании — партиция, + * полученная от approval-route API непосредственно перед вызовом (см. + * getCurrentListPartition в WorkplaceSetupForm.tsx) — именно этот вызов, а не + * кеш из onInit, проверяет порог элементов и запускает архивацию следующей + * партиции. Не задан (сбой запроса за текущей партицией) → откат на список по + * конвенции имён, как до появления партиционирования. */ export async function saveWorkplaceRequest( existingId: number | undefined, @@ -89,8 +92,7 @@ export async function saveWorkplaceRequest( sourceHireId?: number, listId?: string ): Promise { - const list = - existingId && listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); + const list = listId ? getSP().web.getList(listId) : getSP().web.lists.getByTitle(dataListTitle(FORM_ID)); const item = buildItem(data, status, sourceHireId); let id: number; @@ -106,6 +108,6 @@ export async function saveWorkplaceRequest( await list.items.getById(id).update({ RequestUniqueCode: code }); } - await writeRoute(workplaceApprovalService(existingId ? listId : undefined), id, approvers, executors, status); + await writeRoute(workplaceApprovalService(listId), id, approvers, executors, status); return { id, code }; } diff --git a/src/webparts/requestPortal/shared/approval/approvalRoute.service.ts b/src/webparts/requestPortal/shared/approval/approvalRoute.service.ts index 385e9f9..cd44e43 100644 --- a/src/webparts/requestPortal/shared/approval/approvalRoute.service.ts +++ b/src/webparts/requestPortal/shared/approval/approvalRoute.service.ts @@ -3,6 +3,7 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; import { APPROVAL_API_CLIENT_ID, getApprovalApiBaseUrl } from '../config'; import { ApprovalStatus, + ICurrentListPartition, IApiResponse, IRequestType, IRequestTypeListSeries, @@ -54,6 +55,30 @@ export async function getListPartitions(context: WebPartContext): Promise { + const c = await client(context); + const response: HttpClientResponse = await c.get( + `${getApprovalApiBaseUrl()}/api/request-types/${requestTypeId}/lists/current`, + AadHttpClient.configurations.v1 + ); + if (!response.ok) { + throw new Error(`request-types/${requestTypeId}/lists/current: ${response.status} ${response.statusText}`); + } + const body: IApiResponse = await response.json(); + return body.data; +} + // Кеш на уровне МОДУЛЯ (не useRef/useState) — список типов заявок админ меняет // редко, а без кеша каждая форма и каждый переход в режим согласующего дёргали // бы /api/request-types заново. Живёт до перезагрузки страницы — тот же принцип, diff --git a/src/webparts/requestPortal/shared/approval/index.ts b/src/webparts/requestPortal/shared/approval/index.ts index abc7170..433dd59 100644 --- a/src/webparts/requestPortal/shared/approval/index.ts +++ b/src/webparts/requestPortal/shared/approval/index.ts @@ -14,6 +14,7 @@ export type { IApprovalRow } from './models/IApprovalRow'; export type { ApprovalStatus, IListPartition, + ICurrentListPartition, IRequestType, IRequestTypeListSeries, IResolvedRoute, @@ -21,7 +22,13 @@ export type { IResolveFieldValue, } from './models/IApprovalRouteApi'; -export { getRequestTypes, getRequestTypeIdByPageUrl, getListPartitions, resolveRoute } from './approvalRoute.service'; +export { + getRequestTypes, + getRequestTypeIdByPageUrl, + getListPartitions, + getCurrentListPartition, + resolveRoute +} from './approvalRoute.service'; export { initListPartitions, listPartitionsLoaded, diff --git a/src/webparts/requestPortal/shared/approval/models/IApprovalRouteApi.ts b/src/webparts/requestPortal/shared/approval/models/IApprovalRouteApi.ts index 26b997d..1085577 100644 --- a/src/webparts/requestPortal/shared/approval/models/IApprovalRouteApi.ts +++ b/src/webparts/requestPortal/shared/approval/models/IApprovalRouteApi.ts @@ -50,6 +50,23 @@ export interface IRequestTypeListSeries { partitions: IListPartition[]; } +/** + * GET /api/request-types/{id}/lists/current → { data: ICurrentListPartition } + * + * Партиция, в которую нужно писать НОВУЮ заявку. Не чистый GET на бэкенде — + * этот же запрос проверяет порог элементов и при необходимости ставит в + * очередь создание следующей партиции, поэтому его нужно звать перед КАЖДЫМ + * созданием заявки, а не читать из кеша (см. shared/approval/listPartitions.ts, + * который для этого не подходит — грузится один раз при onInit). + */ +export interface ICurrentListPartition { + requestTypeId: number; + number: number; + siteUrl: string; + listId: string; + title?: string; +} + // Условие видимости шага маршрута (настраивается в админке маршрутов — // «Показывать шаг только при выполнении условия», сравнение «Поле списка»/ // «Оператор»/«Значение»). fieldName — внутреннее имя колонки списка (напр.