diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx index dbf08d0..8896c22 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx @@ -37,7 +37,7 @@ import { IAttachmentInfo } from '../../shared/attachments/attachments.service'; import { effectiveUserEmail } from '../../shared/impersonation'; -import { getEmployeeOrganizations, formatEmployeeOrganizations } from '../../shared/dictionary'; +import { getEmployeeOrganizations, employeeCompanies } from '../../shared/dictionary'; import { friendlyErrorMessage } from '../../shared/diagnostics'; import { IRequestFormProps } from '../../models/types'; import { @@ -127,7 +127,7 @@ const WorkplaceSetupForm: React.FC = ({ // Merge, а не replace: организация заявителя (ниже) резолвится отдельным // эффектом и могла успеть записаться раньше — не затираем её пустышкой // из prefilled (в loadPrefillFromHire её нет). - setData((prev) => ({ ...prefilled, Organization: prev.Organization || prefilled.Organization })); + setData((prev) => ({ ...prefilled, Companies: prev.Companies })); } }) .catch((e) => { @@ -149,8 +149,8 @@ const WorkplaceSetupForm: React.FC = ({ // Организации заявителя (Employee/ByEmail) — форма не о самом заявителе, а о // новом сотруднике (FullName — обычный текст, предзаполняется из «Заявки на // прием», и его чаще всего ещё нет в системе кадров), поэтому организация - // тянется по ТЕКУЩЕМУ пользователю. На редактировании уже сохранённое - // значение не перезатираем. + // тянется по ТЕКУЩЕМУ пользователю. Это только предложение: поле дальше + // редактируется вручную. На редактировании уже сохранённое значение не перезатираем. useEffect(() => { if (editId || !userEmail) { return; @@ -159,7 +159,10 @@ const WorkplaceSetupForm: React.FC = ({ getEmployeeOrganizations(context, userEmail) .then((records) => { if (alive) { - setData((prev) => ({ ...prev, Organization: prev.Organization || formatEmployeeOrganizations(records) })); + setData((prev) => ({ + ...prev, + Companies: prev.Companies.length > 0 ? prev.Companies : employeeCompanies(records) + })); } }) .catch(() => undefined); @@ -213,9 +216,10 @@ const WorkplaceSetupForm: React.FC = ({ (login) => approval.getJobTitle(login) ); - const isValid = data.FullName.trim().length > 0 && data.Position.trim().length > 0; + const isValid = + data.FullName.trim().length > 0 && data.Position.trim().length > 0 && data.Companies.length > 0; const validate = (): string | undefined => { - if (!isValid) return 'Заполните обязательные поля: Ф.И.О., Должность.'; + if (!isValid) return 'Заполните обязательные поля: Ф.И.О., Должность, Организация заявителя.'; if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`; if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.'; return undefined; diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx index 7e583e4..efee40d 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx @@ -35,7 +35,7 @@ import { dataListTitle, dataListTitleForListId } from '../../shared/listTitles'; -import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField, organizationText } from './model'; import { loadWorkplaceRequest } from './load'; import { workplaceApprovalService, updateReviewFields } from './submit'; import { EmployeeSection } from './sections/EmployeeSection'; @@ -218,7 +218,7 @@ const WorkplaceSetupView: React.FC = ({ context, itemI Редактировать )} - + {canEditFields && ( @@ -235,7 +235,7 @@ const WorkplaceSetupView: React.FC = ({ context, itemI )}
- + ; + .select(...SCALAR_FIELDS, 'NeedsEquipment', COMPANIES_JSON_FIELD, 'RequestStatus', 'RequestUniqueCode')() + ) as Record; const data: IFormData = { ...EMPTY_DATA }; const writable = data as unknown as Record; @@ -43,6 +59,7 @@ export async function loadWorkplaceRequest(itemId: number, listId?: string): Pro }); data.NeedsEquipment = item.NeedsEquipment === true; + data.Companies = parseCompanies(item[COMPANIES_JSON_FIELD]); return { data, status: (item.RequestStatus as string) || '', code: (item.RequestUniqueCode as string) || '' }; } diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/model.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/model.ts index b7810f8..b083014 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/model.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/model.ts @@ -1,5 +1,6 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; import { RequestTypeId } from '../../models/types'; +import { ICompany } from '../../shared/dictionary'; /** * Тип заявки этой формы — ключ реестра форм (forms/registry.ts). Имена списков @@ -7,6 +8,12 @@ import { RequestTypeId } from '../../models/types'; */ export const FORM_ID: RequestTypeId = 'it-workplace-setup'; +/** + * Note-колонка с IFormData.Companies в JSON. Имя уникальное, а не CompaniesJson, + * как в подборе/приёме: одинаковые internalName в разных формах уже ломали колонки. + */ +export const COMPANIES_JSON_FIELD = 'WorkplaceCompaniesJson'; + /** * Данные формы «Организация рабочего места». Ключи = internalName колонок SP-списка * «Организация рабочего места — данные». @@ -33,10 +40,17 @@ export interface IFormData { Location: string; StartDate: string; /** - * Организации заявителя (`\n`-joined «Компания — должность» на строку) — - * подтягивается автоматически из Employee/ByEmail по e-mail заявителя (не - * нового сотрудника — его ещё нет в системе кадров), read-only (см. - * shared/ui/OrganizationField). + * Организации заявителя — мультивыбор из справочника (CompanyPickerMulti). + * Предзаполняется из Employee/ByEmail по e-mail заявителя (не нового + * сотрудника — его ещё нет в системе кадров), дальше редактируется вручную. + * В SP хранится JSON-ом в колонке WorkplaceCompaniesJson (см. submit.ts). + */ + Companies: ICompany[]; + /** + * Текст организаций (`\n`-joined, по строке на компанию) — колонка Organization. + * При сохранении пересобирается из Companies; читается для печати (OrgLetterhead) + * и для старых заявок, созданных до мультивыбора (у них Companies пуст). + * Брать значение — через organizationText(), а не напрямую. */ Organization: string; // Оборудование и доступы (заполняются вручную) @@ -45,6 +59,15 @@ export interface IFormData { AccessRights: string; } +/** + * Организации заявителя текстом, по строке на компанию — формат, который ждут + * PrintSplitButton/OrgLetterhead. Старые заявки (до мультивыбора) — сохранённый текст. + */ +export function organizationText(data: IFormData): string { + if (data.Companies.length === 0) return data.Organization; + return data.Companies.map((c) => c.Name).join('\n'); +} + /** Обновление одного поля формы с сохранением типа значения. */ export type SetField = (key: K, value: IFormData[K]) => void; @@ -69,6 +92,7 @@ export const EMPTY_DATA: IFormData = { WorkKind: '', Location: '', StartDate: '', + Companies: [], Organization: '', NeedsEquipment: false, Software: '', diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/provisioning.json b/src/webparts/requestPortal/forms/WorkplaceSetup/provisioning.json index 131b971..3bc322b 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/provisioning.json +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/provisioning.json @@ -16,6 +16,7 @@ { "internalName": "Location", "title": "Местоположение", "type": "Text" }, { "internalName": "StartDate", "title": "Дата приёма", "type": "DateTime", "dateOnly": true }, { "internalName": "Organization", "title": "Организация заявителя", "type": "Note" }, + { "internalName": "WorkplaceCompaniesJson", "title": "Организации заявителя (JSON)", "type": "Note" }, { "internalName": "NeedsEquipment", "title": "Нужна ли техника", "type": "Boolean" }, { "internalName": "Software", "title": "Необходимое ПО", "type": "Note" }, diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/sections/ApplicantOrganizationField.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/sections/ApplicantOrganizationField.tsx new file mode 100644 index 0000000..e05dae2 --- /dev/null +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/sections/ApplicantOrganizationField.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { FormField, OrganizationField, useFormReadOnly } from '../../../shared/ui'; +import { CompanyPickerMulti } from '../../../shared/dictionary'; +import { ISectionProps } from '../model'; + +const ORGANIZATION_LABEL = 'Организация заявителя'; + +/** + * Организации заявителя: мультивыбор из справочника (как «1.5 Организация» в + * подборе). У старых заявок (до мультивыбора) компаний нет, только текст в + * Organization — в режиме чтения показываем его прежним полем. + */ +export const ApplicantOrganizationField: React.FC = ({ data, set, context, printOrgIndex }) => { + const readOnly = useFormReadOnly(); + const isLegacyText = data.Companies.length === 0 && data.Organization !== ''; + + if (readOnly && isLegacyText) { + return ; + } + + return ( + + set('Companies', v)} + placeholder="Начните вводить название организации…" + /> + + ); +}; diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/sections/EmployeeSection.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/sections/EmployeeSection.tsx index 6ee1e92..64e83cb 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/sections/EmployeeSection.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/sections/EmployeeSection.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; -import { CollapseSection, FormField, TextInput, PhoneInput, OrganizationField } from '../../../shared/ui'; +import { CollapseSection, FormField, TextInput, PhoneInput } from '../../../shared/ui'; import { DepartmentPicker, ProjectPicker } from '../../../shared/dictionary'; import { ISectionProps } from '../model'; +import { ApplicantOrganizationField } from './ApplicantOrganizationField'; export const EmployeeSection: React.FC = ({ data, set, context, printOrgIndex }) => ( @@ -49,6 +50,6 @@ export const EmployeeSection: React.FC = ({ data, set, context, p set('StartDate', v)} /> - + ); diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts index b2d0841..c9e619a 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts @@ -1,6 +1,6 @@ import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval'; import { ISaveItemResult, resolveDataList, saveRequestItem } from '../../shared/dataList'; -import { FORM_ID, IFormData } from './model'; +import { COMPANIES_JSON_FIELD, FORM_ID, IFormData, organizationText } from './model'; /** * Экземпляр сервиса списка согласования этой формы — тонкая обёртка над @@ -40,6 +40,16 @@ function buildItem(data: IFormData, status: string, sourceHireId?: number): Reco const item: Record = { Title: buildTitle(data), RequestStatus: status }; (Object.keys(data) as (keyof IFormData)[]).forEach((key) => { + if (key === 'Companies') { + item[COMPANIES_JSON_FIELD] = data.Companies.length > 0 ? JSON.stringify(data.Companies) : null; + return; + } + if (key === 'Organization') { + // Текстовая копия выбранных компаний — для печати и списка в SP (см. organizationText). + const text = organizationText(data); + item.Organization = text === '' ? null : text; + return; + } const value = data[key]; if (typeof value === 'boolean') { item[key] = value;