sokko-g/website/src/features/toolboxes/modules/TaskPlannerModule.jsx
Shinuwa 055fb78d60
All checks were successful
Deploy Sokko G / deploy (push) Successful in 17s
Unify category autocomplete fields
2026-09-04 14:09:08 +02:00

229 lines
8.9 KiB
JavaScript

// Rôle : fournit l'outil task planner avec tâches récurrentes, parents, pré requis et échange texte.
import { useEffect, useMemo, useRef, useState } from "react";
import { useDraftForm } from "../../../hooks/useDraftForm.js";
import { TaskPlannerBranch } from "./TaskPlannerBranch.jsx";
import { TaskPlannerSettings } from "./TaskPlannerSettings.jsx";
import { TaskPlannerTopbar } from "./TaskPlannerTopbar.jsx";
import {
applyDueResets,
getTaskParentMap
} from "./taskPlannerUtils.js";
import { useTaskPlannerReorder } from "./useTaskPlannerReorder.js";
export function TaskPlannerModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
const data = context.normalizeTaskPlannerData(context.getModuleData(toolboxId, moduleId, { tasks: [] }));
const textContent = context.moduleText?.taskPlanner || {};
const taskDraft = useDraftForm({ title: "", type: "daily", category: "" });
const categoryOptions = useMemo(() => [
...data.categoryOrder,
...data.tasks.map((task) => task.category).filter(Boolean).filter((category) => !data.categoryOrder.includes(category))
], [data.categoryOrder, data.tasks]);
const [nowMs, setNowMs] = useState(() => Date.now());
const [settingsOpen, setSettingsOpen] = useState(false);
const [openDescriptions, setOpenDescriptions] = useState(() => new Set());
const [openTaskSettings, setOpenTaskSettings] = useState(() => new Set());
const [prerequisiteNoticeTaskIds, setPrerequisiteNoticeTaskIds] = useState(() => new Set());
const prerequisiteNoticeTimeouts = useRef(new Map());
const parentMap = useMemo(() => getTaskParentMap(data.tasks, data.relations), [data.tasks, data.relations]);
function save(nextData) {
context.setModuleData(toolboxId, moduleId, nextData, "taskPlanner");
}
const reorder = useTaskPlannerReorder({ data, parentMap, save });
useEffect(() => {
const intervalId = window.setInterval(() => setNowMs(Date.now()), 60000);
return () => window.clearInterval(intervalId);
}, []);
useEffect(() => () => {
prerequisiteNoticeTimeouts.current.forEach((timeoutId) => window.clearTimeout(timeoutId));
}, []);
useEffect(() => {
const resetData = applyDueResets(data, nowMs);
if (resetData !== data) save(resetData);
});
function addTask(event) {
taskDraft.handleSubmit(event, (draft, { reset }) => {
const cleanTitle = draft.title.trim();
if (!cleanTitle) return;
const category = draft.category.trim();
const task = {
id: context.uid("task"),
title: cleanTitle,
description: "",
type: draft.type,
checked: false,
checkedAt: 0
};
if (category) task.category = category;
save({
...data,
tasks: [...data.tasks, task],
categoryOrder: category && !data.categoryOrder.includes(category) ? [...data.categoryOrder, category] : data.categoryOrder
});
reset();
});
}
function updateTask(taskId, updater) {
save({ ...data, tasks: data.tasks.map((task) => task.id === taskId ? updater(task) : task) });
}
function deleteTask(taskId) {
save({
...data,
tasks: data.tasks.filter((task) => task.id !== taskId),
relations: data.relations.filter((relation) => relation.fromTaskId !== taskId && relation.toTaskId !== taskId)
});
}
function addLinkTarget(fromTaskId, toTaskId) {
if (!toTaskId || fromTaskId === toTaskId || data.relations.some((relation) => relation.fromTaskId === fromTaskId && relation.toTaskId === toTaskId)) return;
save({
...data,
tasks: data.tasks.map((task) => {
if (task.id !== fromTaskId || !task.category) return task;
const nextTask = { ...task };
delete nextTask.category;
return nextTask;
}),
relations: [
...data.relations.filter((relation) => relation.fromTaskId !== fromTaskId),
{ id: context.uid("relation"), fromTaskId, toTaskId, prerequisite: false }
]
});
}
function removeLinkTarget(fromTaskId, toTaskId) {
save({
...data,
relations: data.relations.filter((relation) => relation.fromTaskId !== fromTaskId || relation.toTaskId !== toTaskId)
});
}
function setPrerequisite(fromTaskId, toTaskId, prerequisite) {
const hasRelation = data.relations.some((relation) => relation.fromTaskId === fromTaskId && relation.toTaskId === toTaskId);
save({
...data,
relations: hasRelation
? data.relations.map((relation) => relation.fromTaskId === fromTaskId && relation.toTaskId === toTaskId ? { ...relation, prerequisite } : relation)
: [...data.relations, { id: context.uid("relation"), fromTaskId, toTaskId, prerequisite }]
});
}
function setTaskCategory(taskId, category) {
const normalizedCategory = String(category || "").trim();
const tasks = data.tasks.map((task) => {
if (task.id !== taskId) return task;
const nextTask = { ...task };
if (normalizedCategory) nextTask.category = normalizedCategory;
else delete nextTask.category;
return nextTask;
});
const categoryOrder = [
...data.categoryOrder.filter((categoryName) => tasks.some((task) => task.category === categoryName)),
...tasks.map((task) => task.category).filter(Boolean).filter((categoryName) => !data.categoryOrder.includes(categoryName))
];
save({ ...data, tasks, categoryOrder });
}
function toggleCategoryCollapsed(category) {
const collapsedCategories = data.collapsedCategories.includes(category)
? data.collapsedCategories.filter((item) => item !== category)
: [...data.collapsedCategories, category];
save({ ...data, collapsedCategories });
}
function clearCompletedUniqueTasks() {
const removedTaskIds = new Set(data.tasks.filter((task) => task.type === "unique" && task.checked).map((task) => task.id));
if (!removedTaskIds.size) return;
const tasks = data.tasks.filter((task) => !removedTaskIds.has(task.id));
const relations = data.relations.filter((relation) => !removedTaskIds.has(relation.fromTaskId) && !removedTaskIds.has(relation.toTaskId));
save({ ...data, tasks, relations });
}
function toggleSet(setter, taskId) {
setter((current) => {
const next = new Set(current);
if (next.has(taskId)) next.delete(taskId);
else next.add(taskId);
return next;
});
}
function showPrerequisiteNotice(taskId) {
const currentTimeoutId = prerequisiteNoticeTimeouts.current.get(taskId);
if (currentTimeoutId) window.clearTimeout(currentTimeoutId);
setPrerequisiteNoticeTaskIds((current) => new Set(current).add(taskId));
const timeoutId = window.setTimeout(() => {
prerequisiteNoticeTimeouts.current.delete(taskId);
setPrerequisiteNoticeTaskIds((current) => {
const next = new Set(current);
next.delete(taskId);
return next;
});
}, 2600);
prerequisiteNoticeTimeouts.current.set(taskId, timeoutId);
}
return (
<div className="task-planner-module">
<TaskPlannerTopbar
data={data}
context={context}
editing={editing}
settingsOpen={settingsOpen}
categoryOptions={categoryOptions}
taskDraft={taskDraft}
textContent={textContent}
textExchangeOpen={textExchangeOpen}
onAddTask={addTask}
onHideCompletedToggle={() => save({ ...data, hideCompleted: !data.hideCompleted })}
onSettingsToggle={() => setSettingsOpen((value) => !value)}
onImport={(nextData) => save(nextData)}
/>
{settingsOpen && (
<TaskPlannerSettings
data={data}
textContent={textContent}
onWeeklyResetDayChange={(weeklyResetDay) => save({ ...data, weeklyResetDay })}
onResetTimeChange={(resetTime) => save({ ...data, resetTime })}
onClearCompletedUniqueTasks={clearCompletedUniqueTasks}
/>
)}
<ul className="task-planner-list">
{data.tasks.length ? (
<TaskPlannerBranch
parentId=""
parentMap={parentMap}
tasks={data.tasks}
data={data}
nowMs={nowMs}
reorder={reorder}
textContent={textContent}
openDescriptions={openDescriptions}
openTaskSettings={openTaskSettings}
prerequisiteNoticeTaskIds={prerequisiteNoticeTaskIds}
onToggleDescription={(taskId) => toggleSet(setOpenDescriptions, taskId)}
onToggleSettings={(taskId) => toggleSet(setOpenTaskSettings, taskId)}
onPrerequisiteNotice={showPrerequisiteNotice}
onUpdateTask={updateTask}
onDeleteTask={deleteTask}
onAddLinkTarget={addLinkTarget}
onRemoveLinkTarget={removeLinkTarget}
onSetPrerequisite={setPrerequisite}
onSetTaskCategory={setTaskCategory}
onToggleCategoryCollapsed={toggleCategoryCollapsed}
/>
) : (
<li className="task-planner-empty">{textContent.emptyTasks || "Aucune tâche planifiée."}</li>
)}
</ul>
</div>
);
}