diff --git a/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessDto.cs b/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessDto.cs index 3cb7950..30c432c 100644 --- a/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessDto.cs +++ b/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessDto.cs @@ -1,5 +1,18 @@ +using Tnsu.ApprovalRoute.Application.ListPartitioning; + namespace Tnsu.ApprovalRoute.Application.RequestTypeAccesses; -public sealed record RequestTypeAccessDto(int Id, int RequestTypeId, string Email); +/// +/// Вся серия SharePoint-списков данных (архив + текущая партиция) типа заявки — +/// именно на них нужно выдать email'у право чтения, иначе поиск по порталу +/// продолжит отдавать только его собственные заявки: доступ здесь настраивает +/// видимость в UI approval-route, но не права SharePoint, которые остаются +/// отдельным ручным шагом. +/// +public sealed record RequestTypeAccessDto( + int Id, + int RequestTypeId, + string Email, + IReadOnlyList SharePointLists); public sealed record CreateRequestTypeAccessRequest(int RequestTypeId, string Email); diff --git a/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessService.cs b/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessService.cs index 6de3477..029ce54 100644 --- a/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessService.cs +++ b/backend/Tnsu.ApprovalRoute.Application/RequestTypeAccesses/RequestTypeAccessService.cs @@ -3,26 +3,53 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Tnsu.ApprovalRoute.Application.Common.Errors; using Tnsu.ApprovalRoute.Application.Common.Interfaces; +using Tnsu.ApprovalRoute.Application.ListPartitioning; using Tnsu.ApprovalRoute.Domain.Entities; using Tnsu.ApprovalRoute.Infrastructure.Persistence; namespace Tnsu.ApprovalRoute.Application.RequestTypeAccesses; public sealed class RequestTypeAccessService( - ApprovalRouteDbContext db, ICurrentUser currentUser, ILogger logger) + ApprovalRouteDbContext db, + ListPartitionService partitions, + ICurrentUser currentUser, + ILogger logger) { public async Task>> ListAsync( int? requestTypeId, CancellationToken cancellationToken) { - var items = await db.RequestTypeAccesses.AsNoTracking() + var entities = await db.RequestTypeAccesses.AsNoTracking() .Where(x => requestTypeId == null || x.RequestTypeId == requestTypeId) .OrderBy(x => x.RequestTypeId).ThenBy(x => x.Email) - .Select(x => new RequestTypeAccessDto(x.Id, x.RequestTypeId, x.Email)) .ToListAsync(cancellationToken); + // Партиции запрашиваются РАЗ на тип заявки, а не на строку — иначе список + // из N email по одному типу заново обходил бы SharePoint N раз подряд. + var listsByType = new Dictionary>(); + var items = new List(entities.Count); + foreach (var e in entities) + { + var lists = await GetListsCachedAsync(listsByType, e.RequestTypeId, cancellationToken); + items.Add(new RequestTypeAccessDto(e.Id, e.RequestTypeId, e.Email, lists)); + } + return Result.Ok>(items); } + private async Task> GetListsCachedAsync( + Dictionary> cache, + int requestTypeId, + CancellationToken cancellationToken) + { + if (cache.TryGetValue(requestTypeId, out var cached)) + return cached; + + var result = await partitions.ListAsync(requestTypeId, cancellationToken); + var lists = result.IsSuccess ? result.Value : Array.Empty(); + cache[requestTypeId] = lists; + return lists; + } + /// Список email'ов с доступом к типу заявок — дёргает Power Automate. public async Task>> ListEmailsAsync(int requestTypeId, CancellationToken cancellationToken) { @@ -58,7 +85,9 @@ public sealed class RequestTypeAccessService( "Доступ к типу заявки {RequestTypeId} выдан {Email} пользователем {Author}", entity.RequestTypeId, entity.Email, currentUser.Email); - return Result.Ok(new RequestTypeAccessDto(entity.Id, entity.RequestTypeId, entity.Email)); + var listsResult = await partitions.ListAsync(entity.RequestTypeId, cancellationToken); + var lists = listsResult.IsSuccess ? listsResult.Value : Array.Empty(); + return Result.Ok(new RequestTypeAccessDto(entity.Id, entity.RequestTypeId, entity.Email, lists)); } public async Task DeleteAsync(int id, CancellationToken cancellationToken) diff --git a/frontend/src/webparts/approvalAdmin/components/screens/RequestTypeAccessScreen.tsx b/frontend/src/webparts/approvalAdmin/components/screens/RequestTypeAccessScreen.tsx index 64f628d..ab74268 100644 --- a/frontend/src/webparts/approvalAdmin/components/screens/RequestTypeAccessScreen.tsx +++ b/frontend/src/webparts/approvalAdmin/components/screens/RequestTypeAccessScreen.tsx @@ -45,6 +45,14 @@ export const RequestTypeAccessScreen: React.FC = const requestTypeName = (id: number): string => requestTypes.items.find(rt => rt.id === id)?.name ?? `#${id}`; + // Список SharePoint нужно вручную открыть на чтение этому email — иначе доступ + // здесь настраивает только видимость в UI approval-route, но не в самом + // SharePoint (поиск по порталу отфильтрует чужие заявки правами списка). + const formatSharePointLists = (item: RequestTypeAccess): string => + item.sharePointLists.length === 0 + ? texts.common.empty + : item.sharePointLists.map(l => l.title ?? l.listId).join(', '); + const openCreate = (): void => { setRequestTypeId(''); setEmail(''); @@ -86,6 +94,7 @@ export const RequestTypeAccessScreen: React.FC = columns={[ { key: 'requestType', header: texts.requestTypeAccess.columnRequestType, render: r => {requestTypeName(r.requestTypeId)} }, { key: 'email', header: texts.requestTypeAccess.columnEmail, render: r => r.email }, + { key: 'sharePointLists', header: texts.requestTypeAccess.columnSharePointLists, render: formatSharePointLists }, { key: 'actions', header: '', diff --git a/frontend/src/webparts/approvalAdmin/models/requestTypeAccess.ts b/frontend/src/webparts/approvalAdmin/models/requestTypeAccess.ts index 8178b6e..dfacc02 100644 --- a/frontend/src/webparts/approvalAdmin/models/requestTypeAccess.ts +++ b/frontend/src/webparts/approvalAdmin/models/requestTypeAccess.ts @@ -1,9 +1,17 @@ // DTO эндпоинта api/request-type-access (Application/RequestTypeAccesses/*.cs). +import { RequestListPartition } from './requestTypeLists'; export interface RequestTypeAccess { id: number; requestTypeId: number; email: string; + /** + * Вся серия SharePoint-списков данных типа заявки (архив + текущая партиция) — + * именно на них нужно вручную выдать email'у право чтения, иначе поиск по + * порталу продолжит отдавать только его собственные заявки: этот реестр + * настраивает видимость в UI approval-route, а не права SharePoint. + */ + sharePointLists: RequestListPartition[]; } export interface CreateRequestTypeAccessInput { diff --git a/frontend/src/webparts/approvalAdmin/texts.ts b/frontend/src/webparts/approvalAdmin/texts.ts index b638be3..7479a73 100644 --- a/frontend/src/webparts/approvalAdmin/texts.ts +++ b/frontend/src/webparts/approvalAdmin/texts.ts @@ -187,6 +187,7 @@ export const texts = { emptyList: 'Пока никому не выдан доступ ни к одному типу заявок.', columnRequestType: 'Тип заявки', columnEmail: 'Сотрудник', + columnSharePointLists: 'Списки SharePoint (выдать доступ вручную)', createTitle: 'Выдать доступ', requestTypeLabel: 'Тип заявки', requestTypePlaceholder: 'Выберите тип заявки',