From 45fc06a6ac6da69bf9fbbdf2b5bde4797f963bb1 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, 29 Oct 2026 17:29:54 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=20?= =?UTF-8?q?=C2=AB=D0=9C=D0=B5=D0=B1=D0=B5=D0=BB=D1=8C=C2=BB=20=D0=B2=20?= =?UTF-8?q?=D0=BE=D1=80=D0=B3=D0=B0=D0=BD=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D1=80=D0=B0=D0=B1=D0=BE=D1=87=D0=B5=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BC=D0=B5=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Мультивыбор (стол, кресло, тумбочка, канцелярия, другое) с полем своего значения при «Другое». Необязательный; руководитель на своём шаге правит его наравне с ПО и доступами. Хранится в WorkplaceFurnitureJson/WorkplaceFurnitureOther и уходит в маршрут (fieldValues), чтобы шаг исполнения «по мебели» мог назначаться условием в админке. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/permissions.md | 2 +- .../forms/WorkplaceSetup/README.md | 11 +++-- .../WorkplaceSetup/WorkplaceSetupForm.tsx | 2 + .../WorkplaceSetup/WorkplaceSetupView.tsx | 8 ++-- .../forms/WorkplaceSetup/load.ts | 23 +++++++--- .../forms/WorkplaceSetup/model.ts | 26 ++++++++++- .../forms/WorkplaceSetup/provisioning.json | 2 + .../forms/WorkplaceSetup/routeFields.ts | 20 ++++++--- .../sections/FurnitureSection.tsx | 45 +++++++++++++++++++ .../forms/WorkplaceSetup/submit.ts | 37 ++++++++++++--- .../forms/WorkplaceSetup/useApproverRights.ts | 4 +- 11 files changed, 152 insertions(+), 28 deletions(-) create mode 100644 src/webparts/requestPortal/forms/WorkplaceSetup/sections/FurnitureSection.tsx diff --git a/docs/permissions.md b/docs/permissions.md index 24ef29f..3251b40 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -26,7 +26,7 @@ | Править **все поля и вложения** заявки (добавлять и удалять файлы) на своём активном шаге согласования | Алтынай Бултурукова | email в коде (список по каждой форме) | Подбор, Приём (Реферальная рекомендация — нет) | `shared/approval/fullEditRights.ts` → `canEditAllFields` | | То же: **все поля и вложения**, но **не только на своём шаге**: достаточно быть в маршруте (согласование или исполнение), пока заявка «На согласовании» или «Выполнение» | Ербол Шаймерденов | email в коде (список по каждой форме) + участие в маршруте | **Только** Организация рабочего места | `shared/approval/fullEditRights.ts` → `canEditAllFields` + `forms/WorkplaceSetup/useApproverRights.ts` | | Заменять человека на шаге согласования (не удалять шаг) **до отправки**: новая заявка, черновик, доработка | Любой инициатор | право по статусу заявки (форма редактируема) | **Только** Организация рабочего места | `forms/WorkplaceSetup/WorkplaceSetupForm.tsx` (`canReplaceRow`) + `useMatrixEditor.replace` | -| Заполнять «Необходимое ПО» и «Доступы» на своём шаге согласования | Первый шаг маршрута согласования (по маршруту — непосредственный руководитель) | порядок шага в маршруте | Организация рабочего места | `forms/WorkplaceSetup/useApproverRights.ts` (`canEditReviewFields`) | +| Заполнять «Необходимое ПО», «Доступы» и «Мебель» на своём шаге согласования | Первый шаг маршрута согласования (по маршруту — непосредственный руководитель) | порядок шага в маршруте | Организация рабочего места | `forms/WorkplaceSetup/useApproverRights.ts` (`canEditReviewFields`) | | Править оклад на ИС, оклад после ИС, «Отдел/Участок» на своём шаге согласования | Начальник расчётной группы (сейчас Ким Анна) | должность шага: «начальник/руководитель» + «расчет» | Заявка на приём | `forms/RecruitingHire/approverRights.ts` → `canEditPayrollFields` | | Ставить дату приёма (2.2) | Активный исполнитель | роль в маршруте (шаг «Исполнение»), без проверки должности | Заявка на приём | `forms/RecruitingHire/RecruitingHireView.tsx` (`hireDateEditable`) | | Править карту СКУД и сумму за обучение | Начальник отдела кадрового делопроизводства | должность шага: «начальник/руководитель» + «кадров» + «делопроизводств» | Обходной лист | `forms/Clearance/approverRights.ts` → `canEditHrRecordsFields` | diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/README.md b/src/webparts/requestPortal/forms/WorkplaceSetup/README.md index 385ed6f..1b1a2db 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/README.md +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/README.md @@ -23,16 +23,21 @@ - **Полное право правки** (`shared/approval/fullEditRights.ts`, сейчас Ербол Шаймерденов): все поля и вложения, если он есть в маршруте и заявка «На согласовании»/«Выполнение» (`useApproverRights.ts`). -- **Поля «Необходимое ПО» и «Доступы» заполняет согласующий-руководитель** на своём - этапе (инициатор их обычно не знает). Доступны для правки, когда очередь дошла до +- **Поля «Необходимое ПО», «Доступы» и «Мебель» заполняет согласующий-руководитель** + на своём этапе (инициатор их обычно не знает). Доступны для правки, когда очередь дошла до **первого** шага согласования и это текущий пользователь; сохраняются кнопкой «Сохранить» и автоматически при «Согласовать» (`updateReviewFields`, `DecisionPanel.onBeforeDecide`). Остальная форма при этом read-only. +- **Мебель** — мультивыбор (`FURNITURE_OPTIONS`); при «Другое» появляется поле + своего значения. В SP — `WorkplaceFurnitureJson` (JSON-массив) и + `WorkplaceFurnitureOther`. Необязательная. Уходит в маршрут как + `WorkplaceFurnitureJson` (пункты через запятую, `routeFields.ts`) — на условие + шага исполнения «исполнитель по мебели». - **Маска телефона** (PhoneInput). - Печать — общий механизм (без «Вложений»). ## Секции формы -HireRequestPicker (только в редактируемой форме) · EmployeeSection · EquipmentSection +HireRequestPicker (только в редактируемой форме) · EmployeeSection · EquipmentSection · FurnitureSection ## Точки входа - rptype: `it-workplace-setup` diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx index 4d18eff..5139849 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupForm.tsx @@ -63,6 +63,7 @@ import { loadWorkplaceRequest } from './load'; import { hasRequiredFields, REQUIRED_FIELDS_MESSAGE } from './validation'; import { EmployeeSection } from './sections/EmployeeSection'; import { EquipmentSection } from './sections/EquipmentSection'; +import { FurnitureSection } from './sections/FurnitureSection'; import { useStableCallback } from '../../shared/useStableCallback'; const WorkplaceSetupForm: React.FC = ({ @@ -392,6 +393,7 @@ const WorkplaceSetupForm: React.FC = ({ + diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx index 62acab6..f84cce5 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/WorkplaceSetupView.tsx @@ -49,6 +49,7 @@ import { hasRequiredFields, REQUIRED_FIELDS_MESSAGE } from './validation'; import { useApproverRights } from './useApproverRights'; import { EmployeeSection } from './sections/EmployeeSection'; import { EquipmentSection } from './sections/EquipmentSection'; +import { FurnitureSection } from './sections/FurnitureSection'; import { useStableCallback } from '../../shared/useStableCallback'; interface IWorkplaceSetupViewProps { @@ -64,7 +65,7 @@ interface IWorkplaceSetupViewProps { type SaveState = 'idle' | 'saving' | 'saved' | 'error'; -const REVIEW_FIELDS_HINT = 'Как согласующий вы можете заполнить «Необходимое ПО» и «Доступы».'; +const REVIEW_FIELDS_HINT = 'Как согласующий вы можете заполнить «Необходимое ПО», «Доступы» и «Мебель».'; const FULL_EDIT_HINT = 'Как согласующий вы можете редактировать все поля и вложения заявки.'; // Заявка «орг. рабочего места» только для просмотра (режим согласующего). @@ -183,8 +184,8 @@ const WorkplaceSetupView: React.FC = ({ context, itemI await attachments.uploadPending(); }; - const saveReviewFields = (): Promise => - updateReviewFields(itemId, { Software: data.Software, AccessRights: data.AccessRights }, listId); + // updateReviewFields пишет только поля руководителя (ReviewFields), остальное из data игнорирует. + const saveReviewFields = (): Promise => updateReviewFields(itemId, data, listId); // Полное право шире: оно включает и «ПО»/«Доступы». const saveFields = (): Promise => (canEditAll ? saveAllFields() : saveReviewFields()); @@ -283,6 +284,7 @@ const WorkplaceSetupView: React.FC = ({ context, itemI + diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts index f69ca25..40084db 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/load.ts @@ -3,18 +3,25 @@ import { dataListTitle } from '../../shared/listTitles'; import { spDateOnlyToIso } from '../../shared/dateUtils'; import { ICompany } from '../../shared/dictionary'; import { IFieldChange, parseFieldChanges } from '../../shared/fieldChanges/fieldChanges'; -import { COMPANIES_JSON_FIELD, FORM_ID, IFormData, EMPTY_DATA } from './model'; +import { + COMPANIES_JSON_FIELD, + FURNITURE_JSON_FIELD, + FURNITURE_OTHER_FIELD, + FORM_ID, + IFormData, + EMPTY_DATA +} from './model'; import { APPROVER_CHANGES_FIELD } from './changeTracking'; /** - * JSON выбранных компаний → массив. Пусто — заявка до мультивыбора (тогда - * организации есть только текстом в Organization) или битый JSON. + * JSON-колонка с массивом → массив. Пусто — колонка не заполнена (заявка создана + * до появления поля: компании до мультивыбора, мебель до 2026-10-29) или битый JSON. */ -function parseCompanies(raw: unknown): ICompany[] { +function parseJsonArray(raw: unknown): T[] { if (typeof raw !== 'string' || raw.trim() === '') return []; try { const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? (parsed as ICompany[]) : []; + return Array.isArray(parsed) ? (parsed as T[]) : []; } catch { return []; } @@ -50,6 +57,8 @@ export async function loadWorkplaceRequest(itemId: number, listId?: string): Pro ...SCALAR_FIELDS, 'NeedsEquipment', COMPANIES_JSON_FIELD, + FURNITURE_JSON_FIELD, + FURNITURE_OTHER_FIELD, APPROVER_CHANGES_FIELD, 'RequestStatus', 'RequestUniqueCode' @@ -71,7 +80,9 @@ export async function loadWorkplaceRequest(itemId: number, listId?: string): Pro }); data.NeedsEquipment = item.NeedsEquipment === true; - data.Companies = parseCompanies(item[COMPANIES_JSON_FIELD]); + data.Companies = parseJsonArray(item[COMPANIES_JSON_FIELD]); + data.Furniture = parseJsonArray(item[FURNITURE_JSON_FIELD]); + data.FurnitureOther = (item[FURNITURE_OTHER_FIELD] as string) || ''; return { data, diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/model.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/model.ts index 644bfd7..a311b1d 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/model.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/model.ts @@ -1,5 +1,6 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; import { RequestTypeId } from '../../models/types'; +import { ISelectOption } from '@tnsu/ui-kit'; import { ICompany } from '../../shared/dictionary'; /** @@ -14,6 +15,22 @@ export const FORM_ID: RequestTypeId = 'it-workplace-setup'; */ export const COMPANIES_JSON_FIELD = 'WorkplaceCompaniesJson'; +/** Note-колонка с IFormData.Furniture в JSON. Префикс Workplace — по той же причине, что у компаний. */ +export const FURNITURE_JSON_FIELD = 'WorkplaceFurnitureJson'; +/** Text-колонка с IFormData.FurnitureOther. */ +export const FURNITURE_OTHER_FIELD = 'WorkplaceFurnitureOther'; + +/** Значение-триггер «Другое» в «Мебели»: при нём показываем поле ручного ввода. */ +export const FURNITURE_OTHER_VALUE = 'Другое'; + +export const FURNITURE_OPTIONS: ISelectOption[] = [ + { value: 'Стол', label: 'Стол' }, + { value: 'Кресло', label: 'Кресло' }, + { value: 'Тумбочка', label: 'Тумбочка' }, + { value: 'Канцелярские принадлежности', label: 'Канцелярские принадлежности' }, + { value: FURNITURE_OTHER_VALUE, label: FURNITURE_OTHER_VALUE } +]; + /** * Данные формы «Организация рабочего места». Ключи = internalName колонок SP-списка * «Организация рабочего места — данные». @@ -57,6 +74,11 @@ export interface IFormData { NeedsEquipment: boolean; Software: string; AccessRights: string; + // Мебель (необязательно; заполняется вручную) + /** Выбранные пункты из FURNITURE_OPTIONS. В SP — JSON в колонке WorkplaceFurnitureJson. */ + Furniture: string[]; + /** Своё значение — только если выбрано «Другое», иначе ''. В SP — колонка WorkplaceFurnitureOther. */ + FurnitureOther: string; } /** @@ -96,5 +118,7 @@ export const EMPTY_DATA: IFormData = { Organization: '', NeedsEquipment: false, Software: '', - AccessRights: '' + AccessRights: '', + Furniture: [], + FurnitureOther: '' }; diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/provisioning.json b/src/webparts/requestPortal/forms/WorkplaceSetup/provisioning.json index 9d58b9c..44619ad 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/provisioning.json +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/provisioning.json @@ -21,6 +21,8 @@ { "internalName": "NeedsEquipment", "title": "Нужна ли техника", "type": "Boolean" }, { "internalName": "Software", "title": "Необходимое ПО", "type": "Note" }, { "internalName": "AccessRights", "title": "Доступы", "type": "Note" }, + { "internalName": "WorkplaceFurnitureJson", "title": "Мебель (JSON)", "type": "Note" }, + { "internalName": "WorkplaceFurnitureOther", "title": "Мебель (другое)", "type": "Text" }, { "internalName": "WorkplaceApproverChangesJson", "title": "Изменения согласующего — рабочее место (JSON)", "type": "Note" }, { "internalName": "SourceHire", "title": "Источник (заявка на прием)", "type": "Lookup", "lookupList": "Прием на работу — данные", "lookupShowField": "Title" } diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/routeFields.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/routeFields.ts index 913db03..855df52 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/routeFields.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/routeFields.ts @@ -1,5 +1,5 @@ import { IResolveFieldValue } from '../../shared/approval'; -import { COMPANIES_JSON_FIELD, IFormData } from './model'; +import { COMPANIES_JSON_FIELD, FURNITURE_JSON_FIELD, IFormData } from './model'; /** * Значения полей заявки для условных шагов маршрута (fieldValues при resolve). @@ -11,12 +11,18 @@ import { COMPANIES_JSON_FIELD, IFormData } from './model'; * строку (массив → 400), а «содержит» ищет подстроку. Тот же формат, что в * заявках на приём и подбор (см. RecruitingHireForm). * - * Маршрут планируется при подаче, поэтому условие по Software сработает, - * только если «Необходимое ПО» указал сам инициатор (руководитель заполняет - * его позже, на своём шаге). + * Мебель — «WorkplaceFurnitureJson содержит Стол» и т.п. (исполнитель по мебели): + * пункты одной строкой через запятую — «содержит» ищет подстроку, разделитель + * ему безразличен, а в диагностике админки так читается легче. Сортируем, чтобы + * порядок кликов не менял строку и не перепланировал матрицу впустую. + * + * Маршрут планируется при подаче, поэтому условия по Software и мебели при + * подаче сработают, только если их указал сам инициатор. Если их потом + * заполнит руководитель, недостающих исполнителей добавляет просмотр заявки + * при сохранении правок. */ export function routeFieldValues( - data: Pick, + data: Pick, software: string ): IResolveFieldValue[] { const fieldValues: IResolveFieldValue[] = []; @@ -29,5 +35,9 @@ export function routeFieldValues( if (softwareText) { fieldValues.push({ fieldName: 'Software', value: softwareText }); } + const furnitureText = data.Furniture.slice().sort().join(', '); + if (furnitureText) { + fieldValues.push({ fieldName: FURNITURE_JSON_FIELD, value: furnitureText }); + } return fieldValues; } diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/sections/FurnitureSection.tsx b/src/webparts/requestPortal/forms/WorkplaceSetup/sections/FurnitureSection.tsx new file mode 100644 index 0000000..07a0bdc --- /dev/null +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/sections/FurnitureSection.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { CollapseSection, FormField, FormModeProvider, MultiChoiceField, TextInput } from '../../../shared/ui'; +import { FURNITURE_OPTIONS, FURNITURE_OTHER_VALUE, ISectionProps } from '../model'; + +interface IFurnitureSectionProps extends ISectionProps { + /** Согласующий-руководитель на своём шаге может отметить мебель, даже когда + * остальная форма read-only (как ПО и доступы в EquipmentSection). */ + approverEditable?: boolean; +} + +export const FurnitureSection: React.FC = ({ data, set, approverEditable }) => { + const hasOther = data.Furniture.indexOf(FURNITURE_OTHER_VALUE) !== -1; + + const handleFurnitureChange = (values: string[]): void => { + set('Furniture', values); + // Сняли «Другое» — своё значение больше не относится к заявке, не храним его. + if (values.indexOf(FURNITURE_OTHER_VALUE) === -1) set('FurnitureOther', ''); + }; + + const fields = ( + <> + + + + {hasOther && ( + + set('FurnitureOther', v)} /> + + )} + + ); + + return ( + + {/* MultiChoiceField берёт режим только из контекста (своего readOnly у него нет), + поэтому право руководителя даём вложенным провайдером, перекрывающим внешний. */} + {approverEditable ? {fields} : fields} + + ); +}; diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts index 0222afa..7b699f3 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/submit.ts @@ -1,7 +1,14 @@ import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval'; import { ISaveItemResult, resolveDataList, saveRequestItem } from '../../shared/dataList'; import { IFieldChange, serializeFieldChanges } from '../../shared/fieldChanges/fieldChanges'; -import { COMPANIES_JSON_FIELD, FORM_ID, IFormData, organizationText } from './model'; +import { + COMPANIES_JSON_FIELD, + FURNITURE_JSON_FIELD, + FURNITURE_OTHER_FIELD, + FORM_ID, + IFormData, + organizationText +} from './model'; import { APPROVER_CHANGES_FIELD } from './changeTracking'; /** @@ -13,22 +20,30 @@ export function workplaceApprovalService(listId?: string): ApprovalListService { return approvalServiceFor(FORM_ID, listId); } +/** Поля, которые заполняет согласующий-руководитель на своём шаге (см. updateReviewFields). */ +export type ReviewFields = Pick; + +/** Колонки SP «Мебели»: массив — JSON-ом, пустые значения — null. */ +function furnitureFields(data: Pick): Record { + return { + [FURNITURE_JSON_FIELD]: data.Furniture.length > 0 ? JSON.stringify(data.Furniture) : null, + [FURNITURE_OTHER_FIELD]: data.FurnitureOther === '' ? null : data.FurnitureOther + }; +} + /** * Обновить поля, которые заполняет согласующий-руководитель на этапе согласования - * (инициатор часто не знает нужное ПО и доступы). Пишет только эти два поля. + * (инициатор часто не знает нужное ПО, доступы и мебель). Пишет только эти поля. * * listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadWorkplaceRequest): элемент уже * существует, поэтому читаем/пишем напрямую по url его списка, если он задан. */ -export async function updateReviewFields( - id: number, - fields: { Software: string; AccessRights: string }, - listId?: string -): Promise { +export async function updateReviewFields(id: number, fields: ReviewFields, listId?: string): Promise { const list = resolveDataList(FORM_ID, listId); await list.items.getById(id).update({ Software: fields.Software === '' ? null : fields.Software, AccessRights: fields.AccessRights === '' ? null : fields.AccessRights, + ...furnitureFields(fields) }); } @@ -63,6 +78,14 @@ function buildContentFields(data: IFormData): Record { item[COMPANIES_JSON_FIELD] = data.Companies.length > 0 ? JSON.stringify(data.Companies) : null; return; } + if (key === 'Furniture') { + Object.assign(item, furnitureFields(data)); + return; + } + if (key === 'FurnitureOther') { + // Пишется вместе с Furniture (см. выше). + return; + } if (key === 'Organization') { // Текстовая копия выбранных компаний — для печати и списка в SP (см. organizationText). const text = organizationText(data); diff --git a/src/webparts/requestPortal/forms/WorkplaceSetup/useApproverRights.ts b/src/webparts/requestPortal/forms/WorkplaceSetup/useApproverRights.ts index ab6d8db..ed5dde5 100644 --- a/src/webparts/requestPortal/forms/WorkplaceSetup/useApproverRights.ts +++ b/src/webparts/requestPortal/forms/WorkplaceSetup/useApproverRights.ts @@ -11,7 +11,7 @@ import { effectiveUserEmail } from '../../shared/impersonation'; import { FORM_ID } from './model'; export interface IApproverRights { - /** Первый шаг согласования (руководитель) может заполнить «Необходимое ПО» и «Доступы». */ + /** Первый шаг согласования (руководитель) может заполнить «Необходимое ПО», «Доступы» и «Мебель». */ canEditReviewFields: boolean; /** Участник маршрута из shared/approval/fullEditRights.ts правит все поля и вложения. */ canEditAll: boolean; @@ -28,7 +28,7 @@ const FULL_EDIT_STATUSES: ReadonlyArray = [REQUEST_STATUS.onApproval, RE /** * Что текущий пользователь может править в поданной заявке. - * - «ПО»/«Доступы» — только на СВОЁМ активном шаге согласования, если это первый шаг. + * - «ПО»/«Доступы»/«Мебель» — только на СВОЁМ активном шаге согласования, если это первый шаг. * - Все поля — если пользователь есть в маршруте (согласование или исполнение) * НЕЗАВИСИМО от того, чья сейчас очередь, пока заявка в работе (решение * Тимура 2026-10-07: так в «Организации рабочего места» работает Ербол).