diff --git a/src/webparts/requestPortal/components/RequestPortal.tsx b/src/webparts/requestPortal/components/RequestPortal.tsx index 54995fc..6cef99a 100644 --- a/src/webparts/requestPortal/components/RequestPortal.tsx +++ b/src/webparts/requestPortal/components/RequestPortal.tsx @@ -34,11 +34,13 @@ import EditFeature from '../features/edit/EditFeature'; import { ListView, RequestsTableView } from '../features/myRequests/RequestsTableView'; import { OwnTabs } from '../features/myRequests/OwnTabs'; import { SearchView } from '../features/myRequests/SearchView'; -import { getNavCounts, INavCounts } from '../features/myRequests/myRequests.service'; +import { getLinkedTypes, getNavCounts, INavCounts } from '../features/myRequests/myRequests.service'; import { IMyRequest } from '../features/myRequests/models/IMyRequest'; import { isEditableStatus } from '../shared/approval'; import { hasReportsAccess } from '../shared/reports/reportsAccess'; import { ReportsFeature } from '../features/reports/ReportsFeature'; +import { AllRequestsFeature } from '../features/myRequests/AllRequestsFeature'; +import { accessibleAllRequestsTypes } from '../shared/approval'; // Оболочка портала: тёмное боковое меню (разделы) + основная область. // Логики фич тут нет — файл общий, меняется редко. @@ -102,6 +104,33 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context } }, [route.nav, canSeeReports]); + // Раздел «Все заявки» — виден только тем, кому в админке approval-route + // выдан RequestTypeAccess хотя бы на один тип (см. shared/approval/ + // requestTypeAccess.ts). undefined — ответ ещё не пришёл. + const [canSeeAllRequests, setCanSeeAllRequests] = React.useState(undefined); + React.useEffect(() => { + let cancelled = false; + setCanSeeAllRequests(undefined); + getLinkedTypes(context) + .then((types) => accessibleAllRequestsTypes(context, types)) + .then((accessible) => { + if (!cancelled) setCanSeeAllRequests(accessible.length > 0); + }) + .catch(() => { + if (!cancelled) setCanSeeAllRequests(false); + }); + return () => { + cancelled = true; + }; + }, [context, impersonatedEmail]); + + // Та же защита от прямой ссылки, что и у «Отчётов» выше. + React.useEffect(() => { + if (route.nav === 'allRequests' && canSeeAllRequests === false) { + navigate({ mode: 'submit', nav: 'new' }, { replace: true }); + } + }, [route.nav, canSeeAllRequests]); + // Уникальный код заявки для заголовка Drawer (approve/edit) — сама форма // узнаёт его только после загрузки (см. onCodeLoaded в ApproveFeature/ // EditFeature), поэтому не берём route.itemId (это RowID в SharePoint, а не @@ -224,6 +253,24 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context }); }; + // Переход в «Все заявки» — либо с бокового меню (typeId не задан, раздел сам + // выберет тип), либо с кнопки формы (typeId уже известен, см. + // gotoAllRequestsForType и SubmitFeature.onOpenAllRequests). + const gotoAllRequestsType = (typeId: number): void => { + navigate({ + mode: 'submit', + nav: 'allRequests', + allTypeId: typeId, + page: storedPageFor('allRequests'), + query: storedQueryFor('allRequests') || undefined, + }); + }; + + const gotoAllRequestsForType = (typeId: number): void => { + persistNav('allRequests'); + gotoAllRequestsType(typeId); + }; + const setOwnStatus = (ownStatus: string): void => { const normalized = ownStatus || undefined; persistOwnStatus(normalized); @@ -281,6 +328,7 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context listId: request.listId, ownTab: route.ownTab, ownStatus: route.ownStatus, + allTypeId: route.allTypeId, // Страница и запрос — часть того же списка под панелью: без них ссылка на // открытую заявку вела бы на первую страницу без поиска. page: route.page, @@ -295,6 +343,7 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context nav: route.nav, ownTab: route.ownTab, ownStatus: route.ownStatus, + allTypeId: route.allTypeId, page: route.page, query: route.query, }); @@ -324,6 +373,7 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context { id: 'rework', title: 'Доработка', icon: 'edit', count: navCounts?.rework }, { id: 'own', title: 'Мои заявки', icon: 'clipboard' }, { id: 'search', title: 'Поиск', icon: 'search' }, + ...(canSeeAllRequests ? [{ id: 'allRequests', title: 'Все заявки', icon: 'people' } as ISidebarItem] : []), ...(canSeeReports ? [{ id: 'reports', title: 'Отчёты', icon: 'chart' } as ISidebarItem] : []), { id: 'instructions', title: 'Инструкция пользователя', icon: 'info' }, ]; @@ -413,6 +463,7 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context : undefined } onFormSelected={(formKey) => navigate({ mode: 'create', nav: 'new', formKey })} + onOpenAllRequests={gotoAllRequestsForType} /> @@ -433,6 +484,19 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context {route.nav === 'reports' && canSeeReports && } + {route.nav === 'allRequests' && canSeeAllRequests && ( + + )} + {LIST_VIEWS[route.nav] !== undefined && ( void; + page: number; + onPageChange: (page: number) => void; + query: string; + onQueryChange: (query: string) => void; + onOpen: (request: IMyRequest) => void; +} + +/** + * Раздел «Все заявки»: доступен только тем, у кого в админке approval-route + * настроен RequestTypeAccess хотя бы на один тип заявки (см. + * shared/approval/requestTypeAccess.ts). Показывает заявки ВСЕХ авторов + * выбранного типа — в отличие от «Моих заявок», где отбор всегда идёт по + * текущему пользователю. + */ +export const AllRequestsFeature: React.FC = ({ + context, + selectedTypeId, + onTypeChange, + page, + onPageChange, + query, + onQueryChange, + onOpen, +}) => { + const [types, setTypes] = React.useState(undefined); + const [error, setError] = React.useState(''); + + React.useEffect(() => { + let cancelled = false; + getLinkedTypes(context) + .then((all) => accessibleAllRequestsTypes(context, all)) + .then((accessible) => { + if (!cancelled) setTypes(accessible); + }) + .catch((e) => { + if (!cancelled) setError((e as Error).message); + }); + return () => { + cancelled = true; + }; + }, [context]); + + // Тип по умолчанию — из адреса, если он всё ещё доступен, иначе первый + // доступный. Отдельным эффектом (а не при рендере): смена выбора идёт через + // navigate() у вызывающей стороны, и до его отработки selectedTypeId ещё + // хранит предыдущее значение. + React.useEffect(() => { + if (!types || types.length === 0) return; + const stillValid = !!selectedTypeId && types.some((t) => t.id === selectedTypeId); + if (!stillValid) onTypeChange(types[0].id); + // eslint-disable-next-line react-hooks/exhaustive-deps -- onTypeChange стабилен по смыслу (см. gotoAllRequestsType в RequestPortal), включение его в deps дало бы лишний повторный запуск на каждый рендер + }, [types, selectedTypeId]); + + if (error) { + return Не удалось загрузить типы заявок: {error}; + } + if (types === undefined) { + return
Загрузка…
; + } + if (types.length === 0) { + return
Вам не выдан доступ ни к одному типу заявок.
; + } + + const selectedType = types.find((t) => t.id === selectedTypeId) ?? types[0]; + + return ( +
+ {types.length > 1 && ( +
+