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:
+1
-1
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user