Import XLSX support: add collection fields and migration script
This commit is contained in:
14
README.md
14
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)
|
- Compteur visuel du nombre de jeux par console (bulle sur l'onglet)
|
||||||
- Ajout de jeux avec champs:
|
- Ajout de jeux avec champs:
|
||||||
- titre
|
- titre
|
||||||
|
- version
|
||||||
- genre
|
- genre
|
||||||
- editeur
|
- editeur
|
||||||
|
- double (oui/non)
|
||||||
- annee
|
- annee
|
||||||
|
- prix d'achat
|
||||||
- cote estimee
|
- cote estimee
|
||||||
|
- etat
|
||||||
- prete a
|
- prete a
|
||||||
- Edition d'une fiche existante
|
- Edition d'une fiche existante
|
||||||
- Suppression d'un jeu
|
- Suppression d'un jeu
|
||||||
@@ -146,6 +150,16 @@ git pull
|
|||||||
- snapshot auto en base avant restore `replace` (`backup_snapshots`)
|
- snapshot auto en base avant restore `replace` (`backup_snapshots`)
|
||||||
- actions accessibles dans le panneau lateral `Outils`
|
- 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
|
## Licence
|
||||||
|
|
||||||
Projet prive personnel.
|
Projet prive personnel.
|
||||||
|
|||||||
112
api/server.js
112
api/server.js
@@ -105,13 +105,21 @@ async function runMigrations() {
|
|||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
genre TEXT,
|
genre TEXT,
|
||||||
publisher 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)),
|
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),
|
estimated_value NUMERIC(10,2) CHECK (estimated_value IS NULL OR estimated_value >= 0),
|
||||||
|
condition_score NUMERIC(4,2),
|
||||||
loaned_to TEXT,
|
loaned_to TEXT,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
updated_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(`
|
await pool.query(`
|
||||||
CREATE TABLE IF NOT EXISTS backup_snapshots (
|
CREATE TABLE IF NOT EXISTS backup_snapshots (
|
||||||
@@ -210,8 +218,12 @@ async function getCatalogFull() {
|
|||||||
g.title,
|
g.title,
|
||||||
g.genre,
|
g.genre,
|
||||||
g.publisher,
|
g.publisher,
|
||||||
|
g.game_version,
|
||||||
|
g.is_duplicate,
|
||||||
g.release_year,
|
g.release_year,
|
||||||
|
g.purchase_price,
|
||||||
g.estimated_value,
|
g.estimated_value,
|
||||||
|
g.condition_score,
|
||||||
g.loaned_to,
|
g.loaned_to,
|
||||||
g.created_at
|
g.created_at
|
||||||
FROM games g
|
FROM games g
|
||||||
@@ -237,8 +249,12 @@ async function getCatalogFull() {
|
|||||||
title: row.title,
|
title: row.title,
|
||||||
genre: row.genre || "",
|
genre: row.genre || "",
|
||||||
publisher: row.publisher || "",
|
publisher: row.publisher || "",
|
||||||
|
version: row.game_version || "",
|
||||||
|
isDuplicate: Boolean(row.is_duplicate),
|
||||||
year: row.release_year || null,
|
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,
|
value: row.estimated_value != null ? Number(row.estimated_value) : null,
|
||||||
|
condition: row.condition_score != null ? Number(row.condition_score) : null,
|
||||||
loanedTo: row.loaned_to || "",
|
loanedTo: row.loaned_to || "",
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
});
|
});
|
||||||
@@ -272,8 +288,12 @@ async function exportCatalogDumpWithClient(client) {
|
|||||||
g.title,
|
g.title,
|
||||||
g.genre,
|
g.genre,
|
||||||
g.publisher,
|
g.publisher,
|
||||||
|
g.game_version,
|
||||||
|
g.is_duplicate,
|
||||||
g.release_year,
|
g.release_year,
|
||||||
|
g.purchase_price,
|
||||||
g.estimated_value,
|
g.estimated_value,
|
||||||
|
g.condition_score,
|
||||||
g.loaned_to,
|
g.loaned_to,
|
||||||
g.created_at,
|
g.created_at,
|
||||||
g.updated_at
|
g.updated_at
|
||||||
@@ -307,8 +327,12 @@ async function exportCatalogDumpWithClient(client) {
|
|||||||
title: row.title,
|
title: row.title,
|
||||||
genre: row.genre || "",
|
genre: row.genre || "",
|
||||||
publisher: row.publisher || "",
|
publisher: row.publisher || "",
|
||||||
|
version: row.game_version || "",
|
||||||
|
isDuplicate: Boolean(row.is_duplicate),
|
||||||
year: row.release_year || null,
|
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,
|
value: row.estimated_value != null ? Number(row.estimated_value) : null,
|
||||||
|
condition: row.condition_score != null ? Number(row.condition_score) : null,
|
||||||
loanedTo: row.loaned_to || "",
|
loanedTo: row.loaned_to || "",
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at,
|
updatedAt: row.updated_at,
|
||||||
@@ -431,6 +455,7 @@ async function restoreCatalogDump(mode, dump) {
|
|||||||
const ensured = await ensureConsole(client, brand, consoleName);
|
const ensured = await ensureConsole(client, brand, consoleName);
|
||||||
const consoleId = ensured.consoleId;
|
const consoleId = ensured.consoleId;
|
||||||
const year = gameEntry && gameEntry.year != null && gameEntry.year !== "" ? Number(gameEntry.year) : null;
|
const year = gameEntry && gameEntry.year != null && gameEntry.year !== "" ? Number(gameEntry.year) : null;
|
||||||
|
const version = normalizeText(gameEntry && gameEntry.version);
|
||||||
|
|
||||||
const dedupeResult = await client.query(
|
const dedupeResult = await client.query(
|
||||||
`
|
`
|
||||||
@@ -439,9 +464,10 @@ async function restoreCatalogDump(mode, dump) {
|
|||||||
WHERE console_id = $1
|
WHERE console_id = $1
|
||||||
AND LOWER(title) = LOWER($2)
|
AND LOWER(title) = LOWER($2)
|
||||||
AND COALESCE(release_year, 0) = COALESCE($3, 0)
|
AND COALESCE(release_year, 0) = COALESCE($3, 0)
|
||||||
|
AND COALESCE(LOWER(game_version), '') = COALESCE(LOWER($4), '')
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
`,
|
`,
|
||||||
[consoleId, title, year],
|
[consoleId, title, year, version || null],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (dedupeResult.rowCount) {
|
if (dedupeResult.rowCount) {
|
||||||
@@ -451,17 +477,38 @@ async function restoreCatalogDump(mode, dump) {
|
|||||||
const genre = normalizeText(gameEntry && gameEntry.genre) || null;
|
const genre = normalizeText(gameEntry && gameEntry.genre) || null;
|
||||||
const publisher = normalizeText(gameEntry && gameEntry.publisher) || null;
|
const publisher = normalizeText(gameEntry && gameEntry.publisher) || null;
|
||||||
const loanedTo = normalizeText(gameEntry && gameEntry.loanedTo) || 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 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);
|
const createdAt = normalizeDateOrNull(gameEntry && gameEntry.createdAt);
|
||||||
|
|
||||||
await client.query(
|
await client.query(
|
||||||
`
|
`
|
||||||
INSERT INTO games(
|
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;
|
insertedGames += 1;
|
||||||
@@ -510,19 +557,25 @@ async function createGame(payload) {
|
|||||||
|
|
||||||
const genre = normalizeText(payload.genre) || null;
|
const genre = normalizeText(payload.genre) || null;
|
||||||
const publisher = normalizeText(payload.publisher) || 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 loanedTo = normalizeText(payload.loanedTo) || null;
|
||||||
const year = payload.year != null && payload.year !== "" ? Number(payload.year) : 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 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(
|
const insertResult = await client.query(
|
||||||
`
|
`
|
||||||
INSERT INTO games(
|
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;
|
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");
|
await client.query("COMMIT");
|
||||||
@@ -548,9 +601,14 @@ async function updateGame(id, payload) {
|
|||||||
|
|
||||||
const genre = normalizeText(payload.genre) || null;
|
const genre = normalizeText(payload.genre) || null;
|
||||||
const publisher = normalizeText(payload.publisher) || 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 loanedTo = normalizeText(payload.loanedTo) || null;
|
||||||
const year = payload.year != null && payload.year !== "" ? Number(payload.year) : 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 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(
|
const updateResult = await client.query(
|
||||||
`
|
`
|
||||||
@@ -560,13 +618,30 @@ async function updateGame(id, payload) {
|
|||||||
title = $3,
|
title = $3,
|
||||||
genre = $4,
|
genre = $4,
|
||||||
publisher = $5,
|
publisher = $5,
|
||||||
release_year = $6,
|
game_version = $6,
|
||||||
estimated_value = $7,
|
is_duplicate = $7,
|
||||||
loaned_to = $8
|
release_year = $8,
|
||||||
|
purchase_price = $9,
|
||||||
|
estimated_value = $10,
|
||||||
|
condition_score = $11,
|
||||||
|
loaned_to = $12
|
||||||
WHERE id = $1::uuid
|
WHERE id = $1::uuid
|
||||||
RETURNING id::text AS id;
|
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) {
|
if (!updateResult.rowCount) {
|
||||||
@@ -675,12 +750,17 @@ async function importCatalog(payload) {
|
|||||||
if (!title) {
|
if (!title) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const version = normalizeText(game && game.version);
|
||||||
|
const isDuplicate = Boolean(game && game.isDuplicate);
|
||||||
|
|
||||||
const genre = normalizeText(game && game.genre) || null;
|
const genre = normalizeText(game && game.genre) || null;
|
||||||
const publisher = normalizeText(game && game.publisher) || null;
|
const publisher = normalizeText(game && game.publisher) || null;
|
||||||
const loanedTo = normalizeText(game && game.loanedTo) || null;
|
const loanedTo = normalizeText(game && game.loanedTo) || null;
|
||||||
const year = game && game.year != null && game.year !== "" ? Number(game.year) : 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 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(
|
const dedupeResult = await client.query(
|
||||||
`
|
`
|
||||||
@@ -689,9 +769,10 @@ async function importCatalog(payload) {
|
|||||||
WHERE console_id = $1
|
WHERE console_id = $1
|
||||||
AND LOWER(title) = LOWER($2)
|
AND LOWER(title) = LOWER($2)
|
||||||
AND COALESCE(release_year, 0) = COALESCE($3, 0)
|
AND COALESCE(release_year, 0) = COALESCE($3, 0)
|
||||||
|
AND COALESCE(LOWER(game_version), '') = COALESCE(LOWER($4), '')
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
`,
|
`,
|
||||||
[consoleId, title, year],
|
[consoleId, title, year, version || null],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (dedupeResult.rowCount) {
|
if (dedupeResult.rowCount) {
|
||||||
@@ -701,11 +782,12 @@ async function importCatalog(payload) {
|
|||||||
await client.query(
|
await client.query(
|
||||||
`
|
`
|
||||||
INSERT INTO games(
|
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;
|
insertedGames += 1;
|
||||||
|
|||||||
24
app.js
24
app.js
@@ -20,10 +20,14 @@ const gameForm = document.getElementById("gameForm");
|
|||||||
const brandInput = document.getElementById("brandInput");
|
const brandInput = document.getElementById("brandInput");
|
||||||
const consoleInput = document.getElementById("consoleInput");
|
const consoleInput = document.getElementById("consoleInput");
|
||||||
const titleInput = document.getElementById("titleInput");
|
const titleInput = document.getElementById("titleInput");
|
||||||
|
const versionInput = document.getElementById("versionInput");
|
||||||
const genreInput = document.getElementById("genreInput");
|
const genreInput = document.getElementById("genreInput");
|
||||||
const publisherInput = document.getElementById("publisherInput");
|
const publisherInput = document.getElementById("publisherInput");
|
||||||
const yearInput = document.getElementById("yearInput");
|
const yearInput = document.getElementById("yearInput");
|
||||||
const valueInput = document.getElementById("valueInput");
|
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 loanedToInput = document.getElementById("loanedToInput");
|
||||||
const gameSubmitBtn = document.getElementById("gameSubmitBtn");
|
const gameSubmitBtn = document.getElementById("gameSubmitBtn");
|
||||||
const cancelEditBtn = document.getElementById("cancelEditBtn");
|
const cancelEditBtn = document.getElementById("cancelEditBtn");
|
||||||
@@ -127,10 +131,14 @@ gameForm.addEventListener("submit", async (event) => {
|
|||||||
brand: state.selectedBrand,
|
brand: state.selectedBrand,
|
||||||
consoleName: state.selectedConsole,
|
consoleName: state.selectedConsole,
|
||||||
title,
|
title,
|
||||||
|
version: versionInput.value.trim(),
|
||||||
genre: genreInput.value.trim(),
|
genre: genreInput.value.trim(),
|
||||||
publisher: publisherInput.value.trim(),
|
publisher: publisherInput.value.trim(),
|
||||||
|
isDuplicate: isDuplicateInput.checked,
|
||||||
year: yearInput.value ? Number(yearInput.value) : null,
|
year: yearInput.value ? Number(yearInput.value) : null,
|
||||||
|
purchasePrice: purchasePriceInput.value ? Number(purchasePriceInput.value) : null,
|
||||||
value: valueInput.value ? Number(valueInput.value) : null,
|
value: valueInput.value ? Number(valueInput.value) : null,
|
||||||
|
condition: conditionInput.value ? Number(conditionInput.value) : null,
|
||||||
loanedTo: loanedToInput.value.trim(),
|
loanedTo: loanedToInput.value.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,10 +173,14 @@ gameForm.addEventListener("submit", async (event) => {
|
|||||||
games[idx] = {
|
games[idx] = {
|
||||||
...games[idx],
|
...games[idx],
|
||||||
title,
|
title,
|
||||||
|
version: versionInput.value.trim(),
|
||||||
genre: genreInput.value.trim(),
|
genre: genreInput.value.trim(),
|
||||||
publisher: publisherInput.value.trim(),
|
publisher: publisherInput.value.trim(),
|
||||||
|
isDuplicate: isDuplicateInput.checked,
|
||||||
year: yearInput.value ? Number(yearInput.value) : null,
|
year: yearInput.value ? Number(yearInput.value) : null,
|
||||||
|
purchasePrice: purchasePriceInput.value ? Number(purchasePriceInput.value) : null,
|
||||||
value: valueInput.value ? Number(valueInput.value) : null,
|
value: valueInput.value ? Number(valueInput.value) : null,
|
||||||
|
condition: conditionInput.value ? Number(conditionInput.value) : null,
|
||||||
loanedTo: loanedToInput.value.trim(),
|
loanedTo: loanedToInput.value.trim(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -176,10 +188,14 @@ gameForm.addEventListener("submit", async (event) => {
|
|||||||
const game = {
|
const game = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
title,
|
title,
|
||||||
|
version: versionInput.value.trim(),
|
||||||
genre: genreInput.value.trim(),
|
genre: genreInput.value.trim(),
|
||||||
publisher: publisherInput.value.trim(),
|
publisher: publisherInput.value.trim(),
|
||||||
|
isDuplicate: isDuplicateInput.checked,
|
||||||
year: yearInput.value ? Number(yearInput.value) : null,
|
year: yearInput.value ? Number(yearInput.value) : null,
|
||||||
|
purchasePrice: purchasePriceInput.value ? Number(purchasePriceInput.value) : null,
|
||||||
value: valueInput.value ? Number(valueInput.value) : null,
|
value: valueInput.value ? Number(valueInput.value) : null,
|
||||||
|
condition: conditionInput.value ? Number(conditionInput.value) : null,
|
||||||
loanedTo: loanedToInput.value.trim(),
|
loanedTo: loanedToInput.value.trim(),
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -504,10 +520,14 @@ function renderGames() {
|
|||||||
card.querySelector(".game-title").textContent = game.title;
|
card.querySelector(".game-title").textContent = game.title;
|
||||||
|
|
||||||
const metaParts = [
|
const metaParts = [
|
||||||
|
game.version ? `Version: ${game.version}` : null,
|
||||||
game.genre ? `Genre: ${game.genre}` : null,
|
game.genre ? `Genre: ${game.genre}` : null,
|
||||||
game.publisher ? `Editeur: ${game.publisher}` : null,
|
game.publisher ? `Editeur: ${game.publisher}` : null,
|
||||||
|
game.isDuplicate ? "Double: OUI" : null,
|
||||||
game.year ? `Annee: ${game.year}` : 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.value != null ? `Cote: ${game.value.toFixed(2)} EUR` : null,
|
||||||
|
game.condition != null ? `Etat: ${game.condition}` : null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
card.querySelector(".game-meta").textContent = metaParts.join(" | ") || "Aucune information complementaire";
|
card.querySelector(".game-meta").textContent = metaParts.join(" | ") || "Aucune information complementaire";
|
||||||
@@ -533,10 +553,14 @@ function renderGames() {
|
|||||||
function startEditMode(game) {
|
function startEditMode(game) {
|
||||||
editingGameId = game.id;
|
editingGameId = game.id;
|
||||||
titleInput.value = game.title || "";
|
titleInput.value = game.title || "";
|
||||||
|
versionInput.value = game.version || "";
|
||||||
genreInput.value = game.genre || "";
|
genreInput.value = game.genre || "";
|
||||||
publisherInput.value = game.publisher || "";
|
publisherInput.value = game.publisher || "";
|
||||||
|
isDuplicateInput.checked = Boolean(game.isDuplicate);
|
||||||
yearInput.value = game.year || "";
|
yearInput.value = game.year || "";
|
||||||
|
purchasePriceInput.value = game.purchasePrice != null ? game.purchasePrice : "";
|
||||||
valueInput.value = game.value != null ? game.value : "";
|
valueInput.value = game.value != null ? game.value : "";
|
||||||
|
conditionInput.value = game.condition != null ? game.condition : "";
|
||||||
loanedToInput.value = game.loanedTo || "";
|
loanedToInput.value = game.loanedTo || "";
|
||||||
|
|
||||||
gameSubmitBtn.textContent = "Mettre a jour le jeu";
|
gameSubmitBtn.textContent = "Mettre a jour le jeu";
|
||||||
|
|||||||
16
index.html
16
index.html
@@ -75,6 +75,10 @@
|
|||||||
Titre
|
Titre
|
||||||
<input id="titleInput" required placeholder="Final Fantasy X" />
|
<input id="titleInput" required placeholder="Final Fantasy X" />
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
Version
|
||||||
|
<input id="versionInput" placeholder="Origine / Platinium / Complet..." />
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Genre
|
Genre
|
||||||
<input id="genreInput" placeholder="RPG" />
|
<input id="genreInput" placeholder="RPG" />
|
||||||
@@ -91,6 +95,18 @@
|
|||||||
Cote estimée (€)
|
Cote estimée (€)
|
||||||
<input id="valueInput" type="number" min="0" step="0.01" placeholder="45" />
|
<input id="valueInput" type="number" min="0" step="0.01" placeholder="45" />
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
Prix d'achat (€)
|
||||||
|
<input id="purchasePriceInput" type="number" min="0" step="0.01" placeholder="12.5" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Etat (0-10)
|
||||||
|
<input id="conditionInput" type="number" min="0" max="10" step="0.1" placeholder="5" />
|
||||||
|
</label>
|
||||||
|
<label class="checkbox-row">
|
||||||
|
<input id="isDuplicateInput" type="checkbox" />
|
||||||
|
Jeu en double
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Prêté à
|
Prêté à
|
||||||
<input id="loanedToInput" placeholder="Nom (laisser vide si chez toi)" />
|
<input id="loanedToInput" placeholder="Nom (laisser vide si chez toi)" />
|
||||||
|
|||||||
189
scripts/import_collections_xlsx.py
Executable file
189
scripts/import_collections_xlsx.py
Executable file
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import urllib.request
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
NS_MAIN = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||||
|
NS_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||||
|
NS_PKG_REL = "http://schemas.openxmlformats.org/package/2006/relationships"
|
||||||
|
|
||||||
|
NS = {
|
||||||
|
"m": NS_MAIN,
|
||||||
|
"r": NS_REL,
|
||||||
|
"pr": NS_PKG_REL,
|
||||||
|
}
|
||||||
|
|
||||||
|
CONSOLE_TO_BRAND = {
|
||||||
|
"NES": "NINTENDO",
|
||||||
|
"SNES": "NINTENDO",
|
||||||
|
"WII": "NINTENDO",
|
||||||
|
"PS1": "SONY",
|
||||||
|
"PS2": "SONY",
|
||||||
|
"PS3": "SONY",
|
||||||
|
"PS4": "SONY",
|
||||||
|
"PS5": "SONY",
|
||||||
|
"XBOX 360": "MICROSOFT",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def col_to_index(col: str) -> int:
|
||||||
|
idx = 0
|
||||||
|
for ch in col:
|
||||||
|
idx = idx * 26 + (ord(ch) - 64)
|
||||||
|
return idx
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_console(sheet_name: str) -> str:
|
||||||
|
normalized = sheet_name.strip().upper()
|
||||||
|
if normalized == "WII":
|
||||||
|
return "Wii"
|
||||||
|
return sheet_name.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def to_number(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return float(value)
|
||||||
|
text = str(value).strip().replace(",", ".")
|
||||||
|
if text == "":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(text)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_xlsx(path: str):
|
||||||
|
brands = {}
|
||||||
|
games_by_console = {}
|
||||||
|
|
||||||
|
with zipfile.ZipFile(path) as zf:
|
||||||
|
shared = []
|
||||||
|
if "xl/sharedStrings.xml" in zf.namelist():
|
||||||
|
shared_root = ET.fromstring(zf.read("xl/sharedStrings.xml"))
|
||||||
|
for si in shared_root.findall("m:si", NS):
|
||||||
|
text = "".join(t.text or "" for t in si.findall(".//m:t", NS))
|
||||||
|
shared.append(text)
|
||||||
|
|
||||||
|
wb = ET.fromstring(zf.read("xl/workbook.xml"))
|
||||||
|
rels = ET.fromstring(zf.read("xl/_rels/workbook.xml.rels"))
|
||||||
|
rel_map = {rel.attrib["Id"]: rel.attrib["Target"] for rel in rels.findall("pr:Relationship", NS)}
|
||||||
|
|
||||||
|
sheets = []
|
||||||
|
for sheet in wb.findall(".//m:sheets/m:sheet", NS):
|
||||||
|
name = sheet.attrib["name"]
|
||||||
|
rid = sheet.attrib[f"{{{NS_REL}}}id"]
|
||||||
|
target = rel_map[rid]
|
||||||
|
if not target.startswith("xl/"):
|
||||||
|
target = "xl/" + target
|
||||||
|
sheets.append((name, target))
|
||||||
|
|
||||||
|
for sheet_name, target in sheets:
|
||||||
|
console_name = normalize_console(sheet_name)
|
||||||
|
brand = CONSOLE_TO_BRAND.get(sheet_name.strip().upper())
|
||||||
|
if not brand:
|
||||||
|
continue
|
||||||
|
|
||||||
|
brands.setdefault(brand, [])
|
||||||
|
if console_name not in brands[brand]:
|
||||||
|
brands[brand].append(console_name)
|
||||||
|
games_by_console.setdefault(console_name, [])
|
||||||
|
|
||||||
|
root = ET.fromstring(zf.read(target))
|
||||||
|
rows = root.findall(".//m:sheetData/m:row", NS)
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
cells = row.findall("m:c", NS)
|
||||||
|
values = {i: "" for i in range(1, 7)}
|
||||||
|
|
||||||
|
for cell in cells:
|
||||||
|
ref = cell.attrib.get("r", "A1")
|
||||||
|
match = re.match(r"[A-Z]+", ref)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
idx = col_to_index(match.group(0))
|
||||||
|
if idx > 6:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cell_type = cell.attrib.get("t")
|
||||||
|
value_elem = cell.find("m:v", NS)
|
||||||
|
value = ""
|
||||||
|
if value_elem is not None and value_elem.text is not None:
|
||||||
|
if cell_type == "s":
|
||||||
|
try:
|
||||||
|
value = shared[int(value_elem.text)]
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
value = value_elem.text
|
||||||
|
else:
|
||||||
|
value = value_elem.text
|
||||||
|
values[idx] = value
|
||||||
|
|
||||||
|
title = str(values[1]).strip()
|
||||||
|
version = str(values[2]).strip()
|
||||||
|
duplicate_raw = str(values[3]).strip()
|
||||||
|
purchase_price = to_number(values[4])
|
||||||
|
cote = to_number(values[5])
|
||||||
|
condition = to_number(values[6])
|
||||||
|
|
||||||
|
if version.upper() == "VERSION":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not title:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if re.fullmatch(r"\d+(\.\d+)?", title) and not any([version, duplicate_raw, purchase_price, cote, condition]):
|
||||||
|
continue
|
||||||
|
|
||||||
|
games_by_console[console_name].append(
|
||||||
|
{
|
||||||
|
"title": title,
|
||||||
|
"version": version,
|
||||||
|
"isDuplicate": duplicate_raw.upper() == "OUI",
|
||||||
|
"purchasePrice": purchase_price,
|
||||||
|
"value": cote,
|
||||||
|
"condition": condition,
|
||||||
|
"genre": "",
|
||||||
|
"publisher": "",
|
||||||
|
"year": None,
|
||||||
|
"loanedTo": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"brands": brands, "gamesByConsole": games_by_console}
|
||||||
|
|
||||||
|
|
||||||
|
def post_import(api_base: str, payload: dict):
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{api_base.rstrip('/')}/api/catalog/import",
|
||||||
|
data=json.dumps(payload).encode("utf-8"),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
body = resp.read().decode("utf-8")
|
||||||
|
return json.loads(body)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Import COLLECTIONS.xlsx to video game DB API")
|
||||||
|
parser.add_argument("xlsx_path", help="Path to COLLECTIONS.xlsx")
|
||||||
|
parser.add_argument("--api-base", default="http://127.0.0.1:7001", help="API base URL")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
payload = parse_xlsx(args.xlsx_path)
|
||||||
|
result = post_import(args.api_base, payload)
|
||||||
|
|
||||||
|
total_games = sum(len(v) for v in payload["gamesByConsole"].values())
|
||||||
|
print(json.dumps({
|
||||||
|
"sheetsImported": list(payload["gamesByConsole"].keys()),
|
||||||
|
"parsedGames": total_games,
|
||||||
|
"apiResult": result,
|
||||||
|
}, ensure_ascii=True, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
12
styles.css
12
styles.css
@@ -154,6 +154,18 @@ h1 {
|
|||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-height: 44px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-row input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user