-
-
- {route.tab === 'dashboard' &&
}
- {route.tab === 'requestTypes' &&
}
- {route.tab === 'routes' && !route.routeId && (
-
- )}
- {route.tab === 'routes' && route.routeId && selectedRoute && (
-
navigate({ tab: 'routes', routesFilter: route.routesFilter })}
- />
- )}
- {route.tab === 'resolve' && (
-
- )}
- {route.tab === 'manageRequests' && (
-
- )}
+
+
+ navigateTab(id as ScreenKey)}
+ brand={texts.nav.title}
+ collapsed={navCollapsed}
+ onToggleCollapsed={() => setNavCollapsed(v => !v)}
+ mobileOpen={mobileNavOpen}
+ onMobileClose={() => setMobileNavOpen(false)}
+ />
+
+
+ setMobileNavOpen(true)} />
+
+
+ {route.tab === 'dashboard' && }
+ {route.tab === 'requestTypes' && }
+ {route.tab === 'routes' && !route.routeId && (
+
+ )}
+ {route.tab === 'routes' && route.routeId && selectedRoute && (
+ navigate({ tab: 'routes', routesFilter: route.routesFilter })}
+ />
+ )}
+ {route.tab === 'resolve' && (
+
+ )}
+ {route.tab === 'manageRequests' && (
+
+ )}
+ {route.tab === 'manageByUser' && (
+
+ )}
+
+
);
};
diff --git a/frontend/src/webparts/approvalAdmin/components/app/TopNav.module.scss b/frontend/src/webparts/approvalAdmin/components/app/TopNav.module.scss
deleted file mode 100644
index b5bf541..0000000
--- a/frontend/src/webparts/approvalAdmin/components/app/TopNav.module.scss
+++ /dev/null
@@ -1,76 +0,0 @@
-@import '../../../../styles/design-tokens';
-
-.topnav {
- background: $grad-header;
- padding: 0 22px;
- display: flex;
- align-items: center;
- height: 54px;
- box-shadow: 0 1px 0 rgba(212,160,30,.35);
- border-radius: $r-md $r-md 0 0;
- // Без переноса: высота шапки фиксирована (54px), поэтому перенесённая
- // строка вкладок вылезала бы за её пределы вместе с подчёркиванием.
- // Вместо переноса вкладки скроллятся по горизонтали.
- flex-wrap: nowrap;
- overflow: hidden;
-}
-
-.brand {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-right: 20px;
- flex-shrink: 0;
-}
-
-.brandText {
- color: #fff;
- font-weight: 600;
- font-size: 13px;
- letter-spacing: .4px;
- white-space: nowrap;
-}
-
-.tabs {
- display: flex;
- height: 100%;
- flex-wrap: nowrap;
- // min-width: 0 обязателен — иначе flex-элемент не сожмётся уже своего
- // содержимого и overflow-x никогда не сработает.
- min-width: 0;
- overflow-x: auto;
- overflow-y: hidden;
- // Делает контейнер offsetParent'ом для вкладок: автопрокрутка в TopNav.tsx
- // считает offsetLeft относительно него. Без этого offsetLeft отсчитывался
- // бы от body и прокрутка уезжала бы не туда.
- position: relative;
-
- // Тонкая полупрозрачная полоса — не спрятана полностью: с добавлением новых
- // вкладок (сейчас их 6) они не всегда помещаются в узкую панель, и без
- // видимого скроллбара непонятно, что часть вкладок обрезана, а не пропала.
- scrollbar-width: thin;
- scrollbar-color: rgba(255, 255, 255, .35) transparent;
- &::-webkit-scrollbar { height: 4px; }
- &::-webkit-scrollbar-track { background: transparent; }
- &::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, .35); border-radius: 2px; }
-}
-
-.tab {
- flex-shrink: 0;
- white-space: nowrap;
- padding: 0 13px;
- border: none;
- background: 0;
- color: rgba(255,255,255,.5);
- font-size: 13px;
- font-weight: 500;
- cursor: pointer;
- font-family: inherit;
- transition: .18s;
- height: 54px;
- border-bottom: 2px solid transparent;
- letter-spacing: .2px;
-
- &:hover { color: rgba(255,255,255,.85); }
- &.on { color: $gold-300; border-bottom-color: $gold-400; }
-}
diff --git a/frontend/src/webparts/approvalAdmin/components/app/TopNav.tsx b/frontend/src/webparts/approvalAdmin/components/app/TopNav.tsx
deleted file mode 100644
index 2a33140..0000000
--- a/frontend/src/webparts/approvalAdmin/components/app/TopNav.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import * as React from 'react';
-import styles from './TopNav.module.scss';
-import { ScreenKey } from './types';
-import { texts } from '../../texts';
-
-interface ITab {
- key: ScreenKey;
- label: string;
-}
-
-const TABS: ITab[] = [
- { key: 'dashboard', label: texts.nav.dashboard },
- { key: 'requestTypes', label: texts.nav.requestTypes },
- { key: 'routes', label: texts.nav.routes },
- { key: 'resolve', label: texts.nav.resolve },
- { key: 'manageRequests', label: texts.nav.manageRequests }
-];
-
-export interface ITopNavProps {
- active: ScreenKey;
- onSelect: (key: ScreenKey) => void;
-}
-
-function isTabActive(active: ScreenKey, tabKey: ScreenKey): boolean {
- // Экран шагов маршрута — вложенный, подсвечиваем вкладку «Маршруты».
- return active === tabKey || (active === 'routeSteps' && tabKey === 'routes');
-}
-
-export const TopNav: React.FC
= ({ active, onSelect }) => {
- const tabsRef = React.useRef(null);
- const activeTabRef = React.useRef(null);
-
- // Вкладки скроллятся по горизонтали, поэтому активная может оказаться за
- // краем. Подкручиваем контейнер вручную (не scrollIntoView — тот способен
- // утащить за собой прокрутку всей страницы SharePoint).
- React.useEffect(() => {
- const reveal = (): void => {
- const container = tabsRef.current;
- const tab = activeTabRef.current;
- if (!container || !tab) return;
-
- const left = tab.offsetLeft;
- const width = tab.offsetWidth;
-
- if (width > container.clientWidth) {
- // Вкладка шире видимой полосы — показываем её начало, чтобы подпись
- // читалась с первого слова.
- container.scrollLeft = left;
- } else if (left < container.scrollLeft) {
- container.scrollLeft = left;
- } else if (left + width > container.scrollLeft + container.clientWidth) {
- container.scrollLeft = left + width - container.clientWidth;
- }
- };
-
- reveal();
-
- // Шрифты (Lora/Inter) подключаются рантаймом: пока они не применились,
- // ширины вкладок другие и позиция прокрутки получается устаревшей.
- const fonts = (document as Document & { fonts?: FontFaceSet }).fonts;
- let cancelled = false;
- if (fonts?.ready) {
- fonts.ready.then(() => { if (!cancelled) reveal(); }).catch(() => { /* не критично */ });
- }
-
- window.addEventListener('resize', reveal);
- return () => {
- cancelled = true;
- window.removeEventListener('resize', reveal);
- };
- }, [active]);
-
- return (
-
- );
-};
diff --git a/frontend/src/webparts/approvalAdmin/components/app/types.ts b/frontend/src/webparts/approvalAdmin/components/app/types.ts
index d6b53d8..71b47cb 100644
--- a/frontend/src/webparts/approvalAdmin/components/app/types.ts
+++ b/frontend/src/webparts/approvalAdmin/components/app/types.ts
@@ -1 +1 @@
-export type ScreenKey = 'dashboard' | 'requestTypes' | 'routes' | 'routeSteps' | 'resolve' | 'manageRequests';
+export type ScreenKey = 'dashboard' | 'requestTypes' | 'routes' | 'routeSteps' | 'resolve' | 'manageRequests' | 'manageByUser';
diff --git a/frontend/src/webparts/approvalAdmin/components/screens/DashboardScreen.tsx b/frontend/src/webparts/approvalAdmin/components/screens/DashboardScreen.tsx
index d9664fd..2451be1 100644
--- a/frontend/src/webparts/approvalAdmin/components/screens/DashboardScreen.tsx
+++ b/frontend/src/webparts/approvalAdmin/components/screens/DashboardScreen.tsx
@@ -69,6 +69,12 @@ export const DashboardScreen: React.FC = ({ api, onNaviga
description={texts.dashboard.manageRequestsCard.description}
onClick={() => onNavigate('manageRequests')}
/>
+ onNavigate('manageByUser')}
+ />
);
diff --git a/frontend/src/webparts/approvalAdmin/components/screens/ManageByUserDuplicateModal.tsx b/frontend/src/webparts/approvalAdmin/components/screens/ManageByUserDuplicateModal.tsx
new file mode 100644
index 0000000..5348bb6
--- /dev/null
+++ b/frontend/src/webparts/approvalAdmin/components/screens/ManageByUserDuplicateModal.tsx
@@ -0,0 +1,34 @@
+import * as React from 'react';
+import { texts } from '../../texts';
+import { Button, Modal } from '../ui';
+
+export type DuplicateChoice = 'delete' | 'skip';
+
+export interface IManageByUserDuplicateModalProps {
+ title: string;
+ message: string;
+ onChoice: (choice: DuplicateChoice, forAll: boolean) => void;
+}
+
+/**
+ * «Да / Да для всех / Нет / Нет для всех» — классический диалог разрешения
+ * конфликтов при пакетной операции (как в проводнике при копировании файлов).
+ * Закрытие крестиком/подложкой равносильно «Нет» (без «для всех» — иначе
+ * случайный клик мимо тихо остановил бы весь оставшийся список).
+ */
+export const ManageByUserDuplicateModal: React.FC