From 73c3e307447e83a2679b265f1c916de49314c7c1 Mon Sep 17 00:00:00 2001 From: Ponte Date: Wed, 11 Feb 2026 15:31:14 +0100 Subject: [PATCH] Import XLSX support: add collection fields and migration script --- README.md | 14 +++ api/server.js | 112 ++++++++++++++--- app.js | 24 ++++ index.html | 16 +++ scripts/import_collections_xlsx.py | 189 +++++++++++++++++++++++++++++ styles.css | 12 ++ 6 files changed, 352 insertions(+), 15 deletions(-) create mode 100755 scripts/import_collections_xlsx.py diff --git a/README.md b/README.md index ca6f573..7c84667 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,14 @@ Centraliser ta collection dans une interface rapide a utiliser, evolutive, et fa - Compteur visuel du nombre de jeux par console (bulle sur l'onglet) - Ajout de jeux avec champs: - titre + - version - genre - editeur + - double (oui/non) - annee + - prix d'achat - cote estimee + - etat - prete a - Edition d'une fiche existante - Suppression d'un jeu @@ -146,6 +150,16 @@ git pull - snapshot auto en base avant restore `replace` (`backup_snapshots`) - actions accessibles dans le panneau lateral `Outils` +## Import Excel (COLLECTIONS.xlsx) + +- Script: `scripts/import_collections_xlsx.py` +- Commande: + - `python3 scripts/import_collections_xlsx.py '/Users/beuz/Downloads/COLLECTIONS.xlsx' --api-base http://127.0.0.1:7001` +- Mapping consoles -> marques: + - `NES/SNES/Wii -> NINTENDO` + - `PS1/PS2/PS3/PS4/PS5 -> SONY` + - `XBOX 360 -> MICROSOFT` + ## Licence Projet prive personnel. diff --git a/api/server.js b/api/server.js index 2a765b6..00973e0 100644 --- a/api/server.js +++ b/api/server.js @@ -105,13 +105,21 @@ async function runMigrations() { title TEXT NOT NULL, genre TEXT, publisher TEXT, + game_version TEXT, + is_duplicate BOOLEAN NOT NULL DEFAULT FALSE, release_year INTEGER CHECK (release_year IS NULL OR (release_year >= 1970 AND release_year <= 2100)), + purchase_price NUMERIC(10,2) CHECK (purchase_price IS NULL OR purchase_price >= 0), estimated_value NUMERIC(10,2) CHECK (estimated_value IS NULL OR estimated_value >= 0), + condition_score NUMERIC(4,2), loaned_to TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); `); + await pool.query("ALTER TABLE games ADD COLUMN IF NOT EXISTS game_version TEXT;"); + await pool.query("ALTER TABLE games ADD COLUMN IF NOT EXISTS is_duplicate BOOLEAN NOT NULL DEFAULT FALSE;"); + await pool.query("ALTER TABLE games ADD COLUMN IF NOT EXISTS purchase_price NUMERIC(10,2);"); + await pool.query("ALTER TABLE games ADD COLUMN IF NOT EXISTS condition_score NUMERIC(4,2);"); await pool.query(` CREATE TABLE IF NOT EXISTS backup_snapshots ( @@ -210,8 +218,12 @@ async function getCatalogFull() { g.title, g.genre, g.publisher, + g.game_version, + g.is_duplicate, g.release_year, + g.purchase_price, g.estimated_value, + g.condition_score, g.loaned_to, g.created_at FROM games g @@ -237,8 +249,12 @@ async function getCatalogFull() { title: row.title, genre: row.genre || "", publisher: row.publisher || "", + version: row.game_version || "", + isDuplicate: Boolean(row.is_duplicate), year: row.release_year || null, + purchasePrice: row.purchase_price != null ? Number(row.purchase_price) : null, value: row.estimated_value != null ? Number(row.estimated_value) : null, + condition: row.condition_score != null ? Number(row.condition_score) : null, loanedTo: row.loaned_to || "", createdAt: row.created_at, }); @@ -272,8 +288,12 @@ async function exportCatalogDumpWithClient(client) { g.title, g.genre, g.publisher, + g.game_version, + g.is_duplicate, g.release_year, + g.purchase_price, g.estimated_value, + g.condition_score, g.loaned_to, g.created_at, g.updated_at @@ -307,8 +327,12 @@ async function exportCatalogDumpWithClient(client) { title: row.title, genre: row.genre || "", publisher: row.publisher || "", + version: row.game_version || "", + isDuplicate: Boolean(row.is_duplicate), year: row.release_year || null, + purchasePrice: row.purchase_price != null ? Number(row.purchase_price) : null, value: row.estimated_value != null ? Number(row.estimated_value) : null, + condition: row.condition_score != null ? Number(row.condition_score) : null, loanedTo: row.loaned_to || "", createdAt: row.created_at, updatedAt: row.updated_at, @@ -431,6 +455,7 @@ async function restoreCatalogDump(mode, dump) { const ensured = await ensureConsole(client, brand, consoleName); const consoleId = ensured.consoleId; const year = gameEntry && gameEntry.year != null && gameEntry.year !== "" ? Number(gameEntry.year) : null; + const version = normalizeText(gameEntry && gameEntry.version); const dedupeResult = await client.query( ` @@ -439,9 +464,10 @@ async function restoreCatalogDump(mode, dump) { WHERE console_id = $1 AND LOWER(title) = LOWER($2) AND COALESCE(release_year, 0) = COALESCE($3, 0) + AND COALESCE(LOWER(game_version), '') = COALESCE(LOWER($4), '') LIMIT 1; `, - [consoleId, title, year], + [consoleId, title, year, version || null], ); if (dedupeResult.rowCount) { @@ -451,17 +477,38 @@ async function restoreCatalogDump(mode, dump) { const genre = normalizeText(gameEntry && gameEntry.genre) || null; const publisher = normalizeText(gameEntry && gameEntry.publisher) || null; const loanedTo = normalizeText(gameEntry && gameEntry.loanedTo) || null; + const isDuplicate = Boolean(gameEntry && gameEntry.isDuplicate); + const purchasePrice = + gameEntry && gameEntry.purchasePrice != null && gameEntry.purchasePrice !== "" + ? Number(gameEntry.purchasePrice) + : null; const value = gameEntry && gameEntry.value != null && gameEntry.value !== "" ? Number(gameEntry.value) : null; + const condition = + gameEntry && gameEntry.condition != null && gameEntry.condition !== "" ? Number(gameEntry.condition) : null; const createdAt = normalizeDateOrNull(gameEntry && gameEntry.createdAt); await client.query( ` INSERT INTO games( - console_id, title, genre, publisher, release_year, estimated_value, loaned_to, created_at + console_id, title, genre, publisher, game_version, is_duplicate, release_year, purchase_price, + estimated_value, condition_score, loaned_to, created_at ) - VALUES ($1, $2, $3, $4, $5, $6, $7, COALESCE($8::timestamptz, NOW())); + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, COALESCE($12::timestamptz, NOW())); `, - [consoleId, title, genre, publisher, year, value, loanedTo, createdAt], + [ + consoleId, + title, + genre, + publisher, + version || null, + isDuplicate, + year, + purchasePrice, + value, + condition, + loanedTo, + createdAt, + ], ); insertedGames += 1; @@ -510,19 +557,25 @@ async function createGame(payload) { const genre = normalizeText(payload.genre) || null; const publisher = normalizeText(payload.publisher) || null; + const version = normalizeText(payload.version) || null; + const isDuplicate = Boolean(payload.isDuplicate); const loanedTo = normalizeText(payload.loanedTo) || null; const year = payload.year != null && payload.year !== "" ? Number(payload.year) : null; + const purchasePrice = + payload.purchasePrice != null && payload.purchasePrice !== "" ? Number(payload.purchasePrice) : null; const value = payload.value != null && payload.value !== "" ? Number(payload.value) : null; + const condition = payload.condition != null && payload.condition !== "" ? Number(payload.condition) : null; const insertResult = await client.query( ` INSERT INTO games( - console_id, title, genre, publisher, release_year, estimated_value, loaned_to + console_id, title, genre, publisher, game_version, is_duplicate, release_year, purchase_price, + estimated_value, condition_score, loaned_to ) - VALUES ($1, $2, $3, $4, $5, $6, $7) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id::text AS id; `, - [consoleData.consoleId, title, genre, publisher, year, value, loanedTo], + [consoleData.consoleId, title, genre, publisher, version, isDuplicate, year, purchasePrice, value, condition, loanedTo], ); await client.query("COMMIT"); @@ -548,9 +601,14 @@ async function updateGame(id, payload) { const genre = normalizeText(payload.genre) || null; const publisher = normalizeText(payload.publisher) || null; + const version = normalizeText(payload.version) || null; + const isDuplicate = Boolean(payload.isDuplicate); const loanedTo = normalizeText(payload.loanedTo) || null; const year = payload.year != null && payload.year !== "" ? Number(payload.year) : null; + const purchasePrice = + payload.purchasePrice != null && payload.purchasePrice !== "" ? Number(payload.purchasePrice) : null; const value = payload.value != null && payload.value !== "" ? Number(payload.value) : null; + const condition = payload.condition != null && payload.condition !== "" ? Number(payload.condition) : null; const updateResult = await client.query( ` @@ -560,13 +618,30 @@ async function updateGame(id, payload) { title = $3, genre = $4, publisher = $5, - release_year = $6, - estimated_value = $7, - loaned_to = $8 + game_version = $6, + is_duplicate = $7, + release_year = $8, + purchase_price = $9, + estimated_value = $10, + condition_score = $11, + loaned_to = $12 WHERE id = $1::uuid RETURNING id::text AS id; `, - [id, consoleData.consoleId, title, genre, publisher, year, value, loanedTo], + [ + id, + consoleData.consoleId, + title, + genre, + publisher, + version, + isDuplicate, + year, + purchasePrice, + value, + condition, + loanedTo, + ], ); if (!updateResult.rowCount) { @@ -675,12 +750,17 @@ async function importCatalog(payload) { if (!title) { continue; } + const version = normalizeText(game && game.version); + const isDuplicate = Boolean(game && game.isDuplicate); const genre = normalizeText(game && game.genre) || null; const publisher = normalizeText(game && game.publisher) || null; const loanedTo = normalizeText(game && game.loanedTo) || null; const year = game && game.year != null && game.year !== "" ? Number(game.year) : null; + const purchasePrice = + game && game.purchasePrice != null && game.purchasePrice !== "" ? Number(game.purchasePrice) : null; const value = game && game.value != null && game.value !== "" ? Number(game.value) : null; + const condition = game && game.condition != null && game.condition !== "" ? Number(game.condition) : null; const dedupeResult = await client.query( ` @@ -689,9 +769,10 @@ async function importCatalog(payload) { WHERE console_id = $1 AND LOWER(title) = LOWER($2) AND COALESCE(release_year, 0) = COALESCE($3, 0) + AND COALESCE(LOWER(game_version), '') = COALESCE(LOWER($4), '') LIMIT 1; `, - [consoleId, title, year], + [consoleId, title, year, version || null], ); if (dedupeResult.rowCount) { @@ -701,11 +782,12 @@ async function importCatalog(payload) { await client.query( ` INSERT INTO games( - console_id, title, genre, publisher, release_year, estimated_value, loaned_to + console_id, title, genre, publisher, game_version, is_duplicate, release_year, purchase_price, + estimated_value, condition_score, loaned_to ) - VALUES ($1, $2, $3, $4, $5, $6, $7); + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11); `, - [consoleId, title, genre, publisher, year, value, loanedTo], + [consoleId, title, genre, publisher, version || null, isDuplicate, year, purchasePrice, value, condition, loanedTo], ); insertedGames += 1; diff --git a/app.js b/app.js index 38384b6..df6809a 100644 --- a/app.js +++ b/app.js @@ -20,10 +20,14 @@ const gameForm = document.getElementById("gameForm"); const brandInput = document.getElementById("brandInput"); const consoleInput = document.getElementById("consoleInput"); const titleInput = document.getElementById("titleInput"); +const versionInput = document.getElementById("versionInput"); const genreInput = document.getElementById("genreInput"); const publisherInput = document.getElementById("publisherInput"); const yearInput = document.getElementById("yearInput"); const valueInput = document.getElementById("valueInput"); +const purchasePriceInput = document.getElementById("purchasePriceInput"); +const conditionInput = document.getElementById("conditionInput"); +const isDuplicateInput = document.getElementById("isDuplicateInput"); const loanedToInput = document.getElementById("loanedToInput"); const gameSubmitBtn = document.getElementById("gameSubmitBtn"); const cancelEditBtn = document.getElementById("cancelEditBtn"); @@ -127,10 +131,14 @@ gameForm.addEventListener("submit", async (event) => { brand: state.selectedBrand, consoleName: state.selectedConsole, title, + version: versionInput.value.trim(), genre: genreInput.value.trim(), publisher: publisherInput.value.trim(), + isDuplicate: isDuplicateInput.checked, year: yearInput.value ? Number(yearInput.value) : null, + purchasePrice: purchasePriceInput.value ? Number(purchasePriceInput.value) : null, value: valueInput.value ? Number(valueInput.value) : null, + condition: conditionInput.value ? Number(conditionInput.value) : null, loanedTo: loanedToInput.value.trim(), }; @@ -165,10 +173,14 @@ gameForm.addEventListener("submit", async (event) => { games[idx] = { ...games[idx], title, + version: versionInput.value.trim(), genre: genreInput.value.trim(), publisher: publisherInput.value.trim(), + isDuplicate: isDuplicateInput.checked, year: yearInput.value ? Number(yearInput.value) : null, + purchasePrice: purchasePriceInput.value ? Number(purchasePriceInput.value) : null, value: valueInput.value ? Number(valueInput.value) : null, + condition: conditionInput.value ? Number(conditionInput.value) : null, loanedTo: loanedToInput.value.trim(), }; } @@ -176,10 +188,14 @@ gameForm.addEventListener("submit", async (event) => { const game = { id: crypto.randomUUID(), title, + version: versionInput.value.trim(), genre: genreInput.value.trim(), publisher: publisherInput.value.trim(), + isDuplicate: isDuplicateInput.checked, year: yearInput.value ? Number(yearInput.value) : null, + purchasePrice: purchasePriceInput.value ? Number(purchasePriceInput.value) : null, value: valueInput.value ? Number(valueInput.value) : null, + condition: conditionInput.value ? Number(conditionInput.value) : null, loanedTo: loanedToInput.value.trim(), createdAt: new Date().toISOString(), }; @@ -504,10 +520,14 @@ function renderGames() { card.querySelector(".game-title").textContent = game.title; const metaParts = [ + game.version ? `Version: ${game.version}` : null, game.genre ? `Genre: ${game.genre}` : null, game.publisher ? `Editeur: ${game.publisher}` : null, + game.isDuplicate ? "Double: OUI" : null, game.year ? `Annee: ${game.year}` : null, + game.purchasePrice != null ? `Prix achat: ${game.purchasePrice.toFixed(2)} EUR` : null, game.value != null ? `Cote: ${game.value.toFixed(2)} EUR` : null, + game.condition != null ? `Etat: ${game.condition}` : null, ].filter(Boolean); card.querySelector(".game-meta").textContent = metaParts.join(" | ") || "Aucune information complementaire"; @@ -533,10 +553,14 @@ function renderGames() { function startEditMode(game) { editingGameId = game.id; titleInput.value = game.title || ""; + versionInput.value = game.version || ""; genreInput.value = game.genre || ""; publisherInput.value = game.publisher || ""; + isDuplicateInput.checked = Boolean(game.isDuplicate); yearInput.value = game.year || ""; + purchasePriceInput.value = game.purchasePrice != null ? game.purchasePrice : ""; valueInput.value = game.value != null ? game.value : ""; + conditionInput.value = game.condition != null ? game.condition : ""; loanedToInput.value = game.loanedTo || ""; gameSubmitBtn.textContent = "Mettre a jour le jeu"; diff --git a/index.html b/index.html index d5062b8..c089b70 100644 --- a/index.html +++ b/index.html @@ -75,6 +75,10 @@ Titre + + + +