313 lines
13 KiB
Python
313 lines
13 KiB
Python
"""
|
|
Medications API - medication scheduling, logging, and adherence tracking
|
|
"""
|
|
|
|
import os
|
|
import uuid
|
|
import flask
|
|
import jwt
|
|
import core.auth as auth
|
|
import core.postgres as postgres
|
|
|
|
|
|
def _get_user_uuid(token):
|
|
try:
|
|
payload = jwt.decode(token, os.getenv("JWT_SECRET"), algorithms=["HS256"])
|
|
return payload.get("sub")
|
|
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
|
|
return None
|
|
|
|
|
|
def _auth(request):
|
|
"""Extract and verify token. Returns user_uuid or None."""
|
|
header = request.headers.get("Authorization", "")
|
|
if not header.startswith("Bearer "):
|
|
return None
|
|
token = header[7:]
|
|
user_uuid = _get_user_uuid(token)
|
|
if not user_uuid or not auth.verifyLoginToken(token, userUUID=user_uuid):
|
|
return None
|
|
return user_uuid
|
|
|
|
|
|
def register(app):
|
|
|
|
# ── Medications CRUD ──────────────────────────────────────────
|
|
|
|
@app.route("/api/medications", methods=["GET"])
|
|
def api_listMedications():
|
|
"""List all medications for the logged-in user."""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
meds = postgres.select("medications", where={"user_uuid": user_uuid}, order_by="name")
|
|
return flask.jsonify(meds), 200
|
|
|
|
@app.route("/api/medications", methods=["POST"])
|
|
def api_addMedication():
|
|
"""Add a medication. Body: {name, dosage, unit, frequency, times: ["08:00","20:00"], notes?}"""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
data = flask.request.get_json()
|
|
if not data:
|
|
return flask.jsonify({"error": "missing body"}), 400
|
|
required = ["name", "dosage", "unit", "frequency"]
|
|
missing = [f for f in required if not data.get(f)]
|
|
if missing:
|
|
return flask.jsonify({"error": f"missing required fields: {', '.join(missing)}"}), 400
|
|
data["id"] = str(uuid.uuid4())
|
|
data["user_uuid"] = user_uuid
|
|
data["times"] = data.get("times", [])
|
|
data["active"] = True
|
|
med = postgres.insert("medications", data)
|
|
return flask.jsonify(med), 201
|
|
|
|
@app.route("/api/medications/<med_id>", methods=["GET"])
|
|
def api_getMedication(med_id):
|
|
"""Get a single medication with its schedule."""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
|
if not med:
|
|
return flask.jsonify({"error": "not found"}), 404
|
|
return flask.jsonify(med), 200
|
|
|
|
@app.route("/api/medications/<med_id>", methods=["PUT"])
|
|
def api_updateMedication(med_id):
|
|
"""Update medication details. Body: {name?, dosage?, unit?, frequency?, times?, notes?, active?}"""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
data = flask.request.get_json()
|
|
if not data:
|
|
return flask.jsonify({"error": "missing body"}), 400
|
|
existing = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
|
if not existing:
|
|
return flask.jsonify({"error": "not found"}), 404
|
|
allowed = ["name", "dosage", "unit", "frequency", "times", "notes", "active"]
|
|
updates = {k: v for k, v in data.items() if k in allowed}
|
|
if not updates:
|
|
return flask.jsonify({"error": "no valid fields to update"}), 400
|
|
result = postgres.update("medications", updates, {"id": med_id, "user_uuid": user_uuid})
|
|
return flask.jsonify(result[0] if result else {}), 200
|
|
|
|
@app.route("/api/medications/<med_id>", methods=["DELETE"])
|
|
def api_deleteMedication(med_id):
|
|
"""Delete a medication and its logs."""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
existing = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
|
if not existing:
|
|
return flask.jsonify({"error": "not found"}), 404
|
|
postgres.delete("med_logs", {"medication_id": med_id})
|
|
postgres.delete("medications", {"id": med_id, "user_uuid": user_uuid})
|
|
return flask.jsonify({"deleted": True}), 200
|
|
|
|
# ── Medication Logging (take / skip / snooze) ─────────────────
|
|
|
|
@app.route("/api/medications/<med_id>/take", methods=["POST"])
|
|
def api_takeMedication(med_id):
|
|
"""Log that a dose was taken. Body: {scheduled_time?, notes?}"""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
|
if not med:
|
|
return flask.jsonify({"error": "not found"}), 404
|
|
data = flask.request.get_json() or {}
|
|
log_entry = {
|
|
"id": str(uuid.uuid4()),
|
|
"medication_id": med_id,
|
|
"user_uuid": user_uuid,
|
|
"action": "taken",
|
|
"scheduled_time": data.get("scheduled_time"),
|
|
"notes": data.get("notes"),
|
|
}
|
|
log = postgres.insert("med_logs", log_entry)
|
|
return flask.jsonify(log), 201
|
|
|
|
@app.route("/api/medications/<med_id>/skip", methods=["POST"])
|
|
def api_skipMedication(med_id):
|
|
"""Log a skipped dose. Body: {scheduled_time?, reason?}"""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
|
if not med:
|
|
return flask.jsonify({"error": "not found"}), 404
|
|
data = flask.request.get_json() or {}
|
|
log_entry = {
|
|
"id": str(uuid.uuid4()),
|
|
"medication_id": med_id,
|
|
"user_uuid": user_uuid,
|
|
"action": "skipped",
|
|
"scheduled_time": data.get("scheduled_time"),
|
|
"notes": data.get("reason"),
|
|
}
|
|
log = postgres.insert("med_logs", log_entry)
|
|
return flask.jsonify(log), 201
|
|
|
|
@app.route("/api/medications/<med_id>/snooze", methods=["POST"])
|
|
def api_snoozeMedication(med_id):
|
|
"""Snooze a reminder. Body: {minutes: 15}"""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
|
if not med:
|
|
return flask.jsonify({"error": "not found"}), 404
|
|
data = flask.request.get_json() or {}
|
|
minutes = data.get("minutes", 15)
|
|
return flask.jsonify({"snoozed_until_minutes": minutes}), 200
|
|
|
|
# ── Medication Log / History ──────────────────────────────────
|
|
|
|
@app.route("/api/medications/<med_id>/log", methods=["GET"])
|
|
def api_getMedLog(med_id):
|
|
"""Get dose log for a medication. Query: ?days=30"""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
|
if not med:
|
|
return flask.jsonify({"error": "not found"}), 404
|
|
days = flask.request.args.get("days", 30, type=int)
|
|
logs = postgres.select(
|
|
"med_logs",
|
|
where={"medication_id": med_id},
|
|
order_by="created_at DESC",
|
|
limit=days * 10,
|
|
)
|
|
return flask.jsonify(logs), 200
|
|
|
|
@app.route("/api/medications/today", methods=["GET"])
|
|
def api_todaysMeds():
|
|
"""Get today's medication schedule with taken/pending status."""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
meds = postgres.select("medications", where={"user_uuid": user_uuid, "active": True})
|
|
from datetime import datetime
|
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
result = []
|
|
for med in meds:
|
|
times = med.get("times", [])
|
|
taken_times = [
|
|
log["scheduled_time"]
|
|
for log in postgres.select(
|
|
"med_logs",
|
|
where={"medication_id": med["id"], "action": "taken"},
|
|
)
|
|
if log.get("scheduled_time", "").startswith(today)
|
|
]
|
|
result.append({
|
|
"medication": med,
|
|
"scheduled_times": times,
|
|
"taken_times": taken_times,
|
|
})
|
|
return flask.jsonify(result), 200
|
|
|
|
# ── Adherence Stats ───────────────────────────────────────────
|
|
|
|
@app.route("/api/medications/adherence", methods=["GET"])
|
|
def api_adherenceStats():
|
|
"""Get adherence stats across all meds. Query: ?days=30"""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
days = flask.request.args.get("days", 30, type=int)
|
|
meds = postgres.select("medications", where={"user_uuid": user_uuid, "active": True})
|
|
result = []
|
|
for med in meds:
|
|
logs = postgres.select(
|
|
"med_logs",
|
|
where={"medication_id": med["id"]},
|
|
limit=days * 10,
|
|
)
|
|
taken = sum(1 for log in logs if log.get("action") == "taken")
|
|
skipped = sum(1 for log in logs if log.get("action") == "skipped")
|
|
total = taken + skipped
|
|
adherence = (taken / total * 100) if total > 0 else 0
|
|
result.append({
|
|
"medication_id": med["id"],
|
|
"name": med["name"],
|
|
"taken": taken,
|
|
"skipped": skipped,
|
|
"adherence_percent": round(adherence, 1),
|
|
})
|
|
return flask.jsonify(result), 200
|
|
|
|
@app.route("/api/medications/<med_id>/adherence", methods=["GET"])
|
|
def api_medAdherence(med_id):
|
|
"""Get adherence stats for a single medication. Query: ?days=30"""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
|
if not med:
|
|
return flask.jsonify({"error": "not found"}), 404
|
|
days = flask.request.args.get("days", 30, type=int)
|
|
logs = postgres.select(
|
|
"med_logs",
|
|
where={"medication_id": med_id},
|
|
limit=days * 10,
|
|
)
|
|
taken = sum(1 for log in logs if log.get("action") == "taken")
|
|
skipped = sum(1 for log in logs if log.get("action") == "skipped")
|
|
total = taken + skipped
|
|
adherence = (taken / total * 100) if total > 0 else 0
|
|
return flask.jsonify({
|
|
"medication_id": med_id,
|
|
"name": med["name"],
|
|
"taken": taken,
|
|
"skipped": skipped,
|
|
"adherence_percent": round(adherence, 1),
|
|
}), 200
|
|
|
|
# ── Refills ───────────────────────────────────────────────────
|
|
|
|
@app.route("/api/medications/<med_id>/refill", methods=["PUT"])
|
|
def api_setRefill(med_id):
|
|
"""Set refill info. Body: {quantity_remaining, refill_date?, pharmacy_notes?}"""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
med = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
|
if not med:
|
|
return flask.jsonify({"error": "not found"}), 404
|
|
data = flask.request.get_json()
|
|
if not data:
|
|
return flask.jsonify({"error": "missing body"}), 400
|
|
allowed = ["quantity_remaining", "refill_date", "pharmacy_notes"]
|
|
updates = {k: v for k, v in data.items() if k in allowed}
|
|
if not updates:
|
|
return flask.jsonify({"error": "no valid fields to update"}), 400
|
|
result = postgres.update("medications", updates, {"id": med_id, "user_uuid": user_uuid})
|
|
return flask.jsonify(result[0] if result else {}), 200
|
|
|
|
@app.route("/api/medications/refills-due", methods=["GET"])
|
|
def api_refillsDue():
|
|
"""Get medications that need refills soon. Query: ?days_ahead=7"""
|
|
user_uuid = _auth(flask.request)
|
|
if not user_uuid:
|
|
return flask.jsonify({"error": "unauthorized"}), 401
|
|
days_ahead = flask.request.args.get("days_ahead", 7, type=int)
|
|
from datetime import datetime, timedelta
|
|
cutoff = (datetime.now() + timedelta(days=days_ahead)).strftime("%Y-%m-%d")
|
|
meds = postgres.select(
|
|
"medications",
|
|
where={"user_uuid": user_uuid},
|
|
)
|
|
due = []
|
|
for med in meds:
|
|
qty = med.get("quantity_remaining")
|
|
refill_date = med.get("refill_date")
|
|
if qty is not None and qty <= 7:
|
|
due.append(med)
|
|
elif refill_date and refill_date <= cutoff:
|
|
due.append(med)
|
|
return flask.jsonify(due), 200
|