fix: внес изменения в заявках рекрутинга от 16.09.26
Tests / frontend-test (push) Successful in 1m20s
Tests / frontend-test (push) Successful in 1m20s
This commit is contained in:
@@ -25,6 +25,15 @@
|
||||
`editable=false` даже в форме подачи, не только в просмотре. Если шаг маршрута не
|
||||
разрешился в конкретного человека — отправка блокируется с просьбой обратиться к
|
||||
администратору маршрутов (сам заявитель это не чинит).
|
||||
- **Шаг «Руководитель» переопределяется полем формы.** Роль с `positionCode: "Chief"`
|
||||
в ответе `resolve` approval-route резолвит сам по своей оргструктуре — это не всегда
|
||||
тот, кого заявитель вписал/выбрал в поле «Руководитель» (ContactSection,
|
||||
`PeopleTextField`). `managerOverride.ts` после загрузки планируемой матрицы находит
|
||||
строку с этим `positionCode` и подставляет туда email/ФИО из `data.Manager` вместо
|
||||
того, что вернул resolve. Пустое/нераспознанное значение поля → шаг остаётся без
|
||||
исполнителя, это ловит тот же `unassignedStepMessage`, что и обычный незаполненный
|
||||
шаг. Работает только для НОВОЙ заявки (`positionCode` есть только у планируемого
|
||||
маршрута, не у уже записанных строк списка согласования).
|
||||
|
||||
## Особое поведение
|
||||
- **Контакты заявителя предзаполняются через Microsoft Graph** (`/me`): ФИО, email,
|
||||
|
||||
@@ -51,6 +51,7 @@ import { getApplicantProfile } from './services/profile.service';
|
||||
import { RequestSection } from './sections/RequestSection';
|
||||
import { ContactSection } from './sections/ContactSection';
|
||||
import { useStableCallback } from '../../shared/useStableCallback';
|
||||
import { applyManagerOverride, managerEmail } from './managerOverride';
|
||||
|
||||
const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
||||
requestType,
|
||||
@@ -182,6 +183,43 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
||||
!!editId || (!!data.WorkType && !!requestTypeId),
|
||||
(login) => approval.getJobTitle(login)
|
||||
);
|
||||
// Должность нового «Руководителя» — approval-route её не знает (резолвил шаг
|
||||
// на ДРУГОГО человека и отдал его должность), поэтому подтягиваем сами по
|
||||
// e-mail из поля (SPS-JobTitle, тот же источник, что и у обычных согласующих,
|
||||
// см. useMatrixEditor.getJobTitle выше). undefined, пока запрос не отработал —
|
||||
// applyManagerOverride в этом случае не трогает должность строки, чтобы не
|
||||
// мигать пустым значением на каждое изменение поля.
|
||||
const [managerPosition, setManagerPosition] = useState<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
const email = managerEmail(data.Manager);
|
||||
if (!email) {
|
||||
setManagerPosition(undefined);
|
||||
return;
|
||||
}
|
||||
let alive = true;
|
||||
setManagerPosition(''); // сбрасываем сразу — иначе на экране на миг видна должность ПРЕЖНЕГО руководителя
|
||||
approval
|
||||
.getJobTitle(email)
|
||||
.then((title) => {
|
||||
if (alive) setManagerPosition(title);
|
||||
})
|
||||
.catch(() => {
|
||||
if (alive) setManagerPosition('');
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [data.Manager, approval]);
|
||||
|
||||
// Шаг с ролью «Руководитель» (positionCode Chief) переопределяем полем формы,
|
||||
// а не тем, кого approval-route резолвит сам по оргструктуре — см.
|
||||
// managerOverride.ts. Пересчёт локальный (без похода в resolve), поэтому
|
||||
// безопасно зависеть от data.Manager, который меняется на каждое нажатие клавиши.
|
||||
const matrixRows = useMemo(
|
||||
() => applyManagerOverride(matrix.rows, data.Manager, managerPosition),
|
||||
[matrix.rows, data.Manager, managerPosition]
|
||||
);
|
||||
const matrixEditor = useMemo(() => ({ ...matrix, rows: matrixRows }), [matrix, matrixRows]);
|
||||
|
||||
const isValid = !!(
|
||||
data.TaskName.trim() &&
|
||||
@@ -248,7 +286,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
||||
const { id, code } = await saveTicketRequest(
|
||||
requestId,
|
||||
data,
|
||||
toApprovers(matrix.rows),
|
||||
toApprovers(matrixRows),
|
||||
finalStatus,
|
||||
writeListId
|
||||
);
|
||||
@@ -297,7 +335,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
||||
// незаполненный шаг заявитель исправить сам не может, только администратор
|
||||
// маршрутов (роль в шаге не разрешилась в конкретного человека).
|
||||
const unassignedError = unassignedStepMessage(
|
||||
matrix.rows,
|
||||
matrixRows,
|
||||
(r) => `Шаг ${r.order} маршрута согласования («${r.position || 'без должности'}») не назначен на конкретного человека. Обратитесь к администратору маршрутов согласования.`
|
||||
);
|
||||
if (unassignedError) return unassignedError;
|
||||
@@ -360,7 +398,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
||||
)}
|
||||
|
||||
<ApprovalMatrix
|
||||
editor={matrix}
|
||||
editor={matrixEditor}
|
||||
search={(q) => approval.searchUsers(q)}
|
||||
emptyText="Маршрут для этого вида работ пока не настроен — обратитесь к администратору маршрутов согласования."
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { IMatrixRow } from '../../shared/approval';
|
||||
|
||||
// Формат значения поля «Руководитель» — тот же, что у автоподстановки
|
||||
// (Ticket1CForm.tsx) и у PeopleTextField/ContactSection: «ФИО · email».
|
||||
const MANAGER_SEPARATOR = ' · ';
|
||||
const CHIEF_POSITION_CODE = 'Chief';
|
||||
const EMAIL_RE = /^\S+@\S+\.\S+$/;
|
||||
|
||||
/** Email из значения поля «Руководитель» — используется для запроса должности (см. Ticket1CForm.tsx). */
|
||||
export function managerEmail(value: string): string {
|
||||
return parseManager(value).email;
|
||||
}
|
||||
|
||||
function parseManager(value: string): { name: string; email: string } {
|
||||
const trimmed = value.trim();
|
||||
const sepIndex = trimmed.lastIndexOf(MANAGER_SEPARATOR);
|
||||
if (sepIndex !== -1) {
|
||||
return {
|
||||
name: trimmed.slice(0, sepIndex).trim(),
|
||||
email: trimmed.slice(sepIndex + MANAGER_SEPARATOR.length).trim()
|
||||
};
|
||||
}
|
||||
// Ввели/вставили просто e-mail без ФИО (напр. руками, без пикера).
|
||||
return { name: '', email: EMAIL_RE.test(trimmed) ? trimmed : '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Шаг маршрута с ролью «Руководитель» (positionCode Chief в админке маршрутов)
|
||||
* approval-route резолвит сам по оргструктуре — этот источник не всегда
|
||||
* совпадает с тем, что заявитель вписал в поле «Руководитель» (ContactSection),
|
||||
* а заявитель мог осознанно указать другого человека через PeopleTextField или
|
||||
* вручную. Поле формы должно быть главным источником для ЭТОГО шага.
|
||||
*
|
||||
* Пустой/нераспознанный e-mail в поле → шаг остаётся без назначенного
|
||||
* исполнителя (email ''), это поймает unassignedStepMessage при отправке —
|
||||
* отдельной проверки здесь не нужно.
|
||||
*
|
||||
* position — должность НОВОГО исполнителя (SPS-JobTitle по managerEmail, см.
|
||||
* Ticket1CForm.tsx), которую approval-route вернуть не может — он резолвил
|
||||
* шаг на другого человека и отдал ЕГО должность. Без этого параметра в
|
||||
* колонке «Должность» осталась бы должность прежнего (авторезолвленного)
|
||||
* исполнителя рядом с ФИО нового. undefined (запрос ещё не отработал/упал) →
|
||||
* должность строки временно не меняем, чтобы не мигать пустым значением на
|
||||
* каждое изменение поля.
|
||||
*
|
||||
* Только для ПЛАНИРУЕМОГО маршрута (positionCode есть только у него, см.
|
||||
* IMatrixRow.positionCode) — для уже записанных строк заявки (editId) это
|
||||
* no-op, что и нужно: решение согласующего задним числом не переписываем.
|
||||
*/
|
||||
export function applyManagerOverride(rows: IMatrixRow[], manager: string, position?: string): IMatrixRow[] {
|
||||
const { name, email } = parseManager(manager);
|
||||
return rows.map((r) =>
|
||||
r.positionCode === CHIEF_POSITION_CODE ? { ...r, email, who: name || email, position: position ?? r.position } : r
|
||||
);
|
||||
}
|
||||
@@ -22,7 +22,11 @@ export interface IFormData {
|
||||
Phone: string;
|
||||
Department: string;
|
||||
Position: string;
|
||||
/** Непосредственный руководитель заявителя — предзаполняется через getManagerEmail, редактируем. */
|
||||
/**
|
||||
* Непосредственный руководитель заявителя — предзаполняется через
|
||||
* getManagerEmail, редактируем: вручную текстом или через PeopleTextField
|
||||
* (кнопка «выбрать из списка сотрудников»), см. ContactSection.
|
||||
*/
|
||||
Manager: string;
|
||||
Question: string;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { CollapseSection, FormField, TextInput, PhoneInput } from '../../../shared/ui';
|
||||
import { CollapseSection, FormField, TextInput, PhoneInput, PeopleTextField } from '../../../shared/ui';
|
||||
import { ISectionProps } from '../model';
|
||||
|
||||
export const ContactSection: React.FC<ISectionProps> = ({ data, set }) => (
|
||||
export const ContactSection: React.FC<ISectionProps> = ({ data, set, context }) => (
|
||||
<CollapseSection title="Контакты заявителя" icon="people">
|
||||
<FormField label="Ф.И.О." htmlFor="FIO">
|
||||
<TextInput id="FIO" value={data.FIO} onChange={(v) => set('FIO', v)} />
|
||||
@@ -20,7 +20,13 @@ export const ContactSection: React.FC<ISectionProps> = ({ data, set }) => (
|
||||
<TextInput id="Position" value={data.Position} onChange={(v) => set('Position', v)} />
|
||||
</FormField>
|
||||
<FormField label="Руководитель" htmlFor="Manager">
|
||||
<TextInput id="Manager" value={data.Manager} onChange={(v) => set('Manager', v)} />
|
||||
<PeopleTextField
|
||||
id="Manager"
|
||||
context={context}
|
||||
value={data.Manager}
|
||||
onChange={(v) => set('Manager', v)}
|
||||
placeholder="ФИО · email или выберите из списка"
|
||||
/>
|
||||
</FormField>
|
||||
</CollapseSection>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user