Compare commits
2 Commits
551d42a251
...
ccea6b0367
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccea6b0367 | ||
|
|
f640a3b1ee |
269
app.js
269
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 = '<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");
|
||||
|
||||
19
index.html
19
index.html
@@ -132,6 +132,16 @@
|
||||
<div class="games-actions-bar">
|
||||
<button id="loanedFilterBtn" type="button" class="btn-secondary">Voir jeux pretes</button>
|
||||
</div>
|
||||
<div id="bulkActionsBar" class="bulk-actions-bar">
|
||||
<label class="checkbox-row">
|
||||
<input id="bulkSelectPage" type="checkbox" />
|
||||
Tout selectionner (page)
|
||||
</label>
|
||||
<span id="bulkSelectionInfo" class="bulk-selection-info">0 selectionne</span>
|
||||
<button id="bulkLoanBtn" type="button" class="btn-secondary" disabled>Marquer prete</button>
|
||||
<button id="bulkReturnBtn" type="button" class="btn-secondary" disabled>Marquer rendu</button>
|
||||
<button id="bulkDeleteBtn" type="button" class="btn-inline danger" disabled>Supprimer</button>
|
||||
</div>
|
||||
<div class="scanner-zone">
|
||||
<div class="scanner-header">
|
||||
<strong>Scan camera (mobile)</strong>
|
||||
@@ -210,12 +220,21 @@
|
||||
<input id="coverUrlInput" type="hidden" />
|
||||
|
||||
<div id="gamesList" class="games-list"></div>
|
||||
<div id="paginationBar" class="pagination-bar">
|
||||
<button id="prevPageBtn" type="button" class="btn-secondary">Precedent</button>
|
||||
<span id="pageInfo" class="page-info">Page 1/1</span>
|
||||
<button id="nextPageBtn" type="button" class="btn-secondary">Suivant</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<div id="toastContainer" class="toast-container" aria-live="polite" aria-atomic="true"></div>
|
||||
|
||||
<template id="gameCardTemplate">
|
||||
<article class="game-card">
|
||||
<label class="game-select">
|
||||
<input type="checkbox" data-action="select" />
|
||||
<span>Selection</span>
|
||||
</label>
|
||||
<img class="game-cover hidden" alt="Pochette du jeu" loading="lazy" />
|
||||
<div class="game-main">
|
||||
<h3 class="game-title"></h3>
|
||||
|
||||
64
styles.css
64
styles.css
@@ -244,6 +244,24 @@ h1 {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.bulk-actions-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.8rem;
|
||||
padding: 0.55rem 0.65rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #f7faff;
|
||||
}
|
||||
|
||||
.bulk-selection-info {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.scanner-zone {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
@@ -439,6 +457,20 @@ button {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.game-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.game-select input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.console-theme-default {
|
||||
background: #ffffff;
|
||||
}
|
||||
@@ -520,6 +552,21 @@ button {
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.pagination-bar {
|
||||
margin-top: 0.8rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 0.88rem;
|
||||
color: var(--muted);
|
||||
min-width: 98px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inline-editor {
|
||||
margin-top: 0.75rem;
|
||||
border-top: 1px dashed #ccd7e4;
|
||||
@@ -750,6 +797,10 @@ body.ui-v2 .game-card {
|
||||
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
||||
}
|
||||
|
||||
body.ui-v2 .game-select span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.ui-v2 .game-card:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: #d3cef9;
|
||||
@@ -814,6 +865,15 @@ body.ui-v2 .hero {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.bulk-actions-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.bulk-selection-info {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.grid-form,
|
||||
.game-form {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -864,4 +924,8 @@ body.ui-v2 .hero {
|
||||
width: 100%;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.pagination-bar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user