diff --git a/app.js b/app.js index 792cda8..43d20f6 100644 --- a/app.js +++ b/app.js @@ -61,6 +61,16 @@ const googleRestoreBtn = document.getElementById("googleRestoreBtn"); const quickSearchInput = document.getElementById("quickSearchInput"); const quickSearchResults = document.getElementById("quickSearchResults"); const loanedFilterBtn = document.getElementById("loanedFilterBtn"); +const bulkActionsBar = document.getElementById("bulkActionsBar"); +const bulkSelectPage = document.getElementById("bulkSelectPage"); +const bulkSelectionInfo = document.getElementById("bulkSelectionInfo"); +const bulkLoanBtn = document.getElementById("bulkLoanBtn"); +const bulkReturnBtn = document.getElementById("bulkReturnBtn"); +const bulkDeleteBtn = document.getElementById("bulkDeleteBtn"); +const paginationBar = document.getElementById("paginationBar"); +const prevPageBtn = document.getElementById("prevPageBtn"); +const nextPageBtn = document.getElementById("nextPageBtn"); +const pageInfo = document.getElementById("pageInfo"); const v2Toolbar = document.getElementById("v2Toolbar"); const v2SearchInput = document.getElementById("v2SearchInput"); const v2ConsoleFilter = document.getElementById("v2ConsoleFilter"); @@ -90,6 +100,10 @@ let scannerRunning = false; let scannerLoopId = null; let scannerLastCodeValue = ""; let scannerLastCodeAt = 0; +let selectedGameIds = new Set(); +let currentPage = 1; +let currentPageGameIds = []; +let currentTotalPages = 1; // V2 is now the default UI. Use ?ui=v1 to force legacy mode if needed. const uiParam = new URLSearchParams(window.location.search).get("ui"); const uiV2Enabled = uiParam !== "v1"; @@ -218,12 +232,72 @@ quickSearchInput.addEventListener("input", (event) => { loanedFilterBtn.addEventListener("click", () => { showLoanedOnly = !showLoanedOnly; + resetPaging(); + selectedGameIds.clear(); + renderGames(); +}); + +if (bulkSelectPage) { + bulkSelectPage.addEventListener("change", (event) => { + const checked = Boolean(event.target.checked); + if (checked) { + for (const id of currentPageGameIds) { + selectedGameIds.add(id); + } + } else { + for (const id of currentPageGameIds) { + selectedGameIds.delete(id); + } + } + renderGames(); + }); +} + +if (bulkLoanBtn) { + bulkLoanBtn.addEventListener("click", async () => { + await performBulkAction("loan"); + }); +} + +if (bulkReturnBtn) { + bulkReturnBtn.addEventListener("click", async () => { + await performBulkAction("return"); + }); +} + +if (bulkDeleteBtn) { + bulkDeleteBtn.addEventListener("click", async () => { + await performBulkAction("delete"); + }); +} + +if (prevPageBtn) { + prevPageBtn.addEventListener("click", () => { + if (currentPage > 1) { + currentPage -= 1; + renderGames(); + } + }); +} + +if (nextPageBtn) { + nextPageBtn.addEventListener("click", () => { + if (currentPage < currentTotalPages) { + currentPage += 1; + renderGames(); + } + }); +} + +window.addEventListener("resize", () => { renderGames(); }); if (v2SearchInput) { v2SearchInput.addEventListener("input", (event) => { v2SearchTerm = event.target.value.trim(); + resetPaging(); + selectedGameIds.clear(); renderGames(); }); } @@ -231,6 +305,8 @@ if (v2SearchInput) { if (v2ConsoleFilter) { v2ConsoleFilter.addEventListener("change", (event) => { v2ConsoleValue = event.target.value; + resetPaging(); + selectedGameIds.clear(); renderGames(); }); } @@ -238,6 +314,8 @@ if (v2ConsoleFilter) { if (v2GenreFilter) { v2GenreFilter.addEventListener("change", (event) => { v2GenreValue = event.target.value; + resetPaging(); + selectedGameIds.clear(); renderGames(); }); } @@ -245,6 +323,8 @@ if (v2GenreFilter) { if (v2SortSelect) { v2SortSelect.addEventListener("change", (event) => { v2SortValue = event.target.value || "title_asc"; + resetPaging(); + selectedGameIds.clear(); renderGames(); }); } @@ -409,6 +489,8 @@ gameForm.addEventListener("submit", async (event) => { } resetEditMode(); + resetPaging(); + selectedGameIds.clear(); persist(); markLocalDataForImport(); render(); @@ -433,6 +515,8 @@ brandTabs.addEventListener("click", (event) => { const consoles = state.brands[brand] || []; state.selectedConsole = consoles[0] || ""; resetEditMode(); + resetPaging(); + selectedGameIds.clear(); persist(); render(); }); @@ -453,6 +537,8 @@ consoleTabs.addEventListener("click", (event) => { state.selectedConsole = consoleName; resetEditMode(); + resetPaging(); + selectedGameIds.clear(); persist(); render(); }); @@ -574,6 +660,7 @@ gamesList.addEventListener("click", async (event) => { try { if (action === "delete") { await apiRequest(`/api/catalog/games/${id}`, { method: "DELETE" }); + selectedGameIds.delete(id); if (editingGameId === id) { resetEditMode(); } @@ -615,6 +702,7 @@ gamesList.addEventListener("click", async (event) => { return; } games.splice(idx, 1); + selectedGameIds.delete(id); if (editingGameId === id) { resetEditMode(); } @@ -643,6 +731,26 @@ gamesList.addEventListener("click", async (event) => { showToast("Action enregistree.", "success"); }); +gamesList.addEventListener("change", (event) => { + if (!(event.target instanceof Element)) { + return; + } + const target = event.target.closest('input[type="checkbox"][data-action="select"]'); + if (!(target instanceof HTMLInputElement)) { + return; + } + const id = target.dataset.id; + if (!id) { + return; + } + if (target.checked) { + selectedGameIds.add(id); + } else { + selectedGameIds.delete(id); + } + renderGames(); +}); + cancelEditBtn.addEventListener("click", () => { resetEditMode(); }); @@ -1040,6 +1148,55 @@ function conditionBadgeClass(conditionValue) { return "status-low"; } +function pageSizeForViewport() { + return window.innerWidth <= 640 ? 12 : 24; +} + +function resetPaging() { + currentPage = 1; +} + +function updateBulkAndPaginationUi(pageGames, totalFilteredCount) { + const pageIds = pageGames.map((game) => game.id); + const selectedOnPage = pageIds.filter((id) => selectedGameIds.has(id)).length; + const hasAnySelection = selectedGameIds.size > 0; + + currentPageGameIds = pageIds; + if (bulkSelectionInfo) { + bulkSelectionInfo.textContent = `${selectedGameIds.size} selectionne${selectedGameIds.size > 1 ? "s" : ""}`; + } + if (bulkLoanBtn) { + bulkLoanBtn.disabled = !hasAnySelection; + } + if (bulkReturnBtn) { + bulkReturnBtn.disabled = !hasAnySelection; + } + if (bulkDeleteBtn) { + bulkDeleteBtn.disabled = !hasAnySelection; + } + if (bulkSelectPage) { + bulkSelectPage.checked = pageIds.length > 0 && selectedOnPage === pageIds.length; + bulkSelectPage.indeterminate = selectedOnPage > 0 && selectedOnPage < pageIds.length; + bulkSelectPage.disabled = pageIds.length === 0; + } + + if (pageInfo) { + pageInfo.textContent = `Page ${currentPage}/${currentTotalPages}`; + } + if (prevPageBtn) { + prevPageBtn.disabled = currentPage <= 1; + } + if (nextPageBtn) { + nextPageBtn.disabled = currentPage >= currentTotalPages; + } + if (paginationBar) { + paginationBar.classList.toggle("hidden", totalFilteredCount <= pageSizeForViewport()); + } + if (bulkActionsBar) { + bulkActionsBar.classList.toggle("hidden", totalFilteredCount === 0); + } +} + function renderSearchResults() { if (!quickSearchResults) { return; @@ -1142,6 +1299,95 @@ function renderConsoleTabs() { } } +async function performBulkAction(action) { + const selectedIds = Array.from(selectedGameIds); + if (!selectedIds.length) { + return; + } + + let loanedTo = ""; + if (action === "loan") { + const borrower = window.prompt("Nom de la personne a qui tu pretes ces jeux :"); + if (borrower === null) { + return; + } + loanedTo = borrower.trim(); + if (!loanedTo) { + alert("Le nom est obligatoire pour marquer les jeux comme pretes."); + return; + } + } + + if (action === "delete") { + const confirmed = window.confirm(`Supprimer ${selectedIds.length} jeu(x) selectionne(s) ?`); + if (!confirmed) { + return; + } + } + + if (apiReachable && dataMode !== "local-pending-import") { + try { + for (const id of selectedIds) { + const gameRef = findGameById(id); + if (!gameRef) { + continue; + } + const { game, brand, consoleName } = gameRef; + if (action === "delete") { + await apiRequest(`/api/catalog/games/${id}`, { method: "DELETE" }); + continue; + } + if (action === "return") { + if (!game.loanedTo) { + continue; + } + const payload = buildGamePayload(game, brand, consoleName, { loanedTo: "" }); + await apiRequest(`/api/catalog/games/${id}`, { method: "PUT", body: payload }); + continue; + } + if (action === "loan") { + const payload = buildGamePayload(game, brand, consoleName, { loanedTo }); + await apiRequest(`/api/catalog/games/${id}`, { method: "PUT", body: payload }); + } + } + + selectedGameIds.clear(); + await refreshFromApi(state.selectedBrand, state.selectedConsole); + showToast("Action groupée enregistree.", "success"); + return; + } catch (error) { + console.error(error); + alert("Action groupée impossible via l'API."); + return; + } + } + + for (const id of selectedIds) { + const gameRef = findGameById(id); + if (!gameRef) { + continue; + } + const { games, idx } = gameRef; + if (action === "delete") { + games.splice(idx, 1); + continue; + } + if (action === "return") { + games[idx].loanedTo = ""; + continue; + } + if (action === "loan") { + games[idx].loanedTo = loanedTo; + } + } + + selectedGameIds.clear(); + persist(); + markLocalDataForImport(); + render(); + showToast("Action groupée enregistree.", "success"); +} + function renderGames() { const selectedConsole = state.selectedConsole; const inV2 = uiV2Enabled; @@ -1157,6 +1403,7 @@ function renderGames() { if (!inV2 && !showLoanedOnly && !selectedConsole) { gamesList.innerHTML = '
Ajoute une section pour commencer.
'; + updateBulkAndPaginationUi([], 0); return; } @@ -1210,14 +1457,27 @@ function renderGames() { } } - if (!games.length) { + const allCurrentIds = new Set(collectAllGames().map((game) => game.id)); + selectedGameIds = new Set(Array.from(selectedGameIds).filter((id) => allCurrentIds.has(id))); + + const totalFilteredCount = games.length; + const pageSize = pageSizeForViewport(); + currentTotalPages = Math.max(1, Math.ceil(totalFilteredCount / pageSize)); + if (currentPage > currentTotalPages) { + currentPage = currentTotalPages; + } + const startIdx = (currentPage - 1) * pageSize; + const pageGames = games.slice(startIdx, startIdx + pageSize); + updateBulkAndPaginationUi(pageGames, totalFilteredCount); + + if (!totalFilteredCount) { gamesList.innerHTML = showLoanedOnly ? 'Aucun jeu prete actuellement.
' : 'Aucun jeu pour ces filtres.
'; return; } - for (const game of games) { + for (const game of pageGames) { const card = gameCardTemplate.content.cloneNode(true); const article = card.querySelector(".game-card"); article.classList.add(consoleThemeClass(game.consoleName)); @@ -1282,6 +1542,7 @@ function renderGames() { const editBtn = card.querySelector('[data-action="edit"]'); const toggleBtn = card.querySelector('[data-action="toggle-loan"]'); const deleteBtn = card.querySelector('[data-action="delete"]'); + const selectInput = card.querySelector('input[type="checkbox"][data-action="select"]'); editBtn.dataset.id = game.id; editBtn.textContent = "✏️ Editer"; @@ -1296,6 +1557,10 @@ function renderGames() { deleteBtn.textContent = "🗑️ Supprimer"; deleteBtn.title = "Supprimer ce jeu"; deleteBtn.setAttribute("aria-label", "Supprimer ce jeu"); + if (selectInput instanceof HTMLInputElement) { + selectInput.dataset.id = game.id; + selectInput.checked = selectedGameIds.has(game.id); + } if (inlineEditingGameId === game.id) { const editor = document.createElement("div"); diff --git a/index.html b/index.html index cfd1211..fd23571 100644 --- a/index.html +++ b/index.html @@ -132,6 +132,16 @@ +