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>
This commit is contained in:
Andrey Epifancev 2026-11-12 16:05:21 +04:00
parent 24062ed729
commit 9c8871139c
4 changed files with 45 additions and 7 deletions

View File

@ -214,8 +214,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
columnId,
isAdmin: isAdminUser,
stageColumns: stageColumnsSet,
formType,
}),
[isAdminUser, stageColumnsSet],
[isAdminUser, stageColumnsSet, formType],
);
// AURORA-1891: cellIsEditable(row, columnId) не знает про сам column.def —
@ -371,12 +372,13 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
getCellErrors,
denomination,
sheetName,
formType,
});
} catch (error) {
console.error('Error creating columns:', error);
return [];
}
}, [columnsConfig, columnsCurStage, userRoleId, handleCellUpdateError, handleClickRowCell, handleUpdateCell, getCellErrors, denomination, sheetName]);
}, [columnsConfig, columnsCurStage, userRoleId, handleCellUpdateError, handleClickRowCell, handleUpdateCell, getCellErrors, denomination, sheetName, formType]);
// Мемоизация ячеек MRT (memoMode: 'cells') сравнивает только объект cell. Состояние вне данных строки,
// от которого зависят стили закрепления и подсветка фильтров, сбрасывает её новым массивом колонок.

View File

@ -126,6 +126,7 @@ export const getTableColumns = ({
getCellErrors,
denomination = 'thousand',
sheetName,
formType,
}) => {
const columnColors = { ...columnsConfig.colors };
const columns = structuredClone(columnsConfig.columns);
@ -159,6 +160,7 @@ export const getTableColumns = ({
columnId,
isAdmin: userRoleId === ROLES_NAME_ID.admin,
stageColumns,
formType,
})
) {
return false;

View File

@ -62,7 +62,7 @@ const DATA_PREFIX = 'data.';
* OTCH9F). columnId — это React Table column.id в формате accessorKey, т.е.
* всегда с префиксом "data." для полей строки (например "data.q1.booking").
*/
export const isComputedColumn = (columnId) => {
export const isComputedColumn = (columnId, formType) => {
if (typeof columnId !== 'string' || !columnId.startsWith(DATA_PREFIX)) return false;
const parts = columnId.slice(DATA_PREFIX.length).split('.');
@ -81,6 +81,11 @@ export const isComputedColumn = (columnId) => {
// 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;
@ -96,12 +101,12 @@ export const isComputedColumn = (columnId) => {
};
/**
* isCellEditable({ rowType, columnId, isAdmin, stageColumns })
* isCellEditable({ rowType, columnId, isAdmin, stageColumns, formType })
*
* rowType === 'INPUT' && !isComputedColumn(columnId) && (isAdmin || stageColumns.has(columnId))
* rowType === 'INPUT' && !isComputedColumn(columnId, formType) && (isAdmin || stageColumns.has(columnId))
*
* stageColumns — Set с ключами вида "data.<column>" (columnsCurStage из
* useRealtimeData, отражающий окно текущего этапа пользователя).
*/
export const isCellEditable = ({ rowType, columnId, isAdmin, stageColumns }) =>
rowType === 'INPUT' && !isComputedColumn(columnId) && (isAdmin || stageColumns.has(columnId));
export const isCellEditable = ({ rowType, columnId, isAdmin, stageColumns, formType }) =>
rowType === 'INPUT' && !isComputedColumn(columnId, formType) && (isAdmin || stageColumns.has(columnId));

View File

@ -144,3 +144,32 @@ test('plain non-"data." column ids (row number, grouping headers) are never comp
assert.equal(isComputedColumn(undefined), false);
assert.equal(isComputedColumn(null), false);
});
test('AURORA-1938: transfer_econ is computed on FORM_2 only (FORM_4 keeps it editable)', () => {
for (const q of ['q1', 'q2', 'q3', 'q4']) {
const columnId = `data.${q}.transfer_econ`;
assert.equal(isComputedColumn(columnId, 'FORM_2'), true, `${columnId} must be computed on FORM_2`);
assert.equal(isComputedColumn(columnId, 'FORM_4'), false, `${columnId} must stay editable on FORM_4`);
assert.equal(isComputedColumn(columnId), false);
}
assert.equal(
isCellEditable({
rowType: 'INPUT',
columnId: 'data.q1.transfer_econ',
isAdmin: true,
stageColumns: new Set(['data.q1.transfer_econ']),
formType: 'FORM_2',
}),
false,
);
assert.equal(
isCellEditable({
rowType: 'INPUT',
columnId: 'data.q1.transfer_econ',
isAdmin: true,
stageColumns: new Set(),
formType: 'FORM_4',
}),
true,
);
});