From 5bc80df11c6a513326938701c2256f5f1de9f6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Tue, 4 Aug 2026 17:40:46 +0500 Subject: [PATCH] feat: changed initial page layout --- .../CategoryTabs/CategoryTabs.module.scss | 20 +-- .../components/RequestPortal.module.scss | 35 +++-- .../components/RequestPortal.tsx | 10 ++ .../components/SideNav/SideNav.module.scss | 133 +++++++++++++++--- .../components/SideNav/SideNav.tsx | 88 +++++++----- .../features/submit/SubmitFeature.tsx | 9 +- .../RecruitingHireView.module.scss | 6 + .../RecruitingHire/RecruitingHireView.tsx | 19 ++- 8 files changed, 235 insertions(+), 85 deletions(-) diff --git a/src/webparts/requestPortal/components/CategoryTabs/CategoryTabs.module.scss b/src/webparts/requestPortal/components/CategoryTabs/CategoryTabs.module.scss index 074cb29..21f6c44 100644 --- a/src/webparts/requestPortal/components/CategoryTabs/CategoryTabs.module.scss +++ b/src/webparts/requestPortal/components/CategoryTabs/CategoryTabs.module.scss @@ -1,25 +1,24 @@ @import '../../../../styles/design-tokens'; +// Светлая полоса вкладок категорий (единственный тёмный элемент портала — +// боковой рейл; вкладки светлые, активная — navy с золотым подчёркиванием). .tabs { display: flex; flex-wrap: wrap; align-items: stretch; - gap: $sp-1; - padding: 0 $sp-5; - background: $grad-header; - border-radius: $r-lg; - box-shadow: 0 1px 0 rgba(212, 160, 30, .35), $sh-2; + gap: $sp-2; + border-bottom: 1px solid $gray-200; } .tab { display: inline-flex; align-items: center; gap: $sp-2; - padding: 14px 18px; + padding: 12px 16px; border: 0; border-bottom: 2px solid transparent; background: transparent; - color: rgba(255, 255, 255, .5); + color: $gray-500; font-family: $font-sans; font-size: 13px; font-weight: 500; @@ -29,11 +28,12 @@ transition: color .18s, border-color .18s; &:hover { - color: rgba(255, 255, 255, .85); + color: $navy-800; } } .active { - color: $gold-300; - border-bottom-color: $gold-400; + color: $navy-900; + font-weight: 600; + border-bottom-color: $gold-500; } diff --git a/src/webparts/requestPortal/components/RequestPortal.module.scss b/src/webparts/requestPortal/components/RequestPortal.module.scss index bd3c4ad..d624a9e 100644 --- a/src/webparts/requestPortal/components/RequestPortal.module.scss +++ b/src/webparts/requestPortal/components/RequestPortal.module.scss @@ -23,8 +23,8 @@ // Корень занимает 100% контейнера, без CSS-трюков с 100vw. width: 100%; - // Только вертикальные отступы; горизонтальные — на внутренних секциях. - padding: $sp-4 $sp-5 $sp-8; + // App-shell «edge-to-edge»: рейл примыкает к левому краю, отступы — внутри main. + padding: 0; } .teams { @@ -32,44 +32,41 @@ font-family: $font-sans; } -// Раскладка: боковое меню (side) + основная область (main). +// Раскладка: боковой рейл (side) на всю высоту + основная область (main). .layout { display: flex; - align-items: flex-start; - gap: $sp-6; + align-items: stretch; // рейл тянется на высоту контента width: 100%; - max-width: 1600px; - margin: 0 auto; + min-height: 78vh; // «присутствие» рейла даже когда контента мало +} + +// Рейл-колонка: сам SideNav тянется по высоте (flex-стретч). +.side { + flex: 0 0 auto; + display: flex; } .main { flex: 1 1 auto; min-width: 0; // чтобы контент не распирал колонку + display: flex; + flex-direction: column; + padding: $sp-5 $sp-6 $sp-8; } -// Ширину задаёт само меню (SideNav сворачивается) — колонка просто под него. -.side { - flex: 0 0 auto; - position: sticky; - top: $sp-4; -} - -// Та же ширина, что у контента подачи (SubmitFeature .content) — чтобы вкладки -// не уезжали шире формы под ними. .tabsWrap { - max-width: 1080px; margin-bottom: $sp-6; } -// На узких экранах меню уходит наверх, в одну строку. +// На узких экранах рейл уходит наверх. @media (max-width: 900px) { .layout { flex-direction: column; + min-height: 0; } .side { width: 100%; - position: static; } } diff --git a/src/webparts/requestPortal/components/RequestPortal.tsx b/src/webparts/requestPortal/components/RequestPortal.tsx index 0855574..0b2a4f5 100644 --- a/src/webparts/requestPortal/components/RequestPortal.tsx +++ b/src/webparts/requestPortal/components/RequestPortal.tsx @@ -61,6 +61,15 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context }; }, [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 => navigate({ mode: 'submit', nav }); const closeDrawer = (): void => navigate({ mode: 'submit', nav: route.nav }); @@ -75,6 +84,7 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context approvalsCount={approvalsCount} collapsed={navCollapsed} onToggleCollapsed={() => setNavCollapsed((v) => !v)} + userName={context.pageContext.user.displayName} /> diff --git a/src/webparts/requestPortal/components/SideNav/SideNav.module.scss b/src/webparts/requestPortal/components/SideNav/SideNav.module.scss index 8ea413b..aa708c4 100644 --- a/src/webparts/requestPortal/components/SideNav/SideNav.module.scss +++ b/src/webparts/requestPortal/components/SideNav/SideNav.module.scss @@ -1,28 +1,77 @@ @import '../../../../styles/design-tokens'; -// Тёмное боковое меню портала (фирменный navy-градиент, золотой маркер -// активного пункта — как в остальном дизайне). +// Тёмный боковой рейл портала на всю высоту (фирменный navy-градиент, золотой +// маркер активного пункта). Не «плавает»: примыкает к контенту, тянется по высоте. .nav { display: flex; flex-direction: column; - gap: 2px; - width: 240px; - padding: $sp-3 0; + width: 248px; + padding: $sp-4 0 $sp-3; background: $grad-header; - border-radius: $r-lg; - box-shadow: $sh-2; - overflow: hidden; + box-shadow: inset -1px 0 0 rgba(255, 255, 255, .06); transition: width .2s; } +.navCollapsed { + width: 64px; +} + +// ---- Шапка рейла: бренд + сворачивание ---- +.head { + display: flex; + align-items: center; + justify-content: space-between; + gap: $sp-2; + padding: 0 $sp-4 $sp-4; + border-bottom: 1px solid rgba(255, 255, 255, .08); +} + +.navCollapsed .head { + flex-direction: column; + gap: $sp-3; + padding: 0 0 $sp-3; +} + +.brand { + display: flex; + align-items: center; + gap: $sp-2; + 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; + font-weight: 600; + color: $white; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .toggle { - align-self: flex-end; + flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; - margin: 0 $sp-3 $sp-2; padding: 0; border: 1px solid rgba(255, 255, 255, .18); border-radius: $r-sm; @@ -37,18 +86,16 @@ } } -.navCollapsed { - width: 56px; +// ---- Пункты разделов ---- +.items { + display: flex; + flex-direction: column; + gap: 2px; + margin-top: $sp-4; +} - .toggle { - align-self: center; - margin: 0 0 $sp-2; - } - - .item { - justify-content: center; - padding: 12px 0; - } +.navCollapsed .items { + margin-top: $sp-3; } .item { @@ -76,6 +123,11 @@ } } +.navCollapsed .item { + justify-content: center; + padding: 12px 0; +} + .itemActive { color: $white; background: rgba(255, 255, 255, .07); @@ -111,9 +163,46 @@ text-align: center; } -// В свёрнутом меню бейдж «прилипает» к иконке, т.к. подписи нет. .navCollapsed .badge { position: absolute; 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; +} diff --git a/src/webparts/requestPortal/components/SideNav/SideNav.tsx b/src/webparts/requestPortal/components/SideNav/SideNav.tsx index c46e177..dd84c9a 100644 --- a/src/webparts/requestPortal/components/SideNav/SideNav.tsx +++ b/src/webparts/requestPortal/components/SideNav/SideNav.tsx @@ -23,53 +23,77 @@ 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 = ({ activeId, onChange, approvalsCount, collapsed, onToggleCollapsed, + userName, }) => { const navClasses = [styles.nav, collapsed ? styles.navCollapsed : ''].filter(Boolean).join(' '); return ( ); }; diff --git a/src/webparts/requestPortal/features/submit/SubmitFeature.tsx b/src/webparts/requestPortal/features/submit/SubmitFeature.tsx index a43881e..b7e773c 100644 --- a/src/webparts/requestPortal/features/submit/SubmitFeature.tsx +++ b/src/webparts/requestPortal/features/submit/SubmitFeature.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect, useMemo, useRef } from 'react'; import type { WebPartContext } from '@microsoft/sp-webpart-base'; import styles from './SubmitFeature.module.scss'; import { Icon } from '../../shared/ui/Icon/Icon'; @@ -30,7 +30,14 @@ const SubmitFeature: React.FC = ({ context, activeCategoryI const [selectedRequest, setSelectedRequest] = useState(initialSelected); // Смена категории (вкладка сверху) — возвращаемся к списку плиток. + // Первый прогон пропускаем: иначе форма, открытая по initialForm «на основе» + // (её категория может не совпадать с текущей вкладкой), тут же закроется. + const firstCategorySync = useRef(true); useEffect(() => { + if (firstCategorySync.current) { + firstCategorySync.current = false; + return; + } setSelectedRequest((prev) => (prev && prev.categoryId !== activeCategoryId ? null : prev)); }, [activeCategoryId]); diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.module.scss b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.module.scss index d921f54..6a27763 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.module.scss +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.module.scss @@ -6,3 +6,9 @@ font-size: $fs-sm; text-align: center; } + +.toolbar { + display: flex; + justify-content: flex-end; + margin-bottom: $sp-4; +} diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx index 240da28..a1c0547 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx @@ -2,8 +2,9 @@ import * as React from 'react'; import { useState, useEffect, useMemo } from 'react'; import type { WebPartContext } from '@microsoft/sp-webpart-base'; import styles from './RecruitingHireView.module.scss'; -import { Alert, FormCard, FormModeProvider, CollapseSection, FormField, AttachmentList } from '../../shared/ui'; +import { Alert, Button, FormCard, FormModeProvider, CollapseSection, FormField, AttachmentList, Icon } from '../../shared/ui'; import { ApproverService, DecisionPanel, StatusBadge } from '../../shared/approval'; +import { navigate } from '../../shared/routing'; import { getAttachments, IAttachmentInfo } from '../../shared/attachments/attachments.service'; import { IFormData, EMPTY_DATA, SetField } from './model'; import { loadHireRequest } from './load'; @@ -18,6 +19,10 @@ interface IRecruitingHireViewProps { itemId: number; } +// Ключ формы «Организация рабочего места» в реестре (data/requests) — цель +// производной заявки «на основе прием». +const WORKPLACE_FORM_KEY = 'it-workplace-setup'; + // В режиме чтения поля не редактируются, поэтому обновление — пустышка. const NOOP_SET: SetField = () => undefined; @@ -76,6 +81,18 @@ const RecruitingHireView: React.FC = ({ context, itemI return (
+
+ +
+