4 Commits

Author SHA1 Message Date
Тимур Городецкий 41cce93b41 fix: плитки заявок одной высоты, плашка «В разработке» по размеру текста
Tests / frontend-test (push) Successful in 2m3s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 11:24:44 +05:00
Тимур Городецкий 664b58a44a fix: трудовой отпуск подаётся за 7 дней вместо 14
По требованию HR минимальный срок подачи заявления сокращён до недели.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 11:24:43 +05:00
Тимур Городецкий 755cfc8329 fix: отпуск без содержания — без лимита 5 дней, «Иная причина»
5 дней работодатель обязан дать только при браке/рождении/смерти,
сверх — по договорённости, поэтому форма больше не ограничивает число
дней. Вариант «В иных случаях» подписан как «Иная причина» — читается
как ответ на «Причина»; значение в SP прежнее.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 11:24:42 +05:00
Тимур Городецкий e46632d67a fix: примечание СБ в заявке на приём — необязательное
По просьбе заказчика примечание при отметке «Согласован СБ» больше не
требуется для отправки: поле по-прежнему появляется только при отметке,
но его можно оставить пустым.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 15:44:17 +05:00
10 changed files with 27 additions and 33 deletions
@@ -40,6 +40,8 @@
align-items: center;
gap: 16px;
width: 100%;
// Плитки одного ряда — одной высоты, даже если заголовок у соседа в 3 строки.
height: 100%;
padding: 22px 24px;
text-align: left;
background: $white;
@@ -122,7 +124,9 @@
.tileBody {
display: flex;
flex-direction: column;
gap: 2px;
// Без этого плашка «В разработке» растягивается на ширину заголовка.
align-items: flex-start;
gap: 4px;
min-width: 0;
}
@@ -86,7 +86,7 @@ export interface IFormData {
AgencyName: string;
AgencyTerms: string;
// Служба безопасности: инициатор при подаче отмечает, что кандидат согласован
// с СБ; при отметке обязательно примечание (см. validation.ts). Информационное —
// с СБ; примечание при отметке — по желанию. Информационное —
// на маршрут и решения не влияет.
HireSecurityApproved: boolean;
HireSecurityNote: string;
@@ -4,7 +4,7 @@ import { ISectionProps } from '../model';
/**
* 6. Служба безопасности: инициатор отмечает, что кандидат согласован с СБ, и
* тогда обязательно пишет примечание (см. validation.ts). Поле информационное —
* при желании пишет примечание (необязательно). Поле информационное —
* на маршрут и решения согласующих не влияет. При снятии отметки примечание
* очищается, чтобы в заявку не ушёл текст скрытого поля (как в AgencySection).
*/
@@ -20,7 +20,7 @@ export const SecuritySection: React.FC<ISectionProps> = ({ data, set }) => {
<Toggle checked={data.HireSecurityApproved} onChange={handleToggle} onLabel="Да" />
</FormField>
{data.HireSecurityApproved && (
<FormField label="6.2 Примечание СБ" htmlFor="HireSecurityNote" required full>
<FormField label="6.2 Примечание СБ" htmlFor="HireSecurityNote" full>
<TextArea
id="HireSecurityNote"
value={data.HireSecurityNote}
@@ -38,13 +38,8 @@ describe('RecruitingHire validation', () => {
});
});
it('«Согласован СБ» требует примечание СБ, без отметки — не требует', () => {
expect(labels(submitIssues(DRAFT_READY))).not.toContain('6.2 Примечание СБ');
expect(labels(submitIssues({ ...DRAFT_READY, HireSecurityApproved: true }))).toContain('6.2 Примечание СБ');
expect(
labels(submitIssues({ ...DRAFT_READY, HireSecurityApproved: true, HireSecurityNote: 'Проверка пройдена' }))
).not.toContain('6.2 Примечание СБ');
// Черновик без примечания сохранить можно.
it('примечание СБ необязательно, даже при отметке «Согласован СБ»', () => {
expect(labels(submitIssues({ ...DRAFT_READY, HireSecurityApproved: true }))).not.toContain('6.2 Примечание СБ');
expect(draftIssues({ ...DRAFT_READY, HireSecurityApproved: true })).toEqual([]);
});
@@ -19,7 +19,7 @@ interface IRequiredRule extends IFieldIssue {
* агентство» не проверяется — у булевых нет пустого состояния.
* 2.2 «Дата приёма» не нужна, если отмечено «По приезду на объект»
* (HireOnArrival) — тогда дату проставляет исполнитель на своём шаге.
* 6.2 «Примечание СБ» обязательно только при отметке 6.1 «Согласован СБ».
* 6.2 «Примечание СБ» необязательно (по просьбе заказчика) — даже при отметке 6.1.
*
* 1.3 «Проект» — хотя бы один проект из справочника: первый уходит в resolve
* маршрута (projectId), а без него approval-route отвечал 404 и заявка ушла бы
@@ -49,13 +49,7 @@ const REQUIRED_RULES: ReadonlyArray<IRequiredRule> = [
{ field: 'ProbationPeriod', label: '4.1 Испытательный срок', ok: (d) => !!d.ProbationPeriod },
{ field: 'SalaryProbation', label: '4.2 Оклад при испытательном сроке', ok: (d) => !!d.SalaryProbation.trim() },
{ field: 'SalaryAfterProbation', label: '4.3 Оклад по завершении ИС', ok: (d) => !!d.SalaryAfterProbation.trim() },
{ field: 'WorkSchedule', label: '4.4 График работы', ok: (d) => !!d.WorkSchedule },
{
field: 'HireSecurityNote',
label: '6.2 Примечание СБ',
message: 'отмечено «Согласован СБ» — укажите примечание',
ok: (d) => !d.HireSecurityApproved || !!d.HireSecurityNote.trim()
}
{ field: 'WorkSchedule', label: '4.4 График работы', ok: (d) => !!d.WorkSchedule }
];
function toIssue({ field, label, message }: IRequiredRule): IFieldIssue {
@@ -22,10 +22,11 @@
- **Период вводится двумя датами** (с / по), обе обязательны, конец ≥ начала.
`DaysCount` считается при сохранении (включительно) для отчётности — в UI не показывается.
- **Причина** (`UnpaidLeaveReasonType`, Choice) — обязательна: брак / рождение ребёнка /
смерть близких / «В иных случаях». Только при «иных случаях» появляется обязательный
смерть близких / «В иных случаях» (в форме подписано «Иная причина», значение в SP прежнее). Только при «иных случаях» появляется обязательный
текст причины (`Reason`). У заявок до появления колонки причины нет — их прежний
текст из `Reason` по-прежнему показывается (`isReasonTextShown`).
- **Не более 5 календарных дней** (`MAX_DAYS`) — при любой причине.
- **Количество дней без верхнего предела** — 5 дней работодатель обязан дать при браке /
рождении / смерти, остальное — по договорённости, поэтому форма лимит не ставит.
- **Правило «не позднее чем за 14 дней»** (`MIN_NOTICE_DAYS`) — только для «иных случаев»
(`requiresAdvanceNotice`): брак, рождение и смерть заранее не планируются.
- **Подсветка проблемных полей** — `FieldIssuesProvider` + правила в `validation.ts`
@@ -94,8 +94,6 @@ export const SUBSTITUTE_PLACEHOLDER = 'Начните вводить Ф.И.О.
* Действует только для «иных случаев» — см. requiresAdvanceNotice.
*/
export const MIN_NOTICE_DAYS = 14;
/** Отпуск без сохранения заработной платы — не более стольких календарных дней (по любой причине). */
export const MAX_DAYS = 5;
/** Причины отпуска. Значения совпадают с choices колонки UnpaidLeaveReasonType. */
export const REASON_MARRIAGE = 'Регистрация брака';
@@ -108,7 +106,8 @@ export const UNPAID_LEAVE_REASONS: ISelectOption[] = [
{ value: REASON_MARRIAGE, label: REASON_MARRIAGE },
{ value: REASON_CHILD_BIRTH, label: REASON_CHILD_BIRTH },
{ value: REASON_RELATIVE_DEATH, label: REASON_RELATIVE_DEATH },
{ value: REASON_OTHER, label: REASON_OTHER }
// Значение в SP остаётся прежним (choices колонки и старые заявки), меняется только подпись.
{ value: REASON_OTHER, label: 'Иная причина' }
];
/**
@@ -6,7 +6,6 @@ import {
EMPLOYEE_PLACEHOLDER,
SUBSTITUTE_PLACEHOLDER,
MIN_NOTICE_DAYS,
MAX_DAYS,
REASON_OTHER,
UNPAID_LEAVE_REASONS,
requiresAdvanceNotice,
@@ -61,14 +60,13 @@ export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, on
min={requiresAdvanceNotice(data.ReasonType) ? isoDatePlusDays(MIN_NOTICE_DAYS) : undefined}
/>
</FormField>
<FormField label={`Количество дней (не более ${MAX_DAYS})`} htmlFor="DaysCount" required>
<FormField label="Количество дней" htmlFor="DaysCount" required>
<TextInput
id="DaysCount"
type="number"
value={data.DaysCount}
onChange={(v) => set('DaysCount', v)}
min={1}
max={MAX_DAYS}
/>
</FormField>
<FormField label="Причина" htmlFor="UnpaidLeaveReasonType" required full>
@@ -1,6 +1,6 @@
import type { IFieldIssue } from '../../shared/ui';
import { isoDatePlusDays, formatIsoDate } from '../../shared/dateUtils';
import { IFormData, MAX_DAYS, MIN_NOTICE_DAYS, REASON_OTHER, requiresAdvanceNotice } from './model';
import { IFormData, MIN_NOTICE_DAYS, REASON_OTHER, requiresAdvanceNotice } from './model';
/** Дата начала: обязательна, а в «иных случаях» — не раньше чем через MIN_NOTICE_DAYS дней. */
function startDateIssue(data: IFormData): IFieldIssue | undefined {
@@ -12,17 +12,20 @@ function startDateIssue(data: IFormData): IFieldIssue | undefined {
return {
field: 'StartDate',
label: 'Дата начала',
message: `в иных случаях заявление подают не позднее чем за ${MIN_NOTICE_DAYS} дней — не раньше ${formatIsoDate(minStartDate)}`
message: `при иной причине заявление подают не позднее чем за ${MIN_NOTICE_DAYS} дней — не раньше ${formatIsoDate(minStartDate)}`
};
}
/** Количество дней: обязательно, от 1 до MAX_DAYS. */
/**
* Количество дней: обязательно, целое и больше нуля. Верхнего предела нет:
* сверх 5 дней (обязательных при браке/рождении/смерти) — по договорённости с работодателем.
*/
function daysCountIssue(data: IFormData): IFieldIssue | undefined {
if (!data.DaysCount.trim()) return { field: 'DaysCount', label: 'Количество дней' };
const days = Number(data.DaysCount);
if (Number.isFinite(days) && days > 0 && days <= MAX_DAYS) return undefined;
return { field: 'DaysCount', label: 'Количество дней', message: `от 1 до ${MAX_DAYS} календарных дней` };
if (Number.isInteger(days) && days > 0) return undefined;
return { field: 'DaysCount', label: 'Количество дней', message: 'целое число больше нуля' };
}
/**
@@ -79,6 +79,6 @@ export const EMPTY_DATA: IFormData = {
/** Плейсхолдер для поля выбора сотрудника. */
export const EMPLOYEE_PLACEHOLDER = 'Начните вводить Ф.И.О. сотрудника…';
/** Заявку нужно подавать не позднее чем за столько дней до даты начала отпуска. */
export const MIN_NOTICE_DAYS = 14;
export const MIN_NOTICE_DAYS = 7;
/** Плейсхолдер для поля выбора замещающего. */
export const SUBSTITUTE_PLACEHOLDER = 'Начните вводить Ф.И.О. замещающего…';