// Rôle : regroupe les actions métier de création, import/export, lien et édition toolbox. import { compactModuleDataForStorage, createGlobalExportPayload, createToolboxExportPayload, globalModuleKey, labelFromFileName, normalizeToolbox, remapModuleOrderIds, uid } from "./storage/toolboxStorage.js"; import { compressImage } from "../../utils/imageCompression.js"; function downloadJson(payload, filename) { const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = filename; link.click(); URL.revokeObjectURL(link.href); } function getLibraryUrl() { if (typeof window === "undefined") return "https://sokko-g.local/#/library"; return `${window.location.origin}${window.location.pathname}#/library`; } export function useToolboxActions({ store, notify, creationTemplate, setConfirmModal, setCreateModal, setLinkModalGameId, setDrawerGameId, setImage }) { function createToolbox(name, gameId = "", options = {}) { const now = new Date().toISOString(); const memosModuleId = uid("mod"); const linksModuleId = uid("mod"); const welcomeText = creationTemplate?.memos?.text || creationTemplate?.notepad?.text || "Profite bien de ta nouvelle toolbox."; const libraryLink = { id: uid("link"), title: creationTemplate?.links?.title || "Librairie des outils", url: getLibraryUrl() }; const toolbox = { id: uid("tbx"), name: name.trim() || "Nouvelle toolbox", updatedAt: now, modules: [ { id: memosModuleId, type: "memos", title: creationTemplate?.memos?.title || "Mémos" }, { id: linksModuleId, type: "links", title: creationTemplate?.links?.moduleTitle || "Liens utiles" } ] }; if (options.tutorial === true) toolbox.tutorial = true; const memos = compactModuleDataForStorage("memos", { items: [{ id: uid("memo"), text: welcomeText, color: "#f6c453", updatedAt: now }] }); const links = compactModuleDataForStorage("links", { links: [libraryLink] }); void store.persistToolboxSnapshot((current) => ({ toolboxes: [toolbox, ...current.toolboxes], links: gameId ? { ...current.links, [gameId]: toolbox.id } : current.links, moduleData: { ...current.moduleData, [globalModuleKey(toolbox.id, memosModuleId)]: memos, [globalModuleKey(toolbox.id, linksModuleId)]: links } })); return toolbox; } function updateToolbox(nextToolbox) { const updatedAt = new Date().toISOString(); store.setToolboxes((toolboxes) => toolboxes.map((toolbox) => toolbox.id === nextToolbox.id ? { ...nextToolbox, updatedAt } : toolbox)); } function deleteToolbox(id) { return store.persistToolboxSnapshot((current) => { const toolbox = current.toolboxes.find((item) => item.id === id); const moduleData = { ...current.moduleData }; (toolbox?.modules || []).forEach((module) => delete moduleData[`${id}:${module.id}`]); return { toolboxes: current.toolboxes.filter((item) => item.id !== id), moduleData, links: Object.fromEntries(Object.entries(current.links).filter(([, toolboxId]) => toolboxId !== id)) }; }); } function linkToolboxToGame(gameId, toolboxId) { const updatedAt = new Date().toISOString(); return store.persistToolboxSnapshot((current) => { const links = { ...current.links }; const touched = new Set([links[gameId], toolboxId].filter(Boolean)); if (toolboxId) links[gameId] = toolboxId; else delete links[gameId]; return { ...current, links, toolboxes: current.toolboxes.map((toolbox) => touched.has(toolbox.id) ? { ...toolbox, updatedAt } : toolbox) }; }); } async function addImageFiles(toolboxId, moduleId, files) { const imageFiles = [...files].filter((file) => file?.type?.startsWith("image/")); if (!imageFiles.length) return false; const images = []; for (const file of imageFiles) { const image = { id: uid("image"), dataUrl: await compressImage(file) }; const label = labelFromFileName(file.name); if (label) image.label = label; images.unshift(image); } const data = store.getModuleData(toolboxId, moduleId, { images: [] }); return store.updateModuleData(toolboxId, moduleId, { ...data, images: [...images, ...(Array.isArray(data.images) ? data.images : [])] }); } function createChecklistFromList(gameId, list) { const toolboxId = store.links[gameId] || ""; const toolbox = store.toolboxes.find((item) => item.id === toolboxId); if (!toolbox || !list) return false; const moduleId = uid("mod"); const sections = (list.categories || []).map((category) => ({ id: uid("section"), title: category.title || "", items: (category.items || []).map((item) => ({ id: uid("item"), label: item.name, qtyTarget: Math.max(1, Number.parseInt(item.quantity, 10) || 1), qtyCurrent: 0 })).filter((item) => item.label) })).filter((section) => section.items.length); if (!sections.length) return false; setConfirmModal({ title: "Créer une checklist", message: `Créer la checklist "${list.title || "Checklist"}" dans la toolbox "${toolbox.name}" ?`, confirmLabel: "Créer", onResolve: (confirmed) => { if (!confirmed) return; const updatedAt = new Date().toISOString(); store.setToolboxes((toolboxes) => toolboxes.map((item) => item.id === toolboxId ? { ...item, modules: [...item.modules, { id: moduleId, type: "checklist", title: list.title || "Checklist" }], updatedAt } : item)); store.updateModuleData(toolboxId, moduleId, { sections }, "checklist"); notify("Checklist créée dans la toolbox associée."); } }); return true; } async function importToolboxPayload(file, gameId = "") { const payload = JSON.parse(await file.text()); if (!payload.toolbox?.modules) throw new Error("Format d'outils invalide"); const imported = normalizeToolbox({ ...payload.toolbox, id: uid("tbx"), updatedAt: new Date().toISOString() }); const moduleIdMap = new Map(); imported.modules = imported.modules.map((module) => { const nextId = uid("mod"); moduleIdMap.set(module.id, nextId); return { ...module, id: nextId }; }); imported.moduleOrder = remapModuleOrderIds(imported.moduleOrder, moduleIdMap); const normalizedImport = normalizeToolbox(imported); const nextModuleData = {}; Object.entries(payload.modules || {}).forEach(([oldId, data]) => { const nextId = moduleIdMap.get(oldId); const module = normalizedImport.modules.find((item) => item.id === nextId); if (nextId) { const compact = compactModuleDataForStorage(module?.type, data); if (compact) nextModuleData[`${normalizedImport.id}:${nextId}`] = compact; } }); const saved = await store.persistToolboxSnapshot((current) => ({ toolboxes: [normalizedImport, ...current.toolboxes], links: gameId ? { ...current.links, [gameId]: normalizedImport.id } : current.links, moduleData: { ...current.moduleData, ...nextModuleData } })); if (!saved) return null; return normalizedImport; } async function importAllToolboxesPayload(file) { const payload = JSON.parse(await file.text()); if (!Array.isArray(payload.toolboxes) || !payload.modules || typeof payload.modules !== "object") { throw new Error("Format d'import global invalide"); } const toolboxIdMap = new Map(); const moduleIdMap = new Map(); const importedToolboxes = payload.toolboxes.map((toolbox) => { const nextToolboxId = uid("tbx"); toolboxIdMap.set(toolbox.id, nextToolboxId); const localModuleIdMap = new Map(); const modules = (toolbox.modules || []).map((module) => { const nextModuleId = uid("mod"); moduleIdMap.set(globalModuleKey(toolbox.id, module.id), nextModuleId); localModuleIdMap.set(module.id, nextModuleId); return { ...module, id: nextModuleId }; }); const moduleOrder = remapModuleOrderIds(toolbox.moduleOrder, localModuleIdMap); return normalizeToolbox({ ...toolbox, id: nextToolboxId, name: `${toolbox.name || "Toolbox"} (import)`, modules, moduleOrder, updatedAt: new Date().toISOString() }); }); const nextLinks = {}; Object.entries(payload.links || {}).forEach(([gameId, oldToolboxId]) => { const nextToolboxId = toolboxIdMap.get(oldToolboxId); if (nextToolboxId) nextLinks[gameId] = nextToolboxId; }); const nextModuleData = {}; Object.entries(payload.modules).forEach(([key, data]) => { const [oldToolboxId] = key.split(":"); const nextToolboxId = toolboxIdMap.get(oldToolboxId); const nextModuleId = moduleIdMap.get(key); const toolbox = importedToolboxes.find((item) => item.id === nextToolboxId); const module = toolbox?.modules.find((item) => item.id === nextModuleId); const compact = compactModuleDataForStorage(module?.type, data); if (nextToolboxId && nextModuleId && compact) nextModuleData[`${nextToolboxId}:${nextModuleId}`] = compact; }); const saved = await store.persistToolboxSnapshot((current) => ({ toolboxes: [...importedToolboxes, ...current.toolboxes], links: { ...current.links, ...nextLinks }, moduleData: { ...current.moduleData, ...nextModuleData } })); if (!saved) throw new Error("Impossible d'enregistrer l'import global."); } return { createToolbox, updateToolbox, deleteToolbox, linkToolboxToGame, setCreateModal, setConfirmModal, setLinkModalGameId, setDrawerGameId, setImage, createChecklistFromList, addImageFiles, notify, removeModuleData: store.removeModuleData, updateModuleData: store.updateModuleData, updateToolboxOrder: (orderedIds) => { const order = new Map(orderedIds.map((id, index) => [id, index])); store.setToolboxes((toolboxes) => [...toolboxes].sort((a, b) => (order.get(a.id) ?? 9999) - (order.get(b.id) ?? 9999))); }, importToolbox: async (file, gameId = "") => { try { const toolbox = await importToolboxPayload(file, gameId); if (toolbox) notify("Toolbox importée."); return toolbox; } catch (error) { setConfirmModal({ title: "Import impossible", message: error.message, confirmLabel: "Compris", cancelLabel: "Fermer", danger: true }); return null; } }, importAllToolboxes: async (file) => { try { await importAllToolboxesPayload(file); notify("Import global terminé."); return true; } catch (error) { setConfirmModal({ title: "Import global impossible", message: error.message, confirmLabel: "Compris", cancelLabel: "Fermer", danger: true }); return false; } }, exportToolbox: (id) => { const toolbox = normalizeToolbox(store.toolboxes.find((item) => item.id === id)); if (!toolbox) return; downloadJson(createToolboxExportPayload(toolbox, store.moduleData), `${toolbox.name.toLowerCase().replace(/[^a-z0-9]+/gi, "-")}.sokkog.json`); notify("Export de la toolbox lancé."); }, exportAllToolboxes: () => { downloadJson(createGlobalExportPayload(store.toolboxes, store.links, store.moduleData), `sokkog-global-${new Date().toISOString().slice(0, 10)}.json`); notify("Export global lancé."); } }; }