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
+
+
+
+