Add grid cell inventory linking and component quick-create
- New 'grid' location type on inventory entries (grid_id, grid_row, grid_col) - Clicking a grid cell shows a popup with what's stored there - Popup has '+ Add entry' pre-filled with the cell coordinates - Inventory dialog: 'New...' button next to component selector opens component creation dialog on top, returns with new component selected - Grid entries display as e.g. 'Black Component Box R3C5' in lists - Store original filename on source image upload Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
131
public/app.mjs
131
public/app.mjs
@@ -51,14 +51,21 @@ async function load_all() {
|
|||||||
// Helpers
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LOCATION_TYPE_LABEL = { physical: 'Physical', bom: 'BOM', digital: 'Digital' };
|
const LOCATION_TYPE_LABEL = { physical: 'Physical', bom: 'BOM', digital: 'Digital', grid: 'Grid' };
|
||||||
|
|
||||||
function ref_label_for_type(type) {
|
function ref_label_for_type(type) {
|
||||||
if (type === 'physical') return 'Location (drawer, bin, shelf…)';
|
if (type === 'physical') return 'Location (drawer, bin, shelf…)';
|
||||||
if (type === 'bom') return 'Document / project name';
|
if (type === 'bom') return 'Document / project name';
|
||||||
|
if (type === 'grid') return 'Grid cell';
|
||||||
return 'Note / description';
|
return 'Note / description';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function grid_cell_label(entry) {
|
||||||
|
const grid = all_grids.find(g => g.id === entry.grid_id);
|
||||||
|
if (!grid) { return `Grid cell (R${(entry.grid_row ?? 0) + 1}C${(entry.grid_col ?? 0) + 1})`; }
|
||||||
|
return `${grid.name} R${(entry.grid_row ?? 0) + 1}C${(entry.grid_col ?? 0) + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
function inventory_for_component(component_id) {
|
function inventory_for_component(component_id) {
|
||||||
return all_inventory.filter(e => e.component_id === component_id);
|
return all_inventory.filter(e => e.component_id === component_id);
|
||||||
}
|
}
|
||||||
@@ -297,7 +304,7 @@ function build_detail_inv_entry(entry) {
|
|||||||
type_el.className = `type-pill type-${entry.location_type}`;
|
type_el.className = `type-pill type-${entry.location_type}`;
|
||||||
type_el.textContent = LOCATION_TYPE_LABEL[entry.location_type] ?? entry.location_type;
|
type_el.textContent = LOCATION_TYPE_LABEL[entry.location_type] ?? entry.location_type;
|
||||||
|
|
||||||
set_text(el, '.detail-inv-ref', entry.location_ref || '—');
|
set_text(el, '.detail-inv-ref', entry.location_type === 'grid' ? grid_cell_label(entry) : (entry.location_ref || '—'));
|
||||||
set_text(el, '.detail-inv-qty', entry.quantity ? `×${entry.quantity}` : '');
|
set_text(el, '.detail-inv-qty', entry.quantity ? `×${entry.quantity}` : '');
|
||||||
set_text(el, '.detail-inv-notes', entry.notes || '');
|
set_text(el, '.detail-inv-notes', entry.notes || '');
|
||||||
|
|
||||||
@@ -389,13 +396,13 @@ function build_inventory_row(entry) {
|
|||||||
pill.textContent = LOCATION_TYPE_LABEL[entry.location_type] ?? entry.location_type;
|
pill.textContent = LOCATION_TYPE_LABEL[entry.location_type] ?? entry.location_type;
|
||||||
qs(row, '.inv-type-badge').replaceChildren(pill);
|
qs(row, '.inv-type-badge').replaceChildren(pill);
|
||||||
|
|
||||||
set_text(row, '.inv-location-ref', entry.location_ref);
|
set_text(row, '.inv-location-ref', entry.location_type === 'grid' ? grid_cell_label(entry) : entry.location_ref);
|
||||||
set_text(row, '.inv-quantity', entry.quantity);
|
set_text(row, '.inv-quantity', entry.quantity);
|
||||||
set_text(row, '.inv-notes', entry.notes);
|
set_text(row, '.inv-notes', entry.notes);
|
||||||
|
|
||||||
qs(row, '.btn-edit').addEventListener('click', () => open_inventory_dialog(entry));
|
qs(row, '.btn-edit').addEventListener('click', () => open_inventory_dialog(entry));
|
||||||
qs(row, '.btn-delete').addEventListener('click', () => confirm_delete(
|
qs(row, '.btn-delete').addEventListener('click', () => confirm_delete(
|
||||||
`Delete this inventory entry (${LOCATION_TYPE_LABEL[entry.location_type]}: ${entry.location_ref || '—'})?`,
|
`Delete this inventory entry (${LOCATION_TYPE_LABEL[entry.location_type]}: ${entry.location_type === 'grid' ? grid_cell_label(entry) : (entry.location_ref || '—')})?`,
|
||||||
async () => {
|
async () => {
|
||||||
await api.delete_inventory(entry.id);
|
await api.delete_inventory(entry.id);
|
||||||
all_inventory = all_inventory.filter(e => e.id !== entry.id);
|
all_inventory = all_inventory.filter(e => e.id !== entry.id);
|
||||||
@@ -991,16 +998,75 @@ function render_grid_viewer() {
|
|||||||
if (filename) {
|
if (filename) {
|
||||||
const img = qs(cell, '.grid-cell-img');
|
const img = qs(cell, '.grid-cell-img');
|
||||||
img.src = `/img/${filename}`;
|
img.src = `/img/${filename}`;
|
||||||
img.addEventListener('click', () => window.open(`/img/${filename}`, '_blank'));
|
|
||||||
} else {
|
} else {
|
||||||
cell.classList.add('empty');
|
cell.classList.add('empty');
|
||||||
}
|
}
|
||||||
set_text(cell, '.grid-cell-label', `${row},${col}`);
|
set_text(cell, '.grid-cell-label', `R${row + 1}C${col + 1}`);
|
||||||
|
cell.addEventListener('click', (e) => open_cell_inventory(grid, row, col, e));
|
||||||
return cell;
|
return cell;
|
||||||
});
|
});
|
||||||
cells_el.replaceChildren(...all_cells);
|
cells_el.replaceChildren(...all_cells);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function open_cell_inventory(grid, row, col, e) {
|
||||||
|
// Remove any existing overlay
|
||||||
|
document.getElementById('cell-inventory-overlay')?.remove();
|
||||||
|
|
||||||
|
const frag = document.getElementById('t-cell-inventory').content.cloneNode(true);
|
||||||
|
document.body.appendChild(frag);
|
||||||
|
const overlay = document.getElementById('cell-inventory-overlay');
|
||||||
|
|
||||||
|
// Position near click
|
||||||
|
const x = Math.min(e.clientX + 8, window.innerWidth - 340);
|
||||||
|
const y = Math.min(e.clientY + 8, window.innerHeight - 200);
|
||||||
|
overlay.style.left = x + 'px';
|
||||||
|
overlay.style.top = y + 'px';
|
||||||
|
|
||||||
|
qs(overlay, '.cell-inventory-title').textContent = `${grid.name} R${row + 1}C${col + 1}`;
|
||||||
|
|
||||||
|
const entries = all_inventory.filter(inv =>
|
||||||
|
inv.location_type === 'grid' && inv.grid_id === grid.id &&
|
||||||
|
inv.grid_row === row && inv.grid_col === col
|
||||||
|
);
|
||||||
|
|
||||||
|
const list_el = qs(overlay, '.cell-inventory-list');
|
||||||
|
if (entries.length === 0) {
|
||||||
|
const empty = document.createElement('div');
|
||||||
|
empty.className = 'cell-inv-empty';
|
||||||
|
empty.textContent = 'Nothing stored here yet';
|
||||||
|
list_el.appendChild(empty);
|
||||||
|
} else {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
const comp = component_by_id(entry.component_id);
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'cell-inv-item';
|
||||||
|
const name_span = document.createElement('span');
|
||||||
|
name_span.textContent = comp?.name ?? '?';
|
||||||
|
const qty_span = document.createElement('span');
|
||||||
|
qty_span.className = 'cell-inv-qty';
|
||||||
|
qty_span.textContent = entry.quantity || '';
|
||||||
|
item.append(name_span, qty_span);
|
||||||
|
list_el.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
qs(overlay, '#cell-inv-close').addEventListener('click', () => overlay.remove());
|
||||||
|
qs(overlay, '#cell-inv-add').addEventListener('click', () => {
|
||||||
|
overlay.remove();
|
||||||
|
open_inventory_dialog(null, null, { grid_id: grid.id, grid_row: row, grid_col: col });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on outside click
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('click', function handler(ev) {
|
||||||
|
if (!overlay.contains(ev.target)) {
|
||||||
|
overlay.remove();
|
||||||
|
document.removeEventListener('click', handler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Dialog: Component
|
// Dialog: Component
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1121,7 +1187,7 @@ function open_component_dialog(comp = null) {
|
|||||||
|
|
||||||
let inventory_dialog_callback = null;
|
let inventory_dialog_callback = null;
|
||||||
|
|
||||||
function open_inventory_dialog(entry = null, default_component_id = null) {
|
function open_inventory_dialog(entry = null, default_component_id = null, default_grid_cell = null) {
|
||||||
const dlg = document.getElementById('dialog-inventory');
|
const dlg = document.getElementById('dialog-inventory');
|
||||||
const title = qs(dlg, '.dialog-title');
|
const title = qs(dlg, '.dialog-title');
|
||||||
const comp_sel = qs(dlg, '#i-component');
|
const comp_sel = qs(dlg, '#i-component');
|
||||||
@@ -1130,6 +1196,11 @@ function open_inventory_dialog(entry = null, default_component_id = null) {
|
|||||||
const ref_label = qs(dlg, '#i-ref-label');
|
const ref_label = qs(dlg, '#i-ref-label');
|
||||||
const qty_input = qs(dlg, '#i-qty');
|
const qty_input = qs(dlg, '#i-qty');
|
||||||
const notes_input = qs(dlg, '#i-notes');
|
const notes_input = qs(dlg, '#i-notes');
|
||||||
|
const new_comp_btn = qs(dlg, '#i-new-component');
|
||||||
|
const grid_row_div = qs(dlg, '#i-grid-row');
|
||||||
|
const grid_sel = qs(dlg, '#i-grid-select');
|
||||||
|
const row_num_input = qs(dlg, '#i-grid-row-num');
|
||||||
|
const col_num_input = qs(dlg, '#i-grid-col-num');
|
||||||
|
|
||||||
title.textContent = entry ? 'Edit inventory entry' : 'Add inventory entry';
|
title.textContent = entry ? 'Edit inventory entry' : 'Add inventory entry';
|
||||||
|
|
||||||
@@ -1141,13 +1212,35 @@ function open_inventory_dialog(entry = null, default_component_id = null) {
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
comp_sel.value = entry?.component_id ?? default_component_id ?? '';
|
comp_sel.value = entry?.component_id ?? default_component_id ?? '';
|
||||||
type_sel.value = entry?.location_type ?? 'physical';
|
|
||||||
|
const effective_grid_cell = default_grid_cell ?? (entry?.location_type === 'grid' ? { grid_id: entry.grid_id, grid_row: entry.grid_row, grid_col: entry.grid_col } : null);
|
||||||
|
if (effective_grid_cell) {
|
||||||
|
type_sel.value = 'grid';
|
||||||
|
} else {
|
||||||
|
type_sel.value = entry?.location_type ?? 'physical';
|
||||||
|
}
|
||||||
|
|
||||||
ref_input.value = entry?.location_ref ?? '';
|
ref_input.value = entry?.location_ref ?? '';
|
||||||
qty_input.value = entry?.quantity ?? '';
|
qty_input.value = entry?.quantity ?? '';
|
||||||
notes_input.value = entry?.notes ?? '';
|
notes_input.value = entry?.notes ?? '';
|
||||||
|
|
||||||
|
// Populate grid selector
|
||||||
|
grid_sel.replaceChildren(
|
||||||
|
Object.assign(document.createElement('option'), { value: '', textContent: '— select grid —' }),
|
||||||
|
...all_grids.map(g => Object.assign(document.createElement('option'), {
|
||||||
|
value: g.id,
|
||||||
|
textContent: g.name,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
if (effective_grid_cell?.grid_id) { grid_sel.value = effective_grid_cell.grid_id; }
|
||||||
|
row_num_input.value = effective_grid_cell?.grid_row != null ? effective_grid_cell.grid_row + 1 : 1;
|
||||||
|
col_num_input.value = effective_grid_cell?.grid_col != null ? effective_grid_cell.grid_col + 1 : 1;
|
||||||
|
|
||||||
function update_ref_label() {
|
function update_ref_label() {
|
||||||
ref_label.textContent = ref_label_for_type(type_sel.value);
|
ref_label.textContent = ref_label_for_type(type_sel.value);
|
||||||
|
const is_grid = type_sel.value === 'grid';
|
||||||
|
grid_row_div.hidden = !is_grid;
|
||||||
|
ref_input.closest('.form-row').hidden = is_grid;
|
||||||
}
|
}
|
||||||
update_ref_label();
|
update_ref_label();
|
||||||
|
|
||||||
@@ -1156,13 +1249,33 @@ function open_inventory_dialog(entry = null, default_component_id = null) {
|
|||||||
type_sel._change_handler = update_ref_label;
|
type_sel._change_handler = update_ref_label;
|
||||||
type_sel.addEventListener('change', type_sel._change_handler);
|
type_sel.addEventListener('change', type_sel._change_handler);
|
||||||
|
|
||||||
|
new_comp_btn.onclick = () => {
|
||||||
|
const known_ids = new Set(all_components.map(c => c.id));
|
||||||
|
open_component_dialog(null);
|
||||||
|
document.getElementById('dialog-component').addEventListener('close', () => {
|
||||||
|
// Rebuild the selector and select the newly created component (if any)
|
||||||
|
comp_sel.replaceChildren(
|
||||||
|
Object.assign(document.createElement('option'), { value: '', textContent: '— select component —' }),
|
||||||
|
...all_components.map(c => Object.assign(document.createElement('option'), {
|
||||||
|
value: c.id, textContent: c.name,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const new_comp = all_components.find(c => !known_ids.has(c.id));
|
||||||
|
if (new_comp) comp_sel.value = new_comp.id;
|
||||||
|
}, { once: true });
|
||||||
|
};
|
||||||
|
|
||||||
inventory_dialog_callback = async () => {
|
inventory_dialog_callback = async () => {
|
||||||
|
const is_grid = type_sel.value === 'grid';
|
||||||
const body = {
|
const body = {
|
||||||
component_id: comp_sel.value,
|
component_id: comp_sel.value,
|
||||||
location_type: type_sel.value,
|
location_type: type_sel.value,
|
||||||
location_ref: ref_input.value.trim(),
|
location_ref: is_grid ? '' : ref_input.value.trim(),
|
||||||
quantity: qty_input.value.trim(),
|
quantity: qty_input.value.trim(),
|
||||||
notes: notes_input.value.trim(),
|
notes: notes_input.value.trim(),
|
||||||
|
grid_id: is_grid ? (grid_sel.value || null) : null,
|
||||||
|
grid_row: is_grid ? parseInt(row_num_input.value) - 1 : null,
|
||||||
|
grid_col: is_grid ? parseInt(col_num_input.value) - 1 : null,
|
||||||
};
|
};
|
||||||
if (entry) {
|
if (entry) {
|
||||||
const result = await api.update_inventory(entry.id, body);
|
const result = await api.update_inventory(entry.id, body);
|
||||||
|
|||||||
103
public/style.css
103
public/style.css
@@ -1019,6 +1019,17 @@ nav {
|
|||||||
|
|
||||||
/* ===== FORM ROW PAIR ===== */
|
/* ===== FORM ROW PAIR ===== */
|
||||||
|
|
||||||
|
.input-with-action {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-action .filter-select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.form-row-pair {
|
.form-row-pair {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
@@ -1311,3 +1322,95 @@ nav {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== GRID TYPE PILL ===== */
|
||||||
|
|
||||||
|
.type-pill.type-grid {
|
||||||
|
background: #2e7d4f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== GRID CELL PICKER (inventory dialog) ===== */
|
||||||
|
|
||||||
|
.i-grid-cell-picker {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.i-grid-coords {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-narrow {
|
||||||
|
width: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== CELL INVENTORY OVERLAY ===== */
|
||||||
|
|
||||||
|
.grid-cell {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-inventory-overlay {
|
||||||
|
position: fixed;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
min-width: 220px;
|
||||||
|
max-width: 320px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-inventory-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-inventory-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-inventory-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
min-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-inv-item {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-inv-qty {
|
||||||
|
color: var(--text-dim);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-inv-empty {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-inventory-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.25rem 0.6rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -115,6 +115,7 @@
|
|||||||
<option value="physical">Physical</option>
|
<option value="physical">Physical</option>
|
||||||
<option value="bom">BOM / Drawing</option>
|
<option value="bom">BOM / Drawing</option>
|
||||||
<option value="digital">Digital / Note</option>
|
<option value="digital">Digital / Note</option>
|
||||||
|
<option value="grid">Grid cell</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn btn-primary" id="btn-add-inventory">+ Add entry</button>
|
<button class="btn btn-primary" id="btn-add-inventory">+ Add entry</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -354,9 +355,12 @@
|
|||||||
<form method="dialog" id="form-inventory">
|
<form method="dialog" id="form-inventory">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="i-component">Component</label>
|
<label for="i-component">Component</label>
|
||||||
<select id="i-component" required class="filter-select wide">
|
<div class="input-with-action">
|
||||||
<option value="">— select component —</option>
|
<select id="i-component" required class="filter-select">
|
||||||
</select>
|
<option value="">— select component —</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm" id="i-new-component">New…</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="i-type">Location type</label>
|
<label for="i-type">Location type</label>
|
||||||
@@ -364,12 +368,25 @@
|
|||||||
<option value="physical">Physical location</option>
|
<option value="physical">Physical location</option>
|
||||||
<option value="bom">BOM / Drawing</option>
|
<option value="bom">BOM / Drawing</option>
|
||||||
<option value="digital">Digital / Note</option>
|
<option value="digital">Digital / Note</option>
|
||||||
|
<option value="grid">Grid cell</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="i-ref" id="i-ref-label">Location reference</label>
|
<label for="i-ref" id="i-ref-label">Location reference</label>
|
||||||
<input type="text" id="i-ref" autocomplete="off">
|
<input type="text" id="i-ref" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-row" id="i-grid-row" hidden>
|
||||||
|
<label>Grid cell</label>
|
||||||
|
<div class="i-grid-cell-picker">
|
||||||
|
<select id="i-grid-select" class="filter-select"></select>
|
||||||
|
<div class="i-grid-coords">
|
||||||
|
<label for="i-grid-row-num">Row</label>
|
||||||
|
<input type="number" id="i-grid-row-num" min="1" class="input-narrow">
|
||||||
|
<label for="i-grid-col-num">Col</label>
|
||||||
|
<input type="number" id="i-grid-col-num" min="1" class="input-narrow">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="i-qty">Quantity</label>
|
<label for="i-qty">Quantity</label>
|
||||||
<input type="text" id="i-qty" autocomplete="off" placeholder="e.g. 10, ~50, see BOM">
|
<input type="text" id="i-qty" autocomplete="off" placeholder="e.g. 10, ~50, see BOM">
|
||||||
@@ -479,3 +496,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- ===== CELL INVENTORY OVERLAY ===== -->
|
||||||
|
<template id="t-cell-inventory">
|
||||||
|
<div class="cell-inventory-overlay" id="cell-inventory-overlay">
|
||||||
|
<div class="cell-inventory-header">
|
||||||
|
<span class="cell-inventory-title"></span>
|
||||||
|
<button type="button" class="btn-icon" id="cell-inv-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="cell-inventory-list" id="cell-inventory-list"></div>
|
||||||
|
<div class="cell-inventory-actions">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" id="cell-inv-add">+ Add entry</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -156,7 +156,8 @@ app.get('/api/inventory', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/inventory', (req, res) => {
|
app.post('/api/inventory', (req, res) => {
|
||||||
const { component_id, location_type, location_ref = '', quantity = '', notes = '' } = req.body;
|
const { component_id, location_type, location_ref = '', quantity = '', notes = '',
|
||||||
|
grid_id = null, grid_row = null, grid_col = null } = req.body;
|
||||||
if (!component_id) return fail(res, 'component_id is required');
|
if (!component_id) return fail(res, 'component_id is required');
|
||||||
if (!location_type) return fail(res, 'location_type is required');
|
if (!location_type) return fail(res, 'location_type is required');
|
||||||
if (!get_component(component_id)) return fail(res, 'component not found', 404);
|
if (!get_component(component_id)) return fail(res, 'component not found', 404);
|
||||||
@@ -168,6 +169,9 @@ app.post('/api/inventory', (req, res) => {
|
|||||||
location_ref: String(location_ref).trim(),
|
location_ref: String(location_ref).trim(),
|
||||||
quantity: String(quantity).trim(),
|
quantity: String(quantity).trim(),
|
||||||
notes: String(notes).trim(),
|
notes: String(notes).trim(),
|
||||||
|
grid_id: grid_id ?? null,
|
||||||
|
grid_row: grid_row != null ? parseInt(grid_row) : null,
|
||||||
|
grid_col: grid_col != null ? parseInt(grid_col) : null,
|
||||||
images: [],
|
images: [],
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
@@ -185,6 +189,9 @@ app.put('/api/inventory/:id', (req, res) => {
|
|||||||
if (location_ref !== undefined) updated.location_ref = String(location_ref).trim();
|
if (location_ref !== undefined) updated.location_ref = String(location_ref).trim();
|
||||||
if (quantity !== undefined) updated.quantity = String(quantity).trim();
|
if (quantity !== undefined) updated.quantity = String(quantity).trim();
|
||||||
if (notes !== undefined) updated.notes = String(notes).trim();
|
if (notes !== undefined) updated.notes = String(notes).trim();
|
||||||
|
if (req.body.grid_id !== undefined) updated.grid_id = req.body.grid_id ?? null;
|
||||||
|
if (req.body.grid_row !== undefined) updated.grid_row = req.body.grid_row != null ? parseInt(req.body.grid_row) : null;
|
||||||
|
if (req.body.grid_col !== undefined) updated.grid_col = req.body.grid_col != null ? parseInt(req.body.grid_col) : null;
|
||||||
set_inventory_entry(updated);
|
set_inventory_entry(updated);
|
||||||
ok(res, { entry: updated });
|
ok(res, { entry: updated });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user