feat: прежние оплаты по основанию берутся из approval-route, ссылка — только при правах
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -142,7 +142,7 @@ const PaymentRequestForm: React.FC<IRequestFormProps> = ({
|
||||
// ещё правят (черновик, возврат), перечитываем их: по основанию могли
|
||||
// подать новые, а лимит суммы должен считаться по актуальным.
|
||||
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 });
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface IPreviousPayment {
|
||||
/** Уникальный код заявки на оплату. */
|
||||
code: string;
|
||||
amount: number;
|
||||
/** Id элемента списка данных — для ссылки на заявку. Нет у снимков, сохранённых до появления ссылок. */
|
||||
/** Id элемента списка данных — для ссылки на заявку. Нет, если у пользователя нет прав её открыть (или в старом снимке). */
|
||||
id?: number;
|
||||
/** Список партиции (серверный относительный URL), где лежит заявка; '' — текущая. */
|
||||
listId?: string;
|
||||
|
||||
+23
-29
@@ -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<IPreviousPayment[]> {
|
||||
export async function loadPreviousPayments(
|
||||
context: WebPartContext,
|
||||
basisKey: string,
|
||||
excludeId?: number
|
||||
): Promise<IPreviousPayment[]> {
|
||||
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
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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(() => [])
|
||||
]);
|
||||
|
||||
@@ -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:<Id>», «need:<oid>»), по всем партициям. Читаются на сервере от
|
||||
* имени приложения: права на элементы заявок разорваны поштучно, и прямым
|
||||
* чтением списка пользователь увидел бы только свои.
|
||||
*/
|
||||
export async function getBasisRequests(
|
||||
context: WebPartContext,
|
||||
requestTypeId: number,
|
||||
basisKey: string
|
||||
): Promise<IBasisRequest[]> {
|
||||
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<IBasisRequest[]> = await response.json();
|
||||
return body.data ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
// GET /api/request-type-access/{requestTypeId}/emails — email'ы, кому в
|
||||
// админке approval-route выдан доступ видеть ВСЕ заявки этого типа (не только
|
||||
// свои). Тот же эндпоинт дёргает и Power Automate — здесь просто ещё один
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user