229 lines
8.9 KiB
JavaScript
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>
|
|
);
|
|
}
|