diff --git a/docs/forms.md b/docs/forms.md index d09ca26..502a415 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 57b3cd5..ff56c2d 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx @@ -170,10 +170,13 @@ const ClearanceView: React.FC = ({ context, itemId, formKey ); // Пишем ТОЛЬКО те отметки, на которые у согласующего есть права: иначе он - // затёр бы чужие поля значениями, которые видел у себя на экране. Пары сумм к - // списанию — только НЕПУСТЫЕ и только среди editableWriteOffKeys; частично - // заполненная пара (сумма без комментария или наоборот) бросает ДО записи - // чего бы то ни было — половинчатая запись хуже отказа сохранить. + // затёр бы чужие поля значениями, которые видел у себя на экране. Суммы к + // списанию — только НЕПУСТЫЕ и только среди editableWriteOffKeys; комментарий + // ОДИН на все суммы, заполняемые СЕЙЧАС (не пара на каждую, см. IFormData. + // WriteOffComment) — если хоть одна из них не пуста, комментарий обязателен + // (бросает ДО записи чего бы то ни было) и при записи дублируется в + // Comment-колонку КАЖДОЙ такой категории — так каждая остаётся + // прокомментированной в самом списке. const saveApproverFields = useCallback(async (): Promise => { const fields: Partial = {}; if (hrRecordsEditable) { @@ -184,19 +187,19 @@ 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 = {}; - for (const cat of WRITE_OFF_CATEGORIES) { - if (!editableWriteOffKeys.has(cat.key)) continue; - const amount = data[cat.amountField].trim(); - const comment = data[cat.commentField].trim(); - if (!amount && !comment) continue; // не трогали — не пишем - if (!amount || !comment) { - throw new Error( - `«${cat.label}»: заполните и сумму, и комментарий (за что конкретно) — или оставьте пару пустой.` - ); + 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[cat.commentField] = comment; } - writeOffFields[cat.amountField] = data[cat.amountField]; - writeOffFields[cat.commentField] = data[cat.commentField]; } await updateApproverFields(itemId, fields, listId); diff --git a/src/webparts/requestPortal/forms/Clearance/README.md b/src/webparts/requestPortal/forms/Clearance/README.md index e943790..af8baaf 100644 --- a/src/webparts/requestPortal/forms/Clearance/README.md +++ b/src/webparts/requestPortal/forms/Clearance/README.md @@ -55,21 +55,28 @@ RequestTypeId» в `docs/forms.md`). Тип уже зарегистрирова заведённых руками, ссылки нет и поле не рендерится. ## Суммы к списанию (согласование) -6 НЕЗАВИСИМЫХ пар «сумма + обязательный комментарий (за что конкретно)», колонки -`WriteOff{Assets,Ppe,Accountable,Training,Nma,Tmc}{Amount,Comment}` (см. `model.ts -WRITE_OFF_CATEGORIES`): «ОС», «СИЗ», «Подотчётная сумма», «Обучение», «НМА», «ТМЦ». +6 НЕЗАВИСИМЫХ сумм, компактной сеткой (короткие подписи, без комментария под +каждой — см. ниже), колонки `WriteOff{Assets,Ppe,Accountable,Training,Nma,Tmc} +{Amount,Comment}` (см. `model.ts WRITE_OFF_CATEGORIES`): «ОС», «СИЗ», «Подотчётная +сумма», «Обучение», «НМА», «ТМЦ». - **Не привязаны к должности**, в отличие от отметок выше: заполнить может ЛЮБОЙ согласующий, у которого сейчас активный шаг (`ClearanceView.canEditWriteOffs`, фаза «Согласование» — исполнителям недоступно, как и остальным отметкам). -- **Каждая пара блокируется НАВСЕГДА после первого ввода** — как только сумма +- **Каждая сумма блокируется НАВСЕГДА после первого ввода** — как только она непустая, поле становится read-only для всех, включая того, кто её ввёл. Признак блокировки — сама непустая сумма на МОМЕНТ ЗАГРУЗКИ страницы (`ClearanceView.lockedWriteOffs`, снимок при загрузке, а не реактивно от текущего `data` — иначе поле блокировалось бы прямо во время набора цифр). -- **Комментарий обязателен вместе с суммой**: заполнить можно либо обе половины - пары, либо ни одной — `saveApproverFields` бросает ДО записи чего бы то ни - было, если половина пары осталась пустой. +- **Комментарий — ОДИН на все суммы, заполняемые в ЭТОМ сохранении** (не пара на + каждую категорию, как раньше): появляется под сеткой, только когда хоть одна + редактируемая сумма не пуста (`writeOffTouchedCategories` в `RequestSection`), + и обязателен в этот момент — `saveApproverFields` бросает ДО записи чего бы то + ни было, если сумма есть, а комментарий пуст. Плейсхолдер называет категории, + для которых он сейчас пишется («Введите, за что сумма: ОС, ТМЦ»). При записи + этот ОДИН текст дублируется в Comment-колонку КАЖДОЙ заполняемой сейчас + категории — так индивидуальный комментарий по категории в списке остаётся, а + форма не заставляет писать одно и то же под каждой суммой отдельно. - Сохраняются той же кнопкой «Сохранить» (`reviewBar`), что и остальные отметки, и тем же `onBeforeDecide` — при решении несохранённые суммы уйдут вместе с ним. - Это ОТДЕЛЬНЫЙ механизм от `TrainingAmount` («Обучение: сумма» выше, привязана к @@ -78,7 +85,8 @@ WRITE_OFF_CATEGORIES`): «ОС», «СИЗ», «Подотчётная сумм ## Секции формы RequestSection (сотрудник · должность · подразделение · дата расторжения · сумма -обучения · парковочная карта · карта СКУД · суммы к списанию · ссылка на заявление) +обучения · парковочная карта · карта СКУД · суммы к списанию (компактно) + общий +комментарий · ссылка на заявление) ## Точки входа - rptype: `clearance-form` diff --git a/src/webparts/requestPortal/forms/Clearance/model.ts b/src/webparts/requestPortal/forms/Clearance/model.ts index 2270ba0..8ce3f5a 100644 --- a/src/webparts/requestPortal/forms/Clearance/model.ts +++ b/src/webparts/requestPortal/forms/Clearance/model.ts @@ -39,13 +39,20 @@ export interface IFormData { DismissalUrl: string; /** - * Суммы к списанию по обходному листу — 6 независимых пар (сумма + - * обязательный комментарий, «за что конкретно»), см. WRITE_OFF_CATEGORIES. - * «Сырые» цифры без пробелов, как TrainingAmount (см. MoneyInput). Пусто = - * не заполнено. КАЖДАЯ пара — независимая: заполнить может любой активный - * согласующий (см. ClearanceView.canEditWriteOffs), но как только сумма - * непустая — пара блокируется НАВСЕГДА, для всех (включая того, кто её ввёл). - * Признак блокировки — сама непустая сумма, отдельного флага в списке нет. + * Суммы к списанию по обходному листу — 6 независимых сумм (компактная сетка + * полей, см. WRITE_OFF_CATEGORIES). «Сырые» цифры без пробелов, как + * TrainingAmount (см. MoneyInput). Пусто = не заполнено. КАЖДАЯ — + * независимая: заполнить может любой активный согласующий (см. + * ClearanceView.canEditWriteOffs), но как только сумма непустая — блокируется + * НАВСЕГДА, для всех (включая того, кто её ввёл). Признак блокировки — сама + * непустая сумма, отдельного флага в списке нет. + * + * Комментарий («за что конкретно») — ОБЩИЙ на все суммы, вводимые ОДНИМ + * сохранением (WriteOffComment, ниже), не отдельная пара на каждую категорию: + * при записи он дублируется в Comment-колонку КАЖДОЙ категории, чью сумму + * заполнили именно сейчас (см. ClearanceView.saveApproverFields) — так каждая + * категория остаётся комментированной в самом списке, а форма не заставляет + * писать одно и то же по кругу под каждой суммой. */ WriteOffAssetsAmount: string; WriteOffAssetsComment: string; @@ -59,6 +66,12 @@ export interface IFormData { WriteOffNmaComment: string; WriteOffTmcAmount: string; WriteOffTmcComment: string; + /** + * Комментарий к сумме(ам), заполняемым ПРЯМО СЕЙЧАС (не из списка — вводится + * заново при каждом сохранении, см. комментарий выше). Обязателен, если хоть + * одна из редактируемых сумм не пуста (см. RequestSection). + */ + WriteOffComment: string; } /** Имена всех 12 строковых полей сумм/комментариев к списанию — см. IFormData.WriteOff*. */ @@ -145,7 +158,8 @@ export const EMPTY_DATA: IFormData = { WriteOffNmaAmount: '', WriteOffNmaComment: '', WriteOffTmcAmount: '', - WriteOffTmcComment: '' + WriteOffTmcComment: '', + WriteOffComment: '' }; /** Плейсхолдер для поля выбора сотрудника. */ diff --git a/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx b/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx index e8d33e2..13f45b6 100644 --- a/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx +++ b/src/webparts/requestPortal/forms/Clearance/sections/RequestSection.tsx @@ -31,114 +31,125 @@ export const RequestSection: React.FC = ({ hrRecordsEditable, parkingEditable, writeOffEditableKeys -}) => ( - - - set('Employee', v))} - placeholder={EMPLOYEE_PLACEHOLDER} - /> - - {/* Подтягивается из профиля при выборе сотрудника, но поле остаётся - редактируемым: если профиль не синхронизирован (SPS-JobTitle пуст), - HR должен суметь ввести должность вручную — иначе маршрут - согласования (условие «IsManager» резолвится именно по ней) не - соберётся, и список исполнителей ниже останется пустым. */} - - set('Position', v)} - placeholder="Подставится автоматически по выбранному сотруднику" - /> - - - - - - - set('TerminationDate', v)} - /> - - {/* Явный readOnly={false} перебивает read-only контекст просмотра (профильный - согласующий правит своё поле); undefined — обычное поведение из FormMode. */} - - set('TrainingAmount', v)} - readOnly={hrRecordsEditable ? false : undefined} - /> - - - set('ParkingCard', v)} - onLabel="Да" - readOnly={parkingEditable ? false : undefined} - /> - - - set('AccessCard', v)} - onLabel="Да" - readOnly={hrRecordsEditable ? false : undefined} - /> - - {/* Суммы к списанию — 6 независимых пар (сумма + обязательный комментарий, - за что конкретно). Каждую может заполнить ЛЮБОЙ активный согласующий - (не только профильный, в отличие от отметок выше) — но как только сумма - указана, пара блокируется навсегда для всех, поэтому у незаблокированных - (writeOffEditableKeys.has(cat.key)) есть readOnly={false}, у остальных — - обычное поведение FormMode (просмотр — заблокировано и так). */} - {writeOffEditableKeys && - WRITE_OFF_CATEGORIES.map((cat) => { - const editable = writeOffEditableKeys.has(cat.key); - return ( - - - set(cat.amountField, v)} - readOnly={editable ? false : undefined} - /> - - +}) => { + // Категории, чью сумму заполняют ПРЯМО СЕЙЧАС (редактируема и не пуста) — + // от них зависит, показывать ли общий комментарий и что подставить в его + // плейсхолдер (см. IFormData.WriteOffComment в model.ts). + const writeOffTouchedCategories = writeOffEditableKeys + ? WRITE_OFF_CATEGORIES.filter((cat) => writeOffEditableKeys.has(cat.key) && data[cat.amountField].trim() !== '') + : []; + + return ( + + + set('Employee', v))} + placeholder={EMPLOYEE_PLACEHOLDER} + /> + + {/* Подтягивается из профиля при выборе сотрудника, но поле остаётся + редактируемым: если профиль не синхронизирован (SPS-JobTitle пуст), + HR должен суметь ввести должность вручную — иначе маршрут + согласования (условие «IsManager» резолвится именно по ней) не + соберётся, и список исполнителей ниже останется пустым. */} + + set('Position', v)} + placeholder="Подставится автоматически по выбранному сотруднику" + /> + + + + + + + set('TerminationDate', v)} + /> + + {/* Явный readOnly={false} перебивает read-only контекст просмотра (профильный + согласующий правит своё поле); undefined — обычное поведение из FormMode. */} + + set('TrainingAmount', v)} + readOnly={hrRecordsEditable ? false : undefined} + /> + + + set('ParkingCard', v)} + onLabel="Да" + readOnly={parkingEditable ? false : undefined} + /> + + + set('AccessCard', v)} + onLabel="Да" + readOnly={hrRecordsEditable ? false : undefined} + /> + + {/* Суммы к списанию — 6 независимых сумм, компактной сеткой (обычная + 2-колоночная сетка секции, короткие подписи — без full). Каждую может + заполнить ЛЮБОЙ активный согласующий (не только профильный, в отличие + от отметок выше) — но как только сумма указана, блокируется навсегда + для всех, поэтому у незаблокированных (writeOffEditableKeys.has(cat.key)) + есть readOnly={false}, у остальных — обычное поведение FormMode + (просмотр — заблокировано и так). Комментарий — ОДИН на все суммы, + вводимые сейчас (см. WRITE_OFF_CATEGORIES/WriteOffComment в model.ts). */} + {writeOffEditableKeys && ( + <> + {WRITE_OFF_CATEGORIES.map((cat) => { + const editable = writeOffEditableKeys.has(cat.key); + return ( + + set(cat.amountField, v)} + readOnly={editable ? false : undefined} + /> + + ); + })} + {writeOffTouchedCategories.length > 0 && ( +