diff --git a/data/inventory.db b/data/inventory.db index 664a2e5..f75f198 100644 Binary files a/data/inventory.db and b/data/inventory.db differ diff --git a/static/app.js b/static/app.js index 748cbd4..a7cbef9 100644 --- a/static/app.js +++ b/static/app.js @@ -72,10 +72,13 @@ function toast(message) { const element = $('#toast'); element.textContent = mes function ensureTypeManager() { if ($('#typeManagerModal')) return; - document.body.insertAdjacentHTML('beforeend', ''); + document.body.insertAdjacentHTML('beforeend', ''); $('#closeTypeManager').addEventListener('click', () => $('#typeManagerModal').classList.remove('open')); - $('#resetTypeForm').addEventListener('click', resetTypeForm); $('#typeManagerModal').addEventListener('click', (event) => { if (event.target === $('#typeManagerModal')) $('#typeManagerModal').classList.remove('open'); }); + $('#newTypeButton').addEventListener('click', () => openTypeEditor()); + $('#closeTypeEditor').addEventListener('click', closeTypeEditor); + $('#cancelTypeEditor').addEventListener('click', closeTypeEditor); + $('#typeEditorModal').addEventListener('click', (event) => { if (event.target === $('#typeEditorModal')) closeTypeEditor(); }); $('#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); }); } @@ -85,9 +88,11 @@ function renderTypeManager() { $('#typeList').innerHTML = state.types.map((type) => `
${escapeHtml(type.name)}${type.fields.map((field) => escapeHtml(fieldLabels[field])).join(', ') || 'Name, type, and status only'}
`).join(''); } 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); }); } +function closeTypeEditor() { $('#typeEditorModal').classList.remove('open'); } +function openTypeEditor(type = null) { resetTypeForm(); $('#typeEditorTitle').textContent = type ? 'Edit type' : 'New type'; $('#typeFields').innerHTML = `Fields shown for this type
${Object.entries(fieldLabels).map(([key, label]) => ``).join('')}
`; if (type) { $('#editingTypeName').value = type.name; $('#typeName').value = type.name; document.querySelectorAll('[name="type_field"]').forEach((field) => { field.checked = type.fields.includes(field.value); }); } $('#typeEditorModal').classList.add('open'); $('#typeName').focus(); } +function editType(name) { const type = state.types.find((entry) => entry.name === name); if (type) openTypeEditor(type); } 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); } } +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(); closeTypeEditor(); toast('Inventory type saved.'); } catch (error) { toast(error.message); } } window.editItem = (id) => openModal(state.items.find((item) => item.id === id)); window.deleteImage = async (imageId) => { if (!state.editingId || !confirm('Delete this picture?')) return; try { const item = await request(`/api/items/${state.editingId}/images/${imageId}`, { method: 'DELETE' }); renderImageGallery(item.images); $('#imageName').textContent = `${item.images.length} saved picture${item.images.length === 1 ? '' : 's'}. Add more below.`; state.items = state.items.map((entry) => entry.id === item.id ? item : entry); renderItems(state.items); toast('Picture deleted.'); } catch (error) { toast(error.message); } }; diff --git a/static/style.css b/static/style.css index 18eed0c..2a40be5 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: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}} +.type-manager{max-width:720px}.type-manager-actions{padding:20px 28px 0}.type-editor{max-width:640px}.type-editor-backdrop{z-index:6}.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:4px 28px 0}.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)}