Add bin types: reusable named dimension presets for bins
Bin types store a name, physical W×H in mm, and optional description. When editing a bin, a type can be selected from a dropdown; this pre-fills and locks the dimension inputs. Custom dimensions remain available when no type is selected. - lib/storage.mjs: bin type CRUD with bt: prefix - server.mjs: /api/bin-types CRUD routes; type_id accepted on bin create/update routes; DELETE protected if any bin references the type; type dims copied onto bin when type_id is set - public/lib/api.mjs: bin type wrappers; rename_bin → update_bin (accepts any fields) - public/templates.html: Types tab in bins section; t-bin-type-row; t-dialog-bin-type; type selector in bin editor dialog - public/app.mjs: all_bin_types state loaded at startup; render_bin_types_list(); open_bin_type_dialog(); type selector in open_bin_editor(); /bins/types routing - public/style.css: bin types list styles Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
106
server.mjs
106
server.mjs
@@ -19,6 +19,7 @@ import {
|
||||
list_component_templates, get_component_template, set_component_template, delete_component_template,
|
||||
list_pdfs, get_pdf, set_pdf, delete_pdf,
|
||||
list_bins, get_bin, set_bin, delete_bin,
|
||||
list_bin_types, get_bin_type, set_bin_type, delete_bin_type,
|
||||
} from './lib/storage.mjs';
|
||||
|
||||
mkdirSync('./data/images', { recursive: true });
|
||||
@@ -648,6 +649,59 @@ app.post('/api/maintenance/pdf-thumbs', (req, res) => {
|
||||
ok(res, { generated, total: pdfs.length });
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bin types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
app.get('/api/bin-types', (req, res) => {
|
||||
ok(res, { bin_types: list_bin_types() });
|
||||
});
|
||||
|
||||
app.post('/api/bin-types', (req, res) => {
|
||||
const { name, phys_w, phys_h, description = '' } = req.body;
|
||||
if (!name?.trim()) return fail(res, 'name is required');
|
||||
if (!(phys_w > 0)) return fail(res, 'phys_w must be a positive number');
|
||||
if (!(phys_h > 0)) return fail(res, 'phys_h must be a positive number');
|
||||
const bt = {
|
||||
id: generate_id(),
|
||||
name: name.trim(),
|
||||
phys_w: Number(phys_w),
|
||||
phys_h: Number(phys_h),
|
||||
description: description.trim(),
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
};
|
||||
set_bin_type(bt);
|
||||
ok(res, { bin_type: bt });
|
||||
});
|
||||
|
||||
app.put('/api/bin-types/:id', (req, res) => {
|
||||
const existing = get_bin_type(req.params.id);
|
||||
if (!existing) return fail(res, 'not found', 404);
|
||||
const { name, phys_w, phys_h, description } = req.body;
|
||||
const updated = { ...existing, updated_at: Date.now() };
|
||||
if (name !== undefined) updated.name = name.trim();
|
||||
if (phys_w !== undefined) {
|
||||
if (!(Number(phys_w) > 0)) return fail(res, 'phys_w must be a positive number');
|
||||
updated.phys_w = Number(phys_w);
|
||||
}
|
||||
if (phys_h !== undefined) {
|
||||
if (!(Number(phys_h) > 0)) return fail(res, 'phys_h must be a positive number');
|
||||
updated.phys_h = Number(phys_h);
|
||||
}
|
||||
if (description !== undefined) updated.description = description.trim();
|
||||
set_bin_type(updated);
|
||||
ok(res, { bin_type: updated });
|
||||
});
|
||||
|
||||
app.delete('/api/bin-types/:id', (req, res) => {
|
||||
if (!get_bin_type(req.params.id)) return fail(res, 'not found', 404);
|
||||
const in_use = list_bins().find(b => b.type_id === req.params.id);
|
||||
if (in_use) return fail(res, `In use by bin "${in_use.name}"`, 409);
|
||||
delete_bin_type(req.params.id);
|
||||
ok(res);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bins
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -664,30 +718,34 @@ app.get('/api/bins/:id', (req, res) => {
|
||||
|
||||
// Create a bin from an already-uploaded source image
|
||||
app.post('/api/bins/from-source', (req, res) => {
|
||||
const { source_id, name = '' } = req.body;
|
||||
const { source_id, name = '', type_id = null } = req.body;
|
||||
if (!source_id) return fail(res, 'source_id is required');
|
||||
const src = get_source_image(source_id);
|
||||
if (!src) return fail(res, 'source image not found', 404);
|
||||
if (type_id && !get_bin_type(type_id)) return fail(res, 'bin type not found', 404);
|
||||
|
||||
// Add 'bin' to uses if not already there
|
||||
if (!src.uses?.includes('bin')) {
|
||||
add_source_image({ ...src, uses: [...(src.uses ?? []), 'bin'] });
|
||||
}
|
||||
|
||||
const bt = type_id ? get_bin_type(type_id) : null;
|
||||
const mx = Math.round(src.width * 0.15);
|
||||
const my = Math.round(src.height * 0.15);
|
||||
const bin = {
|
||||
id: generate_id(),
|
||||
name: name.trim() || src.original_name?.replace(/\.[^.]+$/, '') || 'Bin',
|
||||
type_id,
|
||||
source_id,
|
||||
source_w: src.width,
|
||||
source_h: src.height,
|
||||
corners: [
|
||||
{ x: mx, y: my },
|
||||
{ x: src.width - mx, y: my },
|
||||
{ x: mx, y: my },
|
||||
{ x: src.width - mx, y: my },
|
||||
{ x: src.width - mx, y: src.height - my },
|
||||
{ x: mx, y: src.height - my },
|
||||
{ x: mx, y: src.height - my },
|
||||
],
|
||||
phys_w: bt?.phys_w ?? null,
|
||||
phys_h: bt?.phys_h ?? null,
|
||||
image_filename: null,
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
@@ -699,36 +757,38 @@ app.post('/api/bins/from-source', (req, res) => {
|
||||
// Upload a source image for a bin and create the bin record (no processing yet)
|
||||
app.post('/api/bins', upload.single('image'), async (req, res) => {
|
||||
if (!req.file) return fail(res, 'no image uploaded');
|
||||
const { name = '' } = req.body;
|
||||
const { name = '', type_id = null } = req.body;
|
||||
if (type_id && !get_bin_type(type_id)) return fail(res, 'bin type not found', 404);
|
||||
const final_name = settle_image_filename(req.file.filename, req.file.originalname);
|
||||
const meta = await sharp(join('./data/images', final_name)).metadata();
|
||||
|
||||
// Register in unified source image gallery
|
||||
const src = {
|
||||
add_source_image({
|
||||
id: final_name,
|
||||
original_name: req.file.originalname,
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
uses: ['bin'],
|
||||
created_at: Date.now(),
|
||||
};
|
||||
add_source_image(src);
|
||||
});
|
||||
|
||||
const bt = type_id ? get_bin_type(type_id) : null;
|
||||
const mx = Math.round(meta.width * 0.15);
|
||||
const my = Math.round(meta.height * 0.15);
|
||||
const default_corners = [
|
||||
{ x: mx, y: my },
|
||||
{ x: meta.width - mx, y: my },
|
||||
{ x: meta.width - mx, y: meta.height - my },
|
||||
{ x: mx, y: meta.height - my },
|
||||
];
|
||||
const bin = {
|
||||
id: generate_id(),
|
||||
name: name.trim() || 'Bin',
|
||||
type_id,
|
||||
source_id: final_name,
|
||||
source_w: meta.width,
|
||||
source_h: meta.height,
|
||||
corners: default_corners,
|
||||
corners: [
|
||||
{ x: mx, y: my },
|
||||
{ x: meta.width - mx, y: my },
|
||||
{ x: meta.width - mx, y: meta.height - my },
|
||||
{ x: mx, y: meta.height - my },
|
||||
],
|
||||
phys_w: bt?.phys_w ?? null,
|
||||
phys_h: bt?.phys_h ?? null,
|
||||
image_filename: null,
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
@@ -777,13 +837,21 @@ app.put('/api/bins/:id/corners', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Update name only
|
||||
// Update name / type
|
||||
app.put('/api/bins/:id', (req, res) => {
|
||||
const bin = get_bin(req.params.id);
|
||||
if (!bin) return fail(res, 'not found', 404);
|
||||
const { name } = req.body;
|
||||
const { name, type_id } = req.body;
|
||||
if (type_id !== undefined && type_id !== null && !get_bin_type(type_id)) {
|
||||
return fail(res, 'bin type not found', 404);
|
||||
}
|
||||
const updated = { ...bin, updated_at: Date.now() };
|
||||
if (name !== undefined) updated.name = name.trim() || 'Bin';
|
||||
if (type_id !== undefined) {
|
||||
updated.type_id = type_id;
|
||||
const bt = type_id ? get_bin_type(type_id) : null;
|
||||
if (bt) { updated.phys_w = bt.phys_w; updated.phys_h = bt.phys_h; }
|
||||
}
|
||||
set_bin(updated);
|
||||
ok(res, { bin: updated });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user