From 788e046ebe1ca70e7b615fbc65f98559bb3b2068 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: Mon, 3 Aug 2026 12:06:48 +0500 Subject: [PATCH] feat: changed folder structure for the secod dev --- ROADMAP.md | 136 ++++++++++++++++++ .../components/RequestPortal.module.scss | 75 ++++------ .../components/RequestPortal.tsx | 68 ++------- .../myRequests/MyRequestsPanel.module.scss | 33 +++++ .../features/myRequests/MyRequestsPanel.tsx | 26 ++++ .../features/submit/SubmitFeature.module.scss | 52 +++++++ .../features/submit/SubmitFeature.tsx | 73 ++++++++++ .../RecruitingHire/approval/provisioning.json | 2 +- .../forms/RecruitingHire/submit.ts | 2 +- .../shared/approval/approvalList.service.ts | 2 +- .../shared/approval/models/IApprovalRow.ts | 2 +- 11 files changed, 367 insertions(+), 104 deletions(-) create mode 100644 ROADMAP.md create mode 100644 src/webparts/requestPortal/features/myRequests/MyRequestsPanel.module.scss create mode 100644 src/webparts/requestPortal/features/myRequests/MyRequestsPanel.tsx create mode 100644 src/webparts/requestPortal/features/submit/SubmitFeature.module.scss create mode 100644 src/webparts/requestPortal/features/submit/SubmitFeature.tsx diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..8fa9dd0 --- /dev/null +++ b/ROADMAP.md @@ -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` (`'': React.lazy(...)`). +5. **Плитка:** уже есть в `data/requests.ts` (id совпадает с реестром). +6. **Проверка:** `heft build` → тест на hosted workbench сайта `/sites/requests`. + +--- + +## 7. Открытые вопросы (решить по ходу) + +- Каким заявкам нужно согласование, а каким нет? +- Порядок Фазы C (какие формы делаем первыми)? +- Нужен ли реестр/дашборд поданных заявок (просмотр своих заявок и их статусов)? diff --git a/src/webparts/requestPortal/components/RequestPortal.module.scss b/src/webparts/requestPortal/components/RequestPortal.module.scss index 44f948f..eba25ac 100644 --- a/src/webparts/requestPortal/components/RequestPortal.module.scss +++ b/src/webparts/requestPortal/components/RequestPortal.module.scss @@ -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//.module.scss. +// Здесь — только оболочка (root + раскладка колонок), общая для всех фич. diff --git a/src/webparts/requestPortal/components/RequestPortal.tsx b/src/webparts/requestPortal/components/RequestPortal.tsx index cfd7fac..75cc30f 100644 --- a/src/webparts/requestPortal/components/RequestPortal.tsx +++ b/src/webparts/requestPortal/components/RequestPortal.tsx @@ -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) обернуть контент в
с двумя колонками: +//
+//
+// (классы layout/main/side уже есть в RequestPortal.module.scss) const RequestPortal: React.FC = ({ hasTeamsContext, context }) => { - const [activeCategoryId, setActiveCategoryId] = useState(DEFAULT_CATEGORY_ID); - const [selectedRequest, setSelectedRequest] = useState(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 ( -
- -

Форма «{request.title}» ещё в разработке.

-
- ); - } - - // Ленивый рендер формы: её код подтягивается только сейчас, при открытии. - return ( - Загрузка формы…
}> - - - ); - }; return (
-
- - - {selectedRequest ? ( - renderSelected(selectedRequest) - ) : ( - - )} -
+
); }; diff --git a/src/webparts/requestPortal/features/myRequests/MyRequestsPanel.module.scss b/src/webparts/requestPortal/features/myRequests/MyRequestsPanel.module.scss new file mode 100644 index 0000000..038b24a --- /dev/null +++ b/src/webparts/requestPortal/features/myRequests/MyRequestsPanel.module.scss @@ -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; +} diff --git a/src/webparts/requestPortal/features/myRequests/MyRequestsPanel.tsx b/src/webparts/requestPortal/features/myRequests/MyRequestsPanel.tsx new file mode 100644 index 0000000..bdc0039 --- /dev/null +++ b/src/webparts/requestPortal/features/myRequests/MyRequestsPanel.tsx @@ -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 = (/* { context } */) => { + return ( + + ); +}; + +export default MyRequestsPanel; diff --git a/src/webparts/requestPortal/features/submit/SubmitFeature.module.scss b/src/webparts/requestPortal/features/submit/SubmitFeature.module.scss new file mode 100644 index 0000000..3210d2a --- /dev/null +++ b/src/webparts/requestPortal/features/submit/SubmitFeature.module.scss @@ -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; +} diff --git a/src/webparts/requestPortal/features/submit/SubmitFeature.tsx b/src/webparts/requestPortal/features/submit/SubmitFeature.tsx new file mode 100644 index 0000000..c8a9d7f --- /dev/null +++ b/src/webparts/requestPortal/features/submit/SubmitFeature.tsx @@ -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 = ({ context }) => { + const [activeCategoryId, setActiveCategoryId] = useState(DEFAULT_CATEGORY_ID); + const [selectedRequest, setSelectedRequest] = useState(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 ( +
+ +

Форма «{request.title}» ещё в разработке.

+
+ ); + } + + // Ленивый рендер формы: её код подтягивается только сейчас, при открытии. + return ( + Загрузка формы…}> + + + ); + }; + + return ( +
+ + + {selectedRequest ? ( + renderSelected(selectedRequest) + ) : ( + + )} +
+ ); +}; + +export default SubmitFeature; diff --git a/src/webparts/requestPortal/forms/RecruitingHire/approval/provisioning.json b/src/webparts/requestPortal/forms/RecruitingHire/approval/provisioning.json index 0db4495..e26593a 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/approval/provisioning.json +++ b/src/webparts/requestPortal/forms/RecruitingHire/approval/provisioning.json @@ -1,5 +1,5 @@ { - "dataListTitle": "Согласование — Прием на работу", + "dataListTitle": "Прием на работу — согласование", "columns": [ { "internalName": "Approver", "title": "Согласующий", "type": "User", "userSelectionMode": "PeopleOnly" }, { "internalName": "Status", "title": "Статус", "type": "Choice", "choices": ["Черновик", "На согласовании", "Согласовано", "Отклонено", "Возвращена на доработку", "Выполнение", "Закрыта"] }, diff --git a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts index 222e3ce..b5d352b 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts @@ -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; diff --git a/src/webparts/requestPortal/shared/approval/approvalList.service.ts b/src/webparts/requestPortal/shared/approval/approvalList.service.ts index 4b298f0..479e5cf 100644 --- a/src/webparts/requestPortal/shared/approval/approvalList.service.ts +++ b/src/webparts/requestPortal/shared/approval/approvalList.service.ts @@ -27,7 +27,7 @@ export interface IApproverToWrite { allowReassign: boolean; } -// Все запросы к списку «Согласование — <форма>». listTitle задаётся формой. +// Все запросы к списку «<форма> — согласование». listTitle задаётся формой. export class ApprovalListService { constructor(private readonly listTitle: string) {} diff --git a/src/webparts/requestPortal/shared/approval/models/IApprovalRow.ts b/src/webparts/requestPortal/shared/approval/models/IApprovalRow.ts index 17e1044..3984b29 100644 --- a/src/webparts/requestPortal/shared/approval/models/IApprovalRow.ts +++ b/src/webparts/requestPortal/shared/approval/models/IApprovalRow.ts @@ -1,4 +1,4 @@ -// Строка согласования из списка «Согласование — <форма>». +// Строка согласования из списка «<форма> — согласование». export interface IApprovalRow { id: number; status: string;