add task planner & alert improvments
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s

This commit is contained in:
Shinuwa 2026-07-27 22:44:08 +02:00
parent 2e6d8a2c0c
commit e306bfc5d6
15 changed files with 1547 additions and 4 deletions

View file

@ -1,9 +1,13 @@
// Rôle : normalise, compacte et sérialise les données toolbox persistées.
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", section: "g", image: "s", link: "l", counter: "c", calc: "r", marker: "k", timer: "z" };
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", section: "g", image: "s", link: "l", counter: "c", calc: "r", marker: "k", timer: "z", task: "a", relation: "e" };
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
const TIMER_TABS = new Set(["stopwatch", "countdown"]);
const COUNTDOWN_TYPES = new Set(["duration", "daily_time", "time_pattern", "interval"]);
const TIMER_ALERT_MODES = new Set(["off", "visible", "site"]);
const TASK_TYPES = new Set(["unique", "daily", "weekly"]);
const TASK_SORT_MODES = new Set(["manual", "type"]);
const DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY = 1;
const DEFAULT_TASK_PLANNER_RESET_TIME = "00:00";
const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
const TOOLBOX_ICON_FILES = [
"toolbox.png",
@ -34,6 +38,7 @@ const DEFAULT_MODULE_TITLES = {
counters: "Compteurs",
calculator: "Calculateur",
timer: "Timer",
taskPlanner: "Planificateur de tâches",
imageAnnotation: "Annotation d'images"
};
@ -224,6 +229,22 @@ function isTimeString(value, allowWildcard = false) {
});
}
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_TASK_PLANNER_WEEKLY_RESET_DAY) {
const parsed = Number.parseInt(value, 10);
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 6 ? parsed : fallback;
}
function normalizeTimestamp(value) {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
export function labelFromFileName(name) {
if (!name || name === "image.png") return "";
return name.replace(/\.[^.]+$/, "").trim();
@ -300,6 +321,65 @@ export function normalizeTimerData(data) {
};
}
function normalizeTaskPlannerTask(task) {
const title = String(task?.title || "").trim() || "Tâche";
const type = TASK_TYPES.has(task?.type) ? task.type : "unique";
const normalized = {
id: task?.id || uid("task"),
title,
description: String(task?.description || "").trim(),
type,
checked: task?.checked === true,
checkedAt: normalizeTimestamp(task?.checkedAt)
};
if (type === "weekly" && task?.weeklyResetDay !== undefined) normalized.weeklyResetDay = normalizeWeekDay(task.weeklyResetDay);
if (!normalized.checked) normalized.checkedAt = 0;
return normalized;
}
function normalizeTaskPlannerRelations(data, taskIds) {
const seen = new Set();
const seenChildren = new Set();
const legacyDependencies = new Set((Array.isArray(data?.dependencies) ? data.dependencies : [])
.map((relation) => `${relation?.fromTaskId || ""}:${relation?.toTaskId || ""}`));
const sourceRelations = Array.isArray(data?.relations)
? data.relations
: [
...(Array.isArray(data?.links) ? data.links : []),
...(Array.isArray(data?.dependencies) ? data.dependencies : []).map((relation) => ({ ...relation, dependency: true }))
];
return sourceRelations
.map((relation) => {
const fromTaskId = String(relation?.fromTaskId || "");
const toTaskId = String(relation?.toTaskId || "");
if (!taskIds.has(fromTaskId) || !taskIds.has(toTaskId) || fromTaskId === toTaskId) return null;
const key = `${fromTaskId}:${toTaskId}`;
if (seen.has(key) || seenChildren.has(fromTaskId)) return null;
seen.add(key);
seenChildren.add(fromTaskId);
return {
id: relation?.id || uid("relation"),
fromTaskId,
toTaskId,
dependency: relation?.dependency === true || legacyDependencies.has(key)
};
})
.filter(Boolean);
}
export function normalizeTaskPlannerData(data) {
const tasks = (Array.isArray(data?.tasks) ? data.tasks : []).map(normalizeTaskPlannerTask).filter(Boolean);
const taskIds = new Set(tasks.map((task) => task.id));
return {
sortMode: TASK_SORT_MODES.has(data?.sortMode) ? data.sortMode : "manual",
weeklyResetDay: normalizeWeekDay(data?.weeklyResetDay),
resetTime: isClockTimeString(data?.resetTime) ? data.resetTime : DEFAULT_TASK_PLANNER_RESET_TIME,
lastResetAt: normalizeTimestamp(data?.lastResetAt),
tasks,
relations: normalizeTaskPlannerRelations(data, taskIds)
};
}
function clampPercent(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return 0;
@ -433,6 +513,40 @@ export function compactModuleDataForStorage(type, value) {
}
return Object.keys(compact).length ? compact : null;
}
if (type === "taskPlanner") {
const normalized = normalizeTaskPlannerData(value);
const compact = {};
if (normalized.sortMode !== "manual") compact.sortMode = normalized.sortMode;
if (normalized.weeklyResetDay !== DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY) compact.weeklyResetDay = normalized.weeklyResetDay;
if (normalized.resetTime !== DEFAULT_TASK_PLANNER_RESET_TIME) compact.resetTime = normalized.resetTime;
if (normalized.lastResetAt) compact.lastResetAt = normalized.lastResetAt;
if (normalized.tasks.length) {
compact.tasks = normalized.tasks.map((task) => {
const compactTask = {
id: task.id,
title: task.title,
type: task.type
};
if (task.description) compactTask.description = task.description;
if (task.checked) compactTask.checked = true;
if (task.checked && task.checkedAt) compactTask.checkedAt = task.checkedAt;
if (task.type === "weekly" && task.weeklyResetDay !== undefined) compactTask.weeklyResetDay = task.weeklyResetDay;
return compactTask;
});
}
if (normalized.relations.length) {
compact.relations = normalized.relations.map((relation) => {
const compactRelation = {
id: relation.id,
fromTaskId: relation.fromTaskId,
toTaskId: relation.toTaskId
};
if (relation.dependency) compactRelation.dependency = true;
return compactRelation;
});
}
return Object.keys(compact).length ? compact : null;
}
return value;
}
@ -511,6 +625,24 @@ function remapModuleDataForExport(type, data, nextId) {
return remapped;
}
if (type === "taskPlanner") {
const taskIdMap = new Map();
compact.tasks?.forEach((task) => taskIdMap.set(task.id, nextId("task")));
const remapped = { ...compact };
if (compact.tasks) remapped.tasks = compact.tasks.map((task) => ({ ...task, id: taskIdMap.get(task.id) }));
if (compact.relations) {
remapped.relations = compact.relations
.map((relation) => ({
...relation,
id: nextId("relation"),
fromTaskId: taskIdMap.get(relation.fromTaskId),
toTaskId: taskIdMap.get(relation.toTaskId)
}))
.filter((relation) => relation.fromTaskId && relation.toTaskId);
}
return remapped;
}
return compact;
}