feat: добавил поле организация во все заявки, а также расширил компонет выбора даты для ui-kit
This commit is contained in:
@@ -25,14 +25,16 @@ import {
|
||||
resolveWriteListId
|
||||
} from '../../shared/approval';
|
||||
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 } from './model';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA, SetField, MIN_NOTICE_DAYS } from './model';
|
||||
import { saveUnpaidLeaveRequest, unpaidLeaveApprovalService } from './submit';
|
||||
import { loadUnpaidLeaveRequest } from './load';
|
||||
import { RequestSection } from './sections/RequestSection';
|
||||
@@ -88,7 +90,10 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [editId, editListId]);
|
||||
|
||||
// Новая заявка — по умолчанию на себя: подставляем текущего пользователя и его должность.
|
||||
// Новая заявка — по умолчанию на себя: подставляем текущего пользователя,
|
||||
// его должность и организации (Employee/ByEmail — см. shared/dictionary/
|
||||
// employeeOrganizations.service.ts; сотрудник может состоять сразу в
|
||||
// нескольких организациях, должность берём из ПЕРВОЙ записи).
|
||||
useEffect(() => {
|
||||
if (editId) {
|
||||
return;
|
||||
@@ -96,12 +101,14 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
||||
let alive = true;
|
||||
const user = context.pageContext.user;
|
||||
setData((prev) => ({ ...prev, Employee: prev.Employee || user.email }));
|
||||
approval
|
||||
.getJobTitle(user.loginName)
|
||||
.then((title) => {
|
||||
if (alive && title) {
|
||||
setData((prev) => ({ ...prev, Position: prev.Position || title }));
|
||||
}
|
||||
getEmployeeOrganizations(context, user.email)
|
||||
.then((records) => {
|
||||
if (!alive) return;
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
Position: prev.Position || records[0]?.position || prev.Position,
|
||||
Organization: formatEmployeeOrganizations(records)
|
||||
}));
|
||||
})
|
||||
.catch(() => undefined);
|
||||
return () => {
|
||||
@@ -110,15 +117,21 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [editId]);
|
||||
|
||||
// Выбор другого сотрудника подтягивает его должность.
|
||||
// Выбор другого сотрудника подтягивает его должность и организации.
|
||||
const handleEmployeeChange = (login: string): void => {
|
||||
setData((prev) => ({ ...prev, Employee: login }));
|
||||
if (!login) {
|
||||
setData((prev) => ({ ...prev, Organization: '' }));
|
||||
return;
|
||||
}
|
||||
approval
|
||||
.getJobTitle(login)
|
||||
.then((title) => setData((prev) => ({ ...prev, Position: title || prev.Position })))
|
||||
getEmployeeOrganizations(context, login)
|
||||
.then((records) =>
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
Position: records[0]?.position || prev.Position,
|
||||
Organization: formatEmployeeOrganizations(records)
|
||||
}))
|
||||
)
|
||||
.catch(() => undefined);
|
||||
};
|
||||
|
||||
@@ -142,13 +155,22 @@ const UnpaidLeaveForm: React.FC<IRequestFormProps> = ({
|
||||
|
||||
// Даты корректны, если конец не раньше начала (обе обязательны).
|
||||
const datesValid = !!(data.StartDate && data.EndDate && data.EndDate >= data.StartDate);
|
||||
// Заявку нужно подавать не позднее чем за 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() &&
|
||||
datesValid &&
|
||||
isStartDateValid &&
|
||||
data.Reason.trim()
|
||||
);
|
||||
const validate = (): string | undefined => {
|
||||
if (data.StartDate && !isStartDateValid) {
|
||||
return `Заявку на отпуск нужно подать не позднее чем за ${MIN_NOTICE_DAYS} дней до даты начала — минимально доступная дата: ${formatIsoDate(minStartDate)}.`;
|
||||
}
|
||||
if (!data.Employee || !data.Position.trim() || !data.StartDate || !data.EndDate || !data.Reason.trim()) {
|
||||
return 'Заполните обязательные поля: сотрудник, должность, дата начала, дата окончания, основание.';
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ const UnpaidLeaveView: React.FC<IUnpaidLeaveViewProps> = ({ context, itemId, for
|
||||
|
||||
<div className="rp-print-root">
|
||||
<FormCard
|
||||
title="Отпуск без содержания"
|
||||
title="Заявка на отпуск без содержания"
|
||||
subtitle={`№${code || itemId}`}
|
||||
belowSubtitle={
|
||||
<InitiatorLine
|
||||
|
||||
@@ -29,6 +29,7 @@ export async function loadUnpaidLeaveRequest(itemId: number, listId?: string): P
|
||||
'StartDate',
|
||||
'EndDate',
|
||||
'Reason',
|
||||
'Organization',
|
||||
'RequestStatus',
|
||||
'RequestUniqueCode',
|
||||
'Employee/Title',
|
||||
@@ -49,7 +50,8 @@ export async function loadUnpaidLeaveRequest(itemId: number, listId?: string): P
|
||||
StartDate: start.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
|
||||
EndDate: end.slice(0, 10),
|
||||
Reason: (item.Reason as string) || '',
|
||||
Substitute: substitute?.EMail || ''
|
||||
Substitute: substitute?.EMail || '',
|
||||
Organization: (item.Organization as string) || ''
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -27,6 +27,12 @@ export interface IFormData {
|
||||
Reason: string;
|
||||
/** E-mail замещающего на время отсутствия (People). Пусто = не выбран. */
|
||||
Substitute: string;
|
||||
/**
|
||||
* Организации сотрудника (`\n`-joined «Компания — должность» на строку) —
|
||||
* подтягивается автоматически из Employee/ByEmail при выборе Employee,
|
||||
* read-only (см. shared/ui/OrganizationField).
|
||||
*/
|
||||
Organization: string;
|
||||
}
|
||||
|
||||
/** Обновление одного поля формы с сохранением типа значения. */
|
||||
@@ -50,10 +56,13 @@ export const EMPTY_DATA: IFormData = {
|
||||
StartDate: '',
|
||||
EndDate: '',
|
||||
Reason: '',
|
||||
Substitute: ''
|
||||
Substitute: '',
|
||||
Organization: ''
|
||||
};
|
||||
|
||||
/** Плейсхолдер для поля выбора сотрудника. */
|
||||
export const EMPLOYEE_PLACEHOLDER = 'Начните вводить Ф.И.О. сотрудника…';
|
||||
/** Плейсхолдер для поля выбора замещающего. */
|
||||
export const SUBSTITUTE_PLACEHOLDER = 'Начните вводить Ф.И.О. замещающего…';
|
||||
/** Заявку нужно подавать не позднее чем за столько дней до даты начала отпуска. */
|
||||
export const MIN_NOTICE_DAYS = 14;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
{ "internalName": "EndDate", "title": "Дата окончания", "type": "DateTime", "dateOnly": true },
|
||||
{ "internalName": "DaysCount", "title": "Количество дней", "type": "Number" },
|
||||
{ "internalName": "Reason", "title": "Основание (в связи с)", "type": "Note" },
|
||||
{ "internalName": "Substitute", "title": "Лицо, замещающее на время отсутствия", "type": "User", "userSelectionMode": "PeopleOnly" }
|
||||
{ "internalName": "Substitute", "title": "Лицо, замещающее на время отсутствия", "type": "User", "userSelectionMode": "PeopleOnly" },
|
||||
{ "internalName": "Organization", "title": "Организация", "type": "Note" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { CollapseSection, FormField, TextInput, TextArea, PeopleField } from '../../../shared/ui';
|
||||
import { ISectionProps, EMPLOYEE_PLACEHOLDER, SUBSTITUTE_PLACEHOLDER } from '../model';
|
||||
import { CollapseSection, FormField, TextInput, TextArea, PeopleField, OrganizationField } from '../../../shared/ui';
|
||||
import { ISectionProps, EMPLOYEE_PLACEHOLDER, SUBSTITUTE_PLACEHOLDER, MIN_NOTICE_DAYS } from '../model';
|
||||
import { isoDatePlusDays } from '../../../shared/dateUtils';
|
||||
|
||||
/** Единственная секция формы «Отпуск без содержания»: сотрудник, должность, период, основание, замещающий. */
|
||||
export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, onEmployeeChange }) => (
|
||||
<CollapseSection title="Отпуск без содержания" icon="people">
|
||||
<CollapseSection title="Заявка на отпуск без содержания" icon="people">
|
||||
<FormField label="Сотрудник" htmlFor="Employee" required full>
|
||||
<PeopleField
|
||||
context={context}
|
||||
@@ -16,8 +17,15 @@ export const RequestSection: React.FC<ISectionProps> = ({ data, set, context, on
|
||||
<FormField label="Должность" htmlFor="Position" required>
|
||||
<TextInput id="Position" value={data.Position} onChange={(v) => set('Position', v)} />
|
||||
</FormField>
|
||||
<OrganizationField value={data.Organization} />
|
||||
<FormField label="Дата начала" htmlFor="StartDate" required>
|
||||
<TextInput id="StartDate" type="date" value={data.StartDate} onChange={(v) => set('StartDate', v)} />
|
||||
<TextInput
|
||||
id="StartDate"
|
||||
type="date"
|
||||
value={data.StartDate}
|
||||
onChange={(v) => set('StartDate', v)}
|
||||
min={isoDatePlusDays(MIN_NOTICE_DAYS)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Дата окончания" htmlFor="EndDate" required>
|
||||
<TextInput id="EndDate" type="date" value={data.EndDate} onChange={(v) => set('EndDate', v)} />
|
||||
|
||||
@@ -48,7 +48,8 @@ function buildItem(
|
||||
EndDate: data.EndDate || null,
|
||||
DaysCount: days ?? null,
|
||||
Reason: data.Reason || null,
|
||||
SubstituteId: substituteId ?? null
|
||||
SubstituteId: substituteId ?? null,
|
||||
Organization: data.Organization || null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user