From 100c0797a4842f9d5175a261b3ff64aabc9c343e Mon Sep 17 00:00:00 2001 From: Tero Huttunen Date: Thu, 27 Aug 2026 01:31:10 +0300 Subject: [PATCH] Make inventory items more dynamic --- .vscode/tasks.json | 113 +++++++++++++++++++++++++++++++++++++++++++++ app.py | 11 ++++- static/app.js | 23 ++++++++- 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 220ccc9..3df7c86 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,6 +1,119 @@ { "version": "2.0.0", "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", "type": "shell", diff --git a/app.py b/app.py index c487d2f..0c3360e 100644 --- a/app.py +++ b/app.py @@ -221,7 +221,7 @@ def clean(value): def item_payload(data): - return { + payload = { "name": clean(data.get("name")), "item_type": clean(data.get("item_type")), "brand": clean(data.get("brand")), @@ -240,6 +240,15 @@ def item_payload(data): "storage": clean(data.get("storage")), "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): diff --git a/static/app.js b/static/app.js index 149c04d..ebd30fa 100644 --- a/static/app.js +++ b/static/app.js @@ -33,8 +33,27 @@ function renderItems(items) { function escapeHtml(value) { return String(value).replace(/[&<>'"]/g, (char) => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[char])); } function ensureImageControls() { if (!$('#destination')) $('[name="location"]').insertAdjacentHTML('afterend', ''); if (!$('#checkoutDate')) $('#destination').parentElement.insertAdjacentHTML('afterend', ''); if ($('#imageInput')) return; const section = document.querySelector('.form-section'); section.insertAdjacentHTML('beforebegin', ''); } +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) => ``).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 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(); }); ['searchInput', 'typeFilter', 'statusFilter'].forEach((id) => $(`#${id}`).addEventListener(id === 'searchInput' ? 'input' : 'change', load)); 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'; }); $('#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); });