Andrey Epifancev 1aec5d2c2d AURORA-1978: удаление пустой программы и проекта на форме 4
«Удалить строку» на строке программы (GROUP) или проекта (ITEM) ничего не
делала: у этих строк нет line_id, окно подтверждения не открывалось, а
события program_deleted / project_deleted, которые сервер уже поддерживает,
фронт не отправлял.

- getDeleteTarget: по выделенной строке выбирает программу, проект или
  строку бюджета; для непустых программы/проекта сразу показывает причину.
- RealtimeContext: deleteProgram / deleteProject и обработка ответов.
- useRealtimeData: строка убирается из дерева, пересчёт ROOT/GROUP из
  ответа сопоставляется по section_code / program_id / project_id
  (alignNodeRows), а не по sort_order — сервер нумерует строки заново, и у
  листа без строк бюджета нет ROOT (найдено при проверке на стенде).
- Окно подтверждения называет удаляемую программу/проект.
- getWsErrorMessage срезает префикс "<class 'asyncpg...'>: " — иначе отказ
  сервера (программа с проектами у устаревшего клиента) показывался с ним.

Refs #303

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 20:02:41 +04:00

97 lines
3.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const ROW_TYPE_LEVELS = {
ROOT: 0,
GROUP: 1,
ITEM: 2,
SUB_ITEM: 3,
INPUT: 4,
};
export function buildRowHierarchy(items, mapItem = (item) => item) {
const result = [];
const stack = [];
for (const [index, item] of items.entries()) {
const currentLevel = ROW_TYPE_LEVELS[item.row_type] ?? 999;
while (stack.length > 0 && (ROW_TYPE_LEVELS[stack.at(-1).row_type] ?? 999) >= currentLevel) {
stack.pop();
}
const parent = stack.at(-1);
const newNode = { ...mapItem(item, index), subRows: [] };
if (parent) parent.subRows.push(newNode);
else result.push(newNode);
if (item.row_type !== 'INPUT') stack.push(newNode);
}
return result;
}
export function getDataRowId(data) {
return data?.id || data?.line_id || null;
}
export function getRowId(row) {
return getDataRowId(row.original.data);
}
// AURORA-1978: что удаляет кнопка «Удалить строку» для выделенной строки.
// На форме 4 у строк программы (GROUP) и проекта (ITEM) нет line_id — их
// удаляют отдельные события program_deleted / project_deleted по id из header.
// Непустые программу/проект сервер не удалит (v3.del_form4_program /
// v3.del_form4_project), поэтому причину показываем сразу, без запроса.
// rowData — row.original (row_type, data, subRows).
export function getDeleteTarget(rowData, { isForm4 = false } = {}) {
if (!rowData) return null;
const header = rowData.data?.header;
const hasChildren = Array.isArray(rowData.subRows) && rowData.subRows.length > 0;
if (isForm4 && rowData.row_type === 'GROUP' && header?.program_id != null) {
return {
kind: 'program',
id: header.program_id,
name: header.name ?? '',
blockedReason: hasChildren ? 'Нельзя удалить программу: в ней есть проекты или расходы' : null,
};
}
if (isForm4 && rowData.row_type === 'ITEM' && header?.project_id != null) {
return {
kind: 'project',
id: header.project_id,
name: header.name ?? '',
blockedReason: hasChildren ? 'Нельзя удалить проект: в нём есть расходы' : null,
};
}
const id = getDataRowId(rowData.data);
return id == null ? null : { kind: 'row', id };
}
export function isVspRow(row) {
if (row?.original?.data?.id) {
return true;
}
return false;
}
export function isVspNewRow(rowData) {
if (!rowData) return false;
const newRow = rowData[3];
return !!newRow.id;
}
export function getParentId(data) {
return data?.project_id || data?.expense_item_id || null;
}
// AURORA-1887: лист AHR отдаёт header.can_add_row=false для статей
// аренды/коммуналки/охраны (у записи сателлита нет понятия "сверх лимита" —
// ручная строка там не имеет смысла). На остальных листах и для остальных
// статей (включая лимитные — AURORA-1907 "сверх лимита" не трогаем) поле
// либо true, либо вовсе отсутствует/null — по умолчанию добавление строки
// разрешено.
export function canAddRowForHeader(header) {
return header?.can_add_row !== false;
}