Add goal tree toolbox module
All checks were successful
Deploy Sokko G / deploy (push) Successful in 15s

This commit is contained in:
Shinuwa 2026-08-22 18:15:39 +02:00
parent e4cbe3e087
commit 59fdcb0927
63 changed files with 4062 additions and 252 deletions

View file

@ -30,6 +30,7 @@ const SUPPORTED_TEXT_EXCHANGE_TYPES = new Set([
"timer",
"calendar",
"taskPlanner",
"goalTree",
"equipmentPlanner",
"images",
"imageAnnotation"
@ -61,6 +62,7 @@ export function hasTextExchangeContent(type, data) {
if (type === "timer") return Boolean(data?.stopwatch?.laps?.length || data?.countdowns?.length || data?.stopwatch?.elapsedMs);
if (type === "calendar") return Boolean(data?.events?.length);
if (type === "taskPlanner") return Boolean(data?.tasks?.length);
if (type === "goalTree") return Boolean(data?.nodes?.length);
if (type === "equipmentPlanner") return Boolean(data?.equipments?.length);
if (type === "images") return Boolean(data?.images?.length);
if (type === "imageAnnotation") return Boolean(data?.image);
@ -78,6 +80,7 @@ export function exportModuleText(type, data) {
if (type === "timer") return exportTimerText(data);
if (type === "calendar") return exportCalendarText(data);
if (type === "taskPlanner") return exportTaskPlannerText(data);
if (type === "goalTree") return exportGoalTreeText(data);
if (type === "equipmentPlanner") return exportEquipmentPlannerText(data);
if (type === "images") return exportImagesText(data);
if (type === "imageAnnotation") return exportImageAnnotationText(data);
@ -99,6 +102,7 @@ export function importModuleText(type, text, context) {
else if (type === "timer") data = importTimerText(value, context);
else if (type === "calendar") data = importCalendarText(value, context);
else if (type === "taskPlanner") data = importTaskPlannerText(value, context);
else if (type === "goalTree") data = importGoalTreeText(value, context);
else if (type === "equipmentPlanner") data = importEquipmentPlannerText(value, context);
else if (type === "images") data = importImagesText(value, context);
else if (type === "imageAnnotation") data = importImageAnnotationText(value, context);
@ -586,6 +590,90 @@ function importTaskPlannerText(text, context) {
return context.normalizeTaskPlannerData(data);
}
function exportGoalTreeText(data) {
const lines = [];
if (data.lastResetAt) lines.push(`@lastResetAt: ${data.lastResetAt}`);
function visit(parentId = "", depth = 0) {
(data.nodes || []).filter((node) => (node.parentId || "") === parentId).forEach((node) => {
const prefix = " ".repeat(depth);
const formula = String(node.formula || "").trim();
const qty = Number.isFinite(node.computedQtyTarget) ? node.computedQtyTarget : node.qtyTarget;
const suffix = node.kind === "step" ? (formula ? ` = ${formula}` : ` x${qty}`) : "";
const meta = formatMeta({
kind: node.kind !== "step" ? node.kind : "",
icon: node.kind === "step" && node.icon && node.icon !== "cube" ? node.icon : "",
color: node.kind === "step" && node.color && node.color !== "gold" ? node.color : "",
items: node.kind !== "step" && node.items?.length ? encodeURIComponent(JSON.stringify(node.items)) : "",
current: node.qtyCurrent || "",
reset: node.resetType !== "none" ? node.resetType : "",
resetTime: node.resetTime && node.resetTime !== "00:00" ? node.resetTime : "",
weeklyResetDay: node.weeklyResetDay !== 1 ? node.weeklyResetDay : "",
collapsed: node.collapsed
});
lines.push(`${prefix}- ${node.title}${suffix}${meta ? ` | ${meta}` : ""}`);
visit(node.id, depth + 1);
});
}
visit();
return lines.join("\n");
}
function importGoalTreeText(text, context) {
const nodes = [];
const stack = [];
const data = { lastResetAt: 0, nodes };
splitLines(text).forEach((line, index) => {
if (!line.trim()) return;
const cleanLine = line.trim();
const rootMeta = parseMetaLine(cleanLine);
if (rootMeta && !stack.length) {
if (rootMeta.key === "lastResetAt") data.lastResetAt = parseInteger(rootMeta.value, 0);
return;
}
const depth = Math.floor((line.match(/^ */)?.[0].length || 0) / 2);
const match = cleanLine.match(/^-\s+(.+)$/);
if (!match) throw lineError(index, `ligne arbre d'objectifs invalide: ${cleanLine}`);
const { title: rawTitle, meta } = parsePipeParts(match[1]);
const formulaIndex = rawTitle.lastIndexOf("=");
const quantityMatch = rawTitle.match(/\s+x(\d+(?:[,.]\d+)?)\s*$/i);
const hasFormula = formulaIndex >= 0;
const title = (hasFormula ? rawTitle.slice(0, formulaIndex) : rawTitle.replace(/\s+x(\d+(?:[,.]\d+)?)\s*$/i, "")).trim();
if (!title) throw lineError(index, "titre manquant.");
const parent = stack[depth - 1];
const node = {
id: context.uid("goal"),
parentId: parent?.id || "",
title,
kind: meta.kind || "step",
icon: meta.icon || "cube",
color: meta.color || "gold",
qtyTarget: hasFormula ? 1 : Math.max(0, parseNumber(quantityMatch?.[1], 1)) || 1,
qtyCurrent: 0,
formula: hasFormula ? rawTitle.slice(formulaIndex + 1).trim() : "",
items: parseGoalTreeItemsMeta(meta.items),
resetType: meta.reset || "none",
resetTime: meta.resetTime || "00:00",
weeklyResetDay: parseInteger(meta.weeklyResetDay, 1),
checkedAt: 0,
collapsed: parseBoolean(meta.collapsed)
};
nodes.push(node);
stack[depth] = node;
stack.length = depth + 1;
});
return context.normalizeGoalTreeData(data);
}
function parseGoalTreeItemsMeta(value) {
if (!value) return [];
try {
const parsed = JSON.parse(decodeURIComponent(value));
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
function exportTimerText(data) {
const lines = [];
if (data.activeTab && data.activeTab !== "stopwatch") lines.push(`@activeTab: ${data.activeTab}`);