feat: changed folder structure for the secod dev
This commit is contained in:
@@ -27,58 +27,45 @@
|
||||
padding: $sp-4 $sp-5 $sp-8;
|
||||
}
|
||||
|
||||
// Центрированная колонка контента. 1080px — ширина контента из дизайн-системы
|
||||
// (как .sec в референсе): форма и плитки читаются аккуратно, не растягиваясь.
|
||||
.content {
|
||||
width: 100%;
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.teams {
|
||||
// Задел под специфику Teams-контекста; пока совпадает с базовым.
|
||||
font-family: $font-sans;
|
||||
}
|
||||
|
||||
// ── Заглушка формы (до появления реальных форм) ──
|
||||
.formPlaceholder {
|
||||
margin-top: $sp-6;
|
||||
padding: $sp-8;
|
||||
background: $white;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: $r-lg;
|
||||
box-shadow: $sh-1;
|
||||
// Двухколоночная раскладка: формы (main) + постоянная панель (side).
|
||||
.layout {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $sp-6;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $sp-2;
|
||||
margin-bottom: $sp-4;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: $navy-700;
|
||||
font-family: $font-sans;
|
||||
font-size: $fs-sm;
|
||||
cursor: pointer;
|
||||
transition: color .18s;
|
||||
.main {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0; // чтобы контент не распирал колонку
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $gold-600;
|
||||
.side {
|
||||
flex: 0 0 380px;
|
||||
position: sticky;
|
||||
top: $sp-4;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
// На узких экранах панель уходит под формы.
|
||||
@media (max-width: 900px) {
|
||||
.layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.side {
|
||||
flex-basis: auto;
|
||||
width: 100%;
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholderText {
|
||||
font-family: $font-serif;
|
||||
font-size: 18px;
|
||||
color: $navy-900;
|
||||
}
|
||||
|
||||
.loading {
|
||||
margin-top: $sp-6;
|
||||
padding: $sp-8;
|
||||
color: $gray-500;
|
||||
font-size: $fs-sm;
|
||||
text-align: center;
|
||||
}
|
||||
// Стили конкретных фич живут в features/<name>/<Name>.module.scss.
|
||||
// Здесь — только оболочка (root + раскладка колонок), общая для всех фич.
|
||||
|
||||
@@ -1,72 +1,28 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import styles from './RequestPortal.module.scss';
|
||||
import type { IRequestPortalProps } from './IRequestPortalProps';
|
||||
import { IconSprite } from '../shared/ui/Icon/IconSprite';
|
||||
import { Icon } from '../shared/ui/Icon/Icon';
|
||||
import { CategoryTabs } from './CategoryTabs/CategoryTabs';
|
||||
import { RequestGrid } from './RequestGrid/RequestGrid';
|
||||
import { CATEGORIES, DEFAULT_CATEGORY_ID } from '../data/categories';
|
||||
import { getRequestsByCategory } from '../data/requests';
|
||||
import { getFormComponent } from '../forms/registry';
|
||||
import { IRequestType } from '../models/types';
|
||||
import SubmitFeature from '../features/submit/SubmitFeature';
|
||||
|
||||
// Оболочка портала. Логики фич тут нет — файл общий, меняется редко.
|
||||
//
|
||||
// Пока в UI только формы подачи (features/submit). Боковая панель «Мои заявки»
|
||||
// (features/myRequests) готова как компонент, но НЕ смонтирована — её подключает
|
||||
// второй разработчик, когда наполнит. Для этого:
|
||||
// 1) import MyRequestsPanel from '../features/myRequests/MyRequestsPanel';
|
||||
// 2) обернуть контент в <div className={styles.layout}> с двумя колонками:
|
||||
// <main className={styles.main}><SubmitFeature .../></main>
|
||||
// <div className={styles.side}><MyRequestsPanel context={context} /></div>
|
||||
// (классы layout/main/side уже есть в RequestPortal.module.scss)
|
||||
const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context }) => {
|
||||
const [activeCategoryId, setActiveCategoryId] = useState<string>(DEFAULT_CATEGORY_ID);
|
||||
const [selectedRequest, setSelectedRequest] = useState<IRequestType | null>(null);
|
||||
|
||||
const rootClasses = [styles.requestPortal, hasTeamsContext ? styles.teams : '']
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const requests = getRequestsByCategory(activeCategoryId);
|
||||
|
||||
const handleCategoryChange = (categoryId: string): void => {
|
||||
setActiveCategoryId(categoryId);
|
||||
setSelectedRequest(null); // при смене категории возвращаемся к списку плиток
|
||||
};
|
||||
|
||||
const handleBack = (): void => setSelectedRequest(null);
|
||||
|
||||
const renderSelected = (request: IRequestType): React.ReactNode => {
|
||||
const FormComponent = getFormComponent(request.id);
|
||||
|
||||
// Форма ещё не реализована — показываем заглушку.
|
||||
if (!FormComponent) {
|
||||
return (
|
||||
<div className={styles.formPlaceholder}>
|
||||
<button type="button" className={styles.backButton} onClick={handleBack}>
|
||||
<Icon name="arrow-left" size="sm" />
|
||||
<span>К списку заявок</span>
|
||||
</button>
|
||||
<p className={styles.placeholderText}>Форма «{request.title}» ещё в разработке.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Ленивый рендер формы: её код подтягивается только сейчас, при открытии.
|
||||
return (
|
||||
<React.Suspense fallback={<div className={styles.loading}>Загрузка формы…</div>}>
|
||||
<FormComponent requestType={request} context={context} onCancel={handleBack} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={rootClasses}>
|
||||
<IconSprite />
|
||||
<div className={styles.content}>
|
||||
<CategoryTabs
|
||||
categories={CATEGORIES}
|
||||
activeId={activeCategoryId}
|
||||
onChange={handleCategoryChange}
|
||||
/>
|
||||
|
||||
{selectedRequest ? (
|
||||
renderSelected(selectedRequest)
|
||||
) : (
|
||||
<RequestGrid requests={requests} onSelect={setSelectedRequest} />
|
||||
)}
|
||||
</div>
|
||||
<SubmitFeature context={context} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
@import '../../../../styles/design-tokens';
|
||||
|
||||
// Постоянная боковая панель (всегда открыта). Раскладку колонок задаёт корень;
|
||||
// здесь — только внутреннее оформление панели.
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: $white;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: $r-lg;
|
||||
box-shadow: $sh-1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: $sp-4 $sp-5;
|
||||
border-bottom: 1px solid $gray-200;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: $font-serif;
|
||||
font-size: 18px;
|
||||
color: $navy-900;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: $sp-5;
|
||||
color: $gray-500;
|
||||
font-size: $fs-sm;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import styles from './MyRequestsPanel.module.scss';
|
||||
|
||||
export interface IMyRequestsPanelProps {
|
||||
context: WebPartContext;
|
||||
}
|
||||
|
||||
// Постоянная боковая панель «Мои заявки»: пользователь видит, что он отправлял
|
||||
// и согласовывал. Всегда открыта (без кнопки/оверлея).
|
||||
// Владелец: второй разработчик. Вся логика/данные — внутри этой папки.
|
||||
// Общий UI-кит (shared/ui), pnpjs и config можно импортировать, но НЕ менять.
|
||||
const MyRequestsPanel: React.FC<IMyRequestsPanelProps> = (/* { context } */) => {
|
||||
return (
|
||||
<aside className={styles.panel} aria-label="Мои заявки">
|
||||
<div className={styles.header}>
|
||||
<span className={styles.title}>Мои заявки</span>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
Раздел в разработке: здесь будут заявки пользователя, статусы и согласования.
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyRequestsPanel;
|
||||
@@ -0,0 +1,52 @@
|
||||
@import '../../../../styles/design-tokens';
|
||||
|
||||
// Центрированная колонка контента. 1080px — ширина контента из дизайн-системы
|
||||
// (как .sec в референсе): форма и плитки читаются аккуратно, не растягиваясь.
|
||||
.content {
|
||||
width: 100%;
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// ── Заглушка формы (до появления реальных форм) ──
|
||||
.formPlaceholder {
|
||||
margin-top: $sp-6;
|
||||
padding: $sp-8;
|
||||
background: $white;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: $r-lg;
|
||||
box-shadow: $sh-1;
|
||||
}
|
||||
|
||||
.backButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $sp-2;
|
||||
margin-bottom: $sp-4;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: $navy-700;
|
||||
font-family: $font-sans;
|
||||
font-size: $fs-sm;
|
||||
cursor: pointer;
|
||||
transition: color .18s;
|
||||
|
||||
&:hover {
|
||||
color: $gold-600;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholderText {
|
||||
font-family: $font-serif;
|
||||
font-size: 18px;
|
||||
color: $navy-900;
|
||||
}
|
||||
|
||||
.loading {
|
||||
margin-top: $sp-6;
|
||||
padding: $sp-8;
|
||||
color: $gray-500;
|
||||
font-size: $fs-sm;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import styles from './SubmitFeature.module.scss';
|
||||
import { Icon } from '../../shared/ui/Icon/Icon';
|
||||
import { CategoryTabs } from '../../components/CategoryTabs/CategoryTabs';
|
||||
import { RequestGrid } from '../../components/RequestGrid/RequestGrid';
|
||||
import { CATEGORIES, DEFAULT_CATEGORY_ID } from '../../data/categories';
|
||||
import { getRequestsByCategory } from '../../data/requests';
|
||||
import { getFormComponent } from '../../forms/registry';
|
||||
import { IRequestType } from '../../models/types';
|
||||
|
||||
export interface ISubmitFeatureProps {
|
||||
context: WebPartContext;
|
||||
}
|
||||
|
||||
// Фича «Подать заявку»: категории → плитки → беспоук-форма.
|
||||
// Владелец: автор портала. Живёт целиком в features/submit/**.
|
||||
const SubmitFeature: React.FC<ISubmitFeatureProps> = ({ context }) => {
|
||||
const [activeCategoryId, setActiveCategoryId] = useState<string>(DEFAULT_CATEGORY_ID);
|
||||
const [selectedRequest, setSelectedRequest] = useState<IRequestType | null>(null);
|
||||
|
||||
const requests = getRequestsByCategory(activeCategoryId);
|
||||
|
||||
const handleCategoryChange = (categoryId: string): void => {
|
||||
setActiveCategoryId(categoryId);
|
||||
setSelectedRequest(null); // при смене категории возвращаемся к списку плиток
|
||||
};
|
||||
|
||||
const handleBack = (): void => setSelectedRequest(null);
|
||||
|
||||
const renderSelected = (request: IRequestType): React.ReactNode => {
|
||||
const FormComponent = getFormComponent(request.id);
|
||||
|
||||
// Форма ещё не реализована — показываем заглушку.
|
||||
if (!FormComponent) {
|
||||
return (
|
||||
<div className={styles.formPlaceholder}>
|
||||
<button type="button" className={styles.backButton} onClick={handleBack}>
|
||||
<Icon name="arrow-left" size="sm" />
|
||||
<span>К списку заявок</span>
|
||||
</button>
|
||||
<p className={styles.placeholderText}>Форма «{request.title}» ещё в разработке.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Ленивый рендер формы: её код подтягивается только сейчас, при открытии.
|
||||
return (
|
||||
<React.Suspense fallback={<div className={styles.loading}>Загрузка формы…</div>}>
|
||||
<FormComponent requestType={request} context={context} onCancel={handleBack} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<CategoryTabs
|
||||
categories={CATEGORIES}
|
||||
activeId={activeCategoryId}
|
||||
onChange={handleCategoryChange}
|
||||
/>
|
||||
|
||||
{selectedRequest ? (
|
||||
renderSelected(selectedRequest)
|
||||
) : (
|
||||
<RequestGrid requests={requests} onSelect={setSelectedRequest} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubmitFeature;
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dataListTitle": "Согласование — Прием на работу",
|
||||
"dataListTitle": "Прием на работу — согласование",
|
||||
"columns": [
|
||||
{ "internalName": "Approver", "title": "Согласующий", "type": "User", "userSelectionMode": "PeopleOnly" },
|
||||
{ "internalName": "Status", "title": "Статус", "type": "Choice", "choices": ["Черновик", "На согласовании", "Согласовано", "Отклонено", "Возвращена на доработку", "Выполнение", "Закрыта"] },
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ApprovalListService, IApproverToWrite, writeApprovers } from '../../sha
|
||||
import { IFormData } from './model';
|
||||
|
||||
const LIST_TITLE = 'Прием на работу — данные';
|
||||
export const APPROVAL_LIST_TITLE = 'Согласование — Прием на работу';
|
||||
export const APPROVAL_LIST_TITLE = 'Прием на работу — согласование';
|
||||
// Тип заявки «Заявка на прием» во внешнем approval-route API
|
||||
// (GET /api/request-types → id 5). Захардкожен, как «отпуск = 1» в pto-tracker.
|
||||
export const HIRE_REQUEST_TYPE_ID = 5;
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface IApproverToWrite {
|
||||
allowReassign: boolean;
|
||||
}
|
||||
|
||||
// Все запросы к списку «Согласование — <форма>». listTitle задаётся формой.
|
||||
// Все запросы к списку «<форма> — согласование». listTitle задаётся формой.
|
||||
export class ApprovalListService {
|
||||
constructor(private readonly listTitle: string) {}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Строка согласования из списка «Согласование — <форма>».
|
||||
// Строка согласования из списка «<форма> — согласование».
|
||||
export interface IApprovalRow {
|
||||
id: number;
|
||||
status: string;
|
||||
|
||||
Reference in New Issue
Block a user