// Rôle : fournit l'outil checklist avec quantités, catégories et échange texte. import { useState } from "react"; import { Icon } from "../../../components/Icon.jsx"; import { useGroupedReorder, moveItem } from "../../../hooks/useGroupedReorder.js"; import { useInlineEdit } from "../../../hooks/useInlineEdit.js"; import { TextExchangeActions } from "./TextExchangeActions.jsx"; export function ChecklistModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) { const data = context.normalizeChecklistData(context.getModuleData(toolboxId, moduleId, { items: [] })); const textContent = context.moduleText?.checklist || {}; const [label, setLabel] = useState(""); const [sectionTitle, setSectionTitle] = useState(""); const [qty, setQty] = useState(1); const namedSections = data.sections.filter((section) => section.title); const completedSectionsMode = data.hideCompletedSectionsFully ? "hidden" : data.hideCompletedSections ? "reduced" : "visible"; const reorderItems = getChecklistReorderItems(data.sections); const { itemReorder, groupReorder, getItemProps, getGroupProps, getGroupBoundaryProps, isItemDragging, isItemDropTarget, isGroupDragging, isGroupDropTarget, isBoundaryDropTarget, getDropPlacement, shouldShowGroupBoundaries } = useGroupedReorder({ namespace: `checklist-${moduleId}`, items: reorderItems, getItemId: (item) => item.id, getItemGroup: (item) => item.sectionId, getEffectiveGroup: (item) => item.sectionId, reorderFeatures: { item: { groupChange: true, boundaryDrop: true }, group: { reorder: true, boundaryDrop: true } }, onItemMove: (operation) => save(moveChecklistItem(data.sections, operation, context)), onGroupMove: (operation) => save(moveItem(data.sections, operation.sourceId, operation.targetId, operation.placement)) }); const reorder = { itemReorder, groupReorder, getItemProps, getGroupProps, getGroupBoundaryProps, isItemDragging, isItemDropTarget, isGroupDragging, isGroupDropTarget, isBoundaryDropTarget, getDropPlacement, shouldShowGroupBoundaries }; const showBoundaryDropZones = reorder.shouldShowGroupBoundaries(); const visibleSections = data.sections.filter((section) => !(section.title && data.hideCompletedSectionsFully && isSectionComplete(section, context))); function save(sections, settings = {}) { context.setModuleData(toolboxId, moduleId, { ...data, ...settings, sections }); } function appendItems(sections, title, items) { const cleanTitle = title.trim(); const existingIndex = sections.findIndex((section) => section.title.toLowerCase() === cleanTitle.toLowerCase()); if (existingIndex >= 0) { return sections.map((section, index) => index === existingIndex ? { ...section, items: [...section.items, ...items] } : section); } return [...sections, { id: context.uid("section"), title: cleanTitle, items }]; } function addItem(event) { event.preventDefault(); const cleanLabel = label.trim(); if (!cleanLabel) return; const nextItem = { id: context.uid("item"), label: cleanLabel, qtyTarget: Math.max(1, Number(qty) || 1), qtyCurrent: 0 }; save(appendItems(data.sections, sectionTitle, [nextItem])); setLabel(""); setQty(1); } function setCompletedSectionsMode(mode) { save(data.sections.map((section) => ({ ...section, hideWhenComplete: undefined })), { hideCompletedSections: mode === "reduced", hideCompletedSectionsFully: mode === "hidden" }); } function updateSection(sectionId, updater) { save(data.sections.map((section) => section.id === sectionId ? updater(section) : section).filter(keepChecklistSection)); } function renameSection(sectionId, title) { save(renameChecklistSection(data.sections, sectionId, title)); } function updateItem(itemId, updater) { save(updateChecklistItem(data.sections, itemId, updater)); } function moveItemToCategory(itemId, title) { save(moveChecklistItemToCategory(data.sections, itemId, title, context)); } function deleteItem(itemId) { save(data.sections.map((section) => ({ ...section, items: section.items.filter((item) => item.id !== itemId) })).filter(keepChecklistSection)); } return ( <> {editing && (
setSectionTitle(event.target.value)} /> setLabel(event.target.value)} /> setQty(event.target.value)} aria-label={textContent.quantityLabel || "Quantité cible"} />
)} {textExchangeOpen && (
context.setModuleData(toolboxId, moduleId, nextData, "checklist")} />
)} {namedSections.length > 0 && (
{[ { mode: "visible", label: textContent.completedSectionsVisibleLabel || "Visible", icon: "eye-open" }, { mode: "reduced", label: textContent.completedSectionsReducedLabel || "Réduit", icon: "hide" }, { mode: "hidden", label: textContent.completedSectionsHiddenLabel || "Caché", icon: "eye-closed" } ].map((option) => ( ))}
)} ); } function isSectionComplete(section, context) { return section.items.length > 0 && section.items.every((item) => context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget); } function getSectionHideWhenComplete(section, globalHide) { return typeof section.hideWhenComplete === "boolean" ? section.hideWhenComplete : globalHide; } function keepChecklistSection(section) { return section.items.length; } function getChecklistReorderItems(sections) { return sections.flatMap((section) => section.items.map((item) => ({ ...item, sectionId: section.id }))); } function findChecklistSectionByItemId(sections, itemId) { return sections.find((section) => section.items.some((item) => item.id === itemId)); } function renameChecklistSection(sections, sectionId, title) { const cleanTitle = title.trim(); const section = sections.find((entry) => entry.id === sectionId); if (!section) return sections; const existingSection = sections.find((entry) => entry.id !== sectionId && entry.title.toLowerCase() === cleanTitle.toLowerCase()); if (!existingSection) { return sections.map((entry) => entry.id === sectionId ? { ...entry, title: cleanTitle } : entry).filter(keepChecklistSection); } return sections.map((entry) => { if (entry.id === existingSection.id) return { ...entry, items: [...entry.items, ...section.items] }; return entry; }).filter((entry) => entry.id !== sectionId).filter(keepChecklistSection); } function updateChecklistItem(sections, itemId, updater) { return sections.map((section) => ({ ...section, items: section.items.map((item) => item.id === itemId ? updater(item) : item).filter((item) => item.label) })).filter(keepChecklistSection); } function moveChecklistItemToCategory(sections, itemId, title, context) { const cleanTitle = title.trim(); const sourceSection = findChecklistSectionByItemId(sections, itemId); const item = sourceSection?.items.find((entry) => entry.id === itemId); if (!sourceSection || !item || sourceSection.title.toLowerCase() === cleanTitle.toLowerCase()) return sections; const targetSection = sections.find((section) => section.title.toLowerCase() === cleanTitle.toLowerCase()); const sectionsWithoutItem = sections.map((section) => ({ ...section, items: section.items.filter((entry) => entry.id !== itemId) })).filter(keepChecklistSection); if (targetSection) { return sectionsWithoutItem.map((section) => section.id === targetSection.id ? { ...section, items: [...section.items, item] } : section); } return [...sectionsWithoutItem, { id: context.uid("section"), title: cleanTitle, items: [item] }]; } function moveChecklistItem(sections, operation, context) { const sourceSection = findChecklistSectionByItemId(sections, operation.sourceId); const sourceItem = sourceSection?.items.find((item) => item.id === operation.sourceId); if (!sourceSection || !sourceItem) return sections; if (operation.targetType === "boundary") return moveChecklistItemToUngroupedBoundary(sections, sourceSection, sourceItem, operation, context); const targetSection = operation.targetType === "item" ? findChecklistSectionByItemId(sections, operation.targetId) : sections.find((section) => section.id === operation.targetGroup); if (!targetSection) return sections; const sectionsWithoutItem = sections.map((section) => ({ ...section, items: section.items.filter((item) => item.id !== operation.sourceId) })).filter(keepChecklistSection); return sectionsWithoutItem.map((section) => { if (section.id !== targetSection.id) return section; if (operation.targetType === "item") { const targetIndex = section.items.findIndex((item) => item.id === operation.targetId); if (targetIndex < 0) return { ...section, items: [...section.items, sourceItem] }; const nextItems = [...section.items]; nextItems.splice(operation.placement === "after" ? targetIndex + 1 : targetIndex, 0, sourceItem); return { ...section, items: nextItems }; } return { ...section, items: operation.placement === "after" ? [...section.items, sourceItem] : [sourceItem, ...section.items] }; }); } function moveChecklistItemToUngroupedBoundary(sections, sourceSection, sourceItem, operation, context) { const targetIndex = sections.findIndex((section) => section.id === operation.targetGroup); if (targetIndex < 0) return sections; const preferredUngroupedSection = sourceSection.title ? sections.find((section) => !section.title && section.id !== sourceSection.id) : sourceSection; const ungroupedSectionId = preferredUngroupedSection?.id || context.uid("section"); const baseSections = sections .map((section) => ({ ...section, items: section.items.filter((item) => item.id !== sourceItem.id) })) .filter((section) => section.id !== ungroupedSectionId) .filter(keepChecklistSection); const targetSection = sections[targetIndex]; const nextTargetIndex = baseSections.findIndex((section) => section.id === targetSection.id); const insertIndex = nextTargetIndex < 0 ? Math.min(targetIndex, baseSections.length) : nextTargetIndex + (operation.placement === "after" ? 1 : 0); const ungroupedSection = { id: ungroupedSectionId, title: "", items: [...(preferredUngroupedSection?.items || []).filter((item) => item.id !== sourceItem.id), sourceItem] }; const nextSections = [...baseSections]; nextSections.splice(insertIndex, 0, ungroupedSection); return nextSections; } function ChecklistSection({ section, context, data, reorder, onRenameSection, onUpdateSection, onSaveSections, onUpdateItem, onMoveItemToCategory, onDeleteItem }) { const showTitle = Boolean(section.title); const sectionComplete = showTitle && isSectionComplete(section, context); const hideWhenComplete = getSectionHideWhenComplete(section, data.hideCompletedSections); const isAutoCollapsed = sectionComplete && hideWhenComplete; const isCollapsed = Boolean(section.collapsed) || isAutoCollapsed; const textContent = context.moduleText?.checklist || {}; const className = [ "checklist-section", showTitle ? "is-grouped" : "", sectionComplete ? "is-complete" : "", isCollapsed ? "is-collapsed" : "", reorder.isGroupDragging(section.id) ? "is-dragging" : "", reorder.isGroupDropTarget(section.id) ? "is-drop-target" : "", reorder.getDropPlacement("group", section.id) === "after" ? "drop-after" : "" ].filter(Boolean).join(" "); const titleEdit = useInlineEdit({ value: section.title, onCommit: (title) => onRenameSection(section.id, title) }); const [editingItemId, setEditingItemId] = useState(""); if (sectionComplete && data.hideCompletedSectionsFully) return null; function saveItems(items) { onUpdateSection(section.id, (entry) => ({ ...entry, items })); } function toggleSectionCollapsed() { const nextCollapsed = !isCollapsed; const nextSections = data.sections.map((entry) => { if (entry.id === section.id) { const nextSection = { ...entry }; if (nextCollapsed) { if (sectionComplete) { nextSection.hideWhenComplete = true; delete nextSection.collapsed; } else { nextSection.collapsed = true; } } else { delete nextSection.collapsed; if (sectionComplete) nextSection.hideWhenComplete = false; } return nextSection; } if (data.hideCompletedSections && !nextCollapsed && entry.title && isSectionComplete(entry, context)) return { ...entry, hideWhenComplete: true }; return entry; }); const nextCompletedSections = nextSections.filter((entry) => entry.title && isSectionComplete(entry, context)); const allHidden = nextCompletedSections.length > 0 && nextCompletedSections.every((entry) => getSectionHideWhenComplete(entry, false)); onSaveSections(nextSections, { hideCompletedSections: allHidden }); } return (
  • {showTitle && (

    {section.items.filter((item) => context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget).length} / {section.items.length}
    )} {!isCollapsed && ( )}
  • ); } function ChecklistItem({ item, section, context, reorder, items, save, onUpdateItem, onMoveItemToCategory, onDeleteItem, editing, onToggleEditing }) { const done = context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget; const textContent = context.moduleText?.checklist || {}; const className = [ "checklist-item", done ? "is-complete" : "", reorder.isItemDragging(item.id) ? "is-dragging" : "", reorder.isItemDropTarget(item.id) ? "is-drop-target" : "", reorder.getDropPlacement("item", item.id) === "after" ? "drop-after" : "" ].filter(Boolean).join(" "); const labelEdit = useInlineEdit({ value: item.label, transform: (draft) => String(draft || "").trim() || item.label, onCommit: (label) => onUpdateItem(item.id, (entry) => ({ ...entry, label })), autoFocus: editing, focusKey: item.id }); const categoryEdit = useInlineEdit({ value: section.title, onCommit: (title) => onMoveItemToCategory(item.id, title) }); function updateItem(updater) { save(items.map((entry) => entry.id === item.id ? updater(entry) : entry)); } function toggleChecked() { updateItem((entry) => ({ ...entry, qtyCurrent: done ? 0 : entry.qtyTarget })); } return (
  • {item.qtyTarget === 1 ? ( updateItem((entry) => ({ ...entry, qtyTarget: 1, qtyCurrent: event.target.checked ? 1 : 0 }))} aria-label={`Terminer ${item.label}`} /> ) : (
    )} {editing ? ( <> ) : ( )}
  • ); }