раздел «Отчёты» в боковом меню — скачивание отчёта по вакансиям (#17)
Tests / frontend-test (push) Successful in 1m15s
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:
@@ -0,0 +1,69 @@
|
||||
import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { APPROVAL_API_CLIENT_ID, getApprovalApiBaseUrl } from '../config';
|
||||
|
||||
// Обращения к api/reports approval-route API — по тому же принципу, что и
|
||||
// approvalRoute.service.ts (AadHttpClient, SSO текущего пользователя), но
|
||||
// ответ — не JSON, а сам файл (см. ReportsController.Vacancies).
|
||||
|
||||
/** Отчёт по вакансиям за период: даты YYYY-MM-DD (как в TextInput type="date"). */
|
||||
export async function downloadVacancyReport(
|
||||
context: WebPartContext,
|
||||
from: string,
|
||||
to: string
|
||||
): Promise<void> {
|
||||
const client = await context.aadHttpClientFactory.getClient(APPROVAL_API_CLIENT_ID);
|
||||
const url = `${getApprovalApiBaseUrl()}/api/reports/vacancies?from=${from}&to=${to}`;
|
||||
const response: HttpClientResponse = await client.get(url, AadHttpClient.configurations.v1);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await extractErrorMessage(response));
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
// Имя файла собираем сами, а не читаем Content-Disposition: approval-route
|
||||
// не объявляет его в Access-Control-Expose-Headers (см. Program.cs, CORS),
|
||||
// и без этого браузер отдал бы fetch-у пустой заголовок. Формат — тот же,
|
||||
// что и в VacancyReportWorkbook.FileName на бэкенде.
|
||||
saveBlob(blob, vacancyReportFileName(from, to));
|
||||
}
|
||||
|
||||
/** «Отчет по вакансиям dd.MM.yyyy-dd.MM.yyyy.xlsx» — как в VacancyReportWorkbook.FileName. */
|
||||
function vacancyReportFileName(from: string, to: string): string {
|
||||
return `Отчет по вакансиям ${formatRu(from)}-${formatRu(to)}.xlsx`;
|
||||
}
|
||||
|
||||
function formatRu(isoDate: string): string {
|
||||
const [year, month, day] = isoDate.split('-');
|
||||
return `${day}.${month}.${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 400 от ReportsController приходит как { errors: string[] } (см.
|
||||
* VacancyReportService.GetAsync — например «конец периода раньше начала»);
|
||||
* остальные статусы — обычный текст/пусто.
|
||||
*/
|
||||
async function extractErrorMessage(response: HttpClientResponse): Promise<string> {
|
||||
const text = await response.text().catch(() => '');
|
||||
try {
|
||||
const parsed: { errors?: string[] } = JSON.parse(text);
|
||||
if (parsed.errors && parsed.errors.length > 0) return parsed.errors.join('; ');
|
||||
} catch {
|
||||
// Не JSON — используем как есть.
|
||||
}
|
||||
return text || `${response.status} ${response.statusText}`;
|
||||
}
|
||||
|
||||
/** Отдать Blob пользователю как скачивание файла — через временную ссылку. */
|
||||
function saveBlob(blob: Blob, fileName: string): void {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
// Отложенный revoke — на некоторых браузерах немедленный обрывает скачивание,
|
||||
// начатое кликом синхронно перед этим.
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { effectiveUserEmail } from '../impersonation';
|
||||
|
||||
/**
|
||||
* Кто видит раздел «Отчёты» в боковом меню. Пока это фиксированный список
|
||||
* почт — группы HR в SharePoint ещё нет (её заведут отдельно); появится —
|
||||
* список ниже заменится на реальную проверку членства (см. TODO).
|
||||
*
|
||||
* Email — из справочника сотрудников (api.tnsu.kz/Employee/All), а не из
|
||||
* Title сайта: у части учёток встречаются устаревшие site user записи со
|
||||
* старым email (пример — Тришина: m.trishina@tnsu.kz как устаревший алиас
|
||||
* рядом с текущим trishina.mariya@tnsu.kz).
|
||||
*/
|
||||
const ALLOWED_EMAILS: ReadonlySet<string> = new Set([
|
||||
'trishina.mariya@tnsu.kz', // Мария Тришина
|
||||
'gorodetskiy.timur@tnsu.kz', // Тимур Городецкий
|
||||
'gorbenko.pavel@tnsu.kz', // Павел Горбенко
|
||||
]);
|
||||
|
||||
// TODO: заменить точечный список на членство в SharePoint-группе HR, когда
|
||||
// её заведут — тогда список выше станет фолбэком для тех троих, кто должен
|
||||
// видеть раздел независимо от состава группы (администраторы портала).
|
||||
|
||||
/**
|
||||
* Есть ли у текущего пользователя доступ к разделу «Отчёты». Использует
|
||||
* effectiveUserEmail — то есть учитывает отладочную подмену личности (см.
|
||||
* impersonation.ts): под подменённым пользователем раздел виден/не виден
|
||||
* так же, как было бы у него самого.
|
||||
*/
|
||||
export function hasReportsAccess(context: WebPartContext): boolean {
|
||||
const email = effectiveUserEmail(context).trim().toLowerCase();
|
||||
return email.length > 0 && ALLOWED_EMAILS.has(email);
|
||||
}
|
||||
Reference in New Issue
Block a user