UI: add bulk actions and pagination for games list

This commit is contained in:
Ponte
2026-02-15 00:28:31 +01:00
parent 551d42a251
commit f640a3b1ee
3 changed files with 350 additions and 2 deletions

269
app.js
View File

@@ -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 = '<p class="empty">Ajoute une section pour commencer.</p>';
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
? '<p class="empty">Aucun jeu prete actuellement.</p>'
: '<p class="empty">Aucun jeu pour ces filtres.</p>';
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");