// 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. из списка вычисляемых полей квартала. 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." (columnsCurStage из * useRealtimeData, отражающий окно текущего этапа пользователя). */ export const isCellEditable = ({ rowType, columnId, isAdmin, stageColumns, formType }) => rowType === 'INPUT' && !isComputedColumn(columnId, formType) && (isAdmin || stageColumns.has(columnId));