diff --git a/eslint.config.js b/eslint.config.js index e628cdc..c9c9357 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -41,7 +41,7 @@ export default [ varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }], - "react-refresh/only-export-components": "off" + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }] } }, { diff --git a/tests/static-styles.test.mjs b/tests/static-styles.test.mjs index 0fe0d53..b4f6cc3 100644 --- a/tests/static-styles.test.mjs +++ b/tests/static-styles.test.mjs @@ -13,7 +13,18 @@ test("style entrypoint and theme layers are wired", async () => { const styleCards = await readFile("website/src/styles/_cards.scss", "utf8"); const styleHome = await readFile("website/src/styles/_home.scss", "utf8"); const styleToolboxesEntry = await readFile("website/src/styles/_toolboxes.scss", "utf8"); - const styleToolboxes = await readFile("website/src/styles/toolboxes/_core.scss", "utf8"); + const styleToolboxesCore = await readFile("website/src/styles/toolboxes/_core.scss", "utf8"); + const styleToolboxLayout = await readFile("website/src/styles/toolboxes/_layout.scss", "utf8"); + const styleToolboxShell = await readFile("website/src/styles/toolboxes/_module-shell.scss", "utf8"); + const styleToolboxTextExchange = await readFile("website/src/styles/toolboxes/_text-exchange.scss", "utf8"); + const styleToolboxNotepad = await readFile("website/src/styles/toolboxes/_notepad.scss", "utf8"); + const styleToolboxChecklist = await readFile("website/src/styles/toolboxes/_checklist.scss", "utf8"); + const styleToolboxCalculator = await readFile("website/src/styles/toolboxes/_calculator.scss", "utf8"); + const styleToolboxCalculatorResults = await readFile("website/src/styles/toolboxes/_calculator-results.scss", "utf8"); + const styleToolboxTable = await readFile("website/src/styles/toolboxes/_table.scss", "utf8"); + const styleToolboxCombos = await readFile("website/src/styles/toolboxes/_combos.scss", "utf8"); + const styleToolboxImages = await readFile("website/src/styles/toolboxes/_images.scss", "utf8"); + const styleToolboxSharedControls = await readFile("website/src/styles/toolboxes/_shared-controls.scss", "utf8"); const styleOverlays = await readFile("website/src/styles/_overlays.scss", "utf8"); const styleIcons = await readFile("website/src/styles/_icons.scss", "utf8"); const styleGames = await readFile("website/src/styles/_games.scss", "utf8"); @@ -24,6 +35,11 @@ test("style entrypoint and theme layers are wired", async () => { assert.match(styles, /@use "nebula"/); assert.match(styles, /@use "toolboxes"/); assert.match(styleToolboxesEntry, /@use "toolboxes\/core"/); + assert.match(styleToolboxesCore, /@use "layout"/); + assert.match(styleToolboxesCore, /@use "module-shell"/); + assert.match(styleToolboxesCore, /@use "task-planner"/); + assert.match(styleToolboxesCore, /@use "equipment-planner"/); + assert.match(styleToolboxesCore, /@use "shared-controls"/); assert.match(styles, /@use "mhwilds"/); assert.match(styles, /@use "diablo4"/); assert.match(styleTokens, /--gradient-brand/); @@ -48,22 +64,22 @@ test("style entrypoint and theme layers are wired", async () => { assert.match(styleMixins, /@mixin gold-panel-frame/); assert.match(styleShell, /\.sidebar-about-link/); assert.match(styleShell, /\.sidebar-mascot/); - assert.match(styleToolboxes, /\.module-icon-abacus/); - assert.match(styleToolboxes, /\.module-icon-calculator/); - assert.match(styleToolboxes, /\.module-icon-columns/); - assert.match(styleToolboxes, /\.module-icon-table/); - assert.match(styleToolboxes, /\.calculator-module/); - assert.match(styleToolboxes, /\.table-module/); - assert.match(styleToolboxes, /\.table-cell-input\.is-selected/); - assert.match(styleToolboxes, /\.table-cell-input\.is-move-target/); - assert.match(styleToolboxes, /\.notepad-toolbar-button\.danger/); - assert.match(styleToolboxes, /\.toolbox-icon-picker/); - assert.match(styleToolboxes, /\.toolbox-game-icon/); - assert.match(styleToolboxes, /\.toolbox-hero-link/); - assert.match(styleToolboxes, /\.toolbox-actions-row/); - assert.match(styleToolboxes, /\.text-import-actions/); - assert.match(styleToolboxes, /\.text-import-modal-dialog/); - assert.doesNotMatch(styleToolboxes, /lecture seule/); + assert.match(styleToolboxShell, /\.module-icon-abacus/); + assert.match(styleToolboxShell, /\.module-icon-calculator/); + assert.match(styleToolboxShell, /\.module-icon-columns/); + assert.match(styleToolboxShell, /\.module-icon-table/); + assert.match(styleToolboxCalculator, /\.calculator-module/); + assert.match(styleToolboxTable, /\.table-module/); + assert.match(styleToolboxTable, /\.table-cell-input\.is-selected/); + assert.match(styleToolboxTable, /\.table-cell-input\.is-move-target/); + assert.match(styleToolboxSharedControls, /\.notepad-toolbar-button\.danger/); + assert.match(styleToolboxLayout, /\.toolbox-icon-picker/); + assert.match(styleToolboxLayout, /\.toolbox-game-icon/); + assert.match(styleToolboxLayout, /\.toolbox-hero-link/); + assert.match(styleToolboxLayout, /\.toolbox-actions-row/); + assert.match(styleToolboxTextExchange, /\.text-import-actions/); + assert.match(styleToolboxTextExchange, /\.text-import-modal-dialog/); + assert.doesNotMatch(styleToolboxNotepad, /lecture seule/); assert.match(styleIcons, /ui-icon-drag/); assert.match(styleIcons, /ui-icon-dropdown/); assert.match(styleIcons, /ui-icon-enter/); @@ -85,12 +101,17 @@ test("style entrypoint and theme layers are wired", async () => { assert.match(styleIcons, /ui-icon-grab/); assert.match(styleIcons, /ui-icon-shield/); assert.match(styleIcons, /ui-icon-gun/); - assert.match(styleToolboxes, /module-icon-chest-armor/); - assert.match(styleToolboxes, /\.combo-hold-controls/); - assert.match(styleToolboxes, /\.combo-input-direction/); - assert.match(styleToolboxes, /\.combo-device-n64\.combo-value-stick-up/); - assert.match(styleToolboxes, /\.combo-device-n64\.combo-value-up/); - assert.match(styleToolboxes, /\.combo-value-grab \.ui-icon/); + assert.match(styleToolboxShell, /module-icon-chest-armor/); + assert.match(styleToolboxCombos, /\.combo-hold-controls/); + assert.match(styleToolboxCombos, /\.combo-input-direction/); + assert.match(styleToolboxCombos, /\.combo-device-n64\.combo-value-stick-up/); + assert.match(styleToolboxCombos, /\.combo-device-n64\.combo-value-up/); + assert.match(styleToolboxCombos, /\.combo-value-grab \.ui-icon/); + assert.match(styleToolboxChecklist, /\.timer-alert-mode-button/); + assert.match(styleToolboxSharedControls, /\.timer-alert-mode-switch\.is-blocked/); + assert.match(styleToolboxCalculatorResults, /\.calculator-result/); + assert.match(styleToolboxSharedControls, /\.tool-split-scroll-toggle/); + assert.match(styleToolboxImages, /\.annotation-marker/); assert.match(styleGames, /\.game-home-card/); assert.match(styleGames, /\.game-layout/); assert.match(styleGames, /\.game-grid/); diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index 313b650..500c63a 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -2,6 +2,8 @@ import { readFile } from "node:fs/promises"; import { test } from "node:test"; import assert from "node:assert/strict"; +import { SOCKET_SHAPES, formatValue, getSocketItemName, setItemCategory } from "../website/src/features/toolboxes/modules/equipmentPlannerOptions.js"; +import { applyDueResets, getTaskParentMap, getTaskTreeIds, moveTask } from "../website/src/features/toolboxes/modules/taskPlannerUtils.js"; test("toolbox storage, cards and pages are wired", async () => { const source = await readFile("website/src/main.jsx", "utf8"); @@ -111,7 +113,10 @@ test("toolbox module registry and modules expose expected behavior", async () => const tableFormulaEngine = await readFile("website/src/features/toolboxes/modules/tableFormulaEngine.js", "utf8"); const timerModule = await readFile("website/src/features/toolboxes/modules/TimerModule.jsx", "utf8"); const taskPlannerModule = await readFile("website/src/features/toolboxes/modules/TaskPlannerModule.jsx", "utf8"); + const taskPlannerItems = await readFile("website/src/features/toolboxes/modules/TaskPlannerItems.jsx", "utf8"); + const taskPlannerUtils = await readFile("website/src/features/toolboxes/modules/taskPlannerUtils.js", "utf8"); const equipmentPlannerModule = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerModule.jsx", "utf8"); + const equipmentPlannerOptions = await readFile("website/src/features/toolboxes/modules/equipmentPlannerOptions.js", "utf8"); const checklistModule = await readFile("website/src/features/toolboxes/modules/ChecklistModule.jsx", "utf8"); const imagesModule = await readFile("website/src/features/toolboxes/modules/ImagesModule.jsx", "utf8"); const imageAnnotationModule = await readFile("website/src/features/toolboxes/modules/ImageAnnotationModule.jsx", "utf8"); @@ -253,17 +258,20 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(taskPlannerModule, /export function TaskPlannerModule/); assert.match(taskPlannerModule, /normalizeTaskPlannerData/); assert.match(taskPlannerModule, /applyDueResets/); - assert.match(taskPlannerModule, /relations/); - assert.match(taskPlannerModule, /prerequisite/); - assert.match(taskPlannerModule, /categoryOrder/); - assert.match(taskPlannerModule, /categoryLabel/); - assert.match(taskPlannerModule, /task-planner-warning/); + assert.match(taskPlannerModule, /TaskPlannerBranch/); assert.match(taskPlannerModule, /useGroupedReorder/); - assert.match(taskPlannerModule, /useInlineEdit/); assert.match(taskPlannerModule, /useDraftForm/); - assert.match(taskPlannerModule, /commitOnEnter: false/); assert.match(taskPlannerModule, /TextExchangeActions/); assert.match(taskPlannerModule, /reorderFeatures/); + assert.match(taskPlannerItems, /export function TaskPlannerBranch/); + assert.match(taskPlannerItems, /function TaskPlannerItem/); + assert.match(taskPlannerItems, /task-planner-warning/); + assert.match(taskPlannerItems, /useInlineEdit/); + assert.match(taskPlannerItems, /commitOnEnter: false/); + assert.match(taskPlannerItems, /categoryLabel/); + assert.match(taskPlannerItems, /prerequisite/); + assert.match(taskPlannerUtils, /export function getTaskParentMap/); + assert.match(taskPlannerUtils, /export function applyDueResets/); assert.doesNotMatch(taskPlannerModule, /canMoveItem:/); assert.doesNotMatch(taskPlannerModule, /canMoveGroup:/); assert.match(equipmentPlannerModule, /export function EquipmentPlannerModule/); @@ -274,6 +282,8 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(equipmentPlannerModule, /useGroupedReorder/); assert.match(equipmentPlannerModule, /reorderFeatures/); assert.match(equipmentPlannerModule, /TextExchangeActions/); + assert.match(equipmentPlannerOptions, /export const EQUIPMENT_ICONS/); + assert.match(equipmentPlannerOptions, /export function formatValue/); assert.match(checklistModule, /export function ChecklistModule/); assert.match(checklistModule, /TextExchangeActions/); assert.match(checklistModule, /editing &&/); @@ -354,3 +364,35 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(imageViewer, / { + const tasks = [ + { id: "a", title: "A", type: "daily", checked: true, checkedAt: Date.UTC(2026, 0, 1, 7) }, + { id: "b", title: "B", type: "unique", checked: false }, + { id: "c", title: "C", type: "weekly", checked: true, checkedAt: Date.UTC(2026, 0, 1, 7) } + ]; + const relations = [{ id: "r1", fromTaskId: "b", toTaskId: "a", prerequisite: false }]; + const parentMap = getTaskParentMap(tasks, relations); + + assert.equal(parentMap.get("b"), "a"); + assert.deepEqual(getTaskTreeIds(["a"], tasks, parentMap), ["a", "b"]); + assert.deepEqual(moveTask(tasks, "c", "a").map((task) => task.id), ["c", "a", "b"]); + + const resetData = applyDueResets({ + tasks, + relations, + resetTime: "06:00", + weeklyResetDay: 4, + categoryOrder: [], + lastResetAt: Date.UTC(2026, 0, 1, 5) + }, Date.UTC(2026, 0, 2, 8)); + assert.equal(resetData.tasks.find((task) => task.id === "a").checked, false); +}); + +test("equipment planner options expose stable formatting and socket helpers", () => { + assert.deepEqual(SOCKET_SHAPES, ["ball", "jewel"]); + assert.equal(formatValue(1.23456), "1.235"); + assert.deepEqual(setItemCategory({ id: "item" }, "DPS"), { id: "item", category: "DPS" }); + assert.deepEqual(setItemCategory({ id: "item", category: "DPS" }, ""), { id: "item" }); + assert.equal(getSocketItemName({ socketItems: [{ id: "gem", name: "Ruby" }] }, "gem"), "Ruby"); +}); diff --git a/website/src/features/games/CopyChecklistItemsButton.jsx b/website/src/features/games/CopyChecklistItemsButton.jsx index 70855e8..609b6e0 100644 --- a/website/src/features/games/CopyChecklistItemsButton.jsx +++ b/website/src/features/games/CopyChecklistItemsButton.jsx @@ -1,4 +1,5 @@ // Rôle : copie des données filtrées au format compatible avec l'import checklist. +/* eslint-disable react-refresh/only-export-components -- expose aussi les formatters publics testés pour l'import checklist. */ import { useState } from "react"; import { Icon } from "../../components/Icon.jsx"; diff --git a/website/src/features/toolboxes/ToolboxCard.jsx b/website/src/features/toolboxes/ToolboxCard.jsx index 09a7d55..35e841b 100644 --- a/website/src/features/toolboxes/ToolboxCard.jsx +++ b/website/src/features/toolboxes/ToolboxCard.jsx @@ -1,4 +1,5 @@ // Rôle : affiche une toolbox dans la liste, avec actions rapides et icône personnalisable. +/* eslint-disable react-refresh/only-export-components -- expose aussi les helpers publics de rendu des cartes toolbox. */ import { CompactDropdown } from "../../components/CompactDropdown.jsx"; import { Icon } from "../../components/Icon.jsx"; import { 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)} +
    +
      {nestedBranch}
    +
  • + ); + } + 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 && ( +
      + {groupTasks.map(renderTask).filter(Boolean)} +
    + )} +
  • , + 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 && ( +
    + +