feat: подсветка полей, изменённых согласующим, с прежними значениями
Tests / frontend-test (push) Successful in 1m15s
Tests / frontend-test (push) Successful in 1m15s
This commit is contained in:
@@ -6,6 +6,8 @@ import {
|
||||
FormCard,
|
||||
InitiatorLine,
|
||||
FormModeProvider,
|
||||
FieldChangesProvider,
|
||||
FieldChangesBanner,
|
||||
FileDropzone,
|
||||
AttachmentList,
|
||||
CollapseSection,
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
getDraftDeleteHandler,
|
||||
useRequestSubmit
|
||||
} from '../../shared/ui';
|
||||
import { IFieldChange, toChangeMap } from '../../shared/fieldChanges/fieldChanges';
|
||||
import {
|
||||
ApprovalMatrix,
|
||||
StatusBadge,
|
||||
@@ -87,6 +90,8 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
||||
const [requestId, setRequestId] = useState<number | undefined>(editId);
|
||||
const [requestStatus, setRequestStatus] = useState<string | undefined>(undefined);
|
||||
const [requestCode, setRequestCode] = useState<string | undefined>(undefined);
|
||||
// Журнал правок согласующего — виден и здесь, когда заявку вернули на доработку.
|
||||
const [approverChanges, setApproverChanges] = useState<IFieldChange[]>([]);
|
||||
const submit = useRequestSubmit();
|
||||
// onCodeLoaded — колбэк родителя (RequestPortal → EditFeature): его ссылка
|
||||
// может меняться на каждом рендере, поэтому в зависимости эффектов идёт
|
||||
@@ -112,6 +117,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
||||
.then(([loaded, atts]) => {
|
||||
if (alive) {
|
||||
setData(loaded.data);
|
||||
setApproverChanges(loaded.approverChanges);
|
||||
setRequestStatus(loaded.status);
|
||||
setRequestCode(loaded.code || undefined);
|
||||
if (loaded.code) notifyCode(loaded.code);
|
||||
@@ -405,13 +411,16 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({
|
||||
)}
|
||||
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
||||
|
||||
<FormModeProvider readOnly={readOnly}>
|
||||
<ApplicationSection {...sectionProps} />
|
||||
<CandidateSection {...sectionProps} />
|
||||
<PpeSection {...sectionProps} />
|
||||
<ConditionsSection {...sectionProps} />
|
||||
<AgencySection {...sectionProps} />
|
||||
</FormModeProvider>
|
||||
<FieldChangesBanner changes={approverChanges} />
|
||||
<FieldChangesProvider changes={toChangeMap(approverChanges)}>
|
||||
<FormModeProvider readOnly={readOnly}>
|
||||
<ApplicationSection {...sectionProps} />
|
||||
<CandidateSection {...sectionProps} />
|
||||
<PpeSection {...sectionProps} />
|
||||
<ConditionsSection {...sectionProps} />
|
||||
<AgencySection {...sectionProps} />
|
||||
</FormModeProvider>
|
||||
</FieldChangesProvider>
|
||||
|
||||
{!readOnly && (
|
||||
<CollapseSection title="Вложения" icon="upload">
|
||||
|
||||
@@ -2,7 +2,22 @@ import * as React from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import styles from './RecruitingHireView.module.scss';
|
||||
import { Alert, Button, FormCard, InitiatorLine, FormModeProvider, CollapseSection, FormField, AttachmentList, Icon, PrintButton } from '../../shared/ui';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
FormCard,
|
||||
InitiatorLine,
|
||||
FormModeProvider,
|
||||
CollapseSection,
|
||||
FormField,
|
||||
AttachmentList,
|
||||
Icon,
|
||||
PrintButton,
|
||||
FieldChangesProvider,
|
||||
FieldChangesBanner
|
||||
} from '../../shared/ui';
|
||||
import { IFieldChange, mergeFieldChanges, toChangeMap } from '../../shared/fieldChanges/fieldChanges';
|
||||
import { HIRE_CHANGE_TRACKING } from './changeTracking';
|
||||
import {
|
||||
ApproverService,
|
||||
DecisionPanel,
|
||||
@@ -91,6 +106,11 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
// Все поля заявки правит на своём активном шаге согласующий из
|
||||
// shared/approval/fullEditRights.ts (сейчас — Алтынай Бултурукова).
|
||||
const [fullEditable, setFullEditable] = useState<boolean>(false);
|
||||
// То, что сейчас лежит в заявке на сервере (до несохранённых правок), — с ним
|
||||
// сравниваем при сохранении, чтобы записать в журнал только реальные правки.
|
||||
const [savedData, setSavedData] = useState<IFormData>(EMPTY_DATA);
|
||||
// Журнал правок согласующего с полным правом — подсветка полей и «Было: …».
|
||||
const [approverChanges, setApproverChanges] = useState<IFieldChange[]>([]);
|
||||
const [saveState, setSaveState] = useState<SaveState>('idle');
|
||||
const [saveErrorMessage, setSaveErrorMessage] = useState<string>('');
|
||||
// Право удалять согласующих из уже поданного маршрута — см.
|
||||
@@ -180,7 +200,11 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
// формы, загруженным при открытии экрана.
|
||||
const saveApproverFields = async (): Promise<void> => {
|
||||
if (fullEditable) {
|
||||
await updateAllHireFields(itemId, data, listId);
|
||||
const editor = { name: context.pageContext.user.displayName, email: effectiveUserEmail(context) };
|
||||
const nextChanges = mergeFieldChanges(approverChanges, savedData, data, HIRE_CHANGE_TRACKING, editor, new Date());
|
||||
await updateAllHireFields(itemId, data, nextChanges, listId);
|
||||
setApproverChanges(nextChanges);
|
||||
setSavedData(data);
|
||||
return;
|
||||
}
|
||||
const fields: Partial<IApproverEditableFields> = {};
|
||||
@@ -247,6 +271,8 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
.then(([loaded, files]) => {
|
||||
if (alive) {
|
||||
setData(loaded.data);
|
||||
setSavedData(loaded.data);
|
||||
setApproverChanges(loaded.approverChanges);
|
||||
setStatus(loaded.status);
|
||||
setCode(loaded.code);
|
||||
notifyCode(loaded.code);
|
||||
@@ -334,14 +360,17 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
||||
}
|
||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||
>
|
||||
<FieldChangesBanner changes={approverChanges} />
|
||||
{/* Вложения ниже — вне этого блока: полное право открывает только поля. */}
|
||||
<FormModeProvider readOnly={!fullEditable}>
|
||||
<ApplicationSection {...sectionProps} />
|
||||
<CandidateSection {...sectionProps} />
|
||||
<PpeSection {...sectionProps} />
|
||||
<ConditionsSection {...sectionProps} />
|
||||
<AgencySection {...sectionProps} />
|
||||
</FormModeProvider>
|
||||
<FieldChangesProvider changes={toChangeMap(approverChanges)}>
|
||||
<FormModeProvider readOnly={!fullEditable}>
|
||||
<ApplicationSection {...sectionProps} />
|
||||
<CandidateSection {...sectionProps} />
|
||||
<PpeSection {...sectionProps} />
|
||||
<ConditionsSection {...sectionProps} />
|
||||
<AgencySection {...sectionProps} />
|
||||
</FormModeProvider>
|
||||
</FieldChangesProvider>
|
||||
|
||||
<div className="rp-no-print">
|
||||
<CollapseSection title="Вложения" icon="upload">
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { IChangeTrackingConfig, formatFieldValue, EMPTY_VALUE } from '../../shared/fieldChanges/fieldChanges';
|
||||
import { IFormData, ICompanyShare } from './model';
|
||||
|
||||
/** Колонка журнала правок согласующего (Note, JSON) — см. provisioning.json. */
|
||||
export const APPROVER_CHANGES_FIELD = 'HireApproverChangesJson';
|
||||
|
||||
function companyName(data: IFormData, companyId: string): string {
|
||||
const company = data.Companies.find((c) => c.Id === companyId);
|
||||
return company ? company.Name : companyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Что пишем в журнал правок согласующего (shared/fieldChanges).
|
||||
*
|
||||
* Без своего поля на форме (не пишем): Id/код проекта — меняются вместе с
|
||||
* «Проект»; связь с подбором — вместе с её кодом (1.1); HireOnArrival — флажок
|
||||
* внутри поля 2.2, его правку видно по самой дате.
|
||||
*/
|
||||
export const HIRE_CHANGE_TRACKING: IChangeTrackingConfig<IFormData> = {
|
||||
ignore: ['ProjectId', 'ProjectCode', 'SelectionRequestId', 'SelectionRequestListId', 'HireOnArrival'],
|
||||
formatters: {
|
||||
MainCompanyId: (value, data) => (value ? companyName(data, String(value)) : EMPTY_VALUE),
|
||||
CompanyShares: (value, data) => {
|
||||
const shares = (value as ICompanyShare[]) || [];
|
||||
if (shares.length === 0) return EMPTY_VALUE;
|
||||
return shares.map((s) => `${companyName(data, s.companyId)}: ${formatFieldValue(s.percent)}%`).join(', ');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { ICompany } from '../../shared/dictionary';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { IFieldChange, parseFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA, ICompanyShare } from './model';
|
||||
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
||||
|
||||
/** Безопасный разбор JSON-массива из Note-колонки (пустой при ошибке/пустом). */
|
||||
function parseJsonArray<T>(raw: unknown): T[] {
|
||||
@@ -30,6 +32,8 @@ export interface ILoadedRequest {
|
||||
status: string;
|
||||
/** Уникальный код заявки; '' — если запись создана до появления поля. */
|
||||
code: string;
|
||||
/** Журнал правок согласующего (подсветка полей) — см. shared/fieldChanges. */
|
||||
approverChanges: IFieldChange[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +55,7 @@ export async function loadHireRequest(itemId: number, listId?: string): Promise<
|
||||
'CompanySharesJson',
|
||||
'RecruitingAgency',
|
||||
'HireOnArrival',
|
||||
APPROVER_CHANGES_FIELD,
|
||||
'RequestStatus',
|
||||
'RequestUniqueCode'
|
||||
)()) as Record<string, unknown>;
|
||||
@@ -77,5 +82,10 @@ export async function loadHireRequest(itemId: number, listId?: string): Promise<
|
||||
}
|
||||
});
|
||||
|
||||
return { data, status: (item.RequestStatus as string) || '', code: (item.RequestUniqueCode as string) || '' };
|
||||
return {
|
||||
data,
|
||||
status: (item.RequestStatus as string) || '',
|
||||
code: (item.RequestUniqueCode as string) || '',
|
||||
approverChanges: parseFieldChanges(item[APPROVER_CHANGES_FIELD])
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
{ "internalName": "HireCandidateName", "title": "Ф.И.О. кандидата", "type": "Text", "required": true },
|
||||
{ "internalName": "HireHireDate", "title": "Дата приёма", "type": "DateTime", "dateOnly": true },
|
||||
{ "internalName": "HireApproverChangesJson", "title": "Изменения согласующего — приём (JSON)", "type": "Note" },
|
||||
{ "internalName": "HireOnArrival", "title": "Дата приёма — по приезду на объект", "type": "Boolean" },
|
||||
{ "internalName": "Position", "title": "Должность", "type": "Text", "required": true },
|
||||
{ "internalName": "SiteName", "title": "Отдел / участок (устарело)", "type": "Text" },
|
||||
|
||||
@@ -18,7 +18,7 @@ export const AgencySection: React.FC<ISectionProps> = ({ data, set }) => {
|
||||
|
||||
return (
|
||||
<CollapseSection title="5. Рекрутинговое агентство" icon="building">
|
||||
<FormField label="5.1 Привлекается рекрутинговое агентство">
|
||||
<FormField label="5.1 Привлекается рекрутинговое агентство" fieldKey="RecruitingAgency">
|
||||
<Toggle checked={data.RecruitingAgency} onChange={handleToggle} onLabel="Да" />
|
||||
</FormField>
|
||||
{data.RecruitingAgency && (
|
||||
|
||||
@@ -39,7 +39,7 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
|
||||
|
||||
return (
|
||||
<CollapseSection title="1. Заявка" icon="clipboard">
|
||||
<FormField label="1.1 Заявка на подбор" full>
|
||||
<FormField label="1.1 Заявка на подбор" fieldKey="SelectionRequestCode" full>
|
||||
<SelectionRequestField data={data} set={set} />
|
||||
</FormField>
|
||||
<FormField label="1.2 Организация" htmlFor="Companies" required full>
|
||||
|
||||
@@ -46,7 +46,7 @@ export const CompanySharesField: React.FC<ICompanySharesFieldProps> = ({
|
||||
placeholder="— выберите —"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Распределение ЗП по компаниям" full>
|
||||
<FormField label="Распределение ЗП по компаниям" fieldKey="CompanyShares" full>
|
||||
<div className={styles.row}>
|
||||
{companies.map((c) => (
|
||||
<div key={c.Id} className={styles.item}>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval';
|
||||
import { ISaveItemResult, resolveDataList, saveRequestItem } from '../../shared/dataList';
|
||||
import { IFieldChange, serializeFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
||||
import { FORM_ID, IFormData } from './model';
|
||||
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
||||
|
||||
// Числовые поля: пустая строка → null, иначе Number().
|
||||
const NUMBER_FIELDS: (keyof IFormData)[] = ['PpeHeight', 'PpeShoeSize', 'SalaryProbation', 'SalaryAfterProbation', 'SelectionRequestId'];
|
||||
@@ -53,11 +55,18 @@ export type ISaveResult = ISaveItemResult;
|
||||
|
||||
/**
|
||||
* Перезаписать ВСЕ поля заявки — для согласующего с полным правом правки
|
||||
* (см. shared/approval/fullEditRights.ts). Статус, маршрут и вложения не
|
||||
* (см. shared/approval/fullEditRights.ts) — вместе с журналом его правок
|
||||
* (подсветка полей, см. shared/fieldChanges). Статус, маршрут и вложения не
|
||||
* трогаются: заявка остаётся на том же шаге согласования.
|
||||
*/
|
||||
export async function updateAllHireFields(id: number, data: IFormData, listId?: string): Promise<void> {
|
||||
await resolveDataList(FORM_ID, listId).items.getById(id).update(buildFields(data));
|
||||
export async function updateAllHireFields(
|
||||
id: number,
|
||||
data: IFormData,
|
||||
approverChanges: IFieldChange[],
|
||||
listId?: string
|
||||
): Promise<void> {
|
||||
const item = { ...buildFields(data), [APPROVER_CHANGES_FIELD]: serializeFieldChanges(approverChanges) };
|
||||
await resolveDataList(FORM_ID, listId).items.getById(id).update(item);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
FormCard,
|
||||
InitiatorLine,
|
||||
FormModeProvider,
|
||||
FieldChangesProvider,
|
||||
FieldChangesBanner,
|
||||
FileDropzone,
|
||||
AttachmentList,
|
||||
CollapseSection,
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
getDraftDeleteHandler,
|
||||
useRequestSubmit
|
||||
} from '../../shared/ui';
|
||||
import { IFieldChange, toChangeMap } from '../../shared/fieldChanges/fieldChanges';
|
||||
import {
|
||||
ApprovalMatrix,
|
||||
StatusBadge,
|
||||
@@ -74,6 +77,8 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
||||
const [requestId, setRequestId] = useState<number | undefined>(editId);
|
||||
const [requestStatus, setRequestStatus] = useState<string | undefined>(undefined);
|
||||
const [requestCode, setRequestCode] = useState<string | undefined>(undefined);
|
||||
// Журнал правок согласующего — виден и здесь, когда заявку вернули на доработку.
|
||||
const [approverChanges, setApproverChanges] = useState<IFieldChange[]>([]);
|
||||
const submit = useRequestSubmit();
|
||||
// onCodeLoaded — колбэк родителя (RequestPortal → EditFeature): его ссылка
|
||||
// может меняться на каждом рендере, поэтому в зависимости эффектов идёт
|
||||
@@ -98,6 +103,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
||||
.then(([loaded, atts]) => {
|
||||
if (alive) {
|
||||
setData(loaded.data);
|
||||
setApproverChanges(loaded.approverChanges);
|
||||
setRequestStatus(loaded.status);
|
||||
setRequestCode(loaded.code || undefined);
|
||||
if (loaded.code) notifyCode(loaded.code);
|
||||
@@ -418,13 +424,16 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({
|
||||
)}
|
||||
{submit.error && <Alert variant="err">{submit.error}</Alert>}
|
||||
|
||||
<FormModeProvider readOnly={readOnly}>
|
||||
<ApplicationSection {...sectionProps} />
|
||||
<DepartmentSection {...sectionProps} />
|
||||
<VacancySection {...sectionProps} />
|
||||
<HealthTripsSection {...sectionProps} />
|
||||
<ConditionsSection {...sectionProps} />
|
||||
</FormModeProvider>
|
||||
<FieldChangesBanner changes={approverChanges} />
|
||||
<FieldChangesProvider changes={toChangeMap(approverChanges)}>
|
||||
<FormModeProvider readOnly={readOnly}>
|
||||
<ApplicationSection {...sectionProps} />
|
||||
<DepartmentSection {...sectionProps} />
|
||||
<VacancySection {...sectionProps} />
|
||||
<HealthTripsSection {...sectionProps} />
|
||||
<ConditionsSection {...sectionProps} />
|
||||
</FormModeProvider>
|
||||
</FieldChangesProvider>
|
||||
|
||||
{!readOnly && (
|
||||
<CollapseSection title="Вложения" icon="upload">
|
||||
|
||||
@@ -2,7 +2,21 @@ import * as React from 'react';
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import type { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import styles from './RecruitingSelectionView.module.scss';
|
||||
import { Alert, Button, FormCard, InitiatorLine, FormModeProvider, CollapseSection, FormField, AttachmentList, PrintButton } from '../../shared/ui';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
FormCard,
|
||||
InitiatorLine,
|
||||
FormModeProvider,
|
||||
CollapseSection,
|
||||
FormField,
|
||||
AttachmentList,
|
||||
PrintButton,
|
||||
FieldChangesProvider,
|
||||
FieldChangesBanner
|
||||
} from '../../shared/ui';
|
||||
import { IFieldChange, mergeFieldChanges, toChangeMap } from '../../shared/fieldChanges/fieldChanges';
|
||||
import { SELECTION_CHANGE_TRACKING } from './changeTracking';
|
||||
import {
|
||||
ApproverService,
|
||||
DecisionPanel,
|
||||
@@ -79,6 +93,11 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
// Все поля заявки правит на своём АКТИВНОМ шаге согласующий из
|
||||
// shared/approval/fullEditRights.ts (сейчас — Алтынай Бултурукова).
|
||||
const [fullEditable, setFullEditable] = useState<boolean>(false);
|
||||
// То, что сейчас лежит в заявке на сервере (до несохранённых правок), — с ним
|
||||
// сравниваем при сохранении, чтобы записать в журнал только реальные правки.
|
||||
const [savedData, setSavedData] = useState<IFormData>(EMPTY_DATA);
|
||||
// Журнал правок согласующего с полным правом — подсветка полей и «Было: …».
|
||||
const [approverChanges, setApproverChanges] = useState<IFieldChange[]>([]);
|
||||
const [saveState, setSaveState] = useState<SaveState>('idle');
|
||||
const [saveErrorMessage, setSaveErrorMessage] = useState<string>('');
|
||||
|
||||
@@ -141,7 +160,11 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
|
||||
const saveAllFields = async (): Promise<void> => {
|
||||
if (!fullEditable) return;
|
||||
await updateAllSelectionFields(itemId, data, listId);
|
||||
const editor = { name: context.pageContext.user.displayName, email: effectiveUserEmail(context) };
|
||||
const nextChanges = mergeFieldChanges(approverChanges, savedData, data, SELECTION_CHANGE_TRACKING, editor, new Date());
|
||||
await updateAllSelectionFields(itemId, data, nextChanges, listId);
|
||||
setApproverChanges(nextChanges);
|
||||
setSavedData(data);
|
||||
};
|
||||
|
||||
const handleSaveFields = async (): Promise<void> => {
|
||||
@@ -184,6 +207,8 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
.then(([loaded, files]) => {
|
||||
if (alive) {
|
||||
setData(loaded.data);
|
||||
setSavedData(loaded.data);
|
||||
setApproverChanges(loaded.approverChanges);
|
||||
setStatus(loaded.status);
|
||||
setCode(loaded.code);
|
||||
notifyCode(loaded.code);
|
||||
@@ -256,14 +281,17 @@ const RecruitingSelectionView: React.FC<IRecruitingSelectionViewProps> = ({ cont
|
||||
}
|
||||
headerRight={status ? <StatusBadge status={status} /> : undefined}
|
||||
>
|
||||
<FieldChangesBanner changes={approverChanges} />
|
||||
{/* Вложения ниже — вне этого блока: полное право открывает только поля. */}
|
||||
<FormModeProvider readOnly={!fullEditable}>
|
||||
<ApplicationSection {...sectionProps} />
|
||||
<DepartmentSection {...sectionProps} />
|
||||
<VacancySection {...sectionProps} />
|
||||
<HealthTripsSection {...sectionProps} />
|
||||
<ConditionsSection {...sectionProps} />
|
||||
</FormModeProvider>
|
||||
<FieldChangesProvider changes={toChangeMap(approverChanges)}>
|
||||
<FormModeProvider readOnly={!fullEditable}>
|
||||
<ApplicationSection {...sectionProps} />
|
||||
<DepartmentSection {...sectionProps} />
|
||||
<VacancySection {...sectionProps} />
|
||||
<HealthTripsSection {...sectionProps} />
|
||||
<ConditionsSection {...sectionProps} />
|
||||
</FormModeProvider>
|
||||
</FieldChangesProvider>
|
||||
|
||||
<div className="rp-no-print">
|
||||
<CollapseSection title="Вложения" icon="upload">
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { IChangeTrackingConfig } from '../../shared/fieldChanges/fieldChanges';
|
||||
import { IFormData } from './model';
|
||||
|
||||
/** Колонка журнала правок согласующего (Note, JSON) — см. provisioning.json. */
|
||||
export const APPROVER_CHANGES_FIELD = 'SelectionApproverChangesJson';
|
||||
|
||||
/**
|
||||
* Что пишем в журнал правок согласующего (shared/fieldChanges). Id/код
|
||||
* проекта и Id подразделения своего поля не имеют — меняются вместе с
|
||||
* «Объект/Проект» и «Подразделение».
|
||||
*/
|
||||
export const SELECTION_CHANGE_TRACKING: IChangeTrackingConfig<IFormData> = {
|
||||
ignore: ['ObjectProjectId', 'ObjectProjectCode', 'DepartmentId']
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { ICompany } from '../../shared/dictionary';
|
||||
import { dataListTitle } from '../../shared/listTitles';
|
||||
import { IFieldChange, parseFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
||||
import { FORM_ID, IFormData, EMPTY_DATA, ILanguageLevel } from './model';
|
||||
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
||||
|
||||
/** Безопасный разбор JSON-массива из Note-колонки (пустой при ошибке/пустом). */
|
||||
function parseJsonArray<T>(raw: unknown): T[] {
|
||||
@@ -32,6 +34,8 @@ export interface ILoadedRequest {
|
||||
status: string;
|
||||
/** Уникальный код заявки; '' — если запись создана до появления поля. */
|
||||
code: string;
|
||||
/** Журнал правок согласующего (подсветка полей) — см. shared/fieldChanges. */
|
||||
approverChanges: IFieldChange[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,6 +67,7 @@ export async function loadSelectionRequest(itemId: number, listId?: string): Pro
|
||||
'LanguagesJson',
|
||||
'ProgramsJson',
|
||||
'WorkplaceProvisionJson',
|
||||
APPROVER_CHANGES_FIELD,
|
||||
'RequestStatus',
|
||||
'RequestUniqueCode',
|
||||
...personSelects
|
||||
@@ -102,6 +107,7 @@ export async function loadSelectionRequest(itemId: number, listId?: string): Pro
|
||||
return {
|
||||
data,
|
||||
status: (item.RequestStatus as string) || '',
|
||||
code: (item.RequestUniqueCode as string) || ''
|
||||
code: (item.RequestUniqueCode as string) || '',
|
||||
approverChanges: parseFieldChanges(item[APPROVER_CHANGES_FIELD])
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
{ "internalName": "VacancyOpenReason", "title": "Причина открытия вакансии", "type": "Choice", "choices": ["Замена уволенного сотрудника", "Перевод сотрудника", "Новая штатная единица", "Декрет", "Другое"] },
|
||||
{ "internalName": "VacancyOpenReasonDetails", "title": "Причина открытия — ФИО / уточнение", "type": "Text" },
|
||||
{ "internalName": "CompaniesJson", "title": "Компании (JSON)", "type": "Note" },
|
||||
{ "internalName": "SelectionApproverChangesJson", "title": "Изменения согласующего — подбор (JSON)", "type": "Note" },
|
||||
{ "internalName": "ObjectProject", "title": "Объект/Проект", "type": "Text" },
|
||||
{ "internalName": "ObjectProjectId", "title": "Объект/Проект — Id", "type": "Text" },
|
||||
{ "internalName": "ObjectProjectCode", "title": "Объект/Проект — код", "type": "Text" },
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ export const ApplicationSection: React.FC<ISectionProps> = ({ data, set, context
|
||||
|
||||
return (
|
||||
<CollapseSection title="1. Заявка" icon="clipboard">
|
||||
<FormField label="1.1 Ответственный за подачу заявки" required full>
|
||||
<FormField label="1.1 Ответственный за подачу заявки" fieldKey="Responsible" required full>
|
||||
<PeopleField
|
||||
context={context}
|
||||
value={data.Responsible}
|
||||
|
||||
@@ -61,7 +61,7 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
||||
onChange={(v) => set("RateAfterProbation", v)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="5.4 Сдельная оплата труда">
|
||||
<FormField label="5.4 Сдельная оплата труда" fieldKey="PieceworkPay">
|
||||
<Toggle
|
||||
checked={data.PieceworkPay}
|
||||
onChange={(v) => set("PieceworkPay", v)}
|
||||
@@ -110,14 +110,14 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
<FormField label="5.7 Материальная ответственность (строительные площадки, склады)">
|
||||
<FormField label="5.7 Материальная ответственность (строительные площадки, склады)" fieldKey="MaterialResponsibility">
|
||||
<Toggle
|
||||
checked={data.MaterialResponsibility}
|
||||
onChange={(v) => set("MaterialResponsibility", v)}
|
||||
onLabel="Да"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="5.8 Обеспечение рабочего места" required full>
|
||||
<FormField label="5.8 Обеспечение рабочего места" fieldKey="WorkplaceProvision" required full>
|
||||
<MultiChoiceField
|
||||
value={data.WorkplaceProvision}
|
||||
onChange={(v) => set("WorkplaceProvision", v)}
|
||||
@@ -126,14 +126,14 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
||||
placeholder="Выберите категорию"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="5.9 Охватывается реферальной программой">
|
||||
<FormField label="5.9 Охватывается реферальной программой" fieldKey="ReferralProgram">
|
||||
<Toggle
|
||||
checked={data.ReferralProgram}
|
||||
onChange={(v) => set("ReferralProgram", v)}
|
||||
onLabel="Да"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="5.10 Предполагаемый наставник">
|
||||
<FormField label="5.10 Предполагаемый наставник" fieldKey="ProposedMentor">
|
||||
<PeopleField
|
||||
context={context}
|
||||
value={data.ProposedMentor}
|
||||
@@ -141,7 +141,7 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
||||
placeholder={PERSON_PLACEHOLDER}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="5.11 Непосредственный руководитель" required>
|
||||
<FormField label="5.11 Непосредственный руководитель" fieldKey="DirectManager" required>
|
||||
<PeopleField
|
||||
context={context}
|
||||
value={data.DirectManager}
|
||||
@@ -149,7 +149,7 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
||||
placeholder={PERSON_PLACEHOLDER}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="5.12 Кто принимает решение о приеме (собеседования)" required full>
|
||||
<FormField label="5.12 Кто принимает решение о приеме (собеседования)" fieldKey="HiringDecisionMaker" required full>
|
||||
<PeopleFieldMulti
|
||||
context={context}
|
||||
value={data.HiringDecisionMaker}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { ISectionProps, BUSINESS_TRIP_OPTIONS, OTHER_VALUE } from '../model';
|
||||
|
||||
export const HealthTripsSection: React.FC<ISectionProps> = ({ data, set }) => (
|
||||
<CollapseSection title="4. Командировки" icon="send">
|
||||
<FormField label="4.1 Командировки">
|
||||
<FormField label="4.1 Командировки" fieldKey="BusinessTrips">
|
||||
<Toggle checked={data.BusinessTrips} onChange={(v) => set('BusinessTrips', v)} onLabel="Да" />
|
||||
</FormField>
|
||||
{data.BusinessTrips && (
|
||||
|
||||
@@ -60,7 +60,7 @@ export const VacancySection: React.FC<ISectionProps> = ({ data, set, context })
|
||||
placeholder="— выберите —"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="3.8 Необходимые знания в программах" full>
|
||||
<FormField label="3.8 Необходимые знания в программах" fieldKey="Programs" full>
|
||||
<MultiChoiceField
|
||||
value={data.Programs}
|
||||
onChange={(v) => set('Programs', v)}
|
||||
@@ -69,7 +69,7 @@ export const VacancySection: React.FC<ISectionProps> = ({ data, set, context })
|
||||
placeholder="Выберите программу"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="3.9 Иностранные языки" full>
|
||||
<FormField label="3.9 Иностранные языки" fieldKey="Languages" full>
|
||||
<LanguagesField value={data.Languages} onChange={(v) => set('Languages', v)} />
|
||||
</FormField>
|
||||
<FormField label="3.10 Проживание" htmlFor="Residence" required>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { getSP } from '../../shared/pnp/pnpjs';
|
||||
import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval';
|
||||
import { ISaveItemResult, resolveDataList, saveRequestItem } from '../../shared/dataList';
|
||||
import { IFieldChange, serializeFieldChanges } from '../../shared/fieldChanges/fieldChanges';
|
||||
import { FORM_ID, IFormData } from './model';
|
||||
import { APPROVER_CHANGES_FIELD } from './changeTracking';
|
||||
|
||||
// Поля-люди (один человек): логин → ensureUser → Id в колонку <Field>Id.
|
||||
const PERSON_FIELDS: (keyof IFormData)[] = ['Responsible', 'ProposedMentor', 'DirectManager'];
|
||||
@@ -87,12 +89,19 @@ export type ISaveResult = ISaveItemResult;
|
||||
|
||||
/**
|
||||
* Перезаписать ВСЕ поля заявки — для согласующего с полным правом правки
|
||||
* (см. shared/approval/fullEditRights.ts). Статус, маршрут и вложения не
|
||||
* (см. shared/approval/fullEditRights.ts) — вместе с журналом его правок
|
||||
* (подсветка полей, см. shared/fieldChanges). Статус, маршрут и вложения не
|
||||
* трогаются: заявка остаётся на том же шаге согласования.
|
||||
*/
|
||||
export async function updateAllSelectionFields(id: number, data: IFormData, listId?: string): Promise<void> {
|
||||
export async function updateAllSelectionFields(
|
||||
id: number,
|
||||
data: IFormData,
|
||||
approverChanges: IFieldChange[],
|
||||
listId?: string
|
||||
): Promise<void> {
|
||||
const item = buildScalarBody(data);
|
||||
await applyPeople(item, data);
|
||||
item[APPROVER_CHANGES_FIELD] = serializeFieldChanges(approverChanges);
|
||||
await resolveDataList(FORM_ID, listId).items.getById(id).update(item);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user