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

View File

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

View File

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