refactor(RecruitingHire): CandidateName/HireDate/ReplacedEmployee -> Hire* (обход коллизии Search)
Tests / frontend-test (pull_request) Successful in 1m26s
Tests / frontend-test (pull_request) Successful in 1m26s
Те же три поля internalName делит форма «Реферальная программа» — Search индексировал managed property только у одного из двух списков (см. sp-write sitefield/field: разные Id при одинаковом имени). Тот же приём, что для VacancyOpenReason в RecruitingSelection: переименовать в уникальное имя, данные перенести. CandidateName -> HireCandidateName HireDate -> HireHireDate ReplacedEmployee -> HireReplacedEmployee На этот раз — genuine столбцы САЙТА (createsitefield + linksitefield в sp-write), а не просто колонки списка: так задумано и в самом провижинере (Ensure-SiteColumn — простые типы должны быть общим эталоном), просто на практике это не всегда соблюдается (Position тоже разъехался по Id между списками). Проверено: Id столбца на сайте и на списке совпадают. Данные на живом сайте перенесены отдельным скриптом (migrate-hire-fields.sh, разово, вне репозитория) — все 37 элементов, сверено на трёх. Задело WorkplaceSetup/prefill.ts — оно читает CandidateName/HireDate ИЗ загруженной заявки на приём для кросс-форменного предзаполнения. Referral (та же коллизия, другая форма) НЕ трогал. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -185,7 +185,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
|||||||
(login) => approval.getJobTitle(login)
|
(login) => approval.getJobTitle(login)
|
||||||
);
|
);
|
||||||
|
|
||||||
const isValid = data.CandidateName.trim().length > 0 && data.Position.trim().length > 0;
|
const isValid = data.HireCandidateName.trim().length > 0 && data.Position.trim().length > 0;
|
||||||
const validate = (): string | undefined => {
|
const validate = (): string | undefined => {
|
||||||
if (!isValid) return 'Заполните обязательные поля: Ф.И.О. кандидата, Должность.';
|
if (!isValid) return 'Заполните обязательные поля: Ф.И.О. кандидата, Должность.';
|
||||||
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
|
if (requestTypeIdState.error) return `Не удалось определить тип заявки: ${requestTypeIdState.error}`;
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ function parseJsonArray<T>(raw: unknown): T[] {
|
|||||||
|
|
||||||
// Скалярные поля (текст/выбор/число/дата) — читаем как есть, приводим к строке.
|
// Скалярные поля (текст/выбор/число/дата) — читаем как есть, приводим к строке.
|
||||||
const SCALAR_FIELDS: (keyof IFormData)[] = [
|
const SCALAR_FIELDS: (keyof IFormData)[] = [
|
||||||
'Phone', 'HireType', 'ReplacedEmployee', 'Project', 'ProjectId', 'ProjectCode',
|
'Phone', 'HireType', 'HireReplacedEmployee', 'Project', 'ProjectId', 'ProjectCode',
|
||||||
'CandidateName', 'HireDate', 'Position', 'WorkKind',
|
'HireCandidateName', 'HireHireDate', 'Position', 'WorkKind',
|
||||||
'Department', 'DepartmentId', 'SiteLocation', 'PpeHeight', 'PpeClothingSize', 'PpeShoeSize', 'ProbationPeriod',
|
'Department', 'DepartmentId', 'SiteLocation', 'PpeHeight', 'PpeClothingSize', 'PpeShoeSize', 'ProbationPeriod',
|
||||||
'SalaryProbation', 'SalaryAfterProbation', 'WorkSchedule', 'WorkScheduleOther',
|
'SalaryProbation', 'SalaryAfterProbation', 'WorkSchedule', 'WorkScheduleOther',
|
||||||
'AgencyName', 'AgencyTerms',
|
'AgencyName', 'AgencyTerms',
|
||||||
@@ -58,7 +58,7 @@ export async function loadHireRequest(itemId: number, listId?: string): Promise<
|
|||||||
const value = item[key];
|
const value = item[key];
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
writable[key] = '';
|
writable[key] = '';
|
||||||
} else if (key === 'HireDate' && typeof value === 'string') {
|
} else if (key === 'HireHireDate' && typeof value === 'string') {
|
||||||
writable[key] = value.slice(0, 10); // ISO → YYYY-MM-DD для input type=date
|
writable[key] = value.slice(0, 10); // ISO → YYYY-MM-DD для input type=date
|
||||||
} else {
|
} else {
|
||||||
writable[key] = String(value);
|
writable[key] = String(value);
|
||||||
|
|||||||
@@ -18,7 +18,16 @@ export interface IFormData {
|
|||||||
Companies: ICompany[]; // мультивыбор компаний (справочник), хранится CompaniesJson
|
Companies: ICompany[]; // мультивыбор компаний (справочник), хранится CompaniesJson
|
||||||
Phone: string;
|
Phone: string;
|
||||||
HireType: string;
|
HireType: string;
|
||||||
ReplacedEmployee: string;
|
/**
|
||||||
|
* Префикс Hire — обход коллизии managed properties: этот internalName
|
||||||
|
* (без префикса) используется и в форме «Реферальная программа» —
|
||||||
|
* ДРУГИМ полем с ДРУГИМ Id. Search индексировал managed property только у
|
||||||
|
* одного из двух списков сайта (см. sp-write props/field в tools; тот же
|
||||||
|
* приём, что VacancyOpenReason в RecruitingSelection). CandidateName и
|
||||||
|
* HireDate ниже переименованы по той же причине; ReferralForm НЕ трогать —
|
||||||
|
* там имя осталось прежним.
|
||||||
|
*/
|
||||||
|
HireReplacedEmployee: string;
|
||||||
// Проект/объект (справочник /Project) — справочное поле, на маршрут
|
// Проект/объект (справочник /Project) — справочное поле, на маршрут
|
||||||
// согласования НЕ влияет (в отличие от RecruitingSelection/TmcTransfer).
|
// согласования НЕ влияет (в отличие от RecruitingSelection/TmcTransfer).
|
||||||
Project: string;
|
Project: string;
|
||||||
@@ -30,8 +39,8 @@ export interface IFormData {
|
|||||||
SelectionRequestListId: string; // url партиции заявки на подбор (→ rplist)
|
SelectionRequestListId: string; // url партиции заявки на подбор (→ rplist)
|
||||||
SelectionRequestCode: string; // уникальный код для подписи/ссылки
|
SelectionRequestCode: string; // уникальный код для подписи/ссылки
|
||||||
// Кандидат
|
// Кандидат
|
||||||
CandidateName: string;
|
HireCandidateName: string;
|
||||||
HireDate: string;
|
HireHireDate: string;
|
||||||
Position: string;
|
Position: string;
|
||||||
WorkKind: string;
|
WorkKind: string;
|
||||||
/**
|
/**
|
||||||
@@ -75,15 +84,15 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
Companies: [],
|
Companies: [],
|
||||||
Phone: '',
|
Phone: '',
|
||||||
HireType: '',
|
HireType: '',
|
||||||
ReplacedEmployee: '',
|
HireReplacedEmployee: '',
|
||||||
Project: '',
|
Project: '',
|
||||||
ProjectId: '',
|
ProjectId: '',
|
||||||
ProjectCode: '',
|
ProjectCode: '',
|
||||||
SelectionRequestId: '',
|
SelectionRequestId: '',
|
||||||
SelectionRequestListId: '',
|
SelectionRequestListId: '',
|
||||||
SelectionRequestCode: '',
|
SelectionRequestCode: '',
|
||||||
CandidateName: '',
|
HireCandidateName: '',
|
||||||
HireDate: '',
|
HireHireDate: '',
|
||||||
Position: '',
|
Position: '',
|
||||||
WorkKind: '',
|
WorkKind: '',
|
||||||
Department: '',
|
Department: '',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
{ "internalName": "Phone", "title": "Телефон", "type": "Text" },
|
{ "internalName": "Phone", "title": "Телефон", "type": "Text" },
|
||||||
{ "internalName": "HireType", "title": "Тип приёма", "type": "Choice", "choices": ["Новый сотрудник", "Вместо уволенного"] },
|
{ "internalName": "HireType", "title": "Тип приёма", "type": "Choice", "choices": ["Новый сотрудник", "Вместо уволенного"] },
|
||||||
{ "internalName": "ReplacedEmployee", "title": "Взамен кого (Ф.И.О. уволенного)", "type": "Text" },
|
{ "internalName": "HireReplacedEmployee", "title": "Взамен кого (Ф.И.О. уволенного)", "type": "Text" },
|
||||||
|
|
||||||
{ "internalName": "Project", "title": "Проект", "type": "Text" },
|
{ "internalName": "Project", "title": "Проект", "type": "Text" },
|
||||||
{ "internalName": "ProjectId", "title": "Проект — Id", "type": "Text" },
|
{ "internalName": "ProjectId", "title": "Проект — Id", "type": "Text" },
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
{ "internalName": "SelectionRequestListId", "title": "Заявка на подбор — партиция (listId)", "type": "Text" },
|
{ "internalName": "SelectionRequestListId", "title": "Заявка на подбор — партиция (listId)", "type": "Text" },
|
||||||
{ "internalName": "SelectionRequestCode", "title": "Заявка на подбор — код", "type": "Text" },
|
{ "internalName": "SelectionRequestCode", "title": "Заявка на подбор — код", "type": "Text" },
|
||||||
|
|
||||||
{ "internalName": "CandidateName", "title": "Ф.И.О. кандидата", "type": "Text", "required": true },
|
{ "internalName": "HireCandidateName", "title": "Ф.И.О. кандидата", "type": "Text", "required": true },
|
||||||
{ "internalName": "HireDate", "title": "Дата приёма", "type": "DateTime", "dateOnly": true },
|
{ "internalName": "HireHireDate", "title": "Дата приёма", "type": "DateTime", "dateOnly": true },
|
||||||
{ "internalName": "Position", "title": "Должность", "type": "Text", "required": true },
|
{ "internalName": "Position", "title": "Должность", "type": "Text", "required": true },
|
||||||
{ "internalName": "WorkKind", "title": "Вид работ", "type": "Text" },
|
{ "internalName": "WorkKind", "title": "Вид работ", "type": "Text" },
|
||||||
{ "internalName": "SiteName", "title": "Отдел / участок (устарело)", "type": "Text" },
|
{ "internalName": "SiteName", "title": "Отдел / участок (устарело)", "type": "Text" },
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
|
|||||||
const handleHireTypeChange = (value: string): void => {
|
const handleHireTypeChange = (value: string): void => {
|
||||||
set('HireType', value);
|
set('HireType', value);
|
||||||
if (value !== HIRE_TYPE_REPLACEMENT) {
|
if (value !== HIRE_TYPE_REPLACEMENT) {
|
||||||
set('ReplacedEmployee', ''); // при не-замене очищаем скрытое поле
|
set('HireReplacedEmployee', ''); // при не-замене очищаем скрытое поле
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,11 +54,11 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
{isReplacement && (
|
{isReplacement && (
|
||||||
<FormField label="Взамен кого (Ф.И.О. уволенного)" htmlFor="ReplacedEmployee" full>
|
<FormField label="Взамен кого (Ф.И.О. уволенного)" htmlFor="HireReplacedEmployee" full>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="ReplacedEmployee"
|
id="HireReplacedEmployee"
|
||||||
value={data.ReplacedEmployee}
|
value={data.HireReplacedEmployee}
|
||||||
onChange={(v) => set('ReplacedEmployee', v)}
|
onChange={(v) => set('HireReplacedEmployee', v)}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { ISectionProps } from '../model';
|
|||||||
|
|
||||||
export const CandidateSection: React.FC<ISectionProps> = ({ data, set, context }) => (
|
export const CandidateSection: React.FC<ISectionProps> = ({ data, set, context }) => (
|
||||||
<CollapseSection title="Кандидат" icon="people">
|
<CollapseSection title="Кандидат" icon="people">
|
||||||
<FormField label="Ф.И.О. кандидата" htmlFor="CandidateName" required full>
|
<FormField label="Ф.И.О. кандидата" htmlFor="HireCandidateName" required full>
|
||||||
<TextInput id="CandidateName" value={data.CandidateName} onChange={(v) => set('CandidateName', v)} />
|
<TextInput id="HireCandidateName" value={data.HireCandidateName} onChange={(v) => set('HireCandidateName', v)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Дата приёма" htmlFor="HireDate">
|
<FormField label="Дата приёма" htmlFor="HireHireDate">
|
||||||
<TextInput id="HireDate" type="date" value={data.HireDate} onChange={(v) => set('HireDate', v)} />
|
<TextInput id="HireHireDate" type="date" value={data.HireHireDate} onChange={(v) => set('HireHireDate', v)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Должность" htmlFor="Position" required>
|
<FormField label="Должность" htmlFor="Position" required>
|
||||||
<TextInput id="Position" value={data.Position} onChange={(v) => set('Position', v)} />
|
<TextInput id="Position" value={data.Position} onChange={(v) => set('Position', v)} />
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function hireApprovalService(listId?: string): ApprovalListService {
|
|||||||
|
|
||||||
/** Осмысленный заголовок элемента списка (системная колонка Title). */
|
/** Осмысленный заголовок элемента списка (системная колонка Title). */
|
||||||
function buildTitle(data: IFormData): string {
|
function buildTitle(data: IFormData): string {
|
||||||
return ['Прием', data.CandidateName, data.Position].filter(Boolean).join(' — ');
|
return ['Прием', data.HireCandidateName, data.Position].filter(Boolean).join(' — ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Поля элемента списка данных из модели формы (текст/выбор/даты/числа). */
|
/** Поля элемента списка данных из модели формы (текст/выбор/даты/числа). */
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ export async function loadPrefillFromHire(hireId: number): Promise<IFormData> {
|
|||||||
const { data: hire } = await loadHireRequest(hireId);
|
const { data: hire } = await loadHireRequest(hireId);
|
||||||
return {
|
return {
|
||||||
...EMPTY_DATA,
|
...EMPTY_DATA,
|
||||||
FullName: hire.CandidateName,
|
FullName: hire.HireCandidateName,
|
||||||
Phone: hire.Phone,
|
Phone: hire.Phone,
|
||||||
Position: hire.Position,
|
Position: hire.Position,
|
||||||
Department: hire.Department,
|
Department: hire.Department,
|
||||||
WorkKind: hire.WorkKind,
|
WorkKind: hire.WorkKind,
|
||||||
Location: hire.SiteLocation,
|
Location: hire.SiteLocation,
|
||||||
StartDate: hire.HireDate
|
StartDate: hire.HireHireDate
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user