Feature: barcode lookup with auto-fill and owned detection
This commit is contained in:
169
api/server.js
169
api/server.js
@@ -120,6 +120,7 @@ async function runMigrations() {
|
|||||||
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),
|
condition_score NUMERIC(4,2),
|
||||||
loaned_to TEXT,
|
loaned_to TEXT,
|
||||||
|
barcode 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()
|
||||||
);
|
);
|
||||||
@@ -128,6 +129,7 @@ async function runMigrations() {
|
|||||||
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 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 purchase_price NUMERIC(10,2);");
|
||||||
await pool.query("ALTER TABLE games ADD COLUMN IF NOT EXISTS condition_score NUMERIC(4,2);");
|
await pool.query("ALTER TABLE games ADD COLUMN IF NOT EXISTS condition_score NUMERIC(4,2);");
|
||||||
|
await pool.query("ALTER TABLE games ADD COLUMN IF NOT EXISTS barcode TEXT;");
|
||||||
|
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
CREATE TABLE IF NOT EXISTS backup_snapshots (
|
CREATE TABLE IF NOT EXISTS backup_snapshots (
|
||||||
@@ -245,6 +247,7 @@ async function getCatalogFull() {
|
|||||||
g.estimated_value,
|
g.estimated_value,
|
||||||
g.condition_score,
|
g.condition_score,
|
||||||
g.loaned_to,
|
g.loaned_to,
|
||||||
|
g.barcode,
|
||||||
g.created_at
|
g.created_at
|
||||||
FROM games g
|
FROM games g
|
||||||
JOIN consoles c ON c.id = g.console_id
|
JOIN consoles c ON c.id = g.console_id
|
||||||
@@ -276,6 +279,7 @@ async function getCatalogFull() {
|
|||||||
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,
|
condition: row.condition_score != null ? Number(row.condition_score) : null,
|
||||||
loanedTo: row.loaned_to || "",
|
loanedTo: row.loaned_to || "",
|
||||||
|
barcode: row.barcode || "",
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -315,6 +319,7 @@ async function exportCatalogDumpWithClient(client) {
|
|||||||
g.estimated_value,
|
g.estimated_value,
|
||||||
g.condition_score,
|
g.condition_score,
|
||||||
g.loaned_to,
|
g.loaned_to,
|
||||||
|
g.barcode,
|
||||||
g.created_at,
|
g.created_at,
|
||||||
g.updated_at
|
g.updated_at
|
||||||
FROM games g
|
FROM games g
|
||||||
@@ -354,6 +359,7 @@ async function exportCatalogDumpWithClient(client) {
|
|||||||
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,
|
condition: row.condition_score != null ? Number(row.condition_score) : null,
|
||||||
loanedTo: row.loaned_to || "",
|
loanedTo: row.loaned_to || "",
|
||||||
|
barcode: row.barcode || "",
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at,
|
updatedAt: row.updated_at,
|
||||||
})),
|
})),
|
||||||
@@ -497,6 +503,7 @@ 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 barcode = normalizeText(gameEntry && gameEntry.barcode) || null;
|
||||||
const isDuplicate = Boolean(gameEntry && gameEntry.isDuplicate);
|
const isDuplicate = Boolean(gameEntry && gameEntry.isDuplicate);
|
||||||
const purchasePrice =
|
const purchasePrice =
|
||||||
gameEntry && gameEntry.purchasePrice != null && gameEntry.purchasePrice !== ""
|
gameEntry && gameEntry.purchasePrice != null && gameEntry.purchasePrice !== ""
|
||||||
@@ -513,7 +520,7 @@ async function restoreCatalogDump(mode, dump) {
|
|||||||
console_id, title, genre, publisher, game_version, is_duplicate, release_year, purchase_price,
|
console_id, title, genre, publisher, game_version, is_duplicate, release_year, purchase_price,
|
||||||
estimated_value, condition_score, loaned_to, created_at
|
estimated_value, condition_score, loaned_to, created_at
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, COALESCE($12::timestamptz, NOW()));
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, COALESCE($13::timestamptz, NOW()));
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
consoleId,
|
consoleId,
|
||||||
@@ -527,6 +534,7 @@ async function restoreCatalogDump(mode, dump) {
|
|||||||
value,
|
value,
|
||||||
condition,
|
condition,
|
||||||
loanedTo,
|
loanedTo,
|
||||||
|
barcode,
|
||||||
createdAt,
|
createdAt,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -580,6 +588,7 @@ async function createGame(payload) {
|
|||||||
const version = normalizeText(payload.version) || null;
|
const version = normalizeText(payload.version) || null;
|
||||||
const isDuplicate = Boolean(payload.isDuplicate);
|
const isDuplicate = Boolean(payload.isDuplicate);
|
||||||
const loanedTo = normalizeText(payload.loanedTo) || null;
|
const loanedTo = normalizeText(payload.loanedTo) || null;
|
||||||
|
const barcode = normalizeText(payload.barcode) || 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 =
|
const purchasePrice =
|
||||||
payload.purchasePrice != null && payload.purchasePrice !== "" ? Number(payload.purchasePrice) : null;
|
payload.purchasePrice != null && payload.purchasePrice !== "" ? Number(payload.purchasePrice) : null;
|
||||||
@@ -590,12 +599,25 @@ async function createGame(payload) {
|
|||||||
`
|
`
|
||||||
INSERT INTO games(
|
INSERT INTO games(
|
||||||
console_id, title, genre, publisher, game_version, is_duplicate, release_year, purchase_price,
|
console_id, title, genre, publisher, game_version, is_duplicate, release_year, purchase_price,
|
||||||
estimated_value, condition_score, loaned_to
|
estimated_value, condition_score, loaned_to, barcode
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||||
RETURNING id::text AS id;
|
RETURNING id::text AS id;
|
||||||
`,
|
`,
|
||||||
[consoleData.consoleId, title, genre, publisher, version, isDuplicate, year, purchasePrice, value, condition, loanedTo],
|
[
|
||||||
|
consoleData.consoleId,
|
||||||
|
title,
|
||||||
|
genre,
|
||||||
|
publisher,
|
||||||
|
version,
|
||||||
|
isDuplicate,
|
||||||
|
year,
|
||||||
|
purchasePrice,
|
||||||
|
value,
|
||||||
|
condition,
|
||||||
|
loanedTo,
|
||||||
|
barcode,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
await client.query("COMMIT");
|
await client.query("COMMIT");
|
||||||
@@ -624,6 +646,7 @@ async function updateGame(id, payload) {
|
|||||||
const version = normalizeText(payload.version) || null;
|
const version = normalizeText(payload.version) || null;
|
||||||
const isDuplicate = Boolean(payload.isDuplicate);
|
const isDuplicate = Boolean(payload.isDuplicate);
|
||||||
const loanedTo = normalizeText(payload.loanedTo) || null;
|
const loanedTo = normalizeText(payload.loanedTo) || null;
|
||||||
|
const barcode = normalizeText(payload.barcode) || 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 =
|
const purchasePrice =
|
||||||
payload.purchasePrice != null && payload.purchasePrice !== "" ? Number(payload.purchasePrice) : null;
|
payload.purchasePrice != null && payload.purchasePrice !== "" ? Number(payload.purchasePrice) : null;
|
||||||
@@ -644,7 +667,8 @@ async function updateGame(id, payload) {
|
|||||||
purchase_price = $9,
|
purchase_price = $9,
|
||||||
estimated_value = $10,
|
estimated_value = $10,
|
||||||
condition_score = $11,
|
condition_score = $11,
|
||||||
loaned_to = $12
|
loaned_to = $12,
|
||||||
|
barcode = $13
|
||||||
WHERE id = $1::uuid
|
WHERE id = $1::uuid
|
||||||
RETURNING id::text AS id;
|
RETURNING id::text AS id;
|
||||||
`,
|
`,
|
||||||
@@ -661,6 +685,7 @@ async function updateGame(id, payload) {
|
|||||||
value,
|
value,
|
||||||
condition,
|
condition,
|
||||||
loanedTo,
|
loanedTo,
|
||||||
|
barcode,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -696,6 +721,108 @@ async function toggleGameLoan(id) {
|
|||||||
return result.rowCount > 0;
|
return result.rowCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeBarcode(rawValue) {
|
||||||
|
const trimmed = normalizeText(rawValue);
|
||||||
|
return trimmed.replace(/[^\dA-Za-z-]/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function lookupOwnedByBarcode(barcode) {
|
||||||
|
const result = await pool.query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
g.id::text AS id,
|
||||||
|
g.title,
|
||||||
|
g.publisher,
|
||||||
|
g.game_version,
|
||||||
|
g.barcode,
|
||||||
|
c.name AS console_name,
|
||||||
|
b.name AS brand_name
|
||||||
|
FROM games g
|
||||||
|
JOIN consoles c ON c.id = g.console_id
|
||||||
|
JOIN brands b ON b.id = c.brand_id
|
||||||
|
WHERE g.barcode = $1
|
||||||
|
ORDER BY g.created_at DESC
|
||||||
|
LIMIT 1;
|
||||||
|
`,
|
||||||
|
[barcode],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.rowCount) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = result.rows[0];
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
title: row.title,
|
||||||
|
publisher: row.publisher || "",
|
||||||
|
version: row.game_version || "",
|
||||||
|
barcode: row.barcode || "",
|
||||||
|
consoleName: row.console_name,
|
||||||
|
brand: row.brand_name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function lookupBarcodeExternal(barcode) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 4500);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.upcitemdb.com/prod/trial/lookup?upc=${encodeURIComponent(barcode)}`, {
|
||||||
|
method: "GET",
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const payload = await response.json();
|
||||||
|
const item = payload && Array.isArray(payload.items) ? payload.items[0] : null;
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const title = normalizeText(item.title);
|
||||||
|
if (!title) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
publisher: normalizeText(item.brand),
|
||||||
|
source: "upcitemdb",
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function lookupBarcode(barcodeRaw) {
|
||||||
|
const barcode = normalizeBarcode(barcodeRaw);
|
||||||
|
if (!barcode || barcode.length < 8) {
|
||||||
|
throw new Error("invalid barcode");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownedGame = await lookupOwnedByBarcode(barcode);
|
||||||
|
if (ownedGame) {
|
||||||
|
return {
|
||||||
|
barcode,
|
||||||
|
owned: true,
|
||||||
|
game: ownedGame,
|
||||||
|
lookup: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const external = await lookupBarcodeExternal(barcode);
|
||||||
|
return {
|
||||||
|
barcode,
|
||||||
|
owned: false,
|
||||||
|
game: null,
|
||||||
|
lookup: external,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function importCatalog(payload) {
|
async function importCatalog(payload) {
|
||||||
const brands = payload && payload.brands && typeof payload.brands === "object" ? payload.brands : {};
|
const brands = payload && payload.brands && typeof payload.brands === "object" ? payload.brands : {};
|
||||||
const gamesByConsole =
|
const gamesByConsole =
|
||||||
@@ -776,6 +903,7 @@ async function importCatalog(payload) {
|
|||||||
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 barcode = normalizeText(game && game.barcode) || 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 =
|
const purchasePrice =
|
||||||
game && game.purchasePrice != null && game.purchasePrice !== "" ? Number(game.purchasePrice) : null;
|
game && game.purchasePrice != null && game.purchasePrice !== "" ? Number(game.purchasePrice) : null;
|
||||||
@@ -803,11 +931,24 @@ async function importCatalog(payload) {
|
|||||||
`
|
`
|
||||||
INSERT INTO games(
|
INSERT INTO games(
|
||||||
console_id, title, genre, publisher, game_version, is_duplicate, release_year, purchase_price,
|
console_id, title, genre, publisher, game_version, is_duplicate, release_year, purchase_price,
|
||||||
estimated_value, condition_score, loaned_to
|
estimated_value, condition_score, loaned_to, barcode
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12);
|
||||||
`,
|
`,
|
||||||
[consoleId, title, genre, publisher, version || null, isDuplicate, year, purchasePrice, value, condition, loanedTo],
|
[
|
||||||
|
consoleId,
|
||||||
|
title,
|
||||||
|
genre,
|
||||||
|
publisher,
|
||||||
|
version || null,
|
||||||
|
isDuplicate,
|
||||||
|
year,
|
||||||
|
purchasePrice,
|
||||||
|
value,
|
||||||
|
condition,
|
||||||
|
loanedTo,
|
||||||
|
barcode,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
insertedGames += 1;
|
insertedGames += 1;
|
||||||
@@ -1229,6 +1370,18 @@ async function handleRequest(request, response) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const barcodeLookupMatch = url.pathname.match(/^\/api\/barcode\/lookup\/([^/]+)$/);
|
||||||
|
if (request.method === "GET" && barcodeLookupMatch) {
|
||||||
|
try {
|
||||||
|
const result = await lookupBarcode(decodeURIComponent(barcodeLookupMatch[1]));
|
||||||
|
sendJson(response, 200, { status: "ok", ...result });
|
||||||
|
} catch (error) {
|
||||||
|
const statusCode = error.message === "invalid barcode" ? 400 : 500;
|
||||||
|
sendJson(response, statusCode, { status: "error", message: error.message });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const gameIdMatch = url.pathname.match(/^\/api\/catalog\/games\/([0-9a-fA-F-]+)$/);
|
const gameIdMatch = url.pathname.match(/^\/api\/catalog\/games\/([0-9a-fA-F-]+)$/);
|
||||||
if (request.method === "PUT" && gameIdMatch) {
|
if (request.method === "PUT" && gameIdMatch) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
68
app.js
68
app.js
@@ -20,6 +20,7 @@ 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 barcodeInput = document.getElementById("barcodeInput");
|
||||||
const versionInput = document.getElementById("versionInput");
|
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");
|
||||||
@@ -241,6 +242,7 @@ gameForm.addEventListener("submit", async (event) => {
|
|||||||
brand: state.selectedBrand,
|
brand: state.selectedBrand,
|
||||||
consoleName: state.selectedConsole,
|
consoleName: state.selectedConsole,
|
||||||
title,
|
title,
|
||||||
|
barcode: barcodeInput.value.trim(),
|
||||||
version: versionInput.value.trim(),
|
version: versionInput.value.trim(),
|
||||||
genre: genreInput.value.trim(),
|
genre: genreInput.value.trim(),
|
||||||
publisher: publisherInput.value.trim(),
|
publisher: publisherInput.value.trim(),
|
||||||
@@ -283,6 +285,7 @@ gameForm.addEventListener("submit", async (event) => {
|
|||||||
games[idx] = {
|
games[idx] = {
|
||||||
...games[idx],
|
...games[idx],
|
||||||
title,
|
title,
|
||||||
|
barcode: barcodeInput.value.trim(),
|
||||||
version: versionInput.value.trim(),
|
version: versionInput.value.trim(),
|
||||||
genre: genreInput.value.trim(),
|
genre: genreInput.value.trim(),
|
||||||
publisher: publisherInput.value.trim(),
|
publisher: publisherInput.value.trim(),
|
||||||
@@ -298,6 +301,7 @@ gameForm.addEventListener("submit", async (event) => {
|
|||||||
const game = {
|
const game = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
title,
|
title,
|
||||||
|
barcode: barcodeInput.value.trim(),
|
||||||
version: versionInput.value.trim(),
|
version: versionInput.value.trim(),
|
||||||
genre: genreInput.value.trim(),
|
genre: genreInput.value.trim(),
|
||||||
publisher: publisherInput.value.trim(),
|
publisher: publisherInput.value.trim(),
|
||||||
@@ -686,6 +690,7 @@ function buildGamePayload(game, brand, consoleName, overrides = {}) {
|
|||||||
brand,
|
brand,
|
||||||
consoleName,
|
consoleName,
|
||||||
title: game.title || "",
|
title: game.title || "",
|
||||||
|
barcode: game.barcode || "",
|
||||||
version: game.version || "",
|
version: game.version || "",
|
||||||
genre: game.genre || "",
|
genre: game.genre || "",
|
||||||
publisher: game.publisher || "",
|
publisher: game.publisher || "",
|
||||||
@@ -865,6 +870,7 @@ function renderGames() {
|
|||||||
|
|
||||||
const metaParts = [
|
const metaParts = [
|
||||||
showLoanedOnly ? `${game.brand} / ${game.consoleName}` : null,
|
showLoanedOnly ? `${game.brand} / ${game.consoleName}` : null,
|
||||||
|
game.barcode ? `Code: ${game.barcode}` : null,
|
||||||
game.version ? `Version: ${game.version}` : null,
|
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,
|
||||||
@@ -898,6 +904,7 @@ function renderGames() {
|
|||||||
function startEditMode(game) {
|
function startEditMode(game) {
|
||||||
editingGameId = game.id;
|
editingGameId = game.id;
|
||||||
titleInput.value = game.title || "";
|
titleInput.value = game.title || "";
|
||||||
|
barcodeInput.value = game.barcode || "";
|
||||||
versionInput.value = game.version || "";
|
versionInput.value = game.version || "";
|
||||||
genreInput.value = game.genre || "";
|
genreInput.value = game.genre || "";
|
||||||
publisherInput.value = game.publisher || "";
|
publisherInput.value = game.publisher || "";
|
||||||
@@ -1029,6 +1036,10 @@ async function scanLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyScannedCode(codeValue) {
|
function applyScannedCode(codeValue) {
|
||||||
|
if (barcodeInput) {
|
||||||
|
barcodeInput.value = codeValue;
|
||||||
|
}
|
||||||
|
|
||||||
if (scannerLastCode) {
|
if (scannerLastCode) {
|
||||||
scannerLastCode.textContent = `Dernier code detecte: ${codeValue}`;
|
scannerLastCode.textContent = `Dernier code detecte: ${codeValue}`;
|
||||||
scannerLastCode.classList.remove("hidden");
|
scannerLastCode.classList.remove("hidden");
|
||||||
@@ -1043,6 +1054,63 @@ function applyScannedCode(codeValue) {
|
|||||||
if (titleInput && !normalizeText(titleInput.value)) {
|
if (titleInput && !normalizeText(titleInput.value)) {
|
||||||
titleInput.value = codeValue;
|
titleInput.value = codeValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lookupScannedBarcode(codeValue).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
updateScannerStatus(`Code detecte: ${codeValue} (lookup indisponible).`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function lookupScannedBarcode(codeValue) {
|
||||||
|
const normalized = normalizeText(codeValue);
|
||||||
|
if (!normalized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await apiRequest(`/api/barcode/lookup/${encodeURIComponent(normalized)}`, { timeoutMs: 7000 });
|
||||||
|
if (!result || result.status !== "ok") {
|
||||||
|
updateScannerStatus(`Code detecte: ${normalized}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const owned = result.owned && result.game;
|
||||||
|
if (owned) {
|
||||||
|
const ownedTitle = normalizeText(result.game.title) || "Jeu inconnu";
|
||||||
|
const ownedConsole = normalizeText(result.game.consoleName);
|
||||||
|
const ownedLabel = ownedConsole ? `${ownedTitle} (${ownedConsole})` : ownedTitle;
|
||||||
|
updateScannerStatus(`Deja possede: ${ownedLabel}`);
|
||||||
|
alert(`Deja dans ta collection: ${ownedLabel}`);
|
||||||
|
|
||||||
|
if (quickSearchInput) {
|
||||||
|
quickSearchInput.value = ownedTitle;
|
||||||
|
quickSearchTerm = ownedTitle;
|
||||||
|
renderSearchResults();
|
||||||
|
}
|
||||||
|
if (titleInput) {
|
||||||
|
titleInput.value = ownedTitle;
|
||||||
|
}
|
||||||
|
if (publisherInput && !normalizeText(publisherInput.value) && result.game.publisher) {
|
||||||
|
publisherInput.value = result.game.publisher;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAutoData = result.lookup && normalizeText(result.lookup.title);
|
||||||
|
if (hasAutoData) {
|
||||||
|
titleInput.value = result.lookup.title;
|
||||||
|
if (publisherInput && !normalizeText(publisherInput.value) && result.lookup.publisher) {
|
||||||
|
publisherInput.value = result.lookup.publisher;
|
||||||
|
}
|
||||||
|
if (quickSearchInput) {
|
||||||
|
quickSearchInput.value = result.lookup.title;
|
||||||
|
quickSearchTerm = result.lookup.title;
|
||||||
|
renderSearchResults();
|
||||||
|
}
|
||||||
|
updateScannerStatus(`Titre trouve automatiquement: ${result.lookup.title}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScannerStatus(`Code detecte: ${normalized} (aucune fiche auto).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeText(value) {
|
function normalizeText(value) {
|
||||||
|
|||||||
@@ -151,6 +151,10 @@
|
|||||||
Titre
|
Titre
|
||||||
<input id="titleInput" required placeholder="Final Fantasy X" />
|
<input id="titleInput" required placeholder="Final Fantasy X" />
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
Code-barres
|
||||||
|
<input id="barcodeInput" placeholder="EAN/UPC (auto via scan)" />
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Version
|
Version
|
||||||
<input id="versionInput" placeholder="Origine / Platinium / Complet..." />
|
<input id="versionInput" placeholder="Origine / Platinium / Complet..." />
|
||||||
|
|||||||
Reference in New Issue
Block a user