Refactor large toolbox modules
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s

This commit is contained in:
Shinuwa 2026-08-25 08:20:56 +02:00
parent c2e37ad3f2
commit 6dd64f87c3
19 changed files with 2042 additions and 1659 deletions

View file

@ -0,0 +1,287 @@
// 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 data = context.normalizeGoalTreeData(context.getModuleData(toolboxId, moduleId, { nodes: [] }));
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
};
}