Редактирование заявок у конкретного пользователя #14
@@ -0,0 +1 @@
|
||||
@tnsu:registry=http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/
|
||||
Generated
+13
@@ -17,6 +17,7 @@
|
||||
"@microsoft/sp-property-pane": "1.22.1",
|
||||
"@microsoft/sp-webpart-base": "1.22.1",
|
||||
"@pnp/sp": "^4.20.0",
|
||||
"@tnsu/ui-kit": "^0.4.0",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"tslib": "2.3.1"
|
||||
@@ -6281,6 +6282,18 @@
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@tnsu/ui-kit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/%40tnsu%2Fui-kit/-/0.4.0/ui-kit-0.4.0.tgz",
|
||||
"integrity": "sha512-J3cBvoR2vV0SC5GihOagLAqvC0txQE342qIh6viabMt+Um6Jg4mhTff+ZkVahG0g+bF6sqSCUl3x813aO82KuQ==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"@fluentui/react": "^8.106.4",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"tslib": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz",
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluentui/react": "^8.106.4",
|
||||
"@pnp/sp": "^4.20.0",
|
||||
"@microsoft/sp-component-base": "1.22.1",
|
||||
"@microsoft/sp-core-library": "1.22.1",
|
||||
"@microsoft/sp-http": "1.22.1",
|
||||
@@ -21,6 +20,8 @@
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.22.1",
|
||||
"@microsoft/sp-property-pane": "1.22.1",
|
||||
"@microsoft/sp-webpart-base": "1.22.1",
|
||||
"@pnp/sp": "^4.20.0",
|
||||
"@tnsu/ui-kit": "^0.4.0",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"tslib": "2.3.1"
|
||||
|
||||
@@ -1,23 +1,63 @@
|
||||
@import '../../../../styles/design-tokens';
|
||||
|
||||
// Раскладка — тот же боковой рейл + основная область, что и в request-portal
|
||||
// (@tnsu/ui-kit Sidebar, см. RequestPortal.module.scss): единый стиль
|
||||
// навигации во всех веб-частях Tnsu. В отличие от request-portal (рейл во всю
|
||||
// высоту окна, edge-to-edge), здесь виджет — ограниченная карточка на странице
|
||||
// SharePoint, поэтому без sticky/100vh-трюков: рейл просто растягивается по
|
||||
// высоте своего соседа (.main) через align-items: stretch.
|
||||
.shell {
|
||||
background: linear-gradient(180deg, #F6F8FA 0%, #EEF2F6 100%);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
border-radius: $r-lg;
|
||||
border: 1px solid $gray-200;
|
||||
overflow: hidden;
|
||||
box-shadow: $sh-1;
|
||||
color: $gray-900;
|
||||
font-family: $font-sans;
|
||||
font-size: $fs-body;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.side {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
background: $navy-800;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0; // чтобы контент не распирал колонку
|
||||
background: linear-gradient(180deg, #F6F8FA 0%, #EEF2F6 100%);
|
||||
color: $gray-900;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 28px 30px 36px;
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// Мобильная кнопка-бургер (открывает выезжающий Sidebar). На десктопе скрыта.
|
||||
.mobileNav {
|
||||
display: none;
|
||||
padding: $sp-4 $sp-6 0;
|
||||
}
|
||||
|
||||
// < 768px: рейл сам уходит в position:fixed и уезжает за левый край экрана,
|
||||
// пока не открыт (см. @tnsu/ui-kit Sidebar — это его собственный CSS, чисто
|
||||
// media-query, без участия JS) — НЕ display:none на .side: рейл вёрстке всё
|
||||
// равно нужен в потоке, чтобы при mobileOpen выехать поверх контента.
|
||||
@media (max-width: 768px) {
|
||||
.mobileNav {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.side {
|
||||
align-self: flex-start;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.centerState {
|
||||
padding: 60px 30px;
|
||||
text-align: center;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import styles from './AdminApp.module.scss';
|
||||
import { TopNav } from './TopNav';
|
||||
import '@tnsu/ui-kit/styles.css';
|
||||
import { IconSprite, Sidebar, SidebarToggle } from '@tnsu/ui-kit';
|
||||
import type { ISidebarItem } from '@tnsu/ui-kit';
|
||||
import { ScreenKey } from './types';
|
||||
import { ApprovalRouteApi } from '../../services/api';
|
||||
import { PortalDictionaryClient } from '../../services/portalDictionary';
|
||||
@@ -8,6 +10,7 @@ import { SharePointListsClient } from '../../services/sharePointLists';
|
||||
import { RequestManageService } from '../../services/requestManageService';
|
||||
import { RequestType, TemplateRoute } from '../../models';
|
||||
import { navigate, useRoute } from '../../routing';
|
||||
import { texts } from '../../texts';
|
||||
import { DashboardScreen } from '../screens/DashboardScreen';
|
||||
import { RequestTypesScreen } from '../screens/RequestTypesScreen';
|
||||
import { RoutesScreen } from '../screens/RoutesScreen';
|
||||
@@ -15,6 +18,22 @@ import { RouteStepsScreen } from '../screens/RouteStepsScreen';
|
||||
import { ResolveScreen } from '../screens/ResolveScreen';
|
||||
import { ManageRequestsScreen } from '../screens/ManageRequestsScreen';
|
||||
|
||||
// Тот же боковой рейл (@tnsu/ui-kit Sidebar), что и в request-portal — единый
|
||||
// стиль навигации во всех веб-частях Tnsu. 'routeSteps' сюда не входит: это не
|
||||
// отдельная вкладка URL, а состояние вкладки 'routes' при открытых шагах
|
||||
// конкретного маршрута (см. routing.ts) — подсвечиваем в этом случае 'routes'.
|
||||
const NAV_ITEMS: ISidebarItem[] = [
|
||||
{ id: 'dashboard', title: texts.nav.dashboard, icon: 'chart' },
|
||||
{ id: 'requestTypes', title: texts.nav.requestTypes, icon: 'document' },
|
||||
{ id: 'routes', title: texts.nav.routes, icon: 'send' },
|
||||
{ id: 'resolve', title: texts.nav.resolve, icon: 'search' },
|
||||
{ id: 'manageRequests', title: texts.nav.manageRequests, icon: 'clipboard' }
|
||||
];
|
||||
|
||||
function sidebarActiveId(tab: ScreenKey): string {
|
||||
return tab === 'routeSteps' ? 'routes' : tab;
|
||||
}
|
||||
|
||||
export interface IAdminAppProps {
|
||||
api: ApprovalRouteApi;
|
||||
/** Undefined, пока SPFx выдаёт AAD-клиент для api.tnsu.kz. */
|
||||
@@ -28,8 +47,13 @@ export const AdminApp: React.FC<IAdminAppProps> = ({ api, dictionary, sharePoint
|
||||
const route = useRoute();
|
||||
const [selectedRoute, setSelectedRoute] = React.useState<TemplateRoute | undefined>();
|
||||
const [selectedRequestType, setSelectedRequestType] = React.useState<RequestType | undefined>();
|
||||
const [navCollapsed, setNavCollapsed] = React.useState(false);
|
||||
const [mobileNavOpen, setMobileNavOpen] = React.useState(false);
|
||||
|
||||
const navigateTab = (key: ScreenKey): void => navigate({ tab: key });
|
||||
const navigateTab = (key: ScreenKey): void => {
|
||||
setMobileNavOpen(false);
|
||||
navigate({ tab: key });
|
||||
};
|
||||
|
||||
const openSteps = (r: TemplateRoute): void => {
|
||||
setSelectedRoute(r);
|
||||
@@ -76,7 +100,23 @@ export const AdminApp: React.FC<IAdminAppProps> = ({ api, dictionary, sharePoint
|
||||
|
||||
return (
|
||||
<div className={styles.shell}>
|
||||
<TopNav active={route.tab} onSelect={navigateTab} />
|
||||
<IconSprite />
|
||||
<div className={styles.side}>
|
||||
<Sidebar
|
||||
items={NAV_ITEMS}
|
||||
activeId={sidebarActiveId(route.tab)}
|
||||
onChange={id => navigateTab(id as ScreenKey)}
|
||||
brand={texts.nav.title}
|
||||
collapsed={navCollapsed}
|
||||
onToggleCollapsed={() => setNavCollapsed(v => !v)}
|
||||
mobileOpen={mobileNavOpen}
|
||||
onMobileClose={() => setMobileNavOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
<main className={styles.main}>
|
||||
<div className={styles.mobileNav}>
|
||||
<SidebarToggle onClick={() => setMobileNavOpen(true)} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{route.tab === 'dashboard' && <DashboardScreen api={api} onNavigate={navigateTab} />}
|
||||
{route.tab === 'requestTypes' && <RequestTypesScreen api={api} sharePoint={sharePoint} />}
|
||||
@@ -111,6 +151,7 @@ export const AdminApp: React.FC<IAdminAppProps> = ({ api, dictionary, sharePoint
|
||||
<ManageRequestsScreen api={api} dictionary={dictionary} requestManage={requestManage} route={route.manage} />
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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<ITopNavProps> = ({ active, onSelect }) => {
|
||||
const tabsRef = React.useRef<HTMLDivElement>(null);
|
||||
const activeTabRef = React.useRef<HTMLButtonElement>(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 (
|
||||
<nav className={styles.topnav}>
|
||||
<div className={styles.brand}>
|
||||
<div className={styles.brandText}>{texts.nav.title}</div>
|
||||
</div>
|
||||
<div className={styles.tabs} ref={tabsRef}>
|
||||
{TABS.map(tab => {
|
||||
const on = isTabActive(active, tab.key);
|
||||
return (
|
||||
<button
|
||||
key={tab.key}
|
||||
ref={on ? activeTabRef : undefined}
|
||||
className={`${styles.tab} ${on ? styles.on : ''}`}
|
||||
onClick={() => onSelect(tab.key)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user