b82008943f
Tests / frontend-test (push) Successful in 1m20s
Иконка людей совпадала с иконкой категории «Отдел рекрутинга» и плитки «Заявка на прием» и не говорила, что за кнопкой список заявок. Заменена на menu на плитке, на кнопке формы и в боковом меню. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
181 lines
9.5 KiB
TypeScript
181 lines
9.5 KiB
TypeScript
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;
|