Правки #16

Merged
gorbenko.pavel merged 27 commits from fired-request into main 2026-08-26 11:10:27 +00:00
6 changed files with 71 additions and 40 deletions
Showing only changes of commit 5758101243 - Show all commits
@@ -59,11 +59,16 @@ RequestTypeId» в `docs/forms.md`). Тип уже зарегистрирова
`.writeOffLabel`) объединяет визуально ДВЕ РАЗНЫЕ по правам вещи, идущие сразу
за подписью:
1. **`TrainingAmount`** («Обучение: сумма») — уже существовавшее поле, правит
1. **`TrainingAmount`** («Обучение: сумма») — доступность КАК И РАНЬШЕ: правит
только КДП (`hrRecordsEditable`, `approverRights.ts`), сумма НЕ блокируется
после ввода. Заголовок рисуется независимо от него (`writeOffEditableKeys &&
<div>…</div>`, а само поле — БЕЗ этого условия), поэтому на форме подачи
(`ClearanceForm`, `writeOffEditableKeys` не передан) поле видно и без заголовка.
после ввода, никак не участвует в WRITE_OFF_CATEGORIES/`writeOffTouchedCategories`
(не требует комментария решения). Изменилась ТОЛЬКО колонка хранения — пишется
и читается через `WriteOffTrainingAmount` (`ClearanceView.saveApproverFields`,
`submit.ts.buildItem`, `load.ts`), а не через одноимённую `TrainingAmount` —
та осталась в схеме списка неиспользуемой. Заголовок рисуется независимо от
поля (`writeOffEditableKeys && <div>…</div>`, а само поле — БЕЗ этого условия),
поэтому на форме подачи (`ClearanceForm`, `writeOffEditableKeys` не передан)
поле видно и без заголовка.
2. **5 НЕЗАВИСИМЫХ сумм**, компактной сеткой (короткие подписи, обычная
2-колоночная сетка секции), колонки `WriteOff{Assets,Ppe,Accountable,Nma,
Tmc}Amount` (см. `model.ts WRITE_OFF_CATEGORIES`): «ОС», «СИЗ», «Подотчётная
@@ -35,12 +35,12 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro
'TerminationDate',
'ParkingCard',
'AccessCard',
'TrainingAmount',
'Organization',
'DismissalUrl',
'WriteOffAssetsAmount',
'WriteOffPpeAmount',
'WriteOffAccountableAmount',
'WriteOffTrainingAmount',
'WriteOffNmaAmount',
'WriteOffTmcAmount',
'RequestStatus',
@@ -52,7 +52,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,
@@ -62,7 +61,9 @@ 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) || '',
// Гиперссылка приходит объектом {Url, Description} — модели нужен только адрес.
DismissalUrl: (item.DismissalUrl as { Url?: string } | undefined)?.Url || '',
@@ -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 «Компания — должность» на строку) —
@@ -40,15 +50,12 @@ export interface IFormData {
/**
* Суммы к списанию по обходному листу — 5 независимых сумм (компактная сетка
* полей, см. WRITE_OFF_CATEGORIES). Обучение сюда НЕ входит — для него уже
* есть отдельное поле `TrainingAmount` (см. выше) с ДРУГИМ поведением
* (правит только КДП, не любой согласующий); заводить второе «Обучение» с
* иным поведением рядом только путало (полностью совпадающие подписи).
* «Сырые» цифры без пробелов, как TrainingAmount (см. MoneyInput). Пусто =
* не заполнено. КАЖДАЯ — независимая: заполнить может любой активный
* согласующий (см. ClearanceView.canEditWriteOffs), но как только сумма
* непустая — блокируется НАВСЕГДА, для всех (включая того, кто её ввёл).
* Признак блокировки — сама непустая сумма, отдельного флага в списке нет.
* полей, см. WRITE_OFF_CATEGORIES). Обучение сюда НЕ входит — у него другая
* доступность (см. TrainingAmount выше): эти пять доступны ЛЮБОМУ активному
* согласующему и блокируются НАВСЕГДА после первого ввода (включая того, кто
* ввёл) — признак блокировки: сама непустая сумма, отдельного флага в
* списке нет. «Сырые» цифры без пробелов (см. MoneyInput). Пусто = не
* заполнено.
*
* «За что конкретно» — своего поля НЕТ: заполнил хоть одну сумму — становится
* обязательным СТАНДАРТНЫЙ комментарий решения (DecisionPanel, «Ваше решение»),
@@ -62,9 +69,16 @@ export interface IFormData {
WriteOffAccountableAmount: string;
WriteOffNmaAmount: string;
WriteOffTmcAmount: string;
/**
* Колонка ХРАНЕНИЯ для TrainingAmount (см. выше) — НЕ участвует в
* WRITE_OFF_CATEGORIES/WriteOffFieldKey и в общем механизме «любой
* согласующий, блокировка навсегда»: доступность и незаблокируемость у
* «Обучение: сумма» остались как были, изменилась только SharePoint-колонка.
*/
WriteOffTrainingAmount: string;
}
/** Имена всех 5 сумм к списанию — см. IFormData.WriteOff*Amount. */
/** Имена 5 сумм к списанию (не считая WriteOffTrainingAmount — у него своя доступность) — см. IFormData.WriteOff*Amount. */
export type WriteOffFieldKey =
| 'WriteOffAssetsAmount'
| 'WriteOffPpeAmount'
@@ -121,6 +135,7 @@ export const EMPTY_DATA: IFormData = {
WriteOffAssetsAmount: '',
WriteOffPpeAmount: '',
WriteOffAccountableAmount: '',
WriteOffTrainingAmount: '',
WriteOffNmaAmount: '',
WriteOffTmcAmount: ''
};
@@ -19,6 +19,7 @@
{ "internalName": "WriteOffAssetsAmount", "title": "Сумма к списанию: ОС", "type": "Number", "decimals": 2 },
{ "internalName": "WriteOffPpeAmount", "title": "Сумма к списанию: СИЗ", "type": "Number", "decimals": 2 },
{ "internalName": "WriteOffAccountableAmount", "title": "Сумма к списанию: подотчётная сумма", "type": "Number", "decimals": 2 },
{ "internalName": "WriteOffTrainingAmount", "title": "Сумма к списанию: обучение", "type": "Number", "decimals": 2 },
{ "internalName": "WriteOffNmaAmount", "title": "Сумма к списанию: НМА", "type": "Number", "decimals": 2 },
{ "internalName": "WriteOffTmcAmount", "title": "Сумма к списанию: ТМЦ", "type": "Number", "decimals": 2 }
]
@@ -90,24 +90,22 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
readOnly={hrRecordsEditable ? false : undefined}
/>
</FormField>
{/* Суммы к списанию — TrainingAmount («Обучение: сумма», правит только
КДП) ПОДВЕДЕНА под тот же заголовок, что и 5 сумм ниже (правит любой
активный согласующий, см. WRITE_OFF_CATEGORIES) — визуально одна
группа «Суммы к списанию», хотя права на них разные. Без заголовка
«Обучение» из WRITE_OFF_CATEGORIES читалось бы дублем «Обучение:
сумма» — их когда-то было два похожих поля, оставили одно (TrainingAmount,
см. README «Отдельный механизм от TrainingAmount»), но заголовок группы
пригодился и без второго «Обучение».
Явный readOnly={false} у TrainingAmount перебивает read-only контекст
просмотра (профильный согласующий правит своё поле); undefined —
обычное поведение из FormMode. У сумм из WRITE_OFF_CATEGORIES —
заполнить может ЛЮБОЙ активный согласующий (не только профильный), но
как только сумма указана, блокируется навсегда для всех, поэтому у
незаблокированных (writeOffEditableKeys.has(cat.key)) readOnly={false},
у остальных — обычное поведение FormMode. «За что конкретно» — своего
поля здесь нет: заполнил хоть одну сумму — обязательным становится
СТАНДАРТНЫЙ комментарий решения в DecisionPanel («Ваше решение»), см.
ClearanceView (commentRequired/commentPlaceholder). */}
{/* Суммы к списанию — TrainingAmount («Обучение: сумма») ПОДВЕДЕНА под
тот же заголовок, что и 5 сумм ниже, — визуально одна группа «Суммы к
списанию», хотя ДОСТУПНОСТЬ у них РАЗНАЯ:
- TrainingAmount — как раньше: правит только КДП (hrRecordsEditable),
без блокировки после ввода; хранится в WriteOffTrainingAmount (не
одноимённой колонке, см. IFormData.TrainingAmount), но это деталь
хранения, на форму не влияет;
- WRITE_OFF_CATEGORIES (5 сумм) — заполнить может ЛЮБОЙ активный
согласующий (не только профильный), но как только сумма указана,
блокируется НАВСЕГДА для всех, поэтому у незаблокированных
(writeOffEditableKeys.has(cat.key)) readOnly={false}, у остальных
— обычное поведение FormMode. «За что конкретно» у них — своего
поля нет: заполнил хоть одну сумму — обязательным становится
СТАНДАРТНЫЙ комментарий решения в DecisionPanel («Ваше решение»),
см. ClearanceView (commentRequired/commentPlaceholder). У
TrainingAmount этого правила нет — комментарий не требует. */}
{writeOffEditableKeys && <div className={styles.writeOffLabel}>Суммы к списанию</div>}
<FormField label="Обучение: сумма" htmlFor="TrainingAmount">
<MoneyInput
@@ -3,7 +3,7 @@ import { ISaveItemResult, resolveDataList, saveRequestItem } from '../../shared/
import { ensureEmployee } from '../../shared/employee';
import { FORM_ID, IFormData, WriteOffFieldKey } from './model';
/** 6 сумм к списанию — см. model.ts WRITE_OFF_CATEGORIES. «За что конкретно» — не здесь, см. IFormData. */
/** 5 сумм к списанию — см. model.ts WRITE_OFF_CATEGORIES. «За что конкретно» — не здесь, см. IFormData. */
export type IWriteOffFields = Pick<IFormData, WriteOffFieldKey>;
/**
@@ -56,6 +56,11 @@ export interface IApproverEditableFields {
* Пишутся ровно переданные поля: у каждого отдела свой набор прав, и лишние
* ключи затёрли бы чужие отметки значениями из формы этого согласующего.
*
* fields.TrainingAmount пишется в колонку WriteOffTrainingAmount, а НЕ в
* одноимённый TrainingAmount — см. IFormData.TrainingAmount; доступность
* (только КДП, без блокировки навсегда) не изменилась, изменилась только
* колонка хранения.
*
* listId — партиция ОБНОВЛЯЕМОЙ заявки (см. loadClearanceRequest); не задан —
* текущая партиция, как раньше.
*/
@@ -69,7 +74,7 @@ export async function updateApproverFields(
if (fields.ParkingCard !== undefined) item.ParkingCard = fields.ParkingCard;
if (fields.TrainingAmount !== undefined) {
const amount = Number(fields.TrainingAmount);
item.TrainingAmount =
item.WriteOffTrainingAmount =
Number.isFinite(amount) && fields.TrainingAmount.trim() !== '' ? amount : null;
}
if (Object.keys(item).length === 0) return;
@@ -87,7 +92,13 @@ function buildTitle(employeeName: string): string {
return `Обходной лист — ${employeeName} — ${pad2(now.getDate())}.${pad2(now.getMonth() + 1)}.${now.getFullYear()}`;
}
/** Поля элемента списка данных из модели формы (+ Employee по Id). */
/**
* Поля элемента списка данных из модели формы (+ Employee по Id).
*
* Обычно data.TrainingAmount тут пуст (заявитель его не заполняет, только КДП
* на своём шаге — см. updateApproverFields), но пишем на случай редактирования
* уже созданной заявки: колонка WriteOffTrainingAmount, та же, что и там.
*/
function buildItem(data: IFormData, status: string, employeeId: number, employeeName: string): Record<string, unknown> {
const amount = Number(data.TrainingAmount);
return {
@@ -99,7 +110,7 @@ function buildItem(data: IFormData, status: string, employeeId: number, employee
TerminationDate: data.TerminationDate || null,
ParkingCard: data.ParkingCard,
AccessCard: data.AccessCard,
TrainingAmount: Number.isFinite(amount) && data.TrainingAmount.trim() !== '' ? amount : null,
WriteOffTrainingAmount: Number.isFinite(amount) && data.TrainingAmount.trim() !== '' ? amount : null,
Organization: data.Organization || null
};
}