Three features: device dropdown filter, charge log history, unassign-all

- Device dropdowns (quick-assign, bulk install, assign page) now only show
  devices with free slots; full devices are excluded entirely
- New ChargeLog model tracks charge dates with optional cycle increment;
  battery detail page gets a Charge History card with add/delete rows
- Device list page gets per-device Unassign All button (with confirmation)
  via new POST /device/<id>/unassign-all route
This commit is contained in:
2026-04-13 08:12:23 -05:00
parent 6597fcd4ac
commit b1bc02e963
6 changed files with 167 additions and 25 deletions
+72 -6
View File
@@ -2,7 +2,7 @@ from flask import Flask, render_template, redirect, url_for, request, flash, abo
from sqlalchemy import create_engine, func
from sqlalchemy.orm import scoped_session, sessionmaker
from models import Base, Battery, Device, CapacityTest
from models import Base, Battery, Device, CapacityTest, ChargeLog
def create_app(config_object="config"):
@@ -39,8 +39,10 @@ def create_app(config_object="config"):
.distinct().order_by(Battery.storage_location).all()
]
devices = db.query(Device).order_by(Device.name).all()
devices_with_slots = [d for d in devices if d.installed_count() < d.battery_slots]
return render_template("dashboard.html", batteries=batteries,
storage_locations=storage_locations, devices=devices)
storage_locations=storage_locations, devices=devices,
devices_with_slots=devices_with_slots)
# ------------------------------------------------------------------ #
# Battery — add
@@ -114,9 +116,14 @@ def create_app(config_object="config"):
.filter_by(battery_id=battery_id)
.order_by(CapacityTest.tested_date, CapacityTest.id)
.all())
charge_logs = (db.query(ChargeLog)
.filter_by(battery_id=battery_id)
.order_by(ChargeLog.charged_date.desc(), ChargeLog.id.desc())
.all())
return render_template("battery_detail.html", battery=battery,
storage_locations=storage_locations,
capacity_tests=capacity_tests)
capacity_tests=capacity_tests,
charge_logs=charge_logs)
# ------------------------------------------------------------------ #
# Battery — edit notes
@@ -199,6 +206,42 @@ def create_app(config_object="config"):
flash("Test record deleted.", "success")
return redirect(url_for("battery_detail", battery_id=battery_id))
# ------------------------------------------------------------------ #
# Battery — charge log history
# ------------------------------------------------------------------ #
@app.route("/battery/<int:battery_id>/charge-log/add", methods=["POST"])
def battery_charge_log_add(battery_id):
battery = db.get(Battery, battery_id)
if battery is None:
abort(404)
date_val = request.form.get("charged_date", "").strip()
if not date_val:
flash("Date is required.", "error")
return redirect(url_for("battery_detail", battery_id=battery_id))
increment = 1 if request.form.get("increment_cycles") else 0
notes = request.form.get("notes", "").strip() or None
if increment:
battery.charge_cycles = (battery.charge_cycles or 0) + 1
db.add(ChargeLog(battery_id=battery_id, charged_date=date_val,
increment_cycles=increment, notes=notes))
db.commit()
flash("Charge log entry added.", "success")
return redirect(url_for("battery_detail", battery_id=battery_id))
@app.route("/battery/<int:battery_id>/charge-log/<int:log_id>/delete", methods=["POST"])
def battery_charge_log_delete(battery_id, log_id):
battery = db.get(Battery, battery_id)
log = db.get(ChargeLog, log_id)
if battery is None or log is None or log.battery_id != battery_id:
abort(404)
if log.increment_cycles:
battery.charge_cycles = max(0, (battery.charge_cycles or 0) - 1)
db.delete(log)
db.commit()
flash("Charge log entry deleted.", "success")
return redirect(url_for("battery_detail", battery_id=battery_id))
# ------------------------------------------------------------------ #
# Battery — assign
# ------------------------------------------------------------------ #
@@ -214,13 +257,14 @@ def create_app(config_object="config"):
return redirect(url_for("battery_detail", battery_id=battery_id))
devices = db.query(Device).order_by(Device.name).all()
devices_with_slots = [d for d in devices if d.installed_count() < d.battery_slots]
if request.method == "POST":
device_id = request.form.get("device_id", type=int)
device = db.get(Device, device_id)
if device is None:
flash("Device not found.", "error")
return render_template("assign.html", battery=battery, devices=devices)
return render_template("assign.html", battery=battery, devices=devices_with_slots)
if device.installed_count() >= device.battery_slots:
flash(
@@ -228,7 +272,7 @@ def create_app(config_object="config"):
f"({device.battery_slots}/{device.battery_slots} slots used).",
"error",
)
return render_template("assign.html", battery=battery, devices=devices)
return render_template("assign.html", battery=battery, devices=devices_with_slots)
# Warn on brand mix (non-blocking)
existing_brands = device.installed_brands()
@@ -249,7 +293,7 @@ def create_app(config_object="config"):
flash(f"{battery.label} assigned to {device.name}.", "success")
return redirect(url_for("dashboard"))
return render_template("assign.html", battery=battery, devices=devices)
return render_template("assign.html", battery=battery, devices=devices_with_slots)
# ------------------------------------------------------------------ #
# Battery — unassign
@@ -668,6 +712,28 @@ def create_app(config_object="config"):
flash(f"Device '{name}' deleted. All batteries marked available.", "success")
return redirect(url_for("device_list"))
# ------------------------------------------------------------------ #
# Devices — unassign all batteries
# ------------------------------------------------------------------ #
@app.route("/device/<int:device_id>/unassign-all", methods=["POST"])
def device_unassign_all(device_id):
device = db.get(Device, device_id)
if device is None:
abort(404)
count = 0
for battery in device.batteries:
if battery.status == "installed":
battery.status = "available"
battery.device_id = None
count += 1
db.commit()
flash(
f"Unassigned {count} batter{'y' if count == 1 else 'ies'} from {device.name}.",
"success",
)
return redirect(url_for("device_list"))
return app