diff --git a/docs/forms.md b/docs/forms.md index d09ca26..879abe9 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» резолвится по должности из самой заявки; да/нет-поля (парковка, СКУД); 5 независимых сумм к списанию компактной сеткой (ОС, СИЗ, подотчётная сумма, НМА, ТМЦ — обучение не входит, у него своё поле) — вводит любой активный согласующий, блокируется навсегда после первого ввода; заполнил хоть одну — стандартный комментарий решения (в блоке «Ваше решение») становится обязательным на любое решение, не только возврат/отклонение | ✅ | | [`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 b786f95..633a964 100644 --- a/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx +++ b/src/webparts/requestPortal/forms/Clearance/ClearanceView.tsx @@ -175,11 +175,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; частично - // заполненная пара (сумма без комментария или наоборот) бросает ДО записи - // чего бы то ни было — половинчатая запись хуже отказа сохранить. + // затёр бы чужие поля значениями, которые видел у себя на экране. Суммы к + // списанию — только НЕПУСТЫЕ и только среди editableWriteOffKeys. const saveApproverFields = useCallback(async (): Promise => { const fields: Partial = {}; if (hrRecordsEditable) { @@ -191,25 +199,15 @@ const ClearanceView: React.FC = ({ context, itemId, formKey } 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}»: заполните и сумму, и комментарий (за что конкретно) — или оставьте пару пустой.` - ); - } + for (const cat of writeOffTouchedCategories) { writeOffFields[cat.amountField] = data[cat.amountField]; - writeOffFields[cat.commentField] = data[cat.commentField]; } 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'); @@ -324,6 +322,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 e943790..048fa68 100644 --- a/src/webparts/requestPortal/forms/Clearance/README.md +++ b/src/webparts/requestPortal/forms/Clearance/README.md @@ -55,30 +55,57 @@ RequestTypeId» в `docs/forms.md`). Тип уже зарегистрирова заведённых руками, ссылки нет и поле не рендерится. ## Суммы к списанию (согласование) -6 НЕЗАВИСИМЫХ пар «сумма + обязательный комментарий (за что конкретно)», колонки -`WriteOff{Assets,Ppe,Accountable,Training,Nma,Tmc}{Amount,Comment}` (см. `model.ts -WRITE_OFF_CATEGORIES`): «ОС», «СИЗ», «Подотчётная сумма», «Обучение», «НМА», «ТМЦ». +Отдельная `CollapseSection title="Суммы к списанию"` (в стиле бывшего общего +заголовка формы «Заявка», удалённого — см. `docs/forms.md`) объединяет +визуально ДВЕ РАЗНЫЕ по правам вещи: + +1. **`TrainingAmount`** («Обучение») — доступность КАК И РАНЬШЕ: правит + только КДП (`hrRecordsEditable`, `approverRights.ts`), сумма НЕ блокируется + после ввода, никак не участвует в WRITE_OFF_CATEGORIES/`writeOffTouchedCategories` + (не требует комментария решения). Изменилась ТОЛЬКО колонка хранения — пишется + и читается через `WriteOffTrainingAmount` (`ClearanceView.saveApproverFields`, + `submit.ts.buildItem`, `load.ts`), а не через одноимённую `TrainingAmount` — + та осталась в схеме списка неиспользуемой. Секция видна независимо от + `writeOffEditableKeys` (само поле — без этого условия), поэтому на форме + подачи (`ClearanceForm`, `writeOffEditableKeys` не передан) поле видно и без + пяти сумм ниже. +2. **5 НЕЗАВИСИМЫХ сумм**, компактной сеткой (короткие подписи, обычная + 2-колоночная сетка секции), колонки `WriteOff{Assets,Ppe,Accountable,Nma, + Tmc}Amount` (см. `model.ts WRITE_OFF_CATEGORIES`): «ОС», «СИЗ», «Подотчётная + сумма», «НМА», «ТМЦ» — правит любой активный согласующий, см. ниже. Раньше + среди них было ещё и «Обучение» — убрали как дубль `TrainingAmount` с ДРУГИМ + поведением под тем же названием; общий заголовок группы остался. - **Не привязаны к должности**, в отличие от отметок выше: заполнить может ЛЮБОЙ согласующий, у которого сейчас активный шаг (`ClearanceView.canEditWriteOffs`, фаза «Согласование» — исполнителям недоступно, как и остальным отметкам). -- **Каждая пара блокируется НАВСЕГДА после первого ввода** — как только сумма +- **Каждая сумма блокируется НАВСЕГДА после первого ввода** — как только она непустая, поле становится read-only для всех, включая того, кто её ввёл. Признак блокировки — сама непустая сумма на МОМЕНТ ЗАГРУЗКИ страницы (`ClearanceView.lockedWriteOffs`, снимок при загрузке, а не реактивно от текущего `data` — иначе поле блокировалось бы прямо во время набора цифр). -- **Комментарий обязателен вместе с суммой**: заполнить можно либо обе половины - пары, либо ни одной — `saveApproverFields` бросает ДО записи чего бы то ни - было, если половина пары осталась пустой. -- Сохраняются той же кнопкой «Сохранить» (`reviewBar`), что и остальные отметки, - и тем же `onBeforeDecide` — при решении несохранённые суммы уйдут вместе с ним. -- Это ОТДЕЛЬНЫЙ механизм от `TrainingAmount` («Обучение: сумма» выше, привязана к +- **«За что конкретно» — своего поля НЕТ.** Заполнил хоть одну сумму + (`ClearanceView.writeOffTouchedCategories`) — становится обязательным + СТАНДАРТНЫЙ комментарий решения: то самое поле «Комментарий» в блоке «Ваше + решение» (`DecisionPanel`), которым и так сопровождают Согласовать/Вернуть/ + Отклонить — обычно обязательное только при возврате/отклонении, но не здесь + (`commentRequired` у `DecisionPanel`, см. её же новый проп). Плейсхолдер в этот + момент называет категории, которые нужно объяснить («Введите, за что сумма: + ОС, ТМЦ», см. `commentPlaceholder`). Комментарий уходит в СТРОКУ маршрута этого + согласующего (колонка `Comment` списка согласования) — как обычно, отдельного + поля под него на данных заявки не заводили. +- Суммы сохраняются кнопкой «Сохранить» (`reviewBar`) и тем же `onBeforeDecide` + — при решении несохранённые суммы уйдут вместе с ним, ДО проверки + обязательности комментария (см. `DecisionPanel.decide`). +- Это ОТДЕЛЬНЫЙ механизм от `TrainingAmount` («Обучение» выше, привязана к КДП) — сознательно не объединялись, чтобы не менять уже установившееся поведение существующего поля. ## Секции формы -RequestSection (сотрудник · должность · подразделение · дата расторжения · сумма -обучения · парковочная карта · карта СКУД · суммы к списанию · ссылка на заявление) +RequestSection (сотрудник · должность · подразделение · дата расторжения · +парковочная карта · карта СКУД — без заголовка, `FormGrid`) + отдельная +CollapseSection «Суммы к списанию» (обучение + 5 сумм списания) + ссылка на +заявление ## Точки входа - rptype: `clearance-form` diff --git a/src/webparts/requestPortal/forms/Clearance/load.ts b/src/webparts/requestPortal/forms/Clearance/load.ts index 0563fa5..51d283c 100644 --- a/src/webparts/requestPortal/forms/Clearance/load.ts +++ b/src/webparts/requestPortal/forms/Clearance/load.ts @@ -35,23 +35,16 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro 'TerminationDate', 'ParkingCard', 'AccessCard', - 'TrainingAmount', 'Organization', 'Project', 'ProjectId', 'DismissalUrl', 'WriteOffAssetsAmount', - 'WriteOffAssetsComment', 'WriteOffPpeAmount', - 'WriteOffPpeComment', 'WriteOffAccountableAmount', - 'WriteOffAccountableComment', 'WriteOffTrainingAmount', - 'WriteOffTrainingComment', 'WriteOffNmaAmount', - 'WriteOffNmaComment', 'WriteOffTmcAmount', - 'WriteOffTmcComment', 'RequestStatus', 'RequestUniqueCode', 'Employee/Title', @@ -61,7 +54,6 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro const employee = item.Employee as { Title?: string; EMail?: string } | undefined; const termination = (item.TerminationDate as string) || ''; - const amount = item.TrainingAmount; const data: IFormData = { ...EMPTY_DATA, @@ -71,24 +63,19 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro TerminationDate: termination.slice(0, 10), // ISO → YYYY-MM-DD для input type=date ParkingCard: !!item.ParkingCard, AccessCard: !!item.AccessCard, - TrainingAmount: amount === null || amount === undefined ? '' : String(amount), + // Хранится в WriteOffTrainingAmount, не в одноимённом TrainingAmount — см. + // IFormData.TrainingAmount. + TrainingAmount: numberField(item.WriteOffTrainingAmount), Organization: (item.Organization as string) || '', Project: (item.Project as string) || '', ProjectId: (item.ProjectId as string) || '', // Гиперссылка приходит объектом {Url, Description} — модели нужен только адрес. DismissalUrl: (item.DismissalUrl as { Url?: string } | undefined)?.Url || '', WriteOffAssetsAmount: numberField(item.WriteOffAssetsAmount), - WriteOffAssetsComment: (item.WriteOffAssetsComment as string) || '', WriteOffPpeAmount: numberField(item.WriteOffPpeAmount), - WriteOffPpeComment: (item.WriteOffPpeComment as string) || '', WriteOffAccountableAmount: numberField(item.WriteOffAccountableAmount), - WriteOffAccountableComment: (item.WriteOffAccountableComment as string) || '', - WriteOffTrainingAmount: numberField(item.WriteOffTrainingAmount), - WriteOffTrainingComment: (item.WriteOffTrainingComment as string) || '', WriteOffNmaAmount: numberField(item.WriteOffNmaAmount), - WriteOffNmaComment: (item.WriteOffNmaComment as string) || '', - WriteOffTmcAmount: numberField(item.WriteOffTmcAmount), - WriteOffTmcComment: (item.WriteOffTmcComment 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 9c84866..d8d554f 100644 --- a/src/webparts/requestPortal/forms/Clearance/model.ts +++ b/src/webparts/requestPortal/forms/Clearance/model.ts @@ -23,7 +23,17 @@ export interface IFormData { TerminationDate: string; ParkingCard: boolean; AccessCard: boolean; - /** Сумма обучения — «сырые» цифры без пробелов (см. MoneyInput). Пусто = не указана. */ + /** + * Сумма обучения — «сырые» цифры без пробелов (см. MoneyInput). Пусто = не + * указана. Доступность — КАК И РАНЬШЕ: правит только начальник КДП + * (`hrRecordsEditable`, `approverRights.ts`), не блокируется после ввода (в + * отличие от WRITE_OFF_CATEGORIES ниже). Единственное, что изменилось, — + * КОЛОНКА ХРАНЕНИЯ: пишется и читается через `WriteOffTrainingAmount` (см. + * ClearanceView.saveApproverFields/load.ts), а не через одноимённый + * `TrainingAmount` в списке — тот остался в схеме неиспользуемым (как и + * другие столбцы, из которых код этой сессии перестал писать/читать, см. + * README «Убрано поле…»). + */ TrainingAmount: string; /** * Организации сотрудника (`\n`-joined «Компания — должность» на строку) — @@ -43,74 +53,62 @@ export interface IFormData { DismissalUrl: string; /** - * Суммы к списанию по обходному листу — 6 независимых пар (сумма + - * обязательный комментарий, «за что конкретно»), см. WRITE_OFF_CATEGORIES. - * «Сырые» цифры без пробелов, как TrainingAmount (см. MoneyInput). Пусто = - * не заполнено. КАЖДАЯ пара — независимая: заполнить может любой активный - * согласующий (см. ClearanceView.canEditWriteOffs), но как только сумма - * непустая — пара блокируется НАВСЕГДА, для всех (включая того, кто её ввёл). - * Признак блокировки — сама непустая сумма, отдельного флага в списке нет. + * Суммы к списанию по обходному листу — 5 независимых сумм (компактная сетка + * полей, см. WRITE_OFF_CATEGORIES). Обучение сюда НЕ входит — у него другая + * доступность (см. TrainingAmount выше): эти пять доступны ЛЮБОМУ активному + * согласующему и блокируются НАВСЕГДА после первого ввода (включая того, кто + * ввёл) — признак блокировки: сама непустая сумма, отдельного флага в + * списке нет. «Сырые» цифры без пробелов (см. MoneyInput). Пусто = не + * заполнено. + * + * «За что конкретно» — своего поля НЕТ: заполнил хоть одну сумму — становится + * обязательным СТАНДАРТНЫЙ комментарий решения (DecisionPanel, «Ваше решение»), + * тот же, каким и так сопровождают Согласовать/Вернуть/Отклонить. Он уже + * хранится построчно в списке согласования (колонка Comment на шаге этого + * согласующего) — заводить для него отдельное поле на данных заявки незачем + * (см. ClearanceView — commentRequired/commentPlaceholder у DecisionPanel). */ WriteOffAssetsAmount: string; - WriteOffAssetsComment: string; WriteOffPpeAmount: string; - WriteOffPpeComment: string; WriteOffAccountableAmount: string; - WriteOffAccountableComment: string; - WriteOffTrainingAmount: string; - WriteOffTrainingComment: string; WriteOffNmaAmount: string; - WriteOffNmaComment: string; WriteOffTmcAmount: string; - WriteOffTmcComment: string; + /** + * Колонка ХРАНЕНИЯ для TrainingAmount (см. выше) — НЕ участвует в + * WRITE_OFF_CATEGORIES/WriteOffFieldKey и в общем механизме «любой + * согласующий, блокировка навсегда»: доступность и незаблокируемость у + * «Обучение: сумма» остались как были, изменилась только SharePoint-колонка. + */ + WriteOffTrainingAmount: string; } -/** Имена всех 12 строковых полей сумм/комментариев к списанию — см. IFormData.WriteOff*. */ +/** Имена 5 сумм к списанию (не считая WriteOffTrainingAmount — у него своя доступность) — см. IFormData.WriteOff*Amount. */ export type WriteOffFieldKey = | 'WriteOffAssetsAmount' - | 'WriteOffAssetsComment' | 'WriteOffPpeAmount' - | 'WriteOffPpeComment' | 'WriteOffAccountableAmount' - | 'WriteOffAccountableComment' - | 'WriteOffTrainingAmount' - | 'WriteOffTrainingComment' | 'WriteOffNmaAmount' - | 'WriteOffNmaComment' - | 'WriteOffTmcAmount' - | 'WriteOffTmcComment'; + | 'WriteOffTmcAmount'; /** - * Одна из 6 независимых категорий списания — см. IFormData.WriteOff*. - * amountField/commentField сужены до WriteOffFieldKey (а не keyof IFormData - * целиком): у IFormData есть и булевы поля (ParkingCard/AccessCard), и - * `data[cat.amountField]` должен типизироваться строго строкой — иначе - * `.trim()` в валидации и передача в MoneyInput/TextArea не собираются. + * Одна из 5 независимых категорий списания — см. IFormData.WriteOff*Amount. + * amountField сужен до WriteOffFieldKey (а не keyof IFormData целиком): у + * IFormData есть и булевы поля (ParkingCard/AccessCard), и `data[cat.amountField]` + * должен типизироваться строго строкой — иначе `.trim()` в валидации и + * передача в MoneyInput не собираются. */ export interface IWriteOffCategory { key: string; label: string; amountField: WriteOffFieldKey; - commentField: WriteOffFieldKey; } export const WRITE_OFF_CATEGORIES: IWriteOffCategory[] = [ - { key: 'assets', label: 'ОС', amountField: 'WriteOffAssetsAmount', commentField: 'WriteOffAssetsComment' }, - { key: 'ppe', label: 'СИЗ', amountField: 'WriteOffPpeAmount', commentField: 'WriteOffPpeComment' }, - { - key: 'accountable', - label: 'Подотчётная сумма', - amountField: 'WriteOffAccountableAmount', - commentField: 'WriteOffAccountableComment' - }, - { - key: 'training', - label: 'Обучение', - amountField: 'WriteOffTrainingAmount', - commentField: 'WriteOffTrainingComment' - }, - { key: 'nma', label: 'НМА', amountField: 'WriteOffNmaAmount', commentField: 'WriteOffNmaComment' }, - { key: 'tmc', label: 'ТМЦ', amountField: 'WriteOffTmcAmount', commentField: 'WriteOffTmcComment' } + { key: 'assets', label: 'ОС', amountField: 'WriteOffAssetsAmount' }, + { key: 'ppe', label: 'СИЗ', amountField: 'WriteOffPpeAmount' }, + { key: 'accountable', label: 'Подотчётная сумма', amountField: 'WriteOffAccountableAmount' }, + { key: 'nma', label: 'НМА', amountField: 'WriteOffNmaAmount' }, + { key: 'tmc', label: 'ТМЦ', amountField: 'WriteOffTmcAmount' } ]; /** Обновление одного поля формы с сохранением типа значения. */ @@ -143,17 +141,11 @@ export const EMPTY_DATA: IFormData = { ProjectId: '', DismissalUrl: '', WriteOffAssetsAmount: '', - WriteOffAssetsComment: '', WriteOffPpeAmount: '', - WriteOffPpeComment: '', WriteOffAccountableAmount: '', - WriteOffAccountableComment: '', WriteOffTrainingAmount: '', - WriteOffTrainingComment: '', WriteOffNmaAmount: '', - WriteOffNmaComment: '', - WriteOffTmcAmount: '', - WriteOffTmcComment: '' + WriteOffTmcAmount: '' }; /** Плейсхолдер для поля выбора сотрудника. */ diff --git a/src/webparts/requestPortal/forms/Clearance/provisioning.json b/src/webparts/requestPortal/forms/Clearance/provisioning.json index 317b330..1d5c1b2 100644 --- a/src/webparts/requestPortal/forms/Clearance/provisioning.json +++ b/src/webparts/requestPortal/forms/Clearance/provisioning.json @@ -19,16 +19,10 @@ { "internalName": "DismissalUrl", "title": "Ссылка на заявление на увольнение", "type": "Url" }, { "internalName": "WriteOffAssetsAmount", "title": "Сумма к списанию: ОС", "type": "Number", "decimals": 2 }, - { "internalName": "WriteOffAssetsComment", "title": "Комментарий: ОС", "type": "Note" }, { "internalName": "WriteOffPpeAmount", "title": "Сумма к списанию: СИЗ", "type": "Number", "decimals": 2 }, - { "internalName": "WriteOffPpeComment", "title": "Комментарий: СИЗ", "type": "Note" }, { "internalName": "WriteOffAccountableAmount", "title": "Сумма к списанию: подотчётная сумма", "type": "Number", "decimals": 2 }, - { "internalName": "WriteOffAccountableComment", "title": "Комментарий: подотчётная сумма", "type": "Note" }, { "internalName": "WriteOffTrainingAmount", "title": "Сумма к списанию: обучение", "type": "Number", "decimals": 2 }, - { "internalName": "WriteOffTrainingComment", "title": "Комментарий: обучение", "type": "Note" }, { "internalName": "WriteOffNmaAmount", "title": "Сумма к списанию: НМА", "type": "Number", "decimals": 2 }, - { "internalName": "WriteOffNmaComment", "title": "Комментарий: НМА", "type": "Note" }, - { "internalName": "WriteOffTmcAmount", "title": "Сумма к списанию: ТМЦ", "type": "Number", "decimals": 2 }, - { "internalName": "WriteOffTmcComment", "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 0796369..e0401d0 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 { FormGrid, CollapseSection, FormField, TextInput, MoneyInput, Toggle, PeopleField, OrganizationField, LinkField } from '../../../shared/ui'; import { ProjectPicker } from '../../../shared/dictionary'; import { ISectionProps, EMPLOYEE_PLACEHOLDER, WRITE_OFF_CATEGORIES } from '../model'; @@ -33,39 +33,41 @@ export const RequestSection: React.FC = ({ parkingEditable, printOrgIndex, writeOffEditableKeys -}) => ( - - - set('Employee', v))} - placeholder={EMPLOYEE_PLACEHOLDER} - /> - - {/* Подтягивается из профиля при выборе сотрудника, но поле остаётся - редактируемым: если профиль не синхронизирован (SPS-JobTitle пуст), - HR должен суметь ввести должность вручную — иначе маршрут - согласования (условие «IsManager» резолвится именно по ней) не - соберётся, и список исполнителей ниже останется пустым. */} - - set('Position', v)} - placeholder="Подставится автоматически по выбранному сотруднику" - /> - - - - - +}) => { + return ( + <> + + + set('Employee', v))} + placeholder={EMPLOYEE_PLACEHOLDER} + /> + + {/* Подтягивается из профиля при выборе сотрудника, но поле остаётся + редактируемым: если профиль не синхронизирован (SPS-JobTitle пуст), + HR должен суметь ввести должность вручную — иначе маршрут + согласования (условие «IsManager» резолвится именно по ней) не + соберётся, и список исполнителей ниже останется пустым. */} + + set('Position', v)} + placeholder="Подставится автоматически по выбранному сотруднику" + /> + + + + + = ({ 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('TerminationDate', v)} + /> + + + set('ParkingCard', v)} + onLabel="Да" + readOnly={parkingEditable ? false : undefined} + /> + + + set('AccessCard', v)} + onLabel="Да" + readOnly={hrRecordsEditable ? false : undefined} + /> + + + {/* Суммы к списанию — своя CollapseSection (тот же стиль заголовка, что + раньше был у общего «Заявка»), объединяет визуально ДВЕ РАЗНЫЕ по + правам вещи: + - TrainingAmount («Обучение») — как раньше: правит только КДП + (hrRecordsEditable), без блокировки после ввода; хранится в + WriteOffTrainingAmount (не одноимённой колонке, см. + IFormData.TrainingAmount), но это деталь хранения, на форму не + влияет. Показывается всегда, даже без writeOffEditableKeys (форма + подачи). + - WRITE_OFF_CATEGORIES (5 сумм) — заполнить может ЛЮБОЙ активный + согласующий (не только профильный), но как только сумма указана, + блокируется НАВСЕГДА для всех, поэтому у незаблокированных + (writeOffEditableKeys.has(cat.key)) readOnly={false}, у остальных + — обычное поведение FormMode. «За что конкретно» у них — своего + поля нет: заполнил хоть одну сумму — обязательным становится + СТАНДАРТНЫЙ комментарий решения в DecisionPanel («Ваше решение»), + см. ClearanceView (commentRequired/commentPlaceholder). У + TrainingAmount этого правила нет — комментарий не требует. Сама + секция видна всегда (Обучение доступно и на форме подачи), а 5 + категорий — только на согласовании (writeOffEditableKeys задан). */} + + + set('TrainingAmount', v)} + readOnly={hrRecordsEditable ? false : undefined} + /> + + {writeOffEditableKeys && + WRITE_OFF_CATEGORIES.map((cat) => { + const editable = writeOffEditableKeys.has(cat.key); + return ( + = ({ readOnly={editable ? false : undefined} /> - -