diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md index 11b6480..d963423 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -269,11 +269,13 @@ Type : `taskPlanner` "weeklyResetDay": 1, "resetTime": "00:00", "lastResetAt": 1760000000000, + "categoryOrder": ["Raid", "Farm"], "tasks": [ { "id": "task1", "title": "Quête 1", "description": "Préparer la route.", + "category": "Raid", "type": "daily", "checked": true, "checkedAt": 1760000000000 @@ -295,7 +297,7 @@ Type : `taskPlanner` "id": "relation2", "fromTaskId": "task2", "toTaskId": "task1", - "dependency": true + "prerequisite": true } ] } @@ -308,10 +310,14 @@ Notes : - `weeklyResetDay` suit les jours JavaScript : `0` dimanche, `1` lundi, ..., `6` samedi. - `resetTime` est global à l'outil et vaut `HH:MM`. - `weeklyResetDay` sur une tâche hebdo surcharge le jour global. +- `category` sur une tâche racine est facultatif. Les tâches enfants héritent de la catégorie effective de leur parent et ne stockent pas de catégorie propre. +- Les tâches catégorisées sont regroupées par catégorie au niveau racine. +- `categoryOrder` stocke l'ordre d'affichage des catégories utilisées. Les catégories absentes des tâches sont retirées à la normalisation. - `relations` stocke les parents directionnels. `fromTaskId` désigne la tâche enfant, `toTaskId` son parent. - une tâche ne conserve qu'un seul parent. -- `dependency: true` marque aussi ce parent comme pré requis. +- `prerequisite: true` marque aussi ce parent comme pré requis. - Les pré requis sont non bloquants : ils affichent un avertissement si le parent requis n'est pas effectué. +- En tri manuel, une tâche catégorisée ne peut être réordonnée qu'avec des tâches du même parent et de la même catégorie. Stockage compact : @@ -320,10 +326,12 @@ Stockage compact : - `resetTime` est omis si la valeur vaut `00:00`. - `lastResetAt` est omis si la valeur vaut `0`. - `description` est omis si vide. +- `category` est omis si vide ou si la tâche a un parent. +- `categoryOrder` est omis si aucune catégorie n'est utilisée. - `checked` et `checkedAt` sont omis si la tâche n'est pas effectuée. - `weeklyResetDay` unitaire est omis si non défini. - `relations` est omis si vide. -- `relations[].dependency` est omis si `false`. +- `relations[].prerequisite` est omis si `false`. ## Outil Liens diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index 2acdfd5..d396393 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -211,15 +211,18 @@ export function validateSiteContent(site) { "toolboxes.modules.taskPlanner.showDescriptionTitle", "toolboxes.modules.taskPlanner.hideDescriptionTitle", "toolboxes.modules.taskPlanner.taskSettingsTitle", - "toolboxes.modules.taskPlanner.missingDependencyTitle", - "toolboxes.modules.taskPlanner.missingDependencyBadge", - "toolboxes.modules.taskPlanner.missingDependencyNotice", + "toolboxes.modules.taskPlanner.missingPrerequisiteTitle", + "toolboxes.modules.taskPlanner.missingPrerequisiteBadge", + "toolboxes.modules.taskPlanner.missingPrerequisiteNotice", + "toolboxes.modules.taskPlanner.categoryLabel", + "toolboxes.modules.taskPlanner.categoryPlaceholder", + "toolboxes.modules.taskPlanner.categoryReorderTitle", "toolboxes.modules.taskPlanner.taskWeeklyResetDayLabel", "toolboxes.modules.taskPlanner.inheritWeeklyResetDay", "toolboxes.modules.taskPlanner.linksLabel", "toolboxes.modules.taskPlanner.emptyLinks", "toolboxes.modules.taskPlanner.addLinkLabel", - "toolboxes.modules.taskPlanner.dependencyCheckboxLabel", + "toolboxes.modules.taskPlanner.prerequisiteCheckboxLabel", "toolboxes.modules.taskPlanner.removeRelationTitle", "toolboxes.modules.taskPlanner.emptyTasks", "toolboxes.modules.imageAnnotation.addImage", diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index 9ea371f..5bf232b 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -141,7 +141,9 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(taskPlannerModule, /normalizeTaskPlannerData/); assert.match(taskPlannerModule, /applyDueResets/); assert.match(taskPlannerModule, /relations/); - assert.match(taskPlannerModule, /dependency/); + assert.match(taskPlannerModule, /prerequisite/); + assert.match(taskPlannerModule, /categoryOrder/); + assert.match(taskPlannerModule, /categoryLabel/); assert.match(taskPlannerModule, /task-planner-warning/); assert.match(taskPlannerModule, /usePointerReorder/); assert.match(checklistModule, /export function ChecklistModule/); diff --git a/tests/toolbox-modules.test.mjs b/tests/toolbox-modules.test.mjs index c7cef72..538fb47 100644 --- a/tests/toolbox-modules.test.mjs +++ b/tests/toolbox-modules.test.mjs @@ -33,14 +33,15 @@ test("task planner data normalizes invalid settings and relations", () => { sortMode: "unknown", weeklyResetDay: 9, resetTime: "27:80", + categoryOrder: ["Raid", "Missing", "Farm", "Raid"], tasks: [ - { id: "task1", title: "Daily", type: "daily", checked: true, checkedAt: 1000 }, - { id: "task2", title: "Weekly", type: "weekly", weeklyResetDay: 4 }, - { id: "task3", title: "Invalid type", type: "later" } + { id: "task1", title: "Daily", type: "daily", checked: true, checkedAt: 1000, category: "Farm" }, + { id: "task2", title: "Weekly", type: "weekly", weeklyResetDay: 4, category: "Raid" }, + { id: "task3", title: "Invalid type", type: "later", category: "Farm" } ], relations: [ { id: "link1", fromTaskId: "task1", toTaskId: "task2" }, - { id: "dep1", fromTaskId: "task2", toTaskId: "task1", dependency: true }, + { id: "dep1", fromTaskId: "task3", toTaskId: "task2", prerequisite: true }, { id: "bad", fromTaskId: "task1", toTaskId: "missing" }, { id: "self", fromTaskId: "task2", toTaskId: "task2" } ] @@ -49,29 +50,16 @@ test("task planner data normalizes invalid settings and relations", () => { assert.equal(normalized.sortMode, "manual"); assert.equal(normalized.weeklyResetDay, 1); assert.equal(normalized.resetTime, "00:00"); + assert.deepEqual(normalized.categoryOrder, ["Raid"]); + assert.equal(normalized.tasks[0].category, undefined); assert.equal(normalized.tasks[1].weeklyResetDay, 4); + assert.equal(normalized.tasks[1].category, "Raid"); assert.equal(normalized.tasks[2].type, "unique"); assert.deepEqual(normalized.relations.map((relation) => relation.id), ["link1", "dep1"]); - assert.equal(normalized.relations[0].dependency, false); - assert.equal(normalized.relations[1].dependency, true); + assert.equal(normalized.relations[0].prerequisite, false); + assert.equal(normalized.relations[1].prerequisite, true); }); -test("task planner accepts temporary split links data as combined relations", () => { - const normalized = normalizeTaskPlannerData({ - tasks: [ - { id: "task1", title: "Daily", type: "daily" }, - { id: "task2", title: "Weekly", type: "weekly" } - ], - links: [{ id: "link1", fromTaskId: "task1", toTaskId: "task2" }], - dependencies: [{ id: "dep1", fromTaskId: "task2", toTaskId: "task1" }] - }); - - assert.deepEqual(normalized.relations.map((relation) => relation.toTaskId), ["task2", "task1"]); - assert.equal(normalized.relations[0].dependency, false); - assert.equal(normalized.relations[1].dependency, true); -}); - - test("task planner storage compacts defaults and export remaps task relations", () => { const compact = compactModuleDataForStorage("taskPlanner", { sortMode: "type", @@ -79,18 +67,20 @@ test("task planner storage compacts defaults and export remaps task relations", resetTime: "00:00", tasks: [ { id: "task1", title: "Daily", description: "", type: "daily", checked: false }, - { id: "task2", title: "Weekly", description: "Run", type: "weekly", checked: true, checkedAt: 2000, weeklyResetDay: 5 } + { id: "task2", title: "Weekly", description: "Run", type: "weekly", checked: true, checkedAt: 2000, weeklyResetDay: 5, category: "Raid" } ], + categoryOrder: ["Raid"], relations: [ - { id: "link1", fromTaskId: "task1", toTaskId: "task2", dependency: false }, - { id: "dep1", fromTaskId: "task2", toTaskId: "task1", dependency: true } + { id: "dep1", fromTaskId: "task1", toTaskId: "task2", prerequisite: true } ] }); assert.equal(compact.sortMode, "type"); assert.equal(compact.weeklyResetDay, undefined); assert.equal(compact.resetTime, undefined); + assert.deepEqual(compact.categoryOrder, ["Raid"]); assert.deepEqual(compact.tasks[0], { id: "task1", title: "Daily", type: "daily" }); + assert.equal(compact.tasks[1].category, "Raid"); assert.equal(compact.tasks[1].weeklyResetDay, 5); const exported = createToolboxExportPayload( @@ -101,8 +91,5 @@ test("task planner storage compacts defaults and export remaps task relations", assert.deepEqual(exportedPlanner.tasks.map((task) => task.id), ["a1", "a2"]); assert.equal(exportedPlanner.relations[0].fromTaskId, "a1"); assert.equal(exportedPlanner.relations[0].toTaskId, "a2"); - assert.equal(exportedPlanner.relations[0].dependency, undefined); - assert.equal(exportedPlanner.relations[1].fromTaskId, "a2"); - assert.equal(exportedPlanner.relations[1].toTaskId, "a1"); - assert.equal(exportedPlanner.relations[1].dependency, true); + assert.equal(exportedPlanner.relations[0].prerequisite, true); }); diff --git a/website/public/data/site.json b/website/public/data/site.json index 458edc0..42e9487 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -345,15 +345,18 @@ "showDescriptionTitle": "Afficher la description", "hideDescriptionTitle": "Masquer la description", "taskSettingsTitle": "Réglages de la tâche", - "missingDependencyTitle": "Pré requis non effectué", - "missingDependencyBadge": "Pré requis", - "missingDependencyNotice": "Pré requis parent non effectué", + "missingPrerequisiteTitle": "Pré requis non effectué", + "missingPrerequisiteBadge": "Pré requis", + "missingPrerequisiteNotice": "Pré requis parent non effectué", + "categoryLabel": "Catégorie", + "categoryPlaceholder": "Nom de catégorie", + "categoryReorderTitle": "Déplacer la catégorie", "taskWeeklyResetDayLabel": "Jour hebdo", "inheritWeeklyResetDay": "Global", "linksLabel": "Parent", "emptyLinks": "Aucun parent", "addLinkLabel": "Définir un parent", - "dependencyCheckboxLabel": "Pré requis", + "prerequisiteCheckboxLabel": "Pré requis", "removeRelationTitle": "Retirer", "emptyTasks": "Aucune tâche planifiée." }, diff --git a/website/src/features/toolboxes/modules/TaskPlannerModule.jsx b/website/src/features/toolboxes/modules/TaskPlannerModule.jsx index dd66d8c..bdab6ff 100644 --- a/website/src/features/toolboxes/modules/TaskPlannerModule.jsx +++ b/website/src/features/toolboxes/modules/TaskPlannerModule.jsx @@ -76,6 +76,17 @@ function moveTask(tasks, fromTaskId, toTaskId, placement = "before") { return nextTasks; } +function moveCategory(categoryOrder, fromCategory, toCategory, placement = "before") { + const index = categoryOrder.indexOf(fromCategory); + const targetIndex = categoryOrder.indexOf(toCategory); + if (index < 0 || targetIndex < 0 || fromCategory === toCategory) return categoryOrder; + const nextOrder = [...categoryOrder]; + const [moved] = nextOrder.splice(index, 1); + const nextTargetIndex = nextOrder.indexOf(toCategory); + nextOrder.splice(placement === "after" ? nextTargetIndex + 1 : nextTargetIndex, 0, moved); + return nextOrder; +} + function sortTasks(tasks, sortMode) { if (sortMode !== "type") return tasks; const order = new Map(TASK_TYPES.map((type, index) => [type, index])); @@ -112,6 +123,82 @@ function getTasksForParent(tasks, parentMap, parentId, sortMode) { return sortTasks(tasks.filter((task) => (parentMap.get(task.id) || "") === parentId), sortMode); } +function getTaskCategory(task) { + return String(task?.category || "").trim(); +} + +function getTaskById(tasks, taskId) { + return tasks.find((task) => task.id === taskId) || null; +} + +function getEffectiveTaskCategory(task, tasks, parentMap) { + const parentId = parentMap.get(task?.id) || ""; + const parentTask = parentId ? getTaskById(tasks, parentId) : null; + return parentTask ? getEffectiveTaskCategory(parentTask, tasks, parentMap) : getTaskCategory(task); +} + +function getTaskCategoryForParent(task, tasks, parentMap, parentId) { + return parentId ? getEffectiveTaskCategory(task, tasks, parentMap) : getTaskCategory(task); +} + +function getTaskCategoryDataset(task, tasks, parentMap) { + return getEffectiveTaskCategory(task, tasks, parentMap); +} + +function countTaskTree(taskIds, tasks, parentMap) { + let count = 0; + const visited = new Set(); + function visit(taskId) { + if (visited.has(taskId)) return; + visited.add(taskId); + count += 1; + tasks + .filter((task) => (parentMap.get(task.id) || "") === taskId) + .forEach((child) => visit(child.id)); + } + taskIds.forEach(visit); + return count; +} + +function getTaskCategoryGroups(tasks, parentMap, parentId, sortMode, categoryOrder) { + const siblings = getTasksForParent(tasks, parentMap, parentId, sortMode); + if (parentId) { + return { + uncategorized: siblings, + categories: [] + }; + } + const categories = []; + siblings.forEach((task) => { + const category = getTaskCategoryForParent(task, tasks, parentMap, parentId); + if (category && !categories.includes(category)) categories.push(category); + }); + const orderedCategories = [ + ...categoryOrder.filter((category) => categories.includes(category)), + ...categories.filter((category) => !categoryOrder.includes(category)) + ]; + return { + uncategorized: siblings.filter((task) => !getTaskCategoryForParent(task, tasks, parentMap, parentId)), + categories: orderedCategories.map((category) => ({ + category, + tasks: siblings.filter((task) => getTaskCategoryForParent(task, tasks, parentMap, parentId) === category) + })).filter((group) => group.tasks.length) + }; +} + +function getCategorySectionId(parentId, category) { + return `${parentId || ""}\n${category}`; +} + +function parseCategorySectionId(id) { + const separatorIndex = id.indexOf("\n"); + if (separatorIndex < 0) return { parentId: "", category: id }; + return { + parentId: id.slice(0, separatorIndex), + category: id.slice(separatorIndex + 1) + }; +} + export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) { const data = context.normalizeTaskPlannerData(context.getModuleData(toolboxId, moduleId, { tasks: [] })); const textContent = context.moduleText?.taskPlanner || {}; @@ -121,8 +208,8 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) { const [settingsOpen, setSettingsOpen] = useState(false); const [openDescriptions, setOpenDescriptions] = useState(() => new Set()); const [openTaskSettings, setOpenTaskSettings] = useState(() => new Set()); - const [dependencyNoticeTaskIds, setDependencyNoticeTaskIds] = useState(() => new Set()); - const dependencyNoticeTimeouts = useRef(new Map()); + const [prerequisiteNoticeTaskIds, setPrerequisiteNoticeTaskIds] = useState(() => new Set()); + const prerequisiteNoticeTimeouts = useRef(new Map()); const parentMap = useMemo(() => getTaskParentMap(data.tasks, data.relations), [data.tasks, data.relations]); const { draggingId, @@ -131,9 +218,31 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) { } = usePointerReorder({ targetSelector: ".task-planner-item", getTargetId: (target) => target.dataset.taskId, - canDropOn: (target, draggingTaskId) => data.sortMode === "manual" && target.dataset.parentId === (parentMap.get(draggingTaskId) || ""), + canDropOn: (target, draggingTaskId) => { + const draggingTask = data.tasks.find((task) => task.id === draggingTaskId); + return data.sortMode === "manual" + && target.dataset.parentId === (parentMap.get(draggingTaskId) || "") + && target.dataset.category === getTaskCategoryDataset(draggingTask, data.tasks, parentMap); + }, onMove: (fromTaskId, toTaskId, placement) => save({ ...data, tasks: moveTask(data.tasks, fromTaskId, toTaskId, placement) }) }); + const { + draggingId: draggingCategoryId, + dropTarget: categoryDropTarget, + startDrag: startCategoryDrag + } = usePointerReorder({ + targetSelector: ".task-planner-category-section", + getTargetId: (target) => getCategorySectionId(target.dataset.parentId || "", target.dataset.category || ""), + canDropOn: (target, draggingIdValue) => { + const draggingCategory = parseCategorySectionId(draggingIdValue); + return target.dataset.parentId === draggingCategory.parentId && Boolean(target.dataset.category); + }, + onMove: (fromId, toId, placement) => { + const fromCategory = parseCategorySectionId(fromId).category; + const toCategory = parseCategorySectionId(toId).category; + save({ ...data, categoryOrder: moveCategory(data.categoryOrder, fromCategory, toCategory, placement) }); + } + }); function save(nextData) { context.setModuleData(toolboxId, moduleId, nextData, "taskPlanner"); @@ -145,7 +254,7 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) { }, []); useEffect(() => () => { - dependencyNoticeTimeouts.current.forEach((timeoutId) => window.clearTimeout(timeoutId)); + prerequisiteNoticeTimeouts.current.forEach((timeoutId) => window.clearTimeout(timeoutId)); }, []); useEffect(() => { @@ -191,9 +300,15 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) { 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, dependency: false } + { id: context.uid("relation"), fromTaskId, toTaskId, prerequisite: false } ] }); } @@ -205,16 +320,32 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) { }); } - function setDependency(fromTaskId, toTaskId, dependency) { + 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, dependency } : relation) - : [...data.relations, { id: context.uid("relation"), fromTaskId, toTaskId, dependency }] + ? 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 toggleSet(setter, taskId) { setter((current) => { const next = new Set(current); @@ -224,19 +355,19 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) { }); } - function showDependencyNotice(taskId) { - const currentTimeoutId = dependencyNoticeTimeouts.current.get(taskId); + function showPrerequisiteNotice(taskId) { + const currentTimeoutId = prerequisiteNoticeTimeouts.current.get(taskId); if (currentTimeoutId) window.clearTimeout(currentTimeoutId); - setDependencyNoticeTaskIds((current) => new Set(current).add(taskId)); + setPrerequisiteNoticeTaskIds((current) => new Set(current).add(taskId)); const timeoutId = window.setTimeout(() => { - dependencyNoticeTimeouts.current.delete(taskId); - setDependencyNoticeTaskIds((current) => { + prerequisiteNoticeTimeouts.current.delete(taskId); + setPrerequisiteNoticeTaskIds((current) => { const next = new Set(current); next.delete(taskId); return next; }); }, 2600); - dependencyNoticeTimeouts.current.set(taskId, timeoutId); + prerequisiteNoticeTimeouts.current.set(taskId, timeoutId); } return ( @@ -301,19 +432,23 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) { data={data} draggingId={draggingId} dropTarget={dropTarget} + draggingCategoryId={draggingCategoryId} + categoryDropTarget={categoryDropTarget} textContent={textContent} openDescriptions={openDescriptions} openTaskSettings={openTaskSettings} - dependencyNoticeTaskIds={dependencyNoticeTaskIds} + prerequisiteNoticeTaskIds={prerequisiteNoticeTaskIds} onDragStart={startDrag} + onCategoryDragStart={startCategoryDrag} onToggleDescription={(taskId) => toggleSet(setOpenDescriptions, taskId)} onToggleSettings={(taskId) => toggleSet(setOpenTaskSettings, taskId)} - onDependencyNotice={showDependencyNotice} + onPrerequisiteNotice={showPrerequisiteNotice} onUpdateTask={updateTask} onDeleteTask={deleteTask} onAddLinkTarget={addLinkTarget} onRemoveLinkTarget={removeLinkTarget} - onSetDependency={setDependency} + onSetPrerequisite={setPrerequisite} + onSetTaskCategory={setTaskCategory} /> ) : (
  • {textContent.emptyTasks || "Aucune tâche planifiée."}
  • @@ -330,26 +465,31 @@ function TaskPlannerBranch({ data, draggingId, dropTarget, + draggingCategoryId, + categoryDropTarget, textContent, openDescriptions, openTaskSettings, - dependencyNoticeTaskIds, + prerequisiteNoticeTaskIds, onDragStart, + onCategoryDragStart, onToggleDescription, onToggleSettings, - onDependencyNotice, + onPrerequisiteNotice, onUpdateTask, onDeleteTask, onAddLinkTarget, onRemoveLinkTarget, - onSetDependency, + onSetPrerequisite, + onSetTaskCategory, visited = new Set() }) { - const children = getTasksForParent(tasks, parentMap, parentId, data.sortMode).filter((task) => !visited.has(task.id)); + const groupedTasks = getTaskCategoryGroups(tasks, parentMap, parentId, data.sortMode, data.categoryOrder); + const children = [...groupedTasks.uncategorized, ...groupedTasks.categories.flatMap((group) => group.tasks)].filter((task) => !visited.has(task.id)); if (!children.length) return null; const nextVisited = new Set([...visited, ...children.map((task) => task.id)]); - return children.map((task) => { + function renderTask(task) { const nestedChildren = getTasksForParent(tasks, parentMap, task.id, data.sortMode).filter((child) => !nextVisited.has(child.id)); return ( onToggleDescription(task.id)} onToggleSettings={() => onToggleSettings(task.id)} - onDependencyNotice={onDependencyNotice} + onPrerequisiteNotice={onPrerequisiteNotice} onUpdateTask={onUpdateTask} onDeleteTask={onDeleteTask} onAddLinkTarget={onAddLinkTarget} onRemoveLinkTarget={onRemoveLinkTarget} - onSetDependency={onSetDependency} + onSetPrerequisite={onSetPrerequisite} + onSetTaskCategory={onSetTaskCategory} > {nestedChildren.length ? ( ) : null} ); - }); + } + + return [ + ...groupedTasks.uncategorized.filter((task) => !visited.has(task.id)).map(renderTask), + ...groupedTasks.categories.map((group) => { + const groupTasks = group.tasks.filter((task) => !visited.has(task.id)); + if (!groupTasks.length) return null; + const groupCount = countTaskTree(groupTasks.map((task) => task.id), tasks, parentMap); + const categoryId = getCategorySectionId(parentId, group.category); + const sectionClassName = [ + "task-planner-category-section", + draggingCategoryId === categoryId ? "is-dragging" : "", + categoryDropTarget.id === categoryId ? "is-drop-target" : "", + categoryDropTarget.id === categoryId && categoryDropTarget.placement === "after" ? "drop-after" : "" + ].filter(Boolean).join(" "); + return ( +
  • +
    +
    + +

    {group.category}

    +
    + {groupCount} +
    + +
  • + ); + }) + ].filter(Boolean); } function TaskPlannerItem({ @@ -415,26 +598,32 @@ function TaskPlannerItem({ textContent, descriptionOpen, settingsOpen, - dependencyNoticeVisible, + prerequisiteNoticeVisible, onDragStart, onToggleDescription, onToggleSettings, - onDependencyNotice, + onPrerequisiteNotice, onUpdateTask, onDeleteTask, onAddLinkTarget, onRemoveLinkTarget, - onSetDependency, + onSetPrerequisite, + onSetTaskCategory, children }) { + const parentTask = parentId ? getTaskById(tasks, parentId) : null; + const inheritedCategory = parentTask ? getEffectiveTaskCategory(parentTask, tasks, parentMap) : ""; + const categoryValue = parentTask ? inheritedCategory : task.category || ""; + const categoryEditable = !parentTask; + const [categoryDraft, setCategoryDraft] = useState(categoryValue); const relations = data.relations.filter((relation) => relation.fromTaskId === task.id); - const missingDependencies = relations.filter((relation) => relation.dependency && !tasks.find((item) => item.id === relation.toTaskId)?.checked); + const missingPrerequisites = relations.filter((relation) => relation.prerequisite && !tasks.find((item) => item.id === relation.toTaskId)?.checked); const relationTargets = tasks.filter((item) => item.id !== task.id && !relations.some((relation) => relation.toTaskId === item.id)); - const noticeMessage = dependencyNoticeVisible && missingDependencies.length > 0 ? textContent.missingDependencyNotice || "Pré requis parent non effectué" : ""; + const noticeMessage = prerequisiteNoticeVisible && missingPrerequisites.length > 0 ? textContent.missingPrerequisiteNotice || "Pré requis parent non effectué" : ""; const className = [ "task-planner-item", task.checked ? "is-complete" : "", - missingDependencies.length ? "has-missing-dependency" : "", + missingPrerequisites.length ? "has-missing-prerequisite" : "", draggingId === task.id ? "is-dragging" : "", dropTarget.id === task.id ? "is-drop-target" : "", dropTarget.id === task.id && dropTarget.placement === "after" ? "drop-after" : "" @@ -442,7 +631,7 @@ function TaskPlannerItem({ function updateChecked(event) { const checked = event.target.checked; - if (checked && missingDependencies.length > 0) onDependencyNotice(task.id); + if (checked && missingPrerequisites.length > 0) onPrerequisiteNotice(task.id); onUpdateTask(task.id, (current) => ({ ...current, checked, @@ -450,8 +639,16 @@ function TaskPlannerItem({ })); } + useEffect(() => { + setCategoryDraft(categoryValue); + }, [categoryValue, task.id]); + + function commitCategoryDraft() { + if (categoryEditable && categoryDraft.trim() !== (task.category || "")) onSetTaskCategory(task.id, categoryDraft); + } + return ( -
  • +