// Rôle : orchestre l'état, les mutations, les resets et le reorder de l'arbre d'objectifs. import { useCallback, useEffect, useMemo, useState } from "react"; import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js"; import { GOAL_TREE_VIEW_SCALE, KIND_ICONS, getDefaultGoalTreeItem, getEffectiveGoalTreeProgressMode, getGoalTreeConvergenceKeepOpenIds, getGoalTreeLinkedControlIds, isSingleValueGoalTreeNode } from "./goalTreeDisplay.js"; import { applyGoalTreeDueResets, evaluateGoalTreeNodes, getGoalTreeChildren, getGoalTreeDescendantIds, getGoalTreeParentIds, getGoalTreeParentMap } from "./goalTreeUtils.js"; export function useGoalTreeController({ toolboxId, moduleId, context, editing, setEditing, openEditor }) { const rawData = context.getModuleData(toolboxId, moduleId, { nodes: [] }); const data = useMemo(() => context.normalizeGoalTreeData(rawData), [context, rawData]); const textContent = context.moduleText?.goalTree || {}; const treeOrientation = data.orientation || "horizontal"; const verticalView = treeOrientation === "vertical"; const reorderOrientation = treeOrientation === "horizontal" ? "vertical" : "horizontal"; const viewScale = editing ? GOAL_TREE_VIEW_SCALE : 1; const evaluatedNodes = useMemo(() => evaluateGoalTreeNodes(data.nodes), [data.nodes]); const evaluatedData = useMemo(() => ({ ...data, nodes: evaluatedNodes }), [data, evaluatedNodes]); const evaluatedNodeMap = useMemo(() => new Map(evaluatedNodes.map((node) => [node.id, node])), [evaluatedNodes]); const [selectedNodeId, setSelectedNodeId] = useState(() => data.nodes[0]?.id || ""); const [activeFormTab, setActiveFormTab] = useState("base"); const selectedNode = evaluatedNodes.find((node) => node.id === selectedNodeId) || evaluatedNodes[0] || null; const selectedNodeIsStep = selectedNode?.kind === "step"; const selectedNodeChildren = selectedNode ? getGoalTreeChildren(evaluatedNodes, selectedNode.id) : []; const selectedProgressMode = selectedNode ? getEffectiveGoalTreeProgressMode(selectedNode, selectedNodeChildren) : "auto"; const formTabs = [ { id: "base", label: textContent.baseTab || "Base", icon: "cube" }, ...(!selectedNodeIsStep ? [{ id: "content", label: textContent.contentTab || "Contenu", icon: "notepad" }] : []), { id: "advanced", label: textContent.advancedTab || "Avancé", icon: "settings" } ]; const parentMap = useMemo(() => getGoalTreeParentMap(evaluatedNodes), [evaluatedNodes]); const save = useCallback((nextData) => { context.setModuleData(toolboxId, moduleId, nextData, "goalTree"); }, [context, moduleId, toolboxId]); const reorder = useGroupedReorder({ namespace: "goal-tree", items: evaluatedNodes, orientation: reorderOrientation, getItemId: (node) => node.id, getParentId: (node) => parentMap.get(node.id) || "", onItemMove: (operation) => save({ ...data, nodes: moveItem(data.nodes, operation.sourceId, operation.targetId, operation.placement) }), hierarchy: { enabled: true, stickyParents: true } }); useEffect(() => { const resetData = applyGoalTreeDueResets(data, Date.now()); if (resetData !== data) save(resetData); }, [data, save]); useEffect(() => { if (!selectedNodeId && data.nodes[0]) setSelectedNodeId(data.nodes[0].id); if (selectedNodeId && !data.nodes.some((node) => node.id === selectedNodeId)) setSelectedNodeId(data.nodes[0]?.id || ""); }, [data.nodes, selectedNodeId]); useEffect(() => { if (activeFormTab === "content" && selectedNodeIsStep) setActiveFormTab("base"); }, [activeFormTab, selectedNodeIsStep]); function addNode(parentId = "") { const parent = data.nodes.find((node) => node.id === parentId); const node = { id: context.uid("goal"), parentId, title: parent ? textContent.defaultChildTitle || "Nouvelle étape" : textContent.defaultRootTitle || "Nouvel objectif", kind: "step", icon: "cube", color: "gold", qtyTarget: 1, qtyCurrent: 0, formula: parent ? "base" : "", resetType: "none", resetTime: "00:00", weeklyResetDay: 1, items: [], checkedAt: 0, collapsed: false }; save({ ...data, nodes: [...data.nodes, node] }); setSelectedNodeId(node.id); } function editNode(nodeId) { const node = data.nodes.find((item) => item.id === nodeId); if (!node) return; setSelectedNodeId(nodeId); setActiveFormTab(node.kind === "step" ? "base" : "content"); openEditor(); } function addNodeContentItem(nodeId) { const node = data.nodes.find((item) => item.id === nodeId); if (!node || node.kind === "step") { addNode(nodeId); return; } setSelectedNodeId(nodeId); setActiveFormTab("content"); openEditor(); addNodeItem(nodeId); } function updateNode(nodeId, updater) { save({ ...data, nodes: data.nodes.map((node) => node.id === nodeId ? updater(node) : node) }); } function toggleBranch(nodeId) { const target = data.nodes.find((node) => node.id === nodeId); if (!target) return; const nextCollapsed = !target.collapsed; const keepOpenIds = target.collapsed ? getGoalTreeConvergenceKeepOpenIds(data.nodes, nodeId) : new Set(); const linkedControlIds = getGoalTreeLinkedControlIds(data.nodes, nodeId); const siblingIds = data.nodes .filter((node) => node.parentId === target.parentId && node.id !== nodeId) .map((node) => node.id); const closedSiblingIds = new Set(siblingIds.flatMap((id) => [id, ...getGoalTreeDescendantIds(data.nodes, id)])); save({ ...data, nodes: data.nodes.map((node) => { if (linkedControlIds.has(node.id)) return { ...node, collapsed: nextCollapsed }; if (!target.collapsed) return node; if (keepOpenIds.has(node.id)) return { ...node, collapsed: false }; return closedSiblingIds.has(node.id) ? { ...node, collapsed: true } : node; }) }); } function deleteNode(nodeId) { const deletedIds = new Set([nodeId, ...getGoalTreeDescendantIds(data.nodes, nodeId)]); save({ ...data, nodes: data.nodes.filter((node) => !deletedIds.has(node.id)) }); if (deletedIds.has(selectedNodeId)) setSelectedNodeId(data.nodes.find((node) => !deletedIds.has(node.id))?.id || ""); } function setStepTarget(nodeId, value) { const target = String(value || "").replaceAll(",", ".").trim(); const numericTarget = Number(target); updateNode(nodeId, (node) => { if (target && Number.isFinite(numericTarget)) { return { ...node, qtyTarget: Math.max(0, numericTarget), formula: "" }; } return { ...node, formula: target, qtyTarget: target ? node.qtyTarget : 1 }; }); } function setKind(nodeId, kind) { updateNode(nodeId, (node) => ({ ...node, kind, icon: kind === "step" ? node.icon || "cube" : KIND_ICONS[kind], color: kind === "step" ? node.color || "gold" : node.color || "gold", qtyTarget: kind === "step" ? node.qtyTarget : 1, qtyCurrent: kind === "step" ? node.qtyCurrent : 0, formula: kind === "step" ? node.formula : "", items: kind === "step" ? [] : (node.items || []).map((item) => kind === "checklist" ? { ...item, formula: item.formula || (node.parentId ? "base" : "1"), qtyTarget: item.qtyTarget || 1, qtyCurrent: item.qtyCurrent || 0 } : { id: item.id, label: item.label }) })); } function addNodeItem(nodeId) { updateNode(nodeId, (node) => ({ ...node, items: [...(node.items || []), getDefaultGoalTreeItem(node.kind, context, textContent, node.parentId)] })); } function setStepLeafQuantity(nodeId, quantity) { updateNode(nodeId, (node) => ({ ...node, qtyCurrent: quantity, checkedAt: quantity > 0 ? Date.now() : 0 })); } function setStepChildrenCompletion(nodeId, checked) { const childIds = new Set(getGoalTreeChildren(evaluatedNodes, nodeId).filter(isSingleValueGoalTreeNode).map((node) => node.id)); if (!childIds.size) return; save({ ...data, nodes: data.nodes.map((node) => { if (!childIds.has(node.id)) return node; const evaluatedNode = evaluatedNodeMap.get(node.id) || node; if (node.kind === "checklist") { const evaluatedItems = new Map((evaluatedNode.items || []).map((item) => [item.id, item])); return { ...node, items: (node.items || []).map((item) => { const evaluatedItem = evaluatedItems.get(item.id) || item; const target = Math.max(1, evaluatedItem.qtyTarget || item.qtyTarget || 1); return { ...item, qtyCurrent: checked ? target : 0 }; }), checkedAt: checked ? Date.now() : 0 }; } const target = Math.max(1, evaluatedNode.computedQtyTarget || node.qtyTarget || 1); return { ...node, qtyCurrent: checked ? target : 0, checkedAt: checked ? Date.now() : 0 }; }) }); } function updateNodeItem(nodeId, itemId, updater) { updateNode(nodeId, (node) => ({ ...node, items: (node.items || []).map((item) => item.id === itemId ? updater(item) : item) })); } function deleteNodeItem(nodeId, itemId) { updateNode(nodeId, (node) => ({ ...node, items: (node.items || []).filter((item) => item.id !== itemId) })); } function importData(nextData) { save(nextData); setSelectedNodeId(nextData.nodes[0]?.id || ""); } function toggleTreeOrientation() { save({ ...data, orientation: verticalView ? "horizontal" : "vertical" }); } function toggleLinkedParent(nodeId, parentId) { updateNode(nodeId, (node) => { const parentIds = new Set(getGoalTreeParentIds(node)); if (parentIds.has(parentId)) parentIds.delete(parentId); else parentIds.add(parentId); if (node.parentId) parentIds.add(node.parentId); return { ...node, parentIds: [...parentIds] }; }); } return { data, evaluatedData, evaluatedNodes, textContent, treeOrientation, verticalView, viewScale, selectedNode, selectedNodeIsStep, selectedProgressMode, formTabs, activeFormTab, reorder, storageReady: context.storageReady !== false, setEditing, setSelectedNodeId, setActiveFormTab, addNode, editNode, addNodeContentItem, updateNode, toggleBranch, deleteNode, setStepTarget, setKind, addNodeItem, setStepLeafQuantity, setStepChildrenCompletion, updateNodeItem, deleteNodeItem, importData, toggleTreeOrientation, toggleLinkedParent }; }