From e162d506084e421bb7eeb8757e41c52ba5365e9d Mon Sep 17 00:00:00 2001
From: KoalaTrapLord <265133236+KoalaTrapLord@users.noreply.github.com>
Date: Wed, 25 Mar 2026 10:12:03 -0400
Subject: [PATCH] feat: bulk quantity edit for collection table
---
routes/collection.py | 5 ++
ui/collection.css | 85 ++++++++++++++++++++++++++++++++
ui/collection.js | 115 ++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 204 insertions(+), 1 deletion(-)
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 = (() => {
+
+
+ 0 selected
+
+
+ |
+ Qty:
+
+
+
+
+
@@ -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,
};
})();
|