sokko-g/website/src/features/toolboxes/modules/TaskPlannerBranch.jsx
Shinuwa a0d7e80a1a
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s
Split remaining toolbox modules by responsibility
2026-08-25 09:33:15 +02:00

199 lines
8.3 KiB
JavaScript

// Rôle : rend les branches récursives, catégories et zones de drop du task planner.
import { Icon } from "../../../components/Icon.jsx";
import { TaskPlannerItem } from "./TaskPlannerItem.jsx";
import {
countCompletedTaskTree,
countTaskTree,
getTaskTypeLabel,
getTasksForParent,
getTaskCategoryGroups,
isTaskTreeComplete
} from "./taskPlannerUtils.js";
export function TaskPlannerBranch({
parentId,
parentMap,
tasks,
data,
nowMs,
reorder,
textContent,
openDescriptions,
openTaskSettings,
prerequisiteNoticeTaskIds,
onToggleDescription,
onToggleSettings,
onPrerequisiteNotice,
onUpdateTask,
onDeleteTask,
onAddLinkTarget,
onRemoveLinkTarget,
onSetPrerequisite,
onSetTaskCategory,
onToggleCategoryCollapsed,
visited = new Set()
}) {
const groupedTasks = getTaskCategoryGroups(tasks, parentMap, parentId, 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)]);
const showCategoryBoundaryDropZones = !parentId && reorder.shouldShowGroupBoundaries({ parentId });
function hasVisibleDescendants(taskId, seen = new Set()) {
if (seen.has(taskId)) return false;
seen.add(taskId);
return getTasksForParent(tasks, parentMap, taskId).some((child) => !child.checked || hasVisibleDescendants(child.id, seen));
}
function renderTask(task) {
const nestedChildren = getTasksForParent(tasks, parentMap, task.id).filter((child) => !nextVisited.has(child.id));
const nestedBranch = nestedChildren.length ? (
<TaskPlannerBranch
parentId={task.id}
parentMap={parentMap}
tasks={tasks}
data={data}
nowMs={nowMs}
reorder={reorder}
textContent={textContent}
openDescriptions={openDescriptions}
openTaskSettings={openTaskSettings}
prerequisiteNoticeTaskIds={prerequisiteNoticeTaskIds}
onToggleDescription={onToggleDescription}
onToggleSettings={onToggleSettings}
onPrerequisiteNotice={onPrerequisiteNotice}
onUpdateTask={onUpdateTask}
onDeleteTask={onDeleteTask}
onAddLinkTarget={onAddLinkTarget}
onRemoveLinkTarget={onRemoveLinkTarget}
onSetPrerequisite={onSetPrerequisite}
onSetTaskCategory={onSetTaskCategory}
onToggleCategoryCollapsed={onToggleCategoryCollapsed}
visited={nextVisited}
/>
) : null;
if (data.hideCompleted && task.checked) {
if (!hasVisibleDescendants(task.id)) return null;
return (
<li className="task-planner-hidden-parent" key={`${task.id}:visible-children`}>
<div className="task-planner-hidden-parent-line">
<input
type="checkbox"
checked={task.checked}
onChange={(event) => onUpdateTask(task.id, (current) => ({
...current,
checked: event.target.checked,
checkedAt: event.target.checked ? Date.now() : 0
}))}
aria-label={`${textContent.uncheckTitle || "Marquer non effectué"} ${task.title}`}
/>
<span title={task.title}>{task.title}</span>
<small>{getTaskTypeLabel(task.type, textContent)}</small>
</div>
<ul className="task-planner-list task-planner-child-list">{nestedBranch}</ul>
</li>
);
}
return (
<TaskPlannerItem
key={task.id}
task={task}
tasks={tasks}
data={data}
nowMs={nowMs}
parentMap={parentMap}
parentId={parentId}
reorder={reorder}
textContent={textContent}
descriptionOpen={openDescriptions.has(task.id)}
settingsOpen={openTaskSettings.has(task.id)}
prerequisiteNoticeVisible={prerequisiteNoticeTaskIds.has(task.id)}
onToggleDescription={() => onToggleDescription(task.id)}
onToggleSettings={() => onToggleSettings(task.id)}
onPrerequisiteNotice={onPrerequisiteNotice}
onUpdateTask={onUpdateTask}
onDeleteTask={onDeleteTask}
onAddLinkTarget={onAddLinkTarget}
onRemoveLinkTarget={onRemoveLinkTarget}
onSetPrerequisite={onSetPrerequisite}
onSetTaskCategory={onSetTaskCategory}
>
{nestedBranch}
</TaskPlannerItem>
);
}
function renderCategory(group) {
const groupTasks = group.tasks.filter((task) => !visited.has(task.id));
if (!groupTasks.length) return null;
if (data.hideCompleted && isTaskTreeComplete(groupTasks.map((task) => task.id), tasks, parentMap)) return null;
const groupCount = countTaskTree(groupTasks.map((task) => task.id), tasks, parentMap);
const groupCompletedCount = countCompletedTaskTree(groupTasks.map((task) => task.id), tasks, parentMap);
const isCollapsed = data.collapsedCategories.includes(group.category);
const sectionClassName = [
"task-planner-category-section",
groupCompletedCount === groupCount ? "is-complete" : "",
isCollapsed ? "is-collapsed" : "",
reorder.isGroupDragging(group.category, parentId) ? "is-dragging" : "",
reorder.isGroupDropTarget(group.category, parentId) ? "is-drop-target" : "",
reorder.getDropPlacement("group", group.category, parentId) === "after" ? "drop-after" : ""
].filter(Boolean).join(" ");
const beforeDropZone = showCategoryBoundaryDropZones ? (
<li
className={`task-planner-category-boundary-drop-zone ${reorder.isBoundaryDropTarget({ groupId: group.category, parentId, placement: "before" }) ? "is-drop-target" : ""}`}
key={`${parentId || "root"}:${group.category}:before-drop`}
{...reorder.getGroupBoundaryProps({ groupId: group.category, parentId, placement: "before" })}
/>
) : null;
const afterDropZone = showCategoryBoundaryDropZones ? (
<li
className={`task-planner-category-boundary-drop-zone ${reorder.isBoundaryDropTarget({ groupId: group.category, parentId, placement: "after" }) ? "is-drop-target" : ""}`}
key={`${parentId || "root"}:${group.category}:after-drop`}
{...reorder.getGroupBoundaryProps({ groupId: group.category, parentId, placement: "after" })}
/>
) : null;
return [
beforeDropZone,
<li className={sectionClassName} key={`${parentId || "root"}:${group.category}`} {...reorder.getGroupProps({ groupId: group.category, parentId })}>
<div className="checklist-section-header task-planner-category-header">
<div className="task-planner-category-title">
<button
className="task-planner-category-drag-handle"
type="button"
onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: group.category, parentId })}
aria-label={`${textContent.categoryReorderTitle || "Déplacer la catégorie"} ${group.category}`}
title={textContent.categoryReorderTitle || "Déplacer la catégorie"}
>
<Icon name="drag" />
</button>
<h3>{group.category}</h3>
</div>
<div className="task-planner-category-actions">
<span>{groupCompletedCount} / {groupCount}</span>
<button
className="checklist-section-collapse-button"
type="button"
onClick={() => onToggleCategoryCollapsed(group.category)}
aria-expanded={!isCollapsed}
title={isCollapsed ? textContent.showCategoryTitle || "Afficher cette catégorie" : textContent.hideCategoryTitle || "Réduire cette catégorie"}
aria-label={isCollapsed ? textContent.showCategoryTitle || "Afficher cette catégorie" : textContent.hideCategoryTitle || "Réduire cette catégorie"}
>
<Icon name={isCollapsed ? "chevron-down" : "chevron-up"} />
</button>
</div>
</div>
{!isCollapsed && (
<ul className="task-planner-list">
{groupTasks.map(renderTask).filter(Boolean)}
</ul>
)}
</li>,
afterDropZone
];
}
return groupedTasks.entries.map((entry) => {
if (entry.type === "task") return visited.has(entry.task.id) ? null : renderTask(entry.task);
return renderCategory(entry.group);
}).filter(Boolean);
}