diff --git a/app.py b/app.py index 8da426d..1ae66dd 100644 --- a/app.py +++ b/app.py @@ -25,10 +25,10 @@ ROLE_ADMIN = "admin" VALID_ROLES = {ROLE_READ_ONLY, ROLE_READ_WRITE, ROLE_ADMIN} CONFIGURABLE_FIELDS = { "brand", "model", "serial_number", "asset_tag", "location", "destination", - "checkout_date", "purchase_date", "release_date", "notes", "song_list", "cpu", "ram", "gpu", "storage", "media_format", + "checkout_date", "purchase_date", "release_date", "notes", "song_list", "cpu", "ram", "gpu", "storage", "battery", "media_format", } DEFAULT_TYPES = { - "Computer": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "cpu", "ram", "gpu", "storage", "notes"], + "Computer": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "cpu", "ram", "gpu", "storage", "battery", "notes"], "Audio": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "media_format", "notes"], "Graphics": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "gpu", "storage", "notes"], "Long play disk": ["brand", "model", "asset_tag", "location", "purchase_date", "release_date", "media_format", "song_list", "notes"], @@ -72,6 +72,7 @@ def initialize_db(): ram TEXT, gpu TEXT, storage TEXT, + battery TEXT, media_format TEXT, image_filename TEXT, created_at TEXT NOT NULL, @@ -109,6 +110,7 @@ def initialize_db(): "checkout_date": "ALTER TABLE inventory_items ADD COLUMN checkout_date TEXT", "release_date": "ALTER TABLE inventory_items ADD COLUMN release_date TEXT", "song_list": "ALTER TABLE inventory_items ADD COLUMN song_list TEXT", + "battery": "ALTER TABLE inventory_items ADD COLUMN battery TEXT", } for column, statement in migrations.items(): if column not in columns: @@ -122,11 +124,15 @@ def initialize_db(): "INSERT OR IGNORE INTO inventory_types (name, fields) VALUES (?, ?)", (name, json.dumps(fields)), ) - for name in ("CD", "Long play disk"): + for name, additional_fields in { + "CD": ("release_date", "song_list"), + "Long play disk": ("release_date", "song_list"), + "Computer": ("battery",), + }.items(): row = connection.execute("SELECT fields FROM inventory_types WHERE name = ? COLLATE NOCASE", (name,)).fetchone() if row: fields = json.loads(row["fields"]) - updated_fields = fields + [field for field in ("release_date", "song_list") if field not in fields] + updated_fields = fields + [field for field in additional_fields if field not in fields] if updated_fields != fields: connection.execute("UPDATE inventory_types SET fields = ? WHERE name = ? COLLATE NOCASE", (json.dumps(updated_fields), name)) connection.execute( @@ -275,6 +281,7 @@ def item_payload(data): "ram": clean(data.get("ram")), "gpu": clean(data.get("gpu")), "storage": clean(data.get("storage")), + "battery": clean(data.get("battery")), "media_format": clean(data.get("media_format")), } return payload @@ -553,8 +560,8 @@ def create_item(): with get_db() as connection: cursor = connection.execute( """INSERT INTO inventory_items - (name, item_type, brand, model, serial_number, asset_tag, status, location, destination, checkout_date, purchase_date, release_date, notes, song_list, cpu, ram, gpu, storage, media_format, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (name, item_type, brand, model, serial_number, asset_tag, status, location, destination, checkout_date, purchase_date, release_date, notes, song_list, cpu, ram, gpu, storage, battery, media_format, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", (*data.values(), now, now), ) for filename, original_name in images: @@ -587,7 +594,7 @@ def update_item(item_id): return jsonify({"error": "Each inventory item can have up to 5 pictures."}), 400 saved_images = save_images(new_images) result = connection.execute( - """UPDATE inventory_items SET name=?, item_type=?, brand=?, model=?, serial_number=?, asset_tag=?, status=?, location=?, destination=?, checkout_date=?, purchase_date=?, release_date=?, notes=?, song_list=?, cpu=?, ram=?, gpu=?, storage=?, media_format=?, updated_at=? WHERE id=?""", + """UPDATE inventory_items SET name=?, item_type=?, brand=?, model=?, serial_number=?, asset_tag=?, status=?, location=?, destination=?, checkout_date=?, purchase_date=?, release_date=?, notes=?, song_list=?, cpu=?, ram=?, gpu=?, storage=?, battery=?, media_format=?, updated_at=? WHERE id=?""", (*data.values(), item_id), ) for filename, original_name in saved_images: diff --git a/data/inventory.db b/data/inventory.db index 4890627..664a2e5 100644 Binary files a/data/inventory.db and b/data/inventory.db differ diff --git a/data/uploads/3c1ff28d863b4391a08764b2d6cb25c8.jpg b/data/uploads/3c1ff28d863b4391a08764b2d6cb25c8.jpg new file mode 100644 index 0000000..148a563 Binary files /dev/null and b/data/uploads/3c1ff28d863b4391a08764b2d6cb25c8.jpg differ diff --git a/data/uploads/4f1d502d69fa4db8bba7852ac6107725.jpg b/data/uploads/4f1d502d69fa4db8bba7852ac6107725.jpg new file mode 100644 index 0000000..f448cc2 Binary files /dev/null and b/data/uploads/4f1d502d69fa4db8bba7852ac6107725.jpg differ diff --git a/data/uploads/67bd70d6ef9a48c19de9069774430aeb.jpg b/data/uploads/67bd70d6ef9a48c19de9069774430aeb.jpg new file mode 100644 index 0000000..91bef06 Binary files /dev/null and b/data/uploads/67bd70d6ef9a48c19de9069774430aeb.jpg differ diff --git a/data/uploads/b984d6fcdf5d49edbf974a1d9dbc83b3.jpg b/data/uploads/b984d6fcdf5d49edbf974a1d9dbc83b3.jpg new file mode 100644 index 0000000..00bd8ff Binary files /dev/null and b/data/uploads/b984d6fcdf5d49edbf974a1d9dbc83b3.jpg differ diff --git a/data/uploads/c36119d5aaa345448f65e85fd1c43cba.jpg b/data/uploads/c36119d5aaa345448f65e85fd1c43cba.jpg new file mode 100644 index 0000000..410e80c Binary files /dev/null and b/data/uploads/c36119d5aaa345448f65e85fd1c43cba.jpg differ diff --git a/data/uploads/c59bf9091779471a97c3e402b82c381e.jpg b/data/uploads/c59bf9091779471a97c3e402b82c381e.jpg new file mode 100644 index 0000000..419a1e6 Binary files /dev/null and b/data/uploads/c59bf9091779471a97c3e402b82c381e.jpg differ diff --git a/data/uploads/d2c084863cbb4d3e842e66942b9fe4c4.jpg b/data/uploads/d2c084863cbb4d3e842e66942b9fe4c4.jpg new file mode 100644 index 0000000..23eb4b5 Binary files /dev/null and b/data/uploads/d2c084863cbb4d3e842e66942b9fe4c4.jpg differ diff --git a/data/uploads/e9b604f070c4440abd1156b4a3f2e63a.jpg b/data/uploads/e9b604f070c4440abd1156b4a3f2e63a.jpg new file mode 100644 index 0000000..9e6c586 Binary files /dev/null and b/data/uploads/e9b604f070c4440abd1156b4a3f2e63a.jpg differ diff --git a/static/app.js b/static/app.js index c913b2e..748cbd4 100644 --- a/static/app.js +++ b/static/app.js @@ -3,7 +3,7 @@ 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', release_date: 'Release date', notes: 'Notes', song_list: 'Song list', cpu: 'CPU', ram: 'RAM', gpu: 'GPU', storage: 'Disk / storage', media_format: 'Media format' }; +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', release_date: 'Release date', notes: 'Notes', song_list: 'Song list', cpu: 'CPU', ram: 'RAM', gpu: 'GPU', storage: 'Disk / storage', battery: 'Battery', media_format: 'Media format' }; async function request(url, options = {}) { const headers = options.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }; @@ -49,7 +49,7 @@ function updateBulkActions() { } 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 (!$('[name="release_date"]')) $('[name="purchase_date"]').parentElement.insertAdjacentHTML('afterend', ''); if (!$('[name="song_list"]')) $('[name="media_format"]').parentElement.insertAdjacentHTML('afterend', ''); if ($('#imageInput')) return; const section = document.querySelector('.form-section'); section.insertAdjacentHTML('beforebegin', ''); } +function ensureImageControls() { if (!$('#destination')) $('[name="location"]').insertAdjacentHTML('afterend', ''); if (!$('#checkoutDate')) $('#destination').parentElement.insertAdjacentHTML('afterend', ''); if (!$('[name="release_date"]')) $('[name="purchase_date"]').parentElement.insertAdjacentHTML('afterend', ''); if (!$('[name="battery"]')) $('[name="storage"]').parentElement.insertAdjacentHTML('afterend', ''); if (!$('[name="song_list"]')) $('[name="media_format"]').parentElement.insertAdjacentHTML('afterend', ''); if ($('#imageInput')) return; const section = document.querySelector('.form-section'); section.insertAdjacentHTML('beforebegin', ''); } function updateTypeFields() { const type = $('[name="item_type"]').value; const visibleFields = new Set(state.types.find((entry) => entry.name === type)?.fields || []); @@ -77,15 +77,16 @@ function ensureTypeManager() { $('#resetTypeForm').addEventListener('click', resetTypeForm); $('#typeManagerModal').addEventListener('click', (event) => { if (event.target === $('#typeManagerModal')) $('#typeManagerModal').classList.remove('open'); }); $('#typeForm').addEventListener('submit', saveType); + $('#typeList').addEventListener('click', (event) => { const button = event.target.closest('[data-type-action]'); if (!button) return; const name = button.dataset.typeName; if (button.dataset.typeAction === 'edit') editType(name); if (button.dataset.typeAction === 'delete') removeType(name); }); } function resetTypeForm() { $('#typeForm').reset(); $('#editingTypeName').value = ''; } function renderTypeManager() { $('#typeFields').innerHTML = `Fields shown for this type
${Object.entries(fieldLabels).map(([key, label]) => ``).join('')}
`; - $('#typeList').innerHTML = state.types.map((type) => `
${escapeHtml(type.name)}${type.fields.map((field) => escapeHtml(fieldLabels[field])).join(', ') || 'Name, type, and status only'}
`).join(''); + $('#typeList').innerHTML = state.types.map((type) => `
${escapeHtml(type.name)}${type.fields.map((field) => escapeHtml(fieldLabels[field])).join(', ') || 'Name, type, and status only'}
`).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 openTypeManager() { ensureTypeManager(); await loadTypes(); resetTypeForm(); renderTypeManager(); $('#typeManagerModal').classList.add('open'); } +function 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); }); } +async function removeType(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));