diff --git a/docs/forms.md b/docs/forms.md index ea4ad27..457a5a2 100644 --- a/docs/forms.md +++ b/docs/forms.md @@ -45,7 +45,7 @@ | [`recruiting-referral`](../src/webparts/requestPortal/forms/Referral/README.md) | Реферальная рекомендация | Рекрутинг | да | да | да | кандидат — внешний (текстовые поля, не People); маршрут по рекомендателю; исполнитель переназначает на реального; условное уточнение «откуда знаете» | ✅ | | [`it-workplace-setup`](../src/webparts/requestPortal/forms/WorkplaceSetup/README.md) | Организация рабочего места | IT | да | да | да | префилл из «прием», поля ПО/доступы правит руководитель | ✅ | | [`it-1c-ticket`](../src/webparts/requestPortal/forms/Ticket1C/README.md) | Заявка в отдел 1С | IT | да | нет | да | контакты предзаполняются из Graph (/me); вместо фазы исполнения — карточка в Microsoft Planner (согласовано/сразу без согласования — рабочий бакет, отклонено — отдельный бакет) | ✅ | -| [`clearance-form`](../src/webparts/requestPortal/forms/Clearance/README.md) | Обходной лист | Отдел кадров | да | нет | да | подаётся HR за увольняемого (People-picker) либо создаётся сам по согласованному «Заявлению на увольнение»; условие «IsManager» резолвится по должности из самой заявки; да/нет-поля (парковка, СКУД); 6 независимых сумм к списанию компактной сеткой (ОС, СИЗ, подотчётная сумма, обучение, НМА, ТМЦ) — вводит любой активный согласующий, блокируется навсегда после первого ввода; заполнил хоть одну — ОДИН общий комментарий «за что» становится обязательным | ✅ | +| [`clearance-form`](../src/webparts/requestPortal/forms/Clearance/README.md) | Обходной лист | Отдел кадров | да | нет | да | подаётся HR за увольняемого (People-picker) либо создаётся сам по согласованному «Заявлению на увольнение»; условие «IsManager» резолвится по должности из самой заявки; да/нет-поля (парковка, СКУД); 6 независимых сумм к списанию компактной сеткой (ОС, СИЗ, подотчётная сумма, обучение, НМА, ТМЦ) — вводит любой активный согласующий, блокируется навсегда после первого ввода; заполнил хоть одну — стандартный комментарий решения (в блоке «Ваше решение») становится обязательным на любое решение, не только возврат/отклонение | ✅ | | [`hr-dismissal`](../src/webparts/requestPortal/forms/Dismissal/README.md) | Заявление на увольнение | Отдел кадров | да | нет | да | совместители — таблица компаний с удалением строки (снимает и согласующего из матрицы); должность/организация из справочника; **после согласования сам создаёт обходной лист**; печать заблокирована до согласования обходного листа; «Лицо, принимающее ОС/НМА» — заполняет только непосредственный руководитель | ✅ | | `hr-transfer` | Перемещение сотрудников | Отдел кадров | — | — | — | — | ⏳ заглушка (плитка есть, форма не сделана) | diff --git a/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx b/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx index f653f45..7511dd9 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx @@ -169,12 +169,19 @@ const ClearanceView: React.FC = ({ context, itemId, formKey [canEditWriteOffs, lockedWriteOffs] ); + // Категории, чью сумму заполняют ПРЯМО СЕЙЧАС (редактируема и не пуста) — от + // них зависит, обязателен ли комментарий решения (DecisionPanel, «Ваше + // решение») и что подставить в его плейсхолдер. «За что конкретно» здесь не + // хранится отдельным полем — вместо этого требуем стандартный комментарий + // решения, который и так сопровождает Согласовать/Вернуть/Отклонить (см. + // commentRequired/commentPlaceholder у DecisionPanel ниже). + const writeOffTouchedCategories = WRITE_OFF_CATEGORIES.filter( + (cat) => editableWriteOffKeys.has(cat.key) && data[cat.amountField].trim() !== '' + ); + // Пишем ТОЛЬКО те отметки, на которые у согласующего есть права: иначе он // затёр бы чужие поля значениями, которые видел у себя на экране. Суммы к - // списанию — только НЕПУСТЫЕ и только среди editableWriteOffKeys; комментарий - // ОДИН на все суммы (IFormData.WriteOffComment, не пара на каждую категорию) - // — если хоть одна из редактируемых сумм не пуста, комментарий обязателен - // (бросает ДО записи чего бы то ни было). + // списанию — только НЕПУСТЫЕ и только среди editableWriteOffKeys. const saveApproverFields = useCallback(async (): Promise => { const fields: Partial = {}; if (hrRecordsEditable) { @@ -185,26 +192,16 @@ const ClearanceView: React.FC = ({ context, itemId, formKey fields.ParkingCard = data.ParkingCard; } - const touched = WRITE_OFF_CATEGORIES.filter( - (cat) => editableWriteOffKeys.has(cat.key) && data[cat.amountField].trim() !== '' - ); const writeOffFields: Partial = {}; - if (touched.length > 0) { - const comment = data.WriteOffComment.trim(); - if (!comment) { - throw new Error('Укажите комментарий — за что списываются суммы.'); - } - for (const cat of touched) { - writeOffFields[cat.amountField] = data[cat.amountField]; - } - writeOffFields.WriteOffComment = comment; + for (const cat of writeOffTouchedCategories) { + writeOffFields[cat.amountField] = data[cat.amountField]; } await updateApproverFields(itemId, fields, listId); if (Object.keys(writeOffFields).length > 0) { await updateWriteOffFields(itemId, writeOffFields, listId); } - }, [itemId, listId, hrRecordsEditable, parkingEditable, editableWriteOffKeys, data]); + }, [itemId, listId, hrRecordsEditable, parkingEditable, writeOffTouchedCategories, data]); const handleSaveFields = async (): Promise => { setSaveState('saving'); @@ -316,6 +313,17 @@ const ClearanceView: React.FC = ({ context, itemId, formKey // Отметки сохраняются вместе с решением — иначе согласующий, забывший // нажать «Сохранить», согласовал бы заявку со старыми значениями. onBeforeDecide={canEditAnything ? saveApproverFields : undefined} + // Заполнил сумму к списанию — комментарий решения обязателен (не + // только при возврате/отклонении, как обычно): это и есть «за что + // конкретно», отдельного поля под него нет (см. writeOffTouchedCategories). + commentRequired={writeOffTouchedCategories.length > 0} + commentPlaceholder={ + writeOffTouchedCategories.length > 0 + ? `Введите, за что сумма${ + writeOffTouchedCategories.length > 1 ? 'ы' : '' + }: ${writeOffTouchedCategories.map((c) => c.label).join(', ')}` + : undefined + } onDecided={(newStatus) => { setStatus(newStatus); setDecisionSeq((n) => n + 1); diff --git a/src/webparts/requestPortal/forms/Clearance/README.md b/src/webparts/requestPortal/forms/Clearance/README.md index 2deb6ab..aba5068 100644 --- a/src/webparts/requestPortal/forms/Clearance/README.md +++ b/src/webparts/requestPortal/forms/Clearance/README.md @@ -58,7 +58,7 @@ RequestTypeId» в `docs/forms.md`). Тип уже зарегистрирова 6 НЕЗАВИСИМЫХ сумм, компактной сеткой (короткие подписи, обычная 2-колоночная сетка секции), колонки `WriteOff{Assets,Ppe,Accountable,Training,Nma,Tmc}Amount` (см. `model.ts WRITE_OFF_CATEGORIES`): «ОС», «СИЗ», «Подотчётная сумма», -«Обучение», «НМА», «ТМЦ». Плюс ОДИН общий комментарий на всех — `WriteOffComment`. +«Обучение», «НМА», «ТМЦ». - **Не привязаны к должности**, в отличие от отметок выше: заполнить может ЛЮБОЙ согласующий, у которого сейчас активный шаг (`ClearanceView.canEditWriteOffs`, @@ -68,26 +68,27 @@ RequestTypeId» в `docs/forms.md`). Тип уже зарегистрирова Признак блокировки — сама непустая сумма на МОМЕНТ ЗАГРУЗКИ страницы (`ClearanceView.lockedWriteOffs`, снимок при загрузке, а не реактивно от текущего `data` — иначе поле блокировалось бы прямо во время набора цифр). -- **Комментарий («за что конкретно») — ОДИН на ВСЕ суммы**, не пара на каждую - категорию: одна Note-колонка (`WriteOffComment`), не шесть. Read-only, пока - нечего заполнять (показывает то, что уже написали раньше); становится - редактируемым и ОБЯЗАТЕЛЬНЫМ ровно тогда, когда хоть одна редактируемая сумма - не пуста (`writeOffTouchedCategories` в `RequestSection`) — `saveApproverFields` - бросает ДО записи чего бы то ни было, если сумма есть, а комментарий пуст. - Плейсхолдер называет категории, для которых он сейчас пишется («Введите, за - что сумма: ОС, ТМЦ»). Следующий согласующий, заполняющий ДРУГУЮ сумму, увидит - и сможет дописать то, что уже есть в поле, — это обычный редактируемый текст, - без автоматического версионирования/склейки. -- Сохраняются той же кнопкой «Сохранить» (`reviewBar`), что и остальные отметки, - и тем же `onBeforeDecide` — при решении несохранённые суммы уйдут вместе с ним. +- **«За что конкретно» — своего поля НЕТ.** Заполнил хоть одну сумму + (`ClearanceView.writeOffTouchedCategories`) — становится обязательным + СТАНДАРТНЫЙ комментарий решения: то самое поле «Комментарий» в блоке «Ваше + решение» (`DecisionPanel`), которым и так сопровождают Согласовать/Вернуть/ + Отклонить — обычно обязательное только при возврате/отклонении, но не здесь + (`commentRequired` у `DecisionPanel`, см. её же новый проп). Плейсхолдер в этот + момент называет категории, которые нужно объяснить («Введите, за что сумма: + ОС, ТМЦ», см. `commentPlaceholder`). Комментарий уходит в СТРОКУ маршрута этого + согласующего (колонка `Comment` списка согласования) — как обычно, отдельного + поля под него на данных заявки не заводили. +- Суммы сохраняются кнопкой «Сохранить» (`reviewBar`) и тем же `onBeforeDecide` + — при решении несохранённые суммы уйдут вместе с ним, ДО проверки + обязательности комментария (см. `DecisionPanel.decide`). - Это ОТДЕЛЬНЫЙ механизм от `TrainingAmount` («Обучение: сумма» выше, привязана к КДП) — сознательно не объединялись, чтобы не менять уже установившееся поведение существующего поля. ## Секции формы RequestSection (сотрудник · должность · подразделение · дата расторжения · сумма -обучения · парковочная карта · карта СКУД · суммы к списанию (компактно) + общий -комментарий · ссылка на заявление) +обучения · парковочная карта · карта СКУД · суммы к списанию (компактно) · +ссылка на заявление) ## Точки входа - rptype: `clearance-form` diff --git a/src/webparts/requestPortal/forms/Clearance/load.ts b/src/webparts/requestPortal/forms/Clearance/load.ts index 7fec964..1feefa2 100644 --- a/src/webparts/requestPortal/forms/Clearance/load.ts +++ b/src/webparts/requestPortal/forms/Clearance/load.ts @@ -44,7 +44,6 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro 'WriteOffTrainingAmount', 'WriteOffNmaAmount', 'WriteOffTmcAmount', - 'WriteOffComment', 'RequestStatus', 'RequestUniqueCode', 'Employee/Title', @@ -73,8 +72,7 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro WriteOffAccountableAmount: numberField(item.WriteOffAccountableAmount), WriteOffTrainingAmount: numberField(item.WriteOffTrainingAmount), WriteOffNmaAmount: numberField(item.WriteOffNmaAmount), - WriteOffTmcAmount: numberField(item.WriteOffTmcAmount), - WriteOffComment: (item.WriteOffComment as string) || '' + WriteOffTmcAmount: numberField(item.WriteOffTmcAmount) }; return { diff --git a/src/webparts/requestPortal/forms/Clearance/model.ts b/src/webparts/requestPortal/forms/Clearance/model.ts index 6fc4912..b053290 100644 --- a/src/webparts/requestPortal/forms/Clearance/model.ts +++ b/src/webparts/requestPortal/forms/Clearance/model.ts @@ -46,6 +46,13 @@ export interface IFormData { * ClearanceView.canEditWriteOffs), но как только сумма непустая — блокируется * НАВСЕГДА, для всех (включая того, кто её ввёл). Признак блокировки — сама * непустая сумма, отдельного флага в списке нет. + * + * «За что конкретно» — своего поля НЕТ: заполнил хоть одну сумму — становится + * обязательным СТАНДАРТНЫЙ комментарий решения (DecisionPanel, «Ваше решение»), + * тот же, каким и так сопровождают Согласовать/Вернуть/Отклонить. Он уже + * хранится построчно в списке согласования (колонка Comment на шаге этого + * согласующего) — заводить для него отдельное поле на данных заявки незачем + * (см. ClearanceView — commentRequired/commentPlaceholder у DecisionPanel). */ WriteOffAssetsAmount: string; WriteOffPpeAmount: string; @@ -53,14 +60,6 @@ export interface IFormData { WriteOffTrainingAmount: string; WriteOffNmaAmount: string; WriteOffTmcAmount: string; - /** - * Комментарий «за что конкретно» — ОДИН на ВСЕ суммы к списанию (не пара на - * каждую категорию — раньше было так, но колонок-комментариев на каждую - * категорию не завели: одного общего достаточно). Обязателен, если хоть одна - * из РЕДАКТИРУЕМЫХ сумм не пуста (см. RequestSection.writeOffTouchedCategories), - * и до этого момента read-only — показывает то, что уже написали раньше. - */ - WriteOffComment: string; } /** Имена всех 6 сумм к списанию — см. IFormData.WriteOff*Amount. */ @@ -124,8 +123,7 @@ export const EMPTY_DATA: IFormData = { WriteOffAccountableAmount: '', WriteOffTrainingAmount: '', WriteOffNmaAmount: '', - WriteOffTmcAmount: '', - WriteOffComment: '' + WriteOffTmcAmount: '' }; /** Плейсхолдер для поля выбора сотрудника. */ diff --git a/src/webparts/requestPortal/forms/Clearance/provisioning.json b/src/webparts/requestPortal/forms/Clearance/provisioning.json index ada4a96..d9b321c 100644 --- a/src/webparts/requestPortal/forms/Clearance/provisioning.json +++ b/src/webparts/requestPortal/forms/Clearance/provisioning.json @@ -21,7 +21,6 @@ { "internalName": "WriteOffAccountableAmount", "title": "Сумма к списанию: подотчётная сумма", "type": "Number", "decimals": 2 }, { "internalName": "WriteOffTrainingAmount", "title": "Сумма к списанию: обучение", "type": "Number", "decimals": 2 }, { "internalName": "WriteOffNmaAmount", "title": "Сумма к списанию: НМА", "type": "Number", "decimals": 2 }, - { "internalName": "WriteOffTmcAmount", "title": "Сумма к списанию: ТМЦ", "type": "Number", "decimals": 2 }, - { "internalName": "WriteOffComment", "title": "Комментарий к списанию", "type": "Note" } + { "internalName": "WriteOffTmcAmount", "title": "Сумма к списанию: ТМЦ", "type": "Number", "decimals": 2 } ] } diff --git a/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx b/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx index a51c4b0..965d58f 100644 --- a/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx +++ b/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { CollapseSection, FormField, TextArea, TextInput, MoneyInput, Toggle, PeopleField, OrganizationField, LinkField } from '../../../shared/ui'; +import { CollapseSection, FormField, TextInput, MoneyInput, Toggle, PeopleField, OrganizationField, LinkField } from '../../../shared/ui'; import { ISectionProps, EMPLOYEE_PLACEHOLDER, WRITE_OFF_CATEGORIES } from '../model'; // Подразделение подтягивается автоматически при выборе сотрудника (см. @@ -32,13 +32,6 @@ export const RequestSection: React.FC = ({ parkingEditable, writeOffEditableKeys }) => { - // Категории, чью сумму заполняют ПРЯМО СЕЙЧАС (редактируема и не пуста) — - // от них зависит, показывать ли общий комментарий и что подставить в его - // плейсхолдер (см. IFormData.WriteOffComment в model.ts). - const writeOffTouchedCategories = writeOffEditableKeys - ? WRITE_OFF_CATEGORIES.filter((cat) => writeOffEditableKeys.has(cat.key) && data[cat.amountField].trim() !== '') - : []; - return ( @@ -112,50 +105,24 @@ export const RequestSection: React.FC = ({ от отметок выше) — но как только сумма указана, блокируется навсегда для всех, поэтому у незаблокированных (writeOffEditableKeys.has(cat.key)) есть readOnly={false}, у остальных — обычное поведение FormMode - (просмотр — заблокировано и так). Комментарий — ОДИН на ВСЕ суммы - (не пара на каждую категорию, см. IFormData.WriteOffComment). */} - {writeOffEditableKeys && ( - <> - {WRITE_OFF_CATEGORIES.map((cat) => { - const editable = writeOffEditableKeys.has(cat.key); - return ( - - set(cat.amountField, v)} - readOnly={editable ? false : undefined} - /> - - ); - })} - {/* Read-only, если сейчас нечего заполнять (нет свежих сумм) — просто - показывает, что написали раньше; редактируема и обязательна - ровно тогда, когда есть непустая сумма, которую надо объяснить. */} - {(writeOffTouchedCategories.length > 0 || data.WriteOffComment) && ( - 0} - full - > -