Add dynamic inventory type creation
This commit is contained in:
+32
-12
@@ -1,8 +1,9 @@
|
||||
const state = { items: [], editingId: null };
|
||||
const state = { items: [], types: [], editingId: null };
|
||||
const role = document.body.dataset.role;
|
||||
const canWrite = role === 'admin' || role === 'read_write';
|
||||
const canDelete = role === 'admin';
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
const fieldLabels = { brand: 'Brand', model: 'Model', serial_number: 'Serial number', asset_tag: 'Asset tag', location: 'Location', destination: 'Destination / assigned to', checkout_date: 'Checkout date', purchase_date: 'Purchase date', notes: 'Notes', cpu: 'CPU', ram: 'RAM', gpu: 'GPU', storage: 'Disk / storage', media_format: 'Media format' };
|
||||
|
||||
async function request(url, options = {}) {
|
||||
const headers = options.body instanceof FormData ? {} : { 'Content-Type': 'application/json' };
|
||||
@@ -12,6 +13,10 @@ async function request(url, options = {}) {
|
||||
return data;
|
||||
}
|
||||
|
||||
function typeOptions(selected = '') { return `<option value="">Select type</option>${state.types.map((type) => `<option value="${escapeHtml(type.name)}"${type.name === selected ? ' selected' : ''}>${escapeHtml(type.name)}</option>`).join('')}`; }
|
||||
function renderTypeSelectors() { const formType = $('[name="item_type"]'); const selectedFormType = formType.value; formType.innerHTML = typeOptions(selectedFormType); const filter = $('#typeFilter'); const selectedFilter = filter.value; filter.innerHTML = `<option value="">All types</option>${state.types.map((type) => `<option value="${escapeHtml(type.name)}"${type.name === selectedFilter ? ' selected' : ''}>${escapeHtml(type.name)}</option>`).join('')}`; }
|
||||
async function loadTypes() { state.types = await request('/api/types'); renderTypeSelectors(); updateTypeFields(); }
|
||||
|
||||
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')]);
|
||||
@@ -32,24 +37,20 @@ function renderItems(items) {
|
||||
}
|
||||
|
||||
function escapeHtml(value) { return String(value).replace(/[&<>'"]/g, (char) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char])); }
|
||||
function ensureImageControls() { if (!$('#destination')) $('[name="location"]').insertAdjacentHTML('afterend', '<label>Destination / assigned to<input id="destination" name="destination" placeholder="e.g. Employee, buyer, recycling"></label>'); if (!$('#checkoutDate')) $('#destination').parentElement.insertAdjacentHTML('afterend', '<label>Checkout date<input id="checkoutDate" name="checkout_date" type="date"></label>'); 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 ensureImageControls() { if (!$('#destination')) $('[name="location"]').insertAdjacentHTML('afterend', '<label data-configurable-field="destination">Destination / assigned to<input id="destination" name="destination" placeholder="e.g. Employee, buyer, recycling"></label>'); if (!$('#checkoutDate')) $('#destination').parentElement.insertAdjacentHTML('afterend', '<label data-configurable-field="checkout_date">Checkout date<input id="checkoutDate" name="checkout_date" type="date"></label>'); 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 updateTypeFields() {
|
||||
const type = $('[name="item_type"]').value;
|
||||
const technicalFields = new Set(['cpu', 'ram', 'gpu', 'storage']);
|
||||
const computerFields = new Set(['cpu', 'ram', 'gpu', 'storage']);
|
||||
const graphicsFields = new Set(['gpu']);
|
||||
const mediaFields = new Set(['media_format']);
|
||||
const visibleFields = new Set(state.types.find((entry) => entry.name === type)?.fields || []);
|
||||
document.querySelectorAll('#itemForm [name]').forEach((field) => {
|
||||
if (!technicalFields.has(field.name) && field.name !== 'media_format') return;
|
||||
const visible = type === 'Computer' ? computerFields.has(field.name) : type === 'Graphics' ? graphicsFields.has(field.name) : mediaFields.has(field.name) && ['CD', 'Long play disk', 'Audio'].includes(type);
|
||||
if (!Object.hasOwn(fieldLabels, field.name)) return;
|
||||
const visible = visibleFields.has(field.name);
|
||||
field.parentElement.hidden = !visible;
|
||||
if (!visible) field.value = '';
|
||||
});
|
||||
const section = document.querySelector('#itemForm .form-section');
|
||||
if (section) {
|
||||
section.hidden = !['Computer', 'Graphics', 'CD', 'Long play disk', 'Audio'].includes(type);
|
||||
section.hidden = !visibleFields.size;
|
||||
const heading = section.querySelector('span');
|
||||
if (heading) heading.firstChild.textContent = ['CD', 'Long play disk', 'Audio'].includes(type) ? 'Format details ' : 'Technical details ';
|
||||
if (heading) heading.firstChild.textContent = 'Type details ';
|
||||
}
|
||||
}
|
||||
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(''); }
|
||||
@@ -57,12 +58,31 @@ function openModal(item = null) { ensureImageControls(); state.editingId = item?
|
||||
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); }
|
||||
|
||||
function ensureTypeManager() {
|
||||
if ($('#typeManagerModal')) return;
|
||||
document.body.insertAdjacentHTML('beforeend', '<div class="modal-backdrop" id="typeManagerModal"><section class="modal type-manager"><div class="modal-header"><div><p class="eyebrow">Administration</p><h2>Inventory types</h2></div><button class="icon-button" id="closeTypeManager" aria-label="Close">×</button></div><form id="typeForm"><input id="editingTypeName" type="hidden"><div class="form-grid"><label class="full">Type name *<input id="typeName" required maxlength="80" placeholder="e.g. Camera"></label><div class="type-fields full" id="typeFields"></div></div><div class="form-actions"><button type="button" class="secondary-button" id="resetTypeForm">New type</button><button class="primary-button" type="submit">Save type</button></div></form><div class="type-list" id="typeList"></div></section></div>');
|
||||
$('#closeTypeManager').addEventListener('click', () => $('#typeManagerModal').classList.remove('open'));
|
||||
$('#resetTypeForm').addEventListener('click', resetTypeForm);
|
||||
$('#typeManagerModal').addEventListener('click', (event) => { if (event.target === $('#typeManagerModal')) $('#typeManagerModal').classList.remove('open'); });
|
||||
$('#typeForm').addEventListener('submit', saveType);
|
||||
}
|
||||
function resetTypeForm() { $('#typeForm').reset(); $('#editingTypeName').value = ''; }
|
||||
function renderTypeManager() {
|
||||
$('#typeFields').innerHTML = `<span>Fields shown for this type</span><div class="field-checklist">${Object.entries(fieldLabels).map(([key, label]) => `<label><input type="checkbox" name="type_field" value="${key}">${label}</label>`).join('')}</div>`;
|
||||
$('#typeList').innerHTML = state.types.map((type) => `<div class="type-row"><div><strong>${escapeHtml(type.name)}</strong><span>${type.fields.map((field) => escapeHtml(fieldLabels[field])).join(', ') || 'Name, type, and status only'}</span></div><div class="row-actions"><button class="small-action" type="button" onclick="editType(${JSON.stringify(type.name)})">Edit</button><button class="small-action delete" type="button" onclick="removeType(${JSON.stringify(type.name)})">Delete</button></div></div>`).join('');
|
||||
}
|
||||
function openTypeManager() { ensureTypeManager(); resetTypeForm(); renderTypeManager(); $('#typeManagerModal').classList.add('open'); }
|
||||
window.editType = (name) => { const type = state.types.find((entry) => entry.name === name); if (!type) return; $('#editingTypeName').value = type.name; $('#typeName').value = type.name; document.querySelectorAll('[name="type_field"]').forEach((field) => { field.checked = type.fields.includes(field.value); }); };
|
||||
window.removeType = async (name) => { if (!confirm(`Delete the "${name}" inventory type?`)) return; try { await request(`/api/types/${encodeURIComponent(name)}`, { method: 'DELETE' }); await loadTypes(); renderTypeManager(); toast('Inventory type deleted.'); } catch (error) { toast(error.message); } };
|
||||
async function saveType(event) { event.preventDefault(); const previousName = $('#editingTypeName').value; const fields = [...document.querySelectorAll('[name="type_field"]:checked')].map((field) => field.value); try { await request(previousName ? `/api/types/${encodeURIComponent(previousName)}` : '/api/types', { method: previousName ? 'PUT' : 'POST', body: JSON.stringify({ name: $('#typeName').value, fields }) }); await loadTypes(); renderTypeManager(); resetTypeForm(); toast('Inventory type saved.'); } catch (error) { toast(error.message); } }
|
||||
|
||||
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); } };
|
||||
|
||||
if ($('#addButton')) $('#addButton').addEventListener('click', () => openModal());
|
||||
if ($('#emptyAddButton')) $('#emptyAddButton').addEventListener('click', () => openModal());
|
||||
if ($('#typeManagerButton')) $('#typeManagerButton').addEventListener('click', openTypeManager);
|
||||
$('#closeModal').addEventListener('click', closeModal);
|
||||
$('#cancelButton').addEventListener('click', closeModal);
|
||||
$('#modal').addEventListener('click', (event) => { if (event.target === $('#modal')) closeModal(); });
|
||||
@@ -79,4 +99,4 @@ if ($('#restoreInput')) $('#restoreInput').addEventListener('change', async () =
|
||||
async function loadUsers() { const users = await request('/api/users'); $('#usersList').innerHTML = users.map((user) => `<p class="user-row"><strong>${escapeHtml(user.username)}</strong><span>${escapeHtml(user.role.replace('_', ' '))}${user.can_data_tools ? ' · data tools' : ''}</span></p>`).join(''); }
|
||||
if ($('#usersButton')) $('#usersButton').addEventListener('click', async () => { $('#usersPanel').classList.toggle('hidden'); if (!$('#usersPanel').classList.contains('hidden')) { try { await loadUsers(); } catch (error) { toast(error.message); } } });
|
||||
if ($('#userForm')) $('#userForm').addEventListener('submit', async (event) => { event.preventDefault(); const form = new FormData(event.target); try { await request('/api/users', { method: 'POST', body: JSON.stringify({ username: form.get('username'), password: form.get('password'), role: form.get('role'), can_data_tools: form.get('can_data_tools') === 'on' }) }); event.target.reset(); await loadUsers(); toast('User added.'); } catch (error) { toast(error.message); } });
|
||||
load().catch((error) => toast(error.message));
|
||||
loadTypes().then(load).catch((error) => toast(error.message));
|
||||
|
||||
Reference in New Issue
Block a user