feat: убрано «Самостоятельное подразделение», суммы к списанию компактно с общим комментарием

Заявление на увольнение: убрано поле «Самостоятельное подразделение»
(StandaloneUnit) — из модели, формы и манифеста провижинера. Колонка на
тенанте остаётся (провижинер их не удаляет), но код её больше не пишет и не
читает.

Обходной лист: убраны 6 отдельных textarea-комментариев под каждой суммой —
сетка сумм теперь компактная (короткие подписи, обычная 2-колоночная сетка
секции, без full). Вместо пары «сумма+комментарий» на категорию — ОДИН общий
комментарий, который появляется и становится обязательным, как только хоть
одна редактируемая сумма заполнена (при сохранении бросает ДО записи, если
сумма есть, а комментарий пуст). Плейсхолдер называет категории, для которых
он сейчас пишется («Введите, за что сумма: ОС, ТМЦ»).

При записи общий текст дублируется в Comment-колонку КАЖДОЙ заполняемой сейчас
категории (SP-схема из 6 пар Amount/Comment не менялась) — так по каждой
категории в списке остаётся свой комментарий, а форма не заставляет писать
одно и то же несколько раз подряд.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-08-26 12:37:04 +05:00
parent 04f1d0d547
commit cd1fd76c76
11 changed files with 178 additions and 155 deletions
+1 -1
View File
@@ -45,7 +45,7 @@
| [`recruiting-referral`](../src/webparts/requestPortal/forms/Referral/README.md) | Реферальная рекомендация | Рекрутинг | да | да | да | кандидат — внешний (текстовые поля, не People); маршрут по рекомендателю; исполнитель переназначает на реального; условное уточнение «откуда знаете» | ✅ | | [`recruiting-referral`](../src/webparts/requestPortal/forms/Referral/README.md) | Реферальная рекомендация | Рекрутинг | да | да | да | кандидат — внешний (текстовые поля, не People); маршрут по рекомендателю; исполнитель переназначает на реального; условное уточнение «откуда знаете» | ✅ |
| [`it-workplace-setup`](../src/webparts/requestPortal/forms/WorkplaceSetup/README.md) | Организация рабочего места | IT | да | да | да | префилл из «прием», поля ПО/доступы правит руководитель | ✅ | | [`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 (согласовано/сразу без согласования — рабочий бакет, отклонено — отдельный бакет) | ✅ | | [`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-dismissal`](../src/webparts/requestPortal/forms/Dismissal/README.md) | Заявление на увольнение | Отдел кадров | да | нет | да | совместители — таблица компаний с удалением строки (снимает и согласующего из матрицы); должность/организация из справочника; **после согласования сам создаёт обходной лист**; печать заблокирована до согласования обходного листа; «Лицо, принимающее ОС/НМА» — заполняет только непосредственный руководитель | ✅ |
| `hr-transfer` | Перемещение сотрудников | Отдел кадров | — | — | — | — | ⏳ заглушка (плитка есть, форма не сделана) | | `hr-transfer` | Перемещение сотрудников | Отдел кадров | — | — | — | — | ⏳ заглушка (плитка есть, форма не сделана) |
@@ -170,10 +170,13 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
); );
// Пишем ТОЛЬКО те отметки, на которые у согласующего есть права: иначе он // Пишем ТОЛЬКО те отметки, на которые у согласующего есть права: иначе он
// затёр бы чужие поля значениями, которые видел у себя на экране. Пары сумм к // затёр бы чужие поля значениями, которые видел у себя на экране. Суммы к
// списанию — только НЕПУСТЫЕ и только среди editableWriteOffKeys; частично // списанию — только НЕПУСТЫЕ и только среди editableWriteOffKeys; комментарий
// заполненная пара (сумма без комментария или наоборот) бросает ДО записи // ОДИН на все суммы, заполняемые СЕЙЧАС (не пара на каждую, см. IFormData.
// чего бы то ни было — половинчатая запись хуже отказа сохранить. // WriteOffComment) — если хоть одна из них не пуста, комментарий обязателен
// (бросает ДО записи чего бы то ни было) и при записи дублируется в
// Comment-колонку КАЖДОЙ такой категории — так каждая остаётся
// прокомментированной в самом списке.
const saveApproverFields = useCallback(async (): Promise<void> => { const saveApproverFields = useCallback(async (): Promise<void> => {
const fields: Partial<IApproverEditableFields> = {}; const fields: Partial<IApproverEditableFields> = {};
if (hrRecordsEditable) { if (hrRecordsEditable) {
@@ -184,19 +187,19 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
fields.ParkingCard = data.ParkingCard; fields.ParkingCard = data.ParkingCard;
} }
const touched = WRITE_OFF_CATEGORIES.filter(
(cat) => editableWriteOffKeys.has(cat.key) && data[cat.amountField].trim() !== ''
);
const writeOffFields: Partial<IWriteOffFields> = {}; const writeOffFields: Partial<IWriteOffFields> = {};
for (const cat of WRITE_OFF_CATEGORIES) { if (touched.length > 0) {
if (!editableWriteOffKeys.has(cat.key)) continue; const comment = data.WriteOffComment.trim();
const amount = data[cat.amountField].trim(); if (!comment) {
const comment = data[cat.commentField].trim(); throw new Error('Укажите комментарий — за что списываются суммы.');
if (!amount && !comment) continue; // не трогали — не пишем }
if (!amount || !comment) { for (const cat of touched) {
throw new Error( writeOffFields[cat.amountField] = data[cat.amountField];
`«${cat.label}»: заполните и сумму, и комментарий (за что конкретно) — или оставьте пару пустой.` writeOffFields[cat.commentField] = comment;
);
} }
writeOffFields[cat.amountField] = data[cat.amountField];
writeOffFields[cat.commentField] = data[cat.commentField];
} }
await updateApproverFields(itemId, fields, listId); await updateApproverFields(itemId, fields, listId);
@@ -55,21 +55,28 @@ RequestTypeId» в `docs/forms.md`). Тип уже зарегистрирова
заведённых руками, ссылки нет и поле не рендерится. заведённых руками, ссылки нет и поле не рендерится.
## Суммы к списанию (согласование) ## Суммы к списанию (согласование)
6 НЕЗАВИСИМЫХ пар «сумма + обязательный комментарий (за что конкретно)», колонки 6 НЕЗАВИСИМЫХ сумм, компактной сеткой (короткие подписи, без комментария под
`WriteOff{Assets,Ppe,Accountable,Training,Nma,Tmc}{Amount,Comment}` (см. `model.ts каждой — см. ниже), колонки `WriteOff{Assets,Ppe,Accountable,Training,Nma,Tmc}
WRITE_OFF_CATEGORIES`): «ОС», «СИЗ», «Подотчётная сумма», «Обучение», «НМА», «ТМЦ». {Amount,Comment}` (см. `model.ts WRITE_OFF_CATEGORIES`): «ОС», «СИЗ», «Подотчётная
сумма», «Обучение», «НМА», «ТМЦ».
- **Не привязаны к должности**, в отличие от отметок выше: заполнить может ЛЮБОЙ - **Не привязаны к должности**, в отличие от отметок выше: заполнить может ЛЮБОЙ
согласующий, у которого сейчас активный шаг (`ClearanceView.canEditWriteOffs`, согласующий, у которого сейчас активный шаг (`ClearanceView.canEditWriteOffs`,
фаза «Согласование» — исполнителям недоступно, как и остальным отметкам). фаза «Согласование» — исполнителям недоступно, как и остальным отметкам).
- **Каждая пара блокируется НАВСЕГДА после первого ввода** — как только сумма - **Каждая сумма блокируется НАВСЕГДА после первого ввода** — как только она
непустая, поле становится read-only для всех, включая того, кто её ввёл. непустая, поле становится read-only для всех, включая того, кто её ввёл.
Признак блокировки — сама непустая сумма на МОМЕНТ ЗАГРУЗКИ страницы Признак блокировки — сама непустая сумма на МОМЕНТ ЗАГРУЗКИ страницы
(`ClearanceView.lockedWriteOffs`, снимок при загрузке, а не реактивно от (`ClearanceView.lockedWriteOffs`, снимок при загрузке, а не реактивно от
текущего `data` — иначе поле блокировалось бы прямо во время набора цифр). текущего `data` — иначе поле блокировалось бы прямо во время набора цифр).
- **Комментарий обязателен вместе с суммой**: заполнить можно либо обе половины - **Комментарий — ОДИН на все суммы, заполняемые в ЭТОМ сохранении** (не пара на
пары, либо ни одной — `saveApproverFields` бросает ДО записи чего бы то ни каждую категорию, как раньше): появляется под сеткой, только когда хоть одна
было, если половина пары осталась пустой. редактируемая сумма не пуста (`writeOffTouchedCategories` в `RequestSection`),
и обязателен в этот момент — `saveApproverFields` бросает ДО записи чего бы то
ни было, если сумма есть, а комментарий пуст. Плейсхолдер называет категории,
для которых он сейчас пишется («Введите, за что сумма: ОС, ТМЦ»). При записи
этот ОДИН текст дублируется в Comment-колонку КАЖДОЙ заполняемой сейчас
категории — так индивидуальный комментарий по категории в списке остаётся, а
форма не заставляет писать одно и то же под каждой суммой отдельно.
- Сохраняются той же кнопкой «Сохранить» (`reviewBar`), что и остальные отметки, - Сохраняются той же кнопкой «Сохранить» (`reviewBar`), что и остальные отметки,
и тем же `onBeforeDecide` — при решении несохранённые суммы уйдут вместе с ним. и тем же `onBeforeDecide` — при решении несохранённые суммы уйдут вместе с ним.
- Это ОТДЕЛЬНЫЙ механизм от `TrainingAmount` («Обучение: сумма» выше, привязана к - Это ОТДЕЛЬНЫЙ механизм от `TrainingAmount` («Обучение: сумма» выше, привязана к
@@ -78,7 +85,8 @@ WRITE_OFF_CATEGORIES`): «ОС», «СИЗ», «Подотчётная сумм
## Секции формы ## Секции формы
RequestSection (сотрудник · должность · подразделение · дата расторжения · сумма RequestSection (сотрудник · должность · подразделение · дата расторжения · сумма
обучения · парковочная карта · карта СКУД · суммы к списанию · ссылка на заявление) обучения · парковочная карта · карта СКУД · суммы к списанию (компактно) + общий
комментарий · ссылка на заявление)
## Точки входа ## Точки входа
- rptype: `clearance-form` - rptype: `clearance-form`
@@ -39,13 +39,20 @@ export interface IFormData {
DismissalUrl: string; DismissalUrl: string;
/** /**
* Суммы к списанию по обходному листу — 6 независимых пар (сумма + * Суммы к списанию по обходному листу — 6 независимых сумм (компактная сетка
* обязательный комментарий, «за что конкретно»), см. WRITE_OFF_CATEGORIES. * полей, см. WRITE_OFF_CATEGORIES). «Сырые» цифры без пробелов, как
* «Сырые» цифры без пробелов, как TrainingAmount (см. MoneyInput). Пусто = * TrainingAmount (см. MoneyInput). Пусто = не заполнено. КАЖДАЯ —
* не заполнено. КАЖДАЯ пара — независимая: заполнить может любой активный * независимая: заполнить может любой активный согласующий (см.
* согласующий (см. ClearanceView.canEditWriteOffs), но как только сумма * ClearanceView.canEditWriteOffs), но как только сумма непустая — блокируется
* непустая — пара блокируется НАВСЕГДА, для всех (включая того, кто её ввёл). * НАВСЕГДА, для всех (включая того, кто её ввёл). Признак блокировки — сама
* Признак блокировки — сама непустая сумма, отдельного флага в списке нет. * непустая сумма, отдельного флага в списке нет.
*
* Комментарий («за что конкретно») — ОБЩИЙ на все суммы, вводимые ОДНИМ
* сохранением (WriteOffComment, ниже), не отдельная пара на каждую категорию:
* при записи он дублируется в Comment-колонку КАЖДОЙ категории, чью сумму
* заполнили именно сейчас (см. ClearanceView.saveApproverFields) — так каждая
* категория остаётся комментированной в самом списке, а форма не заставляет
* писать одно и то же по кругу под каждой суммой.
*/ */
WriteOffAssetsAmount: string; WriteOffAssetsAmount: string;
WriteOffAssetsComment: string; WriteOffAssetsComment: string;
@@ -59,6 +66,12 @@ export interface IFormData {
WriteOffNmaComment: string; WriteOffNmaComment: string;
WriteOffTmcAmount: string; WriteOffTmcAmount: string;
WriteOffTmcComment: string; WriteOffTmcComment: string;
/**
* Комментарий к сумме(ам), заполняемым ПРЯМО СЕЙЧАС (не из списка — вводится
* заново при каждом сохранении, см. комментарий выше). Обязателен, если хоть
* одна из редактируемых сумм не пуста (см. RequestSection).
*/
WriteOffComment: string;
} }
/** Имена всех 12 строковых полей сумм/комментариев к списанию — см. IFormData.WriteOff*. */ /** Имена всех 12 строковых полей сумм/комментариев к списанию — см. IFormData.WriteOff*. */
@@ -145,7 +158,8 @@ export const EMPTY_DATA: IFormData = {
WriteOffNmaAmount: '', WriteOffNmaAmount: '',
WriteOffNmaComment: '', WriteOffNmaComment: '',
WriteOffTmcAmount: '', WriteOffTmcAmount: '',
WriteOffTmcComment: '' WriteOffTmcComment: '',
WriteOffComment: ''
}; };
/** Плейсхолдер для поля выбора сотрудника. */ /** Плейсхолдер для поля выбора сотрудника. */
@@ -31,114 +31,125 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
hrRecordsEditable, hrRecordsEditable,
parkingEditable, parkingEditable,
writeOffEditableKeys writeOffEditableKeys
}) => ( }) => {
<CollapseSection title="Обходной лист" icon="people"> // Категории, чью сумму заполняют ПРЯМО СЕЙЧАС (редактируема и не пуста) —
<FormField label="Сотрудник" htmlFor="Employee" required full> // от них зависит, показывать ли общий комментарий и что подставить в его
<PeopleField // плейсхолдер (см. IFormData.WriteOffComment в model.ts).
context={context} const writeOffTouchedCategories = writeOffEditableKeys
value={data.Employee} ? WRITE_OFF_CATEGORIES.filter((cat) => writeOffEditableKeys.has(cat.key) && data[cat.amountField].trim() !== '')
onChange={onEmployeeChange ?? ((v) => set('Employee', v))} : [];
placeholder={EMPLOYEE_PLACEHOLDER}
/> return (
</FormField> <CollapseSection title="Обходной лист" icon="people">
{/* Подтягивается из профиля при выборе сотрудника, но поле остаётся <FormField label="Сотрудник" htmlFor="Employee" required full>
редактируемым: если профиль не синхронизирован (SPS-JobTitle пуст), <PeopleField
HR должен суметь ввести должность вручную — иначе маршрут context={context}
согласования (условие «IsManager» резолвится именно по ней) не value={data.Employee}
соберётся, и список исполнителей ниже останется пустым. */} onChange={onEmployeeChange ?? ((v) => set('Employee', v))}
<FormField label="Должность" htmlFor="Position" required> placeholder={EMPLOYEE_PLACEHOLDER}
<TextInput />
id="Position" </FormField>
value={data.Position} {/* Подтягивается из профиля при выборе сотрудника, но поле остаётся
onChange={(v) => set('Position', v)} редактируемым: если профиль не синхронизирован (SPS-JobTitle пуст),
placeholder="Подставится автоматически по выбранному сотруднику" HR должен суметь ввести должность вручную — иначе маршрут
/> согласования (условие «IsManager» резолвится именно по ней) не
</FormField> соберётся, и список исполнителей ниже останется пустым. */}
<FormField label="Подразделение" htmlFor="Department"> <FormField label="Должность" htmlFor="Position" required>
<TextInput <TextInput
id="Department" id="Position"
value={data.Department} value={data.Position}
onChange={NOOP} onChange={(v) => set('Position', v)}
readOnly placeholder="Подставится автоматически по выбранному сотруднику"
placeholder="Подставится автоматически по выбранному сотруднику" />
/> </FormField>
</FormField> <FormField label="Подразделение" htmlFor="Department">
<OrganizationField value={data.Organization} /> <TextInput
<FormField label="Дата расторжения договора" htmlFor="TerminationDate" required> id="Department"
<TextInput value={data.Department}
id="TerminationDate" onChange={NOOP}
type="date" readOnly
value={data.TerminationDate} placeholder="Подставится автоматически по выбранному сотруднику"
onChange={(v) => set('TerminationDate', v)} />
/> </FormField>
</FormField> <OrganizationField value={data.Organization} />
{/* Явный readOnly={false} перебивает read-only контекст просмотра (профильный <FormField label="Дата расторжения договора" htmlFor="TerminationDate" required>
согласующий правит своё поле); undefined — обычное поведение из FormMode. */} <TextInput
<FormField label="Обучение: сумма" htmlFor="TrainingAmount"> id="TerminationDate"
<MoneyInput type="date"
id="TrainingAmount" value={data.TerminationDate}
value={data.TrainingAmount} onChange={(v) => set('TerminationDate', v)}
onChange={(v) => set('TrainingAmount', v)} />
readOnly={hrRecordsEditable ? false : undefined} </FormField>
/> {/* Явный readOnly={false} перебивает read-only контекст просмотра (профильный
</FormField> согласующий правит своё поле); undefined — обычное поведение из FormMode. */}
<FormField label="Парковочная карта"> <FormField label="Обучение: сумма" htmlFor="TrainingAmount">
<Toggle <MoneyInput
checked={data.ParkingCard} id="TrainingAmount"
onChange={(v) => set('ParkingCard', v)} value={data.TrainingAmount}
onLabel="Да" onChange={(v) => set('TrainingAmount', v)}
readOnly={parkingEditable ? false : undefined} readOnly={hrRecordsEditable ? false : undefined}
/> />
</FormField> </FormField>
<FormField label="Карта СКУД"> <FormField label="Парковочная карта">
<Toggle <Toggle
checked={data.AccessCard} checked={data.ParkingCard}
onChange={(v) => set('AccessCard', v)} onChange={(v) => set('ParkingCard', v)}
onLabel="Да" onLabel="Да"
readOnly={hrRecordsEditable ? false : undefined} readOnly={parkingEditable ? false : undefined}
/> />
</FormField> </FormField>
{/* Суммы к списанию — 6 независимых пар (сумма + обязательный комментарий, <FormField label="Карта СКУД">
за что конкретно). Каждую может заполнить ЛЮБОЙ активный согласующий <Toggle
(не только профильный, в отличие от отметок выше) — но как только сумма checked={data.AccessCard}
указана, пара блокируется навсегда для всех, поэтому у незаблокированных onChange={(v) => set('AccessCard', v)}
(writeOffEditableKeys.has(cat.key)) есть readOnly={false}, у остальных — onLabel="Да"
обычное поведение FormMode (просмотр — заблокировано и так). */} readOnly={hrRecordsEditable ? false : undefined}
{writeOffEditableKeys && />
WRITE_OFF_CATEGORIES.map((cat) => { </FormField>
const editable = writeOffEditableKeys.has(cat.key); {/* Суммы к списанию — 6 независимых сумм, компактной сеткой (обычная
return ( 2-колоночная сетка секции, короткие подписи — без full). Каждую может
<React.Fragment key={cat.key}> заполнить ЛЮБОЙ активный согласующий (не только профильный, в отличие
<FormField label={`${cat.label}: сумма`} htmlFor={cat.amountField}> от отметок выше) — но как только сумма указана, блокируется навсегда
<MoneyInput для всех, поэтому у незаблокированных (writeOffEditableKeys.has(cat.key))
id={cat.amountField} есть readOnly={false}, у остальных — обычное поведение FormMode
value={data[cat.amountField]} (просмотр — заблокировано и так). Комментарий — ОДИН на все суммы,
onChange={(v) => set(cat.amountField, v)} вводимые сейчас (см. WRITE_OFF_CATEGORIES/WriteOffComment в model.ts). */}
readOnly={editable ? false : undefined} {writeOffEditableKeys && (
/> <>
</FormField> {WRITE_OFF_CATEGORIES.map((cat) => {
<FormField const editable = writeOffEditableKeys.has(cat.key);
label={`${cat.label}: за что конкретно`} return (
htmlFor={cat.commentField} <FormField key={cat.key} label={cat.label} htmlFor={cat.amountField}>
required={editable} <MoneyInput
full id={cat.amountField}
> value={data[cat.amountField]}
onChange={(v) => set(cat.amountField, v)}
readOnly={editable ? false : undefined}
/>
</FormField>
);
})}
{writeOffTouchedCategories.length > 0 && (
<FormField label="Комментарий: за что списание" htmlFor="WriteOffComment" required full>
<TextArea <TextArea
id={cat.commentField} id="WriteOffComment"
value={data[cat.commentField]} value={data.WriteOffComment}
onChange={(v) => set(cat.commentField, v)} onChange={(v) => set('WriteOffComment', v)}
readOnly={editable ? false : undefined} placeholder={`Введите, за что сумма${
writeOffTouchedCategories.length > 1 ? 'ы' : ''
}: ${writeOffTouchedCategories.map((c) => c.label).join(', ')}`}
/> />
</FormField> </FormField>
</React.Fragment> )}
); </>
})} )}
{/* Есть только у листов, созданных автоматически по согласованному заявлению {/* Есть только у листов, созданных автоматически по согласованному заявлению
на увольнение (см. forms/Dismissal/createClearance.ts). */} на увольнение (см. forms/Dismissal/createClearance.ts). */}
<LinkField <LinkField
label="Заявление на увольнение" label="Заявление на увольнение"
url={data.DismissalUrl} url={data.DismissalUrl}
text="Открыть заявление, по которому создан этот лист" text="Открыть заявление, по которому создан этот лист"
/> />
</CollapseSection> </CollapseSection>
); );
};
@@ -52,8 +52,6 @@ RequestTypeId» в `docs/forms.md`). Тип заведён в админке app
/ `organizationColumn` в `model.ts`/`submit.ts`; не-JSON значение (заявка, / `organizationColumn` в `model.ts`/`submit.ts`; не-JSON значение (заявка,
сохранённая до перехода на этот формат) не роняет чтение — трактуется как одна сохранённая до перехода на этот формат) не роняет чтение — трактуется как одна
компания (см. `parseOrganizationColumn`). компания (см. `parseOrganizationColumn`).
- **Самостоятельное подразделение** (`StandaloneUnit`) — в справочнике его нет,
свободный ввод, единственное текстовое поле, оставшееся отдельно от таблицы.
- **Последний рабочий день** (`LastWorkingDay`) — обязателен, не раньше чем через - **Последний рабочий день** (`LastWorkingDay`) — обязателен, не раньше чем через
`MIN_NOTICE_MONTHS` (1) календарный месяц от сегодня (`isoDatePlusMonths`, `MIN_NOTICE_MONTHS` (1) календарный месяц от сегодня (`isoDatePlusMonths`,
`min` у `input type=date` + проверка в `validate` на случай обхода UI) — срок `min` у `input type=date` + проверка в `validate` на случай обхода UI) — срок
@@ -173,9 +171,8 @@ approval-route (`getPlannedMatrix` для фаз «Согласование» и
## Секции формы ## Секции формы
RequestSection (сотрудник · организация/департамент/должность (таблица — RequestSection (сотрудник · организация/департамент/должность (таблица —
редактируемая на форме подачи, read-only в просмотре) · самостоятельное редактируемая на форме подачи, read-only в просмотре) · последний рабочий день ·
подразделение · последний рабочий день · телефон · комментарий · ссылка на телефон · комментарий · ссылка на обходной лист · лицо, принимающее ОС/НМА)
обходной лист · лицо, принимающее ОС/НМА)
## Точки входа ## Точки входа
- rptype: `hr-dismissal` - rptype: `hr-dismissal`
@@ -27,7 +27,6 @@ export async function loadDismissalRequest(itemId: number, listId?: string): Pro
'Id', 'Id',
'Position', 'Position',
'Department', 'Department',
'StandaloneUnit',
'LastWorkingDay', 'LastWorkingDay',
'Phone', 'Phone',
'Comment', 'Comment',
@@ -48,7 +47,6 @@ export async function loadDismissalRequest(itemId: number, listId?: string): Pro
const data: IFormData = { const data: IFormData = {
...EMPTY_DATA, ...EMPTY_DATA,
Employee: employee?.EMail || '', Employee: employee?.EMail || '',
StandaloneUnit: (item.StandaloneUnit as string) || '',
LastWorkingDay: lastDay.slice(0, 10), // ISO → YYYY-MM-DD для input type=date LastWorkingDay: lastDay.slice(0, 10), // ISO → YYYY-MM-DD для input type=date
Phone: (item.Phone as string) || '', Phone: (item.Phone as string) || '',
Comment: (item.Comment as string) || '', Comment: (item.Comment as string) || '',
@@ -86,8 +86,6 @@ export function zipOrganization(
export interface IFormData { export interface IFormData {
/** E-mail сотрудника, за которого заявка (People). Пусто = не выбран. */ /** E-mail сотрудника, за которого заявка (People). Пусто = не выбран. */
Employee: string; Employee: string;
/** Самостоятельное подразделение — в справочнике его нет, вводится вручную. */
StandaloneUnit: string;
/** Последний рабочий день, ISO YYYY-MM-DD (input type=date). */ /** Последний рабочий день, ISO YYYY-MM-DD (input type=date). */
LastWorkingDay: string; LastWorkingDay: string;
/** Контактный телефон (маска, см. PhoneInput). */ /** Контактный телефон (маска, см. PhoneInput). */
@@ -139,7 +137,6 @@ export interface ISectionProps {
export const EMPTY_DATA: IFormData = { export const EMPTY_DATA: IFormData = {
Employee: '', Employee: '',
StandaloneUnit: '',
LastWorkingDay: '', LastWorkingDay: '',
Phone: '', Phone: '',
Comment: '', Comment: '',
@@ -7,7 +7,6 @@
{ "internalName": "Employee", "title": "Сотрудник", "type": "User", "userSelectionMode": "PeopleOnly", "required": true }, { "internalName": "Employee", "title": "Сотрудник", "type": "User", "userSelectionMode": "PeopleOnly", "required": true },
{ "internalName": "Position", "title": "Должность", "type": "Text" }, { "internalName": "Position", "title": "Должность", "type": "Text" },
{ "internalName": "Department", "title": "Департамент", "type": "Text" }, { "internalName": "Department", "title": "Департамент", "type": "Text" },
{ "internalName": "StandaloneUnit", "title": "Самостоятельное подразделение", "type": "Text" },
{ "internalName": "LastWorkingDay", "title": "Последний рабочий день", "type": "DateTime", "dateOnly": true }, { "internalName": "LastWorkingDay", "title": "Последний рабочий день", "type": "DateTime", "dateOnly": true },
{ "internalName": "Phone", "title": "Контактный телефон", "type": "Text" }, { "internalName": "Phone", "title": "Контактный телефон", "type": "Text" },
{ "internalName": "Comment", "title": "Комментарий", "type": "Note" }, { "internalName": "Comment", "title": "Комментарий", "type": "Note" },
@@ -55,9 +55,6 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
} }
onRemove={onRemoveOrganization} onRemove={onRemoveOrganization}
/> />
<FormField label="Самостоятельное подразделение" htmlFor="StandaloneUnit">
<TextInput id="StandaloneUnit" value={data.StandaloneUnit} onChange={(v) => set('StandaloneUnit', v)} />
</FormField>
{/* Заявление подаётся заранее — датой увольнения нельзя назначить ближе, {/* Заявление подаётся заранее — датой увольнения нельзя назначить ближе,
чем через MIN_NOTICE_MONTHS месяц от сегодня (min). */} чем через MIN_NOTICE_MONTHS месяц от сегодня (min). */}
<FormField label="Последний рабочий день" htmlFor="LastWorkingDay" required> <FormField label="Последний рабочий день" htmlFor="LastWorkingDay" required>
@@ -85,7 +85,6 @@ function buildItem(
Position: organizationColumn(data.Organization, (r) => r.position, 'Должность', TEXT_COLUMN_LIMIT), Position: organizationColumn(data.Organization, (r) => r.position, 'Должность', TEXT_COLUMN_LIMIT),
Department: organizationColumn(data.Organization, (r) => r.department, 'Департамент', TEXT_COLUMN_LIMIT), Department: organizationColumn(data.Organization, (r) => r.department, 'Департамент', TEXT_COLUMN_LIMIT),
Organization: organizationColumn(data.Organization, (r) => r.company, 'Организация'), Organization: organizationColumn(data.Organization, (r) => r.company, 'Организация'),
StandaloneUnit: data.StandaloneUnit || null,
LastWorkingDay: data.LastWorkingDay || null, LastWorkingDay: data.LastWorkingDay || null,
Phone: data.Phone || null, Phone: data.Phone || null,
Comment: data.Comment || null Comment: data.Comment || null