288 lines
10 KiB
JavaScript
288 lines
10 KiB
JavaScript
// 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
|
|
};
|
|
}
|