| """ |
| doc_forms.py β Doctor visit template generator. |
| |
| Pulls current medications and allergies from their respective trackers, |
| combines with the active profile from the multi-profile engine, and |
| generates a formatted visit summary suitable for printing or sharing. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from datetime import datetime |
| from typing import Iterable, List, Optional, Tuple |
|
|
| from profiles import get_profile, get_profiles, compose_address, address_parts |
| from med_tracker import get_medications |
| from food_tracker import get_food |
|
|
|
|
| |
|
|
| def _resolve_profile(profile_id: str = "") -> Tuple[dict, str]: |
| """Resolve the active profile dict from the multi-profile engine. |
| |
| Returns (profile_dict, note). The note is a non-empty warning string |
| when the resolver had to fall back (empty id, id not found, or no |
| profiles exist). Caller should render the note so the user knows |
| the brief is incomplete. |
| |
| Fallback order: |
| 1. profile_id (if provided and found) |
| 2. first available profile |
| 3. empty dict (no profiles exist) |
| """ |
| if profile_id: |
| p = get_profile(profile_id) |
| if p: |
| return p, "" |
| all_p = get_profiles() or [] |
| if all_p: |
| first = all_p[0] |
| label = first.get("label") or first.get("full_name") or "first available" |
| return first, f"β οΈ No profile selected β using '{label}'." |
| return {}, "β οΈ No profiles exist. Create one in βοΈ Settings & Profiles." |
|
|
|
|
| def _resolve_emergency_contacts(profile: dict) -> list[dict]: |
| """Return the list of emergency contacts to render in the brief. |
| |
| Prefers the new `emergency_contacts` list. Falls back to the legacy |
| single-contact fields if the list is empty (so old profiles still |
| render correctly). Filters out entries that have neither a name |
| nor a phone. |
| """ |
| raw = profile.get("emergency_contacts") or [] |
| if isinstance(raw, list) and raw: |
| return [c for c in raw |
| if isinstance(c, dict) and (c.get("name") or c.get("phone"))] |
| |
| name = (profile.get("emergency_contact_name") or "").strip() |
| phone = (profile.get("emergency_contact_phone") or "").strip() |
| if name or phone: |
| return [{"name": name, "phone": phone, "relationship": ""}] |
| return [] |
|
|
|
|
| |
|
|
| def generate_visit_template(reason_for_visit: str = "", profile_id: str = "") -> str: |
| """ |
| Generate a formatted doctor-visit summary as plain text. |
| |
| reason_for_visit: Free text entered by the caregiver for this visit. |
| profile_id: UUID of the active patient profile. Falls back to the |
| first available profile if empty/invalid. |
| Returns a string suitable for display in a Gradio Textbox or export. |
| """ |
| p, note = _resolve_profile(profile_id) |
| meds = get_medications(filter="current", profile_id=profile_id or None) |
| food_data = get_food(profile_id=profile_id or None) |
|
|
| allergies = [e["name"] for e in food_data.get("allergies", [])] |
| today = datetime.now().strftime("%B %d, %Y") |
|
|
| lines = [] |
|
|
| |
| lines.append("=" * 60) |
| lines.append(" PRE-VISIT FORM") |
| lines.append("=" * 60) |
| lines.append(f"Date: {today}") |
| if note: |
| lines.append(note) |
| lines.append("") |
|
|
| |
| lines.append("PATIENT INFORMATION") |
| lines.append("-" * 40) |
| lines.append(f"Name: {p.get('full_name') or '(Not set β add in Settings & Profiles)'}") |
| lines.append(f"Date of Birth: {p.get('dob') or '(Not set β add in Settings & Profiles)'}") |
| _patient_addr = compose_address(*address_parts(p)) or "(Not set β add in Settings & Profiles)" |
| lines.append(f"Address: {_patient_addr.replace(chr(10), ', ')}") |
| lines.append(f"Phone: {p.get('phone_number') or '(Not set β add in Settings & Profiles)'}") |
| lines.append(f"Email: {p.get('email') or '(Not set β add in Settings & Profiles)'}") |
| |
| ins_provider = p.get("insurance_provider") or "" |
| ins_policy = p.get("policy_id") or "" |
| ins_group = p.get("group_id") or "" |
| if ins_provider or ins_policy or ins_group: |
| ins_line = f"Insurance: {ins_provider or '(provider not set)'}" |
| if ins_policy or ins_group: |
| ins_line += f" [Policy: {ins_policy or 'β'} / Group: {ins_group or 'β'}]" |
| lines.append(ins_line) |
| else: |
| lines.append("Insurance: (Not set β add in Settings & Profiles)") |
| lines.append("") |
|
|
| |
| lines.append("PRIMARY CARE PROVIDER") |
| lines.append("-" * 40) |
| if p.get("pcp_name") or p.get("pcp_clinic"): |
| lines.append(f"Provider: {p.get('pcp_name') or '(Not set)'}") |
| lines.append(f"Clinic: {p.get('pcp_clinic') or '(Not set)'}") |
| _pcp_addr = compose_address(*address_parts(p, pcp=True)) or "(Not set)" |
| lines.append(f"Address: {_pcp_addr.replace(chr(10), ', ')}") |
| lines.append(f"Phone: {p.get('pcp_phone') or '(Not set)'}") |
| else: |
| lines.append("(Not set β add in Settings & Profiles)") |
| lines.append("") |
|
|
| |
| lines.append("EMERGENCY CONTACT(S)") |
| lines.append("-" * 40) |
| contacts = _resolve_emergency_contacts(p) |
| if contacts: |
| for i, c in enumerate(contacts, start=1): |
| rel = c.get("relationship") or "" |
| rel_suffix = f" ({rel})" if rel else "" |
| lines.append(f"Contact {i}: {c.get('name') or '(name not set)'}{rel_suffix}") |
| lines.append(f"Phone: {c.get('phone') or '(phone not set)'}") |
| if i < len(contacts): |
| lines.append("") |
| else: |
| lines.append("(Not set β add in Settings & Profiles)") |
| lines.append("") |
|
|
| |
| lines.append("REASON FOR VISIT") |
| lines.append("-" * 40) |
| if reason_for_visit.strip(): |
| lines.append(reason_for_visit.strip()) |
| else: |
| lines.append("(Enter reason for visit in the field above)") |
| lines.append("") |
|
|
| |
| lines.append("CURRENT MEDICATIONS") |
| lines.append("-" * 40) |
| if meds: |
| for med in meds: |
| med_line = f"β’ {med['name']}" |
| if med.get("dosage"): |
| med_line += f" β {med['dosage']}" |
| if med.get("frequency"): |
| med_line += f" ({med['frequency']})" |
| lines.append(med_line) |
| if med.get("side_effects"): |
| lines.append(f" Side effects: {med['side_effects']}") |
| if med.get("personal_notes"): |
| lines.append(f" Notes: {med['personal_notes']}") |
| else: |
| lines.append("(No current medications on file)") |
| lines.append("") |
|
|
| |
| lines.append("KNOWN ALLERGIES") |
| lines.append("-" * 40) |
| if allergies: |
| for allergy in allergies: |
| lines.append(f"β {allergy}") |
| else: |
| lines.append("(No known allergies on file)") |
| lines.append("") |
|
|
| |
| lines.append("DOCTOR'S NOTES") |
| lines.append("-" * 40) |
| lines.append("") |
| lines.append("") |
| lines.append("") |
| lines.append("") |
| lines.append("(space for handwritten or dictated notes)") |
| lines.append("") |
|
|
| |
| lines.append("FOLLOW-UP / NEXT STEPS") |
| lines.append("-" * 40) |
| lines.append("") |
| lines.append("") |
| lines.append("") |
|
|
| lines.append("=" * 60) |
| lines.append("Generated by AidAiLine") |
| lines.append("Build Small Hackathon β Backyard AI Track") |
| lines.append("=" * 60) |
|
|
| return "\n".join(lines) |
|
|
|
|
| def generate_visit_brief_markdown( |
| profile_id: str = "", |
| clinic_doctor: str = "", |
| reason: str = "", |
| concerns: str = "", |
| symptoms: Optional[Iterable[str]] = None, |
| symptom_other: str = "", |
| symptom_notes: str = "", |
| questions: Optional[Iterable[str]] = None, |
| inc_meds: bool = True, |
| inc_allergies: bool = True, |
| inc_history: bool = True, |
| inc_insurance: bool = True, |
| ) -> str: |
| """ |
| Compile a professional markdown pre-visit brief from form inputs + |
| auto-pulled data from the active profile (multi-profile engine) and |
| the medications/food trackers. |
| |
| profile_id: UUID of the active patient profile. Falls back to the |
| first available profile if empty/invalid. |
| Care Mode and Caregiver Name are read from the profile itself |
| (moved off the appointment form so they belong to the |
| patient, not the visit). |
| symptoms: list of checked symptoms (may contain "Other") |
| symptom_other: free-text companion when "Other" is checked |
| questions: list of question strings (one per bullet) |
| inc_insurance: when False, hides the Insurance row in Demographics |
| (other Patient Demographics fields still render) |
| """ |
| symptoms = list(symptoms or []) |
| questions = list(questions or []) |
|
|
| p, profile_note = _resolve_profile(profile_id) |
| care_mode = p.get("care_mode", "Managing someone else's care") or "Managing someone else's care" |
| caregiver_name = p.get("caregiver_name", "") or "" |
| is_self_care = care_mode == "Managing my own care" |
| today = datetime.now().strftime("%B %d, %Y") |
|
|
| L: List[str] = [] |
|
|
| |
| L.append("# π₯ Patient Pre-Visit Intake Brief") |
| L.append("") |
| L.append(f"**Date:** {today}") |
| L.append("") |
| if profile_note: |
| L.append(f"_{profile_note}_") |
| L.append("") |
| L.append("---") |
| L.append("") |
|
|
| |
| L.append("## π€ Patient Demographics") |
| L.append("") |
| L.append(f"- **Legal Name:** {p.get('full_name') or '_(Not set β add in Settings & Profiles)_'}") |
| L.append(f"- **Date of Birth:** {p.get('dob') or '_(Not set β add in Settings & Profiles)_'}") |
| _patient_addr = compose_address(*address_parts(p)) or "_(Not set β add in Settings & Profiles)_" |
| L.append(f"- **Address:** {_patient_addr.replace(chr(10), ', ')}") |
| L.append(f"- **Phone:** {p.get('phone_number') or '_(Not set β add in Settings & Profiles)_'}") |
| L.append(f"- **Email:** {p.get('email') or '_(Not set β add in Settings & Profiles)_'}") |
| if inc_insurance: |
| ins_provider = p.get("insurance_provider") or "" |
| ins_policy = p.get("policy_id") or "" |
| ins_group = p.get("group_id") or "" |
| if ins_provider or ins_policy or ins_group: |
| L.append(f"- **Insurance:** {ins_provider or '_(provider not set)_'}") |
| if ins_policy or ins_group: |
| L.append(f" - Policy #: {ins_policy or '_(not set)_'}") |
| L.append(f" - Group #: {ins_group or '_(not set)_'}") |
| else: |
| L.append("- **Insurance:** _(Not set β add in Settings & Profiles)_") |
| L.append("") |
|
|
| |
| L.append("## π©Ί Primary Care Provider") |
| L.append("") |
| if p.get("pcp_name") or p.get("pcp_clinic"): |
| L.append(f"- **Provider:** {p.get('pcp_name') or '_(Not set)_'}") |
| L.append(f"- **Clinic:** {p.get('pcp_clinic') or '_(Not set)_'}") |
| _pcp_addr = compose_address(*address_parts(p, pcp=True)) or "_(Not set)_" |
| L.append(f"- **Address:** {_pcp_addr.replace(chr(10), ', ')}") |
| L.append(f"- **Phone:** {p.get('pcp_phone') or '_(Not set)_'}") |
| else: |
| L.append("_(Not set β add in Settings & Profiles)_") |
| L.append("") |
|
|
| |
| L.append("## π Emergency Contact(s)") |
| L.append("") |
| contacts = _resolve_emergency_contacts(p) |
| if contacts: |
| for i, c in enumerate(contacts, start=1): |
| rel = c.get("relationship") or "" |
| rel_suffix = f" *({rel})*" if rel else "" |
| L.append(f"- **Contact {i} β {c.get('name') or '_(name not set)_'}**{rel_suffix}") |
| L.append(f" - Phone: {c.get('phone') or '_(phone not set)_'}") |
| if i < len(contacts): |
| L.append("") |
| else: |
| L.append("_(Not set β add in Settings & Profiles)_") |
| L.append("") |
|
|
| |
| L.append("## π§ββοΈ Visit Meta") |
| L.append("") |
| if not is_self_care: |
| L.append(f"- **Caregiver:** {caregiver_name.strip() or '_(Not provided)_'}") |
| L.append(f"- **Targeted Clinic / Doctor:** {clinic_doctor.strip() or '_(Not specified)_'}") |
| L.append("") |
| L.append("---") |
| L.append("") |
|
|
| |
| L.append("## π― Section A: Clinical Target") |
| L.append("") |
| L.append("> **Reason for Visit:** " + (reason or "_(Not specified)_")) |
| L.append(">") |
| if concerns.strip(): |
| for line in concerns.splitlines(): |
| line = line.strip() |
| if line: |
| L.append(f"> β’ {line}") |
| L.append("") |
| L.append("---") |
| L.append("") |
|
|
| |
| L.append("## π Section B: Current Care Summary") |
| L.append("") |
|
|
| if inc_meds: |
| meds = get_medications(filter="current", profile_id=profile_id or None) |
| L.append("### Current Medications") |
| L.append("") |
| if meds: |
| for m in meds: |
| line = f"- **{m['name']}**" |
| if m.get("dosage"): |
| line += f" β {m['dosage']}" |
| if m.get("frequency"): |
| line += f" *({m['frequency']})*" |
| L.append(line) |
| if m.get("side_effects"): |
| L.append(f" - Side effects: {m['side_effects']}") |
| if m.get("personal_notes"): |
| L.append(f" - Notes: {m['personal_notes']}") |
| else: |
| L.append("_(No current medications on file)_") |
| L.append("") |
|
|
| if inc_allergies: |
| food_data = get_food(profile_id=profile_id or None) |
| allergies = food_data.get("allergies", []) |
| aversions = food_data.get("disliked_foods", []) |
| L.append("### Allergies & Food Aversions") |
| L.append("") |
| if allergies or aversions: |
| for a in allergies: |
| sev = a.get("severity", "β") |
| L.append(f"- β οΈ **{a['name']}** β severity: `{sev}`") |
| for a in aversions: |
| L.append(f"- β {a['name']}") |
| else: |
| L.append("_(No known allergies or aversions on file)_") |
| L.append("") |
|
|
| if inc_history: |
| L.append("### Recent Vitals / History") |
| L.append("") |
| L.append("_For full lab results and clinical notes, see the **π Documents** tab._") |
| L.append("") |
|
|
| L.append("---") |
| L.append("") |
|
|
| |
| section_c_title = ( |
| "## π Section C: Your Observations" |
| if is_self_care |
| else "## π Section C: Caregiver Observations" |
| ) |
| L.append(section_c_title) |
| L.append("") |
|
|
| if symptoms or symptom_other.strip(): |
| L.append("### Symptom Tracker (checked)") |
| L.append("") |
| for s in symptoms: |
| if s == "Other": |
| if symptom_other.strip(): |
| L.append(f"- β **Other:** {symptom_other.strip()}") |
| else: |
| L.append(f"- β Other") |
| else: |
| L.append(f"- β {s}") |
| L.append("") |
| else: |
| L.append("### Symptom Tracker") |
| L.append("") |
| L.append("_(No symptoms flagged this visit)_") |
| L.append("") |
|
|
| if symptom_notes.strip(): |
| L.append("### Symptom Notes (severity / duration / triggers)") |
| L.append("") |
| for line in symptom_notes.splitlines(): |
| line = line.strip() |
| if line: |
| L.append(f"> {line}") |
| L.append("") |
|
|
| if questions: |
| L.append("### β Questions for the Doctor") |
| L.append("") |
| for i, q in enumerate(questions, start=1): |
| L.append(f"{i}. {q}") |
| L.append("") |
|
|
| L.append("---") |
| L.append("") |
| L.append("_Generated by AidAiLine β Build Small Hackathon._") |
|
|
| return "\n".join(L) |
|
|
|
|
| def generate_visit_template_html(reason_for_visit: str = "", profile_id: str = "") -> str: |
| """ |
| Generate an HTML version of the visit template for print/preview. |
| Wraps the plain-text version in print-friendly HTML. |
| """ |
| plain = generate_visit_template(reason_for_visit, profile_id=profile_id) |
| escaped = ( |
| plain |
| .replace("&", "&") |
| .replace("<", "<") |
| .replace(">", ">") |
| ) |
|
|
| return f"""<!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>Pre-Visit Form</title> |
| <style> |
| body {{ |
| font-family: 'Segoe UI', Arial, sans-serif; |
| font-size: 14px; |
| line-height: 1.6; |
| color: #1a202c; |
| max-width: 700px; |
| margin: 40px auto; |
| padding: 0 20px; |
| }} |
| pre {{ |
| white-space: pre-wrap; |
| word-wrap: break-word; |
| font-family: inherit; |
| }} |
| @media print {{ |
| body {{ margin: 20px; }} |
| }} |
| </style> |
| </head> |
| <body> |
| <pre>{escaped}</pre> |
| <script> |
| // Auto-print if opened in print mode |
| if (window.location.search.includes('print=1')) {{ |
| window.print(); |
| }} |
| </script> |
| </body> |
| </html>""" |
|
|