feat: кадровик в маршруте отпуска без содержания зависит от проекта
В маршрут #15 добавлен тот же шаг кадровика, что в ежегодном отпуске: в офисе — кадровик компании, на объекте — кадровик объекта. Форма теперь передаёт проект в approval-route, поле «Проект» обязательно и только из справочника. Сборка «организация + проект» вынесена в общий organizationProjectRouteFieldValues — ей пользуются обе формы отпуска. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,7 +47,7 @@ import {
|
|||||||
dataListTitleForListId
|
dataListTitleForListId
|
||||||
} from '../../shared/listTitles';
|
} from '../../shared/listTitles';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField, NOTICE_TITLE, NOTICE_ITEMS } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField, NOTICE_TITLE, NOTICE_ITEMS } from './model';
|
||||||
import { organizationRouteFieldValues } from '../../shared/dictionary/organizationRouteFields';
|
import { organizationProjectRouteFieldValues } from '../../shared/dictionary/organizationRouteFields';
|
||||||
import { formIssues } from './validation';
|
import { formIssues } from './validation';
|
||||||
import { saveUnpaidLeaveRequest, unpaidLeaveApprovalService } from './submit';
|
import { saveUnpaidLeaveRequest, unpaidLeaveApprovalService } from './submit';
|
||||||
import { loadUnpaidLeaveRequest } from './load';
|
import { loadUnpaidLeaveRequest } from './load';
|
||||||
@@ -164,10 +164,14 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
// пользователя) — так работает подача за другого. У заявки на редактировании
|
// пользователя) — так работает подача за другого. У заявки на редактировании
|
||||||
// маршрут читается из уже записанных строк, чтобы не затереть правки инициатора.
|
// маршрут читается из уже записанных строк, чтобы не затереть правки инициатора.
|
||||||
//
|
//
|
||||||
// Кадровый сотрудник и исполнитель зависят от организации сотрудника (условия
|
// Кадровый сотрудник и исполнитель зависят от организации сотрудника и проекта
|
||||||
// «Organization содержит …» в админке) — см. shared/dictionary/organizationRouteFields.ts.
|
// (условия «Organization содержит …», «Project содержит …» в админке) — см.
|
||||||
// Организация приходит позже выбора сотрудника, поэтому она в ключе перезагрузки.
|
// shared/dictionary/organizationRouteFields.ts. Организация приходит позже
|
||||||
const routeFieldValues = useMemo(() => organizationRouteFieldValues(data.Organization), [data.Organization]);
|
// выбора сотрудника, поэтому она в ключе перезагрузки.
|
||||||
|
const routeFieldValues = useMemo(
|
||||||
|
() => organizationProjectRouteFieldValues(data.Organization, data.Project),
|
||||||
|
[data.Organization, data.Project]
|
||||||
|
);
|
||||||
const routeOrgKey = routeFieldValues.map((f) => f.value).join('|');
|
const routeOrgKey = routeFieldValues.map((f) => f.value).join('|');
|
||||||
const loadMatrix = useCallback(
|
const loadMatrix = useCallback(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export interface IFormData {
|
|||||||
Department: string;
|
Department: string;
|
||||||
/** Id отдела из справочника; '' — если введён вручную без выбора из списка. */
|
/** Id отдела из справочника; '' — если введён вручную без выбора из списка. */
|
||||||
DepartmentId: string;
|
DepartmentId: string;
|
||||||
/** Проект (справочник /Project, см. shared/dictionary/ProjectPicker) — необязательное, справочное. */
|
/** Проект (справочник /Project, см. shared/dictionary/ProjectPicker) — обязательный, от него зависит кадровик в маршруте. */
|
||||||
Project: string;
|
Project: string;
|
||||||
/** oid проекта ('' — если введён вручную без выбора из списка). */
|
/** oid проекта ('' — если введён вручную без выбора из списка). */
|
||||||
ProjectId: string;
|
ProjectId: string;
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, on
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Проект" htmlFor="Project">
|
<FormField label="Проект" htmlFor="Project" required>
|
||||||
<ProjectPicker
|
<ProjectPicker
|
||||||
id="Project"
|
id="Project"
|
||||||
context={context}
|
context={context}
|
||||||
|
|||||||
@@ -16,6 +16,17 @@ function startDateIssue(data: IFormData): IFieldIssue | undefined {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проект: обязателен и выбран из справочника. От него зависит кадровик в
|
||||||
|
* маршруте (условия «Project содержит …»), а название, вписанное руками,
|
||||||
|
* условия могут не узнать — поэтому без ProjectId заявку не пропускаем.
|
||||||
|
*/
|
||||||
|
function projectIssue(data: IFormData): IFieldIssue | undefined {
|
||||||
|
if (!data.Project.trim()) return { field: 'Project', label: 'Проект' };
|
||||||
|
if (data.ProjectId) return undefined;
|
||||||
|
return { field: 'Project', label: 'Проект', message: 'выберите проект из списка' };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Количество дней: обязательно, целое и больше нуля. Верхнего предела нет:
|
* Количество дней: обязательно, целое и больше нуля. Верхнего предела нет:
|
||||||
* сверх 5 дней (обязательных при браке/рождении/смерти) — по договорённости с работодателем.
|
* сверх 5 дней (обязательных при браке/рождении/смерти) — по договорённости с работодателем.
|
||||||
@@ -37,6 +48,7 @@ export function formIssues(data: IFormData): IFieldIssue[] {
|
|||||||
const issues: Array<IFieldIssue | undefined> = [
|
const issues: Array<IFieldIssue | undefined> = [
|
||||||
data.Employee ? undefined : { field: 'Employee', label: 'Сотрудник' },
|
data.Employee ? undefined : { field: 'Employee', label: 'Сотрудник' },
|
||||||
data.Position.trim() ? undefined : { field: 'Position', label: 'Должность' },
|
data.Position.trim() ? undefined : { field: 'Position', label: 'Должность' },
|
||||||
|
projectIssue(data),
|
||||||
startDateIssue(data),
|
startDateIssue(data),
|
||||||
daysCountIssue(data),
|
daysCountIssue(data),
|
||||||
data.ReasonType
|
data.ReasonType
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import {
|
|||||||
} from '../../shared/listTitles';
|
} from '../../shared/listTitles';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField, NOTICE_TITLE, NOTICE_ITEMS } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField, NOTICE_TITLE, NOTICE_ITEMS } from './model';
|
||||||
import { formIssues } from './validation';
|
import { formIssues } from './validation';
|
||||||
import { vacationRouteFieldValues } from './routeFields';
|
import { organizationProjectRouteFieldValues } from '../../shared/dictionary/organizationRouteFields';
|
||||||
import { saveVacationRequest, vacationApprovalService } from './submit';
|
import { saveVacationRequest, vacationApprovalService } from './submit';
|
||||||
import { loadVacationRequest } from './load';
|
import { loadVacationRequest } from './load';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
@@ -176,11 +176,12 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
//
|
//
|
||||||
// Кадровый сотрудник и исполнитель в маршруте зависят от организации
|
// Кадровый сотрудник и исполнитель в маршруте зависят от организации
|
||||||
// сотрудника и проекта (условия «Organization содержит …», «Project
|
// сотрудника и проекта (условия «Organization содержит …», «Project
|
||||||
// содержит …» в админке) — см. routeFields.ts. Организация приходит позже
|
// содержит …» в админке) — см. shared/dictionary/organizationRouteFields.ts.
|
||||||
// выбора сотрудника (Employee/ByEmail), поэтому она в ключе перезагрузки:
|
// Организация приходит позже выбора сотрудника (Employee/ByEmail), поэтому
|
||||||
// маршрут пересчитается, когда придёт, а не останется рассчитанным без условий.
|
// она в ключе перезагрузки: маршрут пересчитается, когда придёт, а не
|
||||||
|
// останется рассчитанным без условий.
|
||||||
const routeFieldValues = useMemo(
|
const routeFieldValues = useMemo(
|
||||||
() => vacationRouteFieldValues({ Organization: data.Organization, Project: data.Project }),
|
() => organizationProjectRouteFieldValues(data.Organization, data.Project),
|
||||||
[data.Organization, data.Project]
|
[data.Organization, data.Project]
|
||||||
);
|
);
|
||||||
const routeOrgKey = routeFieldValues.map((f) => f.value).join('|');
|
const routeOrgKey = routeFieldValues.map((f) => f.value).join('|');
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import type { IResolveFieldValue } from '../../shared/approval/models/IApprovalRouteApi';
|
|
||||||
import { organizationRouteFieldValues } from '../../shared/dictionary/organizationRouteFields';
|
|
||||||
import { IFormData } from './model';
|
|
||||||
|
|
||||||
/** Поля заявки, от которых зависит маршрут отпуска (условия шагов в админке маршрута #9). */
|
|
||||||
export type VacationRouteInput = Pick<IFormData, 'Organization' | 'Project'>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Значения полей для условных шагов маршрута (fieldValues при resolve).
|
|
||||||
*
|
|
||||||
* Кадровик на шаге 1 зависит и от компании, и от проекта: в офисе — кадровик
|
|
||||||
* компании («Organization содержит TANSU И Project содержит Офис TANSU»), на
|
|
||||||
* объекте — кадровик объекта («Project содержит Топар»). Проект — название из
|
|
||||||
* справочника /Project, как его показывает поле формы.
|
|
||||||
*/
|
|
||||||
export function vacationRouteFieldValues(input: VacationRouteInput): IResolveFieldValue[] {
|
|
||||||
const fieldValues = organizationRouteFieldValues(input.Organization);
|
|
||||||
const project = input.Project.trim();
|
|
||||||
return project ? [...fieldValues, { fieldName: 'Project', value: project }] : fieldValues;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { organizationRouteFieldValues } from './organizationRouteFields';
|
import { organizationProjectRouteFieldValues, organizationRouteFieldValues } from './organizationRouteFields';
|
||||||
|
|
||||||
describe('organizationRouteFieldValues', () => {
|
describe('organizationRouteFieldValues', () => {
|
||||||
it('одна организация — её название', () => {
|
it('одна организация — её название', () => {
|
||||||
@@ -28,3 +28,18 @@ describe('organizationRouteFieldValues', () => {
|
|||||||
expect(organizationRouteFieldValues('')).toEqual([]);
|
expect(organizationRouteFieldValues('')).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('organizationProjectRouteFieldValues', () => {
|
||||||
|
it('организация и проект — две записи', () => {
|
||||||
|
expect(organizationProjectRouteFieldValues('ТОО TANSU Construction — Инженер', ' Офис TANSU ')).toEqual([
|
||||||
|
{ fieldName: 'Organization', value: 'ТОО TANSU Construction' },
|
||||||
|
{ fieldName: 'Project', value: 'Офис TANSU' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('проект не выбран — только организация', () => {
|
||||||
|
expect(organizationProjectRouteFieldValues('ТОО АСТЕЛ-К — Экономист', '')).toEqual([
|
||||||
|
{ fieldName: 'Organization', value: 'ТОО АСТЕЛ-К' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -22,3 +22,16 @@ export function organizationRouteFieldValues(organization: string): IResolveFiel
|
|||||||
const names = companies.filter((name, i) => companies.indexOf(name) === i).sort().join(' ');
|
const names = companies.filter((name, i) => companies.indexOf(name) === i).sort().join(' ');
|
||||||
return names ? [{ fieldName: 'Organization', value: names }] : [];
|
return names ? [{ fieldName: 'Organization', value: names }] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Организация + проект — для форм, где кадровик зависит и от места работы
|
||||||
|
* (отпуск, отпуск без содержания): в офисе — кадровик компании («Organization
|
||||||
|
* содержит TANSU И Project содержит Офис TANSU»), на объекте — кадровик
|
||||||
|
* объекта («Project содержит …»). Проект — название из справочника /Project,
|
||||||
|
* как его показывает поле формы.
|
||||||
|
*/
|
||||||
|
export function organizationProjectRouteFieldValues(organization: string, project: string): IResolveFieldValue[] {
|
||||||
|
const fieldValues = organizationRouteFieldValues(organization);
|
||||||
|
const projectName = project.trim();
|
||||||
|
return projectName ? [...fieldValues, { fieldName: 'Project', value: projectName }] : fieldValues;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user