Improve calculator progress tracking and responsive layout
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
This commit is contained in:
parent
3f86d863ac
commit
51853245ad
15 changed files with 329 additions and 75 deletions
|
|
@ -7,7 +7,7 @@ import { CalculatorEntries, FormattedCalculatorNumber } from "./CalculatorEntrie
|
|||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||
import {
|
||||
BASE_TOKEN,
|
||||
calculateExpression,
|
||||
calculateRemainingValue,
|
||||
createStoredFormula,
|
||||
evaluateCalculatorEntries,
|
||||
formatDisplayResult,
|
||||
|
|
@ -15,7 +15,8 @@ import {
|
|||
getDescendantIds,
|
||||
getEditableFormula,
|
||||
getEntriesInTreeOrder,
|
||||
getResultLengthClass
|
||||
getResultLengthClass,
|
||||
parseCalculatorCurrent
|
||||
} from "./calculatorUtils.js";
|
||||
|
||||
function focusExpressionInputAtEnd(element) {
|
||||
|
|
@ -41,6 +42,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
|||
const calculatedData = useMemo(() => ({ ...data, entries: calculatedEntries }), [data, calculatedEntries]);
|
||||
const textContent = context.moduleText?.calculator || {};
|
||||
const [expression, setExpression] = useState("");
|
||||
const [current, setCurrent] = useState("");
|
||||
const [showCurrent, setShowCurrent] = useState(false);
|
||||
const [label, setLabel] = useState("");
|
||||
const [activeParentId, setActiveParentId] = useState("");
|
||||
const [editingEntryId, setEditingEntryId] = useState("");
|
||||
|
|
@ -48,12 +51,13 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
|||
const [copied, setCopied] = useState(false);
|
||||
const calculatorCardRef = useRef(null);
|
||||
const expressionInputRef = useRef(null);
|
||||
const currentInputRef = useRef(null);
|
||||
const pendingExpressionFocusRef = useRef(false);
|
||||
const editingEntry = calculatedEntries.find((entry) => entry.id === editingEntryId);
|
||||
const formulaParentId = editingEntry ? editingEntry.parentId || "" : activeParentId;
|
||||
const formulaParent = calculatedEntries.find((entry) => entry.id === formulaParentId);
|
||||
const formMode = editingEntry ? "edit" : formulaParent ? "child" : "root";
|
||||
const result = useMemo(() => calculateExpression(expression, formulaParent?.value), [formulaParent?.value, expression]);
|
||||
const result = useMemo(() => calculateRemainingValue(expression, current, formulaParent?.value), [current, formulaParent?.value, expression]);
|
||||
const scopeParentId = (parentId = "") => `${moduleId}:${parentId}`;
|
||||
const reorder = useGroupedReorder({
|
||||
namespace: "calculator",
|
||||
|
|
@ -99,13 +103,16 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
|||
if (result == null) return;
|
||||
const cleanLabel = label.trim();
|
||||
const storedFormula = createStoredFormula(expression, formulaParent?.value);
|
||||
const storedCurrent = parseCalculatorCurrent(current);
|
||||
if (storedCurrent == null) return;
|
||||
|
||||
if (editingEntry) {
|
||||
save(calculatedEntries.map((entry) => entry.id === editingEntry.id ? {
|
||||
...entry,
|
||||
label: cleanLabel,
|
||||
value: result,
|
||||
formula: storedFormula
|
||||
formula: storedFormula,
|
||||
current: storedCurrent
|
||||
} : entry));
|
||||
setLabel(cleanLabel);
|
||||
return;
|
||||
|
|
@ -118,10 +125,13 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
|||
parentId: formulaParentId,
|
||||
label: cleanLabel,
|
||||
value: result,
|
||||
formula: storedFormula
|
||||
formula: storedFormula,
|
||||
current: storedCurrent
|
||||
}
|
||||
]);
|
||||
setExpression(formulaParent ? BASE_TOKEN : "");
|
||||
setCurrent("");
|
||||
setShowCurrent(false);
|
||||
setLabel("");
|
||||
}
|
||||
|
||||
|
|
@ -129,6 +139,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
|||
setEditingEntryId(entry.id);
|
||||
setActiveParentId("");
|
||||
setExpression(getEditableFormula(entry));
|
||||
setCurrent(entry.current ? formatResult(entry.current) : "");
|
||||
setShowCurrent(Boolean(entry.current));
|
||||
setLabel(entry.label || "");
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +153,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
|||
parentId: entry.id,
|
||||
label: "",
|
||||
value: Number.isFinite(entry.value) ? entry.value : 0,
|
||||
formula: BASE_TOKEN
|
||||
formula: BASE_TOKEN,
|
||||
current: 0
|
||||
}
|
||||
]);
|
||||
setEditingEntryId("");
|
||||
|
|
@ -149,6 +162,8 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
|||
setEditingEntryId(childId);
|
||||
setActiveParentId("");
|
||||
setExpression(BASE_TOKEN);
|
||||
setCurrent("");
|
||||
setShowCurrent(false);
|
||||
setLabel("");
|
||||
}
|
||||
|
||||
|
|
@ -162,12 +177,16 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
|||
setEditingEntryId("");
|
||||
setLabel("");
|
||||
setExpression("");
|
||||
setCurrent("");
|
||||
setShowCurrent(false);
|
||||
}
|
||||
save(calculatedEntries.filter((entry) => !deletedIds.has(entry.id)));
|
||||
}
|
||||
|
||||
function resetCalculator() {
|
||||
setExpression("");
|
||||
setCurrent("");
|
||||
setShowCurrent(false);
|
||||
setLabel("");
|
||||
setActiveParentId("");
|
||||
setEditingEntryId("");
|
||||
|
|
@ -177,9 +196,21 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
|||
setActiveParentId("");
|
||||
setEditingEntryId("");
|
||||
setExpression("");
|
||||
setCurrent("");
|
||||
setShowCurrent(false);
|
||||
setLabel("");
|
||||
}
|
||||
|
||||
function toggleCurrent() {
|
||||
if (showCurrent) {
|
||||
setCurrent("");
|
||||
setShowCurrent(false);
|
||||
return;
|
||||
}
|
||||
setShowCurrent(true);
|
||||
window.requestAnimationFrame(() => currentInputRef.current?.focus());
|
||||
}
|
||||
|
||||
async function copyChecklistImport() {
|
||||
const text = getEntriesInTreeOrder(calculatedEntries)
|
||||
.map((entry) => `${entry.label || formatResult(entry.value)}:${formatResult(entry.value)}`)
|
||||
|
|
@ -201,61 +232,91 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
|
|||
<div className="calculator-card" ref={calculatorCardRef}>
|
||||
<div className="calculator-field">
|
||||
<span>{textContent.expressionLabel || "Calcul"}</span>
|
||||
{formulaParent ? (
|
||||
<MixTagsInput
|
||||
aria-label={textContent.expressionLabel || "Calcul"}
|
||||
className="calculator-expression-input"
|
||||
inputRef={expressionInputRef}
|
||||
onChange={setExpression}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
event.preventDefault();
|
||||
event.currentTarget.closest("form")?.requestSubmit();
|
||||
}}
|
||||
formulaSpacing
|
||||
insertToken={{
|
||||
value: "base",
|
||||
label: textContent.baseTokenLabel || "Valeur du parent",
|
||||
icon: "parent",
|
||||
title: textContent.insertBaseTitle || "Ajouter la valeur du parent"
|
||||
}}
|
||||
placeholder={textContent.formulaPlaceholder || "base * 3"}
|
||||
tokens={[{
|
||||
value: "base",
|
||||
label: textContent.baseTokenLabel || "Valeur du parent"
|
||||
}]}
|
||||
value={expression}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
aria-label={textContent.expressionLabel || "Calcul"}
|
||||
className="calculator-expression-input nds-input"
|
||||
<div className={`calculator-expression-row ${showCurrent ? "has-current" : ""}`}>
|
||||
{showCurrent && <span className="calculator-total-label">{textContent.totalLabel || "Total"}</span>}
|
||||
{formulaParent ? (
|
||||
<MixTagsInput
|
||||
aria-label={textContent.expressionLabel || "Calcul"}
|
||||
className="calculator-expression-input"
|
||||
inputRef={expressionInputRef}
|
||||
onChange={setExpression}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
event.preventDefault();
|
||||
event.currentTarget.closest("form")?.requestSubmit();
|
||||
}}
|
||||
formulaSpacing
|
||||
insertToken={{
|
||||
value: "base",
|
||||
label: textContent.baseTokenLabel || "Valeur du parent",
|
||||
icon: "parent",
|
||||
title: textContent.insertBaseTitle || "Ajouter la valeur du parent"
|
||||
}}
|
||||
placeholder={textContent.formulaPlaceholder || "base * 3"}
|
||||
tokens={[{
|
||||
value: "base",
|
||||
label: textContent.baseTokenLabel || "Valeur du parent"
|
||||
}]}
|
||||
value={expression}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
aria-label={textContent.expressionLabel || "Calcul"}
|
||||
className="calculator-expression-input nds-input"
|
||||
data-size="sm"
|
||||
ref={expressionInputRef}
|
||||
onChange={(event) => setExpression(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
event.preventDefault();
|
||||
event.currentTarget.closest("form")?.requestSubmit();
|
||||
}}
|
||||
inputMode="decimal"
|
||||
placeholder={textContent.expressionPlaceholder || "10*10"}
|
||||
value={expression}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
className="calculator-current-toggle nds-icon-button"
|
||||
data-size="sm"
|
||||
ref={expressionInputRef}
|
||||
onChange={(event) => setExpression(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Enter") return;
|
||||
event.preventDefault();
|
||||
event.currentTarget.closest("form")?.requestSubmit();
|
||||
}}
|
||||
inputMode="decimal"
|
||||
placeholder={textContent.expressionPlaceholder || "10*10"}
|
||||
value={expression}
|
||||
/>
|
||||
)}
|
||||
type="button"
|
||||
onClick={toggleCurrent}
|
||||
aria-label={textContent.subtractCurrentTitle || "Soustraire l'actuel"}
|
||||
aria-pressed={showCurrent}
|
||||
title={textContent.subtractCurrentTitle || "Soustraire l'actuel"}
|
||||
>
|
||||
<span aria-hidden="true">−</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showCurrent && (
|
||||
<div className="calculator-current-field">
|
||||
<span>{textContent.currentLabel || "Actuel"}</span>
|
||||
<input
|
||||
aria-label={textContent.currentLabel || "Actuel"}
|
||||
className="nds-input"
|
||||
data-size="sm"
|
||||
ref={currentInputRef}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={current}
|
||||
onChange={(event) => setCurrent(event.target.value)}
|
||||
placeholder={textContent.currentPlaceholder || "0"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={`calculator-result ${result == null ? "" : getResultLengthClass(result)}`} aria-live="polite">
|
||||
<span>{textContent.resultLabel || "Résultat"}</span>
|
||||
<strong aria-label={result == null ? "-" : formatDisplayResult(result)}>
|
||||
<FormattedCalculatorNumber value={result} />
|
||||
</strong>
|
||||
</div>
|
||||
<label>
|
||||
<label className="calculator-label-field">
|
||||
<span>{textContent.labelLabel || "Libellé"}</span>
|
||||
<input className="nds-input" data-size="sm" type="text" value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
|
||||
</label>
|
||||
<button className="nds-button" data-size="sm" data-variant="primary" disabled={result == null}>{formMode === "edit" ? textContent.updateButton || "Mettre à jour" : textContent.saveButton || "Enregistrer"}</button>
|
||||
<button className="calculator-save-button nds-button" data-size="sm" data-variant="primary" disabled={result == null}>{formMode === "edit" ? textContent.updateButton || "Mettre à jour" : textContent.saveButton || "Enregistrer"}</button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -16,6 +16,20 @@ export function calculateExpression(expression, baseValue = null) {
|
|||
}
|
||||
}
|
||||
|
||||
export function parseCalculatorCurrent(value) {
|
||||
const normalized = String(value ?? "").replaceAll(",", ".").trim();
|
||||
if (!normalized) return 0;
|
||||
const parsed = Number(normalized);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
export function calculateRemainingValue(expression, currentValue = 0, baseValue = null) {
|
||||
const total = calculateExpression(expression, baseValue);
|
||||
const current = parseCalculatorCurrent(currentValue);
|
||||
if (total == null || current == null) return null;
|
||||
return total - current;
|
||||
}
|
||||
|
||||
export function formatResult(value) {
|
||||
if (!Number.isFinite(value)) return "";
|
||||
return Number.parseFloat(value.toFixed(6)).toString();
|
||||
|
|
@ -58,7 +72,8 @@ export function getEntryFormula(entry) {
|
|||
}
|
||||
|
||||
export function getEditableFormula(entry) {
|
||||
return getEntryFormula(entry) || formatResult(entry.value);
|
||||
const current = Number(entry?.current);
|
||||
return getEntryFormula(entry) || formatResult(entry.value + (Number.isFinite(current) ? current : 0));
|
||||
}
|
||||
|
||||
export function createStoredFormula(expression, parentValue) {
|
||||
|
|
@ -82,7 +97,7 @@ export function evaluateCalculatorEntries(entries) {
|
|||
function visit(parentId = "", parentValue = null) {
|
||||
return (byParent.get(parentId) || []).flatMap((entry) => {
|
||||
const formula = getEntryFormula(entry);
|
||||
const calculatedValue = formula ? calculateExpression(formula, parentValue) : entry.value;
|
||||
const calculatedValue = formula ? calculateRemainingValue(formula, entry.current, parentValue) : entry.value;
|
||||
const nextEntry = { ...entry, value: calculatedValue ?? entry.value };
|
||||
return [nextEntry, ...visit(entry.id, nextEntry.value)];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -244,7 +244,8 @@ export function exportCalculatorText(data) {
|
|||
function visit(parentId = "", depth = 0) {
|
||||
entries.filter((entry) => (entry.parentId || "") === parentId).forEach((entry) => {
|
||||
const formula = String(entry.formula || "").trim();
|
||||
lines.push(`${" ".repeat(depth)}${entry.label || ""}: ${entry.value}${formula ? ` | formula=${formula}` : ""}`);
|
||||
const current = Number(entry.current) || 0;
|
||||
lines.push(`${" ".repeat(depth)}${entry.label || ""}: ${entry.value}${formula ? ` | formula=${formula}` : ""}${current ? ` | current=${current}` : ""}`);
|
||||
visit(entry.id, depth + 1);
|
||||
});
|
||||
}
|
||||
|
|
@ -268,10 +269,23 @@ export function importCalculatorText(text, context) {
|
|||
const separatorIndex = line.indexOf(":");
|
||||
if (separatorIndex < 0) throw lineError(index, `ligne calculateur invalide: ${line.trim()}`);
|
||||
const label = line.slice(0, separatorIndex).trim();
|
||||
const [rawValue, rawFormula = ""] = line.slice(separatorIndex + 1).split(/\s+\|\s+formula=/);
|
||||
const [rawValue, ...rawMetadata] = line.slice(separatorIndex + 1).split(/\s+\|\s+/);
|
||||
const value = parseNumber(rawValue, Number.NaN);
|
||||
if (!Number.isFinite(value)) throw lineError(index, `valeur calculateur invalide: ${line.trim()}`);
|
||||
const entry = { id: context.uid("calc"), parentId: parentStack[depth - 1] || "", label, value, formula: rawFormula.trim() };
|
||||
let formula = "";
|
||||
let current = 0;
|
||||
rawMetadata.forEach((metadata) => {
|
||||
if (metadata.startsWith("formula=")) {
|
||||
formula = metadata.slice("formula=".length).trim();
|
||||
return;
|
||||
}
|
||||
if (metadata.startsWith("current=")) {
|
||||
current = parseNumber(metadata.slice("current=".length), Number.NaN);
|
||||
if (Number.isFinite(current)) return;
|
||||
}
|
||||
throw lineError(index, `option calculateur invalide: ${metadata.trim()}`);
|
||||
});
|
||||
const entry = { id: context.uid("calc"), parentId: parentStack[depth - 1] || "", label, value, formula, current };
|
||||
entries.push(entry);
|
||||
parentStack[depth] = entry.id;
|
||||
parentStack.length = depth + 1;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ export function normalizeCalculatorData(data) {
|
|||
parentId: String(entry?.parentId || ""),
|
||||
label: String(entry?.label || "").trim(),
|
||||
value: normalizeCalculatorValue(entry?.value),
|
||||
formula: normalizeCalculatorFormula(entry?.formula)
|
||||
formula: normalizeCalculatorFormula(entry?.formula),
|
||||
current: normalizeCalculatorValue(entry?.current)
|
||||
}));
|
||||
const entryIds = new Set(entries.map((entry) => entry.id));
|
||||
return {
|
||||
|
|
@ -32,6 +33,7 @@ export function compactCalculatorDataForStorage(value) {
|
|||
const compact = { id: entry.id, label: entry.label, value: entry.value };
|
||||
if (entry.parentId) compact.parentId = entry.parentId;
|
||||
if (entry.formula) compact.formula = entry.formula;
|
||||
if (entry.current) compact.current = entry.current;
|
||||
return compact;
|
||||
});
|
||||
if (!entries.length && !normalized.scrollResults) return null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue