// Rôle : fournit l'outil calculateur avec résultats enregistrés en arborescence. import { useLayoutEffect, useMemo, useRef, useState } from "react"; import { Icon } from "../../../components/Icon.jsx"; import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js"; function calculateExpression(expression) { const normalized = String(expression || "").replaceAll(",", ".").trim(); if (!normalized || !/^[\d\s+\-*/().]+$/.test(normalized)) return null; try { const value = Function(`"use strict"; return (${normalized})`)(); return Number.isFinite(value) ? value : null; } catch { return null; } } function formatResult(value) { if (!Number.isFinite(value)) return ""; return Number.parseFloat(value.toFixed(6)).toString(); } function getChildren(entries, parentId) { return entries.filter((entry) => (entry.parentId || "") === parentId); } function getDescendantIds(entries, parentId) { const children = getChildren(entries, parentId); return children.flatMap((child) => [child.id, ...getDescendantIds(entries, child.id)]); } function getEntriesInTreeOrder(entries, parentId = "") { return getChildren(entries, parentId).flatMap((entry) => [entry, ...getEntriesInTreeOrder(entries, entry.id)]); } export function CalculatorModule({ toolboxId, moduleId, context }) { const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] })); const textContent = context.moduleText?.calculator || {}; const [expression, setExpression] = useState(""); const [label, setLabel] = useState(""); const [activeParentId, setActiveParentId] = useState(""); const [calculatorHeight, setCalculatorHeight] = useState(0); const [copied, setCopied] = useState(false); const calculatorCardRef = useRef(null); const result = useMemo(() => calculateExpression(expression), [expression]); const activeParent = data.entries.find((entry) => entry.id === activeParentId); const scopeParentId = (parentId = "") => `${moduleId}:${parentId}`; const reorder = useGroupedReorder({ namespace: "calculator", items: data.entries, getItemId: (entry) => entry.id, getParentId: (entry) => scopeParentId(entry.parentId || ""), onItemMove: (operation) => { save(moveItem(data.entries, 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(); }, []); function save(entries) { context.setModuleData(toolboxId, moduleId, { ...data, entries }); } function setScrollResults(scrollResults) { context.setModuleData(toolboxId, moduleId, { ...data, scrollResults }); } function saveResult(event) { event.preventDefault(); if (result == null) return; const cleanLabel = label.trim(); save([ ...data.entries, { id: context.uid("calc"), parentId: activeParentId, label: cleanLabel, value: result } ]); setExpression(""); setLabel(""); } function useEntry(entry) { setExpression(formatResult(entry.value)); setActiveParentId(entry.id); } function deleteEntry(entryId) { const deletedIds = new Set([entryId, ...getDescendantIds(data.entries, entryId)]); if (deletedIds.has(activeParentId)) setActiveParentId(""); save(data.entries.filter((entry) => !deletedIds.has(entry.id))); } function renameEntry(entryId, label) { const cleanLabel = label.trim(); save(data.entries.map((entry) => entry.id === entryId ? { ...entry, label: cleanLabel } : entry)); } function resetCalculator() { setExpression(""); setLabel(""); setActiveParentId(""); } async function copyChecklistImport() { const text = getEntriesInTreeOrder(data.entries) .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é."}
)}