diff --git a/app.py b/app.py index 259d6a8..230a276 100644 --- a/app.py +++ b/app.py @@ -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//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//charge-log//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//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 diff --git a/models.py b/models.py index 6cd25fa..976b95e 100644 --- a/models.py +++ b/models.py @@ -54,6 +54,11 @@ class Battery(Base): order_by="CapacityTest.tested_date", cascade="all, delete-orphan", ) + charge_logs = relationship( + "ChargeLog", back_populates="battery", + order_by="ChargeLog.charged_date", + cascade="all, delete-orphan", + ) def is_available(self): return self.status == "available" @@ -81,3 +86,18 @@ class CapacityTest(Base): def __repr__(self): return f"" + + +class ChargeLog(Base): + __tablename__ = "charge_log" + + id = Column(Integer, primary_key=True, autoincrement=True) + battery_id = Column(Integer, ForeignKey("battery.id", ondelete="CASCADE"), nullable=False) + charged_date = Column(String(10), nullable=False) # YYYY-MM-DD + increment_cycles = Column(Integer, nullable=False, default=0) # 0 or 1 + notes = Column(Text, nullable=True) + + battery = relationship("Battery", back_populates="charge_logs") + + def __repr__(self): + return f"" diff --git a/templates/assign.html b/templates/assign.html index e0470ad..d3d2701 100644 --- a/templates/assign.html +++ b/templates/assign.html @@ -11,23 +11,16 @@
{% for device in devices %} - {% set full = device.installed_count() >= device.battery_slots %} - {% set mix = device.installed_brands() and battery.brand not in device.installed_brands() %} -
-
{% endblock %} diff --git a/templates/battery_detail.html b/templates/battery_detail.html index f4ef0b5..0db404f 100644 --- a/templates/battery_detail.html +++ b/templates/battery_detail.html @@ -148,6 +148,66 @@
+ +
+

Charge History

+ + {% if charge_logs %} +
+ + + + + + + + + + + {% for log in charge_logs %} + + + + + + + {% endfor %} + +
Date+CycleNotes
{{ log.charged_date }}{{ '✓' if log.increment_cycles else '—' }}{{ log.notes or '—' }} +
+ +
+
+
+ {% else %} +

No charge log entries yet.

+ {% endif %} + +

Add Charge Entry

+
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+
+

Edit Details

diff --git a/templates/dashboard.html b/templates/dashboard.html index d1d0eb0..1eb1563 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -117,7 +117,7 @@ @@ -181,11 +181,8 @@ + + {% endif %}