diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/README.md b/src/webparts/requestPortal/forms/UnpaidLeave/README.md index be99ff1..4067619 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/README.md +++ b/src/webparts/requestPortal/forms/UnpaidLeave/README.md @@ -28,6 +28,8 @@ - **Не более 5 календарных дней** (`MAX_DAYS`) — при любой причине. - **Правило «не позднее чем за 14 дней»** (`MIN_NOTICE_DAYS`) — только для «иных случаев» (`requiresAdvanceNotice`): брак, рождение и смерть заранее не планируются. +- **Подсветка проблемных полей** — `FieldIssuesProvider` + правила в `validation.ts` + (черновик проверяется так же строго, как отправка). - **Замещающий** (`Substitute`, People) — необязателен. - **Заголовок элемента** (`Title`): `Отпуск без содержания — {ФИО} — дд.мм.гггг`. diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx index 57067c1..87a8264 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx +++ b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx @@ -3,6 +3,8 @@ import { useState, useMemo, useCallback, useEffect } from 'react'; import styles from './UnpaidLeaveForm.module.scss'; import { Alert, + FieldIssuesProvider, + issueLabels, FormCard, InitiatorLine, FormModeProvider, @@ -36,23 +38,14 @@ import { effectiveUserEmail } from '../../shared/impersonation'; import { IRequestFormProps } from '../../models/types'; import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary'; import { friendlyErrorMessage } from '../../shared/diagnostics'; -import { isoDatePlusDays, formatIsoDate } from '../../shared/dateUtils'; import { approvalListTitle, approvalListTitleForListId, dataListTitle, dataListTitleForListId } from '../../shared/listTitles'; -import { - FORM_ID, - IFormData, - EMPTY_DATA, - SetField, - MIN_NOTICE_DAYS, - MAX_DAYS, - REASON_OTHER, - requiresAdvanceNotice -} from './model'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; +import { formIssues } from './validation'; import { saveUnpaidLeaveRequest, unpaidLeaveApprovalService } from './submit'; import { loadUnpaidLeaveRequest } from './load'; import { RequestSection } from './sections/RequestSection'; @@ -192,33 +185,11 @@ const UnpaidLeaveForm: React.FC = ({ editId }); - // Продолжительность: от 1 до MAX_DAYS календарных дней. - const daysNum = Number(data.DaysCount); - const daysValid = Number.isFinite(daysNum) && daysNum > 0 && daysNum <= MAX_DAYS; - // В «иных случаях» заявку нужно подавать не позднее чем за MIN_NOTICE_DAYS дней - // до даты начала — календарь (min у input type=date, см. RequestSection) не даёт - // выбрать более раннюю дату, но проверяем и здесь на случай обхода UI. - const minStartDate = isoDatePlusDays(MIN_NOTICE_DAYS); - const isStartDateValid = - !data.StartDate || !requiresAdvanceNotice(data.ReasonType) || data.StartDate >= minStartDate; - // Текст причины обязателен только для «иных случаев». - const isReasonValid = !!data.ReasonType && (data.ReasonType !== REASON_OTHER || !!data.Reason.trim()); - const isValid = !!( - data.Employee && - data.Position.trim() && - data.StartDate && - daysValid && - isStartDateValid && - isReasonValid - ); + // Проблемы полей (см. validation.ts) подсвечиваются под полями через FieldIssuesProvider. + const issues = formIssues(data); + const isValid = issues.length === 0; const validate = (): string | undefined => { - if (data.StartDate && !isStartDateValid) { - return `Заявку на отпуск нужно подать не позднее чем за ${MIN_NOTICE_DAYS} дней до даты начала — минимально доступная дата: ${formatIsoDate(minStartDate)}.`; - } - if (!data.Employee || !data.Position.trim() || !data.StartDate || !isReasonValid) { - return 'Заполните обязательные поля: сотрудник, должность, дата начала, причина.'; - } - if (!daysValid) return `Количество дней — от 1 до ${MAX_DAYS}.`; + if (!isValid) return `Заполните или исправьте поля: ${issueLabels(issues)}.`; if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`; if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.'; return undefined; @@ -322,7 +293,7 @@ const UnpaidLeaveForm: React.FC = ({ }); return ( -
+
+ ); }; diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/validation.ts b/src/webparts/requestPortal/forms/UnpaidLeave/validation.ts new file mode 100644 index 0000000..1da5263 --- /dev/null +++ b/src/webparts/requestPortal/forms/UnpaidLeave/validation.ts @@ -0,0 +1,45 @@ +import type { IFieldIssue } from '../../shared/ui'; +import { isoDatePlusDays, formatIsoDate } from '../../shared/dateUtils'; +import { IFormData, MAX_DAYS, MIN_NOTICE_DAYS, REASON_OTHER, requiresAdvanceNotice } from './model'; + +/** Дата начала: обязательна, а в «иных случаях» — не раньше чем через MIN_NOTICE_DAYS дней. */ +function startDateIssue(data: IFormData): IFieldIssue | undefined { + if (!data.StartDate) return { field: 'StartDate', label: 'Дата начала' }; + if (!requiresAdvanceNotice(data.ReasonType)) return undefined; + + const minStartDate = isoDatePlusDays(MIN_NOTICE_DAYS); + if (data.StartDate >= minStartDate) return undefined; + return { + field: 'StartDate', + label: 'Дата начала', + message: `в иных случаях заявление подают не позднее чем за ${MIN_NOTICE_DAYS} дней — не раньше ${formatIsoDate(minStartDate)}` + }; +} + +/** Количество дней: обязательно, от 1 до MAX_DAYS. */ +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} календарных дней` }; +} + +/** + * Всё, что мешает сохранить или отправить заявку, в порядке формы (пусто — можно). + * field — ключ FormField (htmlFor): по нему поле подсвечивается и к нему + * прокручивается форма. Черновик проверяется так же строго, как отправка. + */ +export function formIssues(data: IFormData): IFieldIssue[] { + const issues: Array = [ + data.Employee ? undefined : { field: 'Employee', label: 'Сотрудник' }, + data.Position.trim() ? undefined : { field: 'Position', label: 'Должность' }, + startDateIssue(data), + daysCountIssue(data), + data.ReasonType + ? undefined + : { field: 'UnpaidLeaveReasonType', label: 'Причина', message: 'выберите вариант из списка' }, + data.ReasonType === REASON_OTHER && !data.Reason.trim() ? { field: 'Reason', label: 'Укажите причину' } : undefined + ]; + return issues.filter((issue): issue is IFieldIssue => issue !== undefined); +}