AURORA-1984: добавление проекта в разделе на фронте
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
69c4a8f9c9
commit
374ac4bd9e
@ -30,7 +30,7 @@ const CreateProgramModalComponent = ({ isOpen, onClose, onCreate, title = '', is
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFormValid = name !== '';
|
const isFormValid = name.trim() !== '';
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@ import { useValidationRules } from './hooks/useValidationRules';
|
|||||||
import { getColumnVirtualizerOptions, getOrderedVisibleColumns } from './utils/columnLayoutUtils';
|
import { getColumnVirtualizerOptions, getOrderedVisibleColumns } from './utils/columnLayoutUtils';
|
||||||
import { isCellEditable } from './utils/cellEditability';
|
import { isCellEditable } from './utils/cellEditability';
|
||||||
import { findNextEditableColumnId, findNextEditableRowId } from './utils/cellNavigation';
|
import { findNextEditableColumnId, findNextEditableRowId } from './utils/cellNavigation';
|
||||||
import { canAddRowForHeader, getDeleteTarget } from './utils/rowUtils';
|
import { canAddRowForHeader, getDeleteTarget, getProjectAddTarget } from './utils/rowUtils';
|
||||||
import { clearLegacyFormulaKeys } from './utils/localStorageUtils';
|
import { clearLegacyFormulaKeys } from './utils/localStorageUtils';
|
||||||
import { formatHeaderPaths, getValidationRowKey } from './utils/validationUtils';
|
import { formatHeaderPaths, getValidationRowKey } from './utils/validationUtils';
|
||||||
|
|
||||||
@ -931,9 +931,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
|
|
||||||
const handleAddProjectRow = useCallback(
|
const handleAddProjectRow = useCallback(
|
||||||
(name) => {
|
(name) => {
|
||||||
const row = getSelectedRow();
|
contextAddProject({ name: name.trim(), ...getProjectAddTarget(getSelectedRow()?.original) });
|
||||||
if (!row) return;
|
|
||||||
contextAddProject({ name, program_id: row.original.data.header.program_id });
|
|
||||||
},
|
},
|
||||||
[contextAddProject, getSelectedRow],
|
[contextAddProject, getSelectedRow],
|
||||||
);
|
);
|
||||||
@ -1015,17 +1013,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
toast.error('Добавление строк недоступно: нет доступных для редактирования столбцов');
|
toast.error('Добавление строк недоступно: нет доступных для редактирования столбцов');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const row = getSelectedRow();
|
|
||||||
if (!row) {
|
|
||||||
toast.error('Выделите строку для вставки');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (row.original.row_type !== 'GROUP') {
|
|
||||||
toast.error('Выделите строку программы');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
modalsRef.current?.open('project');
|
modalsRef.current?.open('project');
|
||||||
}, [canModifyRows, getSelectedRow]);
|
}, [canModifyRows]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
deleteRow,
|
deleteRow,
|
||||||
insertCell,
|
insertCell,
|
||||||
insertProjectRow,
|
insertProjectRow,
|
||||||
|
insertStandaloneProjectRow,
|
||||||
updateCells,
|
updateCells,
|
||||||
} from '../utils/cellUtils';
|
} from '../utils/cellUtils';
|
||||||
import { buildRowHierarchy, getParentId, isVspNewRow } from '../utils/rowUtils';
|
import { buildRowHierarchy, getParentId, isVspNewRow } from '../utils/rowUtils';
|
||||||
@ -150,9 +151,14 @@ const useRealtimeData = (formId, sheetName, direction, formType, year) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setData((prevData) => {
|
setData((prevData) => {
|
||||||
const updateData = updateCells(prevData, updatedCells);
|
let base = structuredClone(prevData);
|
||||||
const newData = insertProjectRow(updateData, newRow, program_id);
|
const rootRow = updates.find((row) => row[0] === 'ROOT');
|
||||||
return newData;
|
if (base[0]?.row_type !== 'ROOT' && rootRow) {
|
||||||
|
base = addRootRow(base, rootRow);
|
||||||
|
}
|
||||||
|
const updateData = updateCells(base, alignNodeRows(base, updatedCells));
|
||||||
|
if (program_id != null) return insertProjectRow(updateData, newRow, program_id);
|
||||||
|
return insertStandaloneProjectRow(updateData, newRow);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -284,6 +284,30 @@ export function insertProjectRow(data, newRow, program_id) {
|
|||||||
return updatedRows;
|
return updatedRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AURORA-1984: самостоятельный проект (без программы) — под первый ROOT или на
|
||||||
|
// верхний уровень, перед первой строкой списка с большим sort_order (исторические
|
||||||
|
// INPUT идут после проектов). Номера строк с sort_order >= нового сдвигаются.
|
||||||
|
export function insertStandaloneProjectRow(data, newRow) {
|
||||||
|
const updatedRows = structuredClone(data);
|
||||||
|
const [row_type, depth, sort_order, dataRow] = newRow;
|
||||||
|
const newRowObject = { row_type, depth, sort_order, subRows: [], data: dataRow };
|
||||||
|
|
||||||
|
const shift = (rows) => {
|
||||||
|
for (const row of rows) {
|
||||||
|
if (row.sort_order >= sort_order) row.sort_order += 1;
|
||||||
|
if (Array.isArray(row.subRows)) shift(row.subRows);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
shift(updatedRows);
|
||||||
|
|
||||||
|
const root = updatedRows.find((row) => row.row_type === 'ROOT');
|
||||||
|
if (root && !Array.isArray(root.subRows)) root.subRows = [];
|
||||||
|
const target = root ? root.subRows : updatedRows;
|
||||||
|
const index = target.findIndex((row) => row.sort_order > sort_order);
|
||||||
|
target.splice(index === -1 ? target.length : index, 0, newRowObject);
|
||||||
|
return updatedRows;
|
||||||
|
}
|
||||||
|
|
||||||
export function deleteRow(data, lineId) {
|
export function deleteRow(data, lineId) {
|
||||||
if (lineId == null) return data;
|
if (lineId == null) return data;
|
||||||
return deleteRowWhere(data, (row) => getDataRowId(row.data) === lineId);
|
return deleteRowWhere(data, (row) => getDataRowId(row.data) === lineId);
|
||||||
|
|||||||
@ -1,6 +1,15 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import { alignNodeRows, deleteProgramRow, deleteProjectRow, deleteRow, updateCells } from './cellUtils.js';
|
import {
|
||||||
|
addRootRow,
|
||||||
|
alignNodeRows,
|
||||||
|
deleteProgramRow,
|
||||||
|
deleteProjectRow,
|
||||||
|
deleteRow,
|
||||||
|
insertCell,
|
||||||
|
insertStandaloneProjectRow,
|
||||||
|
updateCells,
|
||||||
|
} from './cellUtils.js';
|
||||||
|
|
||||||
// Эталон: прежняя реализация updateCells (до AURORA-1838).
|
// Эталон: прежняя реализация updateCells (до AURORA-1838).
|
||||||
function oldUpdateCells(data, newRows) {
|
function oldUpdateCells(data, newRows) {
|
||||||
@ -385,3 +394,49 @@ test('AURORA-1978: alignNodeRows — ROOT по section_code, строки без
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const node = (rowType, sortOrder, header = {}, subRows = []) => ({
|
||||||
|
row_type: rowType,
|
||||||
|
depth: { ROOT: 0, GROUP: 1, ITEM: 2, INPUT: 3 }[rowType],
|
||||||
|
sort_order: sortOrder,
|
||||||
|
subRows,
|
||||||
|
data: { header },
|
||||||
|
});
|
||||||
|
const newItem = (sortOrder) => ['ITEM', 2, sortOrder, { header: { project_id: 77, name: 'Н' } }];
|
||||||
|
|
||||||
|
test('AURORA-1984: самостоятельный проект — под ROOT, после соседей', () => {
|
||||||
|
const result = insertStandaloneProjectRow([node('ROOT', 1, {}, [node('GROUP', 2)])], newItem(3));
|
||||||
|
assert.deepEqual(
|
||||||
|
result[0].subRows.map((r) => [r.row_type, r.sort_order]),
|
||||||
|
[
|
||||||
|
['GROUP', 2],
|
||||||
|
['ITEM', 3],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AURORA-1984: самостоятельный проект — на верхний уровень без ROOT', () => {
|
||||||
|
const result = insertStandaloneProjectRow([node('GROUP', 1)], newItem(2));
|
||||||
|
assert.deepEqual(
|
||||||
|
result.map((r) => r.row_type),
|
||||||
|
['GROUP', 'ITEM'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AURORA-1984: самостоятельный проект — перед историческим расходом, номера сдвигаются', () => {
|
||||||
|
const result = insertStandaloneProjectRow([node('ROOT', 1, {}, [node('GROUP', 2), node('INPUT', 3)])], newItem(3));
|
||||||
|
assert.deepEqual(
|
||||||
|
result[0].subRows.map((r) => [r.row_type, r.sort_order]),
|
||||||
|
[
|
||||||
|
['GROUP', 2],
|
||||||
|
['ITEM', 3],
|
||||||
|
['INPUT', 4],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AURORA-1984: insertCell после addRootRow — первый расход самостоятельного проекта', () => {
|
||||||
|
const withRoot = addRootRow([node('ITEM', 1, { project_id: 21 })], ['ROOT', 0, 1, { header: { expense_item_id: 900 } }]);
|
||||||
|
const result = insertCell(withRoot, ['INPUT', 3, 3, { line_id: 5, header: { project_id: 21 } }], 21);
|
||||||
|
assert.equal(result[0].subRows[0].subRows[0].data.line_id, 5);
|
||||||
|
});
|
||||||
|
|||||||
@ -69,6 +69,15 @@ export function getDeleteTarget(rowData, { isForm4 = false } = {}) {
|
|||||||
return id == null ? null : { kind: 'row', id };
|
return id == null ? null : { kind: 'row', id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AURORA-1984: цель добавления проекта по выделенной строке. Программа задана
|
||||||
|
// только если у GROUP/ITEM/INPUT header.program_id != null; иначе проект
|
||||||
|
// самостоятельный. rowData — row.original.
|
||||||
|
export function getProjectAddTarget(rowData) {
|
||||||
|
const programId = rowData?.data?.header?.program_id;
|
||||||
|
const typed = ['GROUP', 'ITEM', 'INPUT'].includes(rowData?.row_type);
|
||||||
|
return typed && programId != null ? { program_id: programId } : {};
|
||||||
|
}
|
||||||
|
|
||||||
export function isVspRow(row) {
|
export function isVspRow(row) {
|
||||||
if (row?.original?.data?.id) {
|
if (row?.original?.data?.id) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { buildRowHierarchy, canAddRowForHeader, getDataRowId, getDeleteTarget } from './rowUtils.js';
|
import { buildRowHierarchy, canAddRowForHeader, getDataRowId, getDeleteTarget, getProjectAddTarget } from './rowUtils.js';
|
||||||
|
|
||||||
test('row hierarchy uses row types when depth skips a level and keeps INPUT rows as siblings', () => {
|
test('row hierarchy uses row types when depth skips a level and keeps INPUT rows as siblings', () => {
|
||||||
const rows = [
|
const rows = [
|
||||||
@ -87,3 +87,13 @@ test('AURORA-1978: getDeleteTarget — строка бюджета и прочи
|
|||||||
assert.equal(getDeleteTarget(form4Row('ROOT', { section_code: 'S' }), { isForm4: true }), null);
|
assert.equal(getDeleteTarget(form4Row('ROOT', { section_code: 'S' }), { isForm4: true }), null);
|
||||||
assert.equal(getDeleteTarget(null, { isForm4: true }), null);
|
assert.equal(getDeleteTarget(null, { isForm4: true }), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('AURORA-1984: getProjectAddTarget — цель по выделенной строке', () => {
|
||||||
|
assert.deepEqual(getProjectAddTarget(undefined), {});
|
||||||
|
assert.deepEqual(getProjectAddTarget(form4Row('ROOT', { section_code: 'CAP' })), {});
|
||||||
|
assert.deepEqual(getProjectAddTarget(form4Row('GROUP', { program_id: 11 })), { program_id: 11 });
|
||||||
|
assert.deepEqual(getProjectAddTarget(form4Row('ITEM', { program_id: 11, project_id: 21 })), { program_id: 11 });
|
||||||
|
assert.deepEqual(getProjectAddTarget(form4Row('ITEM', { program_id: null, project_id: 21 })), {});
|
||||||
|
assert.deepEqual(getProjectAddTarget(form4Row('ITEM', { project_id: 21 })), {});
|
||||||
|
assert.deepEqual(getProjectAddTarget(form4Row('INPUT', { program_id: 11, project_id: 21 })), { program_id: 11 });
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user