feat: changed folder structure for the secod dev
This commit is contained in:
+136
@@ -0,0 +1,136 @@
|
|||||||
|
# Портал заявок — план работ
|
||||||
|
|
||||||
|
Живая карта проекта: где мы, что осталось, как делать. Отмечай сделанное галочками.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что это за проект (в одну строку)
|
||||||
|
|
||||||
|
SPFx web part: **категории → плитки заявок → форма**. Форма пишет данные в списки
|
||||||
|
SharePoint; у части заявок есть **согласование** (маршрут утверждающих).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Что уже готово ✅
|
||||||
|
|
||||||
|
**Каркас и общее**
|
||||||
|
- [x] UI-поток: категории → плитки → форма (центрированная колонка 1080px, full-width канвы)
|
||||||
|
- [x] Переиспользуемые примитивы формы (`shared/ui`): Button, FormCard, FormField, FormGrid,
|
||||||
|
TextInput, TextArea, Select, Toggle, CollapseSection, FormActions, Alert, Icon,
|
||||||
|
PeopleField / PeopleFieldMulti
|
||||||
|
- [x] Ленивая загрузка форм (реестр `forms/registry.ts`, каждая форма — отдельный чанк)
|
||||||
|
- [x] PnPjs подключён (`shared/pnp/pnpjs.ts`), клиент нацелен на сайт данных `DATA_SITE_URL`
|
||||||
|
|
||||||
|
**Провижининг (создание списков)**
|
||||||
|
- [x] Инструмент `provisioning/` — из `provisioning.json` создаёт списки/колонки идемпотентно
|
||||||
|
- [x] Поддержка типов: Text, Note, Number(точность), DateTime(только дата), Boolean, Choice,
|
||||||
|
Lookup, User (одиночный/мульти, «только люди»)
|
||||||
|
- [x] Доступ: приложение `Sites.Selected`, заперто на сайт `/sites/requests`
|
||||||
|
- [x] Правило именования: `<Название заявки> — данные` / `<Название заявки> — согласование`
|
||||||
|
|
||||||
|
**Формы**
|
||||||
|
- [x] **Заявка на подбор** (`recruiting-selection`) — 42 поля, пишет в SP. Без согласования.
|
||||||
|
- [x] **Заявка на прием** (`recruiting-hire`) — с согласованием (сторона автора).
|
||||||
|
|
||||||
|
**Согласование (`shared/approval`)**
|
||||||
|
- [x] Внешний API маршрутов (`approvalRoute.service.ts`, AadHttpClient/SSO)
|
||||||
|
- [x] Матрица согласующих на форме (`ApprovalMatrix`, `useMatrixEditor`)
|
||||||
|
- [x] Запись строк согласования в SP-список (`approvalList.service`, `approvalSubmission.service`)
|
||||||
|
- [x] Сторона автора: «Сохранить черновик» / «Отправить на согласование»
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Что НЕ готово ⏳ (главные пробелы)
|
||||||
|
|
||||||
|
- [ ] **Экран согласующего** — логика есть (`approverDecision.service.ts`: согласовать/вернуть/
|
||||||
|
отклонить, делегирование, пересчёт статуса), но НЕТ UI и маршрутизации «мои согласования →
|
||||||
|
открыть заявку → решение».
|
||||||
|
- [ ] **8 форм** ещё не сделаны (см. таблицу ниже).
|
||||||
|
- [ ] **Деплой**: web part пока только в workbench; не упакован в каталог приложений.
|
||||||
|
- [ ] **Конфиг**: `DATA_SITE_URL` и id API захардкожены (потом — в property pane).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. План по фазам (предлагаемый порядок)
|
||||||
|
|
||||||
|
### Фаза A — Закрыть согласование целиком
|
||||||
|
Довести до конца одну сквозную вертикаль, чтобы механизм был полным.
|
||||||
|
- [ ] Экран «Мои согласования» (список заявок, ждущих моего решения)
|
||||||
|
- [ ] Режим просмотра заявки для согласующего (read-only форма + панель решения)
|
||||||
|
- [ ] Кнопки решения: Согласовать / Вернуть / Отклонить + комментарий
|
||||||
|
- [ ] (опц.) Делегирование
|
||||||
|
- [ ] Проверка статусов end-to-end на «Заявке на прием»
|
||||||
|
|
||||||
|
### Фаза B — Согласование для «Заявки на подбор»
|
||||||
|
- [ ] Список «Заявка на подбор — согласование» (манифест + провижининг)
|
||||||
|
- [ ] Подключить матрицу и submit-поток к форме подбора (id типа = 4)
|
||||||
|
|
||||||
|
### Фаза C — Остальные формы (конвейер)
|
||||||
|
Для каждой формы по «рецепту одной формы» (раздел 6). Порядок — по важности для бизнеса.
|
||||||
|
- [ ] IT: Организация рабочего места
|
||||||
|
- [ ] Бухгалтерия: Аванс
|
||||||
|
- [ ] Отдел кадров: Трудовой отпуск
|
||||||
|
- [ ] Отдел кадров: Отгул
|
||||||
|
- [ ] Отдел кадров: Увольнение
|
||||||
|
- [ ] Отдел кадров: Обходной лист
|
||||||
|
- [ ] Отдел кадров: БС
|
||||||
|
- [ ] Отдел кадров: Командировка
|
||||||
|
|
||||||
|
### Фаза D — Развёртывание и настройки
|
||||||
|
- [ ] Вынести `DATA_SITE_URL` и id API в property pane
|
||||||
|
- [ ] Одобрить web API permission для approval API (SharePoint admin → API access)
|
||||||
|
- [ ] Упаковать (`heft package-solution`) и выкатить в каталог приложений
|
||||||
|
- [ ] Разместить web part на странице портала
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Справочники
|
||||||
|
|
||||||
|
### Формы и их статус
|
||||||
|
|
||||||
|
| Категория | Заявка | id (реестр) | id API | Форма | Согласование |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| Рекрутинг | Заявка на подбор | recruiting-selection | 4 | ✅ | ⏳ (Фаза B) |
|
||||||
|
| Рекрутинг | Заявка на прием | recruiting-hire | 5 | ✅ | ✅ автор / ⏳ согласующий |
|
||||||
|
| IT | Организация раб. места | it-workplace-setup | — | ⏳ | ? |
|
||||||
|
| Бухгалтерия | Аванс | accounting-advance | — | ⏳ | ? |
|
||||||
|
| Отдел кадров | Трудовой отпуск | hr-labor-leave | — | ⏳ | ? |
|
||||||
|
| Отдел кадров | Отгул | hr-day-off | — | ⏳ | ? |
|
||||||
|
| Отдел кадров | Увольнение | hr-dismissal | — | ⏳ | ? |
|
||||||
|
| Отдел кадров | Обходной лист | hr-clearance-sheet | — | ⏳ | ? |
|
||||||
|
| Отдел кадров | БС | hr-bs | — | ⏳ | ? |
|
||||||
|
| Отдел кадров | Командировка | hr-business-trip | — | ⏳ | ? |
|
||||||
|
|
||||||
|
> «?» в согласовании — решить по каждой заявке: нужно ли ей согласование вообще.
|
||||||
|
|
||||||
|
### Списки в SharePoint (`/sites/requests`)
|
||||||
|
- `Заявка на подбор — данные` ✅
|
||||||
|
- `Прием на работу — данные` ✅
|
||||||
|
- `Прием на работу — согласование` ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Рецепт одной формы (чеклист)
|
||||||
|
|
||||||
|
Как добавить новую заявку — всегда одни и те же шаги:
|
||||||
|
|
||||||
|
1. **Данные (список):** создать `forms/<Форма>/provisioning.json` (колонки) → прогнать
|
||||||
|
`provisioning/Provision-Lists.ps1` → появится список `<Название> — данные`.
|
||||||
|
2. **Согласование (если нужно):** `forms/<Форма>/approval/provisioning.json` → список
|
||||||
|
`<Название> — согласование`.
|
||||||
|
3. **Форма (UI):**
|
||||||
|
- `forms/<Форма>/model.ts` — `IFormData` (ключи = internalName колонок), `EMPTY_DATA`
|
||||||
|
- `forms/<Форма>/sections/*.tsx` — секции из примитивов `shared/ui`
|
||||||
|
- `forms/<Форма>/<Форма>Form.tsx` — сборка секций + валидация + кнопки
|
||||||
|
- `forms/<Форма>/submit.ts` — запись в SP (маппинг типов, люди через ensureUser)
|
||||||
|
4. **Реестр:** строка в `forms/registry.ts` (`'<id>': React.lazy(...)`).
|
||||||
|
5. **Плитка:** уже есть в `data/requests.ts` (id совпадает с реестром).
|
||||||
|
6. **Проверка:** `heft build` → тест на hosted workbench сайта `/sites/requests`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Открытые вопросы (решить по ходу)
|
||||||
|
|
||||||
|
- Каким заявкам нужно согласование, а каким нет?
|
||||||
|
- Порядок Фазы C (какие формы делаем первыми)?
|
||||||
|
- Нужен ли реестр/дашборд поданных заявок (просмотр своих заявок и их статусов)?
|
||||||
@@ -27,58 +27,45 @@
|
|||||||
padding: $sp-4 $sp-5 $sp-8;
|
padding: $sp-4 $sp-5 $sp-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Центрированная колонка контента. 1080px — ширина контента из дизайн-системы
|
|
||||||
// (как .sec в референсе): форма и плитки читаются аккуратно, не растягиваясь.
|
|
||||||
.content {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1080px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.teams {
|
.teams {
|
||||||
// Задел под специфику Teams-контекста; пока совпадает с базовым.
|
// Задел под специфику Teams-контекста; пока совпадает с базовым.
|
||||||
font-family: $font-sans;
|
font-family: $font-sans;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Заглушка формы (до появления реальных форм) ──
|
// Двухколоночная раскладка: формы (main) + постоянная панель (side).
|
||||||
.formPlaceholder {
|
.layout {
|
||||||
margin-top: $sp-6;
|
display: flex;
|
||||||
padding: $sp-8;
|
align-items: flex-start;
|
||||||
background: $white;
|
gap: $sp-6;
|
||||||
border: 1px solid $gray-200;
|
width: 100%;
|
||||||
border-radius: $r-lg;
|
max-width: 1400px;
|
||||||
box-shadow: $sh-1;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backButton {
|
.main {
|
||||||
display: inline-flex;
|
flex: 1 1 auto;
|
||||||
align-items: center;
|
min-width: 0; // чтобы контент не распирал колонку
|
||||||
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 {
|
.side {
|
||||||
color: $gold-600;
|
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 {
|
// Стили конкретных фич живут в features/<name>/<Name>.module.scss.
|
||||||
font-family: $font-serif;
|
// Здесь — только оболочка (root + раскладка колонок), общая для всех фич.
|
||||||
font-size: 18px;
|
|
||||||
color: $navy-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
margin-top: $sp-6;
|
|
||||||
padding: $sp-8;
|
|
||||||
color: $gray-500;
|
|
||||||
font-size: $fs-sm;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,72 +1,28 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState } from 'react';
|
|
||||||
import styles from './RequestPortal.module.scss';
|
import styles from './RequestPortal.module.scss';
|
||||||
import type { IRequestPortalProps } from './IRequestPortalProps';
|
import type { IRequestPortalProps } from './IRequestPortalProps';
|
||||||
import { IconSprite } from '../shared/ui/Icon/IconSprite';
|
import { IconSprite } from '../shared/ui/Icon/IconSprite';
|
||||||
import { Icon } from '../shared/ui/Icon/Icon';
|
import SubmitFeature from '../features/submit/SubmitFeature';
|
||||||
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';
|
|
||||||
|
|
||||||
|
// Оболочка портала. Логики фич тут нет — файл общий, меняется редко.
|
||||||
|
//
|
||||||
|
// Пока в 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 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 : '']
|
const rootClasses = [styles.requestPortal, hasTeamsContext ? styles.teams : '']
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.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 (
|
return (
|
||||||
<section className={rootClasses}>
|
<section className={rootClasses}>
|
||||||
<IconSprite />
|
<IconSprite />
|
||||||
<div className={styles.content}>
|
<SubmitFeature context={context} />
|
||||||
<CategoryTabs
|
|
||||||
categories={CATEGORIES}
|
|
||||||
activeId={activeCategoryId}
|
|
||||||
onChange={handleCategoryChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{selectedRequest ? (
|
|
||||||
renderSelected(selectedRequest)
|
|
||||||
) : (
|
|
||||||
<RequestGrid requests={requests} onSelect={setSelectedRequest} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
</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": [
|
"columns": [
|
||||||
{ "internalName": "Approver", "title": "Согласующий", "type": "User", "userSelectionMode": "PeopleOnly" },
|
{ "internalName": "Approver", "title": "Согласующий", "type": "User", "userSelectionMode": "PeopleOnly" },
|
||||||
{ "internalName": "Status", "title": "Статус", "type": "Choice", "choices": ["Черновик", "На согласовании", "Согласовано", "Отклонено", "Возвращена на доработку", "Выполнение", "Закрыта"] },
|
{ "internalName": "Status", "title": "Статус", "type": "Choice", "choices": ["Черновик", "На согласовании", "Согласовано", "Отклонено", "Возвращена на доработку", "Выполнение", "Закрыта"] },
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ApprovalListService, IApproverToWrite, writeApprovers } from '../../sha
|
|||||||
import { IFormData } from './model';
|
import { IFormData } from './model';
|
||||||
|
|
||||||
const LIST_TITLE = 'Прием на работу — данные';
|
const LIST_TITLE = 'Прием на работу — данные';
|
||||||
export const APPROVAL_LIST_TITLE = 'Согласование — Прием на работу';
|
export const APPROVAL_LIST_TITLE = 'Прием на работу — согласование';
|
||||||
// Тип заявки «Заявка на прием» во внешнем approval-route API
|
// Тип заявки «Заявка на прием» во внешнем approval-route API
|
||||||
// (GET /api/request-types → id 5). Захардкожен, как «отпуск = 1» в pto-tracker.
|
// (GET /api/request-types → id 5). Захардкожен, как «отпуск = 1» в pto-tracker.
|
||||||
export const HIRE_REQUEST_TYPE_ID = 5;
|
export const HIRE_REQUEST_TYPE_ID = 5;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export interface IApproverToWrite {
|
|||||||
allowReassign: boolean;
|
allowReassign: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Все запросы к списку «Согласование — <форма>». listTitle задаётся формой.
|
// Все запросы к списку «<форма> — согласование». listTitle задаётся формой.
|
||||||
export class ApprovalListService {
|
export class ApprovalListService {
|
||||||
constructor(private readonly listTitle: string) {}
|
constructor(private readonly listTitle: string) {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Строка согласования из списка «Согласование — <форма>».
|
// Строка согласования из списка «<форма> — согласование».
|
||||||
export interface IApprovalRow {
|
export interface IApprovalRow {
|
||||||
id: number;
|
id: number;
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user