feat: подсказка о недопустимой дате увольнения
Если последний рабочий день раньше, чем через месяц от сегодня, под полем показывается причина и минимальная дата. Раньше кнопки отправки просто становились неактивными без объяснения — это случалось у заявлений, пролежавших на доработке: дата, допустимая при подаче, переставала подходить. Правило и текст вынесены в lastWorkingDay.ts и общие для подсказки и проверки перед отправкой. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,14 +28,14 @@ import {
|
|||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { getEmployeeOrganizations, getCompanyDirectorEmail, IEmployeeOrganization } from '../../shared/dictionary';
|
import { getEmployeeOrganizations, getCompanyDirectorEmail, IEmployeeOrganization } from '../../shared/dictionary';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import { isoDatePlusMonths, formatIsoDate } from '../../shared/dateUtils';
|
import { lastWorkingDayError } from './lastWorkingDay';
|
||||||
import {
|
import {
|
||||||
approvalListTitle,
|
approvalListTitle,
|
||||||
approvalListTitleForListId,
|
approvalListTitleForListId,
|
||||||
dataListTitle,
|
dataListTitle,
|
||||||
dataListTitleForListId
|
dataListTitleForListId
|
||||||
} from '../../shared/listTitles';
|
} 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 { saveDismissalRequest, dismissalApprovalService } from './submit';
|
||||||
import { loadDismissalRequest } from './load';
|
import { loadDismissalRequest } from './load';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
@@ -197,15 +197,13 @@ const DismissalForm: React.FC<IRequestFormProps> = ({
|
|||||||
|
|
||||||
// Последний рабочий день нужно назначать минимум за MIN_NOTICE_MONTHS месяц —
|
// Последний рабочий день нужно назначать минимум за MIN_NOTICE_MONTHS месяц —
|
||||||
// календарь (min у input type=date, см. RequestSection) не даёт выбрать более
|
// календарь (min у input type=date, см. RequestSection) не даёт выбрать более
|
||||||
// раннюю дату, но проверяем и здесь на случай обхода UI.
|
// раннюю дату, но проверяем и здесь на случай обхода UI. Тот же текст
|
||||||
const minLastWorkingDay = isoDatePlusMonths(MIN_NOTICE_MONTHS);
|
// RequestSection показывает подсказкой под полем (см. lastWorkingDay.ts).
|
||||||
const isLastDayValid = !data.LastWorkingDay || data.LastWorkingDay >= minLastWorkingDay;
|
const lastDayError = lastWorkingDayError(data.LastWorkingDay);
|
||||||
const position = primaryPosition(data.Organization);
|
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 => {
|
const validate = (): string | undefined => {
|
||||||
if (data.LastWorkingDay && !isLastDayValid) {
|
if (lastDayError) return lastDayError;
|
||||||
return `Последний рабочий день нужно назначать минимум за ${MIN_NOTICE_MONTHS} месяц — минимально доступная дата: ${formatIsoDate(minLastWorkingDay)}.`;
|
|
||||||
}
|
|
||||||
if (!data.Employee || !position.trim() || !data.LastWorkingDay || !data.Phone.trim()) {
|
if (!data.Employee || !position.trim() || !data.LastWorkingDay || !data.Phone.trim()) {
|
||||||
return 'Заполните обязательные поля: сотрудник, должность, последний рабочий день, контактный телефон.';
|
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 * as React from 'react';
|
||||||
import { FormGrid, FormField, TextInput, TextArea, PhoneInput, PeopleField, LinkField } from '../../../shared/ui';
|
import { FormGrid, FormField, TextInput, TextArea, PhoneInput, PeopleField, LinkField } from '../../../shared/ui';
|
||||||
import { ISectionProps, EMPLOYEE_PLACEHOLDER, MIN_NOTICE_MONTHS } from '../model';
|
import { useFormReadOnly } from '@tnsu/ui-kit';
|
||||||
import { isoDatePlusMonths } from '../../../shared/dateUtils';
|
import { ISectionProps, EMPLOYEE_PLACEHOLDER } from '../model';
|
||||||
|
import { lastWorkingDayError, minLastWorkingDay } from '../lastWorkingDay';
|
||||||
import { OrganizationsTable } from './OrganizationsTable';
|
import { OrganizationsTable } from './OrganizationsTable';
|
||||||
|
import styles from './RequestSection.module.scss';
|
||||||
|
|
||||||
interface IRequestSectionProps extends ISectionProps {
|
interface IRequestSectionProps extends ISectionProps {
|
||||||
/**
|
/**
|
||||||
@@ -27,7 +29,12 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
|
|||||||
onEmployeeChange,
|
onEmployeeChange,
|
||||||
onRemoveOrganization,
|
onRemoveOrganization,
|
||||||
assetsAcceptorEditable
|
assetsAcceptorEditable
|
||||||
}) => (
|
}) => {
|
||||||
|
// В просмотре (DismissalView) секция только для чтения — подсказка там ни к чему.
|
||||||
|
const readOnly = useFormReadOnly();
|
||||||
|
const lastDayError = lastWorkingDayError(data.LastWorkingDay);
|
||||||
|
|
||||||
|
return (
|
||||||
<FormGrid>
|
<FormGrid>
|
||||||
<FormField label="Сотрудник" htmlFor="Employee" required full>
|
<FormField label="Сотрудник" htmlFor="Employee" required full>
|
||||||
<PeopleField
|
<PeopleField
|
||||||
@@ -56,15 +63,18 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
|
|||||||
onRemove={onRemoveOrganization}
|
onRemove={onRemoveOrganization}
|
||||||
/>
|
/>
|
||||||
{/* Заявление подаётся заранее — датой увольнения нельзя назначить ближе,
|
{/* Заявление подаётся заранее — датой увольнения нельзя назначить ближе,
|
||||||
чем через MIN_NOTICE_MONTHS месяц от сегодня (min). */}
|
чем через MIN_NOTICE_MONTHS месяц от сегодня (min). Если дата уже стоит
|
||||||
|
раньше (заявление пролежало на доработке) — подсказка под полем, иначе
|
||||||
|
непонятно, почему кнопки отправки неактивны. */}
|
||||||
<FormField label="Последний рабочий день" htmlFor="LastWorkingDay" required>
|
<FormField label="Последний рабочий день" htmlFor="LastWorkingDay" required>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="LastWorkingDay"
|
id="LastWorkingDay"
|
||||||
type="date"
|
type="date"
|
||||||
value={data.LastWorkingDay}
|
value={data.LastWorkingDay}
|
||||||
onChange={(v) => set('LastWorkingDay', v)}
|
onChange={(v) => set('LastWorkingDay', v)}
|
||||||
min={isoDatePlusMonths(MIN_NOTICE_MONTHS)}
|
min={minLastWorkingDay()}
|
||||||
/>
|
/>
|
||||||
|
{!readOnly && lastDayError && <div className={styles.fieldError}>{lastDayError}</div>}
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Контактный телефон" htmlFor="Phone" required>
|
<FormField label="Контактный телефон" htmlFor="Phone" required>
|
||||||
<PhoneInput id="Phone" value={data.Phone} onChange={(v) => set('Phone', v)} />
|
<PhoneInput id="Phone" value={data.Phone} onChange={(v) => set('Phone', v)} />
|
||||||
@@ -92,4 +102,5 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</FormGrid>
|
</FormGrid>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user