import * as React from 'react'; import { Employee } from '../../models'; import { texts } from '../../texts'; import { findEmployeeByEmail, toEmployeeOption } from '../../utils/employee'; import { useAsyncSubmit } from '../../hooks/useAsyncSubmit'; import { boolToTriState, isValidSlaHours, parseSlaHours, slaHoursToInput, triStateToBool, TriState, TRI_STATE_OPTIONS } from '../../utils/sla'; import { Field, FormGrid, FormModal, SearchableSelect, Select, TextInput } from '../ui'; export interface IManageRequestsApproverModalProps { title: string; initialEmail?: string; /** SLA строки в часах на момент открытия. null — в строке не задано, работает SLA типа заявки. */ // eslint-disable-next-line @rushstack/no-new-null -- null = «не задано», это не 0 initialSlaHours: number | null; /** Опрос причины просрочки по строке. null — берётся от типа заявки. */ // eslint-disable-next-line @rushstack/no-new-null -- см. initialSlaHours initialAskOverdueReason: boolean | null; employees: Employee[]; employeesLoading: boolean; /** Email'ы (в любом регистре), уже занятые другими шагами той же фазы — этот человек не может встать в фазу второй раз. */ excludedEmails: string[]; onSave: ( email: string, position: string, // eslint-disable-next-line @rushstack/no-new-null -- null = «не задано» slaHours: number | null, // eslint-disable-next-line @rushstack/no-new-null -- null = «не задано» askOverdueReason: boolean | null ) => Promise; onClose: () => void; } /** * Правка существующего шага: кто согласует и какой у шага SLA. * * Одна форма на оба вида правки намеренно — раньше SLA жил в отдельной модалке * со своей кнопкой-флажком в строке, и получалось два похожих диалога и лишняя * иконка в и без того плотном ряду действий. Правки применяются одним * сохранением (см. saveModal в ManageRequestsScreen). * * Должность НЕ вводится — берётся из справочника сотрудников * (api.tnsu.kz/Employee/All, см. Employee.position в models/dictionaries.ts) на * момент сохранения. Добавление НОВОГО шага через модалку не идёт — см. * плашку-пикер под таблицей в ManageRequestsPhaseTable. */ export const ManageRequestsApproverModal: React.FC = ({ title, initialEmail = '', initialSlaHours, initialAskOverdueReason, employees, employeesLoading, excludedEmails, onSave, onClose }) => { const [email, setEmail] = React.useState(initialEmail); const [slaHours, setSlaHours] = React.useState(() => slaHoursToInput(initialSlaHours)); const [askOverdueReason, setAskOverdueReason] = React.useState( () => boolToTriState(initialAskOverdueReason) ); const submit = useAsyncSubmit(); // Тот, кто уже занят в этой фазе, исключён из списка выбора — дубликат // физически нельзя выбрать (см. excludedEmails в ManageRequestsScreen). const excludedSet = new Set(excludedEmails.map(e => e.toLowerCase())); const selectableEmployees = employees.filter(e => !excludedSet.has(e.email.toLowerCase())); const isDirty = email !== initialEmail || slaHours !== slaHoursToInput(initialSlaHours) || askOverdueReason !== boolToTriState(initialAskOverdueReason); const handleSubmit = (): void => { const trimmedEmail = email.trim(); if (!trimmedEmail) { submit.fail(texts.manageRequests.validationApprover); return; } if (excludedSet.has(trimmedEmail.toLowerCase())) { submit.fail(texts.manageRequests.validationDuplicateApprover); return; } if (!isValidSlaHours(slaHours)) { submit.fail(texts.manageRequests.validationSlaHours); return; } const position = findEmployeeByEmail(employees, trimmedEmail)?.position || ''; submit.run(() => onSave(trimmedEmail, position, parseSlaHours(slaHours), triStateToBool(askOverdueReason)) ); }; return ( setSlaHours(e.target.value)} placeholder={texts.manageRequests.slaHoursPlaceholder} />