Files
request-portal/src/webparts/requestPortal/components/RequestPortal.tsx
T
timur.gorodetskiy 6acb22dd95 Search requests (#2)
Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: http://git.tnsu.kz/timur.gorodetskiy/request-portal/pulls/2
Co-authored-by: timur.gorodetskiy <timur.gorodetskiy@tnsu.kz>
Co-committed-by: timur.gorodetskiy <timur.gorodetskiy@tnsu.kz>
2026-08-06 09:59:07 +00:00

162 lines
7.1 KiB
TypeScript

import * as React from 'react';
import styles from './RequestPortal.module.scss';
import type { IRequestPortalProps } from './IRequestPortalProps';
import { IconSprite } from '../shared/ui/Icon/IconSprite';
import { navigate, useRoute, NavSection } from '../shared/routing';
import { onRequestsChanged, emitRequestsChanged } from '../shared/requestEvents';
import { SideNav } from './SideNav/SideNav';
import { Drawer } from './Drawer/Drawer';
import { CategoryTabs } from './CategoryTabs/CategoryTabs';
import { CATEGORIES, DEFAULT_CATEGORY_ID } from '../data/categories';
import { REQUEST_TYPES } from '../data/requests';
import SubmitFeature from '../features/submit/SubmitFeature';
import ApproveFeature from '../features/approve/ApproveFeature';
import { RequestsTableView } from '../features/myRequests/RequestsTableView';
import { SearchView } from '../features/myRequests/SearchView';
import { getPendingApproval } from '../features/myRequests/myRequests.service';
// Оболочка портала: тёмное боковое меню (разделы) + основная область.
// Логики фич тут нет — файл общий, меняется редко.
//
// Разделы (rpnav): 'new' — подача заявки (вкладки категорий + плитки + форма),
// 'approvals'/'own' — таблицы заявок. Клик по строке таблицы открывает заявку
// в выезжающей панели ПОВЕРХ раздела (rpmode=approve&rptype=&rpid=), поэтому
// раздел под ней сохраняется; та же ссылка приходит из потока Power Automate.
const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context }) => {
const rootClasses = [styles.requestPortal, hasTeamsContext ? styles.teams : '']
.filter(Boolean)
.join(' ');
const route = useRoute();
const [navCollapsed, setNavCollapsed] = React.useState<boolean>(false);
const [approvalsCount, setApprovalsCount] = React.useState<number | undefined>(undefined);
const [activeCategoryId, setActiveCategoryId] = React.useState<string>(() => {
if (route.mode === 'create' && route.formKey) {
const found = REQUEST_TYPES.filter((r) => r.id === route.formKey)[0];
if (found) return found.categoryId;
}
return DEFAULT_CATEGORY_ID;
});
// Счётчик на пункте «На согласовании». Обновляется по тому же событию, что и
// сами таблицы (создание заявки, решение согласующего), и на монтировании
// (в т.ч. на обновление страницы). Когда сама вкладка «На согласовании»
// открыта, RequestsTableView сообщает точное число через onMyTurnCountChange
// (ниже) — это перезаписывает значение отсюда, чтобы не было гонки между
// двумя независимыми запросами одних и тех же данных.
React.useEffect(() => {
let cancelled = false;
const load = (): void => {
getPendingApproval(context)
.then((r) => {
if (!cancelled) setApprovalsCount(r.totalRows);
})
.catch(() => {
if (!cancelled) setApprovalsCount(undefined);
});
};
load();
const unsubscribe = onRequestsChanged(load);
return () => {
cancelled = true;
unsubscribe();
};
}, [context]);
// Переход «создать на основе» (в т.ч. из открытой заявки в панели): переключаем
// вкладку категорий на категорию целевой формы, чтобы она совпала с открытой формой.
React.useEffect(() => {
if (route.mode === 'create' && route.formKey) {
const found = REQUEST_TYPES.filter((r) => r.id === route.formKey)[0];
if (found) setActiveCategoryId(found.categoryId);
}
}, [route.mode, route.formKey]);
const goto = (nav: NavSection): void => {
// Клик по УЖЕ активному пункту меню не меняет маршрут, значит эффект
// загрузки в таблице не перезапустится сам. Шлём событие обновления —
// таблица на него подписана и перечитает данные фоном, не убирая с экрана
// текущие строки (см. RequestsTableView/SearchView).
if (nav === route.nav && route.mode === 'submit') emitRequestsChanged();
navigate({ mode: 'submit', nav });
};
const closeDrawer = (): void => navigate({ mode: 'submit', nav: route.nav });
return (
<section className={rootClasses}>
<IconSprite />
<div className={styles.layout}>
<div className={styles.side}>
<SideNav
activeId={route.nav}
onChange={goto}
approvalsCount={approvalsCount}
collapsed={navCollapsed}
onToggleCollapsed={() => setNavCollapsed((v) => !v)}
/>
</div>
<main className={styles.main}>
{route.nav === 'new' ? (
<>
<div className={styles.tabsWrap}>
<CategoryTabs
categories={CATEGORIES}
activeId={activeCategoryId}
onChange={setActiveCategoryId}
/>
</div>
<SubmitFeature
context={context}
activeCategoryId={activeCategoryId}
initialForm={
route.mode === 'create' && route.formKey && route.sourceId
? { formKey: route.formKey, sourceId: route.sourceId }
: undefined
}
/>
</>
) : route.nav === 'search' ? (
<SearchView
context={context}
onOpen={(request) =>
navigate({
mode: 'approve',
nav: route.nav,
formKey: request.formKey,
itemId: request.id,
})
}
/>
) : (
<RequestsTableView
context={context}
kind={route.nav === 'approvals' ? 'approvals' : 'own'}
onOpen={(request) =>
navigate({
mode: 'approve',
nav: route.nav,
formKey: request.formKey,
itemId: request.id,
})
}
onMyTurnCountChange={route.nav === 'approvals' ? setApprovalsCount : undefined}
/>
)}
</main>
</div>
{route.mode === 'approve' && route.formKey && route.itemId && (
<Drawer title={`Заявка №${route.itemId}`} onClose={closeDrawer}>
<ApproveFeature context={context} formKey={route.formKey} itemId={route.itemId} />
</Drawer>
)}
</section>
);
};
export default RequestPortal;