формула для исполнителей
This commit is contained in:
parent
d2b1b3a4d0
commit
bafae2ca58
@ -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;
|
||||
@ -61,7 +61,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
||||
handleUnpinColumn,
|
||||
handleSetColumnWidth,
|
||||
handleToggleColumnVisibility,
|
||||
} = useColumnSettings();
|
||||
} = useColumnSettings(`${formId}_${formType}_${sheetName}_${direction}`);
|
||||
|
||||
useEffect(() => {
|
||||
// Принудительно обновляем состояние закрепленных колонок
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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
|
||||
};
|
||||
};
|
||||
};
|
||||
@ -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>
|
||||
},
|
||||
};
|
||||
|
||||
92
web/src/components/RealtimeTable/utils/formulaUtils.js
Normal file
92
web/src/components/RealtimeTable/utils/formulaUtils.js
Normal 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+$/, '');
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user