Merge pull request 'формула для исполнителей' (#60) from fix-front into test

Reviewed-on: #60
This commit is contained in:
PotapovaA 2026-06-22 13:33:49 +03:00
commit 66c1f09df6
6 changed files with 229 additions and 23 deletions

View File

@ -1,14 +1,41 @@
// EditCell.js - обновленная версия
import { useEffect, useRef, useLayoutEffect, useState } from 'react';
import { isFormula, extractFormula, calculateFormula, formatNumber } from '../../utils/formulaUtils';
import { saveToStorage, loadFromStorage } from '../../utils/localStorageUtils';
const EditCell = ({ refCell, onChange, disabled, value }) => {
const EditCell = ({ refCell, onChange, disabled, value, tableId, cellId }) => {
const [curValue, setCurValue] = useState(value);
const [displayValue, setDisplayValue] = useState(value);
const [isFormulaMode, setIsFormulaMode] = useState(false);
// Ключ для хранения формул в localStorage
const storageKey = `formula_${tableId}_${cellId}`;
const trFrag = refCell.current.offsetParent.offsetParent;
const tdFrag = refCell.current.offsetParent;
const left = tdFrag.offsetLeft;
const width = refCell.current.offsetParent.offsetWidth;
const translateY = trFrag.style.transform;
// Загружаем сохраненную формулу при монтировании
useEffect(() => {
const savedFormula = loadFromStorage(storageKey);
if (savedFormula && isFormula(savedFormula)) {
setIsFormulaMode(true);
setCurValue(savedFormula);
// Вычисляем и отображаем результат
try {
const formula = extractFormula(savedFormula);
const result = calculateFormula(formula);
setDisplayValue(formatNumber(result));
} catch (error) {
setDisplayValue('#ОШИБКА');
}
} else {
setDisplayValue(value);
}
}, [storageKey, value]);
function useAutoResize(value) {
const ref = useRef(null);
@ -37,13 +64,48 @@ const EditCell = ({ refCell, onChange, disabled, value }) => {
}
}, [disabled, ref]);
const handleChange = () => {
onChange?.(curValue);
const handleSave = () => {
// Проверяем, является ли введенное значение формулой
if (isFormula(curValue)) {
// Сохраняем формулу в localStorage
saveToStorage(storageKey, curValue);
// Вычисляем и сохраняем результат
try {
const formula = extractFormula(curValue);
const result = calculateFormula(formula);
const formattedResult = formatNumber(result);
setDisplayValue(formattedResult);
onChange?.(formattedResult);
setIsFormulaMode(true);
return;
} catch (error) {
setDisplayValue('#ОШИБКА');
onChange?.(curValue); // Сохраняем как текст, если ошибка
return;
}
} else {
// Если это не формула, удаляем сохраненную формулу
localStorage.removeItem(storageKey);
setIsFormulaMode(false);
setDisplayValue(curValue);
onChange?.(curValue);
}
};
const handleChangeValue = (event) => {
const input = event.target.value;
setCurValue(input);
// При редактировании показываем введенный текст
setDisplayValue(input);
};
// Функция для отображения значения в ячейке
const getDisplayValue = () => {
if (isFormulaMode && isFormula(curValue)) {
return displayValue;
}
return displayValue;
};
return (
@ -60,6 +122,21 @@ const EditCell = ({ refCell, onChange, disabled, value }) => {
top: 0,
}}
>
{isFormulaMode && (
<div style={{
position: 'absolute',
top: '-20px',
left: '4px',
fontSize: '10px',
color: '#666',
background: '#f0f0f0',
padding: '2px 6px',
borderRadius: '3px',
pointerEvents: 'none'
}}>
fx
</div>
)}
<textarea
ref={ref}
disabled={disabled}
@ -68,10 +145,24 @@ const EditCell = ({ refCell, onChange, disabled, value }) => {
height: 'auto',
fieldSizing: 'content',
fontSize: 'inherit',
backgroundColor: isFormulaMode ? '#f8f9fa' : 'white',
border: isFormulaMode ? '2px solid #4a90d9' : undefined,
}}
onChange={handleChangeValue}
value={curValue}
onBlur={handleChange}
onBlur={handleSave}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSave();
// Закрываем редактирование
if (onChange) {
// Симулируем завершение редактирования
document.activeElement?.blur();
}
}
}}
placeholder={isFormulaMode ? 'Введите формулу (начинается с =)' : ''}
/>
</div>
</td>
@ -79,4 +170,4 @@ const EditCell = ({ refCell, onChange, disabled, value }) => {
);
};
export default EditCell;
export default EditCell;

