feat: bulk quantity edit for collection table
This commit is contained in:
1 parent
49bab7fb0a
commit
e162d50608
3 files changed
+204
-1
No files matched your search
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
+114
-1
@@ -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 = (() => {
|
||||
</table>
|
||||
</div>
|
||||
<div class="coll-pagination" id="coll-pagination"></div>
|
||||
<!-- Bulk Action Bar -->
|
||||
<div id="coll-bulk-bar" class="coll-bulk-bar" style="display:none">
|
||||
<span id="coll-bulk-count" class="coll-bulk-count">0 selected</span>
|
||||
<button class="coll-bulk-btn" onclick="Collection.selectAllRows()">Select All</button>
|
||||
<button class="coll-bulk-btn" onclick="Collection.clearSelection()">Clear</button>
|
||||
<span class="coll-bulk-sep">|</span>
|
||||
<span class="coll-bulk-label">Qty:</span>
|
||||
<button class="coll-bulk-btn" onclick="Collection.bulkAdjustQuantity(-1)" title="Subtract 1">−</button>
|
||||
<button class="coll-bulk-btn" onclick="Collection.bulkAdjustQuantity(1)" title="Add 1">+</button>
|
||||
<input type="number" id="coll-bulk-qty-input" class="coll-bulk-qty-input" min="0" value="1" placeholder="n">
|
||||
<button class="coll-bulk-btn coll-bulk-set-btn" onclick="Collection.bulkSetQuantity()">Set</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1077,7 +1090,7 @@ const Collection = (() => {
|
||||
return `<th class="coll-th coll-th-${col.key} coll-th-sortable${activeClass}"
|
||||
onclick="Collection.setSort('${col.key}')">${col.label}${arrow}</th>`;
|
||||
}).join('');
|
||||
return `<thead><tr>${headers}</tr></thead>`;
|
||||
return `<thead><tr>$<th class="coll-th-select"><input type="checkbox" id="coll-select-all" onclick="Collection.toggleSelectAll(this.checked)" title="Select all"></th>{headers}</tr></thead>`;
|
||||
}
|
||||
|
||||
// ── Table Body ──────────────────────────────────────────
|
||||
@@ -1127,6 +1140,7 @@ const Collection = (() => {
|
||||
const edhrecRank = (card.edhrecRank || card.edhrec_rank) ? `#${card.edhrecRank || card.edhrec_rank}` : '—';
|
||||
|
||||
return `<tr class="coll-row" onclick="Collection.openDrawer(${card.id})" data-id="${card.id}" data-img-url="${card.imageUrl || ''}">
|
||||
<td class="coll-td coll-td-select" onclick="event.stopPropagation()"><input type="checkbox" class="coll-row-check" data-id="${card.id}" ${state.selectedRows.has(card.id) ? 'checked' : ''} onchange="Collection.toggleRowSelect(${card.id}, this.checked)"></td>
|
||||
<td class="coll-td coll-td-quantity">${escapeHtml(card.quantity ?? 1)}</td>
|
||||
<td class="coll-td coll-td-name">
|
||||
<span class="coll-card-name">${escapeHtml(card.name)}</span>
|
||||
@@ -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,
|
||||
};
|
||||
})();
|
||||
Reference in new issue
Block a user