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) {
|
if (isEditableStatus(loaded.status) && loaded.data.BasisKey) {
|
||||||
loadPreviousPayments(loaded.data.BasisKey, editId)
|
loadPreviousPayments(context, loaded.data.BasisKey, editId)
|
||||||
.then((previous) => {
|
.then((previous) => {
|
||||||
if (alive) patch({ PreviousPayments: previous });
|
if (alive) patch({ PreviousPayments: previous });
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface IPreviousPayment {
|
|||||||
/** Уникальный код заявки на оплату. */
|
/** Уникальный код заявки на оплату. */
|
||||||
code: string;
|
code: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
/** Id элемента списка данных — для ссылки на заявку. Нет у снимков, сохранённых до появления ссылок. */
|
/** Id элемента списка данных — для ссылки на заявку. Нет, если у пользователя нет прав её открыть (или в старом снимке). */
|
||||||
id?: number;
|
id?: number;
|
||||||
/** Список партиции (серверный относительный URL), где лежит заявка; '' — текущая. */
|
/** Список партиции (серверный относительный URL), где лежит заявка; '' — текущая. */
|
||||||
listId?: string;
|
listId?: string;
|
||||||
|
|||||||
+23
-29
@@ -1,6 +1,5 @@
|
|||||||
import { getSP } from '../../../shared/pnp/pnpjs';
|
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { dataListTitle } from '../../../shared/listTitles';
|
import { REQUEST_STATUS, getBasisRequests, getRequestTypeIdByPageUrl } from '../../../shared/approval';
|
||||||
import { REQUEST_STATUS, currentPartitionForPageUrl } from '../../../shared/approval';
|
|
||||||
import { FORM_ID, IPreviousPayment } from '../model';
|
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 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 — сама редактируемая заявка: себя в «ранее поданных» показывать нельзя.
|
* 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 [];
|
if (!basisKey) return [];
|
||||||
const safeKey = basisKey.replace(/'/g, "''");
|
const requestTypeId = await getRequestTypeIdByPageUrl(context, FORM_ID);
|
||||||
const rows = (await getSP()
|
const rows = await getBasisRequests(context, requestTypeId, basisKey);
|
||||||
.web.lists.getByTitle(dataListTitle(FORM_ID))
|
|
||||||
.items.select('Id', 'RequestUniqueCode', 'PayAmount', 'RequestStatus')
|
|
||||||
.filter(`BasisKey eq '${safeKey}'`)
|
|
||||||
.top(MAX_PAYMENTS)()) as IPaymentRow[];
|
|
||||||
|
|
||||||
const listId = currentPartitionForPageUrl(FORM_ID)?.listId || '';
|
|
||||||
return rows
|
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) => ({
|
.map((row) => ({
|
||||||
code: row.RequestUniqueCode || `№${row.Id}`,
|
code: row.uniqueCode || `№${row.id}`,
|
||||||
amount: Number(row.PayAmount || 0),
|
amount: Number(row.amount || 0),
|
||||||
id: row.Id,
|
id: row.canOpen ? row.id : undefined,
|
||||||
listId,
|
listId: row.listId,
|
||||||
status: row.RequestStatus || ''
|
status: row.status
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function useBasisSelection(
|
|||||||
try {
|
try {
|
||||||
const info = kind === 'ct' ? await loadCtBasis(context, id) : await loadNeedBasis(context, id);
|
const info = kind === 'ct' ? await loadCtBasis(context, id) : await loadNeedBasis(context, id);
|
||||||
const [previous, projects, companies] = await Promise.all([
|
const [previous, projects, companies] = await Promise.all([
|
||||||
loadPreviousPayments(info.key, editId),
|
loadPreviousPayments(context, info.key, editId),
|
||||||
getProjects(context).catch(() => []),
|
getProjects(context).catch(() => []),
|
||||||
getCompanies(context).catch(() => [])
|
getCompanies(context).catch(() => [])
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ApprovalStatus,
|
ApprovalStatus,
|
||||||
ICurrentListPartition,
|
ICurrentListPartition,
|
||||||
IApiResponse,
|
IApiResponse,
|
||||||
|
IBasisRequest,
|
||||||
IRequestType,
|
IRequestType,
|
||||||
IRequestTypeListSeries,
|
IRequestTypeListSeries,
|
||||||
IResolvedRoute,
|
IResolvedRoute,
|
||||||
@@ -147,6 +148,32 @@ export async function getRequestTypeIdByPageUrl(context: WebPartContext, pageUrl
|
|||||||
return found.id;
|
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'ы, кому в
|
// GET /api/request-type-access/{requestTypeId}/emails — email'ы, кому в
|
||||||
// админке approval-route выдан доступ видеть ВСЕ заявки этого типа (не только
|
// админке approval-route выдан доступ видеть ВСЕ заявки этого типа (не только
|
||||||
// свои). Тот же эндпоинт дёргает и Power Automate — здесь просто ещё один
|
// свои). Тот же эндпоинт дёргает и Power Automate — здесь просто ещё один
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type {
|
|||||||
IListPartition,
|
IListPartition,
|
||||||
ICurrentListPartition,
|
ICurrentListPartition,
|
||||||
IRequestType,
|
IRequestType,
|
||||||
|
IBasisRequest,
|
||||||
IRequestTypeListSeries,
|
IRequestTypeListSeries,
|
||||||
IResolvedRoute,
|
IResolvedRoute,
|
||||||
IResolvedRouteStep,
|
IResolvedRouteStep,
|
||||||
@@ -32,6 +33,7 @@ export {
|
|||||||
getListPartitions,
|
getListPartitions,
|
||||||
getCurrentListPartition,
|
getCurrentListPartition,
|
||||||
getRequestTypeAccessEmails,
|
getRequestTypeAccessEmails,
|
||||||
|
getBasisRequests,
|
||||||
resolveRoute
|
resolveRoute
|
||||||
} from './approvalRoute.service';
|
} from './approvalRoute.service';
|
||||||
export { hasRequestTypeAccess, accessibleAllRequestsTypes, emailsForType } from './requestTypeAccess';
|
export { hasRequestTypeAccess, accessibleAllRequestsTypes, emailsForType } from './requestTypeAccess';
|
||||||
|
|||||||
@@ -126,3 +126,19 @@ export interface IResolvedRouteStep {
|
|||||||
*/
|
*/
|
||||||
positionCode?: string;
|
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