Add search and bulk delete items

This commit is contained in:
2026-08-29 07:24:33 +03:00
parent ade6a0931a
commit 634a25b2c6
85 changed files with 41 additions and 5 deletions
+18 -2
View File
@@ -1,4 +1,4 @@
const state = { items: [], types: [], editingId: null };
const state = { items: [], types: [], editingId: null, selectedIds: new Set() };
const role = document.body.dataset.role;
const canWrite = role === 'admin' || role === 'read_write';
const canDelete = role === 'admin';
@@ -21,6 +21,7 @@ async function load() {
const query = new URLSearchParams({ q: $('#searchInput').value, type: $('#typeFilter').value, status: $('#statusFilter').value });
const [items, summary] = await Promise.all([request(`/api/items?${query}`), request('/api/summary')]);
state.items = items;
state.selectedIds = new Set([...state.selectedIds].filter((itemId) => items.some((item) => item.id === itemId)));
$('#totalCount').textContent = summary.total;
$('#availableCount').textContent = summary.available;
$('#checkedOutCount').textContent = summary.checked_out;
@@ -33,7 +34,18 @@ function renderItems(items) {
const body = $('#inventoryBody');
const actions = (itemId) => `${canWrite ? `<button class="small-action" onclick="editItem(${itemId})">Edit</button>` : ''}${canDelete ? `<button class="small-action delete" onclick="removeItem(${itemId})">Delete</button>` : ''}`;
$('#emptyState').style.display = items.length ? 'none' : 'block';
body.innerHTML = items.map((item) => `<tr class="inventory-row"><td><div class="item-cell">${item.images?.length ? `<img class="item-thumb" src="${escapeHtml(item.images[0].url)}" alt="">${item.images.length > 1 ? `<span class="image-count">+${item.images.length - 1}</span>` : ''}` : '<div class="item-thumb placeholder">◌</div>'}<div><span class="item-title">${escapeHtml(item.name)}</span><span class="item-sub">${escapeHtml([item.brand, item.model].filter(Boolean).join(' · ') || 'No manufacturer details')}</span></div></div></td><td><span class="type-pill">${escapeHtml(item.item_type)}</span></td><td><span class="identifier">${escapeHtml(item.serial_number || 'No serial')}<em>${escapeHtml(item.asset_tag || 'No asset tag')}</em></span></td><td><span class="location">${escapeHtml(item.location || 'Unassigned')}</span></td><td><span class="status ${item.status.toLowerCase().replace(' ', '-')}">${escapeHtml(item.status)}</span></td><td><div class="row-actions">${actions(item.id)}</div></td></tr>`).join('');
body.innerHTML = items.map((item) => `<tr class="inventory-row">${canDelete ? `<td class="selection-column"><input class="item-selector" type="checkbox" value="${item.id}" aria-label="Select ${escapeHtml(item.name)}"${state.selectedIds.has(item.id) ? ' checked' : ''}></td>` : ''}<td><div class="item-cell">${item.images?.length ? `<img class="item-thumb" src="${escapeHtml(item.images[0].url)}" alt="">${item.images.length > 1 ? `<span class="image-count">+${item.images.length - 1}</span>` : ''}` : '<div class="item-thumb placeholder">◌</div>'}<div><span class="item-title">${escapeHtml(item.name)}</span><span class="item-sub">${escapeHtml([item.brand, item.model].filter(Boolean).join(' · ') || 'No manufacturer details')}</span></div></div></td><td><span class="type-pill">${escapeHtml(item.item_type)}</span></td><td><span class="identifier">${escapeHtml(item.serial_number || 'No serial')}<em>${escapeHtml(item.asset_tag || 'No asset tag')}</em></span></td><td><span class="location">${escapeHtml(item.location || 'Unassigned')}</span></td><td><span class="status ${item.status.toLowerCase().replace(' ', '-')}">${escapeHtml(item.status)}</span></td><td><div class="row-actions">${actions(item.id)}</div></td></tr>`).join('');
updateBulkActions();
}
function updateBulkActions() {
if (!canDelete) return;
const count = state.selectedIds.size;
$('#selectedCount').textContent = `${count} ${count === 1 ? 'item' : 'items'} selected`;
$('#bulkActions').classList.toggle('hidden', count === 0);
$('#bulkDeleteButton').disabled = count === 0;
$('#selectAllItems').checked = Boolean(state.items.length) && state.items.every((item) => state.selectedIds.has(item.id));
$('#selectAllItems').indeterminate = count > 0 && !$('#selectAllItems').checked;
}
function escapeHtml(value) { return String(value).replace(/[&<>'"]/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[char])); }
@@ -79,10 +91,14 @@ async function saveType(event) { event.preventDefault(); const previousName = $(
window.editItem = (id) => openModal(state.items.find((item) => item.id === id));
window.deleteImage = async (imageId) => { if (!state.editingId || !confirm('Delete this picture?')) return; try { const item = await request(`/api/items/${state.editingId}/images/${imageId}`, { method: 'DELETE' }); renderImageGallery(item.images); $('#imageName').textContent = `${item.images.length} saved picture${item.images.length === 1 ? '' : 's'}. Add more below.`; state.items = state.items.map((entry) => entry.id === item.id ? item : entry); renderItems(state.items); toast('Picture deleted.'); } catch (error) { toast(error.message); } };
window.removeItem = async (id) => { const item = state.items.find((entry) => entry.id === id); if (!confirm(`Delete "${item.name}"?`)) return; try { await request(`/api/items/${id}`, { method: 'DELETE' }); toast('Item removed from inventory.'); await load(); } catch (error) { toast(error.message); } };
async function removeSelectedItems() { const count = state.selectedIds.size; if (!count || !confirm(`Remove ${count} selected ${count === 1 ? 'item' : 'items'} from inventory?`)) return; try { const result = await request('/api/items/bulk-delete', { method: 'POST', body: JSON.stringify({ item_ids: [...state.selectedIds] }) }); state.selectedIds.clear(); toast(`${result.deleted} ${result.deleted === 1 ? 'item' : 'items'} removed from inventory.`); await load(); } catch (error) { toast(error.message); } }
if ($('#addButton')) $('#addButton').addEventListener('click', () => openModal());
if ($('#emptyAddButton')) $('#emptyAddButton').addEventListener('click', () => openModal());
if ($('#typeManagerButton')) $('#typeManagerButton').addEventListener('click', openTypeManager);
if ($('#selectAllItems')) $('#selectAllItems').addEventListener('change', (event) => { state.items.forEach((item) => { if (event.target.checked) state.selectedIds.add(item.id); else state.selectedIds.delete(item.id); }); renderItems(state.items); });
if ($('#inventoryBody')) $('#inventoryBody').addEventListener('change', (event) => { if (!event.target.matches('.item-selector')) return; const itemId = Number(event.target.value); if (event.target.checked) state.selectedIds.add(itemId); else state.selectedIds.delete(itemId); updateBulkActions(); });
if ($('#bulkDeleteButton')) $('#bulkDeleteButton').addEventListener('click', removeSelectedItems);
$('#closeModal').addEventListener('click', closeModal);
$('#cancelButton').addEventListener('click', closeModal);
$('#modal').addEventListener('click', (event) => { if (event.target === $('#modal')) closeModal(); });
+2 -1
View File
@@ -1,4 +1,5 @@
.type-manager{max-width:720px}.type-fields>span{display:block;color:var(--muted);font-size:12px;font-weight:700;margin-bottom:9px}.field-checklist{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px 14px}.field-checklist label{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--ink)}.field-checklist input{width:auto}.type-list{border-top:1px solid var(--line);margin-top:22px;padding-top:4px}.type-row{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--line);padding:13px 0}.type-row strong,.type-row span{display:block}.type-row span{color:var(--muted);font-size:11px;margin-top:4px;max-width:460px}.type-row .row-actions{display:flex;gap:7px;flex:none}@media(max-width:640px){.field-checklist{grid-template-columns:repeat(2,minmax(0,1fr))}.type-row{align-items:flex-start}.type-row .row-actions{flex-direction:column}.type-row span{max-width:230px}}
.bulk-actions{display:flex;align-items:center;gap:9px;margin-left:auto;color:var(--muted);font-size:12px;font-weight:700}.selection-column{width:34px;text-align:center}.selection-column input{accent-color:var(--teal)}
.type-manager{max-width:720px}.type-fields>span{display:block;color:var(--muted);font-size:12px;font-weight:700;margin-bottom:9px}.field-checklist{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px 14px}.field-checklist label{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--ink)}.field-checklist input{width:auto}.type-list{border-top:1px solid var(--line);margin-top:22px;padding-top:4px}.type-row{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--line);padding:13px 0}.type-row strong,.type-row span{display:block}.type-row span{color:var(--muted);font-size:11px;margin-top:4px;max-width:460px}.type-row .row-actions{display:flex;gap:7px;flex:none}@media(max-width:640px){.field-checklist{grid-template-columns:repeat(2,minmax(0,1fr))}.type-row{align-items:flex-start}.type-row .row-actions{flex-direction:column}.type-row span{max-width:230px}.bulk-actions{width:100%;margin-left:0;justify-content:space-between}}
.item-cell{display:flex;align-items:center;gap:11px}.item-thumb{width:38px;height:38px;object-fit:cover;border-radius:6px;background:#e8efeb;flex:none}.item-thumb.placeholder{display:grid;place-items:center;color:#8aa39b;font-size:20px}.image-count{background:#e7f3f0;color:var(--teal);font-size:10px;font-weight:700;padding:4px 5px;border-radius:4px;margin-left:-7px}.image-field input{padding:7px 0!important;border:0!important}.image-field span{display:block;color:#8c9994;font-size:10px;font-weight:400;margin-top:4px}.image-gallery{display:flex;flex-wrap:wrap;gap:9px;margin-top:11px}.gallery-item{position:relative;width:82px;height:64px}.gallery-item img{width:100%;height:100%;object-fit:cover;border-radius:6px;border:1px solid var(--line)}.gallery-delete{position:absolute;top:-7px;right:-7px;width:20px;height:20px;padding:0;border:0;border-radius:50%;background:#193f3b;color:#fff;cursor:pointer;line-height:18px}.hidden{display:none!important}
.row-actions{opacity:1!important}
.login-page{min-height:100vh;display:grid;place-items:center;background:linear-gradient(135deg,#f6f8f5,#dcece5)}.login-card{width:min(390px,calc(100% - 32px));padding:34px;background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.login-card .brand-mark{margin-bottom:22px}.login-card h1{font-family:'Space Grotesk';margin:0 0 24px}.login-card form{display:grid;gap:16px}.login-card label,.user-form label{display:grid;gap:6px;color:var(--muted);font-size:12px;font-weight:700}.login-card input,.user-form input,.user-form select{width:100%;padding:11px;border:1px solid var(--line);border-radius:6px;background:#fff}.login-card .primary-button{margin-top:8px}.login-error{padding:10px;background:#fff0ed;color:#a33e2b;border-radius:6px;font-size:12px}.logout-button{margin-top:12px;border:0;background:transparent;color:#90afa9;padding:0;cursor:pointer;font-size:11px}.users-panel{margin-top:24px;padding:24px;background:#fff;border:1px solid var(--line);border-radius:8px}.user-form{display:flex;align-items:end;gap:12px;flex-wrap:wrap}.user-form label{min-width:150px}.user-form .checkbox-label{display:flex;align-items:center;gap:7px;min-width:auto}.user-form .checkbox-label input{width:auto}.user-row{display:flex;justify-content:space-between;border-top:1px solid var(--line);padding:10px 0;margin:18px 0 0;font-size:12px}.user-row span{color:var(--muted)}