diff --git a/src/webparts/requestPortal/components/RequestGrid/RequestGrid.tsx b/src/webparts/requestPortal/components/RequestGrid/RequestGrid.tsx index bdac0e2..d188d78 100644 --- a/src/webparts/requestPortal/components/RequestGrid/RequestGrid.tsx +++ b/src/webparts/requestPortal/components/RequestGrid/RequestGrid.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import styles from './RequestGrid.module.scss'; import { Badge, Icon } from '@tnsu/ui-kit'; import { IRequestType } from '../../models/types'; +import { useIsTester } from '../../shared/useIsTester'; interface IRequestGridProps { requests: IRequestType[]; @@ -9,40 +10,47 @@ interface IRequestGridProps { } export const RequestGrid: React.FC = ({ requests, onSelect }) => { + // Группа «Тестирование» видит формы «в разработке» открытыми — чтобы проверять + // их до запуска для всех (см. shared/useIsTester). + const isTester = useIsTester() === true; + if (requests.length === 0) { return

В этой категории пока нет заявок.

; } return (
- {requests.map((request) => ( - - ))} + + {request.title} + {request.comingSoon ? ( + + В разработке + + ) : ( + request.description && {request.description} + )} + + {!isLocked && ( + + + + )} + + ); + })}
); }; diff --git a/src/webparts/requestPortal/features/submit/SubmitFeature.tsx b/src/webparts/requestPortal/features/submit/SubmitFeature.tsx index 525401f..4c84c3c 100644 --- a/src/webparts/requestPortal/features/submit/SubmitFeature.tsx +++ b/src/webparts/requestPortal/features/submit/SubmitFeature.tsx @@ -9,6 +9,7 @@ import { getFormComponent } from '../../forms/registry'; import { IRequestType } from '../../models/types'; import { emitRequestsChanged } from '../../shared/requestEvents'; import { confirmDiscardFormChanges, markFormClean } from '../../shared/unsavedChanges'; +import { useIsTester } from '../../shared/useIsTester'; export interface ISubmitFeatureProps { context: WebPartContext; @@ -57,6 +58,7 @@ const SubmitFeature: React.FC = ({ }, [activeCategoryId]); const requests = getRequestsByCategory(activeCategoryId); + const isTester = useIsTester(); // Возврат к плиткам закрывает форму и теряет введённое (раздел остаётся // смонтированным, но состояние формы живёт в самом FormComponent). Спрашиваем, @@ -73,8 +75,14 @@ const SubmitFeature: React.FC = ({ const renderSelected = (request: IRequestType): React.ReactNode => { const FormComponent = getFormComponent(request.id); - // Форма ещё не реализована — показываем заглушку. - if (!FormComponent) { + // Формы «в разработке» открыты только группе «Тестирование» — плитка у + // остальных задизейблена, но форму можно открыть и прямой ссылкой (rptype). + if (request.comingSoon && isTester === undefined) { + return
Загрузка формы…
; + } + + // Форма ещё не реализована или пока закрыта для этого пользователя — заглушка. + if (!FormComponent || (request.comingSoon && !isTester)) { return (
- {payrollEditable && ( + {hasEditableFields && (
- - Как согласующий вы можете указать оклады (п. 4.2, 4.3) и отдел/участок (п. 2.5). - + {hireDateEditable ? HIRE_DATE_HINT : PAYROLL_HINT} @@ -341,9 +375,9 @@ const RecruitingHireView: React.FC = ({ context, itemI service={approverService} requestId={itemId} search={(q) => approval.searchUsers(q)} - // Оклады сохраняются вместе с решением — иначе согласующий, забывший - // нажать «Сохранить», согласовал бы заявку со старыми значениями. - onBeforeDecide={payrollEditable ? saveApproverFields : undefined} + // Редактируемые поля сохраняются вместе с решением — иначе участник, + // забывший нажать «Сохранить», решил бы заявку со старыми значениями. + onBeforeDecide={hasEditableFields ? handleBeforeDecide : undefined} onDecided={(newStatus) => { setStatus(newStatus); setDecisionSeq((n) => n + 1); diff --git a/src/webparts/requestPortal/forms/RecruitingHire/load.ts b/src/webparts/requestPortal/forms/RecruitingHire/load.ts index b3f9296..de438c7 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/load.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/load.ts @@ -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 item = (await list .items.getById(itemId) - .select(...SCALAR_FIELDS, 'CompaniesJson', 'CompanySharesJson', 'RecruitingAgency', 'RequestStatus', 'RequestUniqueCode')()) as Record; + .select( + ...SCALAR_FIELDS, + 'CompaniesJson', + 'CompanySharesJson', + 'RecruitingAgency', + 'HireOnArrival', + 'RequestStatus', + 'RequestUniqueCode' + )()) as Record; const data: IFormData = { ...EMPTY_DATA }; const writable = data as unknown as Record; @@ -56,6 +64,7 @@ export async function loadHireRequest(itemId: number, listId?: string): Promise< (data as unknown as Record).CompanyShares = parseJsonArray(item.CompanySharesJson); // Флаг агентства — булево поле (Yes/No), не строка. (data as unknown as Record).RecruitingAgency = !!item.RecruitingAgency; + (data as unknown as Record).HireOnArrival = !!item.HireOnArrival; SCALAR_FIELDS.forEach((key) => { const value = item[key]; diff --git a/src/webparts/requestPortal/forms/RecruitingHire/model.ts b/src/webparts/requestPortal/forms/RecruitingHire/model.ts index f3c099e..06028aa 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/model.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/model.ts @@ -51,6 +51,12 @@ export interface IFormData { // Кандидат HireCandidateName: string; HireHireDate: string; + /** + * «По приезду на объект»: точная дата приёма при подаче неизвестна — тогда + * HireHireDate можно не заполнять, её проставляет исполнитель на своём шаге + * (см. RecruitingHireView, hireDateEditable). + */ + HireOnArrival: boolean; Position: string; /** Отдел/Участок — свободный текст, чисто информационное, на маршрут согласования не влияет. */ Department: string; @@ -97,6 +103,7 @@ export const EMPTY_DATA: IFormData = { SelectionRequestCode: '', HireCandidateName: '', HireHireDate: '', + HireOnArrival: false, Position: '', Department: '', SiteLocation: '', @@ -114,6 +121,9 @@ export const EMPTY_DATA: IFormData = { AgencyTerms: '' }; +/** Подпись флажка HireOnArrival рядом с датой приёма. */ +export const HIRE_ON_ARRIVAL_LABEL = 'По приезду на объект'; + /** Значение «Тип приёма», при котором показывается поле «Взамен кого». */ export const HIRE_TYPE_REPLACEMENT = 'Вместо уволенного'; diff --git a/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json b/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json index 5f8c27a..22e1beb 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json +++ b/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json @@ -22,6 +22,7 @@ { "internalName": "HireCandidateName", "title": "Ф.И.О. кандидата", "type": "Text", "required": true }, { "internalName": "HireHireDate", "title": "Дата приёма", "type": "DateTime", "dateOnly": true }, + { "internalName": "HireOnArrival", "title": "Дата приёма — по приезду на объект", "type": "Boolean" }, { "internalName": "Position", "title": "Должность", "type": "Text", "required": true }, { "internalName": "SiteName", "title": "Отдел / участок (устарело)", "type": "Text" }, { "internalName": "Department", "title": "Подразделение (справочник)", "type": "Text" }, diff --git a/src/webparts/requestPortal/forms/RecruitingHire/sections/CandidateSection.tsx b/src/webparts/requestPortal/forms/RecruitingHire/sections/CandidateSection.tsx index 6a4d52b..0d374f6 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/sections/CandidateSection.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/sections/CandidateSection.tsx @@ -2,10 +2,13 @@ import * as React from "react"; import { CollapseSection, FormField, TextInput } from "../../../shared/ui"; import { PositionPicker } from "../../../shared/dictionary"; import { ISectionProps } from "../model"; +import { HireDateField } from "./HireDateField"; interface ICandidateSectionProps extends ISectionProps { /** Начальник расчётной группы правит отдел/участок даже в режиме просмотра (см. approverRights.ts). */ payrollEditable?: boolean; + /** Активный исполнитель проставляет дату приёма в режиме просмотра (см. RecruitingHireView). */ + hireDateEditable?: boolean; } export const CandidateSection: React.FC = ({ @@ -13,6 +16,7 @@ export const CandidateSection: React.FC = ({ set, context, payrollEditable, + hireDateEditable, }) => ( = ({ onChange={(v) => set("HireCandidateName", v)} /> - - set("HireHireDate", v)} - /> - + = ({ 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 ( + +
+ set('HireHireDate', v)} + readOnly={dateReadOnly} + /> + +
+
+ ); +}; diff --git a/src/webparts/requestPortal/forms/RecruitingHire/sections/SelectionRequestField.tsx b/src/webparts/requestPortal/forms/RecruitingHire/sections/SelectionRequestField.tsx index d4642e3..c52dc07 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/sections/SelectionRequestField.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/sections/SelectionRequestField.tsx @@ -100,6 +100,8 @@ export const SelectionRequestField: React.FC = ({ d set('Department', selection.Department); set('WorkSchedule', selection.WorkSchedule); set('WorkScheduleOther', selection.WorkScheduleOther); + set('ProbationPeriod', selection.SelectionProbationPeriod); + set('ProbationPeriodOther', selection.SelectionProbationPeriodOther); set('SalaryProbation', selection.RateProbation); set('SalaryAfterProbation', selection.RateAfterProbation); }) diff --git a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts index 46b63e3..819d238 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/submit.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/submit.ts @@ -47,16 +47,21 @@ function buildItem(data: IFormData, status: string): Record { export type ISaveResult = ISaveItemResult; -/** Поля, которые начальник расчётной группы правит на своём шаге (см. approverRights.ts). */ +/** + * Поля, которые участники маршрута правят на своём шаге: оклады и отдел — + * начальник расчётной группы (см. approverRights.ts), дата приёма — активный + * исполнитель (см. RecruitingHireView, hireDateEditable). + */ export interface IApproverEditableFields { SalaryProbation: string; SalaryAfterProbation: string; Department: string; + HireHireDate: string; } /** - * Обновить только поля, что заполняет согласующий расчётной группы на своём - * шаге. Пишутся ровно переданные поля — как в Clearance.updateApproverFields, + * Обновить только поля, что заполняет участник маршрута на своём шаге. + * Пишутся ровно переданные поля — как в Clearance.updateApproverFields, * чтобы не затирать остальную заявку значениями из уже устаревшего снимка формы. */ export async function updateApproverFields( @@ -74,6 +79,9 @@ export async function updateApproverFields( if (fields.Department !== undefined) { 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; const list = resolveDataList(FORM_ID, listId); await list.items.getById(id).update(item); diff --git a/src/webparts/requestPortal/forms/RecruitingHire/validation.ts b/src/webparts/requestPortal/forms/RecruitingHire/validation.ts index 83802b7..177c801 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/validation.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/validation.ts @@ -9,12 +9,14 @@ import { IFormData } from './model'; * идёт через рекрутинговое агентство (см. AgencySection), без внутреннего * подбора. Toggle-поле 5.1 «Рекрутинговое агентство» в проверку не входит — у * булевых нет пустого состояния. + * 2.2 «Дата приёма» не нужна, если отмечено «По приезду на объект» + * (HireOnArrival) — тогда дату проставляет исполнитель на своём шаге. */ const REQUIRED_FIELDS: ReadonlyArray<{ label: string; ok: (d: IFormData) => boolean }> = [ { label: '1.2 организация', ok: (d) => d.Companies.length > 0 }, { label: '1.4 телефон', ok: (d) => !!d.Phone.trim() }, { 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.2 оклад при испытательном сроке', ok: (d) => !!d.SalaryProbation.trim() }, { label: '4.3 оклад по завершении ИС', ok: (d) => !!d.SalaryAfterProbation.trim() }, diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/model.ts b/src/webparts/requestPortal/forms/RecruitingSelection/model.ts index 9848019..fd89157 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/model.ts +++ b/src/webparts/requestPortal/forms/RecruitingSelection/model.ts @@ -62,6 +62,13 @@ export interface IFormData { BusinessTripsDetails: string; BusinessTripsDetailsOther: string; // Условия работы + /** + * Префикс Selection — обход коллизии internalName: в «Заявке на прием» уже + * есть колонка ProbationPeriod, а одинаковые имена в разных списках сайта + * ломают тип/title колонки (см. историю с WorkSchedule). + */ + SelectionProbationPeriod: string; + SelectionProbationPeriodOther: string; RateProbation: string; RateAfterProbation: string; PieceworkPay: boolean; @@ -122,6 +129,8 @@ export const EMPTY_DATA: IFormData = { BusinessTrips: false, BusinessTripsDetails: '', BusinessTripsDetailsOther: '', + SelectionProbationPeriod: '', + SelectionProbationPeriodOther: '', RateProbation: '', RateAfterProbation: '', PieceworkPay: false, @@ -245,6 +254,18 @@ export const WORK_EXPERIENCE_OPTIONS: ISelectOption[] = [ { 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. */ export const WORK_SCHEDULE_OPTIONS: ISelectOption[] = [ { value: '5/2', label: '5/2' }, diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/provisioning.json b/src/webparts/requestPortal/forms/RecruitingSelection/provisioning.json index cb06b71..2d13a6e 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/provisioning.json +++ b/src/webparts/requestPortal/forms/RecruitingSelection/provisioning.json @@ -35,6 +35,8 @@ { "internalName": "BusinessTripsDetails", "title": "Командировки — продолжительность / частота", "type": "Choice", "choices": ["Редко (1–2 раза в год)", "Ежеквартально", "Ежемесячно", "Еженедельно", "Постоянные разъезды", "Вахтовый метод", "Другое"] }, { "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": "RateAfterProbation", "title": "Ставка после ИС", "type": "Number", "decimals": 2 }, { "internalName": "Awards", "title": "Премии", "type": "Note" }, diff --git a/src/webparts/requestPortal/forms/RecruitingSelection/sections/ConditionsSection.tsx b/src/webparts/requestPortal/forms/RecruitingSelection/sections/ConditionsSection.tsx index 9e3f713..43cc835 100644 --- a/src/webparts/requestPortal/forms/RecruitingSelection/sections/ConditionsSection.tsx +++ b/src/webparts/requestPortal/forms/RecruitingSelection/sections/ConditionsSection.tsx @@ -14,6 +14,7 @@ import { ISectionProps, PERSON_PLACEHOLDER, OTHER_VALUE, + PROBATION_PERIOD_OPTIONS, WORK_SCHEDULE_OPTIONS, WORK_HOURS_OPTIONS, WORKPLACE_PROVISION_OPTIONS, @@ -25,28 +26,49 @@ export const ConditionsSection: React.FC = ({ context, }) => ( - + + = ({ /> )} - +