// Rôle : fournit l'outil checklist avec quantités, catégories et imports texte. import { useCallback, useState } from "react"; import { Icon } from "../../../components/Icon.jsx"; import { TextImportModal } from "./TextImportModal.jsx"; import { parseColonImportLines } from "./textImport.js"; export function ChecklistModule({ toolboxId, moduleId, context, editing }) { 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 [textImport, setTextImport] = useState(""); const [importOpen, setImportOpen] = useState(false); const closeImportModal = useCallback(() => setImportOpen(false), []); const namedSections = data.sections.filter((section) => section.title); const completedSectionsMode = data.hideCompletedSectionsFully ? "hidden" : data.hideCompletedSections ? "reduced" : "visible"; 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 importItems(event) { event.preventDefault(); const importedSections = parseChecklistImport(textImport, context); if (!importedSections.length) return; const nextSections = importedSections.reduce((sections, section) => appendItems(sections, section.title, section.items), data.sections); save(nextSections); setTextImport(""); setImportOpen(false); context.notify?.("Import checklist terminé."); } function setCompletedSectionsMode(mode) { save(data.sections.map((section) => ({ ...section, hideWhenComplete: undefined })), { hideCompletedSections: mode === "reduced", hideCompletedSectionsFully: mode === "hidden" }); } return ( <> {editing && (
setSectionTitle(event.target.value)} /> setLabel(event.target.value)} /> setQty(event.target.value)} aria-label={textContent.quantityLabel || "Quantité cible"} />
)} {importOpen && ( )} {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 parseChecklistImport(text, context) { const sections = []; let currentSection = { id: context.uid("section"), title: "", items: [] }; String(text || "").split(/\r?\n/).forEach((line) => { const cleanLine = line.trim(); if (!cleanLine) return; if (cleanLine.startsWith("#")) { if (currentSection.items.length || currentSection.title) sections.push(currentSection); currentSection = { id: context.uid("section"), title: cleanLine.replace(/^#+/, "").trim(), items: [] }; return; } parseColonImportLines(cleanLine).forEach((entry) => { currentSection.items.push({ id: context.uid("item"), label: entry.label, qtyTarget: Math.max(1, Number.parseInt(entry.value, 10) || 1), qtyCurrent: 0 }); }); }); if (currentSection.items.length || currentSection.title) sections.push(currentSection); return sections.filter((section) => section.items.length); } 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 CategoryTitle({ title }) { return String(title || "").split(/([♂♀])/g).map((part, index) => ( part === "♂" || part === "♀" ? {part} : part )); } function ChecklistSection({ section, context, sections, data, save }) { 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; if (sectionComplete && data.hideCompletedSectionsFully) return null; function saveItems(items) { save(sections.map((entry) => entry.id === section.id ? { ...entry, items } : entry).filter((entry) => entry.items.length || entry.title)); } function toggleSectionCollapsed() { const nextCollapsed = !isCollapsed; const nextSections = 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)); save(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, context, items, save }) { const done = context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget; function updateItem(updater) { save(items.map((entry) => entry.id === item.id ? updater(entry) : entry)); } return (
  • {item.qtyTarget === 1 ? ( updateItem((entry) => ({ ...entry, qtyTarget: 1, qtyCurrent: event.target.checked ? 1 : 0 }))} aria-label={`Terminer ${item.label}`} /> ) : (
    )} {item.label}
  • ); }