diff --git a/src/webparts/requestPortal/forms/PaymentRequest/PaymentRequestForm.tsx b/src/webparts/requestPortal/forms/PaymentRequest/PaymentRequestForm.tsx index 81b5229..65b0acb 100644 --- a/src/webparts/requestPortal/forms/PaymentRequest/PaymentRequestForm.tsx +++ b/src/webparts/requestPortal/forms/PaymentRequest/PaymentRequestForm.tsx @@ -142,7 +142,7 @@ const PaymentRequestForm: React.FC = ({ // ещё правят (черновик, возврат), перечитываем их: по основанию могли // подать новые, а лимит суммы должен считаться по актуальным. if (isEditableStatus(loaded.status) && loaded.data.BasisKey) { - loadPreviousPayments(loaded.data.BasisKey, editId) + loadPreviousPayments(context, loaded.data.BasisKey, editId) .then((previous) => { if (alive) patch({ PreviousPayments: previous }); }) diff --git a/src/webparts/requestPortal/forms/PaymentRequest/model.ts b/src/webparts/requestPortal/forms/PaymentRequest/model.ts index d0f0ca6..7755fa5 100644 --- a/src/webparts/requestPortal/forms/PaymentRequest/model.ts +++ b/src/webparts/requestPortal/forms/PaymentRequest/model.ts @@ -20,7 +20,7 @@ export interface IPreviousPayment { /** Уникальный код заявки на оплату. */ code: string; amount: number; - /** Id элемента списка данных — для ссылки на заявку. Нет у снимков, сохранённых до появления ссылок. */ + /** Id элемента списка данных — для ссылки на заявку. Нет, если у пользователя нет прав её открыть (или в старом снимке). */ id?: number; /** Список партиции (серверный относительный URL), где лежит заявка; '' — текущая. */ listId?: string; diff --git a/src/webparts/requestPortal/forms/PaymentRequest/services/previousPayments.service.ts b/src/webparts/requestPortal/forms/PaymentRequest/services/previousPayments.service.ts index 21d9340..8ecdcb6 100644 --- a/src/webparts/requestPortal/forms/PaymentRequest/services/previousPayments.service.ts +++ b/src/webparts/requestPortal/forms/PaymentRequest/services/previousPayments.service.ts @@ -1,6 +1,5 @@ -import { getSP } from '../../../shared/pnp/pnpjs'; -import { dataListTitle } from '../../../shared/listTitles'; -import { REQUEST_STATUS, currentPartitionForPageUrl } from '../../../shared/approval'; +import type { WebPartContext } from '@microsoft/sp-webpart-base'; +import { REQUEST_STATUS, getBasisRequests, getRequestTypeIdByPageUrl } from '../../../shared/approval'; import { FORM_ID, IPreviousPayment } from '../model'; /** @@ -9,39 +8,34 @@ import { FORM_ID, IPreviousPayment } from '../model'; */ const NOT_COUNTED_STATUSES: string[] = [REQUEST_STATUS.draft, REQUEST_STATUS.rejected, REQUEST_STATUS.returned]; -const MAX_PAYMENTS = 500; - -interface IPaymentRow { - Id: number; - RequestUniqueCode?: string; - PayAmount?: number; - RequestStatus?: string; -} - /** - * Прежние заявки на оплату по тому же основанию (СТ или потребности). Читаем - * текущую партицию списка: архивные пары списков сюда не заглядывают — оплаты - * по одной СТ укладываются во время жизни одной партиции. + * Прежние заявки на оплату по тому же основанию (СТ или потребности) — по всем + * партициям списка. + * + * Читаем не список напрямую, а через approval-route (GET /api/basis-requests): + * права на элементы заявок разорваны поштучно, и под своей учёткой заявитель + * видел бы только свои оплаты — сумма по СТ занижалась бы и проверка превышения + * пропускала переплату. Сервер отдаёт все заявки, а canOpen говорит, можно ли + * текущему пользователю открыть каждую (без прав ссылки в таблице нет). * * excludeId — сама редактируемая заявка: себя в «ранее поданных» показывать нельзя. */ -export async function loadPreviousPayments(basisKey: string, excludeId?: number): Promise { +export async function loadPreviousPayments( + context: WebPartContext, + basisKey: string, + excludeId?: number +): Promise { if (!basisKey) return []; - const safeKey = basisKey.replace(/'/g, "''"); - const rows = (await getSP() - .web.lists.getByTitle(dataListTitle(FORM_ID)) - .items.select('Id', 'RequestUniqueCode', 'PayAmount', 'RequestStatus') - .filter(`BasisKey eq '${safeKey}'`) - .top(MAX_PAYMENTS)()) as IPaymentRow[]; + const requestTypeId = await getRequestTypeIdByPageUrl(context, FORM_ID); + const rows = await getBasisRequests(context, requestTypeId, basisKey); - const listId = currentPartitionForPageUrl(FORM_ID)?.listId || ''; return rows - .filter((row) => row.Id !== excludeId && NOT_COUNTED_STATUSES.indexOf(row.RequestStatus || '') === -1) + .filter((row) => row.id !== excludeId && NOT_COUNTED_STATUSES.indexOf(row.status) === -1) .map((row) => ({ - code: row.RequestUniqueCode || `№${row.Id}`, - amount: Number(row.PayAmount || 0), - id: row.Id, - listId, - status: row.RequestStatus || '' + code: row.uniqueCode || `№${row.id}`, + amount: Number(row.amount || 0), + id: row.canOpen ? row.id : undefined, + listId: row.listId, + status: row.status })); } diff --git a/src/webparts/requestPortal/forms/PaymentRequest/useBasisSelection.ts b/src/webparts/requestPortal/forms/PaymentRequest/useBasisSelection.ts index edcb492..09bc77b 100644 --- a/src/webparts/requestPortal/forms/PaymentRequest/useBasisSelection.ts +++ b/src/webparts/requestPortal/forms/PaymentRequest/useBasisSelection.ts @@ -35,7 +35,7 @@ export function useBasisSelection( try { const info = kind === 'ct' ? await loadCtBasis(context, id) : await loadNeedBasis(context, id); const [previous, projects, companies] = await Promise.all([ - loadPreviousPayments(info.key, editId), + loadPreviousPayments(context, info.key, editId), getProjects(context).catch(() => []), getCompanies(context).catch(() => []) ]); diff --git a/src/webparts/requestPortal/shared/approval/approvalRoute.service.ts b/src/webparts/requestPortal/shared/approval/approvalRoute.service.ts index 822dccc..de89496 100644 --- a/src/webparts/requestPortal/shared/approval/approvalRoute.service.ts +++ b/src/webparts/requestPortal/shared/approval/approvalRoute.service.ts @@ -5,6 +5,7 @@ import { ApprovalStatus, ICurrentListPartition, IApiResponse, + IBasisRequest, IRequestType, IRequestTypeListSeries, IResolvedRoute, @@ -147,6 +148,32 @@ export async function getRequestTypeIdByPageUrl(context: WebPartContext, pageUrl return found.id; } +/** + * GET /api/basis-requests — заявки типа requestTypeId, поданные по основанию + * basisKey («ct:», «need:»), по всем партициям. Читаются на сервере от + * имени приложения: права на элементы заявок разорваны поштучно, и прямым + * чтением списка пользователь увидел бы только свои. + */ +export async function getBasisRequests( + context: WebPartContext, + requestTypeId: number, + basisKey: string +): Promise { + return withNetworkRetry(async () => { + const c = await client(context); + const query = `requestTypeId=${requestTypeId}&basisKey=${encodeURIComponent(basisKey)}`; + const response: HttpClientResponse = await c.get( + `${getApprovalApiBaseUrl()}/api/basis-requests?${query}`, + AadHttpClient.configurations.v1 + ); + if (!response.ok) { + throw new Error(`basis-requests: ${response.status} ${response.statusText}`); + } + const body: IApiResponse = await response.json(); + return body.data ?? []; + }); +} + // GET /api/request-type-access/{requestTypeId}/emails — email'ы, кому в // админке approval-route выдан доступ видеть ВСЕ заявки этого типа (не только // свои). Тот же эндпоинт дёргает и Power Automate — здесь просто ещё один diff --git a/src/webparts/requestPortal/shared/approval/index.ts b/src/webparts/requestPortal/shared/approval/index.ts index 57e6731..6ab847c 100644 --- a/src/webparts/requestPortal/shared/approval/index.ts +++ b/src/webparts/requestPortal/shared/approval/index.ts @@ -19,6 +19,7 @@ export type { IListPartition, ICurrentListPartition, IRequestType, + IBasisRequest, IRequestTypeListSeries, IResolvedRoute, IResolvedRouteStep, @@ -32,6 +33,7 @@ export { getListPartitions, getCurrentListPartition, getRequestTypeAccessEmails, + getBasisRequests, resolveRoute } from './approvalRoute.service'; export { hasRequestTypeAccess, accessibleAllRequestsTypes, emailsForType } from './requestTypeAccess'; diff --git a/src/webparts/requestPortal/shared/approval/models/IApprovalRouteApi.ts b/src/webparts/requestPortal/shared/approval/models/IApprovalRouteApi.ts index abe51ec..e5bca18 100644 --- a/src/webparts/requestPortal/shared/approval/models/IApprovalRouteApi.ts +++ b/src/webparts/requestPortal/shared/approval/models/IApprovalRouteApi.ts @@ -126,3 +126,19 @@ export interface IResolvedRouteStep { */ positionCode?: string; } + +/** + * Заявка, поданная по тому же основанию (СТ, потребность) — GET /api/basis-requests. + * Сервер читает их от имени приложения, поэтому видны все, а не только свои; + * canOpen — есть ли у текущего пользователя права открыть саму заявку. + */ +export interface IBasisRequest { + id: number; + /** Серверный относительный URL списка партиции, где лежит заявка. */ + listId: string; + /** Номер заявки; пока не присвоен — пусто (сервер шлёт null). */ + uniqueCode?: string; + status: string; + amount: number; + canOpen: boolean; +}