no
This commit is contained in:
@@ -39,10 +39,7 @@ _JSON_COLS = {"times", "days_of_week"}
|
|||||||
|
|
||||||
def _wrap_json(data):
|
def _wrap_json(data):
|
||||||
"""Wrap list/dict values in Json() for psycopg2 when targeting JSON columns."""
|
"""Wrap list/dict values in Json() for psycopg2 when targeting JSON columns."""
|
||||||
return {
|
return {k: Json(v) if k in _JSON_COLS and isinstance(v, (list, dict)) else v for k, v in data.items()}
|
||||||
k: Json(v) if k in _JSON_COLS and isinstance(v, (list, dict)) else v
|
|
||||||
for k, v in data.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _filter_times_in_range(times, start, end):
|
def _filter_times_in_range(times, start, end):
|
||||||
@@ -64,84 +61,13 @@ def _is_med_due_today(med, today, current_day):
|
|||||||
start = med.get("start_date")
|
start = med.get("start_date")
|
||||||
interval = med.get("interval_days")
|
interval = med.get("interval_days")
|
||||||
if start and interval:
|
if start and interval:
|
||||||
start_d = (
|
start_d = start if isinstance(start, date) else datetime.strptime(str(start), "%Y-%m-%d").date()
|
||||||
start
|
|
||||||
if isinstance(start, date)
|
|
||||||
else datetime.strptime(str(start), "%Y-%m-%d").date()
|
|
||||||
)
|
|
||||||
days_since = (today - start_d).days
|
days_since = (today - start_d).days
|
||||||
return days_since >= 0 and days_since % interval == 0
|
return days_since >= 0 and days_since % interval == 0
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _get_nearest_scheduled_time(med):
|
|
||||||
"""Find the nearest scheduled time for a medication.
|
|
||||||
|
|
||||||
Looks at the medication's scheduled times and finds the one closest to now.
|
|
||||||
Returns the time as HH:MM string, or None if medication is as_needed.
|
|
||||||
"""
|
|
||||||
freq = med.get("frequency", "daily")
|
|
||||||
|
|
||||||
# PRN medications don't have scheduled times
|
|
||||||
if freq == "as_needed":
|
|
||||||
return None
|
|
||||||
|
|
||||||
times = med.get("times", [])
|
|
||||||
if not times:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Get current time
|
|
||||||
now = datetime.now().time()
|
|
||||||
now_minutes = now.hour * 60 + now.minute
|
|
||||||
|
|
||||||
# Find the time closest to now (within ±4 hours window)
|
|
||||||
best_time = None
|
|
||||||
best_diff = float("inf")
|
|
||||||
window_minutes = 4 * 60 # 4 hour window
|
|
||||||
|
|
||||||
for time_str in times:
|
|
||||||
try:
|
|
||||||
# Parse time string (e.g., "12:00")
|
|
||||||
hour, minute = map(int, time_str.split(":"))
|
|
||||||
time_minutes = hour * 60 + minute
|
|
||||||
|
|
||||||
# Calculate difference (handle wrap-around for times near midnight)
|
|
||||||
diff = abs(time_minutes - now_minutes)
|
|
||||||
# Also check if it's much earlier/later due to day boundary
|
|
||||||
alt_diff = abs((time_minutes + 24 * 60) - now_minutes)
|
|
||||||
diff = min(diff, alt_diff)
|
|
||||||
|
|
||||||
# Only consider times within the window
|
|
||||||
if diff <= window_minutes and diff < best_diff:
|
|
||||||
best_diff = diff
|
|
||||||
best_time = time_str
|
|
||||||
except (ValueError, AttributeError):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# If no time within window, use the most recent past time
|
|
||||||
if not best_time:
|
|
||||||
for time_str in times:
|
|
||||||
try:
|
|
||||||
hour, minute = map(int, time_str.split(":"))
|
|
||||||
time_minutes = hour * 60 + minute
|
|
||||||
|
|
||||||
# If this time was earlier today, it's a candidate
|
|
||||||
if time_minutes <= now_minutes:
|
|
||||||
diff = now_minutes - time_minutes
|
|
||||||
if diff < best_diff:
|
|
||||||
best_diff = diff
|
|
||||||
best_time = time_str
|
|
||||||
except (ValueError, AttributeError):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# If still no time found, use the first scheduled time
|
|
||||||
if not best_time and times:
|
|
||||||
best_time = times[0]
|
|
||||||
|
|
||||||
return best_time
|
|
||||||
|
|
||||||
|
|
||||||
def _compute_next_dose_date(med):
|
def _compute_next_dose_date(med):
|
||||||
"""Compute the next dose date for every_n_days medications."""
|
"""Compute the next dose date for every_n_days medications."""
|
||||||
interval = med.get("interval_days")
|
interval = med.get("interval_days")
|
||||||
@@ -172,11 +98,7 @@ def _count_expected_doses(med, period_start, days):
|
|||||||
start = med.get("start_date")
|
start = med.get("start_date")
|
||||||
if not start:
|
if not start:
|
||||||
return 0
|
return 0
|
||||||
start_d = (
|
start_d = start if isinstance(start, date) else datetime.strptime(str(start), "%Y-%m-%d").date()
|
||||||
start
|
|
||||||
if isinstance(start, date)
|
|
||||||
else datetime.strptime(str(start), "%Y-%m-%d").date()
|
|
||||||
)
|
|
||||||
count = 0
|
count = 0
|
||||||
for d in range(days):
|
for d in range(days):
|
||||||
check_date = period_start + timedelta(days=d)
|
check_date = period_start + timedelta(days=d)
|
||||||
@@ -203,19 +125,14 @@ def _log_local_date(created_at, user_tz):
|
|||||||
def _count_logs_in_period(logs, period_start_str, action, user_tz=None):
|
def _count_logs_in_period(logs, period_start_str, action, user_tz=None):
|
||||||
"""Count logs of a given action where created_at (local date) >= period_start."""
|
"""Count logs of a given action where created_at (local date) >= period_start."""
|
||||||
return sum(
|
return sum(
|
||||||
1
|
1 for log in logs
|
||||||
for log in logs
|
|
||||||
if log.get("action") == action
|
if log.get("action") == action
|
||||||
and (
|
and (_log_local_date(log.get("created_at"), user_tz) if user_tz else str(log.get("created_at", ""))[:10]) >= period_start_str
|
||||||
_log_local_date(log.get("created_at"), user_tz)
|
|
||||||
if user_tz
|
|
||||||
else str(log.get("created_at", ""))[:10]
|
|
||||||
)
|
|
||||||
>= period_start_str
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def register(app):
|
def register(app):
|
||||||
|
|
||||||
# ── Medications CRUD ──────────────────────────────────────────
|
# ── Medications CRUD ──────────────────────────────────────────
|
||||||
|
|
||||||
@app.route("/api/medications", methods=["GET"])
|
@app.route("/api/medications", methods=["GET"])
|
||||||
@@ -224,9 +141,7 @@ def register(app):
|
|||||||
user_uuid = _auth(flask.request)
|
user_uuid = _auth(flask.request)
|
||||||
if not user_uuid:
|
if not user_uuid:
|
||||||
return flask.jsonify({"error": "unauthorized"}), 401
|
return flask.jsonify({"error": "unauthorized"}), 401
|
||||||
meds = postgres.select(
|
meds = postgres.select("medications", where={"user_uuid": user_uuid}, order_by="name")
|
||||||
"medications", where={"user_uuid": user_uuid}, order_by="name"
|
|
||||||
)
|
|
||||||
return flask.jsonify(meds), 200
|
return flask.jsonify(meds), 200
|
||||||
|
|
||||||
@app.route("/api/medications", methods=["POST"])
|
@app.route("/api/medications", methods=["POST"])
|
||||||
@@ -241,9 +156,7 @@ def register(app):
|
|||||||
required = ["name", "dosage", "unit", "frequency"]
|
required = ["name", "dosage", "unit", "frequency"]
|
||||||
missing = [f for f in required if not data.get(f)]
|
missing = [f for f in required if not data.get(f)]
|
||||||
if missing:
|
if missing:
|
||||||
return flask.jsonify(
|
return flask.jsonify({"error": f"missing required fields: {', '.join(missing)}"}), 400
|
||||||
{"error": f"missing required fields: {', '.join(missing)}"}
|
|
||||||
), 400
|
|
||||||
|
|
||||||
row = {
|
row = {
|
||||||
"id": str(uuid.uuid4()),
|
"id": str(uuid.uuid4()),
|
||||||
@@ -261,11 +174,7 @@ def register(app):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Compute next_dose_date for interval meds
|
# Compute next_dose_date for interval meds
|
||||||
if (
|
if data.get("frequency") == "every_n_days" and data.get("start_date") and data.get("interval_days"):
|
||||||
data.get("frequency") == "every_n_days"
|
|
||||||
and data.get("start_date")
|
|
||||||
and data.get("interval_days")
|
|
||||||
):
|
|
||||||
start = datetime.strptime(data["start_date"], "%Y-%m-%d").date()
|
start = datetime.strptime(data["start_date"], "%Y-%m-%d").date()
|
||||||
today = tz.user_today()
|
today = tz.user_today()
|
||||||
if start > today:
|
if start > today:
|
||||||
@@ -276,9 +185,7 @@ def register(app):
|
|||||||
if remainder == 0:
|
if remainder == 0:
|
||||||
row["next_dose_date"] = today.isoformat()
|
row["next_dose_date"] = today.isoformat()
|
||||||
else:
|
else:
|
||||||
row["next_dose_date"] = (
|
row["next_dose_date"] = (today + timedelta(days=data["interval_days"] - remainder)).isoformat()
|
||||||
today + timedelta(days=data["interval_days"] - remainder)
|
|
||||||
).isoformat()
|
|
||||||
|
|
||||||
med = postgres.insert("medications", _wrap_json(row))
|
med = postgres.insert("medications", _wrap_json(row))
|
||||||
return flask.jsonify(med), 201
|
return flask.jsonify(med), 201
|
||||||
@@ -303,30 +210,17 @@ def register(app):
|
|||||||
data = flask.request.get_json()
|
data = flask.request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
return flask.jsonify({"error": "missing body"}), 400
|
return flask.jsonify({"error": "missing body"}), 400
|
||||||
existing = postgres.select_one(
|
existing = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
||||||
"medications", {"id": med_id, "user_uuid": user_uuid}
|
|
||||||
)
|
|
||||||
if not existing:
|
if not existing:
|
||||||
return flask.jsonify({"error": "not found"}), 404
|
return flask.jsonify({"error": "not found"}), 404
|
||||||
allowed = [
|
allowed = [
|
||||||
"name",
|
"name", "dosage", "unit", "frequency", "times", "notes", "active",
|
||||||
"dosage",
|
"days_of_week", "interval_days", "start_date", "next_dose_date",
|
||||||
"unit",
|
|
||||||
"frequency",
|
|
||||||
"times",
|
|
||||||
"notes",
|
|
||||||
"active",
|
|
||||||
"days_of_week",
|
|
||||||
"interval_days",
|
|
||||||
"start_date",
|
|
||||||
"next_dose_date",
|
|
||||||
]
|
]
|
||||||
updates = {k: v for k, v in data.items() if k in allowed}
|
updates = {k: v for k, v in data.items() if k in allowed}
|
||||||
if not updates:
|
if not updates:
|
||||||
return flask.jsonify({"error": "no valid fields to update"}), 400
|
return flask.jsonify({"error": "no valid fields to update"}), 400
|
||||||
result = postgres.update(
|
result = postgres.update("medications", _wrap_json(updates), {"id": med_id, "user_uuid": user_uuid})
|
||||||
"medications", _wrap_json(updates), {"id": med_id, "user_uuid": user_uuid}
|
|
||||||
)
|
|
||||||
return flask.jsonify(result[0] if result else {}), 200
|
return flask.jsonify(result[0] if result else {}), 200
|
||||||
|
|
||||||
@app.route("/api/medications/<med_id>", methods=["DELETE"])
|
@app.route("/api/medications/<med_id>", methods=["DELETE"])
|
||||||
@@ -335,9 +229,7 @@ def register(app):
|
|||||||
user_uuid = _auth(flask.request)
|
user_uuid = _auth(flask.request)
|
||||||
if not user_uuid:
|
if not user_uuid:
|
||||||
return flask.jsonify({"error": "unauthorized"}), 401
|
return flask.jsonify({"error": "unauthorized"}), 401
|
||||||
existing = postgres.select_one(
|
existing = postgres.select_one("medications", {"id": med_id, "user_uuid": user_uuid})
|
||||||
"medications", {"id": med_id, "user_uuid": user_uuid}
|
|
||||||
)
|
|
||||||
if not existing:
|
if not existing:
|
||||||
return flask.jsonify({"error": "not found"}), 404
|
return flask.jsonify({"error": "not found"}), 404
|
||||||
postgres.delete("med_logs", {"medication_id": med_id})
|
postgres.delete("med_logs", {"medication_id": med_id})
|
||||||
@@ -356,19 +248,12 @@ def register(app):
|
|||||||
if not med:
|
if not med:
|
||||||
return flask.jsonify({"error": "not found"}), 404
|
return flask.jsonify({"error": "not found"}), 404
|
||||||
data = flask.request.get_json() or {}
|
data = flask.request.get_json() or {}
|
||||||
|
|
||||||
# Determine scheduled_time
|
|
||||||
scheduled_time = data.get("scheduled_time")
|
|
||||||
if not scheduled_time:
|
|
||||||
# Auto-determine from medication schedule
|
|
||||||
scheduled_time = _get_nearest_scheduled_time(med)
|
|
||||||
|
|
||||||
log_entry = {
|
log_entry = {
|
||||||
"id": str(uuid.uuid4()),
|
"id": str(uuid.uuid4()),
|
||||||
"medication_id": med_id,
|
"medication_id": med_id,
|
||||||
"user_uuid": user_uuid,
|
"user_uuid": user_uuid,
|
||||||
"action": "taken",
|
"action": "taken",
|
||||||
"scheduled_time": scheduled_time,
|
"scheduled_time": data.get("scheduled_time"),
|
||||||
"notes": data.get("notes"),
|
"notes": data.get("notes"),
|
||||||
}
|
}
|
||||||
log = postgres.insert("med_logs", log_entry)
|
log = postgres.insert("med_logs", log_entry)
|
||||||
@@ -376,9 +261,7 @@ def register(app):
|
|||||||
if med.get("frequency") == "every_n_days" and med.get("interval_days"):
|
if med.get("frequency") == "every_n_days" and med.get("interval_days"):
|
||||||
next_date = _compute_next_dose_date(med)
|
next_date = _compute_next_dose_date(med)
|
||||||
if next_date:
|
if next_date:
|
||||||
postgres.update(
|
postgres.update("medications", {"next_dose_date": next_date}, {"id": med_id})
|
||||||
"medications", {"next_dose_date": next_date}, {"id": med_id}
|
|
||||||
)
|
|
||||||
return flask.jsonify(log), 201
|
return flask.jsonify(log), 201
|
||||||
|
|
||||||
@app.route("/api/medications/<med_id>/skip", methods=["POST"])
|
@app.route("/api/medications/<med_id>/skip", methods=["POST"])
|
||||||
@@ -391,19 +274,12 @@ def register(app):
|
|||||||
if not med:
|
if not med:
|
||||||
return flask.jsonify({"error": "not found"}), 404
|
return flask.jsonify({"error": "not found"}), 404
|
||||||
data = flask.request.get_json() or {}
|
data = flask.request.get_json() or {}
|
||||||
|
|
||||||
# Determine scheduled_time
|
|
||||||
scheduled_time = data.get("scheduled_time")
|
|
||||||
if not scheduled_time:
|
|
||||||
# Auto-determine from medication schedule
|
|
||||||
scheduled_time = _get_nearest_scheduled_time(med)
|
|
||||||
|
|
||||||
log_entry = {
|
log_entry = {
|
||||||
"id": str(uuid.uuid4()),
|
"id": str(uuid.uuid4()),
|
||||||
"medication_id": med_id,
|
"medication_id": med_id,
|
||||||
"user_uuid": user_uuid,
|
"user_uuid": user_uuid,
|
||||||
"action": "skipped",
|
"action": "skipped",
|
||||||
"scheduled_time": scheduled_time,
|
"scheduled_time": data.get("scheduled_time"),
|
||||||
"notes": data.get("reason"),
|
"notes": data.get("reason"),
|
||||||
}
|
}
|
||||||
log = postgres.insert("med_logs", log_entry)
|
log = postgres.insert("med_logs", log_entry)
|
||||||
@@ -411,9 +287,7 @@ def register(app):
|
|||||||
if med.get("frequency") == "every_n_days" and med.get("interval_days"):
|
if med.get("frequency") == "every_n_days" and med.get("interval_days"):
|
||||||
next_date = _compute_next_dose_date(med)
|
next_date = _compute_next_dose_date(med)
|
||||||
if next_date:
|
if next_date:
|
||||||
postgres.update(
|
postgres.update("medications", {"next_dose_date": next_date}, {"id": med_id})
|
||||||
"medications", {"next_dose_date": next_date}, {"id": med_id}
|
|
||||||
)
|
|
||||||
return flask.jsonify(log), 201
|
return flask.jsonify(log), 201
|
||||||
|
|
||||||
@app.route("/api/medications/<med_id>/snooze", methods=["POST"])
|
@app.route("/api/medications/<med_id>/snooze", methods=["POST"])
|
||||||
@@ -461,9 +335,7 @@ def register(app):
|
|||||||
if not user_uuid:
|
if not user_uuid:
|
||||||
return flask.jsonify({"error": "unauthorized"}), 401
|
return flask.jsonify({"error": "unauthorized"}), 401
|
||||||
|
|
||||||
meds = postgres.select(
|
meds = postgres.select("medications", where={"user_uuid": user_uuid, "active": True})
|
||||||
"medications", where={"user_uuid": user_uuid, "active": True}
|
|
||||||
)
|
|
||||||
now = tz.user_now()
|
now = tz.user_now()
|
||||||
user_tz = now.tzinfo
|
user_tz = now.tzinfo
|
||||||
today = now.date()
|
today = now.date()
|
||||||
@@ -499,15 +371,13 @@ def register(app):
|
|||||||
and _log_local_date(log.get("created_at"), user_tz) == today_str
|
and _log_local_date(log.get("created_at"), user_tz) == today_str
|
||||||
]
|
]
|
||||||
|
|
||||||
result.append(
|
result.append({
|
||||||
{
|
|
||||||
"medication": med,
|
"medication": med,
|
||||||
"scheduled_times": [] if is_prn else med.get("times", []),
|
"scheduled_times": [] if is_prn else med.get("times", []),
|
||||||
"taken_times": today_taken,
|
"taken_times": today_taken,
|
||||||
"skipped_times": today_skipped,
|
"skipped_times": today_skipped,
|
||||||
"is_prn": is_prn,
|
"is_prn": is_prn,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
seen_med_ids.add(med["id"])
|
seen_med_ids.add(med["id"])
|
||||||
|
|
||||||
# Late night pass (22:00+): include next-day meds scheduled 00:00-02:00
|
# Late night pass (22:00+): include next-day meds scheduled 00:00-02:00
|
||||||
@@ -545,16 +415,14 @@ def register(app):
|
|||||||
and _log_local_date(log.get("created_at"), user_tz) == tomorrow_str
|
and _log_local_date(log.get("created_at"), user_tz) == tomorrow_str
|
||||||
]
|
]
|
||||||
|
|
||||||
result.append(
|
result.append({
|
||||||
{
|
|
||||||
"medication": med,
|
"medication": med,
|
||||||
"scheduled_times": early_times,
|
"scheduled_times": early_times,
|
||||||
"taken_times": tomorrow_taken,
|
"taken_times": tomorrow_taken,
|
||||||
"skipped_times": tomorrow_skipped,
|
"skipped_times": tomorrow_skipped,
|
||||||
"is_prn": False,
|
"is_prn": False,
|
||||||
"is_next_day": True,
|
"is_next_day": True,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
seen_med_ids.add(med["id"])
|
seen_med_ids.add(med["id"])
|
||||||
|
|
||||||
# Early morning pass (<02:00): include previous-day meds scheduled 22:00-23:59
|
# Early morning pass (<02:00): include previous-day meds scheduled 22:00-23:59
|
||||||
@@ -592,16 +460,14 @@ def register(app):
|
|||||||
and _log_local_date(log.get("created_at"), user_tz) == yesterday_str
|
and _log_local_date(log.get("created_at"), user_tz) == yesterday_str
|
||||||
]
|
]
|
||||||
|
|
||||||
result.append(
|
result.append({
|
||||||
{
|
|
||||||
"medication": med,
|
"medication": med,
|
||||||
"scheduled_times": late_times,
|
"scheduled_times": late_times,
|
||||||
"taken_times": yesterday_taken,
|
"taken_times": yesterday_taken,
|
||||||
"skipped_times": yesterday_skipped,
|
"skipped_times": yesterday_skipped,
|
||||||
"is_prn": False,
|
"is_prn": False,
|
||||||
"is_previous_day": True,
|
"is_previous_day": True,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
seen_med_ids.add(med["id"])
|
seen_med_ids.add(med["id"])
|
||||||
|
|
||||||
return flask.jsonify(result), 200
|
return flask.jsonify(result), 200
|
||||||
@@ -615,9 +481,7 @@ def register(app):
|
|||||||
if not user_uuid:
|
if not user_uuid:
|
||||||
return flask.jsonify({"error": "unauthorized"}), 401
|
return flask.jsonify({"error": "unauthorized"}), 401
|
||||||
num_days = flask.request.args.get("days", 30, type=int)
|
num_days = flask.request.args.get("days", 30, type=int)
|
||||||
meds = postgres.select(
|
meds = postgres.select("medications", where={"user_uuid": user_uuid, "active": True})
|
||||||
"medications", where={"user_uuid": user_uuid, "active": True}
|
|
||||||
)
|
|
||||||
now = tz.user_now()
|
now = tz.user_now()
|
||||||
user_tz = now.tzinfo
|
user_tz = now.tzinfo
|
||||||
today = now.date()
|
today = now.date()
|
||||||
@@ -641,8 +505,7 @@ def register(app):
|
|||||||
else:
|
else:
|
||||||
adherence_pct = 0
|
adherence_pct = 0
|
||||||
|
|
||||||
result.append(
|
result.append({
|
||||||
{
|
|
||||||
"medication_id": med["id"],
|
"medication_id": med["id"],
|
||||||
"name": med["name"],
|
"name": med["name"],
|
||||||
"taken": taken,
|
"taken": taken,
|
||||||
@@ -650,8 +513,7 @@ def register(app):
|
|||||||
"expected": expected,
|
"expected": expected,
|
||||||
"adherence_percent": adherence_pct,
|
"adherence_percent": adherence_pct,
|
||||||
"is_prn": is_prn,
|
"is_prn": is_prn,
|
||||||
}
|
})
|
||||||
)
|
|
||||||
return flask.jsonify(result), 200
|
return flask.jsonify(result), 200
|
||||||
|
|
||||||
@app.route("/api/medications/<med_id>/adherence", methods=["GET"])
|
@app.route("/api/medications/<med_id>/adherence", methods=["GET"])
|
||||||
@@ -685,8 +547,7 @@ def register(app):
|
|||||||
else:
|
else:
|
||||||
adherence_pct = 0
|
adherence_pct = 0
|
||||||
|
|
||||||
return flask.jsonify(
|
return flask.jsonify({
|
||||||
{
|
|
||||||
"medication_id": med_id,
|
"medication_id": med_id,
|
||||||
"name": med["name"],
|
"name": med["name"],
|
||||||
"taken": taken,
|
"taken": taken,
|
||||||
@@ -694,8 +555,7 @@ def register(app):
|
|||||||
"expected": expected,
|
"expected": expected,
|
||||||
"adherence_percent": adherence_pct,
|
"adherence_percent": adherence_pct,
|
||||||
"is_prn": is_prn,
|
"is_prn": is_prn,
|
||||||
}
|
}), 200
|
||||||
), 200
|
|
||||||
|
|
||||||
# ── Refills ───────────────────────────────────────────────────
|
# ── Refills ───────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -715,9 +575,7 @@ def register(app):
|
|||||||
updates = {k: v for k, v in data.items() if k in allowed}
|
updates = {k: v for k, v in data.items() if k in allowed}
|
||||||
if not updates:
|
if not updates:
|
||||||
return flask.jsonify({"error": "no valid fields to update"}), 400
|
return flask.jsonify({"error": "no valid fields to update"}), 400
|
||||||
result = postgres.update(
|
result = postgres.update("medications", updates, {"id": med_id, "user_uuid": user_uuid})
|
||||||
"medications", updates, {"id": med_id, "user_uuid": user_uuid}
|
|
||||||
)
|
|
||||||
return flask.jsonify(result[0] if result else {}), 200
|
return flask.jsonify(result[0] if result else {}), 200
|
||||||
|
|
||||||
@app.route("/api/medications/refills-due", methods=["GET"])
|
@app.route("/api/medications/refills-due", methods=["GET"])
|
||||||
|
|||||||
@@ -4,10 +4,190 @@ Medications command handler - bot-side hooks for medication management
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import re
|
import re
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
from bot.command_registry import register_module
|
from bot.command_registry import register_module
|
||||||
import ai.parser as ai_parser
|
import ai.parser as ai_parser
|
||||||
|
|
||||||
|
|
||||||
|
def _get_nearest_scheduled_time(times, user_tz=None):
|
||||||
|
"""Find the nearest scheduled time in user's timezone.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times: List of time strings like ["08:00", "20:00"]
|
||||||
|
user_tz: pytz timezone object for the user
|
||||||
|
|
||||||
|
Returns the time as HH:MM string, or None if no times provided.
|
||||||
|
"""
|
||||||
|
if not times:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get current time in user's timezone
|
||||||
|
if user_tz is None:
|
||||||
|
# Default to UTC if no timezone provided
|
||||||
|
user_tz = timezone.utc
|
||||||
|
now = datetime.now(user_tz)
|
||||||
|
now_minutes = now.hour * 60 + now.minute
|
||||||
|
|
||||||
|
# Find the time closest to now (within ±4 hours window)
|
||||||
|
best_time = None
|
||||||
|
best_diff = float("inf")
|
||||||
|
window_minutes = 4 * 60 # 4 hour window
|
||||||
|
|
||||||
|
for time_str in times:
|
||||||
|
try:
|
||||||
|
# Parse time string (e.g., "12:00")
|
||||||
|
hour, minute = map(int, time_str.split(":"))
|
||||||
|
time_minutes = hour * 60 + minute
|
||||||
|
|
||||||
|
# Calculate difference
|
||||||
|
diff = abs(time_minutes - now_minutes)
|
||||||
|
|
||||||
|
# Only consider times within the window
|
||||||
|
if diff <= window_minutes and diff < best_diff:
|
||||||
|
best_diff = diff
|
||||||
|
best_time = time_str
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If no time within window, use the most recent past time
|
||||||
|
if best_time is None:
|
||||||
|
# Get current time in user's timezone again for reference
|
||||||
|
now = datetime.now(user_tz)
|
||||||
|
now_minutes = now.hour * 60 + now.minute
|
||||||
|
|
||||||
|
# Find the most recent past time
|
||||||
|
past_times = []
|
||||||
|
for time_str in times:
|
||||||
|
try:
|
||||||
|
hour, minute = map(int, time_str.split(":"))
|
||||||
|
time_minutes = hour * 60 + minute
|
||||||
|
|
||||||
|
# If time is in the past today, calculate minutes since then
|
||||||
|
if time_minutes <= now_minutes:
|
||||||
|
past_times.append((time_minutes, time_str))
|
||||||
|
else:
|
||||||
|
# If time is in the future, consider it as yesterday's time
|
||||||
|
past_times.append((time_minutes - 24 * 60, time_str))
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if past_times:
|
||||||
|
# Find the time closest to now (most recent past)
|
||||||
|
past_times.sort()
|
||||||
|
best_time = past_times[-1][1]
|
||||||
|
|
||||||
|
return best_time
|
||||||
|
if not best_time:
|
||||||
|
for time_str in times:
|
||||||
|
try:
|
||||||
|
hour, minute = map(int, time_str.split(":"))
|
||||||
|
time_minutes = hour * 60 + minute
|
||||||
|
|
||||||
|
# If this time was earlier today, it's a candidate
|
||||||
|
if time_minutes <= now_minutes:
|
||||||
|
diff = now_minutes - time_minutes
|
||||||
|
if diff < best_diff:
|
||||||
|
best_diff = diff
|
||||||
|
best_time = time_str
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If still no time found, use the first scheduled time
|
||||||
|
if not best_time and times:
|
||||||
|
best_time = times[0]
|
||||||
|
|
||||||
|
return best_time
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_user_timezone(bot, user_id):
|
||||||
|
"""Check if user has timezone set, ask for it if not.
|
||||||
|
|
||||||
|
Returns the timezone string or None if user cancels.
|
||||||
|
"""
|
||||||
|
# Check if user has timezone set
|
||||||
|
user_data = await bot.api.get_user_data(user_id)
|
||||||
|
if user_data and user_data.get('timezone'):
|
||||||
|
return user_data['timezone']
|
||||||
|
|
||||||
|
# Ask user for their timezone
|
||||||
|
await bot.send_dm(user_id, "🕐 I don't have your timezone set yet. Could you please tell me your timezone?\n\n" +
|
||||||
|
"You can provide it in various formats:\n" +
|
||||||
|
"- Timezone name (e.g., 'America/New_York', 'Europe/London')\n" +
|
||||||
|
"- UTC offset (e.g., 'UTC+2', '-05:00')\n" +
|
||||||
|
"- Common abbreviations (e.g., 'EST', 'PST')\n\n" +
|
||||||
|
"Please reply with your timezone and I'll set it up for you!")
|
||||||
|
|
||||||
|
# Wait for user response (simplified - in real implementation this would be more complex)
|
||||||
|
# For now, we'll just return None to indicate we need to handle this differently
|
||||||
|
return None
|
||||||
|
await bot.send_dm(user_id, "I didn't understand that timezone format. Please say something like:\n"
|
||||||
|
'- "UTC-8" or "-8" for Pacific Time\n'
|
||||||
|
'- "UTC+1" or "+1" for Central European Time\n'
|
||||||
|
'- "PST", "EST", "CST", "MST" for US timezones')
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check if user has timezone set in preferences
|
||||||
|
resp, status = api_request("get", "/api/preferences", token)
|
||||||
|
if status == 200 and resp:
|
||||||
|
offset = resp.get("timezone_offset")
|
||||||
|
if offset is not None:
|
||||||
|
return offset
|
||||||
|
|
||||||
|
# No timezone set - need to ask user
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_timezone(user_input):
|
||||||
|
"""Parse timezone string to offset in minutes.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
"UTC-8" -> 480 (8 hours behind UTC)
|
||||||
|
"-8" -> 480
|
||||||
|
"PST" -> 480
|
||||||
|
"EST" -> 300
|
||||||
|
"+5:30" -> -330
|
||||||
|
|
||||||
|
Returns offset in minutes (positive = behind UTC) or None if invalid.
|
||||||
|
"""
|
||||||
|
user_input = user_input.strip().upper()
|
||||||
|
|
||||||
|
# Common timezone abbreviations
|
||||||
|
tz_map = {
|
||||||
|
"PST": 480,
|
||||||
|
"PDT": 420,
|
||||||
|
"MST": 420,
|
||||||
|
"MDT": 360,
|
||||||
|
"CST": 360,
|
||||||
|
"CDT": 300,
|
||||||
|
"EST": 300,
|
||||||
|
"EDT": 240,
|
||||||
|
"GMT": 0,
|
||||||
|
"UTC": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if user_input in tz_map:
|
||||||
|
return tz_map[user_input]
|
||||||
|
|
||||||
|
# Try to parse offset format
|
||||||
|
# Remove UTC prefix if present
|
||||||
|
if user_input.startswith("UTC"):
|
||||||
|
user_input = user_input[3:]
|
||||||
|
|
||||||
|
# Match patterns like -8, +5, -5:30, +5:30
|
||||||
|
match = re.match(r"^([+-])?(\d+)(?::(\d+))?$", user_input)
|
||||||
|
if match:
|
||||||
|
sign = -1 if match.group(1) == "-" else 1
|
||||||
|
hours = int(match.group(2))
|
||||||
|
minutes = int(match.group(3)) if match.group(3) else 0
|
||||||
|
|
||||||
|
# Convert to offset minutes (positive = behind UTC)
|
||||||
|
# If user says UTC-8, they're 8 hours BEHIND UTC, so offset is +480
|
||||||
|
offset_minutes = (hours * 60 + minutes) * sign * -1
|
||||||
|
return offset_minutes
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def _get_scheduled_time_from_context(message, med_name):
|
async def _get_scheduled_time_from_context(message, med_name):
|
||||||
"""Fetch recent messages and extract scheduled time from medication reminder.
|
"""Fetch recent messages and extract scheduled time from medication reminder.
|
||||||
|
|
||||||
@@ -164,22 +344,48 @@ async def handle_medication(message, session, parsed):
|
|||||||
await message.channel.send("Which medication did you take?")
|
await message.channel.send("Which medication did you take?")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check if we have user's timezone
|
||||||
|
timezone_offset = await _get_user_timezone(message, session, token)
|
||||||
|
|
||||||
|
if timezone_offset is None:
|
||||||
|
# Need to ask for timezone first
|
||||||
|
if "pending_confirmations" not in session:
|
||||||
|
session["pending_confirmations"] = {}
|
||||||
|
|
||||||
|
# Store that we're waiting for timezone, along with the med info
|
||||||
|
session["pending_confirmations"]["timezone"] = {
|
||||||
|
"action": "take",
|
||||||
|
"medication_id": med_id,
|
||||||
|
"name": name,
|
||||||
|
}
|
||||||
|
|
||||||
|
await message.channel.send(
|
||||||
|
"📍 I need to know your timezone to track medications correctly.\n\n"
|
||||||
|
'What timezone are you in? (e.g., "UTC-8", "PST", "EST", "+1")'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Try to get scheduled time from recent reminder context
|
# Try to get scheduled time from recent reminder context
|
||||||
scheduled_time = await _get_scheduled_time_from_context(message, name)
|
scheduled_time = await _get_scheduled_time_from_context(message, name)
|
||||||
|
|
||||||
|
# If not found in context, calculate from medication schedule
|
||||||
|
if not scheduled_time:
|
||||||
|
# Get medication details to find scheduled times
|
||||||
|
med_resp, med_status = api_request(
|
||||||
|
"get", f"/api/medications/{med_id}", token
|
||||||
|
)
|
||||||
|
if med_status == 200 and med_resp:
|
||||||
|
times = med_resp.get("times", [])
|
||||||
|
scheduled_time = _get_nearest_scheduled_time(times, timezone_offset)
|
||||||
|
|
||||||
# Build request body with scheduled_time if found
|
# Build request body with scheduled_time if found
|
||||||
request_body = {}
|
request_body = {}
|
||||||
if scheduled_time:
|
if scheduled_time:
|
||||||
request_body["scheduled_time"] = scheduled_time
|
request_body["scheduled_time"] = scheduled_time
|
||||||
|
|
||||||
print(
|
|
||||||
f"[DEBUG] About to call API: POST /api/medications/{med_id}/take with body: {request_body}",
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
resp, status = api_request(
|
resp, status = api_request(
|
||||||
"post", f"/api/medications/{med_id}/take", token, request_body
|
"post", f"/api/medications/{med_id}/take", token, request_body
|
||||||
)
|
)
|
||||||
print(f"[DEBUG] API response: status={status}, resp={resp}", flush=True)
|
|
||||||
if status == 201:
|
if status == 201:
|
||||||
if scheduled_time:
|
if scheduled_time:
|
||||||
await message.channel.send(
|
await message.channel.send(
|
||||||
|
|||||||
Reference in New Issue
Block a user