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>
This commit was merged in pull request #2.
This commit is contained in:
@@ -3,7 +3,7 @@ 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 } from '../shared/requestEvents';
|
||||
import { onRequestsChanged, emitRequestsChanged } from '../shared/requestEvents';
|
||||
import { SideNav } from './SideNav/SideNav';
|
||||
import { Drawer } from './Drawer/Drawer';
|
||||
import { CategoryTabs } from './CategoryTabs/CategoryTabs';
|
||||
@@ -12,6 +12,7 @@ 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';
|
||||
|
||||
// Оболочка портала: тёмное боковое меню (разделы) + основная область.
|
||||
@@ -39,14 +40,18 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
});
|
||||
|
||||
// Счётчик на пункте «На согласовании». Обновляется по тому же событию, что и
|
||||
// сами таблицы (создание заявки, решение согласующего).
|
||||
// сами таблицы (создание заявки, решение согласующего), и на монтировании
|
||||
// (в т.ч. на обновление страницы). Когда сама вкладка «На согласовании»
|
||||
// открыта, RequestsTableView сообщает точное число через onMyTurnCountChange
|
||||
// (ниже) — это перезаписывает значение отсюда, чтобы не было гонки между
|
||||
// двумя независимыми запросами одних и тех же данных.
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const load = (): void => {
|
||||
getPendingApproval(context)
|
||||
.then((r) => {
|
||||
if (!cancelled) setApprovalsCount(r.rows.length);
|
||||
if (!cancelled) setApprovalsCount(r.totalRows);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setApprovalsCount(undefined);
|
||||
@@ -70,7 +75,14 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
}
|
||||
}, [route.mode, route.formKey]);
|
||||
|
||||
const goto = (nav: NavSection): void => navigate({ mode: 'submit', nav });
|
||||
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 (
|
||||
@@ -84,7 +96,6 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
approvalsCount={approvalsCount}
|
||||
collapsed={navCollapsed}
|
||||
onToggleCollapsed={() => setNavCollapsed((v) => !v)}
|
||||
userName={context.pageContext.user.displayName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -108,6 +119,18 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : route.nav === 'search' ? (
|
||||
<SearchView
|
||||
context={context}
|
||||
onOpen={(request) =>
|
||||
navigate({
|
||||
mode: 'approve',
|
||||
nav: route.nav,
|
||||
formKey: request.formKey,
|
||||
itemId: request.id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<RequestsTableView
|
||||
context={context}
|
||||
@@ -120,6 +143,7 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
||||
itemId: request.id,
|
||||
})
|
||||
}
|
||||
onMyTurnCountChange={route.nav === 'approvals' ? setApprovalsCount : undefined}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
@@ -39,22 +39,6 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: $r-sm;
|
||||
background: $gold-500;
|
||||
color: $navy-950;
|
||||
font-family: $font-serif;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
letter-spacing: .5px;
|
||||
}
|
||||
|
||||
.brandText {
|
||||
font-family: $font-serif;
|
||||
font-size: 15px;
|
||||
@@ -168,41 +152,3 @@
|
||||
top: 6px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
// ---- Подвал рейла: пользователь ----
|
||||
.footer {
|
||||
margin: auto $sp-4 0; // margin-top:auto прижимает к низу; по бокам — инсет для border-top
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-2;
|
||||
padding-top: $sp-3;
|
||||
border-top: 1px solid rgba(255, 255, 255, .08);
|
||||
}
|
||||
|
||||
.navCollapsed .footer {
|
||||
justify-content: center;
|
||||
margin: auto 0 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, .1);
|
||||
color: $white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.userName {
|
||||
min-width: 0;
|
||||
color: rgba(255, 255, 255, .8);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ const ITEMS: INavItem[] = [
|
||||
{ id: 'new', title: 'Новая заявка', icon: 'plus' },
|
||||
{ id: 'approvals', title: 'На согласовании', icon: 'user-check' },
|
||||
{ id: 'own', title: 'Мои заявки', icon: 'clipboard' },
|
||||
{ id: 'search', title: 'Поиск', icon: 'search' },
|
||||
];
|
||||
|
||||
interface ISideNavProps {
|
||||
@@ -23,27 +24,16 @@ interface ISideNavProps {
|
||||
approvalsCount?: number;
|
||||
collapsed: boolean;
|
||||
onToggleCollapsed: () => void;
|
||||
/** Имя текущего пользователя (для подвала рейла). */
|
||||
userName?: string;
|
||||
}
|
||||
|
||||
// Инициалы для аватара (первые буквы двух первых слов имени).
|
||||
function initials(name?: string): string {
|
||||
if (!name) return '—';
|
||||
const parts = name.trim().split(/\s+/).filter(Boolean);
|
||||
const value = ((parts[0]?.[0] ?? '') + (parts[1]?.[0] ?? '')).toUpperCase();
|
||||
return value || '—';
|
||||
}
|
||||
|
||||
// Тёмный боковой рейл портала на всю высоту: бренд сверху, разделы, пользователь
|
||||
// снизу. Сворачивается до узкой полосы с одними иконками.
|
||||
// Тёмный боковой рейл портала на всю высоту: бренд сверху, разделы снизу.
|
||||
// Сворачивается до узкой полосы с одними иконками.
|
||||
export const SideNav: React.FC<ISideNavProps> = ({
|
||||
activeId,
|
||||
onChange,
|
||||
approvalsCount,
|
||||
collapsed,
|
||||
onToggleCollapsed,
|
||||
userName,
|
||||
}) => {
|
||||
const navClasses = [styles.nav, collapsed ? styles.navCollapsed : ''].filter(Boolean).join(' ');
|
||||
|
||||
@@ -51,7 +41,6 @@ export const SideNav: React.FC<ISideNavProps> = ({
|
||||
<nav className={navClasses} aria-label="Разделы портала">
|
||||
<div className={styles.head}>
|
||||
<div className={styles.brand}>
|
||||
<span className={styles.logo}>ТС</span>
|
||||
{!collapsed && <span className={styles.brandText}>Портал заявок</span>}
|
||||
</div>
|
||||
<button
|
||||
@@ -89,11 +78,6 @@ export const SideNav: React.FC<ISideNavProps> = ({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className={styles.footer} title={collapsed ? userName : undefined}>
|
||||
<span className={styles.avatar}>{initials(userName)}</span>
|
||||
{!collapsed && <span className={styles.userName}>{userName || 'Пользователь'}</span>}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user