diff --git a/src/webparts/requestPortal/forms/BusinessTrip/BusinessTripForm.tsx b/src/webparts/requestPortal/forms/BusinessTrip/BusinessTripForm.tsx index 335b6d8..7f61a75 100644 --- a/src/webparts/requestPortal/forms/BusinessTrip/BusinessTripForm.tsx +++ b/src/webparts/requestPortal/forms/BusinessTrip/BusinessTripForm.tsx @@ -89,7 +89,7 @@ const BusinessTripForm: React.FC = ({ // Суточные строятся по маршруту (см. syncPerDiem): секции, проверки и сохранение // работают со сметой, уже приведённой к текущим точкам и датам. const formData: IFormData = { ...data, Estimate: syncPerDiem(data.Estimate, data.Route) }; - const handleEmployeeChange = useTripEmployee(context, editId, setData); + const employee = useTripEmployee(context, editId, data.Employee, setData); const approval = useMemo(() => businessTripApprovalService(editListId), [editListId]); const requestTypeIdState = useRequestTypeId(context, requestType.id); @@ -130,7 +130,7 @@ const BusinessTripForm: React.FC = ({ // Черновику нужен лишь сотрудник, отправке — все обязательные поля (см. validation.ts). const draftMissing = draftIssues(formData); - const submitMissing = submitIssues(formData); + const submitMissing = submitIssues(formData, employee.organizations); const validate = (): string | undefined => { if (draftMissing.length > 0) return `Заполните обязательные поля: ${issueLabels(draftMissing)}.`; @@ -204,7 +204,7 @@ const BusinessTripForm: React.FC = ({ onDeleted: onCancel }); - const sectionProps = { data: formData, set, context, onEmployeeChange: handleEmployeeChange }; + const sectionProps = { data: formData, set, context, employee }; return ( diff --git a/src/webparts/requestPortal/forms/BusinessTrip/load.ts b/src/webparts/requestPortal/forms/BusinessTrip/load.ts index a0c1d76..793e5fd 100644 --- a/src/webparts/requestPortal/forms/BusinessTrip/load.ts +++ b/src/webparts/requestPortal/forms/BusinessTrip/load.ts @@ -28,6 +28,8 @@ export async function loadBusinessTripRequest(itemId: number, listId?: string): 'SubmitDate', 'Position', 'Organization', + 'TripCompany', + 'TripCompanyId', 'TripRouteJson', 'TripPurpose', 'TripTasksJson', @@ -47,6 +49,8 @@ export async function loadBusinessTripRequest(itemId: number, listId?: string): Employee: employee?.EMail || '', Position: (item.Position as string) || '', Organization: (item.Organization as string) || '', + TripCompany: (item.TripCompany as string) || '', + TripCompanyId: (item.TripCompanyId as string) || '', Route: parseRoute(item.TripRouteJson), TripPurpose: (item.TripPurpose as string) || '', Tasks: parseTasks(item.TripTasksJson), diff --git a/src/webparts/requestPortal/forms/BusinessTrip/model.ts b/src/webparts/requestPortal/forms/BusinessTrip/model.ts index 6f94a9f..ecb474e 100644 --- a/src/webparts/requestPortal/forms/BusinessTrip/model.ts +++ b/src/webparts/requestPortal/forms/BusinessTrip/model.ts @@ -1,6 +1,7 @@ // import type — чтобы тесты (route/estimate/validation) не тянули SPFx-рантайм через модель. import type { WebPartContext } from '@microsoft/sp-webpart-base'; import type { RequestTypeId } from '../../models/types'; +import type { IEmployeeOrganization } from '../../shared/dictionary'; /** * Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков @@ -253,6 +254,13 @@ export interface IFormData { Position: string; /** Организации сотрудника (`\n`-joined «Компания — должность»), read-only, см. OrganizationField. */ Organization: string; + /** + * От какой компании едет сотрудник — одна из его организаций (Employee/ByEmail). + * Сотрудник может работать в нескольких компаниях, а командирует только одна. + */ + TripCompany: string; + /** CompanyId выбранной организации из справочника; '' — не выбрана. */ + TripCompanyId: string; Route: ITripRoute; TripPurpose: string; @@ -270,16 +278,25 @@ export interface IFormData { /** Обновление одного поля формы с сохранением типа значения. */ export type SetField = (key: K, value: IFormData[K]) => void; +/** Сотрудник заявки и выбор организации, от которой он едет (см. useTripEmployee). */ +export interface ITripEmployee { + /** Организации текущего сотрудника — варианты «Командирующей организации». */ + organizations: IEmployeeOrganization[]; + onEmployeeChange: (login: string) => void; + /** key — значение варианта (см. tripCompany.ts/companyKey); '' — сбросить выбор. */ + onCompanyChange: (key: string) => void; +} + /** Пропсы секции формы: срез данных + колбэк обновления + контекст (people picker / справочники). */ export interface ISectionProps { data: IFormData; set: SetField; context: WebPartContext; /** - * Выбор сотрудника подтягивает его должность и организации — поэтому у поля - * отдельный обработчик. Не передан (режим чтения) — правка через set. + * Выбор сотрудника и организации подтягивает должность и организации — поэтому + * у этих полей свои обработчики. Не передан (режим чтения) — правка через set. */ - onEmployeeChange?: (login: string) => void; + employee?: ITripEmployee; } export const DEFAULT_PURPOSE = 'Производственная необходимость'; @@ -327,6 +344,8 @@ export const EMPTY_DATA: IFormData = { Employee: '', Position: '', Organization: '', + TripCompany: '', + TripCompanyId: '', Route: EMPTY_ROUTE, TripPurpose: DEFAULT_PURPOSE, // Новая заявка сразу показывает одну пустую строку задачи — понятнее, что заполнять. diff --git a/src/webparts/requestPortal/forms/BusinessTrip/provisioning.json b/src/webparts/requestPortal/forms/BusinessTrip/provisioning.json index a1a79af..bba3c97 100644 --- a/src/webparts/requestPortal/forms/BusinessTrip/provisioning.json +++ b/src/webparts/requestPortal/forms/BusinessTrip/provisioning.json @@ -8,6 +8,8 @@ { "internalName": "Employee", "title": "Сотрудник", "type": "User", "userSelectionMode": "PeopleOnly", "required": true }, { "internalName": "Position", "title": "Должность", "type": "Text" }, { "internalName": "Organization", "title": "Организация", "type": "Note" }, + { "internalName": "TripCompany", "title": "Командирующая организация", "type": "Text" }, + { "internalName": "TripCompanyId", "title": "Id командирующей организации (справочник)", "type": "Text" }, { "internalName": "TripRouteJson", "title": "Маршрут (JSON)", "type": "Note" }, { "internalName": "TripRoute", "title": "Маршрут", "type": "Note" }, diff --git a/src/webparts/requestPortal/forms/BusinessTrip/sections/EmployeeSection.tsx b/src/webparts/requestPortal/forms/BusinessTrip/sections/EmployeeSection.tsx index be0f845..e063095 100644 --- a/src/webparts/requestPortal/forms/BusinessTrip/sections/EmployeeSection.tsx +++ b/src/webparts/requestPortal/forms/BusinessTrip/sections/EmployeeSection.tsx @@ -1,28 +1,49 @@ import * as React from 'react'; -import { FormGrid, FormField, TextInput, PeopleField, OrganizationField } from '../../../shared/ui'; +import { FormGrid, FormField, TextInput, PeopleField, OrganizationField, Select } from '../../../shared/ui'; import { ISectionProps, EMPLOYEE_PLACEHOLDER } from '../model'; +import { companyOptions, selectedCompanyKey } from '../tripCompany'; /** * Командируемый: по умолчанию сам инициатор, но можно выбрать другого * сотрудника (руководитель оформляет за подчинённого) — должность и * организации подтянутся по выбранному. Дату подачи ставит форма. + * Сотрудник может работать в нескольких компаниях — тогда выбирают, от какой он едет. */ -export const EmployeeSection: React.FC = ({ data, set, context, onEmployeeChange }) => ( - - - set('Employee', v))} - placeholder={EMPLOYEE_PLACEHOLDER} - /> - - - set('Position', v)} /> - - - set('SubmitDate', v)} readOnly /> - - - -); +export const EmployeeSection: React.FC = ({ data, set, context, employee }) => { + const options = companyOptions(employee?.organizations ?? [], data); + // Заявки, поданные до появления выбора организации, показывают прежний список всех организаций. + const isLegacyRequest = !employee && !data.TripCompany; + + return ( + + + set('Employee', v))} + placeholder={EMPLOYEE_PLACEHOLDER} + /> + + + set('Position', v)} /> + + + set('SubmitDate', v)} readOnly /> + + {isLegacyRequest && } + {!isLegacyRequest && options.length > 0 && ( + +