diff --git a/app.py b/app.py
index 687739c..8da426d 100644
--- a/app.py
+++ b/app.py
@@ -25,14 +25,14 @@ 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", "notes", "cpu", "ram", "gpu", "storage", "media_format",
+ "checkout_date", "purchase_date", "release_date", "notes", "song_list", "cpu", "ram", "gpu", "storage", "media_format",
}
DEFAULT_TYPES = {
"Computer": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "cpu", "ram", "gpu", "storage", "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", "media_format", "notes"],
- "CD": ["brand", "model", "asset_tag", "location", "purchase_date", "media_format", "notes"],
+ "Long play disk": ["brand", "model", "asset_tag", "location", "purchase_date", "release_date", "media_format", "song_list", "notes"],
+ "CD": ["brand", "model", "asset_tag", "location", "purchase_date", "release_date", "media_format", "song_list", "notes"],
"Display": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "notes"],
"Peripheral": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "notes"],
"Other": ["brand", "model", "serial_number", "asset_tag", "location", "purchase_date", "notes"],
@@ -65,7 +65,9 @@ def initialize_db():
destination TEXT,
checkout_date TEXT,
purchase_date TEXT,
+ release_date TEXT,
notes TEXT,
+ song_list TEXT,
cpu TEXT,
ram TEXT,
gpu TEXT,
@@ -105,6 +107,8 @@ def initialize_db():
"image_filename": "ALTER TABLE inventory_items ADD COLUMN image_filename TEXT",
"destination": "ALTER TABLE inventory_items ADD COLUMN destination TEXT",
"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",
}
for column, statement in migrations.items():
if column not in columns:
@@ -118,6 +122,13 @@ def initialize_db():
"INSERT OR IGNORE INTO inventory_types (name, fields) VALUES (?, ?)",
(name, json.dumps(fields)),
)
+ for name in ("CD", "Long play disk"):
+ 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]
+ if updated_fields != fields:
+ connection.execute("UPDATE inventory_types SET fields = ? WHERE name = ? COLLATE NOCASE", (json.dumps(updated_fields), name))
connection.execute(
"""INSERT INTO inventory_images (item_id, filename, original_name, created_at)
SELECT id, image_filename, image_filename, COALESCE(updated_at, datetime('now'))
@@ -257,7 +268,9 @@ def item_payload(data):
"destination": clean(data.get("destination")),
"checkout_date": clean(data.get("checkout_date")) or None,
"purchase_date": clean(data.get("purchase_date")) or None,
+ "release_date": clean(data.get("release_date")) or None,
"notes": clean(data.get("notes")),
+ "song_list": clean(data.get("song_list")),
"cpu": clean(data.get("cpu")),
"ram": clean(data.get("ram")),
"gpu": clean(data.get("gpu")),
@@ -540,8 +553,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, notes, 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, media_format, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(*data.values(), now, now),
)
for filename, original_name in images:
@@ -574,7 +587,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=?, notes=?, 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=?, 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 8ba638f..4890627 100644
Binary files a/data/inventory.db and b/data/inventory.db differ
diff --git a/static/app.js b/static/app.js
index 584f02a..c913b2e 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', notes: 'Notes', 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', 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 ($('#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="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 || []);
diff --git a/static/style.css b/static/style.css
index 7b7ea75..18eed0c 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1,5 +1,5 @@
.bulk-actions{display:flex;align-items:center;gap:9px;margin-left:auto;color:var(--muted);font-size:12px;font-weight:700}.selection-column{width:34px;text-align:center}.selection-column input{accent-color:var(--teal)}
-.type-manager{max-width:720px}.type-fields>span{display:block;color:var(--muted);font-size:12px;font-weight:700;margin-bottom:9px}.field-checklist{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px 14px}.field-checklist label{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--ink)}.field-checklist input{width:auto}.type-list{border-top:1px solid var(--line);margin-top:22px;padding-top:4px}.type-row{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--line);padding:13px 0}.type-row strong,.type-row span{display:block}.type-row span{color:var(--muted);font-size:11px;margin-top:4px;max-width:460px}.type-row .row-actions{display:flex;gap:7px;flex:none}@media(max-width:640px){.field-checklist{grid-template-columns:repeat(2,minmax(0,1fr))}.type-row{align-items:flex-start}.type-row .row-actions{flex-direction:column}.type-row span{max-width:230px}.bulk-actions{width:100%;margin-left:0;justify-content:space-between}}
+.type-manager{max-width:720px}.type-fields>span{display:block;color:var(--muted);font-size:12px;font-weight:700;margin-bottom:9px}.field-checklist{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px 14px}.field-checklist label{display:grid;grid-template-columns:16px minmax(0,1fr);align-items:center;gap:7px;min-height:28px;font-size:12px;color:var(--ink);white-space:normal}.field-checklist input{width:16px;height:16px;margin:0}.type-list{border-top:1px solid var(--line);margin-top:22px;padding-top:4px}.type-row{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--line);padding:13px 0}.type-row strong,.type-row span{display:block}.type-row span{color:var(--muted);font-size:11px;margin-top:4px;max-width:460px}.type-row .row-actions{display:flex;gap:7px;flex:none}@media(max-width:640px){.field-checklist{grid-template-columns:repeat(2,minmax(0,1fr))}.type-row{align-items:flex-start}.type-row .row-actions{flex-direction:column}.type-row span{max-width:230px}.bulk-actions{width:100%;margin-left:0;justify-content:space-between}}
.item-cell{display:flex;align-items:center;gap:11px}.item-thumb{width:38px;height:38px;object-fit:cover;border-radius:6px;background:#e8efeb;flex:none}.item-thumb.placeholder{display:grid;place-items:center;color:#8aa39b;font-size:20px}.image-count{background:#e7f3f0;color:var(--teal);font-size:10px;font-weight:700;padding:4px 5px;border-radius:4px;margin-left:-7px}.image-field input{padding:7px 0!important;border:0!important}.image-field span{display:block;color:#8c9994;font-size:10px;font-weight:400;margin-top:4px}.image-gallery{display:flex;flex-wrap:wrap;gap:9px;margin-top:11px}.gallery-item{position:relative;width:82px;height:64px}.gallery-item img{width:100%;height:100%;object-fit:cover;border-radius:6px;border:1px solid var(--line)}.gallery-delete{position:absolute;top:-7px;right:-7px;width:20px;height:20px;padding:0;border:0;border-radius:50%;background:#193f3b;color:#fff;cursor:pointer;line-height:18px}.hidden{display:none!important}
.row-actions{opacity:1!important}
.login-page{min-height:100vh;display:grid;place-items:center;background:linear-gradient(135deg,#f6f8f5,#dcece5)}.login-card{width:min(390px,calc(100% - 32px));padding:34px;background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.login-card .brand-mark{margin-bottom:22px}.login-card h1{font-family:'Space Grotesk';margin:0 0 24px}.login-card form{display:grid;gap:16px}.login-card label,.user-form label{display:grid;gap:6px;color:var(--muted);font-size:12px;font-weight:700}.login-card input,.user-form input,.user-form select{width:100%;padding:11px;border:1px solid var(--line);border-radius:6px;background:#fff}.login-card .primary-button{margin-top:8px}.login-error{padding:10px;background:#fff0ed;color:#a33e2b;border-radius:6px;font-size:12px}.logout-button{margin-top:12px;border:0;background:transparent;color:#90afa9;padding:0;cursor:pointer;font-size:11px}.users-panel{margin-top:24px;padding:24px;background:#fff;border:1px solid var(--line);border-radius:8px}.user-form{display:flex;align-items:end;gap:12px;flex-wrap:wrap}.user-form label{min-width:150px}.user-form .checkbox-label{display:flex;align-items:center;gap:7px;min-width:auto}.user-form .checkbox-label input{width:auto}.user-row{display:flex;justify-content:space-between;border-top:1px solid var(--line);padding:10px 0;margin:18px 0 0;font-size:12px}.user-row span{color:var(--muted)}