feat: подсказка о недопустимой дате увольнения

Если последний рабочий день раньше, чем через месяц от сегодня, под полем
показывается причина и минимальная дата. Раньше кнопки отправки просто
становились неактивными без объяснения — это случалось у заявлений,
пролежавших на доработке: дата, допустимая при подаче, переставала
подходить. Правило и текст вынесены в lastWorkingDay.ts и общие для
подсказки и проверки перед отправкой.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-09-29 12:42:13 +05:00
parent 7ad5f79cdf
commit 5b1e94bf42
4 changed files with 115 additions and 75 deletions
@@ -28,14 +28,14 @@ import {
import { IRequestFormProps } from '../../models/types';
import { getEmployeeOrganizations, getCompanyDirectorEmail, IEmployeeOrganization } from '../../shared/dictionary';
import { friendlyErrorMessage } from '../../shared/diagnostics';
import { isoDatePlusMonths, formatIsoDate } from '../../shared/dateUtils';
import { lastWorkingDayError } from './lastWorkingDay';
import {
approvalListTitle,
approvalListTitleForListId,
dataListTitle,
dataListTitleForListId
} from '../../shared/listTitles';
import { FORM_ID, IFormData, EMPTY_DATA, SetField, MIN_NOTICE_MONTHS, IOrganizationRecord, primaryPosition } from './model';
import { FORM_ID, IFormData, EMPTY_DATA, SetField, IOrganizationRecord, primaryPosition } from './model';
import { saveDismissalRequest, dismissalApprovalService } from './submit';
import { loadDismissalRequest } from './load';
import { RequestSection } from './sections/RequestSection';
@@ -197,15 +197,13 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
// Последний рабочий день нужно назначать минимум за MIN_NOTICE_MONTHS месяц —
// календарь (min у input type=date, см. RequestSection) не даёт выбрать более
// раннюю дату, но проверяем и здесь на случай обхода UI.
const minLastWorkingDay = isoDatePlusMonths(MIN_NOTICE_MONTHS);
const isLastDayValid = !data.LastWorkingDay || data.LastWorkingDay >= minLastWorkingDay;
// раннюю дату, но проверяем и здесь на случай обхода UI. Тот же текст
// RequestSection показывает подсказкой под полем (см. lastWorkingDay.ts).
const lastDayError = lastWorkingDayError(data.LastWorkingDay);
const position = primaryPosition(data.Organization);
const isValid = !!(data.Employee && position.trim() && data.LastWorkingDay && isLastDayValid && data.Phone.trim());
const isValid = !!(data.Employee && position.trim() && data.LastWorkingDay && !lastDayError && data.Phone.trim());
const validate = (): string | undefined => {
if (data.LastWorkingDay && !isLastDayValid) {
return `Последний рабочий день нужно назначать минимум за ${MIN_NOTICE_MONTHS} месяц — минимально доступная дата: ${formatIsoDate(minLastWorkingDay)}.`;
}
if (lastDayError) return lastDayError;
if (!data.Employee || !position.trim() || !data.LastWorkingDay || !data.Phone.trim()) {
return 'Заполните обязательные поля: сотрудник, должность, последний рабочий день, контактный телефон.';
}
@@ -0,0 +1,23 @@
import { formatIsoDate, isoDatePlusMonths } from '../../shared/dateUtils';
import { MIN_NOTICE_MONTHS } from './model';
/** Самый ранний допустимый последний рабочий день ('YYYY-MM-DD'): сегодня + MIN_NOTICE_MONTHS. */
export function minLastWorkingDay(): string {
return isoDatePlusMonths(MIN_NOTICE_MONTHS);
}
/**
* Почему последний рабочий день не подходит; undefined — подходит или ещё не
* выбран. Одна функция и для подсказки под полем (RequestSection), и для
* проверки перед отправкой (DismissalForm) — чтобы текст и правило не разошлись.
*
* Правило считается от СЕГОДНЯ: у заявления, пролежавшего на доработке, дата,
* бывшая допустимой при подаче, может перестать подходить — подсказка сразу
* объясняет, почему кнопки отправки неактивны.
*/
export function lastWorkingDayError(value: string): string | undefined {
if (!value) return undefined;
const min = minLastWorkingDay();
if (value >= min) return undefined;
return `Последний рабочий день нужно назначать минимум за ${MIN_NOTICE_MONTHS} месяц — минимально доступная дата: ${formatIsoDate(min)}.`;
}
@@ -0,0 +1,8 @@
@import '../../../../../styles/design-tokens';
// Пояснение под полем, почему значение не подходит (см. lastWorkingDayError).
.fieldError {
margin-top: $sp-1;
font-size: $fs-label;
color: $red-500;
}
@@ -1,8 +1,10 @@
import * as React from 'react';
import { FormGrid, FormField, TextInput, TextArea, PhoneInput, PeopleField, LinkField } from '../../../shared/ui';
import { ISectionProps, EMPLOYEE_PLACEHOLDER, MIN_NOTICE_MONTHS } from '../model';
import { isoDatePlusMonths } from '../../../shared/dateUtils';
import { useFormReadOnly } from '@tnsu/ui-kit';
import { ISectionProps, EMPLOYEE_PLACEHOLDER } from '../model';
import { lastWorkingDayError, minLastWorkingDay } from '../lastWorkingDay';
import { OrganizationsTable } from './OrganizationsTable';
import styles from './RequestSection.module.scss';
interface IRequestSectionProps extends ISectionProps {
/**
@@ -27,7 +29,12 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
onEmployeeChange,
onRemoveOrganization,
assetsAcceptorEditable
}) => (
}) => {
// В просмотре (DismissalView) секция только для чтения — подсказка там ни к чему.
const readOnly = useFormReadOnly();
const lastDayError = lastWorkingDayError(data.LastWorkingDay);
return (
<FormGrid>
<FormField label="Сотрудник" htmlFor="Employee" required full>
<PeopleField
@@ -56,15 +63,18 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
onRemove={onRemoveOrganization}
/>
{/* Заявление подаётся заранее — датой увольнения нельзя назначить ближе,
чем через MIN_NOTICE_MONTHS месяц от сегодня (min). */}
чем через MIN_NOTICE_MONTHS месяц от сегодня (min). Если дата уже стоит
раньше (заявление пролежало на доработке) — подсказка под полем, иначе
непонятно, почему кнопки отправки неактивны. */}
<FormField label="Последний рабочий день" htmlFor="LastWorkingDay" required>
<TextInput
id="LastWorkingDay"
type="date"
value={data.LastWorkingDay}
onChange={(v) => set('LastWorkingDay', v)}
min={isoDatePlusMonths(MIN_NOTICE_MONTHS)}
min={minLastWorkingDay()}
/>
{!readOnly && lastDayError && <div className={styles.fieldError}>{lastDayError}</div>}
</FormField>
<FormField label="Контактный телефон" htmlFor="Phone" required>
<PhoneInput id="Phone" value={data.Phone} onChange={(v) => set('Phone', v)} />
@@ -92,4 +102,5 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
/>
</FormField>
</FormGrid>
);
);
};