diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/README.md b/src/webparts/requestPortal/forms/UnpaidLeave/README.md index f3ad67f..be99ff1 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/README.md +++ b/src/webparts/requestPortal/forms/UnpaidLeave/README.md @@ -21,15 +21,22 @@ для текущего пользователя при создании и при выборе другого сотрудника. - **Период вводится двумя датами** (с / по), обе обязательны, конец ≥ начала. `DaysCount` считается при сохранении (включительно) для отчётности — в UI не показывается. +- **Причина** (`UnpaidLeaveReasonType`, Choice) — обязательна: брак / рождение ребёнка / + смерть близких / «В иных случаях». Только при «иных случаях» появляется обязательный + текст причины (`Reason`). У заявок до появления колонки причины нет — их прежний + текст из `Reason` по-прежнему показывается (`isReasonTextShown`). +- **Не более 5 календарных дней** (`MAX_DAYS`) — при любой причине. +- **Правило «не позднее чем за 14 дней»** (`MIN_NOTICE_DAYS`) — только для «иных случаев» + (`requiresAdvanceNotice`): брак, рождение и смерть заранее не планируются. - **Замещающий** (`Substitute`, People) — необязателен. - **Заголовок элемента** (`Title`): `Отпуск без содержания — {ФИО} — дд.мм.гггг`. ## Отличия от «Заявка на отпуск» (hr-vacation) - Те же «дата начала + количество дней»: даты окончания нет, праздники и выходные не считаются. -- Добавлены поля «В связи с» (`Reason`) и «Лицо замещающее» (`Substitute`). +- Добавлены поля «Причина» (`UnpaidLeaveReasonType` + `Reason`) и «Лицо замещающее» (`Substitute`). ## Секции формы -RequestSection (сотрудник · должность · дата начала · количество дней · основание · замещающий) +RequestSection (сотрудник · должность · дата начала · количество дней · причина · замещающий) ## Точки входа - rptype: `hr-unpaid-leave` diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx index ca38166..57067c1 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx +++ b/src/webparts/requestPortal/forms/UnpaidLeave/UnpaidLeaveForm.tsx @@ -43,7 +43,16 @@ import { dataListTitle, dataListTitleForListId } from '../../shared/listTitles'; -import { FORM_ID, IFormData, EMPTY_DATA, SetField, MIN_NOTICE_DAYS } from './model'; +import { + FORM_ID, + IFormData, + EMPTY_DATA, + SetField, + MIN_NOTICE_DAYS, + MAX_DAYS, + REASON_OTHER, + requiresAdvanceNotice +} from './model'; import { saveUnpaidLeaveRequest, unpaidLeaveApprovalService } from './submit'; import { loadUnpaidLeaveRequest } from './load'; import { RequestSection } from './sections/RequestSection'; @@ -183,30 +192,33 @@ const UnpaidLeaveForm: React.FC = ({ editId }); - // Продолжительность: целое положительное число календарных дней. + // Продолжительность: от 1 до MAX_DAYS календарных дней. const daysNum = Number(data.DaysCount); - const daysValid = Number.isFinite(daysNum) && daysNum > 0; - // Заявку нужно подавать не позднее чем за MIN_NOTICE_DAYS дней до даты начала - // отпуска — календарь (min у input type=date, см. RequestSection) не даёт + 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 || data.StartDate >= minStartDate; + 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 && - data.Reason.trim() + isReasonValid ); const validate = (): string | undefined => { if (data.StartDate && !isStartDateValid) { return `Заявку на отпуск нужно подать не позднее чем за ${MIN_NOTICE_DAYS} дней до даты начала — минимально доступная дата: ${formatIsoDate(minStartDate)}.`; } - if (!data.Employee || !data.Position.trim() || !data.StartDate || !data.Reason.trim()) { - return 'Заполните обязательные поля: сотрудник, должность, дата начала, основание.'; + if (!data.Employee || !data.Position.trim() || !data.StartDate || !isReasonValid) { + return 'Заполните обязательные поля: сотрудник, должность, дата начала, причина.'; } - if (!daysValid) return 'Количество дней должно быть больше нуля.'; + if (!daysValid) return `Количество дней — от 1 до ${MAX_DAYS}.`; if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`; if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.'; return undefined; diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/load.ts b/src/webparts/requestPortal/forms/UnpaidLeave/load.ts index 605a479..626c7af 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/load.ts +++ b/src/webparts/requestPortal/forms/UnpaidLeave/load.ts @@ -29,6 +29,7 @@ export async function loadUnpaidLeaveRequest(itemId: number, listId?: string): P 'Position', 'StartDate', 'DaysCount', + 'UnpaidLeaveReasonType', 'Reason', 'Organization', 'Department', @@ -54,6 +55,7 @@ export async function loadUnpaidLeaveRequest(itemId: number, listId?: string): P Position: (item.Position as string) || '', StartDate: spDateOnlyToIso(start), DaysCount: days === null || days === undefined ? '' : String(days), + ReasonType: (item.UnpaidLeaveReasonType as string) || '', Reason: (item.Reason as string) || '', Substitute: substitute?.EMail || '', Organization: (item.Organization as string) || '', diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/model.ts b/src/webparts/requestPortal/forms/UnpaidLeave/model.ts index b70bd49..7fb6567 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/model.ts +++ b/src/webparts/requestPortal/forms/UnpaidLeave/model.ts @@ -1,5 +1,6 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; import { RequestTypeId } from '../../models/types'; +import { ISelectOption } from '../../shared/ui'; /** * Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков @@ -23,7 +24,12 @@ export interface IFormData { StartDate: string; /** Количество календарных дней (строка из input type=number). */ DaysCount: string; - /** Основание «В связи с …». */ + /** Причина отпуска — одно из UNPAID_LEAVE_REASONS (Choice-колонка). '' — не выбрана или заявка подана до появления поля. */ + ReasonType: string; + /** + * Свободный текст причины — заполняется только при REASON_OTHER. У заявок, + * поданных до появления ReasonType, здесь лежит прежнее основание «В связи с …». + */ Reason: string; /** E-mail замещающего на время отсутствия (People). Пусто = не выбран. */ Substitute: string; @@ -69,6 +75,7 @@ export const EMPTY_DATA: IFormData = { Position: '', StartDate: '', DaysCount: '', + ReasonType: '', Reason: '', Substitute: '', Organization: '', @@ -82,5 +89,40 @@ export const EMPTY_DATA: IFormData = { export const EMPLOYEE_PLACEHOLDER = 'Начните вводить Ф.И.О. сотрудника…'; /** Плейсхолдер для поля выбора замещающего. */ export const SUBSTITUTE_PLACEHOLDER = 'Начните вводить Ф.И.О. замещающего…'; -/** Заявку нужно подавать не позднее чем за столько дней до даты начала отпуска. */ +/** + * Заявку нужно подавать не позднее чем за столько дней до даты начала отпуска. + * Действует только для «иных случаев» — см. requiresAdvanceNotice. + */ export const MIN_NOTICE_DAYS = 14; +/** Отпуск без сохранения заработной платы — не более стольких календарных дней (по любой причине). */ +export const MAX_DAYS = 5; + +/** Причины отпуска. Значения совпадают с choices колонки UnpaidLeaveReasonType. */ +export const REASON_MARRIAGE = 'Регистрация брака'; +export const REASON_CHILD_BIRTH = 'Рождение ребенка'; +export const REASON_RELATIVE_DEATH = + 'Смерть близких родственников, а также супруга (супруги) и (или) их свойственников (полнородные и неполнородные братья и сестры, родители (родитель), дети, дедушка, бабушка, внуки)'; +export const REASON_OTHER = 'В иных случаях'; + +export const UNPAID_LEAVE_REASONS: ISelectOption[] = [ + { value: REASON_MARRIAGE, label: REASON_MARRIAGE }, + { value: REASON_CHILD_BIRTH, label: REASON_CHILD_BIRTH }, + { value: REASON_RELATIVE_DEATH, label: REASON_RELATIVE_DEATH }, + { value: REASON_OTHER, label: REASON_OTHER } +]; + +/** + * Предупреждать за MIN_NOTICE_DAYS нужно только в «иных случаях»: брак, рождение + * ребёнка и смерть близких заранее не планируются. + */ +export function requiresAdvanceNotice(reasonType: string): boolean { + return reasonType === REASON_OTHER; +} + +/** + * Показывать ли поле свободного текста причины: при «иных случаях» и у старых + * заявок без ReasonType, где в Reason лежит прежнее основание — чтобы его не потерять. + */ +export function isReasonTextShown(data: IFormData): boolean { + return data.ReasonType === REASON_OTHER || (!data.ReasonType && !!data.Reason.trim()); +} diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/provisioning.json b/src/webparts/requestPortal/forms/UnpaidLeave/provisioning.json index c53f544..0430678 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/provisioning.json +++ b/src/webparts/requestPortal/forms/UnpaidLeave/provisioning.json @@ -8,6 +8,7 @@ { "internalName": "Position", "title": "Должность", "type": "Text" }, { "internalName": "StartDate", "title": "Дата начала", "type": "DateTime", "dateOnly": true }, { "internalName": "DaysCount", "title": "Количество дней", "type": "Number" }, + { "internalName": "UnpaidLeaveReasonType", "title": "Причина", "type": "Choice", "choices": ["Регистрация брака", "Рождение ребенка", "Смерть близких родственников, а также супруга (супруги) и (или) их свойственников (полнородные и неполнородные братья и сестры, родители (родитель), дети, дедушка, бабушка, внуки)", "В иных случаях"] }, { "internalName": "Reason", "title": "Основание (в связи с)", "type": "Note" }, { "internalName": "Substitute", "title": "Лицо, замещающее на время отсутствия", "type": "User", "userSelectionMode": "PeopleOnly" }, { "internalName": "Organization", "title": "Организация", "type": "Note" }, diff --git a/src/webparts/requestPortal/forms/UnpaidLeave/sections/RequestSection.tsx b/src/webparts/requestPortal/forms/UnpaidLeave/sections/RequestSection.tsx index ece13c1..50b2dcd 100644 --- a/src/webparts/requestPortal/forms/UnpaidLeave/sections/RequestSection.tsx +++ b/src/webparts/requestPortal/forms/UnpaidLeave/sections/RequestSection.tsx @@ -1,10 +1,20 @@ import * as React from 'react'; -import { FormGrid, FormField, TextInput, TextArea, PeopleField, OrganizationField } from '../../../shared/ui'; +import { FormGrid, FormField, TextInput, TextArea, Select, PeopleField, OrganizationField } from '../../../shared/ui'; import { DepartmentPicker, ProjectPicker } from '../../../shared/dictionary'; -import { ISectionProps, EMPLOYEE_PLACEHOLDER, SUBSTITUTE_PLACEHOLDER, MIN_NOTICE_DAYS } from '../model'; +import { + ISectionProps, + EMPLOYEE_PLACEHOLDER, + SUBSTITUTE_PLACEHOLDER, + MIN_NOTICE_DAYS, + MAX_DAYS, + REASON_OTHER, + UNPAID_LEAVE_REASONS, + requiresAdvanceNotice, + isReasonTextShown +} from '../model'; import { isoDatePlusDays } from '../../../shared/dateUtils'; -/** Единственная секция формы «Отпуск без содержания»: сотрудник, должность, период, основание, замещающий. */ +/** Единственная секция формы «Отпуск без содержания»: сотрудник, должность, период, причина, замещающий. */ export const RequestSection: React.FC = ({ data, set, context, onEmployeeChange, printOrgIndex }) => ( @@ -48,15 +58,36 @@ export const RequestSection: React.FC = ({ data, set, context, on type="date" value={data.StartDate} onChange={(v) => set('StartDate', v)} - min={isoDatePlusDays(MIN_NOTICE_DAYS)} + min={requiresAdvanceNotice(data.ReasonType) ? isoDatePlusDays(MIN_NOTICE_DAYS) : undefined} /> - - set('DaysCount', v)} /> + + set('DaysCount', v)} + min={1} + max={MAX_DAYS} + /> - -