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)} /> + + +