From 545db1aee6e5317089dcda6c1b2776fde06a7946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B8=D0=BC=D1=83=D1=80=20=D0=93=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D1=86=D0=BA=D0=B8=D0=B9?= Date: Thu, 8 Oct 2026 15:02:11 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=87?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BE?= =?UTF-8?q?=D0=B3=D0=BB=D0=B0=D1=81=D1=83=D1=8E=D1=89=D0=B8=D1=85=20=D0=B2?= =?UTF-8?q?=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B5=20=D0=BD=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D1=91=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Инициатору нужно передать согласующим важную информацию так, чтобы её не пропустили. Новый раздел «6. Примечание для согласующих» (текст + «Важное»); у согласующих и исполнителей заполненное примечание показывается плашкой над разделами заявки, важное выделено цветом и иконкой. Сделано общим модулем shared/approverNote, чтобы подключать в другие формы. Колонки HireApproverNote (Note) и HireApproverNoteImportant (Boolean). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../RecruitingHire/RecruitingHireForm.tsx | 2 + .../RecruitingHire/RecruitingHireView.tsx | 6 ++ .../forms/RecruitingHire/load.ts | 4 +- .../forms/RecruitingHire/model.ts | 8 +- .../forms/RecruitingHire/provisioning.json | 5 +- .../RecruitingHire/sections/NoteSection.tsx | 22 ++++++ .../approverNote/ApproverNote.module.scss | 73 +++++++++++++++++++ .../approverNote/ApproverNoteBanner.tsx | 33 +++++++++ .../approverNote/ApproverNoteSection.tsx | 54 ++++++++++++++ .../shared/approverNote/NoteText.tsx | 22 ++++++ .../shared/approverNote/approverNoteTexts.ts | 10 +++ .../shared/approverNote/index.ts | 5 ++ .../shared/approverNote/linkify.test.ts | 32 ++++++++ .../shared/approverNote/linkify.ts | 34 +++++++++ 14 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 src/webparts/requestPortal/forms/RecruitingHire/sections/NoteSection.tsx create mode 100644 src/webparts/requestPortal/shared/approverNote/ApproverNote.module.scss create mode 100644 src/webparts/requestPortal/shared/approverNote/ApproverNoteBanner.tsx create mode 100644 src/webparts/requestPortal/shared/approverNote/ApproverNoteSection.tsx create mode 100644 src/webparts/requestPortal/shared/approverNote/NoteText.tsx create mode 100644 src/webparts/requestPortal/shared/approverNote/approverNoteTexts.ts create mode 100644 src/webparts/requestPortal/shared/approverNote/index.ts create mode 100644 src/webparts/requestPortal/shared/approverNote/linkify.test.ts create mode 100644 src/webparts/requestPortal/shared/approverNote/linkify.ts 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 ( + + +