Add inline assign from dashboard, specific battery picker on device, dynamic install rows

- Dashboard: replace Assign link with device dropdown + arrow button for
  quick inline assignment without leaving the page
- Device detail: replace hardcoded 4-row install form with 1 row + '+ Add
  brand' button that clones rows dynamically
- Device detail: add 'Install Specific Battery' card with dropdown of all
  available batteries (label, brand, size, notes) via new /device/<id>/install-one route
- Tests: 4 new acceptance tests covering dashboard quick-assign and
  install-one, including capacity enforcement on both paths (39 total)
This commit is contained in:
2026-04-12 20:15:29 -05:00
parent 4ad29558b3
commit 81e87d2fe2
4 changed files with 170 additions and 26 deletions
+41 -2
View File
@@ -33,8 +33,9 @@ def create_app(config_object="config"):
.filter(Battery.storage_location.isnot(None))
.distinct().order_by(Battery.storage_location).all()
]
devices = db.query(Device).order_by(Device.name).all()
return render_template("dashboard.html", batteries=batteries,
storage_locations=storage_locations)
storage_locations=storage_locations, devices=devices)
# ------------------------------------------------------------------ #
# Battery — add
@@ -361,7 +362,11 @@ def create_app(config_object="config"):
if device is None:
abort(404)
brands = [r[0] for r in db.query(Battery.brand).distinct().order_by(Battery.brand).all()]
return render_template("device_detail.html", device=device, brands=brands)
available_batteries = (db.query(Battery)
.filter_by(status="available")
.order_by(Battery.label).all())
return render_template("device_detail.html", device=device, brands=brands,
available_batteries=available_batteries)
# ------------------------------------------------------------------ #
# Devices — install batteries
@@ -436,6 +441,40 @@ def create_app(config_object="config"):
)
return redirect(url_for("device_detail", device_id=device_id))
# ------------------------------------------------------------------ #
# Devices — install one specific battery
# ------------------------------------------------------------------ #
@app.route("/device/<int:device_id>/install-one", methods=["POST"])
def device_install_one(device_id):
device = db.get(Device, device_id)
if device is None:
abort(404)
battery_id = request.form.get("battery_id", type=int)
battery = db.get(Battery, battery_id)
if battery is None or not battery.is_available():
flash("Battery not found or not available.", "error")
return redirect(url_for("device_detail", device_id=device_id))
if device.installed_count() >= device.battery_slots:
flash(
f"{device.name} is full "
f"({device.battery_slots}/{device.battery_slots} slots used).",
"error",
)
return redirect(url_for("device_detail", device_id=device_id))
existing_brands = device.installed_brands()
if existing_brands and battery.brand not in existing_brands:
flash(
f"Warning: {device.name} already has batteries from "
f"{', '.join(existing_brands)}. Mixing brands is not recommended.",
"warning",
)
battery.status = "installed"
battery.device_id = device.id
db.commit()
flash(f"{battery.label} installed in {device.name}.", "success")
return redirect(url_for("device_detail", device_id=device_id))
# ------------------------------------------------------------------ #
# Devices — delete
# ------------------------------------------------------------------ #