205 lines
9.1 KiB
JavaScript
205 lines
9.1 KiB
JavaScript
// Rôle : fournit l'outil checklist avec quantités, catégories et échange texte.
|
|
import { useState } from "react";
|
|
import { CategoryAutocompleteInput } from "../../../components/CategoryAutocompleteInput.jsx";
|
|
import { Icon } from "../../../components/Icon.jsx";
|
|
import { useGroupedReorder, moveItem } from "../../../hooks/useGroupedReorder.js";
|
|
import { ChecklistSection } from "./ChecklistParts.jsx";
|
|
import { ModuleEmptyMessage } from "./ModuleEmptyMessage.jsx";
|
|
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
|
import {
|
|
appendChecklistItems,
|
|
getChecklistReorderItems,
|
|
isSectionComplete,
|
|
keepChecklistSection,
|
|
moveChecklistItem,
|
|
moveChecklistItemToCategory,
|
|
renameChecklistSection,
|
|
updateChecklistItem
|
|
} from "./checklistUtils.js";
|
|
|
|
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 categoryOptions = namedSections.map((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 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(appendChecklistItems(data.sections, sectionTitle, [nextItem], context));
|
|
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 && (
|
|
<div className="module-add-panel">
|
|
<form className="inline-form checklist-add-form" onSubmit={addItem}>
|
|
<CategoryAutocompleteInput className="nds-input" data-size="sm" name="section" placeholder={textContent.sectionPlaceholder || "Catégorie"} value={sectionTitle} onChange={(event) => setSectionTitle(event.target.value)} options={categoryOptions} />
|
|
<input className="nds-input" data-size="sm" name="label" type="text" placeholder={textContent.itemPlaceholder || "Nouvel item"} value={label} onChange={(event) => setLabel(event.target.value)} />
|
|
<input className="checklist-qty-input nds-input" data-size="sm" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label={textContent.quantityLabel || "Quantité cible"} />
|
|
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addButton || "Ajouter"}</button>
|
|
</form>
|
|
</div>
|
|
)}
|
|
{textExchangeOpen && (
|
|
<div className="module-text-exchange-panel">
|
|
<TextExchangeActions type="checklist" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "checklist")} />
|
|
</div>
|
|
)}
|
|
<div className="checklist-toolbar">
|
|
<button
|
|
className={`checklist-hide-completed-toggle nds-switch ${data.hideCompletedItems ? "active" : ""}`}
|
|
data-size="lg"
|
|
type="button"
|
|
onClick={() => save(data.sections, { hideCompletedItems: !data.hideCompletedItems })}
|
|
aria-pressed={data.hideCompletedItems}
|
|
aria-label={textContent.hideCompletedItemsTitle || "Masquer les éléments terminés"}
|
|
title={textContent.hideCompletedItemsTitle || "Masquer les éléments terminés"}
|
|
>
|
|
<Icon name={data.hideCompletedItems ? "eye-closed" : "eye-open"} />
|
|
<i aria-hidden="true" />
|
|
</button>
|
|
{namedSections.length > 0 && (
|
|
<div className="checklist-complete-mode-group nds-radio-button-group" data-size="md" role="radiogroup" aria-label={textContent.completedSectionsModeTitle || "Affichage des catégories terminées"}>
|
|
{[
|
|
{ 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) => (
|
|
<button
|
|
className={`checklist-complete-mode-button ${completedSectionsMode === option.mode ? "active" : ""}`}
|
|
type="button"
|
|
role="radio"
|
|
aria-checked={completedSectionsMode === option.mode}
|
|
aria-label={option.label}
|
|
key={option.mode}
|
|
onClick={() => setCompletedSectionsMode(option.mode)}
|
|
title={option.label}
|
|
>
|
|
<Icon name={option.icon} />
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{!data.sections.length && <ModuleEmptyMessage>{textContent.emptyItems || "Aucun élément dans cette checklist."}</ModuleEmptyMessage>}
|
|
<ul className="checklist">
|
|
{visibleSections.map((section, index) => {
|
|
const isLastSection = index === visibleSections.length - 1;
|
|
return [
|
|
showBoundaryDropZones ? (
|
|
<li
|
|
className={`checklist-category-boundary-drop-zone ${reorder.isBoundaryDropTarget({ groupId: section.id, placement: "before" }) ? "is-drop-target" : ""}`}
|
|
key={`${section.id}:before-drop`}
|
|
{...reorder.getGroupBoundaryProps({ groupId: section.id, placement: "before" })}
|
|
/>
|
|
) : null,
|
|
<ChecklistSection
|
|
key={section.id}
|
|
section={section}
|
|
data={data}
|
|
context={context}
|
|
reorder={reorder}
|
|
onRenameSection={renameSection}
|
|
onUpdateSection={updateSection}
|
|
onSaveSections={save}
|
|
onUpdateItem={updateItem}
|
|
onMoveItemToCategory={moveItemToCategory}
|
|
onDeleteItem={deleteItem}
|
|
categoryOptions={categoryOptions}
|
|
/>,
|
|
showBoundaryDropZones && isLastSection ? (
|
|
<li
|
|
className={`checklist-category-boundary-drop-zone ${reorder.isBoundaryDropTarget({ groupId: section.id, placement: "after" }) ? "is-drop-target" : ""}`}
|
|
key={`${section.id}:after-drop`}
|
|
{...reorder.getGroupBoundaryProps({ groupId: section.id, placement: "after" })}
|
|
/>
|
|
) : null
|
|
];
|
|
})}
|
|
</ul>
|
|
</>
|
|
);
|
|
}
|