View File

@ -61,7 +61,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
handleUnpinColumn,
handleSetColumnWidth,
handleToggleColumnVisibility,
} = useColumnSettings();
} = useColumnSettings(`${formId}_${formType}_${sheetName}_${direction}`);
useEffect(() => {
// Принудительно обновляем состояние закрепленных колонок

View File

@ -82,7 +82,7 @@ const SettingPanel = ({
<Panel>
<Stack direction="row" sx={{ gap: '1.25rem' }}>
<GroupByObject title="Отображение">
<ColorPickerButton onColorApply={addColorForCells} />
{/* <ColorPickerButton onColorApply={addColorForCells} /> */}
<Tooltip title="Закрепить область">
<IconButton
@ -117,7 +117,7 @@ const SettingPanel = ({
</Tooltip>
</GroupByObject>
<Divider orientation="vertical" flexItem />
{/* <Divider orientation="vertical" flexItem />
<GroupByObject title="Формулы">
<Tooltip title="Добавить формулу">
@ -132,7 +132,7 @@ const SettingPanel = ({
<FormulaSettingPanel />
</IconButton>
</Tooltip>
</GroupByObject>
</GroupByObject> */}
<Divider orientation="vertical" flexItem />
<GroupByObject title="Столбцы">
{columns ? (

View File

@ -3,29 +3,38 @@ import { loadFromStorage, saveToStorage } from '../utils/localStorageUtils.js';
import { getLeafColumns, collectLeafColumns } from '../utils/columnUtils';
import { columnPinningDefault, columnVisibilityDefault } from '../constants/columnConfig.js';
export const useColumnSettings = () => {
export const useColumnSettings = (tableKey) => {
const getStorageKey = (key) => tableKey ? `${tableKey}_${key}` : key;
const [columnSizing, setColumnSizing] = useState(() =>
loadFromStorage('columnSizing', {}),
loadFromStorage(getStorageKey('columnSizing'), {}),
);
const [columnPinning, setColumnPinning] = useState(() =>
loadFromStorage('columnPinning', columnPinningDefault),
loadFromStorage(getStorageKey('columnPinning'), columnPinningDefault),
);
const [columnVisibility, setColumnVisibility] = useState(() =>
loadFromStorage('columnVisibility', columnVisibilityDefault),
loadFromStorage(getStorageKey('columnVisibility'), columnVisibilityDefault),
);
// Сохранение в localStorage
useEffect(() => {
saveToStorage('columnSizing', columnSizing);
}, [columnSizing]);
saveToStorage(getStorageKey('columnSizing'), columnSizing);
}, [columnSizing, tableKey]);
useEffect(() => {
saveToStorage('columnPinning', columnPinning);
}, [columnPinning]);
saveToStorage(getStorageKey('columnPinning'), columnPinning);
}, [columnPinning, tableKey]);
useEffect(() => {
saveToStorage('columnVisibility', columnVisibility);
}, [columnVisibility]);
saveToStorage(getStorageKey('columnVisibility'), columnVisibility);
}, [columnVisibility, tableKey]);
// Сброс настроек при изменении tableKey (опционально)
useEffect(() => {
setColumnSizing(loadFromStorage(getStorageKey('columnSizing'), {}));
setColumnPinning(loadFromStorage(getStorageKey('columnPinning'), columnPinningDefault));
setColumnVisibility(loadFromStorage(getStorageKey('columnVisibility'), columnVisibilityDefault));
}, [tableKey]);
const handlePinColumn = useCallback((colId) => {
setColumnPinning((prev) => ({ ...prev, left: [...prev.left, colId] }));
@ -61,6 +70,13 @@ export const useColumnSettings = () => {
setColumnVisibility((prev) => ({ ...prev, ...leafCols }));
}, []);
// Опционально: функция для сброса всех настроек
const resetAllSettings = useCallback(() => {
setColumnSizing({});
setColumnPinning(columnPinningDefault);
setColumnVisibility(columnVisibilityDefault);
}, []);
return {
columnSizing,
columnPinning,
@ -72,5 +88,7 @@ export const useColumnSettings = () => {
handleUnpinColumn,
handleSetColumnWidth,
handleToggleColumnVisibility,
resetAllSettings, // добавляем функцию сброса
tableKey, // опционально возвращаем tableKey
};
};
};

View File

@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { columns as mockColumns } from '../../mocks/mockTable';
import { config } from './constants/FORM_2/AHR';
import { useParams } from 'react-router';
function EditCellPortal({
cell,
@ -12,6 +13,8 @@ function EditCellPortal({
onError,
isEditable
}) {
const { formId, formType, sheetName, direction } = useParams();
const tableKey = `${formId}_${formType}_${sheetName}_${direction}`;
const ref = useRef(null);
const [refTbody, setRefTbody] = useState(false);
const [isSaving, setIsSaving] = useState(false);
@ -40,7 +43,7 @@ function EditCellPortal({
const errorMsg = 'Не удалось сохранить изменение';
onError?.(errorMsg);
}
}
}
} catch (error) {
console.error('Error updating cell:', error);
onError?.(error.message);
@ -64,6 +67,8 @@ function EditCellPortal({
value={cell.getValue()}
disabled={!isEditable}
onChange={handleChange}
tableId={tableKey}
cellId={cell.row.id + '_' + cell.column.id}
/>,
refTbody,
)}
@ -140,7 +145,7 @@ export const getTableColumns = ({
enableEditing: false,
Cell: ({ cell, row }) => {
return <button onClick={() => { onCellNumberClick(row.id) }}>
{cell.getValue()+ ' '}
{cell.getValue() + ' '}
</button>
},
};

View File

@ -0,0 +1,92 @@
/**
* Проверяет, является ли строка формулой
* @param {string} value - Значение ячейки
* @returns {boolean}
*/
export const isFormula = (value) => {
if (typeof value !== 'string') return false;
return value.startsWith('=');
};
/**
* Извлекает формулу из строки (удаляет знак =)
* @param {string} value - Значение ячейки
* @returns {string} - Формула без знака =
*/
export const extractFormula = (value) => {
if (!isFormula(value)) return value;
return value.substring(1).trim();
};
/**
* Вычисляет математическое выражение
* Поддерживает: +, -, *, /, %, скобки
* @param {string} expression - Математическое выражение
* @returns {number} - Результат вычисления
* @throws {Error} - Если выражение некорректно
*/
export const calculateFormula = (expression) => {
try {
// Удаляем пробелы
const cleanExpr = expression.replace(/\s/g, '');
// Проверяем на допустимые символы (только цифры, операторы, скобки, точка)
const validChars = /^[\d+\-*/%().]+$/;
if (!validChars.test(cleanExpr)) {
throw new Error('Недопустимые символы в формуле');
}
// Заменяем % на /100 для корректного вычисления процентов
const exprWithPercent = cleanExpr.replace(/%/g, '/100');
// Безопасное вычисление с использованием Function
// ВНИМАНИЕ: Это безопасно, так как мы проверяем допустимые символы
const result = new Function(`return (${exprWithPercent})`)();
// Проверяем, что результат - число
if (typeof result !== 'number' || !isFinite(result)) {
throw new Error('Некорректный результат вычисления');
}
// Округляем до 10 знаков после запятой для избежания проблем с плавающей точкой
return Math.round(result * 1e10) / 1e10;
} catch (error) {
console.error('Formula calculation error:', error);
throw new Error(`Ошибка вычисления: ${error.message}`);
}
};
/**
* Вычисляет значение ячейки, если это формула
* @param {string} value - Значение ячейки
* @returns {string|number} - Вычисленное значение или исходное значение
*/
export const evaluateCellValue = (value) => {
if (!isFormula(value)) return value;
try {
const formula = extractFormula(value);
const result = calculateFormula(formula);
return result;
} catch (error) {
console.error('Error evaluating cell:', error);
return '#ОШИБКА';
}
};
/**
* Форматирует число для отображения
* @param {number} value - Число для форматирования
* @returns {string} - Отформатированная строка
*/
export const formatNumber = (value) => {
if (typeof value !== 'number') return String(value);
// Если число целое, отображаем без десятичной части
if (Number.isInteger(value)) {
return value.toString();
}
// Иначе показываем до 10 знаков, но убираем лишние нули
return value.toFixed(10).replace(/\.?0+$/, '');
};