Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 984eb65f5b | |||
| b3c46f32d5 | |||
| 739f247268 | |||
| 4f0d02fe59 |
@@ -17,6 +17,8 @@
|
|||||||
(`CompanyPickerMulti`), хранится JSON-массивом `{Id,Name}` в Note-колонке `CompaniesJson`.
|
(`CompanyPickerMulti`), хранится JSON-массивом `{Id,Name}` в Note-колонке `CompaniesJson`.
|
||||||
- **График работы (WorkSchedule)** — выпадающий список (набор совпадает с «Заявкой
|
- **График работы (WorkSchedule)** — выпадающий список (набор совпадает с «Заявкой
|
||||||
на подбор»); при выборе «Другое» появляется ручной ввод `WorkScheduleOther`.
|
на подбор»); при выборе «Другое» появляется ручной ввод `WorkScheduleOther`.
|
||||||
|
- **Время работы (WorkHours)** — так же, как график: набор и колонка сайта общие
|
||||||
|
с «Заявкой на подбор», оттуда подставляется; «Другое» → `WorkHoursOther`.
|
||||||
- **Испытательный срок (ProbationPeriod)** — выпадающий список частых сроков.
|
- **Испытательный срок (ProbationPeriod)** — выпадающий список частых сроков.
|
||||||
- **Рекрутинговое агентство (условный блок, последняя секция)** — `Toggle`
|
- **Рекрутинговое агентство (условный блок, последняя секция)** — `Toggle`
|
||||||
`RecruitingAgency`; при включении показываются «Название компании» (`AgencyName`)
|
`RecruitingAgency`; при включении показываются «Название компании» (`AgencyName`)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const SCALAR_FIELDS: (keyof IFormData)[] = [
|
|||||||
'HireCandidateName', 'HireHireDate', 'Position',
|
'HireCandidateName', 'HireHireDate', 'Position',
|
||||||
'Department', 'SiteLocation', 'PpeHeight', 'PpeClothingSize', 'PpeShoeSize', 'ProbationPeriod', 'ProbationPeriodOther',
|
'Department', 'SiteLocation', 'PpeHeight', 'PpeClothingSize', 'PpeShoeSize', 'ProbationPeriod', 'ProbationPeriodOther',
|
||||||
'SalaryProbation', 'SalaryAfterProbation', 'WorkSchedule', 'WorkScheduleOther',
|
'SalaryProbation', 'SalaryAfterProbation', 'WorkSchedule', 'WorkScheduleOther',
|
||||||
|
'WorkHours', 'WorkHoursOther',
|
||||||
'AgencyName', 'AgencyTerms', 'HireSecurityNote', 'HireApproverNote',
|
'AgencyName', 'AgencyTerms', 'HireSecurityNote', 'HireApproverNote',
|
||||||
'SelectionRequestId', 'SelectionRequestListId', 'SelectionRequestCode'
|
'SelectionRequestId', 'SelectionRequestListId', 'SelectionRequestCode'
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ export interface IFormData {
|
|||||||
SalaryAfterProbation: string;
|
SalaryAfterProbation: string;
|
||||||
WorkSchedule: string;
|
WorkSchedule: string;
|
||||||
WorkScheduleOther: string;
|
WorkScheduleOther: string;
|
||||||
|
/**
|
||||||
|
* Время работы — та же колонка сайта WorkHours (Choice), что в заявке на подбор:
|
||||||
|
* оттуда и подставляется (см. selectionPrefill).
|
||||||
|
*/
|
||||||
|
WorkHours: string;
|
||||||
|
WorkHoursOther: string;
|
||||||
/**
|
/**
|
||||||
* Сдельная оплата труда — информационная отметка, на обязательность окладов не
|
* Сдельная оплата труда — информационная отметка, на обязательность окладов не
|
||||||
* влияет. Та же колонка сайта PieceworkPay (Boolean), что в заявке на подбор:
|
* влияет. Та же колонка сайта PieceworkPay (Boolean), что в заявке на подбор:
|
||||||
@@ -132,6 +138,8 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
SalaryAfterProbation: '',
|
SalaryAfterProbation: '',
|
||||||
WorkSchedule: '',
|
WorkSchedule: '',
|
||||||
WorkScheduleOther: '',
|
WorkScheduleOther: '',
|
||||||
|
WorkHours: '',
|
||||||
|
WorkHoursOther: '',
|
||||||
PieceworkPay: false,
|
PieceworkPay: false,
|
||||||
RecruitingAgency: false,
|
RecruitingAgency: false,
|
||||||
AgencyName: '',
|
AgencyName: '',
|
||||||
@@ -201,5 +209,19 @@ export const WORK_SCHEDULE_OPTIONS: ISelectOption[] = [
|
|||||||
{ value: OTHER_VALUE, label: OTHER_VALUE }
|
{ value: OTHER_VALUE, label: OTHER_VALUE }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Варианты «Время работы» (24-часовой формат) — совпадают с набором в форме
|
||||||
|
* «Заявка на подбор» (WORK_HOURS_OPTIONS) и с choices колонки WorkHours:
|
||||||
|
* формы независимы, значения продублированы намеренно.
|
||||||
|
*/
|
||||||
|
export const WORK_HOURS_OPTIONS: ISelectOption[] = [
|
||||||
|
{ value: '08:00-17:00', label: '08:00-17:00' },
|
||||||
|
{ value: '09:00-18:00', label: '09:00-18:00' },
|
||||||
|
{ value: '07:00-19:00', label: '07:00-19:00' },
|
||||||
|
{ value: '08:00-20:00', label: '08:00-20:00' },
|
||||||
|
{ value: '09:00-21:00', label: '09:00-21:00' },
|
||||||
|
{ value: OTHER_VALUE, label: OTHER_VALUE }
|
||||||
|
];
|
||||||
|
|
||||||
/** Плейсхолдер для полей-людей. */
|
/** Плейсхолдер для полей-людей. */
|
||||||
export const PERSON_PLACEHOLDER = 'Начните вводить имя сотрудника…';
|
export const PERSON_PLACEHOLDER = 'Начните вводить имя сотрудника…';
|
||||||
|
|||||||
@@ -41,6 +41,8 @@
|
|||||||
{ "internalName": "SalaryAfterProbation", "title": "Оклад по завершении ИС", "type": "Number", "decimals": 2 },
|
{ "internalName": "SalaryAfterProbation", "title": "Оклад по завершении ИС", "type": "Number", "decimals": 2 },
|
||||||
{ "internalName": "WorkSchedule", "title": "График работы", "type": "Text" },
|
{ "internalName": "WorkSchedule", "title": "График работы", "type": "Text" },
|
||||||
{ "internalName": "WorkScheduleOther", "title": "График работы (другое)", "type": "Text" },
|
{ "internalName": "WorkScheduleOther", "title": "График работы (другое)", "type": "Text" },
|
||||||
|
{ "internalName": "WorkHours", "title": "Время работы", "type": "Choice", "choices": ["08:00-17:00", "09:00-18:00", "07:00-19:00", "08:00-20:00", "09:00-21:00", "Другое"] },
|
||||||
|
{ "internalName": "WorkHoursOther", "title": "Время работы (другое)", "type": "Text" },
|
||||||
{ "internalName": "PieceworkPay", "title": "Сдельная оплата труда", "type": "Boolean" },
|
{ "internalName": "PieceworkPay", "title": "Сдельная оплата труда", "type": "Boolean" },
|
||||||
|
|
||||||
{ "internalName": "RecruitingAgency", "title": "Рекрутинговое агентство", "type": "Boolean" },
|
{ "internalName": "RecruitingAgency", "title": "Рекрутинговое агентство", "type": "Boolean" },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { CollapseSection, FormField, TextInput, MoneyInput, Select, Toggle } from '../../../shared/ui';
|
import { CollapseSection, FormField, TextInput, MoneyInput, Select, Toggle } from '../../../shared/ui';
|
||||||
import { ISectionProps, OTHER_VALUE, WORK_SCHEDULE_OPTIONS, PROBATION_PERIOD_OPTIONS } from '../model';
|
import { ISectionProps, OTHER_VALUE, WORK_SCHEDULE_OPTIONS, WORK_HOURS_OPTIONS, PROBATION_PERIOD_OPTIONS } from '../model';
|
||||||
|
|
||||||
interface IConditionsSectionProps extends ISectionProps {
|
interface IConditionsSectionProps extends ISectionProps {
|
||||||
/** Начальник расчётной группы правит оклады даже в режиме просмотра (см. approverRights.ts). */
|
/** Начальник расчётной группы правит оклады даже в режиме просмотра (см. approverRights.ts). */
|
||||||
@@ -67,8 +67,29 @@ export const ConditionsSection: React.FC<IConditionsSectionProps> = ({ data, set
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
)}
|
)}
|
||||||
|
<FormField label="4.5 Время работы" htmlFor="WorkHours" required>
|
||||||
|
<Select
|
||||||
|
id="WorkHours"
|
||||||
|
value={data.WorkHours}
|
||||||
|
onChange={(v) => {
|
||||||
|
set('WorkHours', v);
|
||||||
|
if (v !== OTHER_VALUE) set('WorkHoursOther', '');
|
||||||
|
}}
|
||||||
|
options={WORK_HOURS_OPTIONS}
|
||||||
|
placeholder="— выберите —"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
{data.WorkHours === OTHER_VALUE && (
|
||||||
|
<FormField label="Время работы (другое)" htmlFor="WorkHoursOther">
|
||||||
|
<TextInput
|
||||||
|
id="WorkHoursOther"
|
||||||
|
value={data.WorkHoursOther}
|
||||||
|
onChange={(v) => set('WorkHoursOther', v)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
{/* Просто отметка: обязательность окладов (4.2, 4.3) от неё не зависит. */}
|
{/* Просто отметка: обязательность окладов (4.2, 4.3) от неё не зависит. */}
|
||||||
<FormField label="4.5 Сдельная оплата труда" fieldKey="PieceworkPay">
|
<FormField label="4.6 Сдельная оплата труда" fieldKey="PieceworkPay">
|
||||||
<Toggle checked={data.PieceworkPay} onChange={(v) => set('PieceworkPay', v)} onLabel="Да" />
|
<Toggle checked={data.PieceworkPay} onChange={(v) => set('PieceworkPay', v)} onLabel="Да" />
|
||||||
</FormField>
|
</FormField>
|
||||||
</CollapseSection>
|
</CollapseSection>
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export function selectionPrefill(link: ISelectionLink, selection: ISelectionData
|
|||||||
Department: selection.Department,
|
Department: selection.Department,
|
||||||
WorkSchedule: selection.WorkSchedule,
|
WorkSchedule: selection.WorkSchedule,
|
||||||
WorkScheduleOther: selection.WorkScheduleOther,
|
WorkScheduleOther: selection.WorkScheduleOther,
|
||||||
|
WorkHours: selection.WorkHours,
|
||||||
|
WorkHoursOther: selection.WorkHoursOther,
|
||||||
ProbationPeriod: selection.SelectionProbationPeriod,
|
ProbationPeriod: selection.SelectionProbationPeriod,
|
||||||
ProbationPeriodOther: selection.SelectionProbationPeriodOther,
|
ProbationPeriodOther: selection.SelectionProbationPeriodOther,
|
||||||
SalaryProbation: selection.RateProbation,
|
SalaryProbation: selection.RateProbation,
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ describe('RecruitingHire validation', () => {
|
|||||||
'4.1 Испытательный срок',
|
'4.1 Испытательный срок',
|
||||||
'4.2 Оклад при испытательном сроке',
|
'4.2 Оклад при испытательном сроке',
|
||||||
'4.3 Оклад по завершении ИС',
|
'4.3 Оклад по завершении ИС',
|
||||||
'4.4 График работы'
|
'4.4 График работы',
|
||||||
|
'4.5 Время работы'
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ const REQUIRED_RULES: ReadonlyArray<IRequiredRule> = [
|
|||||||
{ field: 'ProbationPeriod', label: '4.1 Испытательный срок', ok: (d) => !!d.ProbationPeriod },
|
{ field: 'ProbationPeriod', label: '4.1 Испытательный срок', ok: (d) => !!d.ProbationPeriod },
|
||||||
{ field: 'SalaryProbation', label: '4.2 Оклад при испытательном сроке', ok: (d) => !!d.SalaryProbation.trim() },
|
{ field: 'SalaryProbation', label: '4.2 Оклад при испытательном сроке', ok: (d) => !!d.SalaryProbation.trim() },
|
||||||
{ field: 'SalaryAfterProbation', label: '4.3 Оклад по завершении ИС', ok: (d) => !!d.SalaryAfterProbation.trim() },
|
{ field: 'SalaryAfterProbation', label: '4.3 Оклад по завершении ИС', ok: (d) => !!d.SalaryAfterProbation.trim() },
|
||||||
{ field: 'WorkSchedule', label: '4.4 График работы', ok: (d) => !!d.WorkSchedule }
|
{ field: 'WorkSchedule', label: '4.4 График работы', ok: (d) => !!d.WorkSchedule },
|
||||||
|
{ field: 'WorkHours', label: '4.5 Время работы', ok: (d) => !!d.WorkHours }
|
||||||
];
|
];
|
||||||
|
|
||||||
function toIssue({ field, label, message }: IRequiredRule): IFieldIssue {
|
function toIssue({ field, label, message }: IRequiredRule): IFieldIssue {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useState, useMemo, useCallback, useEffect } from 'react';
|
|||||||
import styles from './Ticket1CForm.module.scss';
|
import styles from './Ticket1CForm.module.scss';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
FieldIssuesProvider,
|
||||||
|
issueLabels,
|
||||||
FormCard,
|
FormCard,
|
||||||
InitiatorLine,
|
InitiatorLine,
|
||||||
FormModeProvider,
|
FormModeProvider,
|
||||||
@@ -49,7 +51,8 @@ import {
|
|||||||
dataListTitle,
|
dataListTitle,
|
||||||
dataListTitleForListId
|
dataListTitleForListId
|
||||||
} from '../../shared/listTitles';
|
} from '../../shared/listTitles';
|
||||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField, hasOtherDatabase } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
|
import { formIssues } from './validation';
|
||||||
import { saveTicketRequest, ticket1cApprovalService } from './submit';
|
import { saveTicketRequest, ticket1cApprovalService } from './submit';
|
||||||
import { loadTicketRequest } from './load';
|
import { loadTicketRequest } from './load';
|
||||||
import { getApplicantProfile } from './services/profile.service';
|
import { getApplicantProfile } from './services/profile.service';
|
||||||
@@ -240,20 +243,11 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
);
|
);
|
||||||
const matrixEditor = useMemo(() => ({ ...matrix, rows: matrixRows }), [matrix, matrixRows]);
|
const matrixEditor = useMemo(() => ({ ...matrix, rows: matrixRows }), [matrix, matrixRows]);
|
||||||
|
|
||||||
const isOtherDatabaseMissing = hasOtherDatabase(data) && !data.DB1COther.trim();
|
// Проблемы полей (см. validation.ts) подсвечиваются под полями через FieldIssuesProvider.
|
||||||
const areRequiredFilled = !!(
|
const issues = formIssues(data);
|
||||||
data.TaskName.trim() &&
|
const isValid = issues.length === 0;
|
||||||
data.WorkType &&
|
|
||||||
data.Organization &&
|
|
||||||
data.DB1C.length > 0 &&
|
|
||||||
data.Question.trim()
|
|
||||||
);
|
|
||||||
const isValid = areRequiredFilled && !isOtherDatabaseMissing;
|
|
||||||
const validate = (): string | undefined => {
|
const validate = (): string | undefined => {
|
||||||
if (!areRequiredFilled) {
|
if (!isValid) return `Заполните или исправьте поля: ${issueLabels(issues)}.`;
|
||||||
return 'Заполните обязательные поля: наименование задачи, вид работ, организация, база 1С, вопрос.';
|
|
||||||
}
|
|
||||||
if (isOtherDatabaseMissing) return 'Выбрана база 1С «Другое» — укажите её название.';
|
|
||||||
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
|
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
|
||||||
if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.';
|
if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.';
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -391,7 +385,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<FieldIssuesProvider draftIssues={issues} submitIssues={issues}>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
<Icon name="arrow-left" size="sm" />
|
<Icon name="arrow-left" size="sm" />
|
||||||
<span>К списку заявок</span>
|
<span>К списку заявок</span>
|
||||||
@@ -486,7 +480,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({
|
|||||||
submitLabel="Отправить"
|
submitLabel="Отправить"
|
||||||
/>
|
/>
|
||||||
</FormCard>
|
</FormCard>
|
||||||
</div>
|
</FieldIssuesProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ export const CHOICES_LOADING_PLACEHOLDER = 'Загрузка вариантов
|
|||||||
export const CHOICES_EMPTY_PLACEHOLDER = 'Нет вариантов — обратитесь к администратору';
|
export const CHOICES_EMPTY_PLACEHOLDER = 'Нет вариантов — обратитесь к администратору';
|
||||||
export const CHOICES_ERROR_PREFIX = 'Не удалось загрузить варианты полей';
|
export const CHOICES_ERROR_PREFIX = 'Не удалось загрузить варианты полей';
|
||||||
|
|
||||||
|
// Подсказка в поле «База 1С», когда сработало правило «дата запрета по АТП».
|
||||||
|
export const DB1C_ATP_ONLY_HINT = 'Для даты запрета по АТП другие базы не выбираются';
|
||||||
|
// Предупреждение над полем «База 1С» при виде работ «Дата запрета».
|
||||||
|
export const DB1C_ATP_ONLY_WARNING =
|
||||||
|
'Для даты запрета (открытие/закрытие) по базе АТП можно выбрать только АТП — ' +
|
||||||
|
'дополнительные базы недоступны.';
|
||||||
|
|
||||||
// Значения из property pane старой web part (см. живую страницу SharePoint).
|
// Значения из property pane старой web part (см. живую страницу SharePoint).
|
||||||
export const PLANNER_PLAN_ID = '168WqDV8CEKkW3cL6olCRZgACwYa';
|
export const PLANNER_PLAN_ID = '168WqDV8CEKkW3cL6olCRZgACwYa';
|
||||||
export const PLANNER_BUCKET_NAME = 'Входящие неотсортированные';
|
export const PLANNER_BUCKET_NAME = 'Входящие неотсортированные';
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { DB1C_ATP_OPTION, WORK_TYPE_BAN_DATE, restrictDatabases } from './model';
|
||||||
|
|
||||||
|
const ZUP = '1С: Зарплата и управление персоналом (ЗУП)';
|
||||||
|
|
||||||
|
describe('Ticket1C restrictDatabases', () => {
|
||||||
|
it('дата запрета + АТП — остаётся только АТП', () => {
|
||||||
|
expect(restrictDatabases(WORK_TYPE_BAN_DATE, [ZUP, DB1C_ATP_OPTION])).toEqual([DB1C_ATP_OPTION]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('дата запрета без АТП — базы не трогаем', () => {
|
||||||
|
expect(restrictDatabases(WORK_TYPE_BAN_DATE, [ZUP])).toEqual([ZUP]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('другой вид работ — АТП можно сочетать с другими базами', () => {
|
||||||
|
expect(restrictDatabases('Прочее', [ZUP, DB1C_ATP_OPTION])).toEqual([ZUP, DB1C_ATP_OPTION]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -44,6 +44,27 @@ export function hasOtherDatabase(data: IFormData): boolean {
|
|||||||
return data.DB1C.indexOf(DB1C_OTHER_OPTION) !== -1;
|
return data.DB1C.indexOf(DB1C_OTHER_OPTION) !== -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Варианты колонок SP, для которых действует правило «дата запрета по АТП —
|
||||||
|
* только одна база». Должны дословно совпадать с вариантами колонок
|
||||||
|
* WorkType и DB1C в SharePoint (варианты читаются из SP, см. useFieldChoices).
|
||||||
|
*/
|
||||||
|
export const WORK_TYPE_BAN_DATE = 'Дата запрета (открытие/закрытие)';
|
||||||
|
export const DB1C_ATP_OPTION = '1С: Автотранспортное предприятие (АТП)';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Дата запрета в АТП открывается/закрывается отдельно от остальных баз,
|
||||||
|
* поэтому в такой заявке база АТП может быть только одна.
|
||||||
|
*/
|
||||||
|
export function isAtpOnlyBanDate(workType: string, databases: string[]): boolean {
|
||||||
|
return workType === WORK_TYPE_BAN_DATE && databases.indexOf(DB1C_ATP_OPTION) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Применяет правило isAtpOnlyBanDate: при его срабатывании оставляет только АТП. */
|
||||||
|
export function restrictDatabases(workType: string, databases: string[]): string[] {
|
||||||
|
return isAtpOnlyBanDate(workType, databases) ? [DB1C_ATP_OPTION] : databases;
|
||||||
|
}
|
||||||
|
|
||||||
/** Обновление одного поля формы с сохранением типа значения. */
|
/** Обновление одного поля формы с сохранением типа значения. */
|
||||||
export type SetField = <K extends keyof IFormData>(key: K, value: IFormData[K]) => void;
|
export type SetField = <K extends keyof IFormData>(key: K, value: IFormData[K]) => void;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// Строка на обе колонки сетки секции — как FormField с full.
|
||||||
|
.fullRow {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
@@ -1,10 +1,21 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import styles from './RequestSection.module.scss';
|
||||||
import { Alert, CollapseSection, FormField, MultiChoiceField, Select, TextArea, TextInput, ISelectOption } from '../../../shared/ui';
|
import { Alert, CollapseSection, FormField, MultiChoiceField, Select, TextArea, TextInput, ISelectOption } from '../../../shared/ui';
|
||||||
import { useFieldChoices, IFieldChoicesState } from '../../../shared/fieldChoices';
|
import { useFieldChoices, IFieldChoicesState } from '../../../shared/fieldChoices';
|
||||||
import { dataListTitle } from '../../../shared/listTitles';
|
import { dataListTitle } from '../../../shared/listTitles';
|
||||||
import { DB1C_OTHER_OPTION, FORM_ID, ISectionProps, hasOtherDatabase } from '../model';
|
import {
|
||||||
|
DB1C_OTHER_OPTION,
|
||||||
|
FORM_ID,
|
||||||
|
WORK_TYPE_BAN_DATE,
|
||||||
|
ISectionProps,
|
||||||
|
hasOtherDatabase,
|
||||||
|
isAtpOnlyBanDate,
|
||||||
|
restrictDatabases
|
||||||
|
} from '../model';
|
||||||
import {
|
import {
|
||||||
QUESTION_MAX,
|
QUESTION_MAX,
|
||||||
|
DB1C_ATP_ONLY_HINT,
|
||||||
|
DB1C_ATP_ONLY_WARNING,
|
||||||
CHOICES_LOADING_PLACEHOLDER,
|
CHOICES_LOADING_PLACEHOLDER,
|
||||||
CHOICES_EMPTY_PLACEHOLDER,
|
CHOICES_EMPTY_PLACEHOLDER,
|
||||||
CHOICES_ERROR_PREFIX
|
CHOICES_ERROR_PREFIX
|
||||||
@@ -30,6 +41,16 @@ export const RequestSection: React.FC<ISectionProps> = ({ data, set }) => {
|
|||||||
const organizations = useFieldChoices(listTitle, 'Organization');
|
const organizations = useFieldChoices(listTitle, 'Organization');
|
||||||
const databases = useFieldChoices(listTitle, 'DB1C');
|
const databases = useFieldChoices(listTitle, 'DB1C');
|
||||||
const choicesError = workTypes.error || organizations.error || databases.error;
|
const choicesError = workTypes.error || organizations.error || databases.error;
|
||||||
|
const isAtpOnly = isAtpOnlyBanDate(data.WorkType, data.DB1C);
|
||||||
|
|
||||||
|
// Правило «дата запрета по АТП» зависит и от вида работ, и от баз — поэтому
|
||||||
|
// применяется при смене любого из двух полей.
|
||||||
|
const setDatabases = (workType: string, values: string[]): void => {
|
||||||
|
const restricted = restrictDatabases(workType, values);
|
||||||
|
set('DB1C', restricted);
|
||||||
|
// Сняли «Другое» — вписанное название больше не относится к заявке.
|
||||||
|
if (restricted.indexOf(DB1C_OTHER_OPTION) === -1) set('DB1COther', '');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapseSection title="Заявка" icon="clipboard">
|
<CollapseSection title="Заявка" icon="clipboard">
|
||||||
@@ -41,7 +62,10 @@ export const RequestSection: React.FC<ISectionProps> = ({ data, set }) => {
|
|||||||
<Select
|
<Select
|
||||||
id="WorkType"
|
id="WorkType"
|
||||||
value={data.WorkType}
|
value={data.WorkType}
|
||||||
onChange={(v) => set('WorkType', v)}
|
onChange={(v) => {
|
||||||
|
set('WorkType', v);
|
||||||
|
setDatabases(v, data.DB1C);
|
||||||
|
}}
|
||||||
options={withCurrentValue(workTypes.options, data.WorkType)}
|
options={withCurrentValue(workTypes.options, data.WorkType)}
|
||||||
placeholder={choicesPlaceholder(workTypes, '— выберите —')}
|
placeholder={choicesPlaceholder(workTypes, '— выберите —')}
|
||||||
/>
|
/>
|
||||||
@@ -55,18 +79,22 @@ export const RequestSection: React.FC<ISectionProps> = ({ data, set }) => {
|
|||||||
placeholder={choicesPlaceholder(organizations, '— выберите —')}
|
placeholder={choicesPlaceholder(organizations, '— выберите —')}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
{/* Предупреждаем заранее, ещё до выбора баз, чтобы правило не стало сюрпризом. */}
|
||||||
|
{data.WorkType === WORK_TYPE_BAN_DATE && (
|
||||||
|
<div className={styles.fullRow}>
|
||||||
|
<Alert variant="warn">{DB1C_ATP_ONLY_WARNING}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* На всю ширину: чипы выбранных баз растят поле по высоте, и соседнее
|
{/* На всю ширину: чипы выбранных баз растят поле по высоте, и соседнее
|
||||||
поле в той же строке сетки съезжало бы относительно него. */}
|
поле в той же строке сетки съезжало бы относительно него. */}
|
||||||
<FormField label="База 1С" fieldKey="DB1C" required full>
|
<FormField label="База 1С" fieldKey="DB1C" required full>
|
||||||
<MultiChoiceField
|
<MultiChoiceField
|
||||||
value={data.DB1C}
|
value={data.DB1C}
|
||||||
onChange={(v) => {
|
onChange={(v) => setDatabases(data.WorkType, v)}
|
||||||
set('DB1C', v);
|
// Пустой список вариантов прячет выпадашку — добавить другую базу нельзя,
|
||||||
// Сняли «Другое» — вписанное название больше не относится к заявке.
|
// пока не уберут АТП или не сменят вид работ.
|
||||||
if (v.indexOf(DB1C_OTHER_OPTION) === -1) set('DB1COther', '');
|
options={isAtpOnly ? [] : databases.options}
|
||||||
}}
|
placeholder={isAtpOnly ? DB1C_ATP_ONLY_HINT : choicesPlaceholder(databases, 'Выберите одну или несколько баз')}
|
||||||
options={databases.options}
|
|
||||||
placeholder={choicesPlaceholder(databases, 'Выберите одну или несколько баз')}
|
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
{hasOtherDatabase(data) && (
|
{hasOtherDatabase(data) && (
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { IFieldIssue } from '../../shared/ui';
|
||||||
|
import { IFormData, hasOtherDatabase } from './model';
|
||||||
|
|
||||||
|
/** Название базы вручную обязательно, только если в «База 1С» отмечено «Другое». */
|
||||||
|
function otherDatabaseIssue(data: IFormData): IFieldIssue | undefined {
|
||||||
|
if (!hasOtherDatabase(data) || data.DB1COther.trim()) return undefined;
|
||||||
|
return { field: 'DB1COther', label: 'Другая база 1С', message: 'выбрано «Другое» — укажите название базы' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Всё, что мешает сохранить или отправить заявку, в порядке формы (пусто — можно).
|
||||||
|
* field — ключ FormField (htmlFor/fieldKey): по нему поле подсвечивается и к нему
|
||||||
|
* прокручивается форма.
|
||||||
|
*/
|
||||||
|
export function formIssues(data: IFormData): IFieldIssue[] {
|
||||||
|
const issues: Array<IFieldIssue | undefined> = [
|
||||||
|
data.TaskName.trim() ? undefined : { field: 'TaskName', label: 'Наименование задачи' },
|
||||||
|
data.WorkType ? undefined : { field: 'WorkType', label: 'Виды работ' },
|
||||||
|
data.Organization ? undefined : { field: 'Organization', label: 'Организация' },
|
||||||
|
data.DB1C.length > 0 ? undefined : { field: 'DB1C', label: 'База 1С' },
|
||||||
|
otherDatabaseIssue(data),
|
||||||
|
data.Question.trim() ? undefined : { field: 'Question', label: 'Вопрос' }
|
||||||
|
];
|
||||||
|
return issues.filter((issue): issue is IFieldIssue => issue !== undefined);
|
||||||
|
}
|
||||||
@@ -366,6 +366,7 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
|
formId={FORM_ID}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
onSaveDraft={handleSaveDraft}
|
onSaveDraft={handleSaveDraft}
|
||||||
|
|||||||
@@ -385,6 +385,7 @@ const VacationForm: React.FC<IRequestFormProps> = ({
|
|||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
|
formId={FORM_ID}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
onSaveDraft={handleSaveDraft}
|
onSaveDraft={handleSaveDraft}
|
||||||
|
|||||||
@@ -25,15 +25,18 @@ const SUBMISSION_FROZEN = true;
|
|||||||
* Формы, для которых заморозка снята досрочно (для всех пользователей, не
|
* Формы, для которых заморозка снята досрочно (для всех пользователей, не
|
||||||
* только тестировщиков) — id передаётся через проп formId. Заявка 1С
|
* только тестировщиков) — id передаётся через проп formId. Заявка 1С
|
||||||
* разморожена 2026-09-22, формы рекрутинга (на подбор/на приём/реферал) и
|
* разморожена 2026-09-22, формы рекрутинга (на подбор/на приём/реферал) и
|
||||||
* «Организация рабочего места» — 2026-09-24, все по просьбе Тимура
|
* «Организация рабочего места» — 2026-09-24, обе заявки на отпуск (оплачиваемый
|
||||||
* Городецкого; остальные формы остаются замороженными.
|
* и без сохранения з/п) — 2026-10-16, все по просьбе Тимура Городецкого;
|
||||||
|
* остальные формы остаются замороженными.
|
||||||
*/
|
*/
|
||||||
const UNFROZEN_FORMS = new Set<string>([
|
const UNFROZEN_FORMS = new Set<string>([
|
||||||
'ticket1c',
|
'ticket1c',
|
||||||
'recruiting-selection',
|
'recruiting-selection',
|
||||||
'recruiting-hire',
|
'recruiting-hire',
|
||||||
'recruiting-referral',
|
'recruiting-referral',
|
||||||
'it-workplace-setup'
|
'it-workplace-setup',
|
||||||
|
'hr-vacation',
|
||||||
|
'hr-unpaid-leave'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export interface ISubmitActionsProps {
|
export interface ISubmitActionsProps {
|
||||||
|
|||||||
Reference in New Issue
Block a user