Files
request-portal/src/webparts/requestPortal/features/submit/SubmitFeature.tsx
T
Тимур Городецкий b82008943f
Tests / frontend-test (push) Successful in 1m20s
style: иконка «Все заявки» — список вместо людей
Иконка людей совпадала с иконкой категории «Отдел рекрутинга» и плитки
«Заявка на прием» и не говорила, что за кнопкой список заявок. Заменена
на menu на плитке, на кнопке формы и в боковом меню.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:29:29 +05:00

181 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React 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 '@tnsu/ui-kit';
import { RequestGrid } from '../../components/RequestGrid/RequestGrid';
import { getRequestsByCategory, REQUEST_TYPES } from '../../data/requests';
import { getFormComponent } from '../../forms/registry';
import { IRequestType } from '../../models/types';
import { emitRequestsChanged } from '../../shared/requestEvents';
import { confirmDiscardFormChanges, markFormClean } from '../../shared/unsavedChanges';
import { useIsTester } from '../../shared/useIsTester';
import { useAccessibleAllRequestsFormKeys } from '../../shared/approval';
export interface ISubmitFeatureProps {
context: WebPartContext;
/** Активная категория. Вкладки теперь рендерятся на уровне портала (RequestPortal),
* чтобы оставаться на экране и в режиме согласования — эта фича их только слушает. */
activeCategoryId: string;
/** Открыть сразу форму, минуя плитки — с предзаполнением (производная заявка,
* sourceId задан) или без него (пустая новая форма конкретного типа).
* positionId/nomenclatureId — параметры внешней ссылки (только форма ТМЦ). */
initialForm?: { formKey: string; sourceId?: number; positionId?: string; nomenclatureId?: string; storeName?: string };
/** Открыть сразу форму на РЕДАКТИРОВАНИЕ существующей заявки (черновик/возвращена). */
editForm?: { formKey: string; itemId: number };
/** Пользователь выбрал плитку — сообщаем наверх, чтобы адрес страницы стал
* ссылкой на эту пустую форму (см. newRequestLinkUrl в shared/routing). */
onFormSelected?: (formKey: string) => void;
/**
* Кнопка «Все заявки этого типа» на форме — только когда в админке
* approval-route текущему пользователю выдан доступ к этому типу (см.
* hasRequestTypeAccess ниже). Ведёт в раздел «Все заявки» (nav='allRequests').
*/
onOpenAllRequests?: (requestTypeId: number) => void;
}
// Фича «Подать заявку»: плитки → беспоук-форма (категории/вкладки — в RequestPortal).
// Владелец: автор портала. Живёт целиком в features/submit/**.
const SubmitFeature: React.FC<ISubmitFeatureProps> = ({
context,
activeCategoryId,
initialForm,
editForm,
onFormSelected,
onOpenAllRequests,
}) => {
// «Создать на основе» и «редактировать» оба сразу открывают нужную форму, минуя плитки.
const preOpen = initialForm ?? editForm;
const initialSelected = useMemo(
() => (preOpen ? REQUEST_TYPES.filter((r) => r.id === preOpen.formKey)[0] ?? null : null),
[preOpen]
);
const [selectedRequest, setSelectedRequest] = useState<IRequestType | null>(initialSelected);
// Смена категории (вкладка сверху) — возвращаемся к списку плиток.
// Первый прогон пропускаем: иначе форма, открытая по initialForm «на основе»
// (её категория может не совпадать с текущей вкладкой), тут же закроется.
const firstCategorySync = useRef(true);
useEffect(() => {
if (firstCategorySync.current) {
firstCategorySync.current = false;
return;
}
setSelectedRequest((prev) => (prev && prev.categoryId !== activeCategoryId ? null : prev));
}, [activeCategoryId]);
const requests = getRequestsByCategory(activeCategoryId);
const isTester = useIsTester();
// Кнопка «Все заявки этого типа» — и на плитке, и на открытой форме, только
// если текущему пользователю в админке approval-route выдан доступ к этому
// типу (см. shared/approval/requestTypeAccess.ts). Один общий поход по всем
// типам разом (formKey формы совпадает с pageUrl approval-route по
// соглашению, см. useRequestTypeId) — не по одному запросу на каждую плитку.
const accessibleAllRequestsIds = useAccessibleAllRequestsFormKeys(context);
// Возврат к плиткам закрывает форму и теряет введённое (раздел остаётся
// смонтированным, но состояние формы живёт в самом FormComponent). Спрашиваем,
// если правки есть и они не сохранены; после согласия флаг снимаем — терять
// уже нечего.
const handleBack = (): void => {
confirmDiscardFormChanges().then((ok) => {
if (!ok) return;
markFormClean();
setSelectedRequest(null);
}).catch(() => undefined);
};
const renderSelected = (request: IRequestType): React.ReactNode => {
const FormComponent = getFormComponent(request.id);
// Формы «в разработке» открыты только группе «Тестирование» — плитка у
// остальных задизейблена, но форму можно открыть и прямой ссылкой (rptype).
if (request.comingSoon && isTester === undefined) {
return <div className={styles.loading}>Загрузка формы…</div>;
}
// Форма ещё не реализована или пока закрыта для этого пользователя — заглушка.
if (!FormComponent || (request.comingSoon && !isTester)) {
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>
);
}
// sourceId — «на основе» исходной заявки; editId — правка существующей.
const sourceId =
initialForm && request.id === initialForm.formKey ? initialForm.sourceId : undefined;
const editId = editForm && request.id === editForm.formKey ? editForm.itemId : undefined;
// Параметры внешней ссылки — только когда открыта именно та форма, для
// которой они пришли (formKey совпал).
const external = initialForm && request.id === initialForm.formKey ? initialForm : undefined;
const allRequestsTypeId = accessibleAllRequestsIds?.get(request.id);
// Ленивый рендер формы: её код подтягивается только сейчас, при открытии.
return (
<>
{allRequestsTypeId !== undefined && onOpenAllRequests && (
<div className={styles.formToolbar}>
<button
type="button"
className={styles.allRequestsButton}
onClick={() => onOpenAllRequests(allRequestsTypeId)}
>
<Icon name="menu" size="sm" />
<span>Все заявки этого типа</span>
</button>
</div>
)}
<React.Suspense fallback={<div className={styles.loading}>Загрузка формы…</div>}>
<FormComponent
requestType={request}
context={context}
onCancel={handleBack}
onSaved={emitRequestsChanged}
sourceId={sourceId}
editId={editId}
positionId={external?.positionId}
nomenclatureId={external?.nomenclatureId}
storeName={external?.storeName}
/>
</React.Suspense>
</>
);
};
// Выбор плитки — переключаем и локальный стейт (форма рисуется сразу же), и
// сообщаем наверх formKey, чтобы адрес страницы стал ссылкой на эту форму
// (см. onFormSelected). RequestPortal в ответ вызовет navigate(), отчего
// SubmitFeature пересоздастся (другой key) — initialForm подхватит тот же
// formKey, так что видимый результат не изменится, просто через новый инстанс.
const handleSelect = (request: IRequestType): void => {
setSelectedRequest(request);
onFormSelected?.(request.id);
};
return (
<div className={styles.content}>
{selectedRequest ? (
renderSelected(selectedRequest)
) : (
<RequestGrid
requests={requests}
onSelect={handleSelect}
accessibleAllRequestsIds={accessibleAllRequestsIds}
onOpenAllRequests={onOpenAllRequests}
/>
)}
</div>
);
};
export default SubmitFeature;