Make inventory items more dynamic

This commit is contained in:
2026-08-27 01:31:10 +03:00
parent 0c195eca1b
commit 100c0797a4
3 changed files with 145 additions and 2 deletions
+113
View File
@@ -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",
+10 -1
View File
@@ -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
View File
@@ -33,8 +33,27 @@ function renderItems(items) {
function escapeHtml(value) { return String(value).replace(/[&<>'"]/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[char])); } function escapeHtml(value) { return String(value).replace(/[&<>'"]/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[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); });