diff --git a/website/src/features/toolboxes/modules/EquipmentPlannerModule.jsx b/website/src/features/toolboxes/modules/EquipmentPlannerModule.jsx index 512204e..580f728 100644 --- a/website/src/features/toolboxes/modules/EquipmentPlannerModule.jsx +++ b/website/src/features/toolboxes/modules/EquipmentPlannerModule.jsx @@ -6,70 +6,19 @@ import { Tooltip } from "../../../components/Tooltip.jsx"; import { applyGroupedReorderOperation, moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js"; import { useInlineEdit } from "../../../hooks/useInlineEdit.js"; import { TextExchangeActions } from "./TextExchangeActions.jsx"; - -const SOCKET_SHAPES = ["ball", "jewel"]; -const SOCKET_COLORS = ["red", "orange", "amber", "yellow", "lime", "green", "teal", "cyan", "blue", "indigo", "violet", "purple", "pink", "rose", "white", "gray", "black"]; -const SOCKET_COLOR_VALUES = { - red: "#ef4444", - orange: "#f97316", - amber: "#f59e0b", - yellow: "var(--color-accent-gold)", - lime: "#84cc16", - green: "#22c55e", - teal: "#14b8a6", - cyan: "var(--color-accent-cyan)", - blue: "#3b82f6", - indigo: "var(--color-accent-indigo)", - violet: "#7c3aed", - purple: "var(--color-primary)", - pink: "var(--color-accent-pink)", - rose: "#f43f5e", - white: "#f8fafc", - gray: "#64748b", - black: "#020617" -}; -const EQUIPMENT_ICONS = [ - { icon: "helmet", label: "Casque" }, - { icon: "chest-armor", label: "Chest" }, - { icon: "glove", label: "Gants" }, - { icon: "boot", label: "Bottes" }, - { icon: "necklace", label: "Collier" }, - { icon: "earring", label: "Earring" }, - { icon: "ring", label: "Anneau" }, - { icon: "sword", label: "Épée" }, - { icon: "gun", label: "Gun" }, - { icon: "shield", label: "Bouclier" } -]; -const TRAIT_ICONS = ["sword", "shield"]; -const DETAIL_TABS = ["stats", "sockets", "craft"]; - -function getCharacteristicCategories(equipment) { - const categories = equipment.characteristics.map((trait) => trait.category).filter(Boolean); - return [ - ...equipment.categoryOrder.filter((category) => categories.includes(category)), - ...categories.filter((category, index) => categories.indexOf(category) === index && !equipment.categoryOrder.includes(category)) - ]; -} - -function setItemCategory(item, category) { - const nextItem = { ...item }; - if (category) nextItem.category = category; - else delete nextItem.category; - return nextItem; -} - -function formatValue(value) { - if (typeof value === "number") return Number.isInteger(value) ? String(value) : String(Math.round(value * 1000) / 1000); - return String(value || ""); -} - -function createLocalId(prefix) { - return globalThis.crypto?.randomUUID?.() || `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; -} - -function getSocketItemName(equipment, socketItemId) { - return equipment.socketItems.find((socketItem) => socketItem.id === socketItemId)?.name || ""; -} +import { + DETAIL_TABS, + EQUIPMENT_ICONS, + SOCKET_COLORS, + SOCKET_COLOR_VALUES, + SOCKET_SHAPES, + TRAIT_ICONS, + createLocalId, + formatValue, + getCharacteristicCategories, + getSocketItemName, + setItemCategory +} from "./equipmentPlannerOptions.js"; function SocketShapeIcon({ shape, color = "gray", className = "" }) { return ( diff --git a/website/src/features/toolboxes/modules/TaskPlannerItems.jsx b/website/src/features/toolboxes/modules/TaskPlannerItems.jsx new file mode 100644 index 0000000..fff3b41 --- /dev/null +++ b/website/src/features/toolboxes/modules/TaskPlannerItems.jsx @@ -0,0 +1,522 @@ +// Rôle : rend les branches, items et panneaux secondaires du task planner. +import { Icon } from "../../../components/Icon.jsx"; +import { InlineNotice } from "../../../components/AppOverlays.jsx"; +import { Tooltip } from "../../../components/Tooltip.jsx"; +import { useInlineEdit } from "../../../hooks/useInlineEdit.js"; +import { + TASK_TYPES, + WEEK_DAYS, + countCompletedTaskTree, + countTaskTree, + getEffectiveTaskCategory, + getTaskById, + getTaskCategoryDataset, + getTaskCategoryGroups, + getTaskTitle, + getTaskTypeLabel, + getTasksForParent, + isTaskTreeComplete +} from "./taskPlannerUtils.js"; + +export function TaskPlannerBranch({ + parentId, + parentMap, + tasks, + data, + 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 ? ( + + ) : null; + if (data.hideCompleted && task.checked) { + if (!hasVisibleDescendants(task.id)) return null; + return ( +
  • +
    + 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}`} + /> + {task.title} + {getTaskTypeLabel(task.type, textContent)} +
    + +
  • + ); + } + return ( + onToggleDescription(task.id)} + onToggleSettings={() => onToggleSettings(task.id)} + onPrerequisiteNotice={onPrerequisiteNotice} + onUpdateTask={onUpdateTask} + onDeleteTask={onDeleteTask} + onAddLinkTarget={onAddLinkTarget} + onRemoveLinkTarget={onRemoveLinkTarget} + onSetPrerequisite={onSetPrerequisite} + onSetTaskCategory={onSetTaskCategory} + > + {nestedBranch} + + ); + } + + 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 ? ( +
  • + ) : null; + const afterDropZone = showCategoryBoundaryDropZones ? ( +
  • + ) : null; + return [ + beforeDropZone, +
  • +
    +
    + +

    {group.category}

    +
    +
    + {groupCompletedCount} / {groupCount} + +
    +
    + {!isCollapsed && ( + + )} +
  • , + afterDropZone + ]; + } + + const entries = groupedTasks.entries.map((entry) => { + if (entry.type === "task") return visited.has(entry.task.id) ? null : renderTask(entry.task); + return renderCategory(entry.group); + }).filter(Boolean); + return entries; +} + +function TaskPlannerItem({ + task, + tasks, + data, + parentMap, + parentId, + reorder, + textContent, + descriptionOpen, + settingsOpen, + prerequisiteNoticeVisible, + onToggleDescription, + onToggleSettings, + onPrerequisiteNotice, + onUpdateTask, + onDeleteTask, + onAddLinkTarget, + onRemoveLinkTarget, + 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 relations = data.relations.filter((relation) => relation.fromTaskId === task.id); + 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 = prerequisiteNoticeVisible && missingPrerequisites.length > 0 ? textContent.missingPrerequisiteNotice || "Pré requis parent non effectué" : ""; + const className = [ + "task-planner-item", + task.checked ? "is-complete" : "", + missingPrerequisites.length ? "has-missing-prerequisite" : "", + reorder.isItemDragging(task.id) ? "is-dragging" : "", + reorder.isItemDropTarget(task.id) ? "is-drop-target" : "", + reorder.getDropPlacement("item", task.id) === "after" ? "drop-after" : "" + ].filter(Boolean).join(" "); + const titleEdit = useInlineEdit({ + value: task.title, + transform: (draft) => String(draft || "").trim() || task.title, + onCommit: (title) => onUpdateTask(task.id, (current) => ({ ...current, title })) + }); + const categoryEdit = useInlineEdit({ + value: categoryValue, + onCommit: (category) => { + if (categoryEditable) onSetTaskCategory(task.id, category); + } + }); + const descriptionEdit = useInlineEdit({ + value: task.description, + onCommit: (description) => onUpdateTask(task.id, (current) => ({ ...current, description })), + commitOnEnter: false + }); + + function updateChecked(event) { + const checked = event.target.checked; + if (checked && missingPrerequisites.length > 0) onPrerequisiteNotice(task.id); + onUpdateTask(task.id, (current) => ({ + ...current, + checked, + checkedAt: checked ? Date.now() : 0 + })); + } + + return ( +
  • +
    + + + + + {missingPrerequisites.length > 0 && ( + } + position="top-start" + > + {textContent.missingPrerequisiteBadge || "Pré requis"} + + )} +
    + + + +
    +
    + + {descriptionOpen && ( +
    + +