Andrey Epifancev 9c8871139c fix(form2): фонд экономии на FORM_2 вычисляемый, не редактируется в UI (AURORA-1938, #335)
isComputedColumn/isCellEditable принимают formType; transfer_econ вычисляемый
только на FORM_2, на FORM_4 остаётся редактируемым.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:05:21 +04:00

113 lines
6.0 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.

// AURORA-1890: редактируемость ячейки — чистая функция, вынесенная из
// tableColumns.jsx. Используется tableColumns.jsx (isEditable внутри
// getTableColumns) и будет использоваться AURORA-1891 — сигнатуру без
// необходимости не менять.
//
// isComputedColumn повторяет часть серверной проверки из
// v3._apply_form_cell и v3._apply_form3_cell
// (api/alembic/versions/sql/0023_apply_cell_empty_string.sql; функция
// переопределяется CREATE OR REPLACE в более новых миграциях, но список
// computed_field/normative_field исключений не менялся): какие колонки
// сервер отвергает при попытке правки через _apply_form_cell/_apply_form3_cell,
// те же колонки фронт не должен показывать как редактируемые — даже
// администратору, у которого isEditable иначе был бы всегда true на
// строках INPUT.
//
// Это фронтовый список, зашитый в коде (а не синхронизированный с БД
// автоматически) — умышленно, см. AURORA-1890. Расхождения чинятся отдельным
// PR, а не здесь.
// 0023:416-420 — IF v_field IN (...) THEN RAISE EXCEPTION 'computed_field: %'
// применяется к полям scope q1..q4, плюс FORM_3 (_apply_form3_cell, 0023:515)
// называет те же по смыслу поля иначе (quarter_actual вместо actual_quarter,
// total_corr — отдельное суммарное поле без аналога в FORM_1/2/4).
const QUARTER_COMPUTED_FIELDS = new Set([
'corrected_plan',
'new_plan',
'actual_quarter',
'booking',
'rem_booking',
'rem_actual',
'residual_after_booking',
'residual_after_actual',
'economy',
'total',
// FORM_3 / PROJECT CURRENT_EXPENSES (_apply_form3_cell, 0023:515-517)
'quarter_actual',
'total_corr',
]);
// 0023:200-219 — header.* редактируется через v3.budget_line, остальные
// поля header.* (year/section/item_id/num_group/vsp_address) — computed_field.
const HEADER_EDITABLE_FIELDS = new Set(['name', 'internal_order', 'justification', 'vsp_id']);
// 0023:154-155 — атрибуты ВСП на сателлитах AHR_RENT/AHR_UTILITY/AHR_SECURITY
// правятся через справочник ВСП, не через эту ячейку.
const VSP_ATTRIBUTE_FIELDS = new Set(['address', 'object_type', 'rented_area']);
// 0023:115-127 — лист лимитов AHR_LIMIT: amount_qN вычисляется из
// qty × limit × period_factor, limit_with_vat/limit_without_vat — нормативный
// справочник (правится админом отдельно, не через ячейку формы).
const LIMIT_SHEET_COMPUTED_FIELDS = new Set(['amount_q1', 'amount_q2', 'amount_q3', 'amount_q4', 'limit_with_vat', 'limit_without_vat']);
const QUARTER_SCOPE_PATTERN = /^q[1-4]$/;
const FACT_QUARTER_SCOPE_PATTERN = /^fact_q[1-4]$/;
const DATA_PREFIX = 'data.';
/**
* Повторяет серверный список computed_field/normative_field исключений
* v3._apply_form_cell / v3._apply_form3_cell для набора колонок, который
* знает фронт (FORM_1/2/4, PROJECT, сателлиты AHR_LIMIT/RENT/UTILITY/SECURITY,
* OTCH9F). columnId — это React Table column.id в формате accessorKey, т.е.
* всегда с префиксом "data." для полей строки (например "data.q1.booking").
*/
export const isComputedColumn = (columnId, formType) => {
if (typeof columnId !== 'string' || !columnId.startsWith(DATA_PREFIX)) return false;
const parts = columnId.slice(DATA_PREFIX.length).split('.');
const [scope, field] = parts;
// 0023:221-223 (FORM_1/2/4), 0023:515 (FORM_3) — approved.*/totals.* всегда вычисляемые.
if (scope === 'approved' || scope === 'totals') return true;
// 0023:200-219 — header.* кроме разрешённого списка.
if (scope === 'header') return !HEADER_EDITABLE_FIELDS.has(field);
if (parts.length === 2) {
// *.year — plan.year, seq_dfip.year, seq_ssp.year, reserve.year и т.п.
if (field === 'year') return true;
// qN.<field> из списка вычисляемых полей квартала.
if (QUARTER_SCOPE_PATTERN.test(scope) && QUARTER_COMPUTED_FIELDS.has(field)) return true;
// AURORA-1938: на FORM_2 («Смета РФ») перенос в фонд экономии считается на сервере
// (миграция 0063; _apply_form_cell отвергает запись для FORM_2). На FORM_4
// («Проектная деятельность») transfer_econ — обычное редактируемое поле.
if (formType === 'FORM_2' && QUARTER_SCOPE_PATTERN.test(scope) && field === 'transfer_econ') return true;
// fact_qN.total — сателлиты AHR_RENT/AHR_UTILITY/AHR_SECURITY.
if (FACT_QUARTER_SCOPE_PATTERN.test(scope) && field === 'total') return true;
return false;
}
if (parts.length === 1) {
if (VSP_ATTRIBUTE_FIELDS.has(scope)) return true;
if (LIMIT_SHEET_COMPUTED_FIELDS.has(scope)) return true;
}
return false;
};
/**
* isCellEditable({ rowType, columnId, isAdmin, stageColumns, formType })
*
* rowType === 'INPUT' && !isComputedColumn(columnId, formType) && (isAdmin || stageColumns.has(columnId))
*
* stageColumns — Set с ключами вида "data.<column>" (columnsCurStage из
* useRealtimeData, отражающий окно текущего этапа пользователя).
*/
export const isCellEditable = ({ rowType, columnId, isAdmin, stageColumns, formType }) =>
rowType === 'INPUT' && !isComputedColumn(columnId, formType) && (isAdmin || stageColumns.has(columnId));