Add possibility to upload images for inventory units
This commit is contained in:
+10
-4
@@ -2,7 +2,8 @@ const state = { items: [], editingId: null };
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
|
||||
async function request(url, options = {}) {
|
||||
const response = await fetch(url, { headers: { 'Content-Type': 'application/json' }, ...options });
|
||||
const headers = options.body instanceof FormData ? {} : { 'Content-Type': 'application/json' };
|
||||
const response = await fetch(url, { headers, ...options });
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Something went wrong.');
|
||||
return data;
|
||||
@@ -23,15 +24,18 @@ async function load() {
|
||||
function renderItems(items) {
|
||||
const body = $('#inventoryBody');
|
||||
$('#emptyState').style.display = items.length ? 'none' : 'block';
|
||||
body.innerHTML = items.map((item) => `<tr class="inventory-row"><td><span class="item-title">${escapeHtml(item.name)}</span><span class="item-sub">${escapeHtml([item.brand, item.model].filter(Boolean).join(' · ') || 'No manufacturer details')}</span></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"><button class="small-action" onclick="editItem(${item.id})">Edit</button><button class="small-action delete" onclick="removeItem(${item.id})">Delete</button></div></td></tr>`).join('');
|
||||
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"><button class="small-action" onclick="editItem(${item.id})">Edit</button><button class="small-action delete" onclick="removeItem(${item.id})">Delete</button></div></td></tr>`).join('');
|
||||
}
|
||||
|
||||
function escapeHtml(value) { return String(value).replace(/[&<>'"]/g, (char) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char])); }
|
||||
function openModal(item = null) { state.editingId = item?.id || null; $('#modalTitle').textContent = item ? 'Edit item' : 'Add item'; $('#itemForm').reset(); if (item) Object.entries(item).forEach(([key, value]) => { const field = $(`[name="${key}"]`); if (field) field.value = value || ''; }); $('#modal').classList.add('open'); $('[name="name"]').focus(); }
|
||||
function ensureImageControls() { if ($('#imageInput')) return; const section = document.querySelector('.form-section'); section.insertAdjacentHTML('beforebegin', '<label class="full image-field">Device pictures<input id="imageInput" name="images" type="file" accept="image/jpeg,image/png,image/gif,image/webp" multiple><span id="imageName">Up to 5 pictures</span><div id="imageGallery" class="image-gallery"></div></label>'); }
|
||||
function renderImageGallery(images = []) { $('#imageGallery').innerHTML = images.map((image) => `<div class="gallery-item"><img src="${escapeHtml(image.url)}" alt="${escapeHtml(image.original_name || 'Device picture')}"><button type="button" class="gallery-delete" onclick="deleteImage(${image.id})" aria-label="Delete picture">×</button></div>`).join(''); }
|
||||
function openModal(item = null) { ensureImageControls(); state.editingId = item?.id || null; $('#modalTitle').textContent = item ? 'Edit item' : 'Add item'; $('#itemForm').reset(); $('#imageName').textContent = item?.images?.length ? `${item.images.length} saved picture${item.images.length === 1 ? '' : 's'}. Add more below.` : 'Up to 5 pictures'; renderImageGallery(item?.images || []); $('#modal').classList.add('open'); $('[name="name"]').focus(); if (item) Object.entries(item).forEach(([key, value]) => { const field = $(`[name="${key}"]`); if (field && key !== 'images') field.value = value || ''; }); }
|
||||
function closeModal() { $('#modal').classList.remove('open'); }
|
||||
function toast(message) { const element = $('#toast'); element.textContent = message; element.classList.add('show'); setTimeout(() => element.classList.remove('show'), 2500); }
|
||||
|
||||
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); } };
|
||||
|
||||
$('#addButton').addEventListener('click', () => openModal());
|
||||
@@ -40,6 +44,8 @@ $('#closeModal').addEventListener('click', closeModal);
|
||||
$('#cancelButton').addEventListener('click', closeModal);
|
||||
$('#modal').addEventListener('click', (event) => { if (event.target === $('#modal')) closeModal(); });
|
||||
['searchInput', 'typeFilter', 'statusFilter'].forEach((id) => $(`#${id}`).addEventListener(id === 'searchInput' ? 'input' : 'change', load));
|
||||
$('#itemForm').addEventListener('submit', async (event) => { event.preventDefault(); const formData = new FormData(event.target); const payload = Object.fromEntries(formData.entries()); try { await request(state.editingId ? `/api/items/${state.editingId}` : '/api/items', { method: state.editingId ? 'PUT' : 'POST', body: JSON.stringify(payload) }); closeModal(); toast(state.editingId ? 'Item updated.' : 'Item added to inventory.'); await load(); } catch (error) { toast(error.message); } });
|
||||
ensureImageControls();
|
||||
$('#imageInput').addEventListener('change', () => { const files = [...$('#imageInput').files]; $('#imageName').textContent = files.length ? `${files.length} new picture${files.length === 1 ? '' : 's'} selected` : 'Up to 5 pictures'; });
|
||||
$('#itemForm').addEventListener('submit', async (event) => { event.preventDefault(); const formData = new FormData(event.target); if (!$('#imageInput').files.length) formData.delete('images'); try { await request(state.editingId ? `/api/items/${state.editingId}` : '/api/items', { method: state.editingId ? 'PUT' : 'POST', body: formData }); closeModal(); toast(state.editingId ? 'Item updated.' : 'Item added to inventory.'); await load(); } catch (error) { toast(error.message); } });
|
||||
$('#exportButton').addEventListener('click', () => { if (!state.items.length) return toast('There are no visible items to export.'); const columns = ['name','item_type','brand','model','serial_number','asset_tag','status','location','purchase_date','cpu','ram','gpu','storage','media_format','notes']; const csv = [columns.join(','), ...state.items.map((item) => columns.map((column) => `"${String(item[column] || '').replaceAll('"', '""')}"`).join(','))].join('\n'); const link = document.createElement('a'); link.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' })); link.download = 'stockroom-inventory.csv'; link.click(); URL.revokeObjectURL(link.href); });
|
||||
load().catch((error) => toast(error.message));
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user