Refactor large toolbox modules
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s
This commit is contained in:
parent
c2e37ad3f2
commit
6dd64f87c3
19 changed files with 2042 additions and 1659 deletions
287
website/src/features/toolboxes/modules/useGoalTreeController.js
Normal file
287
website/src/features/toolboxes/modules/useGoalTreeController.js
Normal 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
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue