10c3057eff
Раньше шаг маршрута мог нести ровно одно условие (field_name/operation/ value). Теперь условий может быть сколько угодно, а их объединение задаётся явно — ConditionsLogic.And (все условия) или .Or (любое из условий). Бэкенд: - Новая сущность TemplateRouteStepCondition (дочерняя таблица template_route_step_conditions, FK на шаг, cascade delete) вместо трёх колонок на TemplateRouteStep. Миграция AddMultipleStepConditions переносит существующие условия шагов в новую таблицу перед удалением старых колонок, чтобы не потерять уже настроенные в деве/проде условия. - TemplateRouteService.MatchesConditions: несколько условий шага объединяются по ConditionsLogic (Any/All); HasUnevaluatableManagerCondition проверяет условие на IsManager в списке условий, а не единственное поле. - TemplateRouteStepService: дубликаты шагов теперь сравниваются по всему набору условий (без учёта порядка) и, если условий два и более, по логике их объединения — сравнение в памяти, а не в SQL, кандидатов на дубликат в пределах маршрута всегда мало. - Валидация условия: раньше «все три поля или ни одного», теперь — в каждом элементе списка все три поля обязательны. Фронтенд: - RouteStepFormModal: условие — динамический список строк (добавить/удалить), у каждой строки свой набор допустимых операторов по типу колонки; селектор И/ИЛИ появляется, когда условий два и больше. - describeStepCondition в таблице шагов склеивает все условия через « И »/« ИЛИ ». Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
53 lines
2.9 KiB
TypeScript
53 lines
2.9 KiB
TypeScript
import { ConditionsLogic, Employee, HeadType, ProjectRole, StepExecutorKind, TemplateRouteStep } from '../models';
|
|
import { comparisonOperatorSymbols, conditionsLogicJoiners, headTypeLabels, projectRoleLabels, texts } from '../texts';
|
|
import { findEmployeeByEmail } from './employee';
|
|
|
|
/** Каким из четырёх взаимоисключающих способов у шага задан исполнитель. */
|
|
/* eslint-disable @rushstack/no-new-null -- форма зеркалит TemplateRouteStep (nullable JSON-контракт бэкенда) */
|
|
export function detectExecutorKind(step: {
|
|
executorEmail?: string | null;
|
|
position?: string | null;
|
|
projectRole?: ProjectRole | null;
|
|
headType?: HeadType | null;
|
|
}): StepExecutorKind {
|
|
/* eslint-enable @rushstack/no-new-null */
|
|
if (step.executorEmail) return 'email';
|
|
if (step.position) return 'position';
|
|
if (step.projectRole && step.projectRole !== ProjectRole.None) return 'projectRole';
|
|
return 'headType';
|
|
}
|
|
|
|
export interface StepExecutorDisplay {
|
|
primary: string;
|
|
/** Должность и email под ФИО — когда исполнитель задан конкретным сотрудником и тот найден в справочнике. */
|
|
secondary?: string;
|
|
}
|
|
|
|
/** Колонка «Исполнитель» в таблице шагов шаблона маршрута. */
|
|
export function describeStepExecutor(step: TemplateRouteStep, employees: Employee[]): StepExecutorDisplay {
|
|
if (step.executorEmail) {
|
|
const employee = findEmployeeByEmail(employees, step.executorEmail ?? undefined);
|
|
if (!employee) return { primary: step.executorEmail };
|
|
// Должность рядом с email: по одному ФИО не видно, тот ли это человек, что
|
|
// задумывался при настройке маршрута (в справочнике есть однофамильцы).
|
|
return { primary: employee.fio, secondary: [employee.position, employee.email].filter(Boolean).join(' · ') };
|
|
}
|
|
if (step.position) return { primary: step.position };
|
|
if (step.projectRole && step.projectRole !== ProjectRole.None) {
|
|
return { primary: texts.routeSteps.executorRole(projectRoleLabels[step.projectRole]) };
|
|
}
|
|
if (step.headType && step.headType !== HeadType.None) {
|
|
return { primary: texts.routeSteps.executorHead(headTypeLabels[step.headType]) };
|
|
}
|
|
return { primary: texts.common.empty };
|
|
}
|
|
|
|
/** Колонка «Условие» в таблице шагов — undefined, если у шага условий нет. */
|
|
export function describeStepCondition(step: TemplateRouteStep): string | undefined {
|
|
if (step.conditions.length === 0) return undefined;
|
|
const joiner = conditionsLogicJoiners[step.conditionsLogic ?? ConditionsLogic.And];
|
|
return step.conditions
|
|
.map(c => `${c.fieldName} ${comparisonOperatorSymbols[c.operation]} ${c.value}`)
|
|
.join(joiner);
|
|
}
|