Add size-in-label checkbox to battery add form

This commit is contained in:
2026-04-18 11:12:37 -05:00
parent f64e14e713
commit 66062faac6
3 changed files with 37 additions and 14 deletions
+20 -8
View File
@@ -144,14 +144,19 @@ def create_app(config_object="config"):
purchase_date = f.get("purchase_date", "").strip() or None purchase_date = f.get("purchase_date", "").strip() or None
storage_location = f.get("storage_location", "").strip() or None storage_location = f.get("storage_location", "").strip() or None
include_size = f.get("include_size_in_label") == "on"
label_prefix = f"{brand} {size}" if include_size and size else brand
existing_labels = [ existing_labels = [
r[0] for r in db.query(Battery.label).filter(Battery.brand == brand).all() r[0] for r in db.query(Battery.label).filter(Battery.brand == brand).all()
] ]
nums = [int(m.group(1)) for lbl in existing_labels prefix_labels = [lbl for lbl in existing_labels
if lbl == label_prefix or lbl.startswith(label_prefix + " ")]
nums = [int(m.group(1)) for lbl in prefix_labels
if (m := re.search(r'(\d+)$', lbl))] if (m := re.search(r'(\d+)$', lbl))]
next_num = max(nums, default=0) next_num = max(nums, default=0)
for i in range(count): for i in range(count):
label = f"{brand} {next_num + i + 1:03d}" label = f"{label_prefix} {next_num + i + 1:03d}"
db.add(Battery(label=label, brand=brand, status="available", notes=notes, db.add(Battery(label=label, brand=brand, status="available", notes=notes,
size=size, chemistry=chemistry, capacity_mah=capacity_mah, size=size, chemistry=chemistry, capacity_mah=capacity_mah,
purchase_date=purchase_date, storage_location=storage_location)) purchase_date=purchase_date, storage_location=storage_location))
@@ -165,14 +170,21 @@ def create_app(config_object="config"):
.filter(Battery.storage_location.isnot(None)) .filter(Battery.storage_location.isnot(None))
.distinct().order_by(Battery.storage_location).all() .distinct().order_by(Battery.storage_location).all()
] ]
brand_counts = { prefix_max_nums = {}
r[0]: r[1] for lbl, brand, size in db.query(Battery.label, Battery.brand, Battery.size).all():
for r in db.query(Battery.brand, func.count(Battery.id)) m = re.search(r'(\d+)$', lbl)
.group_by(Battery.brand).all() if not m:
} continue
num = int(m.group(1))
if num > prefix_max_nums.get(brand, 0):
prefix_max_nums[brand] = num
if size:
key = f"{brand} {size}"
if num > prefix_max_nums.get(key, 0):
prefix_max_nums[key] = num
return render_template("battery_add.html", form_count=1, brands=brands, return render_template("battery_add.html", form_count=1, brands=brands,
storage_locations=storage_locations, storage_locations=storage_locations,
brand_counts=brand_counts) prefix_max_nums=prefix_max_nums)
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Battery — detail # Battery — detail
+16 -5
View File
@@ -27,6 +27,12 @@
<label for="count">Quantity</label> <label for="count">Quantity</label>
<input type="number" id="count" name="count" value="{{ form_count|default(1) }}" min="1" max="50"> <input type="number" id="count" name="count" value="{{ form_count|default(1) }}" min="1" max="50">
<small class="text-muted">Labels are auto-generated (e.g. Eneloop 001, Eneloop 002)</small> <small class="text-muted">Labels are auto-generated (e.g. Eneloop 001, Eneloop 002)</small>
<div style="margin-top:0.4rem;">
<label style="display:inline-flex;align-items:center;gap:0.4rem;font-weight:normal;cursor:pointer;">
<input type="checkbox" id="include_size_in_label" name="include_size_in_label">
Include size in label (e.g. Eneloop AAA 001)
</label>
</div>
<div id="label-preview" style="font-size:0.85rem;color:var(--link);margin-top:0.3rem;font-weight:500;min-height:1.2em;"></div> <div id="label-preview" style="font-size:0.85rem;color:var(--link);margin-top:0.3rem;font-weight:500;min-height:1.2em;"></div>
</div> </div>
@@ -97,7 +103,7 @@
</div> </div>
<script> <script>
var brandCounts = {{ brand_counts|default({})|tojson }}; var prefixMaxNums = {{ prefix_max_nums|default({})|tojson }};
function metaSelectChanged(sel, inputId) { function metaSelectChanged(sel, inputId) {
var input = document.getElementById(inputId); var input = document.getElementById(inputId);
@@ -109,25 +115,30 @@ function metaSelectChanged(sel, inputId) {
input.style.display = 'none'; input.style.display = 'none';
input.value = sel.value; input.value = sel.value;
} }
if (inputId === 'brand') updateLabelPreview(); if (inputId === 'brand' || inputId === 'size') updateLabelPreview();
} }
function updateLabelPreview() { function updateLabelPreview() {
var brand = document.getElementById('brand').value.trim(); var brand = document.getElementById('brand').value.trim();
var size = document.getElementById('size').value.trim();
var includeSize = document.getElementById('include_size_in_label').checked;
var count = parseInt(document.getElementById('count').value, 10) || 1; var count = parseInt(document.getElementById('count').value, 10) || 1;
var preview = document.getElementById('label-preview'); var preview = document.getElementById('label-preview');
if (!brand) { preview.textContent = ''; return; } if (!brand) { preview.textContent = ''; return; }
var existing = brandCounts[brand] !== undefined ? brandCounts[brand] : 0; var prefix = (includeSize && size) ? brand + ' ' + size : brand;
var existing = prefixMaxNums[prefix] !== undefined ? prefixMaxNums[prefix] : 0;
var first = existing + 1; var first = existing + 1;
var last = existing + count; var last = existing + count;
var pad = function(n) { return n.toString().padStart(3, '0'); }; var pad = function(n) { return n.toString().padStart(3, '0'); };
preview.textContent = count === 1 preview.textContent = count === 1
? 'Will create: ' + brand + ' ' + pad(first) ? 'Will create: ' + prefix + ' ' + pad(first)
: 'Will create: ' + brand + ' ' + pad(first) + ' \u2192 ' + brand + ' ' + pad(last); : 'Will create: ' + prefix + ' ' + pad(first) + ' \u2192 ' + prefix + ' ' + pad(last);
} }
document.getElementById('count').addEventListener('input', updateLabelPreview); document.getElementById('count').addEventListener('input', updateLabelPreview);
document.getElementById('brand').addEventListener('input', updateLabelPreview); document.getElementById('brand').addEventListener('input', updateLabelPreview);
document.getElementById('size').addEventListener('input', updateLabelPreview);
document.getElementById('include_size_in_label').addEventListener('change', updateLabelPreview);
// Restore brand state on error re-render // Restore brand state on error re-render
(function () { (function () {
+1 -1
View File
@@ -48,7 +48,7 @@ def test_dashboard_loads(seeded_client):
def test_battery_add_get_renders(client): def test_battery_add_get_renders(client):
resp = client.get("/battery/add") resp = client.get("/battery/add")
assert resp.status_code == 200 assert resp.status_code == 200
assert b"brandCounts" in resp.data assert b"prefixMaxNums" in resp.data
def test_battery_add_brand_counts_reflects_existing(seeded_client): def test_battery_add_brand_counts_reflects_existing(seeded_client):