merge branch
Tests / frontend-test (push) Successful in 1m10s

This commit is contained in:
Тимур Городецкий
2026-08-26 16:03:49 +05:00
40 changed files with 2462 additions and 47 deletions
@@ -20,12 +20,14 @@ import {
dataListTitle,
dataListTitleForListId
} from '../../shared/listTitles';
import { FORM_ID, IFormData, EMPTY_DATA, SetField } from './model';
import { FORM_ID, IFormData, EMPTY_DATA, SetField, WRITE_OFF_CATEGORIES } from './model';
import { loadClearanceRequest } from './load';
import {
clearanceApprovalService,
updateApproverFields,
IApproverEditableFields
updateWriteOffFields,
IApproverEditableFields,
IWriteOffFields
} from './submit';
import { canEditHrRecordsFields, canEditParkingField } from './approverRights';
import { RequestSection } from './sections/RequestSection';
@@ -52,10 +54,18 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
const [error, setError] = useState<string | null>(null);
const [hrRecordsEditable, setHrRecordsEditable] = useState<boolean>(false);
const [parkingEditable, setParkingEditable] = useState<boolean>(false);
// Суммы к списанию, в отличие от отметок выше, не привязаны к должности — их
// может заполнить ЛЮБОЙ активный согласующий (см. эффект прав ниже).
const [canEditWriteOffs, setCanEditWriteOffs] = useState<boolean>(false);
// Категории, УЖЕ заблокированные (сумма непустая) НА МОМЕНТ ЗАГРУЗКИ — не
// реактивно от текущего data, иначе поле блокировалось бы прямо во время
// ввода (набрал сумму → data стал непустым → поле сразу стало бы readOnly).
const [lockedWriteOffs, setLockedWriteOffs] = useState<Set<string>>(new Set());
const [saveState, setSaveState] = useState<SaveState>('idle');
// Какая из организаций сотрудника (см. OrganizationField) идёт в печать —
// выбор в тулбаре через PrintOrgSelect, по умолчанию первая.
const [printOrgIndex, setPrintOrgIndex] = useState<number>(0);
const [saveErrorMessage, setSaveErrorMessage] = useState<string>('');
const set: SetField = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
@@ -105,6 +115,9 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
setCode(loaded.code);
setError(null);
setLoading(false);
setLockedWriteOffs(
new Set(WRITE_OFF_CATEGORIES.filter((cat) => !!loaded.data[cat.amountField]).map((cat) => cat.key))
);
}
})
.catch((e) => {
@@ -130,11 +143,15 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
const active = !!pending && !pending.isDelegator && pending.phase !== APPROVAL_PHASE.execution;
setHrRecordsEditable(active && canEditHrRecordsFields(pending?.position));
setParkingEditable(active && canEditParkingField(pending?.position));
// Суммы к списанию — без проверки должности: их вводит тот, кто сейчас
// фактически согласует шаг, вне зависимости от отдела.
setCanEditWriteOffs(active);
})
.catch(() => {
if (alive) {
setHrRecordsEditable(false);
setParkingEditable(false);
setCanEditWriteOffs(false);
}
});
return () => {
@@ -143,9 +160,24 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
// decisionSeq: после решения/делегирования активный шаг меняется — права пересчитываем.
}, [itemId, approverService, decisionSeq]);
// Категории списания, которые может править ИМЕННО СЕЙЧАС этот пользователь:
// ещё не заблокированы (см. lockedWriteOffs) И у него сейчас активный шаг.
// Отдельная переменная, не «canEditWriteOffs && !lockedWriteOffs.has(...)»
// на месте использования — нужна и в саму секцию (RequestSection), и здесь.
const editableWriteOffKeys = useMemo(
() =>
canEditWriteOffs
? new Set(WRITE_OFF_CATEGORIES.filter((cat) => !lockedWriteOffs.has(cat.key)).map((cat) => cat.key))
: new Set<string>(),
[canEditWriteOffs, lockedWriteOffs]
);
// Пишем ТОЛЬКО те отметки, на которые у согласующего есть права: иначе он
// затёр бы чужие поля значениями, которые видел у себя на экране.
const saveApproverFields = useCallback((): Promise<void> => {
// затёр бы чужие поля значениями, которые видел у себя на экране. Пары сумм к
// списанию — только НЕПУСТЫЕ и только среди editableWriteOffKeys; частично
// заполненная пара (сумма без комментария или наоборот) бросает ДО записи
// чего бы то ни было — половинчатая запись хуже отказа сохранить.
const saveApproverFields = useCallback(async (): Promise<void> => {
const fields: Partial<IApproverEditableFields> = {};
if (hrRecordsEditable) {
fields.AccessCard = data.AccessCard;
@@ -154,15 +186,36 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
if (parkingEditable) {
fields.ParkingCard = data.ParkingCard;
}
return updateApproverFields(itemId, fields, listId);
}, [itemId, listId, hrRecordsEditable, parkingEditable, data.AccessCard, data.TrainingAmount, data.ParkingCard]);
const writeOffFields: Partial<IWriteOffFields> = {};
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}»: заполните и сумму, и комментарий (за что конкретно) — или оставьте пару пустой.`
);
}
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]);
const handleSaveFields = async (): Promise<void> => {
setSaveState('saving');
setSaveErrorMessage('');
try {
await saveApproverFields();
setSaveState('saved');
} catch {
} catch (e) {
setSaveErrorMessage(friendlyErrorMessage(e));
setSaveState('error');
}
};
@@ -178,8 +231,21 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
);
}
const canEditAnything = hrRecordsEditable || parkingEditable;
const sectionProps = { data, set, context, hrRecordsEditable, parkingEditable, printOrgIndex };
const canEditAnything = hrRecordsEditable || parkingEditable || canEditWriteOffs;
const reviewHints = [
hrRecordsEditable && 'отметить карту СКУД и указать сумму за обучение',
parkingEditable && 'отметить парковочную карту',
editableWriteOffKeys.size > 0 && 'указать суммы к списанию (если применимо)'
].filter((hint): hint is string => !!hint);
const sectionProps = {
data,
set,
context,
hrRecordsEditable,
parkingEditable,
writeOffEditableKeys: editableWriteOffKeys,
printOrgIndex
};
const notify = (): void => {
// requestTypeId ещё не резолвился — решение уже применилось (строка списка
@@ -203,15 +269,15 @@ const ClearanceView: React.FC<IClearanceViewProps> = ({ context, itemId, formKey
{canEditAnything && (
<div className={`${styles.reviewBar} rp-no-print`}>
<span className={styles.reviewHint}>
{hrRecordsEditable
? 'Как согласующий вы можете отметить карту СКУД и указать сумму за обучение.'
: 'Как согласующий вы можете отметить парковочную карту.'}
{reviewHints.length > 0 && `Как согласующий вы можете ${reviewHints.join('; ')}.`}
</span>
<Button variant="secondary" onClick={handleSaveFields} disabled={saveState === 'saving'}>
Сохранить
</Button>
{saveState === 'saved' && <span className={styles.savedNote}>Сохранено</span>}
{saveState === 'error' && <span className={styles.saveError}>Ошибка сохранения</span>}
{saveState === 'error' && (
<span className={styles.saveError}>{saveErrorMessage || 'Ошибка сохранения'}</span>
)}
</div>
)}
@@ -39,8 +39,46 @@ RequestTypeId» в `docs/forms.md`). Тип уже зарегистрирова
(в отличие от `EndDate` в «Заявке на отпуск», которая считается).
- **Заголовок элемента** (`Title`): `Обходной лист — {ФИО} — дд.мм.гггг`.
## Связь с заявлением на увольнение
Обходной лист может появиться **сам, без этой формы**: когда согласовано «Заявление
на увольнение» (`hr-dismissal`), портал создаёт лист по нему и сразу отправляет на
согласование — см. `forms/Dismissal/createClearance.ts` и README той формы. Такой
лист приходит уже с заполненными сотрудником, должностью, подразделением и датой
расторжения; отметки (парковка, СКУД, обучение) по-прежнему проставляют профильные
согласующие. Подача этой формы руками остаётся — для увольнений, оформленных без
заявления в портале.
Пара связана колонками-гиперссылками: `DismissalUrl` здесь → на заявление,
`ClearanceUrl` в заявлении → на лист. Форма их не пишет (заполняет автосоздание либо
вручную в SharePoint), но **показывает**: поле «Заявление на увольнение» —
`LinkField` из `shared/ui`, read-only, открывается в той же вкладке; у листов,
заведённых руками, ссылки нет и поле не рендерится.
## Суммы к списанию (согласование)
6 НЕЗАВИСИМЫХ пар «сумма + обязательный комментарий (за что конкретно)», колонки
`WriteOff{Assets,Ppe,Accountable,Training,Nma,Tmc}{Amount,Comment}` (см. `model.ts
WRITE_OFF_CATEGORIES`): «ОС», «СИЗ», «Подотчётная сумма», «Обучение», «НМА», «ТМЦ».
- **Не привязаны к должности**, в отличие от отметок выше: заполнить может ЛЮБОЙ
согласующий, у которого сейчас активный шаг (`ClearanceView.canEditWriteOffs`,
фаза «Согласование» — исполнителям недоступно, как и остальным отметкам).
- **Каждая пара блокируется НАВСЕГДА после первого ввода** — как только сумма
непустая, поле становится read-only для всех, включая того, кто её ввёл.
Признак блокировки — сама непустая сумма на МОМЕНТ ЗАГРУЗКИ страницы
(`ClearanceView.lockedWriteOffs`, снимок при загрузке, а не реактивно от
текущего `data` — иначе поле блокировалось бы прямо во время набора цифр).
- **Комментарий обязателен вместе с суммой**: заполнить можно либо обе половины
пары, либо ни одной — `saveApproverFields` бросает ДО записи чего бы то ни
было, если половина пары осталась пустой.
- Сохраняются той же кнопкой «Сохранить» (`reviewBar`), что и остальные отметки,
и тем же `onBeforeDecide` — при решении несохранённые суммы уйдут вместе с ним.
- Это ОТДЕЛЬНЫЙ механизм от `TrainingAmount` («Обучение: сумма» выше, привязана к
КДП) — сознательно не объединялись, чтобы не менять уже установившееся
поведение существующего поля.
## Секции формы
RequestSection (сотрудник · должность · подразделение · дата расторжения · сумма обучения · парковочная карта · карта СКУД)
RequestSection (сотрудник · должность · подразделение · дата расторжения · сумма
обучения · парковочная карта · карта СКУД · суммы к списанию · ссылка на заявление)
## Точки входа
- rptype: `clearance-form`
@@ -2,6 +2,11 @@ import { getSP } from '../../shared/pnp/pnpjs';
import { dataListTitle } from '../../shared/listTitles';
import { FORM_ID, IFormData, EMPTY_DATA } from './model';
/** Число или '' (SharePoint отдаёт null для незаполненного Number). */
function numberField(value: unknown): string {
return value === null || value === undefined ? '' : String(value);
}
export interface ILoadedRequest {
data: IFormData;
/** Ф.И.О. увольняемого сотрудника из поля Employee (для заголовка режима чтения). */
@@ -34,6 +39,19 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro
'Organization',
'Project',
'ProjectId',
'DismissalUrl',
'WriteOffAssetsAmount',
'WriteOffAssetsComment',
'WriteOffPpeAmount',
'WriteOffPpeComment',
'WriteOffAccountableAmount',
'WriteOffAccountableComment',
'WriteOffTrainingAmount',
'WriteOffTrainingComment',
'WriteOffNmaAmount',
'WriteOffNmaComment',
'WriteOffTmcAmount',
'WriteOffTmcComment',
'RequestStatus',
'RequestUniqueCode',
'Employee/Title',
@@ -56,7 +74,21 @@ export async function loadClearanceRequest(itemId: number, listId?: string): Pro
TrainingAmount: amount === null || amount === undefined ? '' : String(amount),
Organization: (item.Organization as string) || '',
Project: (item.Project as string) || '',
ProjectId: (item.ProjectId 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) || ''
};
return {
@@ -35,8 +35,84 @@ export interface IFormData {
Project: string;
/** oid проекта ('' — если введён вручную без выбора из списка). */
ProjectId: string;
/**
* Ссылка на заявление на увольнение, по которому создан этот лист — колонка-
* гиперссылка `DismissalUrl`. Форма её не пишет (заполняет автосоздание, см.
* forms/Dismissal/createClearance.ts) и не даёт править — только показывает.
*/
DismissalUrl: string;
/**
* Суммы к списанию по обходному листу — 6 независимых пар (сумма +
* обязательный комментарий, «за что конкретно»), см. WRITE_OFF_CATEGORIES.
* «Сырые» цифры без пробелов, как TrainingAmount (см. MoneyInput). Пусто =
* не заполнено. КАЖДАЯ пара — независимая: заполнить может любой активный
* согласующий (см. ClearanceView.canEditWriteOffs), но как только сумма
* непустая — пара блокируется НАВСЕГДА, для всех (включая того, кто её ввёл).
* Признак блокировки — сама непустая сумма, отдельного флага в списке нет.
*/
WriteOffAssetsAmount: string;
WriteOffAssetsComment: string;
WriteOffPpeAmount: string;
WriteOffPpeComment: string;
WriteOffAccountableAmount: string;
WriteOffAccountableComment: string;
WriteOffTrainingAmount: string;
WriteOffTrainingComment: string;
WriteOffNmaAmount: string;
WriteOffNmaComment: string;
WriteOffTmcAmount: string;
WriteOffTmcComment: string;
}
/** Имена всех 12 строковых полей сумм/комментариев к списанию — см. IFormData.WriteOff*. */
export type WriteOffFieldKey =
| 'WriteOffAssetsAmount'
| 'WriteOffAssetsComment'
| 'WriteOffPpeAmount'
| 'WriteOffPpeComment'
| 'WriteOffAccountableAmount'
| 'WriteOffAccountableComment'
| 'WriteOffTrainingAmount'
| 'WriteOffTrainingComment'
| 'WriteOffNmaAmount'
| 'WriteOffNmaComment'
| 'WriteOffTmcAmount'
| 'WriteOffTmcComment';
/**
* Одна из 6 независимых категорий списания — см. IFormData.WriteOff*.
* amountField/commentField сужены до WriteOffFieldKey (а не keyof IFormData
* целиком): у IFormData есть и булевы поля (ParkingCard/AccessCard), и
* `data[cat.amountField]` должен типизироваться строго строкой — иначе
* `.trim()` в валидации и передача в MoneyInput/TextArea не собираются.
*/
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' }
];
/** Обновление одного поля формы с сохранением типа значения. */
export type SetField = <K extends keyof IFormData>(key: K, value: IFormData[K]) => void;
@@ -64,7 +140,20 @@ export const EMPTY_DATA: IFormData = {
TrainingAmount: '',
Organization: '',
Project: '',
ProjectId: ''
ProjectId: '',
DismissalUrl: '',
WriteOffAssetsAmount: '',
WriteOffAssetsComment: '',
WriteOffPpeAmount: '',
WriteOffPpeComment: '',
WriteOffAccountableAmount: '',
WriteOffAccountableComment: '',
WriteOffTrainingAmount: '',
WriteOffTrainingComment: '',
WriteOffNmaAmount: '',
WriteOffNmaComment: '',
WriteOffTmcAmount: '',
WriteOffTmcComment: ''
};
/** Плейсхолдер для поля выбора сотрудника. */
@@ -14,6 +14,21 @@
{ "internalName": "TrainingAmount", "title": "Обучение: сумма", "type": "Number", "decimals": 2 },
{ "internalName": "Organization", "title": "Организация", "type": "Note" },
{ "internalName": "Project", "title": "Проект", "type": "Text" },
{ "internalName": "ProjectId", "title": "Id проекта (справочник)", "type": "Text" }
{ "internalName": "ProjectId", "title": "Id проекта (справочник)", "type": "Text" },
{ "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" }
]
}
@@ -1,7 +1,7 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, MoneyInput, Toggle, PeopleField, OrganizationField } from '../../../shared/ui';
import { CollapseSection, FormField, TextArea, TextInput, MoneyInput, Toggle, PeopleField, OrganizationField, LinkField } from '../../../shared/ui';
import { ProjectPicker } from '../../../shared/dictionary';
import { ISectionProps, EMPLOYEE_PLACEHOLDER } from '../model';
import { ISectionProps, EMPLOYEE_PLACEHOLDER, WRITE_OFF_CATEGORIES } from '../model';
// Подразделение подтягивается автоматически при выборе сотрудника (см.
// ClearanceForm.handleEmployeeChange — SharePoint User Profile) — свободный
@@ -13,6 +13,14 @@ interface IRequestSectionProps extends ISectionProps {
hrRecordsEditable?: boolean;
/** Согласующий из отдела подбора правит парковочную карту. */
parkingEditable?: boolean;
/**
* Ключи категорий списания (см. WRITE_OFF_CATEGORIES), которые ЭТОТ
* пользователь может сейчас заполнить — присутствие пропа (даже пустым
* Set'ом) включает блок сумм к списанию; не задан вовсе (форма подачи, см.
* ClearanceForm) — блок не показывается: до первого решения списывать
* нечего. См. ClearanceView.editableWriteOffKeys.
*/
writeOffEditableKeys?: Set<string>;
}
/** Единственная секция формы «Обходной лист»: увольняемый, должность, подразделение, даты, карты, обучение. */
@@ -23,7 +31,8 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
onEmployeeChange,
hrRecordsEditable,
parkingEditable,
printOrgIndex
printOrgIndex,
writeOffEditableKeys
}) => (
<CollapseSection title="Обходной лист" icon="people">
<FormField label="Сотрудник" htmlFor="Employee" required full>
@@ -103,5 +112,47 @@ export const RequestSection: React.FC<IRequestSectionProps> = ({
readOnly={hrRecordsEditable ? false : undefined}
/>
</FormField>
{/* Суммы к списанию — 6 независимых пар (сумма + обязательный комментарий,
за что конкретно). Каждую может заполнить ЛЮБОЙ активный согласующий
(не только профильный, в отличие от отметок выше) — но как только сумма
указана, пара блокируется навсегда для всех, поэтому у незаблокированных
(writeOffEditableKeys.has(cat.key)) есть readOnly={false}, у остальных —
обычное поведение FormMode (просмотр — заблокировано и так). */}
{writeOffEditableKeys &&
WRITE_OFF_CATEGORIES.map((cat) => {
const editable = writeOffEditableKeys.has(cat.key);
return (
<React.Fragment key={cat.key}>
<FormField label={`${cat.label}: сумма`} htmlFor={cat.amountField}>
<MoneyInput
id={cat.amountField}
value={data[cat.amountField]}
onChange={(v) => set(cat.amountField, v)}
readOnly={editable ? false : undefined}
/>
</FormField>
<FormField
label={`${cat.label}: за что конкретно`}
htmlFor={cat.commentField}
required={editable}
full
>
<TextArea
id={cat.commentField}
value={data[cat.commentField]}
onChange={(v) => set(cat.commentField, v)}
readOnly={editable ? false : undefined}
/>
</FormField>
</React.Fragment>
);
})}
{/* Есть только у листов, созданных автоматически по согласованному заявлению
на увольнение (см. forms/Dismissal/createClearance.ts). */}
<LinkField
label="Заявление на увольнение"
url={data.DismissalUrl}
text="Открыть заявление, по которому создан этот лист"
/>
</CollapseSection>
);
@@ -1,7 +1,40 @@
import { ApprovalListService, IApproverToWrite, approvalServiceFor, writeRoute } from '../../shared/approval';
import { ISaveItemResult, resolveDataList, saveRequestItem } from '../../shared/dataList';
import { ensureEmployee } from '../../shared/employee';
import { FORM_ID, IFormData } from './model';
import { FORM_ID, IFormData, WriteOffFieldKey } from './model';
/** Одна из 12 колонок сумм/комментариев к списанию — см. model.ts WRITE_OFF_CATEGORIES. */
export type IWriteOffFields = Pick<IFormData, WriteOffFieldKey>;
/**
* Записать сумму(ы)/комментарий(и) к списанию. Пишет РОВНО переданные поля —
* как updateApproverFields, каждая пара независима.
*
* ВАЖНО: вызывающая сторона (ClearanceView) обязана передавать сюда ТОЛЬКО те
* пары, что ещё не заблокированы (см. IFormData.WriteOff* — блокировка по
* непустой сумме) и заполнены целиком (сумма+комментарий вместе, см.
* validateWriteOffPairs) — эта функция сама повторно ничего не проверяет и
* молча перезапишет то, что ей передали.
*/
export async function updateWriteOffFields(
id: number,
fields: Partial<IWriteOffFields>,
listId?: string
): Promise<void> {
const item: Record<string, unknown> = {};
for (const key of Object.keys(fields) as Array<keyof IWriteOffFields>) {
const raw = fields[key];
if (raw === undefined) continue;
if (key.endsWith('Amount')) {
const amount = Number(raw);
item[key] = Number.isFinite(amount) && raw.trim() !== '' ? amount : null;
} else {
item[key] = raw || null;
}
}
if (Object.keys(item).length === 0) return;
await resolveDataList(FORM_ID, listId).items.getById(id).update(item);
}
/**
* Экземпляр сервиса списка согласования этой формы — тонкая обёртка над