diff --git a/app.py b/app.py index 7db066c..496684f 100644 --- a/app.py +++ b/app.py @@ -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//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 # ------------------------------------------------------------------ # diff --git a/templates/dashboard.html b/templates/dashboard.html index c9f1741..739e649 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -167,7 +167,18 @@ View {% if b.is_available() %} - Assign + + {% endif %} {% if b.is_installed() %} @@ -249,6 +260,18 @@ function applyFilters() { updateToolbar(); } +function quickAssign(action, batteryId) { + var sel = document.getElementById('qas-' + batteryId); + if (!sel.value) { sel.focus(); return; } + var f = document.createElement('form'); + f.method = 'post'; f.action = action; + var inp = document.createElement('input'); + inp.type = 'hidden'; inp.name = 'device_id'; inp.value = sel.value; + f.appendChild(inp); + document.body.appendChild(f); + f.submit(); +} + function resetFilters() { ['filter-status','filter-brand','filter-size','filter-storage'].forEach(function(id) { document.getElementById(id).value = ''; diff --git a/templates/device_detail.html b/templates/device_detail.html index e046e2a..f7ebc1e 100644 --- a/templates/device_detail.html +++ b/templates/device_detail.html @@ -30,41 +30,51 @@ {% set free_slots = device.battery_slots - device.installed_count() %}

{{ free_slots }} slot(s) free

-
+
Brand Qty - {% for i in range(1, 5) %} -
- - +
+
+ + +
+
- - {% endfor %}
+ +
@@ -98,6 +108,28 @@ function brandSelectChanged(sel, inputId) { {% endif %}
+
+

Install Specific Battery

+ {% if available_batteries %} +
+
+ + +
+ +
+ {% else %} +

No available batteries.

+ {% endif %} +
+

Delete Device

diff --git a/tests/test_acceptance.py b/tests/test_acceptance.py index 56c201b..a50456d 100644 --- a/tests/test_acceptance.py +++ b/tests/test_acceptance.py @@ -294,6 +294,56 @@ def test_device_install_over_capacity(client): assert b"slot" in resp.data.lower() +# ------------------------------------------------------------------ # +# Dashboard — quick-assign +# ------------------------------------------------------------------ # + +def test_dashboard_quick_assign(client): + """Quick-assign from dashboard (battery_assign POST) succeeds.""" + client.post("/device/add", data={"name": "Box", "battery_slots": "2"}) + client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) + resp = client.post("/battery/1/assign", data={"device_id": "1"}, + follow_redirects=True) + assert resp.status_code == 200 + assert b"Box" in resp.data + + +def test_dashboard_quick_assign_full_device_blocked(client): + """Quick-assign from dashboard to a full device is blocked.""" + client.post("/device/add", data={"name": "Box", "battery_slots": "1"}) + client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}) + client.post("/battery/1/assign", data={"device_id": "1"}) # fills the slot + resp = client.post("/battery/2/assign", data={"device_id": "1"}, + follow_redirects=True) + assert resp.status_code == 200 + assert b"full" in resp.data.lower() + + +# ------------------------------------------------------------------ # +# Device — install-one (specific battery) +# ------------------------------------------------------------------ # + +def test_install_one_specific_battery(client): + """device_install_one installs a chosen battery.""" + client.post("/device/add", data={"name": "Box", "battery_slots": "2"}) + client.post("/battery/add", data={"brand": "Eneloop", "count": "1"}) + resp = client.post("/device/1/install-one", data={"battery_id": "1"}, + follow_redirects=True) + assert resp.status_code == 200 + assert b"Eneloop 001" in resp.data + + +def test_install_one_over_capacity_blocked(client): + """device_install_one to a full device is blocked.""" + client.post("/device/add", data={"name": "Box", "battery_slots": "1"}) + client.post("/battery/add", data={"brand": "Eneloop", "count": "2"}) + client.post("/device/1/install-one", data={"battery_id": "1"}) # fills slot + resp = client.post("/device/1/install-one", data={"battery_id": "2"}, + follow_redirects=True) + assert resp.status_code == 200 + assert b"full" in resp.data.lower() + + # ------------------------------------------------------------------ # # Device — list # ------------------------------------------------------------------ #