diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx index b4651b2..1ed32c8 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx @@ -194,8 +194,11 @@ const RecruitingHireForm: React.FC = ({ (login) => approval.getJobTitle(login) ); - const isValid = data.HireCandidateName.trim().length > 0 && data.Position.trim().length > 0; + // При совмещении (2+ компании в «Организация») нужно явно указать, какая из них основная. + const mainCompanyMissing = data.Companies.length >= 2 && !data.MainCompanyId; + const isValid = data.HireCandidateName.trim().length > 0 && data.Position.trim().length > 0 && !mainCompanyMissing; const validate = (): string | undefined => { + if (mainCompanyMissing) return 'Выбрано несколько организаций — укажите, какая из них основное место работы.'; if (!isValid) return 'Заполните обязательные поля: Ф.И.О. кандидата, Должность.'; if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`; if (!requestTypeId) return 'Тип заявки ещё определяется — подождите немного и повторите.'; diff --git a/src/webparts/requestPortal/forms/RecruitingHire/load.ts b/src/webparts/requestPortal/forms/RecruitingHire/load.ts index f12c7c6..befc41c 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/load.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/load.ts @@ -1,7 +1,7 @@ import { getSP } from '../../shared/pnp/pnpjs'; import { ICompany } from '../../shared/dictionary'; import { dataListTitle } from '../../shared/listTitles'; -import { FORM_ID, IFormData, EMPTY_DATA } from './model'; +import { FORM_ID, IFormData, EMPTY_DATA, ICompanyShare } from './model'; /** Безопасный разбор JSON-массива из Note-колонки (пустой при ошибке/пустом). */ function parseJsonArray(raw: unknown): T[] { @@ -16,6 +16,7 @@ function parseJsonArray(raw: unknown): T[] { // Скалярные поля (текст/выбор/число/дата) — читаем как есть, приводим к строке. const SCALAR_FIELDS: (keyof IFormData)[] = [ + 'MainCompanyId', 'Phone', 'HireType', 'HireReplacedEmployee', 'Project', 'ProjectId', 'ProjectCode', 'HireCandidateName', 'HireHireDate', 'Position', 'WorkKind', 'Department', 'SiteLocation', 'PpeHeight', 'PpeClothingSize', 'PpeShoeSize', 'ProbationPeriod', @@ -44,13 +45,15 @@ export async function loadHireRequest(itemId: number, listId?: string): Promise< const list = listId ? sp.web.getList(listId) : sp.web.lists.getByTitle(dataListTitle(FORM_ID)); const item = (await list .items.getById(itemId) - .select(...SCALAR_FIELDS, 'CompaniesJson', 'RecruitingAgency', 'RequestStatus', 'RequestUniqueCode')()) as Record; + .select(...SCALAR_FIELDS, 'CompaniesJson', 'CompanySharesJson', 'RecruitingAgency', 'RequestStatus', 'RequestUniqueCode')()) as Record; const data: IFormData = { ...EMPTY_DATA }; const writable = data as unknown as Record; // Мультивыбор компаний хранится Note-колонкой CompaniesJson (не одноимённой колонкой). (data as unknown as Record).Companies = parseJsonArray(item.CompaniesJson); + // % ставки по компаниям (совмещение) — аналогично, Note-колонка CompanySharesJson. + (data as unknown as Record).CompanyShares = parseJsonArray(item.CompanySharesJson); // Флаг агентства — булево поле (Yes/No), не строка. (data as unknown as Record).RecruitingAgency = !!item.RecruitingAgency; diff --git a/src/webparts/requestPortal/forms/RecruitingHire/model.ts b/src/webparts/requestPortal/forms/RecruitingHire/model.ts index 22eea77..600e4ec 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/model.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/model.ts @@ -13,9 +13,19 @@ export const FORM_ID: RequestTypeId = 'recruiting-hire'; * Данные формы «Заявка на прием». Ключи = internalName колонок списка данных * этой формы (см. shared/listTitles.ts), чтобы при записи маппинг был 1:1. */ +/** % ставки на одну из выбранных компаний (см. CompanySharesField) — актуально при совмещении (2+ компании). */ +export interface ICompanyShare { + companyId: string; + percent: string; +} + export interface IFormData { // Заявка (шапка) Companies: ICompany[]; // мультивыбор компаний (справочник), хранится CompaniesJson + /** Id компании из Companies — «Основное место работы»; '' — не выбрано (или выбрана всего 1 компания). */ + MainCompanyId: string; + /** % ставки на каждую компанию из Companies — заполняется при совмещении (2+ компании), хранится CompanySharesJson. */ + CompanyShares: ICompanyShare[]; Phone: string; HireType: string; /** @@ -74,6 +84,8 @@ export interface ISectionProps { export const EMPTY_DATA: IFormData = { Companies: [], + MainCompanyId: '', + CompanyShares: [], Phone: '', HireType: '', HireReplacedEmployee: '', diff --git a/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json b/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json index 655749c..9fe463d 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json +++ b/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json @@ -5,6 +5,8 @@ { "internalName": "RequestStatus", "title": "Статус", "type": "Choice", "choices": ["Черновик", "На согласовании", "Согласовано", "Отклонено", "Возвращена на доработку", "Выполнение", "Закрыта"] }, { "internalName": "CompaniesJson", "title": "Компании (JSON)", "type": "Note" }, + { "internalName": "MainCompanyId", "title": "Основное место работы (Id компании)", "type": "Text" }, + { "internalName": "CompanySharesJson", "title": "Распределение ЗП по компаниям (JSON)", "type": "Note" }, { "internalName": "Phone", "title": "Телефон", "type": "Text" }, { "internalName": "HireType", "title": "Тип приёма", "type": "Choice", "choices": ["Новый сотрудник", "Вместо уволенного"] }, diff --git a/src/webparts/requestPortal/forms/RecruitingHire/sections/AgencySection.tsx b/src/webparts/requestPortal/forms/RecruitingHire/sections/AgencySection.tsx index 81a0a9c..0937b71 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/sections/AgencySection.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/sections/AgencySection.tsx @@ -17,8 +17,8 @@ export const AgencySection: React.FC = ({ data, set }) => { }; return ( - - + + {data.RecruitingAgency && ( diff --git a/src/webparts/requestPortal/forms/RecruitingHire/sections/ApplicationSection.tsx b/src/webparts/requestPortal/forms/RecruitingHire/sections/ApplicationSection.tsx index 3098978..0ace1f6 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/sections/ApplicationSection.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/sections/ApplicationSection.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { CollapseSection, FormField, TextInput, PhoneInput, Select } from '../../../shared/ui'; -import { CompanyPickerMulti, ProjectPicker } from '../../../shared/dictionary'; +import { CompanyPickerMulti, ProjectPicker, ICompany } from '../../../shared/dictionary'; import { ISectionProps, HIRE_TYPE_OPTIONS, HIRE_TYPE_REPLACEMENT } from '../model'; import { SelectionRequestField } from './SelectionRequestField'; +import { CompanySharesField } from './CompanySharesField'; export const ApplicationSection: React.FC = ({ data, set, context }) => { const isReplacement = data.HireType === HIRE_TYPE_REPLACEMENT; @@ -14,18 +15,41 @@ export const ApplicationSection: React.FC = ({ data, set, context } }; + // Компания убрана из выбора — чистим ссылки на неё в «Основное место + // работы» / % ставки, иначе они молча уйдут в заявку по несуществующей компании. + const handleCompaniesChange = (companies: ICompany[]): void => { + set('Companies', companies); + const ids = new Set(companies.map((c) => c.Id)); + if (companies.length < 2) { + set('MainCompanyId', ''); + set('CompanyShares', []); + return; + } + if (data.MainCompanyId && !ids.has(data.MainCompanyId)) { + set('MainCompanyId', ''); + } + set('CompanyShares', data.CompanyShares.filter((s) => ids.has(s.companyId))); + }; + return ( - - + + set('Companies', companies)} + onChange={handleCompaniesChange} placeholder="Начните вводить название организации…" /> - + set('MainCompanyId', v)} + shares={data.CompanyShares} + onSharesChange={(v) => set('CompanyShares', v)} + /> + = ({ data, set, context placeholder="Начните вводить название или код проекта…" /> - + - + set('Phone', v)} /> - + + + +
+ {companies.map((c) => ( +
+ {c.Name} + setPercent(c.Id, v)} + ariaLabel={`% ставки — ${c.Name}`} + borderless + /> + % +
+ ))} +
+
+ + ); +}; diff --git a/src/webparts/requestPortal/forms/RecruitingHire/sections/ConditionsSection.tsx b/src/webparts/requestPortal/forms/RecruitingHire/sections/ConditionsSection.tsx index 6c97ec6..e1baf03 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/sections/ConditionsSection.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/sections/ConditionsSection.tsx @@ -3,8 +3,8 @@ import { CollapseSection, FormField, TextInput, MoneyInput, Select } from '../.. import { ISectionProps, OTHER_VALUE, WORK_SCHEDULE_OPTIONS, PROBATION_PERIOD_OPTIONS } from '../model'; export const ConditionsSection: React.FC = ({ data, set }) => ( - - + + = ({ data, set }) => ( - - + + set('PpeHeight', v)} /> - + set('PpeClothingSize', v)} /> - + set('PpeShoeSize', v)} /> diff --git a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts index e0b4121..7fc7a52 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts @@ -29,6 +29,11 @@ function buildItem(data: IFormData, status: string): Record { item.CompaniesJson = data.Companies.length > 0 ? JSON.stringify(data.Companies) : null; return; } + if (key === 'CompanyShares') { + // % ставки по компаниям (совмещение) — таблица, сериализуем в Note. + item.CompanySharesJson = data.CompanyShares.length > 0 ? JSON.stringify(data.CompanyShares) : null; + return; + } const value = data[key]; if (NUMBER_FIELDS.indexOf(key) !== -1) { item[key] = value === '' ? null : Number(value); diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx index cc358be..5c8c744 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/RecruitingSelectionForm.tsx @@ -44,7 +44,7 @@ import { dataListTitle, dataListTitleForListId } from '../../shared/listTitles'; -import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model'; +import { FORM_ID, IFormData, EMPTY_DATA, SetField, todayIso } from './model'; import { saveSelectionRequest, selectionApprovalService } from './submit'; import { loadSelectionRequest } from './load'; import { ApplicationSection } from './sections/ApplicationSection'; @@ -125,7 +125,7 @@ const RecruitingSelectionForm: React.FC = ({ } let alive = true; const user = context.pageContext.user; - setData((prev) => ({ ...prev, Responsible: prev.Responsible || user.email })); + setData((prev) => ({ ...prev, Responsible: prev.Responsible || user.email, SubmitDate: todayIso() })); getEmployeeOrganizations(context, user.email) .then((records) => { if (alive) { @@ -365,7 +365,7 @@ const RecruitingSelectionForm: React.FC = ({ editor={matrix} search={(q) => approval.searchUsers(q)} waiting={!matrixReady} - waitingText="Выберите «Объект/Проект» (п. 1.6), чтобы построить маршрут согласования." + waitingText="Выберите «Объект/Проект» (п. 1.7), чтобы построить маршрут согласования." /> = ({ data, set, context /> - set('SubmitDate', v)} /> + set('SubmitDate', v)} readOnly /> = ({ onChange={(v) => set("Bonuses", v)} /> - -