diff --git a/static/app.js b/static/app.js index 6ba57e3..ca917da 100644 --- a/static/app.js +++ b/static/app.js @@ -11,11 +11,27 @@ async function api(path, opts = {}) { if (!resp.ok) { let msg = resp.statusText; try { msg = (await resp.json()).detail || msg; } catch {} - throw new Error(msg); + const err = new Error(msg); + err.status = resp.status; + throw err; } return resp.json(); } +// create a request; on a duplicate warning (409) ask the user before retrying +async function createRequest(body) { + try { + return await api("/api/requests", { method: "POST", body: JSON.stringify(body) }); + } catch (err) { + if (err.status === 409 && confirm(err.message + "\nTrotzdem anlegen?")) { + return api("/api/requests?allow_duplicate=true", { + method: "POST", body: JSON.stringify(body), + }); + } + throw err; + } +} + function toast(msg, isError = false) { const t = $("#toast"); t.textContent = msg; @@ -216,15 +232,12 @@ $("#search-form").addEventListener("submit", async (e) => { if (!libId) { toast("Erst eine Library für diesen Typ anlegen (Tab Libraries)", true); return; } const volInput = box.querySelector(`[data-vol="${i}"]`); try { - await api("/api/requests", { - method: "POST", - body: JSON.stringify({ + await createRequest({ library_id: parseInt(libId), title: r.title, authors: r.authors, narrator: r.narrator || "", external_id: r.external_id, year: r.year, series: r.series, cover_url: r.cover_url, volume: volInput && volInput.value ? parseInt(volInput.value) : (r.volume ?? null), - }), }); setLastLib(type, libId); btn.textContent = "✓ Angefragt"; @@ -277,16 +290,13 @@ $("#manual-form").addEventListener("submit", async (e) => { }); toast(`${created.length} Anfragen angelegt`); } else { - await api("/api/requests", { - method: "POST", - body: JSON.stringify({ - library_id: parseInt(data.library_id), - title: data.title, authors: data.authors, series: data.series, - narrator: data.narrator || "", - external_id: data.external_id, - year: data.year ? parseInt(data.year) : null, - volume: data.volume ? parseInt(data.volume) : null, - }), + await createRequest({ + library_id: parseInt(data.library_id), + title: data.title, authors: data.authors, series: data.series, + narrator: data.narrator || "", + external_id: data.external_id, + year: data.year ? parseInt(data.year) : null, + volume: data.volume ? parseInt(data.volume) : null, }); toast("Anfrage angelegt"); } @@ -675,10 +685,47 @@ function openQuickDialog(i) { $("#quick-library").innerHTML = libOptions(item.media_type) || ""; $("#quick-results").innerHTML = ""; + renderQuickOpenRequests(item); $("#quick-dialog").showModal(); runQuickSearch(); } +// offer linking to similar already-open requests instead of creating a duplicate +function renderQuickOpenRequests(item) { + const box = $("#quick-open-requests"); + const tokens = cleanFileName(item.name).toLowerCase().split(" ").filter((t) => t.length > 2); + const matches = missingReqs + .filter((r) => r.media_type === item.media_type) + .map((r) => { + const hay = `${r.title} ${r.authors} ${r.series} ${r.narrator}`.toLowerCase(); + return { r, hits: tokens.filter((t) => hay.includes(t)).length }; + }) + .filter((m) => m.hits >= 2) + .sort((a, b) => b.hits - a.hits) + .slice(0, 3); + if (!matches.length) { box.innerHTML = ""; return; } + box.innerHTML = + '

Passende offene Requests:

' + + matches + .map( + ({ r }) => `
+ ${esc(r.title)}${r.volume != null ? " · Band " + r.volume : ""} — ${esc(r.authors)} (${esc(r.library_name)}) + +
` + ) + .join(""); + box.querySelectorAll("[data-link-req]").forEach((b) => + b.addEventListener("click", () => { + const i = quickItemIndex; + scanItems[i].request_id = parseInt(b.dataset.linkReq); + scanItems[i].checked = true; + renderImportTable(); + $("#quick-dialog").close(); + toast("Mit bestehendem Request verbunden"); + }) + ); +} + async function runQuickSearch() { const item = scanItems[quickItemIndex]; const box = $("#quick-results"); @@ -726,14 +773,11 @@ async function pickQuickResult(r) { if (!libId) { toast("Erst eine Library für diesen Typ anlegen", true); return; } const i = quickItemIndex; try { - const req = await api("/api/requests", { - method: "POST", - body: JSON.stringify({ - library_id: parseInt(libId), - title: r.title, authors: r.authors, narrator: r.narrator || "", - external_id: r.external_id, volume: r.volume ?? null, - year: r.year, series: r.series, cover_url: r.cover_url, - }), + const req = await createRequest({ + library_id: parseInt(libId), + title: r.title, authors: r.authors, narrator: r.narrator || "", + external_id: r.external_id, volume: r.volume ?? null, + year: r.year, series: r.series, cover_url: r.cover_url, }); setLastLib(scanItems[i].media_type, libId); missingReqs.push(req); diff --git a/static/index.html b/static/index.html index 738650a..59f16c5 100644 --- a/static/index.html +++ b/static/index.html @@ -179,6 +179,7 @@ +