This commit is contained in:
Generated
+4
-4
@@ -17,7 +17,7 @@
|
|||||||
"@microsoft/sp-webpart-base": "1.22.1",
|
"@microsoft/sp-webpart-base": "1.22.1",
|
||||||
"@pnp/sp": "^4.20.0",
|
"@pnp/sp": "^4.20.0",
|
||||||
"@pnp/spfx-controls-react": "^3.25.0",
|
"@pnp/spfx-controls-react": "^3.25.0",
|
||||||
"@tnsu/ui-kit": "^0.2.0",
|
"@tnsu/ui-kit": "^0.3.0",
|
||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.1",
|
||||||
"tslib": "2.3.1"
|
"tslib": "2.3.1"
|
||||||
@@ -9870,9 +9870,9 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/@tnsu/ui-kit": {
|
"node_modules/@tnsu/ui-kit": {
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"resolved": "http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/%40tnsu%2Fui-kit/-/0.2.0/ui-kit-0.2.0.tgz",
|
"resolved": "http://git.tnsu.kz/api/packages/timur.gorodetskiy/npm/%40tnsu%2Fui-kit/-/0.3.0/ui-kit-0.3.0.tgz",
|
||||||
"integrity": "sha512-8vQsA91lSbmtlqROlIYkgSH3cNEH5DCVI5vM4mks2RWwvSpYet0zfe2B3wfBcnb+cAWrbLGfCwe81KRDcJ/k+Q==",
|
"integrity": "sha512-V9pTS/rBmU9KInczI8jJ/KHT96hfX0OlnjIJ2NZ/iEQTOE93I9xMfmgpj83PSRrI6/xzyybu+8k/qVO6LP926w==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@fluentui/react": "^8.106.4",
|
"@fluentui/react": "^8.106.4",
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@
|
|||||||
"@microsoft/sp-webpart-base": "1.22.1",
|
"@microsoft/sp-webpart-base": "1.22.1",
|
||||||
"@pnp/sp": "^4.20.0",
|
"@pnp/sp": "^4.20.0",
|
||||||
"@pnp/spfx-controls-react": "^3.25.0",
|
"@pnp/spfx-controls-react": "^3.25.0",
|
||||||
"@tnsu/ui-kit": "^0.2.0",
|
"@tnsu/ui-kit": "^0.3.0",
|
||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.1",
|
||||||
"tslib": "2.3.1"
|
"tslib": "2.3.1"
|
||||||
|
|||||||
@@ -81,15 +81,22 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
// На узких экранах рейл уходит наверх.
|
// Мобильная кнопка-бургер (открывает выезжающий Sidebar). На десктопе скрыта.
|
||||||
@media (max-width: 900px) {
|
.mobileNav {
|
||||||
|
display: none;
|
||||||
|
margin-bottom: $sp-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// < 768px: рейл превращается в выезжающий drawer (см. @tnsu/ui-kit Sidebar). Он
|
||||||
|
// position:fixed и выходит из потока — колонка .side схлопывается сама, main
|
||||||
|
// занимает всю ширину. Показываем бургер в шапке контента.
|
||||||
|
@media (max-width: 768px) {
|
||||||
.layout {
|
.layout {
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side {
|
.mobileNav {
|
||||||
width: 100%;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import * as React from 'react';
|
|||||||
import styles from './RequestPortal.module.scss';
|
import styles from './RequestPortal.module.scss';
|
||||||
import '@tnsu/ui-kit/styles.css';
|
import '@tnsu/ui-kit/styles.css';
|
||||||
import type { IRequestPortalProps } from './IRequestPortalProps';
|
import type { IRequestPortalProps } from './IRequestPortalProps';
|
||||||
import { IconSprite } from '@tnsu/ui-kit';
|
import { IconSprite, Sidebar, SidebarToggle } from '@tnsu/ui-kit';
|
||||||
|
import type { ISidebarItem } from '@tnsu/ui-kit';
|
||||||
import { ConfirmHost } from '../shared/ui';
|
import { ConfirmHost } from '../shared/ui';
|
||||||
import {
|
import {
|
||||||
navigate,
|
navigate,
|
||||||
@@ -21,7 +22,6 @@ import {
|
|||||||
import { onRequestsChanged, emitRequestsChanged } from '../shared/requestEvents';
|
import { onRequestsChanged, emitRequestsChanged } from '../shared/requestEvents';
|
||||||
import { getImpersonatedEmail } from '../shared/impersonation';
|
import { getImpersonatedEmail } from '../shared/impersonation';
|
||||||
import { confirmDiscardFormChanges, markFormClean, markFormDirty } from '../shared/unsavedChanges';
|
import { confirmDiscardFormChanges, markFormClean, markFormDirty } from '../shared/unsavedChanges';
|
||||||
import { SideNav } from './SideNav/SideNav';
|
|
||||||
import { Drawer } from './Drawer/Drawer';
|
import { Drawer } from './Drawer/Drawer';
|
||||||
import { CategoryTabs } from './CategoryTabs/CategoryTabs';
|
import { CategoryTabs } from './CategoryTabs/CategoryTabs';
|
||||||
import { CATEGORIES, DEFAULT_CATEGORY_ID } from '../data/categories';
|
import { CATEGORIES, DEFAULT_CATEGORY_ID } from '../data/categories';
|
||||||
@@ -65,6 +65,7 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const [navCollapsed, setNavCollapsed] = React.useState<boolean>(false);
|
const [navCollapsed, setNavCollapsed] = React.useState<boolean>(false);
|
||||||
|
const [mobileNavOpen, setMobileNavOpen] = React.useState<boolean>(false);
|
||||||
const [navCounts, setNavCounts] = React.useState<INavCounts | undefined>(undefined);
|
const [navCounts, setNavCounts] = React.useState<INavCounts | undefined>(undefined);
|
||||||
|
|
||||||
const [activeCategoryId, setActiveCategoryId] = React.useState<string>(() => {
|
const [activeCategoryId, setActiveCategoryId] = React.useState<string>(() => {
|
||||||
@@ -256,6 +257,16 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
|||||||
}).catch(() => undefined);
|
}).catch(() => undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Разделы бокового меню для пакетного Sidebar (счётчики — из navCounts).
|
||||||
|
const navItems: ISidebarItem[] = [
|
||||||
|
{ id: 'new', title: 'Новая заявка', icon: 'plus' },
|
||||||
|
{ id: 'approvals', title: 'На согласовании', icon: 'user-check', count: navCounts?.approvals },
|
||||||
|
{ id: 'execution', title: 'На исполнении', icon: 'helmet', count: navCounts?.execution },
|
||||||
|
{ id: 'rework', title: 'Доработка', icon: 'edit', count: navCounts?.rework },
|
||||||
|
{ id: 'own', title: 'Мои заявки', icon: 'clipboard' },
|
||||||
|
{ id: 'search', title: 'Поиск', icon: 'search' },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={rootClasses}>
|
<section className={rootClasses}>
|
||||||
<IconSprite />
|
<IconSprite />
|
||||||
@@ -271,20 +282,22 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
|||||||
)}
|
)}
|
||||||
<div className={styles.layout}>
|
<div className={styles.layout}>
|
||||||
<div className={styles.side}>
|
<div className={styles.side}>
|
||||||
<SideNav
|
<Sidebar
|
||||||
|
items={navItems}
|
||||||
activeId={route.nav}
|
activeId={route.nav}
|
||||||
onChange={goto}
|
onChange={(id) => goto(id as NavSection)}
|
||||||
counts={{
|
brand="Портал заявок"
|
||||||
approvals: navCounts?.approvals,
|
|
||||||
execution: navCounts?.execution,
|
|
||||||
rework: navCounts?.rework,
|
|
||||||
}}
|
|
||||||
collapsed={navCollapsed}
|
collapsed={navCollapsed}
|
||||||
onToggleCollapsed={() => setNavCollapsed((v) => !v)}
|
onToggleCollapsed={() => setNavCollapsed((v) => !v)}
|
||||||
|
mobileOpen={mobileNavOpen}
|
||||||
|
onMobileClose={() => setMobileNavOpen(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
|
<div className={styles.mobileNav}>
|
||||||
|
<SidebarToggle onClick={() => setMobileNavOpen(true)} />
|
||||||
|
</div>
|
||||||
{/* Раздел «Новая заявка» держим СМОНТИРОВАННЫМ и просто прячем стилем,
|
{/* Раздел «Новая заявка» держим СМОНТИРОВАННЫМ и просто прячем стилем,
|
||||||
когда открыта другая вкладка. Иначе React размонтировал бы форму, и
|
когда открыта другая вкладка. Иначе React размонтировал бы форму, и
|
||||||
всё введённое пропадало бы при переключении на «Мои заявки» и
|
всё введённое пропадало бы при переключении на «Мои заявки» и
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
@import '../../../../styles/design-tokens';
|
|
||||||
|
|
||||||
// Тёмный боковой рейл портала на всю высоту (фирменный navy-градиент, золотой
|
|
||||||
// маркер активного пункта). Не «плавает»: примыкает к контенту, тянется по высоте.
|
|
||||||
.nav {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 248px;
|
|
||||||
padding: $sp-4 0 $sp-3;
|
|
||||||
background: $grad-header;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brandText {
|
|
||||||
font-family: $font-serif;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: $white;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
padding: 0;
|
|
||||||
border: 1px solid rgba(255, 255, 255, .18);
|
|
||||||
border-radius: $r-sm;
|
|
||||||
background: transparent;
|
|
||||||
color: rgba(255, 255, 255, .6);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: .16s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: $gold-400;
|
|
||||||
color: $gold-300;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Пункты разделов ----
|
|
||||||
.items {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
margin-top: $sp-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navCollapsed .items {
|
|
||||||
margin-top: $sp-3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: $sp-3;
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 18px;
|
|
||||||
border: 0;
|
|
||||||
border-left: 3px solid transparent;
|
|
||||||
background: transparent;
|
|
||||||
color: rgba(255, 255, 255, .62);
|
|
||||||
font-family: $font-sans;
|
|
||||||
font-size: 13.5px;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: left;
|
|
||||||
white-space: nowrap;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color .18s, background .18s, border-color .18s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: rgba(255, 255, 255, .92);
|
|
||||||
background: rgba(255, 255, 255, .04);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navCollapsed .item {
|
|
||||||
justify-content: center;
|
|
||||||
padding: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemActive {
|
|
||||||
color: $white;
|
|
||||||
background: rgba(255, 255, 255, .07);
|
|
||||||
border-left-color: $gold-500;
|
|
||||||
|
|
||||||
.itemIcon {
|
|
||||||
color: $gold-400;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemIcon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: inline-flex;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemTitle {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
flex-shrink: 0;
|
|
||||||
min-width: 18px;
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-radius: $r-full;
|
|
||||||
background: $gold-500;
|
|
||||||
color: $navy-950;
|
|
||||||
font-size: $fs-overline;
|
|
||||||
font-weight: 700;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navCollapsed .badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 6px;
|
|
||||||
right: 8px;
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import styles from './SideNav.module.scss';
|
|
||||||
import { Icon, IconName } from '@tnsu/ui-kit';
|
|
||||||
import { NavSection } from '../../shared/routing';
|
|
||||||
|
|
||||||
interface INavItem {
|
|
||||||
id: NavSection;
|
|
||||||
title: string;
|
|
||||||
icon: IconName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Порядок неслучаен: сверху действие (создать), затем три очереди — то, что
|
|
||||||
// ждёт пользователя прямо сейчас, — и только потом архив и поиск.
|
|
||||||
const ITEMS: INavItem[] = [
|
|
||||||
{ id: 'new', title: 'Новая заявка', icon: 'plus' },
|
|
||||||
{ id: 'approvals', title: 'На согласовании', icon: 'user-check' },
|
|
||||||
{ id: 'execution', title: 'На исполнении', icon: 'helmet' },
|
|
||||||
{ id: 'rework', title: 'Доработка', icon: 'edit' },
|
|
||||||
{ id: 'own', title: 'Мои заявки', icon: 'clipboard' },
|
|
||||||
{ id: 'search', title: 'Поиск', icon: 'search' },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface ISideNavProps {
|
|
||||||
activeId: NavSection;
|
|
||||||
onChange: (id: NavSection) => void;
|
|
||||||
/** Счётчики очередей: сколько заявок ждёт пользователя в каждом разделе. */
|
|
||||||
counts?: Partial<Record<NavSection, number>>;
|
|
||||||
collapsed: boolean;
|
|
||||||
onToggleCollapsed: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Тёмный боковой рейл портала на всю высоту: бренд сверху, разделы снизу.
|
|
||||||
// Сворачивается до узкой полосы с одними иконками.
|
|
||||||
export const SideNav: React.FC<ISideNavProps> = ({
|
|
||||||
activeId,
|
|
||||||
onChange,
|
|
||||||
counts,
|
|
||||||
collapsed,
|
|
||||||
onToggleCollapsed,
|
|
||||||
}) => {
|
|
||||||
const navClasses = [styles.nav, collapsed ? styles.navCollapsed : ''].filter(Boolean).join(' ');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav className={navClasses} aria-label="Разделы портала">
|
|
||||||
<div className={styles.head}>
|
|
||||||
<div className={styles.brand}>
|
|
||||||
{!collapsed && <span className={styles.brandText}>Портал заявок</span>}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.toggle}
|
|
||||||
onClick={onToggleCollapsed}
|
|
||||||
aria-label={collapsed ? 'Развернуть меню' : 'Свернуть меню'}
|
|
||||||
title={collapsed ? 'Развернуть меню' : 'Свернуть меню'}
|
|
||||||
>
|
|
||||||
<Icon name={collapsed ? 'chevron-right' : 'chevron-left'} size="sm" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.items}>
|
|
||||||
{ITEMS.map((item) => {
|
|
||||||
const isActive = item.id === activeId;
|
|
||||||
const itemClasses = [styles.item, isActive ? styles.itemActive : ''].filter(Boolean).join(' ');
|
|
||||||
const count = counts?.[item.id];
|
|
||||||
const showCount = !!count;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={item.id}
|
|
||||||
type="button"
|
|
||||||
className={itemClasses}
|
|
||||||
aria-current={isActive ? 'page' : undefined}
|
|
||||||
onClick={() => onChange(item.id)}
|
|
||||||
title={collapsed ? item.title : undefined}
|
|
||||||
>
|
|
||||||
<span className={styles.itemIcon}>
|
|
||||||
<Icon name={item.icon} size="sm" />
|
|
||||||
</span>
|
|
||||||
{!collapsed && <span className={styles.itemTitle}>{item.title}</span>}
|
|
||||||
{showCount && <span className={styles.badge}>{count}</span>}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
@import '../../../../../styles/design-tokens';
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 5px 12px;
|
|
||||||
border-radius: $r-sm;
|
|
||||||
font-family: $font-sans;
|
|
||||||
font-size: 12.5px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: .2px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Компактный вариант — для ячейки таблицы (RequestsTableView/SearchView),
|
|
||||||
// повторяет размеры, что были у их собственной .status до объединения.
|
|
||||||
.sm {
|
|
||||||
padding: 3px 10px;
|
|
||||||
border-radius: $r-md;
|
|
||||||
font-size: $fs-caption;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.draft {
|
|
||||||
background: $gray-100;
|
|
||||||
color: $gray-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queued {
|
|
||||||
background: $gray-100;
|
|
||||||
color: $gray-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.onApproval {
|
|
||||||
background: $blue-50;
|
|
||||||
color: $blue-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.approved {
|
|
||||||
background: $green-50;
|
|
||||||
color: $green-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rejected {
|
|
||||||
background: $red-50;
|
|
||||||
color: $red-500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.returned {
|
|
||||||
background: $gold-50;
|
|
||||||
color: $amber-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.execution {
|
|
||||||
background: $navy-50;
|
|
||||||
color: $navy-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.closed {
|
|
||||||
background: $gray-100;
|
|
||||||
color: $gray-500;
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,33 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styles from './StatusBadge.module.scss';
|
import { Badge, BadgeVariant } from '@tnsu/ui-kit';
|
||||||
import { REQUEST_STATUS } from '../models/statuses';
|
import { REQUEST_STATUS } from '../models/statuses';
|
||||||
|
|
||||||
// Цвет бейджа по статусу заявки. Неизвестный статус → нейтральный (как черновик).
|
// Единственное место, где СТАТУС ЗАЯВКИ превращается в цвет. Сам цвет живёт в
|
||||||
const VARIANT: Record<string, string> = {
|
// generic Badge (@tnsu/ui-kit) — здесь только смысловой маппинг «статус → variant».
|
||||||
[REQUEST_STATUS.draft]: styles.draft,
|
// Раньше у таблиц была своя копия этой раскраски (statusClass) — она разошлась с
|
||||||
[REQUEST_STATUS.queued]: styles.queued,
|
// этой, поэтому обе копии убраны в пользу общего компонента.
|
||||||
[REQUEST_STATUS.onApproval]: styles.onApproval,
|
const STATUS_VARIANT: Record<string, BadgeVariant> = {
|
||||||
[REQUEST_STATUS.onExecution]: styles.onApproval, // активный шаг — как «на согласовании»
|
[REQUEST_STATUS.draft]: 'neutral',
|
||||||
[REQUEST_STATUS.approved]: styles.approved,
|
[REQUEST_STATUS.queued]: 'muted',
|
||||||
[REQUEST_STATUS.executed]: styles.approved, // выполнено — как «согласовано» (зелёный)
|
[REQUEST_STATUS.onApproval]: 'info',
|
||||||
[REQUEST_STATUS.rejected]: styles.rejected,
|
[REQUEST_STATUS.onExecution]: 'info', // активный шаг — как «на согласовании»
|
||||||
[REQUEST_STATUS.returned]: styles.returned,
|
[REQUEST_STATUS.approved]: 'success',
|
||||||
[REQUEST_STATUS.execution]: styles.execution,
|
[REQUEST_STATUS.executed]: 'success', // выполнено — как «согласовано» (зелёный)
|
||||||
[REQUEST_STATUS.closed]: styles.closed,
|
[REQUEST_STATUS.rejected]: 'danger',
|
||||||
|
[REQUEST_STATUS.returned]: 'warning',
|
||||||
|
[REQUEST_STATUS.execution]: 'primary',
|
||||||
|
[REQUEST_STATUS.closed]: 'muted'
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IStatusBadgeProps {
|
export interface IStatusBadgeProps {
|
||||||
status: string;
|
status: string;
|
||||||
/**
|
/** 'md' (по умолчанию) — шапка формы; 'sm' — компактный для ячейки таблицы. */
|
||||||
* 'md' (по умолчанию) — шапка формы/просмотра заявки; 'sm' — компактный
|
|
||||||
* вариант для ячейки таблицы (RequestsTableView/SearchView). Цвет статуса
|
|
||||||
* не зависит от размера — единственный источник цвета: VARIANT выше.
|
|
||||||
*/
|
|
||||||
size?: 'sm' | 'md';
|
size?: 'sm' | 'md';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Цветная плашка статуса заявки поверх generic Badge из @tnsu/ui-kit. */
|
||||||
* Цветная плашка статуса заявки — ЕДИНСТВЕННОЕ место, где статус превращается
|
|
||||||
* в цвет. У таблиц раньше была своя копия этой раскраски (statusClass в
|
|
||||||
* RequestsTableView.tsx/SearchView.tsx) — она разошлась с этой (на согласовании
|
|
||||||
* и возвращена на доработку оказались перепутаны местами), поэтому обе копии
|
|
||||||
* убраны в пользу общего компонента.
|
|
||||||
*/
|
|
||||||
export const StatusBadge: React.FC<IStatusBadgeProps> = ({ status, size = 'md' }) => (
|
export const StatusBadge: React.FC<IStatusBadgeProps> = ({ status, size = 'md' }) => (
|
||||||
<span className={`${styles.badge} ${size === 'sm' ? styles.sm : ''} ${VARIANT[status] || styles.draft}`}>
|
<Badge variant={STATUS_VARIANT[status] || 'neutral'} size={size}>
|
||||||
{status}
|
{status}
|
||||||
</span>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user