feat: испытательный срок в подборе, дата приёма по приезду, HR-формы для тестировщиков
Tests / frontend-test (push) Successful in 1m19s
Tests / frontend-test (push) Successful in 1m19s
This commit is contained in:
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|||||||
import styles from './RequestGrid.module.scss';
|
import styles from './RequestGrid.module.scss';
|
||||||
import { Badge, Icon } from '@tnsu/ui-kit';
|
import { Badge, Icon } from '@tnsu/ui-kit';
|
||||||
import { IRequestType } from '../../models/types';
|
import { IRequestType } from '../../models/types';
|
||||||
|
import { useIsTester } from '../../shared/useIsTester';
|
||||||
|
|
||||||
interface IRequestGridProps {
|
interface IRequestGridProps {
|
||||||
requests: IRequestType[];
|
requests: IRequestType[];
|
||||||
@@ -9,40 +10,47 @@ interface IRequestGridProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const RequestGrid: React.FC<IRequestGridProps> = ({ requests, onSelect }) => {
|
export const RequestGrid: React.FC<IRequestGridProps> = ({ requests, onSelect }) => {
|
||||||
|
// Группа «Тестирование» видит формы «в разработке» открытыми — чтобы проверять
|
||||||
|
// их до запуска для всех (см. shared/useIsTester).
|
||||||
|
const isTester = useIsTester() === true;
|
||||||
|
|
||||||
if (requests.length === 0) {
|
if (requests.length === 0) {
|
||||||
return <p className={styles.empty}>В этой категории пока нет заявок.</p>;
|
return <p className={styles.empty}>В этой категории пока нет заявок.</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.grid}>
|
<div className={styles.grid}>
|
||||||
{requests.map((request) => (
|
{requests.map((request) => {
|
||||||
<button
|
const isLocked = !!request.comingSoon && !isTester;
|
||||||
key={request.id}
|
return (
|
||||||
type="button"
|
<button
|
||||||
className={request.comingSoon ? `${styles.tile} ${styles.tileDisabled}` : styles.tile}
|
key={request.id}
|
||||||
disabled={request.comingSoon}
|
type="button"
|
||||||
onClick={() => onSelect(request)}
|
className={isLocked ? `${styles.tile} ${styles.tileDisabled}` : styles.tile}
|
||||||
>
|
disabled={isLocked}
|
||||||
<span className={styles.tileIcon}>
|
onClick={() => onSelect(request)}
|
||||||
<Icon name={request.icon} />
|
>
|
||||||
</span>
|
<span className={styles.tileIcon}>
|
||||||
<span className={styles.tileBody}>
|
<Icon name={request.icon} />
|
||||||
<span className={styles.tileTitle}>{request.title}</span>
|
|
||||||
{request.comingSoon ? (
|
|
||||||
<Badge variant="muted" size="sm">
|
|
||||||
В разработке
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
request.description && <span className={styles.tileDesc}>{request.description}</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
{!request.comingSoon && (
|
|
||||||
<span className={styles.tileArrow}>
|
|
||||||
<Icon name="chevron-right" size="sm" />
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
<span className={styles.tileBody}>
|
||||||
</button>
|
<span className={styles.tileTitle}>{request.title}</span>
|
||||||
))}
|
{request.comingSoon ? (
|
||||||
|
<Badge variant="muted" size="sm">
|
||||||
|
В разработке
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
request.description && <span className={styles.tileDesc}>{request.description}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{!isLocked && (
|
||||||
|
<span className={styles.tileArrow}>
|
||||||
|
<Icon name="chevron-right" size="sm" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getFormComponent } from '../../forms/registry';
|
|||||||
import { IRequestType } from '../../models/types';
|
import { IRequestType } from '../../models/types';
|
||||||
import { emitRequestsChanged } from '../../shared/requestEvents';
|
import { emitRequestsChanged } from '../../shared/requestEvents';
|
||||||
import { confirmDiscardFormChanges, markFormClean } from '../../shared/unsavedChanges';
|
import { confirmDiscardFormChanges, markFormClean } from '../../shared/unsavedChanges';
|
||||||
|
import { useIsTester } from '../../shared/useIsTester';
|
||||||
|
|
||||||
export interface ISubmitFeatureProps {
|
export interface ISubmitFeatureProps {
|
||||||
context: WebPartContext;
|
context: WebPartContext;
|
||||||
@@ -57,6 +58,7 @@ const SubmitFeature: React.FC<ISubmitFeatureProps> = ({
|
|||||||
}, [activeCategoryId]);
|
}, [activeCategoryId]);
|
||||||
|
|
||||||
const requests = getRequestsByCategory(activeCategoryId);
|
const requests = getRequestsByCategory(activeCategoryId);
|
||||||
|
const isTester = useIsTester();
|
||||||
|
|
||||||
// Возврат к плиткам закрывает форму и теряет введённое (раздел остаётся
|
// Возврат к плиткам закрывает форму и теряет введённое (раздел остаётся
|
||||||
// смонтированным, но состояние формы живёт в самом FormComponent). Спрашиваем,
|
// смонтированным, но состояние формы живёт в самом FormComponent). Спрашиваем,
|
||||||
@@ -73,8 +75,14 @@ const SubmitFeature: React.FC<ISubmitFeatureProps> = ({
|
|||||||
const renderSelected = (request: IRequestType): React.ReactNode => {
|
const renderSelected = (request: IRequestType): React.ReactNode => {
|
||||||
const FormComponent = getFormComponent(request.id);
|
const FormComponent = getFormComponent(request.id);
|
||||||
|
|
||||||
// Форма ещё не реализована — показываем заглушку.
|
// Формы «в разработке» открыты только группе «Тестирование» — плитка у
|
||||||
if (!FormComponent) {
|
// остальных задизейблена, но форму можно открыть и прямой ссылкой (rptype).
|
||||||
|
if (request.comingSoon && isTester === undefined) {
|
||||||
|
return <div className={styles.loading}>Загрузка формы…</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Форма ещё не реализована или пока закрыта для этого пользователя — заглушка.
|
||||||
|
if (!FormComponent || (request.comingSoon && !isTester)) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.formPlaceholder}>
|
<div className={styles.formPlaceholder}>
|
||||||
<button type="button" className={styles.backButton} onClick={handleBack}>
|
<button type="button" className={styles.backButton} onClick={handleBack}>
|
||||||
|
|||||||
@@ -58,10 +58,15 @@ const WORKPLACE_FORM_KEY = 'it-workplace-setup';
|
|||||||
|
|
||||||
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||||
|
|
||||||
|
const HIRE_DATE_REQUIRED_MESSAGE = 'укажите дату приёма (п. 2.2) — без неё заявку нельзя отметить выполненной.';
|
||||||
|
const PAYROLL_HINT = 'Как согласующий вы можете указать оклады (п. 4.2, 4.3) и отдел/участок (п. 2.4).';
|
||||||
|
const HIRE_DATE_HINT = 'Как исполнитель укажите дату приёма (п. 2.2) — она обязательна для отметки «Выполнено».';
|
||||||
|
|
||||||
// Заявка «на прием» только для просмотра (режим согласующего). Переиспользует
|
// Заявка «на прием» только для просмотра (режим согласующего). Переиспользует
|
||||||
// те же секции, что и форма подачи; редактирование выключено через
|
// те же секции, что и форма подачи; редактирование выключено через
|
||||||
// <FormModeProvider readOnly>, КРОМЕ окладов (4.2/4.3) и отдела/участка (2.5) —
|
// <FormModeProvider readOnly>, КРОМЕ окладов (4.2/4.3) и отдела/участка (2.4) —
|
||||||
// их на своём шаге правит начальник расчётной группы (см. approverRights.ts).
|
// их на своём шаге правит начальник расчётной группы (см. approverRights.ts), —
|
||||||
|
// и даты приёма (2.2), которую проставляет активный исполнитель.
|
||||||
const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemId, formKey, listId, onCodeLoaded }) => {
|
||||||
// onCodeLoaded — колбэк родителя (RequestPortal → ApproveFeature): его ссылка
|
// onCodeLoaded — колбэк родителя (RequestPortal → ApproveFeature): его ссылка
|
||||||
// может меняться на каждом рендере, поэтому в зависимости эффекта загрузки
|
// может меняться на каждом рендере, поэтому в зависимости эффекта загрузки
|
||||||
@@ -75,6 +80,9 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [payrollEditable, setPayrollEditable] = useState<boolean>(false);
|
const [payrollEditable, setPayrollEditable] = useState<boolean>(false);
|
||||||
|
// Дату приёма (2.2) проставляет АКТИВНЫЙ исполнитель — по роли в маршруте, а
|
||||||
|
// не по должности: кто сейчас на шаге исполнения, тот и заполняет.
|
||||||
|
const [hireDateEditable, setHireDateEditable] = useState<boolean>(false);
|
||||||
const [saveState, setSaveState] = useState<SaveState>('idle');
|
const [saveState, setSaveState] = useState<SaveState>('idle');
|
||||||
const [saveErrorMessage, setSaveErrorMessage] = useState<string>('');
|
const [saveErrorMessage, setSaveErrorMessage] = useState<string>('');
|
||||||
// Право удалять согласующих из уже поданного маршрута — см.
|
// Право удалять согласующих из уже поданного маршрута — см.
|
||||||
@@ -130,20 +138,25 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
approval.getJobTitle(l), resolveApproverOrgInfo(context)
|
approval.getJobTitle(l), resolveApproverOrgInfo(context)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Права на оклады и отдел/участок — по должности ТЕКУЩЕГО активного шага
|
// Права на поля — по ТЕКУЩЕМУ активному шагу пользователя: оклады и
|
||||||
// пользователя. isDelegator намеренно не даёт прав: делегировавший не решает
|
// отдел/участок — по должности согласующего, дата приёма — любому исполнителю.
|
||||||
// за делегата (см. DecisionPanel), значит и эти поля за него править не должен.
|
// isDelegator намеренно не даёт прав: делегировавший не решает за делегата
|
||||||
|
// (см. DecisionPanel), значит и эти поля за него править не должен.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
approverService
|
approverService
|
||||||
.getPendingRowForCurrentUser(itemId)
|
.getPendingRowForCurrentUser(itemId)
|
||||||
.then((pending) => {
|
.then((pending) => {
|
||||||
if (!alive) return;
|
if (!alive) return;
|
||||||
const active = !!pending && !pending.isDelegator && pending.phase !== APPROVAL_PHASE.execution;
|
const isActive = !!pending && !pending.isDelegator;
|
||||||
setPayrollEditable(active && canEditPayrollFields(pending?.position));
|
const isExecution = pending?.phase === APPROVAL_PHASE.execution;
|
||||||
|
setPayrollEditable(isActive && !isExecution && canEditPayrollFields(pending?.position));
|
||||||
|
setHireDateEditable(isActive && isExecution);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (alive) setPayrollEditable(false);
|
if (!alive) return;
|
||||||
|
setPayrollEditable(false);
|
||||||
|
setHireDateEditable(false);
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
alive = false;
|
alive = false;
|
||||||
@@ -154,14 +167,37 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
// Пишем ТОЛЬКО эти поля: иначе согласующий затёр бы остальную заявку
|
// Пишем ТОЛЬКО эти поля: иначе согласующий затёр бы остальную заявку
|
||||||
// устаревшим снимком формы, загруженным при открытии экрана.
|
// устаревшим снимком формы, загруженным при открытии экрана.
|
||||||
const saveApproverFields = useCallback(async (): Promise<void> => {
|
const saveApproverFields = useCallback(async (): Promise<void> => {
|
||||||
if (!payrollEditable) return;
|
const fields: Partial<IApproverEditableFields> = {};
|
||||||
const fields: Partial<IApproverEditableFields> = {
|
if (payrollEditable) {
|
||||||
SalaryProbation: data.SalaryProbation,
|
fields.SalaryProbation = data.SalaryProbation;
|
||||||
SalaryAfterProbation: data.SalaryAfterProbation,
|
fields.SalaryAfterProbation = data.SalaryAfterProbation;
|
||||||
Department: data.Department
|
fields.Department = data.Department;
|
||||||
};
|
}
|
||||||
|
if (hireDateEditable) {
|
||||||
|
fields.HireHireDate = data.HireHireDate;
|
||||||
|
}
|
||||||
await updateApproverFields(itemId, fields, listId);
|
await updateApproverFields(itemId, fields, listId);
|
||||||
}, [itemId, listId, payrollEditable, data.SalaryProbation, data.SalaryAfterProbation, data.Department]);
|
}, [
|
||||||
|
itemId,
|
||||||
|
listId,
|
||||||
|
payrollEditable,
|
||||||
|
hireDateEditable,
|
||||||
|
data.SalaryProbation,
|
||||||
|
data.SalaryAfterProbation,
|
||||||
|
data.Department,
|
||||||
|
data.HireHireDate
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Перед решением: исполнитель не может отметить «Выполнено» без даты приёма
|
||||||
|
// (при подаче её могли не указать — «По приезду на объект»). Бросок отменяет
|
||||||
|
// решение, текст ошибки показывает DecisionPanel.
|
||||||
|
const handleBeforeDecide = async (): Promise<void> => {
|
||||||
|
if (hireDateEditable && !data.HireHireDate) {
|
||||||
|
throw new Error(HIRE_DATE_REQUIRED_MESSAGE);
|
||||||
|
}
|
||||||
|
await saveApproverFields();
|
||||||
|
};
|
||||||
|
const hasEditableFields = payrollEditable || hireDateEditable;
|
||||||
|
|
||||||
const handleSaveFields = async (): Promise<void> => {
|
const handleSaveFields = async (): Promise<void> => {
|
||||||
setSaveState('saving');
|
setSaveState('saving');
|
||||||
@@ -224,7 +260,7 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sectionProps = { data, set, context, payrollEditable };
|
const sectionProps = { data, set, context, payrollEditable, hireDateEditable };
|
||||||
|
|
||||||
const notify = (): void => {
|
const notify = (): void => {
|
||||||
if (!requestTypeId) return;
|
if (!requestTypeId) return;
|
||||||
@@ -256,11 +292,9 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
{isApprovedStatus(status) && <PrintButton />}
|
{isApprovedStatus(status) && <PrintButton />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{payrollEditable && (
|
{hasEditableFields && (
|
||||||
<div className={`${styles.reviewBar} rp-no-print`}>
|
<div className={`${styles.reviewBar} rp-no-print`}>
|
||||||
<span className={styles.reviewHint}>
|
<span className={styles.reviewHint}>{hireDateEditable ? HIRE_DATE_HINT : PAYROLL_HINT}</span>
|
||||||
Как согласующий вы можете указать оклады (п. 4.2, 4.3) и отдел/участок (п. 2.5).
|
|
||||||
</span>
|
|
||||||
<Button variant="secondary" onClick={handleSaveFields} disabled={saveState === 'saving'}>
|
<Button variant="secondary" onClick={handleSaveFields} disabled={saveState === 'saving'}>
|
||||||
Сохранить
|
Сохранить
|
||||||
</Button>
|
</Button>
|
||||||
@@ -341,9 +375,9 @@ const RecruitingHireView: React.FC<IRecruitingHireViewProps> = ({ context, itemI
|
|||||||
service={approverService}
|
service={approverService}
|
||||||
requestId={itemId}
|
requestId={itemId}
|
||||||
search={(q) => approval.searchUsers(q)}
|
search={(q) => approval.searchUsers(q)}
|
||||||
// Оклады сохраняются вместе с решением — иначе согласующий, забывший
|
// Редактируемые поля сохраняются вместе с решением — иначе участник,
|
||||||
// нажать «Сохранить», согласовал бы заявку со старыми значениями.
|
// забывший нажать «Сохранить», решил бы заявку со старыми значениями.
|
||||||
onBeforeDecide={payrollEditable ? saveApproverFields : undefined}
|
onBeforeDecide={hasEditableFields ? handleBeforeDecide : undefined}
|
||||||
onDecided={(newStatus) => {
|
onDecided={(newStatus) => {
|
||||||
setStatus(newStatus);
|
setStatus(newStatus);
|
||||||
setDecisionSeq((n) => n + 1);
|
setDecisionSeq((n) => n + 1);
|
||||||
|
|||||||
@@ -45,7 +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 list = listId ? sp.web.getList(listId) : sp.web.lists.getByTitle(dataListTitle(FORM_ID));
|
||||||
const item = (await list
|
const item = (await list
|
||||||
.items.getById(itemId)
|
.items.getById(itemId)
|
||||||
.select(...SCALAR_FIELDS, 'CompaniesJson', 'CompanySharesJson', 'RecruitingAgency', 'RequestStatus', 'RequestUniqueCode')()) as Record<string, unknown>;
|
.select(
|
||||||
|
...SCALAR_FIELDS,
|
||||||
|
'CompaniesJson',
|
||||||
|
'CompanySharesJson',
|
||||||
|
'RecruitingAgency',
|
||||||
|
'HireOnArrival',
|
||||||
|
'RequestStatus',
|
||||||
|
'RequestUniqueCode'
|
||||||
|
)()) as Record<string, unknown>;
|
||||||
|
|
||||||
const data: IFormData = { ...EMPTY_DATA };
|
const data: IFormData = { ...EMPTY_DATA };
|
||||||
const writable = data as unknown as Record<string, string>;
|
const writable = data as unknown as Record<string, string>;
|
||||||
@@ -56,6 +64,7 @@ export async function loadHireRequest(itemId: number, listId?: string): Promise<
|
|||||||
(data as unknown as Record<string, unknown>).CompanyShares = parseJsonArray<ICompanyShare>(item.CompanySharesJson);
|
(data as unknown as Record<string, unknown>).CompanyShares = parseJsonArray<ICompanyShare>(item.CompanySharesJson);
|
||||||
// Флаг агентства — булево поле (Yes/No), не строка.
|
// Флаг агентства — булево поле (Yes/No), не строка.
|
||||||
(data as unknown as Record<string, unknown>).RecruitingAgency = !!item.RecruitingAgency;
|
(data as unknown as Record<string, unknown>).RecruitingAgency = !!item.RecruitingAgency;
|
||||||
|
(data as unknown as Record<string, unknown>).HireOnArrival = !!item.HireOnArrival;
|
||||||
|
|
||||||
SCALAR_FIELDS.forEach((key) => {
|
SCALAR_FIELDS.forEach((key) => {
|
||||||
const value = item[key];
|
const value = item[key];
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ export interface IFormData {
|
|||||||
// Кандидат
|
// Кандидат
|
||||||
HireCandidateName: string;
|
HireCandidateName: string;
|
||||||
HireHireDate: string;
|
HireHireDate: string;
|
||||||
|
/**
|
||||||
|
* «По приезду на объект»: точная дата приёма при подаче неизвестна — тогда
|
||||||
|
* HireHireDate можно не заполнять, её проставляет исполнитель на своём шаге
|
||||||
|
* (см. RecruitingHireView, hireDateEditable).
|
||||||
|
*/
|
||||||
|
HireOnArrival: boolean;
|
||||||
Position: string;
|
Position: string;
|
||||||
/** Отдел/Участок — свободный текст, чисто информационное, на маршрут согласования не влияет. */
|
/** Отдел/Участок — свободный текст, чисто информационное, на маршрут согласования не влияет. */
|
||||||
Department: string;
|
Department: string;
|
||||||
@@ -97,6 +103,7 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
SelectionRequestCode: '',
|
SelectionRequestCode: '',
|
||||||
HireCandidateName: '',
|
HireCandidateName: '',
|
||||||
HireHireDate: '',
|
HireHireDate: '',
|
||||||
|
HireOnArrival: false,
|
||||||
Position: '',
|
Position: '',
|
||||||
Department: '',
|
Department: '',
|
||||||
SiteLocation: '',
|
SiteLocation: '',
|
||||||
@@ -114,6 +121,9 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
AgencyTerms: ''
|
AgencyTerms: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Подпись флажка HireOnArrival рядом с датой приёма. */
|
||||||
|
export const HIRE_ON_ARRIVAL_LABEL = 'По приезду на объект';
|
||||||
|
|
||||||
/** Значение «Тип приёма», при котором показывается поле «Взамен кого». */
|
/** Значение «Тип приёма», при котором показывается поле «Взамен кого». */
|
||||||
export const HIRE_TYPE_REPLACEMENT = 'Вместо уволенного';
|
export const HIRE_TYPE_REPLACEMENT = 'Вместо уволенного';
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
{ "internalName": "HireCandidateName", "title": "Ф.И.О. кандидата", "type": "Text", "required": true },
|
{ "internalName": "HireCandidateName", "title": "Ф.И.О. кандидата", "type": "Text", "required": true },
|
||||||
{ "internalName": "HireHireDate", "title": "Дата приёма", "type": "DateTime", "dateOnly": true },
|
{ "internalName": "HireHireDate", "title": "Дата приёма", "type": "DateTime", "dateOnly": true },
|
||||||
|
{ "internalName": "HireOnArrival", "title": "Дата приёма — по приезду на объект", "type": "Boolean" },
|
||||||
{ "internalName": "Position", "title": "Должность", "type": "Text", "required": true },
|
{ "internalName": "Position", "title": "Должность", "type": "Text", "required": true },
|
||||||
{ "internalName": "SiteName", "title": "Отдел / участок (устарело)", "type": "Text" },
|
{ "internalName": "SiteName", "title": "Отдел / участок (устарело)", "type": "Text" },
|
||||||
{ "internalName": "Department", "title": "Подразделение (справочник)", "type": "Text" },
|
{ "internalName": "Department", "title": "Подразделение (справочник)", "type": "Text" },
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import * as React from "react";
|
|||||||
import { CollapseSection, FormField, TextInput } from "../../../shared/ui";
|
import { CollapseSection, FormField, TextInput } from "../../../shared/ui";
|
||||||
import { PositionPicker } from "../../../shared/dictionary";
|
import { PositionPicker } from "../../../shared/dictionary";
|
||||||
import { ISectionProps } from "../model";
|
import { ISectionProps } from "../model";
|
||||||
|
import { HireDateField } from "./HireDateField";
|
||||||
|
|
||||||
interface ICandidateSectionProps extends ISectionProps {
|
interface ICandidateSectionProps extends ISectionProps {
|
||||||
/** Начальник расчётной группы правит отдел/участок даже в режиме просмотра (см. approverRights.ts). */
|
/** Начальник расчётной группы правит отдел/участок даже в режиме просмотра (см. approverRights.ts). */
|
||||||
payrollEditable?: boolean;
|
payrollEditable?: boolean;
|
||||||
|
/** Активный исполнитель проставляет дату приёма в режиме просмотра (см. RecruitingHireView). */
|
||||||
|
hireDateEditable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CandidateSection: React.FC<ICandidateSectionProps> = ({
|
export const CandidateSection: React.FC<ICandidateSectionProps> = ({
|
||||||
@@ -13,6 +16,7 @@ export const CandidateSection: React.FC<ICandidateSectionProps> = ({
|
|||||||
set,
|
set,
|
||||||
context,
|
context,
|
||||||
payrollEditable,
|
payrollEditable,
|
||||||
|
hireDateEditable,
|
||||||
}) => (
|
}) => (
|
||||||
<CollapseSection title="2. Кандидат" icon="people">
|
<CollapseSection title="2. Кандидат" icon="people">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -27,14 +31,7 @@ export const CandidateSection: React.FC<ICandidateSectionProps> = ({
|
|||||||
onChange={(v) => set("HireCandidateName", v)}
|
onChange={(v) => set("HireCandidateName", v)}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="2.2 Дата приёма" htmlFor="HireHireDate" required>
|
<HireDateField data={data} set={set} hireDateEditable={hireDateEditable} />
|
||||||
<TextInput
|
|
||||||
id="HireHireDate"
|
|
||||||
type="date"
|
|
||||||
value={data.HireHireDate}
|
|
||||||
onChange={(v) => set("HireHireDate", v)}
|
|
||||||
/>
|
|
||||||
</FormField>
|
|
||||||
<FormField label="2.3 Должность" htmlFor="Position" required>
|
<FormField label="2.3 Должность" htmlFor="Position" required>
|
||||||
<PositionPicker
|
<PositionPicker
|
||||||
context={context}
|
context={context}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@import '../../../../../styles/design-tokens';
|
||||||
|
|
||||||
|
.stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $sp-2;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import styles from './HireDateField.module.scss';
|
||||||
|
import { Checkbox, FormField, TextInput } from '../../../shared/ui';
|
||||||
|
import { HIRE_ON_ARRIVAL_LABEL, IFormData, SetField } from '../model';
|
||||||
|
|
||||||
|
interface IHireDateFieldProps {
|
||||||
|
data: IFormData;
|
||||||
|
set: SetField;
|
||||||
|
/** Активный исполнитель проставляет дату в режиме просмотра (см. RecruitingHireView). */
|
||||||
|
hireDateEditable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2.2 «Дата приёма» + флажок «По приезду на объект».
|
||||||
|
*
|
||||||
|
* Инициатор: при отмеченном флажке дата не нужна — поле очищается и
|
||||||
|
* блокируется, чтобы не было двусмысленного «и флажок, и дата».
|
||||||
|
* Исполнитель: поле открыто и обязательно независимо от флажка — к его шагу
|
||||||
|
* дата уже известна. Сам флажок у исполнителя остаётся как был (пометка, что
|
||||||
|
* при подаче дата была неизвестна).
|
||||||
|
*/
|
||||||
|
export const HireDateField: React.FC<IHireDateFieldProps> = ({ data, set, hireDateEditable }) => {
|
||||||
|
const handleOnArrivalChange = (checked: boolean): void => {
|
||||||
|
set('HireOnArrival', checked);
|
||||||
|
if (checked) set('HireHireDate', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Явный readOnly перекрывает режим формы (FormModeProvider): исполнителю
|
||||||
|
// открываем поле в просмотре, инициатору блокируем при отмеченном флажке.
|
||||||
|
let dateReadOnly: boolean | undefined;
|
||||||
|
if (hireDateEditable) {
|
||||||
|
dateReadOnly = false;
|
||||||
|
} else if (data.HireOnArrival) {
|
||||||
|
dateReadOnly = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField label="2.2 Дата приёма" htmlFor="HireHireDate" required={hireDateEditable || !data.HireOnArrival}>
|
||||||
|
<div className={styles.stack}>
|
||||||
|
<TextInput
|
||||||
|
id="HireHireDate"
|
||||||
|
type="date"
|
||||||
|
value={data.HireHireDate}
|
||||||
|
onChange={(v) => set('HireHireDate', v)}
|
||||||
|
readOnly={dateReadOnly}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
id="HireOnArrival"
|
||||||
|
checked={data.HireOnArrival}
|
||||||
|
onChange={handleOnArrivalChange}
|
||||||
|
label={HIRE_ON_ARRIVAL_LABEL}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormField>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -100,6 +100,8 @@ export const SelectionRequestField: React.FC<ISelectionRequestFieldProps> = ({ d
|
|||||||
set('Department', selection.Department);
|
set('Department', selection.Department);
|
||||||
set('WorkSchedule', selection.WorkSchedule);
|
set('WorkSchedule', selection.WorkSchedule);
|
||||||
set('WorkScheduleOther', selection.WorkScheduleOther);
|
set('WorkScheduleOther', selection.WorkScheduleOther);
|
||||||
|
set('ProbationPeriod', selection.SelectionProbationPeriod);
|
||||||
|
set('ProbationPeriodOther', selection.SelectionProbationPeriodOther);
|
||||||
set('SalaryProbation', selection.RateProbation);
|
set('SalaryProbation', selection.RateProbation);
|
||||||
set('SalaryAfterProbation', selection.RateAfterProbation);
|
set('SalaryAfterProbation', selection.RateAfterProbation);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -47,16 +47,21 @@ function buildItem(data: IFormData, status: string): Record<string, unknown> {
|
|||||||
|
|
||||||
export type ISaveResult = ISaveItemResult;
|
export type ISaveResult = ISaveItemResult;
|
||||||
|
|
||||||
/** Поля, которые начальник расчётной группы правит на своём шаге (см. approverRights.ts). */
|
/**
|
||||||
|
* Поля, которые участники маршрута правят на своём шаге: оклады и отдел —
|
||||||
|
* начальник расчётной группы (см. approverRights.ts), дата приёма — активный
|
||||||
|
* исполнитель (см. RecruitingHireView, hireDateEditable).
|
||||||
|
*/
|
||||||
export interface IApproverEditableFields {
|
export interface IApproverEditableFields {
|
||||||
SalaryProbation: string;
|
SalaryProbation: string;
|
||||||
SalaryAfterProbation: string;
|
SalaryAfterProbation: string;
|
||||||
Department: string;
|
Department: string;
|
||||||
|
HireHireDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обновить только поля, что заполняет согласующий расчётной группы на своём
|
* Обновить только поля, что заполняет участник маршрута на своём шаге.
|
||||||
* шаге. Пишутся ровно переданные поля — как в Clearance.updateApproverFields,
|
* Пишутся ровно переданные поля — как в Clearance.updateApproverFields,
|
||||||
* чтобы не затирать остальную заявку значениями из уже устаревшего снимка формы.
|
* чтобы не затирать остальную заявку значениями из уже устаревшего снимка формы.
|
||||||
*/
|
*/
|
||||||
export async function updateApproverFields(
|
export async function updateApproverFields(
|
||||||
@@ -74,6 +79,9 @@ export async function updateApproverFields(
|
|||||||
if (fields.Department !== undefined) {
|
if (fields.Department !== undefined) {
|
||||||
item.Department = fields.Department === '' ? null : fields.Department;
|
item.Department = fields.Department === '' ? null : fields.Department;
|
||||||
}
|
}
|
||||||
|
if (fields.HireHireDate !== undefined) {
|
||||||
|
item.HireHireDate = fields.HireHireDate === '' ? null : fields.HireHireDate;
|
||||||
|
}
|
||||||
if (Object.keys(item).length === 0) return;
|
if (Object.keys(item).length === 0) return;
|
||||||
const list = resolveDataList(FORM_ID, listId);
|
const list = resolveDataList(FORM_ID, listId);
|
||||||
await list.items.getById(id).update(item);
|
await list.items.getById(id).update(item);
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import { IFormData } from './model';
|
|||||||
* идёт через рекрутинговое агентство (см. AgencySection), без внутреннего
|
* идёт через рекрутинговое агентство (см. AgencySection), без внутреннего
|
||||||
* подбора. Toggle-поле 5.1 «Рекрутинговое агентство» в проверку не входит — у
|
* подбора. Toggle-поле 5.1 «Рекрутинговое агентство» в проверку не входит — у
|
||||||
* булевых нет пустого состояния.
|
* булевых нет пустого состояния.
|
||||||
|
* 2.2 «Дата приёма» не нужна, если отмечено «По приезду на объект»
|
||||||
|
* (HireOnArrival) — тогда дату проставляет исполнитель на своём шаге.
|
||||||
*/
|
*/
|
||||||
const REQUIRED_FIELDS: ReadonlyArray<{ label: string; ok: (d: IFormData) => boolean }> = [
|
const REQUIRED_FIELDS: ReadonlyArray<{ label: string; ok: (d: IFormData) => boolean }> = [
|
||||||
{ label: '1.2 организация', ok: (d) => d.Companies.length > 0 },
|
{ label: '1.2 организация', ok: (d) => d.Companies.length > 0 },
|
||||||
{ label: '1.4 телефон', ok: (d) => !!d.Phone.trim() },
|
{ label: '1.4 телефон', ok: (d) => !!d.Phone.trim() },
|
||||||
{ label: '1.5 тип приёма', ok: (d) => !!d.HireType },
|
{ label: '1.5 тип приёма', ok: (d) => !!d.HireType },
|
||||||
{ label: '2.2 дата приёма', ok: (d) => !!d.HireHireDate },
|
{ label: '2.2 дата приёма', ok: (d) => !!d.HireHireDate || d.HireOnArrival },
|
||||||
{ label: '4.1 испытательный срок', ok: (d) => !!d.ProbationPeriod },
|
{ label: '4.1 испытательный срок', ok: (d) => !!d.ProbationPeriod },
|
||||||
{ label: '4.2 оклад при испытательном сроке', ok: (d) => !!d.SalaryProbation.trim() },
|
{ label: '4.2 оклад при испытательном сроке', ok: (d) => !!d.SalaryProbation.trim() },
|
||||||
{ label: '4.3 оклад по завершении ИС', ok: (d) => !!d.SalaryAfterProbation.trim() },
|
{ label: '4.3 оклад по завершении ИС', ok: (d) => !!d.SalaryAfterProbation.trim() },
|
||||||
|
|||||||
@@ -62,6 +62,13 @@ export interface IFormData {
|
|||||||
BusinessTripsDetails: string;
|
BusinessTripsDetails: string;
|
||||||
BusinessTripsDetailsOther: string;
|
BusinessTripsDetailsOther: string;
|
||||||
// Условия работы
|
// Условия работы
|
||||||
|
/**
|
||||||
|
* Префикс Selection — обход коллизии internalName: в «Заявке на прием» уже
|
||||||
|
* есть колонка ProbationPeriod, а одинаковые имена в разных списках сайта
|
||||||
|
* ломают тип/title колонки (см. историю с WorkSchedule).
|
||||||
|
*/
|
||||||
|
SelectionProbationPeriod: string;
|
||||||
|
SelectionProbationPeriodOther: string;
|
||||||
RateProbation: string;
|
RateProbation: string;
|
||||||
RateAfterProbation: string;
|
RateAfterProbation: string;
|
||||||
PieceworkPay: boolean;
|
PieceworkPay: boolean;
|
||||||
@@ -122,6 +129,8 @@ export const EMPTY_DATA: IFormData = {
|
|||||||
BusinessTrips: false,
|
BusinessTrips: false,
|
||||||
BusinessTripsDetails: '',
|
BusinessTripsDetails: '',
|
||||||
BusinessTripsDetailsOther: '',
|
BusinessTripsDetailsOther: '',
|
||||||
|
SelectionProbationPeriod: '',
|
||||||
|
SelectionProbationPeriodOther: '',
|
||||||
RateProbation: '',
|
RateProbation: '',
|
||||||
RateAfterProbation: '',
|
RateAfterProbation: '',
|
||||||
PieceworkPay: false,
|
PieceworkPay: false,
|
||||||
@@ -245,6 +254,18 @@ export const WORK_EXPERIENCE_OPTIONS: ISelectOption[] = [
|
|||||||
{ value: 'От 5 лет и выше', label: 'От 5 лет и выше' }
|
{ value: 'От 5 лет и выше', label: 'От 5 лет и выше' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Варианты «Испытательный срок» — совпадают с choices колонки
|
||||||
|
* SelectionProbationPeriod и с тем же полем в «Заявке на прием».
|
||||||
|
*/
|
||||||
|
export const PROBATION_PERIOD_OPTIONS: ISelectOption[] = [
|
||||||
|
{ value: '1 месяц', label: '1 месяц' },
|
||||||
|
{ value: '2 месяца', label: '2 месяца' },
|
||||||
|
{ value: '3 месяца', label: '3 месяца' },
|
||||||
|
{ value: '6 месяцев', label: '6 месяцев' },
|
||||||
|
{ value: OTHER_VALUE, label: OTHER_VALUE }
|
||||||
|
];
|
||||||
|
|
||||||
/** Варианты «Режим работы» — совпадают с choices колонки WorkSchedule. */
|
/** Варианты «Режим работы» — совпадают с choices колонки WorkSchedule. */
|
||||||
export const WORK_SCHEDULE_OPTIONS: ISelectOption[] = [
|
export const WORK_SCHEDULE_OPTIONS: ISelectOption[] = [
|
||||||
{ value: '5/2', label: '5/2' },
|
{ value: '5/2', label: '5/2' },
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
{ "internalName": "BusinessTripsDetails", "title": "Командировки — продолжительность / частота", "type": "Choice", "choices": ["Редко (1–2 раза в год)", "Ежеквартально", "Ежемесячно", "Еженедельно", "Постоянные разъезды", "Вахтовый метод", "Другое"] },
|
{ "internalName": "BusinessTripsDetails", "title": "Командировки — продолжительность / частота", "type": "Choice", "choices": ["Редко (1–2 раза в год)", "Ежеквартально", "Ежемесячно", "Еженедельно", "Постоянные разъезды", "Вахтовый метод", "Другое"] },
|
||||||
{ "internalName": "BusinessTripsDetailsOther", "title": "Командировки — частота (другое)", "type": "Text" },
|
{ "internalName": "BusinessTripsDetailsOther", "title": "Командировки — частота (другое)", "type": "Text" },
|
||||||
|
|
||||||
|
{ "internalName": "SelectionProbationPeriod", "title": "Испытательный срок", "type": "Choice", "choices": ["1 месяц", "2 месяца", "3 месяца", "6 месяцев", "Другое"] },
|
||||||
|
{ "internalName": "SelectionProbationPeriodOther", "title": "Испытательный срок (другое)", "type": "Text" },
|
||||||
{ "internalName": "RateProbation", "title": "Ставка на ИС", "type": "Number", "decimals": 2 },
|
{ "internalName": "RateProbation", "title": "Ставка на ИС", "type": "Number", "decimals": 2 },
|
||||||
{ "internalName": "RateAfterProbation", "title": "Ставка после ИС", "type": "Number", "decimals": 2 },
|
{ "internalName": "RateAfterProbation", "title": "Ставка после ИС", "type": "Number", "decimals": 2 },
|
||||||
{ "internalName": "Awards", "title": "Премии", "type": "Note" },
|
{ "internalName": "Awards", "title": "Премии", "type": "Note" },
|
||||||
|
|||||||
+33
-11
@@ -14,6 +14,7 @@ import {
|
|||||||
ISectionProps,
|
ISectionProps,
|
||||||
PERSON_PLACEHOLDER,
|
PERSON_PLACEHOLDER,
|
||||||
OTHER_VALUE,
|
OTHER_VALUE,
|
||||||
|
PROBATION_PERIOD_OPTIONS,
|
||||||
WORK_SCHEDULE_OPTIONS,
|
WORK_SCHEDULE_OPTIONS,
|
||||||
WORK_HOURS_OPTIONS,
|
WORK_HOURS_OPTIONS,
|
||||||
WORKPLACE_PROVISION_OPTIONS,
|
WORKPLACE_PROVISION_OPTIONS,
|
||||||
@@ -25,28 +26,49 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
|||||||
context,
|
context,
|
||||||
}) => (
|
}) => (
|
||||||
<CollapseSection title="5. Условия работы" icon="chart">
|
<CollapseSection title="5. Условия работы" icon="chart">
|
||||||
<FormField label="5.1 Ставка на ИС" htmlFor="RateProbation" required>
|
<FormField label="5.1 Испытательный срок" htmlFor="SelectionProbationPeriod" required>
|
||||||
|
<Select
|
||||||
|
id="SelectionProbationPeriod"
|
||||||
|
value={data.SelectionProbationPeriod}
|
||||||
|
onChange={(v) => {
|
||||||
|
set("SelectionProbationPeriod", v);
|
||||||
|
if (v !== OTHER_VALUE) set("SelectionProbationPeriodOther", "");
|
||||||
|
}}
|
||||||
|
options={PROBATION_PERIOD_OPTIONS}
|
||||||
|
placeholder="— выберите —"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
{data.SelectionProbationPeriod === OTHER_VALUE && (
|
||||||
|
<FormField label="Испытательный срок (другое)" htmlFor="SelectionProbationPeriodOther">
|
||||||
|
<TextInput
|
||||||
|
id="SelectionProbationPeriodOther"
|
||||||
|
value={data.SelectionProbationPeriodOther}
|
||||||
|
onChange={(v) => set("SelectionProbationPeriodOther", v)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
<FormField label="5.2 Ставка на ИС" htmlFor="RateProbation" required>
|
||||||
<MoneyInput
|
<MoneyInput
|
||||||
id="RateProbation"
|
id="RateProbation"
|
||||||
value={data.RateProbation}
|
value={data.RateProbation}
|
||||||
onChange={(v) => set("RateProbation", v)}
|
onChange={(v) => set("RateProbation", v)}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="5.2 Ставка после ИС" htmlFor="RateAfterProbation" required>
|
<FormField label="5.3 Ставка после ИС" htmlFor="RateAfterProbation" required>
|
||||||
<MoneyInput
|
<MoneyInput
|
||||||
id="RateAfterProbation"
|
id="RateAfterProbation"
|
||||||
value={data.RateAfterProbation}
|
value={data.RateAfterProbation}
|
||||||
onChange={(v) => set("RateAfterProbation", v)}
|
onChange={(v) => set("RateAfterProbation", v)}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="5.3 Сдельная оплата труда">
|
<FormField label="5.4 Сдельная оплата труда">
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={data.PieceworkPay}
|
checked={data.PieceworkPay}
|
||||||
onChange={(v) => set("PieceworkPay", v)}
|
onChange={(v) => set("PieceworkPay", v)}
|
||||||
onLabel="Да"
|
onLabel="Да"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="5.4 График работы" htmlFor="WorkSchedule" required>
|
<FormField label="5.5 График работы" htmlFor="WorkSchedule" required>
|
||||||
<Select
|
<Select
|
||||||
id="WorkSchedule"
|
id="WorkSchedule"
|
||||||
value={data.WorkSchedule}
|
value={data.WorkSchedule}
|
||||||
@@ -67,7 +89,7 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
)}
|
)}
|
||||||
<FormField label="5.5 Время работы" htmlFor="WorkHours" required>
|
<FormField label="5.6 Время работы" htmlFor="WorkHours" required>
|
||||||
<Select
|
<Select
|
||||||
id="WorkHours"
|
id="WorkHours"
|
||||||
value={data.WorkHours}
|
value={data.WorkHours}
|
||||||
@@ -88,14 +110,14 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
)}
|
)}
|
||||||
<FormField label="5.6 Материальная ответственность (строительные площадки, склады)">
|
<FormField label="5.7 Материальная ответственность (строительные площадки, склады)">
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={data.MaterialResponsibility}
|
checked={data.MaterialResponsibility}
|
||||||
onChange={(v) => set("MaterialResponsibility", v)}
|
onChange={(v) => set("MaterialResponsibility", v)}
|
||||||
onLabel="Да"
|
onLabel="Да"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="5.7 Обеспечение рабочего места" required full>
|
<FormField label="5.8 Обеспечение рабочего места" required full>
|
||||||
<MultiChoiceField
|
<MultiChoiceField
|
||||||
value={data.WorkplaceProvision}
|
value={data.WorkplaceProvision}
|
||||||
onChange={(v) => set("WorkplaceProvision", v)}
|
onChange={(v) => set("WorkplaceProvision", v)}
|
||||||
@@ -104,14 +126,14 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
|||||||
placeholder="Выберите категорию"
|
placeholder="Выберите категорию"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="5.8 Охватывается реферальной программой">
|
<FormField label="5.9 Охватывается реферальной программой">
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={data.ReferralProgram}
|
checked={data.ReferralProgram}
|
||||||
onChange={(v) => set("ReferralProgram", v)}
|
onChange={(v) => set("ReferralProgram", v)}
|
||||||
onLabel="Да"
|
onLabel="Да"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="5.9 Предполагаемый наставник">
|
<FormField label="5.10 Предполагаемый наставник">
|
||||||
<PeopleField
|
<PeopleField
|
||||||
context={context}
|
context={context}
|
||||||
value={data.ProposedMentor}
|
value={data.ProposedMentor}
|
||||||
@@ -119,7 +141,7 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
|||||||
placeholder={PERSON_PLACEHOLDER}
|
placeholder={PERSON_PLACEHOLDER}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="5.10 Непосредственный руководитель" required>
|
<FormField label="5.11 Непосредственный руководитель" required>
|
||||||
<PeopleField
|
<PeopleField
|
||||||
context={context}
|
context={context}
|
||||||
value={data.DirectManager}
|
value={data.DirectManager}
|
||||||
@@ -127,7 +149,7 @@ export const ConditionsSection: React.FC<ISectionProps> = ({
|
|||||||
placeholder={PERSON_PLACEHOLDER}
|
placeholder={PERSON_PLACEHOLDER}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="5.11 Кто принимает решение о приеме (собеседования)" required full>
|
<FormField label="5.12 Кто принимает решение о приеме (собеседования)" required full>
|
||||||
<PeopleFieldMulti
|
<PeopleFieldMulti
|
||||||
context={context}
|
context={context}
|
||||||
value={data.HiringDecisionMaker}
|
value={data.HiringDecisionMaker}
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import { IFormData } from './model';
|
|||||||
* ReferralProgram) и условные «другое»-подполя в проверку не входят — у
|
* ReferralProgram) и условные «другое»-подполя в проверку не входят — у
|
||||||
* булевых нет пустого состояния, а «другое» раскрывается только при выборе
|
* булевых нет пустого состояния, а «другое» раскрывается только при выборе
|
||||||
* соответствующего варианта в уже обязательном родительском поле.
|
* соответствующего варианта в уже обязательном родительском поле.
|
||||||
* 5.9 «Предполагаемый наставник» (ProposedMentor) сделан необязательным
|
* 5.10 «Предполагаемый наставник» (ProposedMentor) сделан необязательным
|
||||||
* по просьбе Тимура (2026-09-24) — не всегда есть кого назначить заранее.
|
* по просьбе Тимура (2026-09-24) — не всегда есть кого назначить заранее.
|
||||||
|
* 5.1 «Испытательный срок» добавлен 2026-09-24 — нумерация раздела 5 сдвинута на 1.
|
||||||
*/
|
*/
|
||||||
const REQUIRED_FIELDS: ReadonlyArray<{ label: string; ok: (d: IFormData) => boolean }> = [
|
const REQUIRED_FIELDS: ReadonlyArray<{ label: string; ok: (d: IFormData) => boolean }> = [
|
||||||
{ label: '1.3 дата подачи заявки', ok: (d) => !!d.SubmitDate },
|
{ label: '1.3 дата подачи заявки', ok: (d) => !!d.SubmitDate },
|
||||||
@@ -25,13 +26,14 @@ const REQUIRED_FIELDS: ReadonlyArray<{ label: string; ok: (d: IFormData) => bool
|
|||||||
{ label: '3.5 личностные характеристики', ok: (d) => !!d.PersonalTraits.trim() },
|
{ label: '3.5 личностные характеристики', ok: (d) => !!d.PersonalTraits.trim() },
|
||||||
{ label: '3.6 образование', ok: (d) => !!d.Education },
|
{ label: '3.6 образование', ok: (d) => !!d.Education },
|
||||||
{ label: '3.7 опыт работы', ok: (d) => !!d.WorkExperience },
|
{ label: '3.7 опыт работы', ok: (d) => !!d.WorkExperience },
|
||||||
{ label: '5.1 ставка на ИС', ok: (d) => !!d.RateProbation.trim() },
|
{ label: '5.1 испытательный срок', ok: (d) => !!d.SelectionProbationPeriod },
|
||||||
{ label: '5.2 ставка после ИС', ok: (d) => !!d.RateAfterProbation.trim() },
|
{ label: '5.2 ставка на ИС', ok: (d) => !!d.RateProbation.trim() },
|
||||||
{ label: '5.4 график работы', ok: (d) => !!d.WorkSchedule },
|
{ label: '5.3 ставка после ИС', ok: (d) => !!d.RateAfterProbation.trim() },
|
||||||
{ label: '5.5 время работы', ok: (d) => !!d.WorkHours },
|
{ label: '5.5 график работы', ok: (d) => !!d.WorkSchedule },
|
||||||
{ label: '5.7 обеспечение рабочего места', ok: (d) => d.WorkplaceProvision.length > 0 },
|
{ label: '5.6 время работы', ok: (d) => !!d.WorkHours },
|
||||||
{ label: '5.10 непосредственный руководитель', ok: (d) => !!d.DirectManager },
|
{ label: '5.8 обеспечение рабочего места', ok: (d) => d.WorkplaceProvision.length > 0 },
|
||||||
{ label: '5.11 кто принимает решение о приёме', ok: (d) => d.HiringDecisionMaker.length > 0 }
|
{ label: '5.11 непосредственный руководитель', ok: (d) => !!d.DirectManager },
|
||||||
|
{ label: '5.12 кто принимает решение о приёме', ok: (d) => d.HiringDecisionMaker.length > 0 }
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Подписи незаполненных обязательных-для-отправки полей (пусто — всё заполнено). */
|
/** Подписи незаполненных обязательных-для-отправки полей (пусто — всё заполнено). */
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export {
|
|||||||
TextArea,
|
TextArea,
|
||||||
Select,
|
Select,
|
||||||
Toggle,
|
Toggle,
|
||||||
|
Checkbox,
|
||||||
CollapseSection,
|
CollapseSection,
|
||||||
FormActions,
|
FormActions,
|
||||||
FileDropzone,
|
FileDropzone,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { isCurrentUserTester } from './ui/SubmitActions/testerGroupAccess';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Состоит ли текущий пользователь в SharePoint-группе «Тестирование».
|
||||||
|
* undefined — ответ ещё не пришёл: вызывающий сам решает, что показывать на это
|
||||||
|
* время (безопасный дефолт — считать, что не тестировщик). Сбой запроса = false,
|
||||||
|
* чтобы закрытые формы не открылись по ошибке.
|
||||||
|
*/
|
||||||
|
export function useIsTester(): boolean | undefined {
|
||||||
|
const [isTester, setIsTester] = React.useState<boolean | undefined>(undefined);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let alive = true;
|
||||||
|
isCurrentUserTester()
|
||||||
|
.then((value) => {
|
||||||
|
if (alive) setIsTester(value);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (alive) setIsTester(false);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
alive = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isTester;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user