Compare commits
4 Commits
23f8255ba7
...
962d8f9c5c
| Author | SHA1 | Date | |
|---|---|---|---|
| 962d8f9c5c | |||
| cf1a8194eb | |||
| 2652b4694e | |||
| 7217d8bee6 |
@@ -15,8 +15,8 @@ export const REQUEST_TYPES: IRequestType[] = [
|
|||||||
{ id: 'it-1c-ticket', categoryId: 'it', title: 'Заявка в отдел 1С', icon: 'clipboard' },
|
{ id: 'it-1c-ticket', categoryId: 'it', title: 'Заявка в отдел 1С', icon: 'clipboard' },
|
||||||
|
|
||||||
// Отдел кадров
|
// Отдел кадров
|
||||||
{ id: 'hr-vacation', categoryId: 'hr', title: 'Заявка на трудовой отпуск', icon: 'calendar' },
|
{ id: 'hr-vacation', categoryId: 'hr', title: 'Заявление на оплачиваемый ежегодный трудовой отпуск', icon: 'calendar' },
|
||||||
{ id: 'hr-unpaid-leave', categoryId: 'hr', title: 'Заявка на отпуск без содержания', icon: 'document' },
|
{ id: 'hr-unpaid-leave', categoryId: 'hr', title: 'Заявление на отпуск без сохранения заработной платы', icon: 'document' },
|
||||||
// Пока только тестировщикам: тип заявки и маршрут ещё не заведены в админке approval-route.
|
// Пока только тестировщикам: тип заявки и маршрут ещё не заведены в админке approval-route.
|
||||||
{
|
{
|
||||||
id: 'hr-skud-explanation',
|
id: 'hr-skud-explanation',
|
||||||
|
|||||||
@@ -21,15 +21,24 @@
|
|||||||
для текущего пользователя при создании и при выборе другого сотрудника.
|
для текущего пользователя при создании и при выборе другого сотрудника.
|
||||||
- **Период вводится двумя датами** (с / по), обе обязательны, конец ≥ начала.
|
- **Период вводится двумя датами** (с / по), обе обязательны, конец ≥ начала.
|
||||||
`DaysCount` считается при сохранении (включительно) для отчётности — в UI не показывается.
|
`DaysCount` считается при сохранении (включительно) для отчётности — в UI не показывается.
|
||||||
|
- **Причина** (`UnpaidLeaveReasonType`, Choice) — обязательна: брак / рождение ребёнка /
|
||||||
|
смерть близких / «В иных случаях». Только при «иных случаях» появляется обязательный
|
||||||
|
текст причины (`Reason`). У заявок до появления колонки причины нет — их прежний
|
||||||
|
текст из `Reason` по-прежнему показывается (`isReasonTextShown`).
|
||||||
|
- **Не более 5 календарных дней** (`MAX_DAYS`) — при любой причине.
|
||||||
|
- **Правило «не позднее чем за 14 дней»** (`MIN_NOTICE_DAYS`) — только для «иных случаев»
|
||||||
|
(`requiresAdvanceNotice`): брак, рождение и смерть заранее не планируются.
|
||||||
|
- **Подсветка проблемных полей** — `FieldIssuesProvider` + правила в `validation.ts`
|
||||||
|
(черновик проверяется так же строго, как отправка).
|
||||||
- **Замещающий** (`Substitute`, People) — необязателен.
|
- **Замещающий** (`Substitute`, People) — необязателен.
|
||||||
- **Заголовок элемента** (`Title`): `Отпуск без содержания — {ФИО} — дд.мм.гггг`.
|
- **Заголовок элемента** (`Title`): `Отпуск без содержания — {ФИО} — дд.мм.гггг`.
|
||||||
|
|
||||||
## Отличия от «Заявка на отпуск» (hr-vacation)
|
## Отличия от «Заявка на отпуск» (hr-vacation)
|
||||||
- Те же «дата начала + количество дней»: даты окончания нет, праздники и выходные не считаются.
|
- Те же «дата начала + количество дней»: даты окончания нет, праздники и выходные не считаются.
|
||||||
- Добавлены поля «В связи с» (`Reason`) и «Лицо замещающее» (`Substitute`).
|
- Добавлены поля «Причина» (`UnpaidLeaveReasonType` + `Reason`) и «Лицо замещающее» (`Substitute`).
|
||||||
|
|
||||||
## Секции формы
|
## Секции формы
|
||||||
RequestSection (сотрудник · должность · дата начала · количество дней · основание · замещающий)
|
RequestSection (сотрудник · должность · дата начала · количество дней · причина · замещающий)
|
||||||
|
|
||||||
## Точки входа
|
## Точки входа
|
||||||
- rptype: `hr-unpaid-leave`
|
- rptype: `hr-unpaid-leave`
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useState, useMemo, useCallback, useEffect } from 'react';
|
|||||||
import styles from './UnpaidLeaveForm.module.scss';
|
import styles from './UnpaidLeaveForm.module.scss';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
FieldIssuesProvider,
|
||||||
|
issueLabels,
|
||||||
FormCard,
|
FormCard,
|
||||||
InitiatorLine,
|
InitiatorLine,
|
||||||
FormModeProvider,
|
FormModeProvider,
|
||||||
@@ -36,14 +38,14 @@ import { effectiveUserEmail } from '../../shared/impersonation';
|
|||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
|
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
|
||||||
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
import { friendlyErrorMessage } from '../../shared/diagnostics';
|
||||||
import { isoDatePlusDays, formatIsoDate } from '../../shared/dateUtils';
|
|
||||||
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_DAYS } from './model';
|
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
|
import { formIssues } from './validation';
|
||||||
import { saveUnpaidLeaveRequest, unpaidLeaveApprovalService } from './submit';
|
import { saveUnpaidLeaveRequest, unpaidLeaveApprovalService } from './submit';
|
||||||
import { loadUnpaidLeaveRequest } from './load';
|
import { loadUnpaidLeaveRequest } from './load';
|
||||||
import { RequestSection } from './sections/RequestSection';
|
import { RequestSection } from './sections/RequestSection';
|
||||||
@@ -183,30 +185,11 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
editId
|
editId
|
||||||
});
|
});
|
||||||
|
|
||||||
// Продолжительность: целое положительное число календарных дней.
|
// Проблемы полей (см. validation.ts) подсвечиваются под полями через FieldIssuesProvider.
|
||||||
const daysNum = Number(data.DaysCount);
|
const issues = formIssues(data);
|
||||||
const daysValid = Number.isFinite(daysNum) && daysNum > 0;
|
const isValid = issues.length === 0;
|
||||||
// Заявку нужно подавать не позднее чем за MIN_NOTICE_DAYS дней до даты начала
|
|
||||||
// отпуска — календарь (min у input type=date, см. RequestSection) не даёт
|
|
||||||
// выбрать более раннюю дату, но проверяем и здесь на случай обхода UI.
|
|
||||||
const minStartDate = isoDatePlusDays(MIN_NOTICE_DAYS);
|
|
||||||
const isStartDateValid = !data.StartDate || data.StartDate >= minStartDate;
|
|
||||||
const isValid = !!(
|
|
||||||
data.Employee &&
|
|
||||||
data.Position.trim() &&
|
|
||||||
data.StartDate &&
|
|
||||||
daysValid &&
|
|
||||||
isStartDateValid &&
|
|
||||||
data.Reason.trim()
|
|
||||||
);
|
|
||||||
const validate = (): string | undefined => {
|
const validate = (): string | undefined => {
|
||||||
if (data.StartDate && !isStartDateValid) {
|
if (!isValid) return `Заполните или исправьте поля: ${issueLabels(issues)}.`;
|
||||||
return `Заявку на отпуск нужно подать не позднее чем за ${MIN_NOTICE_DAYS} дней до даты начала — минимально доступная дата: ${formatIsoDate(minStartDate)}.`;
|
|
||||||
}
|
|
||||||
if (!data.Employee || !data.Position.trim() || !data.StartDate || !data.Reason.trim()) {
|
|
||||||
return 'Заполните обязательные поля: сотрудник, должность, дата начала, основание.';
|
|
||||||
}
|
|
||||||
if (!daysValid) return 'Количество дней должно быть больше нуля.';
|
|
||||||
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
|
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
|
||||||
if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.';
|
if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.';
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -310,7 +293,7 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<FieldIssuesProvider draftIssues={issues} submitIssues={issues}>
|
||||||
<button type="button" className={styles.back} onClick={onCancel}>
|
<button type="button" className={styles.back} onClick={onCancel}>
|
||||||
<Icon name="arrow-left" size="sm" />
|
<Icon name="arrow-left" size="sm" />
|
||||||
<span>К списку заявок</span>
|
<span>К списку заявок</span>
|
||||||
@@ -377,7 +360,7 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
|||||||
submitLabel="Отправить на согласование"
|
submitLabel="Отправить на согласование"
|
||||||
/>
|
/>
|
||||||
</FormCard>
|
</FormCard>
|
||||||
</div>
|
</FieldIssuesProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
|
|||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Заявка на отпуск без содержания"
|
title="Заявление на отпуск без сохранения заработной платы"
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export async function loadUnpaidLeaveRequest(itemId: number, listId?: string): P
|
|||||||
'Position',
|
'Position',
|
||||||
'StartDate',
|
'StartDate',
|
||||||
'DaysCount',
|
'DaysCount',
|
||||||
|
'UnpaidLeaveReasonType',
|
||||||
'Reason',
|
'Reason',
|
||||||
'Organization',
|
'Organization',
|
||||||
'Department',
|
'Department',
|
||||||
@@ -54,6 +55,7 @@ export async function loadUnpaidLeaveRequest(itemId: number, listId?: string): P
|
|||||||
Position: (item.Position as string) || '',
|
Position: (item.Position as string) || '',
|
||||||
StartDate: spDateOnlyToIso(start),
|
StartDate: spDateOnlyToIso(start),
|
||||||
DaysCount: days === null || days === undefined ? '' : String(days),
|
DaysCount: days === null || days === undefined ? '' : String(days),
|
||||||
|
ReasonType: (item.UnpaidLeaveReasonType as string) || '',
|
||||||
Reason: (item.Reason as string) || '',
|
Reason: (item.Reason as string) || '',
|
||||||
Substitute: substitute?.EMail || '',
|
Substitute: substitute?.EMail || '',
|
||||||
Organization: (item.Organization as string) || '',
|
Organization: (item.Organization as string) || '',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { RequestTypeId } from '../../models/types';
|
import { RequestTypeId } from '../../models/types';
|
||||||
|
import { ISelectOption } from '../../shared/ui';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков
|
* Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков
|
||||||
@@ -23,7 +24,12 @@ export interface IFormData {
|
|||||||
StartDate: string;
|
StartDate: string;
|
||||||
/** Количество календарных дней (строка из input type=number). */
|
/** Количество календарных дней (строка из input type=number). */
|
||||||
DaysCount: string;
|
DaysCount: string;
|
||||||
/** Основание «В связи с …». */
|
/** Причина отпуска — одно из UNPAID_LEAVE_REASONS (Choice-колонка). '' — не выбрана или заявка подана до появления поля. */
|
||||||
|
ReasonType: string;
|
||||||
|
/**
|
||||||
|
* Свободный текст причины — заполняется только при REASON_OTHER. У заявок,
|
||||||
|
* поданных до появления ReasonType, здесь лежит прежнее основание «В связи с …».
|
||||||
|
*/
|
||||||
Reason: string;
|
Reason: string;
|
||||||
/** E-mail замещающего на время отсутствия (People). Пусто = не выбран. */
|
/** E-mail замещающего на время отсутствия (People). Пусто = не выбран. */
|
||||||
Substitute: string;
|
Substitute: string;
|
||||||
@@ -69,6 +75,7 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
Position: '',
|
Position: '',
|
||||||
StartDate: '',
|
StartDate: '',
|
||||||
DaysCount: '',
|
DaysCount: '',
|
||||||
|
ReasonType: '',
|
||||||
Reason: '',
|
Reason: '',
|
||||||
Substitute: '',
|
Substitute: '',
|
||||||
Organization: '',
|
Organization: '',
|
||||||
@@ -82,5 +89,40 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
export const EMPLOYEE_PLACEHOLDER = 'Начните вводить Ф.И.О. сотрудника…';
|
export const EMPLOYEE_PLACEHOLDER = 'Начните вводить Ф.И.О. сотрудника…';
|
||||||
/** Плейсхолдер для поля выбора замещающего. */
|
/** Плейсхолдер для поля выбора замещающего. */
|
||||||
export const SUBSTITUTE_PLACEHOLDER = 'Начните вводить Ф.И.О. замещающего…';
|
export const SUBSTITUTE_PLACEHOLDER = 'Начните вводить Ф.И.О. замещающего…';
|
||||||
/** Заявку нужно подавать не позднее чем за столько дней до даты начала отпуска. */
|
/**
|
||||||
|
* Заявку нужно подавать не позднее чем за столько дней до даты начала отпуска.
|
||||||
|
* Действует только для «иных случаев» — см. requiresAdvanceNotice.
|
||||||
|
*/
|
||||||
export const MIN_NOTICE_DAYS = 14;
|
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());
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
{ "internalName": "Position", "title": "Должность", "type": "Text" },
|
{ "internalName": "Position", "title": "Должность", "type": "Text" },
|
||||||
{ "internalName": "StartDate", "title": "Дата начала", "type": "DateTime", "dateOnly": true },
|
{ "internalName": "StartDate", "title": "Дата начала", "type": "DateTime", "dateOnly": true },
|
||||||
{ "internalName": "DaysCount", "title": "Количество дней", "type": "Number" },
|
{ "internalName": "DaysCount", "title": "Количество дней", "type": "Number" },
|
||||||
|
{ "internalName": "UnpaidLeaveReasonType", "title": "Причина", "type": "Choice", "choices": ["Регистрация брака", "Рождение ребенка", "Смерть близких родственников, а также супруга (супруги) и (или) их свойственников (полнородные и неполнородные братья и сестры, родители (родитель), дети, дедушка, бабушка, внуки)", "В иных случаях"] },
|
||||||
{ "internalName": "Reason", "title": "Основание (в связи с)", "type": "Note" },
|
{ "internalName": "Reason", "title": "Основание (в связи с)", "type": "Note" },
|
||||||
{ "internalName": "Substitute", "title": "Лицо, замещающее на время отсутствия", "type": "User", "userSelectionMode": "PeopleOnly" },
|
{ "internalName": "Substitute", "title": "Лицо, замещающее на время отсутствия", "type": "User", "userSelectionMode": "PeopleOnly" },
|
||||||
{ "internalName": "Organization", "title": "Организация", "type": "Note" },
|
{ "internalName": "Organization", "title": "Организация", "type": "Note" },
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import * as React from 'react';
|
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 { 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';
|
import { isoDatePlusDays } from '../../../shared/dateUtils';
|
||||||
|
|
||||||
/** Единственная секция формы «Отпуск без содержания»: сотрудник, должность, период, основание, замещающий. */
|
/** Единственная секция формы «Отпуск без содержания»: сотрудник, должность, период, причина, замещающий. */
|
||||||
export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, onEmployeeChange, printOrgIndex }) => (
|
export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, onEmployeeChange, printOrgIndex }) => (
|
||||||
<FormGrid>
|
<FormGrid>
|
||||||
<FormField label="Сотрудник" htmlFor="Employee" required full>
|
<FormField label="Сотрудник" htmlFor="Employee" required full>
|
||||||
@@ -48,15 +58,36 @@ export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, on
|
|||||||
type="date"
|
type="date"
|
||||||
value={data.StartDate}
|
value={data.StartDate}
|
||||||
onChange={(v) => set('StartDate', v)}
|
onChange={(v) => set('StartDate', v)}
|
||||||
min={isoDatePlusDays(MIN_NOTICE_DAYS)}
|
min={requiresAdvanceNotice(data.ReasonType) ? isoDatePlusDays(MIN_NOTICE_DAYS) : undefined}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Количество дней" htmlFor="DaysCount" required>
|
<FormField label={`Количество дней (не более ${MAX_DAYS})`} htmlFor="DaysCount" required>
|
||||||
<TextInput id="DaysCount" type="number" value={data.DaysCount} onChange={(v) => set('DaysCount', v)} />
|
<TextInput
|
||||||
|
id="DaysCount"
|
||||||
|
type="number"
|
||||||
|
value={data.DaysCount}
|
||||||
|
onChange={(v) => set('DaysCount', v)}
|
||||||
|
min={1}
|
||||||
|
max={MAX_DAYS}
|
||||||
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="В связи с" htmlFor="Reason" required full>
|
<FormField label="Причина" htmlFor="UnpaidLeaveReasonType" required full>
|
||||||
|
<Select
|
||||||
|
id="UnpaidLeaveReasonType"
|
||||||
|
value={data.ReasonType}
|
||||||
|
onChange={(v) => {
|
||||||
|
set('ReasonType', v);
|
||||||
|
if (v !== REASON_OTHER) set('Reason', '');
|
||||||
|
}}
|
||||||
|
options={UNPAID_LEAVE_REASONS}
|
||||||
|
placeholder="— выберите —"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
{isReasonTextShown(data) && (
|
||||||
|
<FormField label="Укажите причину" htmlFor="Reason" required full>
|
||||||
<TextArea id="Reason" value={data.Reason} onChange={(v) => set('Reason', v)} />
|
<TextArea id="Reason" value={data.Reason} onChange={(v) => set('Reason', v)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
)}
|
||||||
<FormField label="Лицо, замещающее на время отсутствия" htmlFor="Substitute" full>
|
<FormField label="Лицо, замещающее на время отсутствия" htmlFor="Substitute" full>
|
||||||
<PeopleField
|
<PeopleField
|
||||||
context={context}
|
context={context}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ function buildItem(
|
|||||||
Position: data.Position || null,
|
Position: data.Position || null,
|
||||||
StartDate: data.StartDate || null,
|
StartDate: data.StartDate || null,
|
||||||
DaysCount: Number.isFinite(days) && days > 0 ? days : null,
|
DaysCount: Number.isFinite(days) && days > 0 ? days : null,
|
||||||
|
UnpaidLeaveReasonType: data.ReasonType || null,
|
||||||
Reason: data.Reason || null,
|
Reason: data.Reason || null,
|
||||||
SubstituteId: substituteId ?? null,
|
SubstituteId: substituteId ?? null,
|
||||||
Organization: data.Organization || null,
|
Organization: data.Organization || null,
|
||||||
|
|||||||
@@ -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<IFieldIssue | undefined> = [
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -198,7 +198,7 @@ const VacationView: React.FC<IVacationViewProps> = ({ context, itemId, formKey,
|
|||||||
<div className="rp-print-root">
|
<div className="rp-print-root">
|
||||||
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
<OrgLetterhead value={data.Organization} selectedIndex={printOrgIndex} />
|
||||||
<FormCard
|
<FormCard
|
||||||
title="Заявка на трудовой отпуск"
|
title="Заявление на оплачиваемый ежегодный трудовой отпуск"
|
||||||
subtitle={`№${code || itemId}`}
|
subtitle={`№${code || itemId}`}
|
||||||
belowSubtitle={
|
belowSubtitle={
|
||||||
<InitiatorLine
|
<InitiatorLine
|
||||||
|
|||||||
Reference in New Issue
Block a user