diff --git a/routes/collection.py b/routes/collection.py index e681535..d375396 100644 --- a/routes/collection.py +++ b/routes/collection.py @@ -406,6 +406,11 @@ async def update_collection_card(cardId: int, body: dict): if finish_val not in ("NORMAL", "FOIL", "ETCHED"): raise HTTPException(400, "finish must be NORMAL, FOIL, or ETCHED") updates["finish"] = finish_val + if "quantity" in body: + qty_val = int(body["quantity"]) + if qty_val < 0: + raise HTTPException(400, "quantity must be >= 0") + updates["quantity"] = qty_val if not updates: return _add_image_url(_row_to_dict(row)) diff --git a/ui/collection.css b/ui/collection.css index a2a0adc..273082b 100644 --- a/ui/collection.css +++ b/ui/collection.css @@ -3147,3 +3147,88 @@ .coll-col-reset:hover { background: rgba(99,102,241,0.25); } + + +/* ── Bulk Action Bar ──────────────────────────────────────────── */ +.coll-bulk-bar { + position: sticky; + bottom: 0; + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: var(--bg-card, #1e1e1e); + border-top: 1px solid var(--border, #30363d); + border-radius: 0 0 8px 8px; + z-index: 50; + flex-wrap: wrap; + box-shadow: 0 -2px 8px rgba(0,0,0,0.3); +} +.coll-bulk-count { + font-size: 0.85rem; + font-weight: 600; + color: var(--accent, #6366f1); + min-width: 80px; +} +.coll-bulk-sep { + color: var(--text-secondary, #8b949e); + padding: 0 2px; +} +.coll-bulk-label { + font-size: 0.82rem; + color: var(--text-secondary, #8b949e); +} +.coll-bulk-btn { + background: rgba(99,102,241,0.15); + border: 1px solid rgba(99,102,241,0.35); + color: var(--text, #e6edf3); + border-radius: 5px; + padding: 4px 10px; + font-size: 0.82rem; + cursor: pointer; + transition: background 0.15s; +} +.coll-bulk-btn:hover:not(:disabled) { + background: rgba(99,102,241,0.3); +} +.coll-bulk-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.coll-bulk-set-btn { + background: rgba(99,102,241,0.25); + font-weight: 600; +} +.coll-bulk-qty-input { + width: 56px; + background: var(--bg, #0d1117); + border: 1px solid var(--border, #30363d); + color: var(--text, #e6edf3); + border-radius: 5px; + padding: 3px 6px; + font-size: 0.82rem; + text-align: center; +} + +/* ── Select Checkbox Column ─────────────────────────────────── */ +.coll-th-select { + width: 32px; + min-width: 32px; + text-align: center; + padding: 4px; +} +.coll-td-select { + width: 32px; + min-width: 32px; + text-align: center; + padding: 4px; +} +.coll-row-check { + cursor: pointer; + width: 14px; + height: 14px; + accent-color: var(--accent, #6366f1); +} +.coll-row.selected-row { + background: rgba(99,102,241,0.08); +} \ No newline at end of file diff --git a/ui/collection.js b/ui/collection.js index fed7991..fc6325b 100644 --- a/ui/collection.js +++ b/ui/collection.js @@ -164,6 +164,7 @@ const Collection = (() => { importHeaders: [], importMissingFinishNormal: true, exportOpen: false, + selectedRows: new Set(), preferences: { pageSize: 50, sortField: 'name', @@ -662,6 +663,18 @@ const Collection = (() => {
+ + @@ -1077,7 +1090,7 @@ const Collection = (() => { return `${col.label}${arrow}`; }).join(''); - return `${headers}`; + return `${headers}`; } // ── Table Body ────────────────────────────────────────── @@ -1127,6 +1140,7 @@ const Collection = (() => { const edhrecRank = (card.edhrecRank || card.edhrec_rank) ? `#${card.edhrecRank || card.edhrec_rank}` : '—'; return ` + ${escapeHtml(card.quantity ?? 1)} ${escapeHtml(card.name)} @@ -2477,6 +2491,98 @@ const Collection = (() => { window.location.href = '/index.html'; } + // ── Bulk Row Selection & Quantity Edit ───────────────────── + function toggleRowSelect(cardId, checked) { + if (checked) { + state.selectedRows.add(cardId); + } else { + state.selectedRows.delete(cardId); + } + updateBulkBar(); + } + + function toggleSelectAll(checked) { + if (checked) { + state.items.forEach(c => state.selectedRows.add(c.id)); + } else { + state.selectedRows.clear(); + } + // Update all checkboxes + document.querySelectorAll('.coll-row-check').forEach(cb => { + cb.checked = checked; + }); + updateBulkBar(); + } + + function selectAllRows() { + state.items.forEach(c => state.selectedRows.add(c.id)); + document.querySelectorAll('.coll-row-check').forEach(cb => cb.checked = true); + const selectAllCb = document.getElementById('coll-select-all'); + if (selectAllCb) selectAllCb.checked = true; + updateBulkBar(); + } + + function clearSelection() { + state.selectedRows.clear(); + document.querySelectorAll('.coll-row-check').forEach(cb => cb.checked = false); + const selectAllCb = document.getElementById('coll-select-all'); + if (selectAllCb) selectAllCb.checked = false; + updateBulkBar(); + } + + function updateBulkBar() { + const bar = document.getElementById('coll-bulk-bar'); + const countEl = document.getElementById('coll-bulk-count'); + const n = state.selectedRows.size; + if (bar) bar.style.display = n > 0 ? 'flex' : 'none'; + if (countEl) countEl.textContent = `${n} selected`; + } + + async function bulkAdjustQuantity(delta) { + if (state.selectedRows.size === 0) return; + const ids = [...state.selectedRows]; + const btn = event.target; + if (btn) btn.disabled = true; + let successCount = 0; + for (const id of ids) { + const card = state.items.find(c => c.id === id); + if (!card) continue; + const newQty = Math.max(0, (card.quantity ?? 1) + delta); + try { + await patchCard(id, { quantity: newQty }); + card.quantity = newQty; + successCount++; + } catch (e) { + console.warn('[Bulk] Failed to update card', id, e); + } + } + if (btn) btn.disabled = false; + renderTable(); + applyColumnVisibility(); + } + + async function bulkSetQuantity() { + if (state.selectedRows.size === 0) return; + const input = document.getElementById('coll-bulk-qty-input'); + const newQty = input ? Math.max(0, parseInt(input.value, 10) || 0) : 0; + const ids = [...state.selectedRows]; + const btn = document.querySelector('.coll-bulk-set-btn'); + if (btn) btn.disabled = true; + for (const id of ids) { + const card = state.items.find(c => c.id === id); + if (!card) continue; + try { + await patchCard(id, { quantity: newQty }); + card.quantity = newQty; + } catch (e) { + console.warn('[Bulk] Failed to set card', id, e); + } + } + if (btn) btn.disabled = false; + renderTable(); + applyColumnVisibility(); + } + // ── Event Binding ──────────────────────────────────────── function bindEvents() { @@ -3081,5 +3187,12 @@ const Collection = (() => { openColumnPanel, closeColumnPanel, resetColumns, + // Bulk selection & quantity + toggleRowSelect, + toggleSelectAll, + selectAllRows, + clearSelection, + bulkAdjustQuantity, + bulkSetQuantity, }; })();