Spaces:
Sleeping
Sleeping
Update api/routes.py
Browse files- api/routes.py +123 -83
api/routes.py
CHANGED
|
@@ -554,7 +554,7 @@ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get
|
|
| 554 |
raise HTTPException(status_code=403, detail="Only clinicians can generate patient PDFs")
|
| 555 |
|
| 556 |
try:
|
| 557 |
-
# Determine if
|
| 558 |
try:
|
| 559 |
obj_id = ObjectId(patient_id)
|
| 560 |
query = { "$or": [ { "_id": obj_id }, { "fhir_id": patient_id } ] }
|
|
@@ -567,86 +567,125 @@ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get
|
|
| 567 |
logger.warning(f"Patient not found: {patient_id}")
|
| 568 |
raise HTTPException(status_code=404, detail="Patient not found")
|
| 569 |
|
| 570 |
-
#
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
\\
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
\\bottomrule
|
| 642 |
-
\\end{{longtable}}
|
| 643 |
-
|
| 644 |
-
\\end{{document}}
|
| 645 |
"""
|
| 646 |
|
| 647 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
with NamedTemporaryFile(suffix=".tex", mode="w", delete=False) as tex_file:
|
| 649 |
-
tex_file.write(
|
| 650 |
tex_path = tex_file.name
|
| 651 |
|
| 652 |
try:
|
|
@@ -655,15 +694,15 @@ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get
|
|
| 655 |
with open(pdf_path, "rb") as f:
|
| 656 |
pdf_bytes = f.read()
|
| 657 |
|
| 658 |
-
# Clean up
|
| 659 |
subprocess.run(["latexmk", "-c", tex_path], check=True)
|
| 660 |
os.remove(tex_path)
|
| 661 |
os.remove(pdf_path)
|
| 662 |
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
|
|
|
| 667 |
|
| 668 |
except subprocess.CalledProcessError as e:
|
| 669 |
logger.error(f"LaTeX compilation failed: {e.stderr}")
|
|
@@ -672,6 +711,7 @@ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get
|
|
| 672 |
except Exception as e:
|
| 673 |
logger.error(f"PDF generation error: {e}")
|
| 674 |
raise HTTPException(status_code=500, detail="Failed to generate PDF")
|
|
|
|
| 675 |
@router.post("/signup", status_code=status.HTTP_201_CREATED)
|
| 676 |
async def signup(data: SignupForm):
|
| 677 |
logger.info(f"Signup attempt for email: {data.email}")
|
|
|
|
| 554 |
raise HTTPException(status_code=403, detail="Only clinicians can generate patient PDFs")
|
| 555 |
|
| 556 |
try:
|
| 557 |
+
# Determine if patient_id is ObjectId or fhir_id
|
| 558 |
try:
|
| 559 |
obj_id = ObjectId(patient_id)
|
| 560 |
query = { "$or": [ { "_id": obj_id }, { "fhir_id": patient_id } ] }
|
|
|
|
| 567 |
logger.warning(f"Patient not found: {patient_id}")
|
| 568 |
raise HTTPException(status_code=404, detail="Patient not found")
|
| 569 |
|
| 570 |
+
# Format LaTeX content using .format() to avoid f-string escape issues
|
| 571 |
+
latex_template = r"""
|
| 572 |
+
\documentclass[a4paper,12pt]{article}
|
| 573 |
+
\usepackage[utf8]{inputenc}
|
| 574 |
+
\usepackage[T1]{fontenc}
|
| 575 |
+
\usepackage{geometry}
|
| 576 |
+
\geometry{margin=1in}
|
| 577 |
+
\usepackage{booktabs,longtable,fancyhdr}
|
| 578 |
+
\pagestyle{fancy}
|
| 579 |
+
\fancyhf{}
|
| 580 |
+
\fancyhead[L]{Patient Report}
|
| 581 |
+
\fancyhead[R]{Generated: \today}
|
| 582 |
+
\fancyfoot[C]{\thepage}
|
| 583 |
+
|
| 584 |
+
\begin{document}
|
| 585 |
+
|
| 586 |
+
\begin{center}
|
| 587 |
+
\Large\textbf{Patient Medical Report} \\
|
| 588 |
+
\vspace{0.2cm}
|
| 589 |
+
\textit{Generated on %(generated_on)s}
|
| 590 |
+
\end{center}
|
| 591 |
+
|
| 592 |
+
\section*{Demographics}
|
| 593 |
+
\begin{itemize}
|
| 594 |
+
\item \textbf{FHIR ID:} %(fhir_id)s
|
| 595 |
+
\item \textbf{Full Name:} %(full_name)s
|
| 596 |
+
\item \textbf{Gender:} %(gender)s
|
| 597 |
+
\item \textbf{Date of Birth:} %(dob)s
|
| 598 |
+
\item \textbf{Age:} %(age)s
|
| 599 |
+
\item \textbf{Address:} %(address)s
|
| 600 |
+
\item \textbf{Marital Status:} %(marital_status)s
|
| 601 |
+
\item \textbf{Language:} %(language)s
|
| 602 |
+
\end{itemize}
|
| 603 |
+
|
| 604 |
+
\section*{Clinical Notes}
|
| 605 |
+
\begin{longtable}{p{3cm}p{3cm}p{7cm}}
|
| 606 |
+
\toprule
|
| 607 |
+
\textbf{Date} & \textbf{Type} & \textbf{Text} \\
|
| 608 |
+
\midrule
|
| 609 |
+
%(notes)s
|
| 610 |
+
\bottomrule
|
| 611 |
+
\end{longtable}
|
| 612 |
+
|
| 613 |
+
\section*{Conditions}
|
| 614 |
+
\begin{longtable}{p{2cm}p{3cm}p{2cm}p{2cm}p{3cm}}
|
| 615 |
+
\toprule
|
| 616 |
+
\textbf{ID} & \textbf{Code} & \textbf{Status} & \textbf{Onset} & \textbf{Verification} \\
|
| 617 |
+
\midrule
|
| 618 |
+
%(conditions)s
|
| 619 |
+
\bottomrule
|
| 620 |
+
\end{longtable}
|
| 621 |
+
|
| 622 |
+
\section*{Medications}
|
| 623 |
+
\begin{longtable}{p{2cm}p{4cm}p{2cm}p{2cm}p{2cm}}
|
| 624 |
+
\toprule
|
| 625 |
+
\textbf{ID} & \textbf{Name} & \textbf{Status} & \textbf{Date} & \textbf{Dosage} \\
|
| 626 |
+
\midrule
|
| 627 |
+
%(medications)s
|
| 628 |
+
\bottomrule
|
| 629 |
+
\end{longtable}
|
| 630 |
+
|
| 631 |
+
\section*{Encounters}
|
| 632 |
+
\begin{longtable}{p{2cm}p{4cm}p{2cm}p{3cm}p{3cm}}
|
| 633 |
+
\toprule
|
| 634 |
+
\textbf{ID} & \textbf{Type} & \textbf{Status} & \textbf{Start} & \textbf{Provider} \\
|
| 635 |
+
\midrule
|
| 636 |
+
%(encounters)s
|
| 637 |
+
\bottomrule
|
| 638 |
+
\end{longtable}
|
| 639 |
+
|
| 640 |
+
\end{document}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
"""
|
| 642 |
|
| 643 |
+
# Build the LaTeX-safe rows
|
| 644 |
+
notes = "\n".join([
|
| 645 |
+
"{} & {} & {} \\\\".format(n.get("date", ""), n.get("type", ""), n.get("text", "").replace("&", "\\&"))
|
| 646 |
+
for n in patient.get("notes", [])
|
| 647 |
+
])
|
| 648 |
+
conditions = "\n".join([
|
| 649 |
+
"{} & {} & {} & {} & {} \\\\".format(
|
| 650 |
+
c.get("id", ""), c.get("code", ""), c.get("status", ""), c.get("onset_date", ""), c.get("verification_status", "")
|
| 651 |
+
)
|
| 652 |
+
for c in patient.get("conditions", [])
|
| 653 |
+
])
|
| 654 |
+
medications = "\n".join([
|
| 655 |
+
"{} & {} & {} & {} & {} \\\\".format(
|
| 656 |
+
m.get("id", ""), m.get("name", ""), m.get("status", ""), m.get("prescribed_date", ""), m.get("dosage", "")
|
| 657 |
+
)
|
| 658 |
+
for m in patient.get("medications", [])
|
| 659 |
+
])
|
| 660 |
+
encounters = "\n".join([
|
| 661 |
+
"{} & {} & {} & {} & {} \\\\".format(
|
| 662 |
+
e.get("id", ""), e.get("type", ""), e.get("status", ""), e.get("period", {}).get("start", ""), e.get("service_provider", "")
|
| 663 |
+
)
|
| 664 |
+
for e in patient.get("encounters", [])
|
| 665 |
+
])
|
| 666 |
+
|
| 667 |
+
latex_filled = latex_template % {
|
| 668 |
+
"generated_on": datetime.now().strftime("%A, %B %d, %Y at %I:%M %p"),
|
| 669 |
+
"fhir_id": patient.get("fhir_id", ""),
|
| 670 |
+
"full_name": patient.get("full_name", ""),
|
| 671 |
+
"gender": patient.get("gender", ""),
|
| 672 |
+
"dob": patient.get("date_of_birth", ""),
|
| 673 |
+
"age": calculate_age(patient.get("date_of_birth", "")) or "N/A",
|
| 674 |
+
"address": "{}, {}, {}, {}, {}".format(
|
| 675 |
+
patient.get("address", ""), patient.get("city", ""), patient.get("state", ""),
|
| 676 |
+
patient.get("postal_code", ""), patient.get("country", "")
|
| 677 |
+
),
|
| 678 |
+
"marital_status": patient.get("marital_status", ""),
|
| 679 |
+
"language": patient.get("language", ""),
|
| 680 |
+
"notes": notes or "No notes available \\\\",
|
| 681 |
+
"conditions": conditions or "No conditions available \\\\",
|
| 682 |
+
"medications": medications or "No medications available \\\\",
|
| 683 |
+
"encounters": encounters or "No encounters available \\\\"
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
# Write LaTeX to file
|
| 687 |
with NamedTemporaryFile(suffix=".tex", mode="w", delete=False) as tex_file:
|
| 688 |
+
tex_file.write(latex_filled)
|
| 689 |
tex_path = tex_file.name
|
| 690 |
|
| 691 |
try:
|
|
|
|
| 694 |
with open(pdf_path, "rb") as f:
|
| 695 |
pdf_bytes = f.read()
|
| 696 |
|
|
|
|
| 697 |
subprocess.run(["latexmk", "-c", tex_path], check=True)
|
| 698 |
os.remove(tex_path)
|
| 699 |
os.remove(pdf_path)
|
| 700 |
|
| 701 |
+
return Response(
|
| 702 |
+
content=pdf_bytes,
|
| 703 |
+
media_type="application/pdf",
|
| 704 |
+
headers={"Content-Disposition": f"attachment; filename=patient_{patient.get('fhir_id', 'unknown')}_record.pdf"}
|
| 705 |
+
)
|
| 706 |
|
| 707 |
except subprocess.CalledProcessError as e:
|
| 708 |
logger.error(f"LaTeX compilation failed: {e.stderr}")
|
|
|
|
| 711 |
except Exception as e:
|
| 712 |
logger.error(f"PDF generation error: {e}")
|
| 713 |
raise HTTPException(status_code=500, detail="Failed to generate PDF")
|
| 714 |
+
|
| 715 |
@router.post("/signup", status_code=status.HTTP_201_CREATED)
|
| 716 |
async def signup(data: SignupForm):
|
| 717 |
logger.info(f"Signup attempt for email: {data.email}")
|