import * as React from 'react'; import styles from './ResolveScreen.module.scss'; import { ApprovalRouteApi } from '../../services/api'; import { PortalDictionaryClient } from '../../services/portalDictionary'; import { isNumericFieldType, SharePointListsClient } from '../../services/sharePointLists'; import { Project, ResolveFieldValue, RouteStepStatus } from '../../models'; import { routeStepStatusLabels, texts } from '../../texts'; import { useRequestTypes } from '../../hooks/useRequestTypes'; import { useResolveRoute } from '../../hooks/useResolveRoute'; import { usePortalEmployees } from '../../hooks/usePortalEmployees'; import { usePortalDictionaries } from '../../hooks/usePortalDictionaries'; import { useSharePointFields } from '../../hooks/useSharePointFields'; import { useLocalStorageState } from '../../hooks/useLocalStorageState'; import { findEmployeeByEmail, toEmployeeOption } from '../../utils/employee'; import { IResolveRouteParams, replaceRoute } from '../../routing'; import { Alert, AsyncSearchableSelect, Breadcrumbs, Button, CellNote, DataTable, Field, FormGrid, IconButton, PageHeader, Panel, PrimaryCell, SearchableSelect, Select, TextInput } from '../ui'; export interface IResolveScreenProps { api: ApprovalRouteApi; /** Undefined, пока SPFx выдаёт AAD-клиент для api.tnsu.kz. */ dictionary?: PortalDictionaryClient; sharePoint: SharePointListsClient; currentUserEmail?: string; /** Заполнение из URL (глубокая ссылка) — есть, только если в адресе заданы * параметры проверки маршрута (aartype и т.д., см. routing.ts). */ route?: IResolveRouteParams; } const STATUS_OPTIONS = [RouteStepStatus.Approval, RouteStepStatus.Execution] .map(v => ({ value: v, label: routeStepStatusLabels[v] })); // Форма проверки маршрута заполняется одними и теми же параметрами раз за разом // (тип заявки, инициатор, проект, статус) — персистим её в localStorage, чтобы // не вводить всё заново при каждом открытии экрана. const RESOLVE_FORM_STORAGE_KEY = 'approvalAdmin.resolveForm'; interface IResolveFormState { requestTypeId: string; userEmail: string; projectId: string; projectLabel: string; status: RouteStepStatus; fieldValues: ResolveFieldValue[]; departmentId: string; } const EMPTY_RESOLVE_FORM: IResolveFormState = { requestTypeId: '', userEmail: '', projectId: '', projectLabel: '', status: RouteStepStatus.Approval, fieldValues: [], departmentId: '' }; export const ResolveScreen: React.FC = ({ api, dictionary, sharePoint, currentUserEmail, route }) => { const requestTypes = useRequestTypes(api); const [form, setForm] = useLocalStorageState(RESOLVE_FORM_STORAGE_KEY, EMPTY_RESOLVE_FORM); const { requestTypeId, userEmail, projectId, projectLabel, status, fieldValues = [], departmentId } = form; // Открыли по глубокой ссылке (aartype и т.д. в URL) — параметры оттуда // перекрывают то, что было в localStorage. Только на монтировании: route // меняется при каждом собственном же replaceRoute ниже, а не только при // переходе по внешней ссылке — сравнивать его на равенство и держать в // зависимостях смысла нет. React.useEffect(() => { if (!route) return; setForm({ requestTypeId: route.requestTypeId, userEmail: route.userEmail, projectId: route.projectId, projectLabel: route.projectLabel, status: route.status, fieldValues: route.fieldValues, departmentId: route.departmentId }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Держим URL синхронным с формой — правки сразу становятся частью ссылки, // которую можно скопировать/сохранить. replaceRoute, а не navigate: иначе // каждый введённый символ добавлял бы шаг в историю браузера. React.useEffect(() => { replaceRoute({ tab: 'resolve', resolve: { requestTypeId, userEmail, projectId, projectLabel, status, fieldValues, departmentId } }); }, [requestTypeId, userEmail, projectId, projectLabel, status, fieldValues, departmentId]); const requestType = requestTypes.items.find(rt => rt.id === Number(requestTypeId)); const hasListId = Boolean(requestType?.listId); const sharePointFields = useSharePointFields(sharePoint, requestType?.siteUrl ?? undefined, requestType?.listId ?? undefined); const fieldNameOptions = sharePointFields.items.map(f => ({ value: f.internalName, label: f.title })); const resolved = useResolveRoute(api); const employees = usePortalEmployees(dictionary); // ~700 отделов — то же соображение, что и в RouteFormModal: дешевле грузить // список один раз и искать по нему на клиенте, чем ходить в сеть на каждый символ. const dictionaries = usePortalDictionaries(dictionary); const searchProjects = React.useCallback( (query: string, page: number) => { if (!dictionary) { return Promise.reject(new Error(texts.resolve.projectDictionaryLoading)); } return dictionary.searchProjects(query, page); }, [dictionary] ); const updateFieldValue = (index: number, patch: Partial): void => { setForm({ ...form, fieldValues: fieldValues.map((fv, i) => (i === index ? { ...fv, ...patch } : fv)) }); }; const addFieldValue = (): void => { setForm({ ...form, fieldValues: [...fieldValues, { fieldName: '', value: '' }] }); }; const removeFieldValue = (index: number): void => { setForm({ ...form, fieldValues: fieldValues.filter((_, i) => i !== index) }); }; const run = (): void => { if (!requestTypeId) { resolved.fail(texts.resolve.validationRequestType); return; } const conditions = fieldValues.filter(fv => fv.fieldName.trim() || fv.value.trim()); if (conditions.some(fv => !fv.fieldName.trim() || !fv.value.trim())) { resolved.fail(texts.resolve.validationConditionField); return; } resolved.resolve({ requestTypeId: Number(requestTypeId), userEmail: userEmail.trim() || undefined, projectId: projectId.trim() || undefined, status, fieldValues: conditions.length > 0 ? conditions : undefined, departmentId: departmentId.trim() || undefined }); }; const result = resolved.result; const unresolvedCount = result ? result.steps.filter(s => !s.executorEmail).length : 0; return (
setForm({ ...form, status: e.target.value as RouteStepStatus })} />
{texts.resolve.conditionsLabel} {requestTypeId && !hasListId && ( {texts.resolve.conditionsDisabledHint} )}
{hasListId && ( <> {fieldValues.map((fv, index) => { const field = sharePointFields.items.find(f => f.internalName === fv.fieldName); const isNumeric = field ? isNumericFieldType(field.typeAsString) : false; return ( // eslint-disable-next-line react/no-array-index-key -- строки не переупорядочиваются, только добавляются/удаляются с конца или явным индексом