AidAILine / appointment_tracker.py
J-Barrert's picture
Upload 14 files
585cfd5 verified
Raw
History Blame Contribute Delete
9.3 kB
"""
appointment_tracker.py — CRUD + lifecycle for appointments.json
Each record is scoped to a care profile via profile_id.
"""
from __future__ import annotations
import json, os, uuid
from datetime import date, datetime, timedelta
from typing import Any, Dict, List, Optional
from config import APPOINTMENTS_JSON
def _default_recurrence() -> Dict[str, Any]:
return {"frequency": "none", "interval": 1,
"end_type": "never", "end_after": 1, "end_date": ""}
def _migrate(appt: Dict[str, Any]) -> Dict[str, Any]:
if "recurrence" not in appt:
rec = _default_recurrence()
if appt.get("is_recurring"):
rec["frequency"] = "weekly"
appt["recurrence"] = rec
if "status" not in appt:
appt["status"] = "scheduled"
if "completed_at" not in appt:
appt["completed_at"] = ""
return appt
def _load() -> List[Dict[str, Any]]:
if APPOINTMENTS_JSON.exists():
try:
with open(APPOINTMENTS_JSON, "r", encoding="utf-8") as f:
return [_migrate(a) for a in json.load(f)]
except Exception:
return []
return []
def _save(appts: List[Dict[str, Any]]):
APPOINTMENTS_JSON.parent.mkdir(parents=True, exist_ok=True)
with open(APPOINTMENTS_JSON, "w", encoding="utf-8") as f:
json.dump(appts, f, indent=2, ensure_ascii=False)
f.flush()
os.fsync(f.fileno())
def _now() -> str:
return datetime.now().isoformat(timespec="seconds")
def _today_str() -> str:
return date.today().isoformat()
def _days_until(date_str: str) -> int:
try:
return (date.fromisoformat(date_str) - date.today()).days
except Exception:
return 9999
def _for_profile(appts: List[Dict[str, Any]], profile_id: Optional[str]) -> List[Dict[str, Any]]:
if not profile_id:
return []
return [a for a in appts if a.get("profile_id") == profile_id]
def _is_in_current_window(appt: Dict[str, Any]) -> bool:
try:
now = datetime.now()
appt_date = date.fromisoformat(appt["date"])
time_str = appt.get("time", "00:00").strip()
try:
appt_time = datetime.strptime(time_str[:8], "%I:%M %p").time()
except ValueError:
try:
appt_time = datetime.strptime(time_str[:5], "%H:%M").time()
except ValueError:
appt_time = datetime.strptime("00:00", "%H:%M").time()
appt_dt = datetime.combine(appt_date, appt_time)
return appt_dt <= now <= (appt_dt + timedelta(hours=3))
except Exception:
return False
def run_lifecycle_check(profile_id: Optional[str] = None) -> int:
appts = _load()
count = 0
for appt in appts:
if profile_id and appt.get("profile_id") != profile_id:
continue
status = appt.get("status", "scheduled")
if status == "scheduled" and _is_in_current_window(appt):
appt["status"] = "current"
count += 1
elif status == "current" and not _is_in_current_window(appt):
appt["status"] = "completed"
appt["completed_at"] = _now()
count += 1
if count:
_save(appts)
return count
def add_appointment(
title: str,
appt_date: str,
appt_time: str = "",
location: str = "",
notes: str = "",
reminders: Optional[List[int]] = None,
recurrence: Optional[Dict[str, Any]] = None,
profile_id: str = "",
) -> Dict[str, Any]:
if not profile_id:
raise ValueError("No active care profile.")
if not title.strip():
raise ValueError("Appointment title is required.")
if not appt_date.strip():
raise ValueError("Appointment date is required.")
try:
date.fromisoformat(appt_date.strip())
except ValueError:
raise ValueError(f"Invalid date: {appt_date!r}. Use YYYY-MM-DD.")
appt = {
"id": str(uuid.uuid4()),
"profile_id": profile_id,
"title": title.strip(),
"date": appt_date.strip(),
"time": appt_time.strip(),
"location": location.strip(),
"notes": notes.strip(),
"reminders": reminders or [],
"recurrence": recurrence if recurrence is not None else _default_recurrence(),
"status": "scheduled",
"completed_at": "",
"created_at": _now(),
"updated_at": _now(),
}
appts = _load()
appts.append(appt)
_save(appts)
return appt
def edit_appointment(
appt_id: str,
title: Optional[str] = None,
appt_date: Optional[str] = None,
appt_time: Optional[str] = None,
location: Optional[str] = None,
notes: Optional[str] = None,
reminders: Optional[List[int]] = None,
recurrence: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
appts = _load()
for appt in appts:
if appt["id"] == appt_id:
if title is not None: appt["title"] = title.strip()
if appt_date is not None: appt["date"] = appt_date.strip()
if appt_time is not None: appt["time"] = appt_time.strip()
if location is not None: appt["location"] = location.strip()
if notes is not None: appt["notes"] = notes.strip()
if reminders is not None: appt["reminders"] = reminders
if recurrence is not None: appt["recurrence"] = recurrence
appt["updated_at"] = _now()
_save(appts)
return appt
return None
def complete_appointment(appt_id: str) -> bool:
appts = _load()
for appt in appts:
if appt["id"] == appt_id:
appt["status"] = "completed"
appt["completed_at"] = _now()
appt["updated_at"] = _now()
_save(appts)
return True
return False
def delete_appointment(appt_id: str) -> bool:
appts = _load()
new_appts = [a for a in appts if a["id"] != appt_id]
if len(new_appts) == len(appts):
return False
_save(new_appts)
return True
def get_all_appointments(include_past: bool = False, profile_id: Optional[str] = None) -> List[Dict[str, Any]]:
today = _today_str()
appts = [a for a in _for_profile(_load(), profile_id) if a.get("status") != "completed"]
if not include_past:
appts = [a for a in appts if a.get("date", "") >= today]
appts.sort(key=lambda a: (a.get("date", ""), a.get("time", "")))
return appts
def get_completed_appointments(profile_id: Optional[str] = None) -> List[Dict[str, Any]]:
appts = [a for a in _for_profile(_load(), profile_id) if a.get("status") == "completed"]
appts.sort(key=lambda a: a.get("completed_at", ""), reverse=True)
return appts
def has_conflicting_appointment(
appt_date: str,
appt_time: str,
exclude_id: str = None,
profile_id: Optional[str] = None,
) -> bool:
for a in _for_profile(_load(), profile_id):
if a.get("status") == "completed":
continue
if exclude_id and a.get("id") == exclude_id:
continue
if a.get("date") == appt_date and a.get("time") == appt_time:
return True
return False
def get_recent_completed(days: int = 30, profile_id: Optional[str] = None) -> List[Dict[str, Any]]:
cutoff_dt = datetime.now() - timedelta(days=days)
cutoff = cutoff_dt.isoformat(timespec="seconds")
appts = [a for a in _for_profile(_load(), profile_id)
if a.get("status") == "completed" and a.get("completed_at", "") >= cutoff]
appts.sort(key=lambda a: a.get("completed_at", ""), reverse=True)
return appts
def get_today(profile_id: Optional[str] = None) -> List[Dict[str, Any]]:
today = _today_str()
appts = [a for a in _for_profile(_load(), profile_id)
if a.get("date", "") == today and a.get("status") != "completed"]
appts.sort(key=lambda a: a.get("time", ""))
return appts
def get_upcoming(profile_id: Optional[str] = None) -> List[Dict[str, Any]]:
today = _today_str()
appts = [a for a in _for_profile(_load(), profile_id)
if a.get("date", "") >= today and a.get("status") != "completed"]
appts.sort(key=lambda a: (a.get("date", ""), a.get("time", "")))
return appts
def get_appointment_by_id(appt_id: str) -> Optional[Dict[str, Any]]:
for a in _load():
if a["id"] == appt_id:
return a
return None
def get_due_reminders(profile_id: Optional[str] = None) -> List[Dict[str, Any]]:
results = []
for appt in _for_profile(_load(), profile_id):
if appt.get("status") == "completed":
continue
days = _days_until(appt.get("date", ""))
if days >= 0 and days in appt.get("reminders", []):
results.append({**appt, "days_until": days})
return results
def delete_for_profile(profile_id: str) -> int:
appts = _load()
kept = [a for a in appts if a.get("profile_id") != profile_id]
deleted = len(appts) - len(kept)
if deleted:
_save(kept)
return deleted