feat: добавил поле организация во все заявки, а также расширил компонет выбора даты для ui-kit

This commit is contained in:
Тимур Городецкий
2026-08-25 11:43:57 +05:00
parent a146877327
commit 3b4ee93b48
55 changed files with 641 additions and 143 deletions
@@ -26,6 +26,7 @@ import {
unassignedStepMessage
} from '../../shared/approval';
import { IRequestFormProps } from '../../models/types';
import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary';
import { friendlyErrorMessage } from '../../shared/diagnostics';
import {
approvalListTitle,
@@ -100,19 +101,29 @@ const ClearanceForm: React.FC<IRequestFormProps> = ({
setData((prev) => ({ ...prev, TerminationDate: prev.TerminationDate || today }));
}, [editId]);
// Должность/подразделение больше не вводятся вручную — подтягиваются из
// SharePoint User Profile (SPS-JobTitle/SPS-Department) при выборе сотрудника.
// Если профиль не синхронизирован и поле там пусто — пока просто остаётся
// пустым (TODO: фолбэк на справочник api.tnsu.kz, если это окажется частой
// проблемой на практике).
// Подразделение подтягивается из SharePoint User Profile (SPS-Department) при
// выборе сотрудника — если профиль не синхронизирован, поле остаётся пустым.
// Должность и организации — из Employee/ByEmail (см. shared/dictionary/
// employeeOrganizations.service.ts), а не из профиля: сотрудник может состоять
// сразу в нескольких организациях, должность берём из ПЕРВОЙ записи, чтобы
// Должность и Организация оставались парой из одной записи.
const handleEmployeeChange = (login: string): void => {
setData((prev) => ({ ...prev, Employee: login, Position: '', Department: '' }));
setData((prev) => ({ ...prev, Employee: login, Position: '', Department: '', Organization: '' }));
if (!login) {
return;
}
approval
.getProfileFields(login)
.then((profile) => setData((prev) => ({ ...prev, Position: profile.jobTitle, Department: profile.department })))
.then((profile) => setData((prev) => ({ ...prev, Department: profile.department })))
.catch(() => undefined);
getEmployeeOrganizations(context, login)
.then((records) =>
setData((prev) => ({
...prev,
Position: records[0]?.position || prev.Position,
Organization: formatEmployeeOrganizations(records)
}))
)
.catch(() => undefined);
};
@@ -31,6 +31,7 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro
'ParkingCard',
'AccessCard',
'TrainingAmount',
'Organization',
'RequestStatus',
'RequestUniqueCode',
'Employee/Title',
@@ -50,7 +51,8 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro
TerminationDate: termination.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
ParkingCard: !!item.ParkingCard,
AccessCard: !!item.AccessCard,
TrainingAmount: amount === null || amount === undefined ? '' : String(amount)
TrainingAmount: amount === null || amount === undefined ? '' : String(amount),
Organization: (item.Organization as string) || ''
};
return {
@@ -25,6 +25,12 @@ export interface IFormData {
AccessCard: boolean;
/** Сумма обучения — «сырые» цифры без пробелов (см. MoneyInput). Пусто = не указана. */
TrainingAmount: string;
/**
* Организации сотрудника (`\n`-joined «Компания — должность» на строку) —
* подтягивается автоматически из Employee/ByEmail при выборе Employee,
* read-only (см. shared/ui/OrganizationField).
*/
Organization: string;
}
/** Обновление одного поля формы с сохранением типа значения. */
@@ -49,7 +55,8 @@ export const EMPTY_DATA: IFormData = {
TerminationDate: '',
ParkingCard: false,
AccessCard: false,
TrainingAmount: ''
TrainingAmount: '',
Organization: ''
};
/** Плейсхолдер для поля выбора сотрудника. */
@@ -11,6 +11,7 @@
{ "internalName": "ParkingCard", "title": "Парковочная карта", "type": "Boolean" },
{ "internalName": "AccessCard", "title": "Карта СКУД", "type": "Boolean" },
{ "internalName": "TrainingAmount", "title": "Обучение: сумма", "type": "Number", "decimals": 2 }
{ "internalName": "TrainingAmount", "title": "Обучение: сумма", "type": "Number", "decimals": 2 },
{ "internalName": "Organization", "title": "Организация", "type": "Note" }
]
}
@@ -1,5 +1,5 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, MoneyInput, Toggle, PeopleField } from '../../../shared/ui';
import { CollapseSection, FormField, TextInput, MoneyInput, Toggle, PeopleField, OrganizationField } from '../../../shared/ui';
import { ISectionProps, EMPLOYEE_PLACEHOLDER } from '../model';
// Подразделение подтягивается автоматически при выборе сотрудника (см.
@@ -54,6 +54,7 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
placeholder="Подставится автоматически по выбранному сотруднику"
/>
</FormField>
<OrganizationField value={data.Organization} />
<FormField label="Дата расторжения договора" htmlFor="TerminationDate" required>
<TextInput
id="TerminationDate"
@@ -67,7 +67,8 @@ function buildItem(data: IFormData, status: string, employeeId: number, employee
TerminationDate: data.TerminationDate || null,
ParkingCard: data.ParkingCard,
AccessCard: data.AccessCard,
TrainingAmount: Number.isFinite(amount) && data.TrainingAmount.trim() !== '' ? amount : null
TrainingAmount: Number.isFinite(amount) && data.TrainingAmount.trim() !== '' ? amount : null,
Organization: data.Organization || null
};
}