раздел «Отчёты» в боковом меню — скачивание отчёта по вакансиям (#17)
Tests / frontend-test (push) Successful in 1m15s

Пункт меню виден только заданному списку почт (Тришина, Городецкий,
Горбенко) — группы HR в SharePoint пока нет, добавили TODO завести
проверку по ней отдельно (reportsAccess.ts). Скрыт из меню совсем, а
не показан неактивным, и прямой переход по ?rpnav=reports у остальных
редиректит на «Новую заявку».

Раздел — форма периода (С/По) и кнопка «Скачать xlsx», дергает
GET api/reports/vacancies через тот же AadHttpClient, что и остальные
вызовы approval-route API. Имя файла собирается на фронте по формату
бэкенда (VacancyReportWorkbook.FileName), а не читается из
Content-Disposition — approval-route не объявляет заголовок в
Access-Control-Expose-Headers, браузер бы его не отдал.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/17
This commit was merged in pull request #17.
This commit is contained in:
2026-08-31 11:00:18 +00:00
parent 6172321710
commit ddce6b4f25
18 changed files with 341 additions and 47 deletions
@@ -36,6 +36,8 @@ import { SearchView } from '../features/myRequests/SearchView';
import { 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';
// Оболочка портала: тёмное боковое меню (разделы) + основная область.
// Логики фич тут нет — файл общий, меняется редко.
@@ -70,6 +72,22 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
const [mobileNavOpen, setMobileNavOpen] = React.useState<boolean>(false);
const [navCounts, setNavCounts] = React.useState<INavCounts | undefined>(undefined);
// Раздел «Отчёты» — точечный список почт, пока в SharePoint нет группы HR
// (см. reportsAccess.ts). Пересчитывается на каждый рендер намеренно: сам
// hasReportsAccess — синхронная проверка без сети, а input (effectiveUserEmail)
// меняется только по подмене личности в property pane, что и так вызывает
// ререндер всей веб-части.
const canSeeReports = hasReportsAccess(context);
// Прямой переход по ссылке (?rpnav=reports) в обход бокового меню — тот же
// раздел, что скрыт из навигации, должен быть недоступен и так: без этого
// редиректа он открылся бы у любого, кто угадает или сохранит такую ссылку.
React.useEffect(() => {
if (route.nav === 'reports' && !canSeeReports) {
navigate({ mode: 'submit', nav: 'new' }, { replace: true });
}
}, [route.nav, canSeeReports]);
// Уникальный код заявки для заголовка Drawer (approve/edit) — сама форма
// узнаёт его только после загрузки (см. onCodeLoaded в ApproveFeature/
// EditFeature), поэтому не берём route.itemId (это RowID в SharePoint, а не
@@ -278,6 +296,8 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
};
// Разделы бокового меню для пакетного Sidebar (счётчики — из navCounts).
// «Отчёты» — только у тех, кому разрешено (см. canSeeReports выше): скрыт из
// меню совсем, а не показан неактивным, — раздела для остальных как бы не существует.
const navItems: ISidebarItem[] = [
{ id: 'new', title: 'Новая заявка', icon: 'plus' },
{ id: 'approvals', title: 'На согласовании', icon: 'user-check', count: navCounts?.approvals },
@@ -285,6 +305,7 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
{ id: 'rework', title: 'Доработка', icon: 'edit', count: navCounts?.rework },
{ id: 'own', title: 'Мои заявки', icon: 'clipboard' },
{ id: 'search', title: 'Поиск', icon: 'search' },
...(canSeeReports ? [{ id: 'reports', title: 'Отчёты', icon: 'chart' } as ISidebarItem] : []),
];
return (
@@ -375,6 +396,8 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
<OwnTabs activeId={route.ownTab ?? 'created'} onChange={gotoOwnTab} />
)}
{route.nav === 'reports' && canSeeReports && <ReportsFeature context={context} />}
{LIST_VIEWS[route.nav] !== undefined && (
<RequestsTableView
context={context}