From 5b1e94bf42bad730707dab8f85a01957a5364c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Tue, 29 Sep 2026 12:42:13 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D0=B4=D1=81=D0=BA=D0=B0?= =?UTF-8?q?=D0=B7=D0=BA=D0=B0=20=D0=BE=20=D0=BD=D0=B5=D0=B4=D0=BE=D0=BF?= =?UTF-8?q?=D1=83=D1=81=D1=82=D0=B8=D0=BC=D0=BE=D0=B9=20=D0=B4=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=20=D1=83=D0=B2=D0=BE=D0=BB=D1=8C=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Если последний рабочий день раньше, чем через месяц от сегодня, под полем показывается причина и минимальная дата. Раньше кнопки отправки просто становились неактивными без объяснения — это случалось у заявлений, пролежавших на доработке: дата, допустимая при подаче, переставала подходить. Правило и текст вынесены в lastWorkingDay.ts и общие для подсказки и проверки перед отправкой. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../forms/Dismissal/DismissalForm.tsx | 16 +- .../forms/Dismissal/lastWorkingDay.ts | 23 +++ .../sections/RequestSection.module.scss | 8 + .../Dismissal/sections/RequestSection.tsx | 143 ++++++++++-------- 4 files changed, 115 insertions(+), 75 deletions(-) create mode 100644 src/webparts/requestPortal/forms/Dismissal/lastWorkingDay.ts create mode 100644 src/webparts/requestPortal/forms/Dismissal/sections/RequestSection.module.scss diff --git a/src/webparts/requestPortal/forms/Dismissal/DismissalForm.tsx b/src/webparts/requestPortal/forms/Dismissal/DismissalForm.tsx index f2b48e4..056b070 100644 --- a/src/webparts/requestPortal/forms/Dismissal/DismissalForm.tsx +++ b/src/webparts/requestPortal/forms/Dismissal/DismissalForm.tsx @@ -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 = ({ // Последний рабочий день нужно назначать минимум за 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 'Заполните обязательные поля: сотрудник, должность, последний рабочий день, контактный телефон.'; } diff --git a/src/webparts/requestPortal/forms/Dismissal/lastWorkingDay.ts b/src/webparts/requestPortal/forms/Dismissal/lastWorkingDay.ts new file mode 100644 index 0000000..170af1f --- /dev/null +++ b/src/webparts/requestPortal/forms/Dismissal/lastWorkingDay.ts @@ -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)}.`; +} diff --git a/src/webparts/requestPortal/forms/Dismissal/sections/RequestSection.module.scss b/src/webparts/requestPortal/forms/Dismissal/sections/RequestSection.module.scss new file mode 100644 index 0000000..cf5bbcd --- /dev/null +++ b/src/webparts/requestPortal/forms/Dismissal/sections/RequestSection.module.scss @@ -0,0 +1,8 @@ +@import '../../../../../styles/design-tokens'; + +// Пояснение под полем, почему значение не подходит (см. lastWorkingDayError). +.fieldError { + margin-top: $sp-1; + font-size: $fs-label; + color: $red-500; +} diff --git a/src/webparts/requestPortal/forms/Dismissal/sections/RequestSection.tsx b/src/webparts/requestPortal/forms/Dismissal/sections/RequestSection.tsx index 3fe75c0..f9ec9b0 100644 --- a/src/webparts/requestPortal/forms/Dismissal/sections/RequestSection.tsx +++ b/src/webparts/requestPortal/forms/Dismissal/sections/RequestSection.tsx @@ -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,69 +29,78 @@ export const RequestSection: React.FC = ({ onEmployeeChange, onRemoveOrganization, assetsAcceptorEditable -}) => ( - - - set('Employee', v))} - placeholder={EMPLOYEE_PLACEHOLDER} +}) => { + // В просмотре (DismissalView) секция только для чтения — подсказка там ни к чему. + const readOnly = useFormReadOnly(); + const lastDayError = lastWorkingDayError(data.LastWorkingDay); + + return ( + + + set('Employee', v))} + placeholder={EMPLOYEE_PLACEHOLDER} + /> + + {/* ЕДИНСТВЕННОЕ место для компании/подразделения/должности — и на форме + подачи (редактируемая, onRemoveOrganization задан), и в просмотре + (read-only, ничего не передано). Отдельных полей для них нет — были бы + дублированием того, что уже показывает эта таблица. */} + + set( + 'Organization', + data.Organization.length > 0 + ? data.Organization.map((r, ri) => (ri === i ? { ...r, position: v } : r)) + : [{ company: '', department: '', position: v }] + )) + } + onRemove={onRemoveOrganization} /> - - {/* ЕДИНСТВЕННОЕ место для компании/подразделения/должности — и на форме - подачи (редактируемая, onRemoveOrganization задан), и в просмотре - (read-only, ничего не передано). Отдельных полей для них нет — были бы - дублированием того, что уже показывает эта таблица. */} - - set( - 'Organization', - data.Organization.length > 0 - ? data.Organization.map((r, ri) => (ri === i ? { ...r, position: v } : r)) - : [{ company: '', department: '', position: v }] - )) - } - onRemove={onRemoveOrganization} - /> - {/* Заявление подаётся заранее — датой увольнения нельзя назначить ближе, - чем через MIN_NOTICE_MONTHS месяц от сегодня (min). */} - - set('LastWorkingDay', v)} - min={isoDatePlusMonths(MIN_NOTICE_MONTHS)} + {/* Заявление подаётся заранее — датой увольнения нельзя назначить ближе, + чем через MIN_NOTICE_MONTHS месяц от сегодня (min). Если дата уже стоит + раньше (заявление пролежало на доработке) — подсказка под полем, иначе + непонятно, почему кнопки отправки неактивны. */} + + set('LastWorkingDay', v)} + min={minLastWorkingDay()} + /> + {!readOnly && lastDayError &&
{lastDayError}
} +
+ + set('Phone', v)} /> + + +