Make inventory items more dynamic
This commit is contained in:
Vendored
+113
@@ -1,6 +1,119 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "Rebuild Stockroom Docker app",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"compose",
|
||||||
|
"up",
|
||||||
|
"--build",
|
||||||
|
"-d"
|
||||||
|
],
|
||||||
|
"isBackground": false,
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Rebuild Stockroom Docker app",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"compose",
|
||||||
|
"up",
|
||||||
|
"--build",
|
||||||
|
"-d"
|
||||||
|
],
|
||||||
|
"isBackground": false,
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Inspect container inventory database",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"compose",
|
||||||
|
"exec",
|
||||||
|
"-T",
|
||||||
|
"inventory",
|
||||||
|
"python",
|
||||||
|
"-c",
|
||||||
|
"import sqlite3; c=sqlite3.connect('/data/inventory.db'); print(c.execute('select count(*) from inventory_items').fetchone()[0]); print(c.execute('select count(*) from inventory_images').fetchone()[0])"
|
||||||
|
],
|
||||||
|
"isBackground": false,
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Inspect Docker inventory mount",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"inspect",
|
||||||
|
"storageinventory-inventory-1",
|
||||||
|
"--format={{json .Mounts}}"
|
||||||
|
],
|
||||||
|
"isBackground": false,
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Check container database count",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"compose",
|
||||||
|
"exec",
|
||||||
|
"-T",
|
||||||
|
"inventory",
|
||||||
|
"python",
|
||||||
|
"-c",
|
||||||
|
"import sqlite3; print(sqlite3.connect(\"/data/inventory.db\").execute(\"select count(*) from inventory_items\").fetchone()[0])"
|
||||||
|
],
|
||||||
|
"isBackground": false,
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "List container data files",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"compose",
|
||||||
|
"exec",
|
||||||
|
"-T",
|
||||||
|
"inventory",
|
||||||
|
"ls",
|
||||||
|
"-l",
|
||||||
|
"/data"
|
||||||
|
],
|
||||||
|
"isBackground": false,
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Check container database path",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"compose",
|
||||||
|
"exec",
|
||||||
|
"-T",
|
||||||
|
"inventory",
|
||||||
|
"printenv",
|
||||||
|
"DATABASE_PATH"
|
||||||
|
],
|
||||||
|
"isBackground": false,
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Check running inventory containers",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"ps",
|
||||||
|
"--format",
|
||||||
|
"{{.Names}} {{.Ports}}"
|
||||||
|
],
|
||||||
|
"isBackground": false,
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Rebuild Stockroom Docker app",
|
"label": "Rebuild Stockroom Docker app",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ def clean(value):
|
|||||||
|
|
||||||
|
|
||||||
def item_payload(data):
|
def item_payload(data):
|
||||||
return {
|
payload = {
|
||||||
"name": clean(data.get("name")),
|
"name": clean(data.get("name")),
|
||||||
"item_type": clean(data.get("item_type")),
|
"item_type": clean(data.get("item_type")),
|
||||||
"brand": clean(data.get("brand")),
|
"brand": clean(data.get("brand")),
|
||||||
@@ -240,6 +240,15 @@ def item_payload(data):
|
|||||||
"storage": clean(data.get("storage")),
|
"storage": clean(data.get("storage")),
|
||||||
"media_format": clean(data.get("media_format")),
|
"media_format": clean(data.get("media_format")),
|
||||||
}
|
}
|
||||||
|
if payload["item_type"] != "Computer":
|
||||||
|
payload["cpu"] = ""
|
||||||
|
payload["ram"] = ""
|
||||||
|
if payload["item_type"] not in {"Computer", "Graphics"}:
|
||||||
|
payload["gpu"] = ""
|
||||||
|
payload["storage"] = ""
|
||||||
|
if payload["item_type"] not in {"CD", "Long play disk", "Audio"}:
|
||||||
|
payload["media_format"] = ""
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
def serialize(row, connection):
|
def serialize(row, connection):
|
||||||
|
|||||||
+22
-1
@@ -33,8 +33,27 @@ function renderItems(items) {
|
|||||||
|
|
||||||
function escapeHtml(value) { return String(value).replace(/[&<>'"]/g, (char) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char])); }
|
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>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 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']);
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
const heading = section.querySelector('span');
|
||||||
|
if (heading) heading.firstChild.textContent = ['CD', 'Long play disk', 'Audio'].includes(type) ? 'Format details ' : 'Technical 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(''); }
|
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 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 || []); if (item) Object.entries(item).forEach(([key, value]) => { const field = $(`[name="${key}"]`); if (field && key !== 'images') field.value = value || ''; }); updateTypeFields(); $('#modal').classList.add('open'); $('[name="name"]').focus(); }
|
||||||
function closeModal() { $('#modal').classList.remove('open'); }
|
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 toast(message) { const element = $('#toast'); element.textContent = message; element.classList.add('show'); setTimeout(() => element.classList.remove('show'), 2500); }
|
||||||
|
|
||||||
@@ -49,6 +68,8 @@ $('#cancelButton').addEventListener('click', closeModal);
|
|||||||
$('#modal').addEventListener('click', (event) => { if (event.target === $('#modal')) closeModal(); });
|
$('#modal').addEventListener('click', (event) => { if (event.target === $('#modal')) closeModal(); });
|
||||||
['searchInput', 'typeFilter', 'statusFilter'].forEach((id) => $(`#${id}`).addEventListener(id === 'searchInput' ? 'input' : 'change', load));
|
['searchInput', 'typeFilter', 'statusFilter'].forEach((id) => $(`#${id}`).addEventListener(id === 'searchInput' ? 'input' : 'change', load));
|
||||||
ensureImageControls();
|
ensureImageControls();
|
||||||
|
$('#itemForm [name="item_type"]').addEventListener('change', updateTypeFields);
|
||||||
|
updateTypeFields();
|
||||||
$('#imageInput').addEventListener('change', () => { const files = [...$('#imageInput').files]; $('#imageName').textContent = files.length ? `${files.length} new picture${files.length === 1 ? '' : 's'} selected` : 'Up to 5 pictures'; });
|
$('#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); } });
|
$('#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); } });
|
||||||
if ($('#exportButton')) $('#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','destination','checkout_date','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); });
|
if ($('#exportButton')) $('#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','destination','checkout_date','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); });
|
||||||
|
|||||||
Reference in New Issue
Block a user