diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx index d0d8f9d..d1d6bc0 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireForm.tsx @@ -71,6 +71,7 @@ import { CandidateSection } from './sections/CandidateSection'; import { PpeSection } from './sections/PpeSection'; import { ConditionsSection } from './sections/ConditionsSection'; import { AgencySection } from './sections/AgencySection'; +import { NoteSection } from './sections/NoteSection'; import { useStableCallback } from '../../shared/useStableCallback'; const RecruitingHireForm: React.FC = ({ @@ -411,6 +412,7 @@ const RecruitingHireForm: React.FC = ({ + diff --git a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx index 1fe3623..fc7852e 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx +++ b/src/webparts/requestPortal/forms/RecruitingHire/RecruitingHireView.tsx @@ -59,6 +59,8 @@ import { CandidateSection } from './sections/CandidateSection'; import { PpeSection } from './sections/PpeSection'; import { ConditionsSection } from './sections/ConditionsSection'; import { AgencySection } from './sections/AgencySection'; +import { NoteSection } from './sections/NoteSection'; +import { ApproverNoteBanner } from '../../shared/approverNote'; import { useStableCallback } from '../../shared/useStableCallback'; interface IRecruitingHireViewProps { @@ -374,6 +376,9 @@ const RecruitingHireView: React.FC = ({ context, itemI } headerRight={status ? : undefined} > + {/* Сохранённое значение, а не редактируемое: плашка — то, что написал + инициатор, и не должна меняться по ходу набора правок согласующим. */} + @@ -382,6 +387,7 @@ const RecruitingHireView: React.FC = ({ context, itemI + diff --git a/src/webparts/requestPortal/forms/RecruitingHire/load.ts b/src/webparts/requestPortal/forms/RecruitingHire/load.ts index 6928ad4..9d757ca 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/load.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/load.ts @@ -25,7 +25,7 @@ const SCALAR_FIELDS: (keyof IFormData)[] = [ 'HireCandidateName', 'HireHireDate', 'Position', 'Department', 'SiteLocation', 'PpeHeight', 'PpeClothingSize', 'PpeShoeSize', 'ProbationPeriod', 'ProbationPeriodOther', 'SalaryProbation', 'SalaryAfterProbation', 'WorkSchedule', 'WorkScheduleOther', - 'AgencyName', 'AgencyTerms', + 'AgencyName', 'AgencyTerms', 'HireApproverNote', 'SelectionRequestId', 'SelectionRequestListId', 'SelectionRequestCode' ]; @@ -62,6 +62,7 @@ export async function loadHireRequest(itemId: number, listId?: string): Promise< 'ProjectCode', 'RecruitingAgency', 'HireOnArrival', + 'HireApproverNoteImportant', APPROVER_CHANGES_FIELD, 'RequestStatus', 'RequestUniqueCode' @@ -78,6 +79,7 @@ export async function loadHireRequest(itemId: number, listId?: string): Promise< // Флаг агентства — булево поле (Yes/No), не строка. (data as unknown as Record).RecruitingAgency = !!item.RecruitingAgency; (data as unknown as Record).HireOnArrival = !!item.HireOnArrival; + data.HireApproverNoteImportant = !!item.HireApproverNoteImportant; 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 0ebaba9..ad6c2e3 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/model.ts +++ b/src/webparts/requestPortal/forms/RecruitingHire/model.ts @@ -79,6 +79,10 @@ export interface IFormData { RecruitingAgency: boolean; AgencyName: string; AgencyTerms: string; + // Примечание для согласующих (shared/approverNote): текст + «Важное». + // Префикс Hire — чтобы internalName не совпал с одноимёнными колонками других форм. + HireApproverNote: string; + HireApproverNoteImportant: boolean; } /** Обновление одного поля формы с сохранением типа значения. */ @@ -119,7 +123,9 @@ export const EMPTY_DATA: IFormData = { WorkScheduleOther: '', RecruitingAgency: false, AgencyName: '', - AgencyTerms: '' + AgencyTerms: '', + HireApproverNote: '', + HireApproverNoteImportant: false }; /** Подпись флажка HireOnArrival рядом с датой приёма. */ diff --git a/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json b/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json index c73ca7f..502f527 100644 --- a/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json +++ b/src/webparts/requestPortal/forms/RecruitingHire/provisioning.json @@ -44,6 +44,9 @@ { "internalName": "RecruitingAgency", "title": "Рекрутинговое агентство", "type": "Boolean" }, { "internalName": "AgencyName", "title": "Название компании (агентство)", "type": "Text" }, - { "internalName": "AgencyTerms", "title": "Условия (агентство)", "type": "Note" } + { "internalName": "AgencyTerms", "title": "Условия (агентство)", "type": "Note" }, + + { "internalName": "HireApproverNote", "title": "Примечание для согласующих", "type": "Note" }, + { "internalName": "HireApproverNoteImportant", "title": "Примечание — важное", "type": "Boolean" } ] } diff --git a/src/webparts/requestPortal/forms/RecruitingHire/sections/NoteSection.tsx b/src/webparts/requestPortal/forms/RecruitingHire/sections/NoteSection.tsx new file mode 100644 index 0000000..65797f6 --- /dev/null +++ b/src/webparts/requestPortal/forms/RecruitingHire/sections/NoteSection.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { ApproverNoteSection, IApproverNoteFieldKeys } from '../../../shared/approverNote'; +import { ISectionProps } from '../model'; + +/** Колонки примечания в списке данных приёма (см. provisioning.json). */ +const NOTE_FIELD_KEYS: IApproverNoteFieldKeys = { + text: 'HireApproverNote', + important: 'HireApproverNoteImportant' +}; + +/** 6. Примечание для согласующих — общий раздел shared/approverNote на полях приёма. */ +export const NoteSection: React.FC = ({ data, set }) => ( + { + set('HireApproverNote', note.text); + set('HireApproverNoteImportant', note.important); + }} + fieldKeys={NOTE_FIELD_KEYS} + /> +); diff --git a/src/webparts/requestPortal/shared/approverNote/ApproverNote.module.scss b/src/webparts/requestPortal/shared/approverNote/ApproverNote.module.scss new file mode 100644 index 0000000..d0f4abb --- /dev/null +++ b/src/webparts/requestPortal/shared/approverNote/ApproverNote.module.scss @@ -0,0 +1,73 @@ +@import '../../../../styles/design-tokens'; + +// Толщина цветной полосы слева у плашки: у важного примечания толще, чтобы +// отличалось не только цветом (дальтонизм, ч/б печать). +$banner-stripe: 3px; +$banner-stripe-important: 6px; + +.banner { + display: flex; + align-items: flex-start; + gap: $sp-2; + width: 100%; + box-sizing: border-box; + margin-bottom: $sp-3; + padding: $sp-2 $sp-3; + border: 1px solid $gray-200; + border-left: $banner-stripe solid $navy-600; + border-radius: $r-sm; + background: $navy-50; + font-family: $font-sans; + color: $gray-900; +} + +.bannerImportant { + border-color: $gold-300; + border-left-width: $banner-stripe-important; + border-left-color: $gold-600; + background: $gold-100; + + .bannerIcon { + color: $gold-600; + } +} + +.bannerIcon { + flex-shrink: 0; + margin-top: 2px; + color: $navy-600; +} + +.bannerBody { + min-width: 0; + flex: 1; +} + +.bannerTitle { + margin-bottom: 2px; + font-size: $fs-sm; + font-weight: 600; +} + +.importantTag { + text-transform: uppercase; + color: $amber-700; +} + +.noteText { + font-size: $fs-sm; + line-height: 1.5; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.link { + color: $navy-600; + text-decoration: underline; +} + +.hint { + margin-top: $sp-1; + font-size: $fs-caption; + color: $gray-500; +} diff --git a/src/webparts/requestPortal/shared/approverNote/ApproverNoteBanner.tsx b/src/webparts/requestPortal/shared/approverNote/ApproverNoteBanner.tsx new file mode 100644 index 0000000..56b1d87 --- /dev/null +++ b/src/webparts/requestPortal/shared/approverNote/ApproverNoteBanner.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import styles from './ApproverNote.module.scss'; +import { Icon } from '../ui'; +import { APPROVER_NOTE_TEXT } from './approverNoteTexts'; +import { NoteText } from './NoteText'; + +interface Props { + text: string; + important: boolean; +} + +/** + * Плашка «Примечание инициатора» над разделами заявки у согласующих и + * исполнителей — чтобы важное сообщение было видно без пролистывания формы. + * Пустое примечание — плашки нет. Важное выделено цветом и иконкой: если все + * примечания яркие, на них быстро перестают обращать внимание. + */ +export const ApproverNoteBanner: React.FC = ({ text, important }) => { + if (!text.trim()) return null; + + return ( +
+ +
+
+ {important && {APPROVER_NOTE_TEXT.bannerImportantPrefix} · } + {APPROVER_NOTE_TEXT.bannerTitle} +
+ +
+
+ ); +}; diff --git a/src/webparts/requestPortal/shared/approverNote/ApproverNoteSection.tsx b/src/webparts/requestPortal/shared/approverNote/ApproverNoteSection.tsx new file mode 100644 index 0000000..650c6f0 --- /dev/null +++ b/src/webparts/requestPortal/shared/approverNote/ApproverNoteSection.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import styles from './ApproverNote.module.scss'; +import { CollapseSection, FormField, TextArea, Toggle, useFormReadOnly } from '../ui'; +import { APPROVER_NOTE_TEXT } from './approverNoteTexts'; + +/** Примечание: текст + признак «Важное». */ +export interface IApproverNote { + text: string; + important: boolean; +} + +/** Ключи полей формы (= internalName колонок) — для подсветки правок и прокрутки к полю. */ +export interface IApproverNoteFieldKeys { + text: string; + important: string; +} + +interface Props { + /** Заголовок раздела с номером по порядку формы, напр. «6. Примечание для согласующих». */ + title: string; + value: IApproverNote; + onChange: (value: IApproverNote) => void; + fieldKeys: IApproverNoteFieldKeys; +} + +/** + * Раздел «Примечание для согласующих» — общий для форм. Сама форма решает, в + * какие колонки писать (fieldKeys) и где показывать плашку ApproverNoteBanner. + * В режиме чтения (FormModeProvider readOnly) поля только показываются. + */ +export const ApproverNoteSection: React.FC = ({ title, value, onChange, fieldKeys }) => { + const readOnly = useFormReadOnly(); + + return ( + + +