// Rôle : fournit l'outil calculateur avec résultats enregistrés en arborescence et échange texte. import { useLayoutEffect, useMemo, useRef, useState } from "react"; import { Icon } from "../../../components/Icon.jsx"; import { MixTagsInput } from "../../../components/MixTagsInput.jsx"; import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js"; import { CalculatorEntries, FormattedCalculatorNumber } from "./CalculatorEntries.jsx"; import { TextExchangeActions } from "./TextExchangeActions.jsx"; import { BASE_TOKEN, calculateRemainingValue, createStoredFormula, evaluateCalculatorEntries, formatDisplayResult, formatResult, getDescendantIds, getEditableFormula, getEntriesInTreeOrder, getResultLengthClass, parseCalculatorCurrent } from "./calculatorUtils.js"; function focusExpressionInputAtEnd(element) { if (!element) return; element.focus(); if (typeof element.setSelectionRange === "function") { const length = element.value?.length || 0; element.setSelectionRange(length, length); return; } if (!element.isContentEditable) return; const range = document.createRange(); range.selectNodeContents(element); range.collapse(false); const selection = window.getSelection(); selection?.removeAllRanges(); selection?.addRange(range); } export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpen }) { const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] })); const calculatedEntries = useMemo(() => evaluateCalculatorEntries(data.entries), [data.entries]); 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(""); const [calculatorHeight, setCalculatorHeight] = useState(0); 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(() => calculateRemainingValue(expression, current, formulaParent?.value), [current, formulaParent?.value, expression]); const scopeParentId = (parentId = "") => `${moduleId}:${parentId}`; const reorder = useGroupedReorder({ namespace: "calculator", items: calculatedEntries, getItemId: (entry) => entry.id, getParentId: (entry) => scopeParentId(entry.parentId || ""), onItemMove: (operation) => { save(moveItem(calculatedEntries, operation.sourceId, operation.targetId, operation.placement)); }, hierarchy: { enabled: true, stickyParents: true } }); useLayoutEffect(() => { const element = calculatorCardRef.current; if (!element) return undefined; function updateHeight() { setCalculatorHeight(Math.ceil(element.getBoundingClientRect().height)); } updateHeight(); const observer = new ResizeObserver(updateHeight); observer.observe(element); return () => observer.disconnect(); }, []); useLayoutEffect(() => { if (!pendingExpressionFocusRef.current || !editingEntryId) return; pendingExpressionFocusRef.current = false; window.requestAnimationFrame(() => focusExpressionInputAtEnd(expressionInputRef.current)); }, [editingEntryId]); function save(entries) { context.setModuleData(toolboxId, moduleId, { ...data, entries: evaluateCalculatorEntries(entries) }); } function setScrollResults(scrollResults) { context.setModuleData(toolboxId, moduleId, { ...data, scrollResults }); } function saveResult(event) { event.preventDefault(); 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, current: storedCurrent } : entry)); setLabel(cleanLabel); return; } save([ ...data.entries, { id: context.uid("calc"), parentId: formulaParentId, label: cleanLabel, value: result, formula: storedFormula, current: storedCurrent } ]); setExpression(formulaParent ? BASE_TOKEN : ""); setCurrent(""); setShowCurrent(false); setLabel(""); } function editEntry(entry) { setEditingEntryId(entry.id); setActiveParentId(""); setExpression(getEditableFormula(entry)); setCurrent(entry.current ? formatResult(entry.current) : ""); setShowCurrent(Boolean(entry.current)); setLabel(entry.label || ""); } function createChildEntry(entry) { const childId = context.uid("calc"); save([ ...data.entries, { id: childId, parentId: entry.id, label: "", value: Number.isFinite(entry.value) ? entry.value : 0, formula: BASE_TOKEN, current: 0 } ]); setEditingEntryId(""); pendingExpressionFocusRef.current = true; setEditingEntryId(childId); setActiveParentId(""); setExpression(BASE_TOKEN); setCurrent(""); setShowCurrent(false); setLabel(""); } function deleteEntry(entryId) { const deletedIds = new Set([entryId, ...getDescendantIds(calculatedEntries, entryId)]); if (deletedIds.has(activeParentId)) { setActiveParentId(""); setExpression(""); } if (deletedIds.has(editingEntryId)) { setEditingEntryId(""); setLabel(""); setExpression(""); setCurrent(""); setShowCurrent(false); } save(calculatedEntries.filter((entry) => !deletedIds.has(entry.id))); } function resetCalculator() { setExpression(""); setCurrent(""); setShowCurrent(false); setLabel(""); setActiveParentId(""); setEditingEntryId(""); } function returnToRoot() { 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)}`) .join("\n"); if (!text || !await context.copyText(text)) return; setCopied(true); context.notify?.(textContent.copiedTitle || "Copié"); window.setTimeout(() => setCopied(false), 1400); } return (
{textContent.emptyResults || "Aucun résultat enregistré."}
)}