Add goal tree toolbox module
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s
This commit is contained in:
parent
e4cbe3e087
commit
59fdcb0927
63 changed files with 4062 additions and 252 deletions
163
website/src/features/toolboxes/storage/modules/goalTree.js
Normal file
163
website/src/features/toolboxes/storage/modules/goalTree.js
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
// Rôle : normalise et compacte les données persistées de l'arbre d'objectifs.
|
||||
import { normalizeTimestamp, uid } from "./shared.js";
|
||||
|
||||
export const DEFAULT_GOAL_TREE_WEEKLY_RESET_DAY = 1;
|
||||
export const DEFAULT_GOAL_TREE_RESET_TIME = "00:00";
|
||||
const NODE_KINDS = new Set(["step", "text", "checklist"]);
|
||||
const NODE_ICONS = new Set(["cube", "fire", "flask", "grass", "leaf", "meat", "powder", "tear", "ball"]);
|
||||
const NODE_COLORS = new Set(["gold", "cyan", "violet", "pink", "green", "gray", "red", "orange", "yellow", "blue", "indigo"]);
|
||||
const RESET_TYPES = new Set(["none", "daily", "weekly"]);
|
||||
const ORIENTATIONS = new Set(["horizontal", "vertical"]);
|
||||
const PROGRESS_MODES = new Set(["auto", "manual"]);
|
||||
|
||||
function isClockTimeString(value) {
|
||||
if (!/^\d{2}:\d{2}$/.test(String(value || ""))) return false;
|
||||
const [hours, minutes] = String(value).split(":").map(Number);
|
||||
return Number.isInteger(hours) && Number.isInteger(minutes) && hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59;
|
||||
}
|
||||
|
||||
function normalizeWeekDay(value, fallback = DEFAULT_GOAL_TREE_WEEKLY_RESET_DAY) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 6 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function normalizeQuantity(value, fallback = 0) {
|
||||
const parsed = Number(String(value ?? "").replace(",", "."));
|
||||
return Number.isFinite(parsed) ? Math.max(0, parsed) : fallback;
|
||||
}
|
||||
|
||||
function normalizeFormula(value) {
|
||||
return String(value || "").replaceAll(",", ".").trim();
|
||||
}
|
||||
|
||||
function normalizeNodeItems(kind, items) {
|
||||
if (!["text", "checklist"].includes(kind)) return [];
|
||||
return (Array.isArray(items) ? items : [])
|
||||
.map((item) => {
|
||||
const label = String(item?.label || item?.text || "");
|
||||
const normalized = { id: item?.id || uid("goalItem"), label };
|
||||
if (kind === "checklist") {
|
||||
normalized.formula = normalizeFormula(item?.formula || item?.qty || item?.qtyTarget || "1") || "1";
|
||||
normalized.qtyTarget = normalizeQuantity(item?.qtyTarget, item?.checked === true ? 1 : 1) || 1;
|
||||
normalized.qtyCurrent = normalizeQuantity(item?.qtyCurrent, item?.checked === true ? 1 : 0);
|
||||
}
|
||||
return normalized;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeParentIds(value) {
|
||||
return [...new Set((Array.isArray(value) ? value : [])
|
||||
.map((id) => String(id || "").trim())
|
||||
.filter(Boolean))];
|
||||
}
|
||||
|
||||
function normalizeGoalTreeNode(node) {
|
||||
const title = String(node?.title || "") || "Objectif";
|
||||
const kind = NODE_KINDS.has(node?.kind) ? node.kind : "step";
|
||||
const resetType = RESET_TYPES.has(node?.resetType) ? node.resetType : "none";
|
||||
const resetTime = isClockTimeString(node?.resetTime) ? node.resetTime : DEFAULT_GOAL_TREE_RESET_TIME;
|
||||
const isStep = kind === "step";
|
||||
return {
|
||||
id: node?.id || uid("goal"),
|
||||
parentId: String(node?.parentId || ""),
|
||||
parentIds: normalizeParentIds(node?.parentIds),
|
||||
title,
|
||||
kind,
|
||||
icon: NODE_ICONS.has(node?.icon) ? node.icon : "cube",
|
||||
color: NODE_COLORS.has(node?.color) ? node.color : "gold",
|
||||
qtyTarget: isStep ? normalizeQuantity(node?.qtyTarget, 1) || 1 : 1,
|
||||
qtyCurrent: isStep ? normalizeQuantity(node?.qtyCurrent, 0) : 0,
|
||||
formula: isStep ? normalizeFormula(node?.formula) : "",
|
||||
progressMode: isStep && PROGRESS_MODES.has(node?.progressMode) ? node.progressMode : "",
|
||||
items: normalizeNodeItems(kind, node?.items),
|
||||
resetType,
|
||||
resetTime,
|
||||
weeklyResetDay: normalizeWeekDay(node?.weeklyResetDay),
|
||||
checkedAt: normalizeTimestamp(node?.checkedAt),
|
||||
collapsed: node?.collapsed === true
|
||||
};
|
||||
}
|
||||
|
||||
function clearInvalidParents(nodes) {
|
||||
const nodeIds = new Set(nodes.map((node) => node.id));
|
||||
const parentById = new Map(nodes.map((node) => [node.id, nodeIds.has(node.parentId) ? node.parentId : ""]));
|
||||
nodes.forEach((node) => {
|
||||
const visited = new Set([node.id]);
|
||||
let parentId = parentById.get(node.id);
|
||||
while (parentId) {
|
||||
if (visited.has(parentId)) {
|
||||
parentById.set(node.id, "");
|
||||
return;
|
||||
}
|
||||
visited.add(parentId);
|
||||
parentId = parentById.get(parentId);
|
||||
}
|
||||
});
|
||||
const normalizedPrimary = nodes.map((node) => ({ ...node, parentId: parentById.get(node.id) || "" }));
|
||||
const rawParentIdsById = new Map(normalizedPrimary.map((node) => [
|
||||
node.id,
|
||||
[...new Set([node.parentId, ...node.parentIds].filter((parentId) => nodeIds.has(parentId) && parentId !== node.id))]
|
||||
]));
|
||||
function wouldCreateCycle(nodeId, parentId) {
|
||||
const visited = new Set([nodeId]);
|
||||
const stack = [parentId];
|
||||
while (stack.length) {
|
||||
const currentId = stack.pop();
|
||||
if (currentId === nodeId) return true;
|
||||
if (!currentId || visited.has(currentId)) continue;
|
||||
visited.add(currentId);
|
||||
stack.push(...(rawParentIdsById.get(currentId) || []));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return normalizedPrimary.map((node) => ({
|
||||
...node,
|
||||
parentIds: (rawParentIdsById.get(node.id) || []).filter((parentId) => !wouldCreateCycle(node.id, parentId))
|
||||
}));
|
||||
}
|
||||
|
||||
export function normalizeGoalTreeData(data) {
|
||||
const nodes = clearInvalidParents((Array.isArray(data?.nodes) ? data.nodes : []).map(normalizeGoalTreeNode));
|
||||
return {
|
||||
lastResetAt: normalizeTimestamp(data?.lastResetAt),
|
||||
orientation: ORIENTATIONS.has(data?.orientation) ? data.orientation : "horizontal",
|
||||
nodes
|
||||
};
|
||||
}
|
||||
|
||||
export function compactGoalTreeDataForStorage(value) {
|
||||
const normalized = normalizeGoalTreeData(value);
|
||||
const compact = {};
|
||||
if (normalized.lastResetAt) compact.lastResetAt = normalized.lastResetAt;
|
||||
if (normalized.orientation !== "horizontal") compact.orientation = normalized.orientation;
|
||||
if (normalized.nodes.length) {
|
||||
compact.nodes = normalized.nodes.map((node) => {
|
||||
const item = { id: node.id, title: node.title };
|
||||
if (node.parentId) item.parentId = node.parentId;
|
||||
if (node.parentIds.some((parentId) => parentId !== node.parentId)) item.parentIds = node.parentIds;
|
||||
if (node.kind !== "step") item.kind = node.kind;
|
||||
if (node.icon !== "cube") item.icon = node.icon;
|
||||
if (node.color !== "gold") item.color = node.color;
|
||||
if (node.kind === "step" && node.qtyTarget !== 1) item.qtyTarget = node.qtyTarget;
|
||||
if (node.kind === "step" && node.qtyCurrent) item.qtyCurrent = node.qtyCurrent;
|
||||
if (node.kind === "step" && node.formula) item.formula = node.formula;
|
||||
if (node.kind === "step" && node.progressMode) item.progressMode = node.progressMode;
|
||||
if (node.kind !== "step" && node.items.length) item.items = node.items.map((entry) => {
|
||||
const compactEntry = { id: entry.id, label: entry.label };
|
||||
if (node.kind === "checklist") {
|
||||
if (entry.formula !== "1") compactEntry.formula = entry.formula;
|
||||
if (entry.qtyTarget !== 1) compactEntry.qtyTarget = entry.qtyTarget;
|
||||
if (entry.qtyCurrent) compactEntry.qtyCurrent = entry.qtyCurrent;
|
||||
}
|
||||
return compactEntry;
|
||||
});
|
||||
if (node.resetType !== "none") item.resetType = node.resetType;
|
||||
if (node.resetTime !== DEFAULT_GOAL_TREE_RESET_TIME) item.resetTime = node.resetTime;
|
||||
if (node.weeklyResetDay !== DEFAULT_GOAL_TREE_WEEKLY_RESET_DAY) item.weeklyResetDay = node.weeklyResetDay;
|
||||
if (node.checkedAt) item.checkedAt = node.checkedAt;
|
||||
if (node.collapsed) item.collapsed = true;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return Object.keys(compact).length ? compact : null;
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { compactChecklistDataForStorage } from "./checklist.js";
|
|||
import { compactCombosDataForStorage } from "./combos.js";
|
||||
import { compactCountersDataForStorage } from "./counters.js";
|
||||
import { compactEquipmentPlannerDataForStorage } from "./equipmentPlanner.js";
|
||||
import { compactGoalTreeDataForStorage } from "./goalTree.js";
|
||||
import { compactImageAnnotationDataForStorage, compactImagesDataForStorage } from "./imageAnnotation.js";
|
||||
import { compactLinksDataForStorage } from "./links.js";
|
||||
import { compactNotepadDataForStorage } from "./notepad.js";
|
||||
|
|
@ -25,5 +26,6 @@ export const MODULE_DATA_COMPACTORS = {
|
|||
table: compactTableDataForStorage,
|
||||
timer: compactTimerDataForStorage,
|
||||
taskPlanner: compactTaskPlannerDataForStorage,
|
||||
goalTree: compactGoalTreeDataForStorage,
|
||||
equipmentPlanner: compactEquipmentPlannerDataForStorage
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const ID_PREFIXES = {
|
|||
marker: "k",
|
||||
timer: "z",
|
||||
task: "a",
|
||||
goal: "g",
|
||||
relation: "e",
|
||||
stroke: "d",
|
||||
equipmentType: "y",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export { normalizeCombosData } from "./modules/combos.js";
|
|||
export { normalizeCountersData } from "./modules/counters.js";
|
||||
export { normalizeDrawingData } from "./modules/drawing.js";
|
||||
export { normalizeEquipmentPlannerData, summarizeEquipmentPlannerData } from "./modules/equipmentPlanner.js";
|
||||
export { normalizeGoalTreeData } from "./modules/goalTree.js";
|
||||
export { labelFromFileName, normalizeImageAnnotationData } from "./modules/imageAnnotation.js";
|
||||
export { hostnameFromUrl, normalizeLinksData, normalizeUrl } from "./modules/links.js";
|
||||
export { normalizeNotepadData, sanitizeNotepadHtml } from "./modules/notepad.js";
|
||||
|
|
@ -275,6 +276,22 @@ function remapModuleDataForExport(type, data, nextId) {
|
|||
return remapped;
|
||||
}
|
||||
|
||||
if (type === "goalTree") {
|
||||
const nodeIdMap = new Map();
|
||||
compact.nodes?.forEach((node) => nodeIdMap.set(node.id, nextId("goal")));
|
||||
const remapped = { ...compact };
|
||||
if (compact.nodes) {
|
||||
remapped.nodes = compact.nodes.map((node) => {
|
||||
const nextNode = { ...node, id: nodeIdMap.get(node.id) };
|
||||
if (node.parentId) nextNode.parentId = nodeIdMap.get(node.parentId) || "";
|
||||
if (node.parentIds) nextNode.parentIds = node.parentIds.map((parentId) => nodeIdMap.get(parentId)).filter(Boolean);
|
||||
if (node.items) nextNode.items = node.items.map((item) => ({ ...item, id: nextId("goal") }));
|
||||
return nextNode;
|
||||
});
|
||||
}
|
||||
return remapped;
|
||||
}
|
||||
|
||||
if (type === "equipmentPlanner") {
|
||||
const typeIdMap = new Map();
|
||||
const equipmentIdMap = new Map();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue