6acb22dd95
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>
162 lines
7.1 KiB
TypeScript
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;
|