Ali2206 commited on
Commit
046a61d
·
verified ·
1 Parent(s): 3a08b41

Update api/routes.py

Browse files
Files changed (1) hide show
  1. api/routes.py +111 -188
api/routes.py CHANGED
@@ -548,207 +548,130 @@ async def add_note(
548
  @router.get("/ehr/patients/{patient_id}/pdf", response_class=Response)
549
  async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get_current_user)):
550
  logger.info(f"Generating PDF for patient: {patient_id} by user {current_user.get('email')}")
 
551
  if current_user.get('role') not in ['doctor', 'admin']:
552
  logger.warning(f"Unauthorized PDF generation attempt by {current_user.get('email')}")
553
- raise HTTPException(
554
- status_code=status.HTTP_403_FORBIDDEN,
555
- detail="Only clinicians can generate patient PDFs"
556
- )
557
-
558
  try:
559
- patient = await patients_collection.find_one({
560
- "$or": [
561
- {"_id": ObjectId(patient_id)},
562
- {"fhir_id": patient_id}
563
- ]
564
- })
565
-
 
 
566
  if not patient:
567
  logger.warning(f"Patient not found: {patient_id}")
568
- raise HTTPException(
569
- status_code=status.HTTP_404_NOT_FOUND,
570
- detail="Patient not found"
571
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
 
573
- # Prepare LaTeX content
574
- latex_content = r"""
575
- \documentclass[a4paper,12pt]{article}
576
- \usepackage[utf8]{inputenc}
577
- \usepackage[T1]{fontenc}
578
- \usepackage{geometry}
579
- \geometry{margin=1in}
580
- \usepackage{booktabs}
581
- \usepackage{longtable}
582
- \usepackage{DejaVuSans} % English font
583
- \usepackage{fancyhdr}
584
- \pagestyle{fancy}
585
- \fancyhf{}
586
- \fancyhead[L]{Patient Medical Record}
587
- \fancyhead[R]{Generated: \today}
588
- \fancyfoot[C]{\thepage}
589
- \renewcommand{\headrulewidth}{0.4pt}
590
- \renewcommand{\footrulewidth}{0.4pt}
591
-
592
- \begin{document}
593
-
594
- \begin{center}
595
- \textbf{\Large Patient Medical Record Report} \\
596
- \vspace{0.2cm}
597
- \textit{Generated on Friday, May 16, 2025 at 12:42 PM CET}
598
- \end{center}
599
-
600
- \section*{Demographics}
601
- \begin{itemize}
602
- \item \textbf{FHIR ID:} patient.get("fhir_id")
603
- \item \textbf{Full Name:} patient.get("full_name")
604
- \item \textbf{Gender:} patient.get("gender")
605
- \item \textbf{Date of Birth:} patient.get("date_of_birth")
606
- \item \textbf{Age:} calculate_age(patient.get("date_of_birth"))
607
- \item \textbf{Address:} patient.get("address") patient.get("city") patient.get("state") patient.get("postal_code") patient.get("country")
608
- \item \textbf{Marital Status:} patient.get("marital_status")
609
- \item \textbf{Language:} patient.get("language")
610
- \end{itemize}
611
-
612
- \section*{Clinical Data}
613
-
614
- \subsection*{Notes}
615
- \begin{longtable}{p{2cm} p{3cm} p{8cm}}
616
- \toprule
617
- \textbf{Date} & \textbf{Type} & \textbf{Text} \\
618
- \midrule
619
- \endhead
620
- for note in patient.get("notes", []):
621
- note.get("date") & note.get("type") & note.get("text") \\
622
- \midrule
623
- \bottomrule
624
- \end{longtable}
625
-
626
- \subsection*{Conditions}
627
- \begin{longtable}{p{2cm} p{3cm} p{2cm} p{2cm} p{2cm}}
628
- \toprule
629
- \textbf{ID} & \textbf{Code} & \textbf{Status} & \textbf{Onset Date} & \textbf{Verification Status} \\
630
- \midrule
631
- \endhead
632
- for condition in patient.get("conditions", []):
633
- condition.get("id") & condition.get("code") & condition.get("status") & condition.get("onset_date") & condition.get("verification_status") \\
634
- \midrule
635
- \bottomrule
636
- \end{longtable}
637
-
638
- \subsection*{Medications}
639
- \begin{longtable}{p{2cm} p{4cm} p{2cm} p{2cm} p{2cm}}
640
- \toprule
641
- \textbf{ID} & \textbf{Name} & \textbf{Status} & \textbf{Prescribed Date} & \textbf{Dosage} \\
642
- \midrule
643
- \endhead
644
- for medication in patient.get("medications", []):
645
- medication.get("id") & medication.get("name") & medication.get("status") & medication.get("prescribed_date") & medication.get("dosage") \\
646
- \midrule
647
- \bottomrule
648
- \end{longtable}
649
-
650
- \subsection*{Encounters}
651
- \begin{longtable}{p{2cm} p{4cm} p{2cm} p{2cm} p{3cm}}
652
- \toprule
653
- \textbf{ID} & \textbf{Type} & \textbf{Status} & \textbf{Start Date} & \textbf{Service Provider} \\
654
- \midrule
655
- \endhead
656
- for encounter in patient.get("encounters", []):
657
- encounter.get("id") & encounter.get("type") & encounter.get("status") & encounter.get("period", {}).get("start") & encounter.get("service_provider") \\
658
- \midrule
659
- \bottomrule
660
- \end{longtable}
661
-
662
- \section*{Metadata}
663
- \begin{itemize}
664
- \item \textbf{Source:} patient.get("source")
665
- \item \textbf{Import Date:} patient.get("import_date")
666
- \item \textbf{Last Updated:} patient.get("last_updated")
667
- \end{itemize}
668
-
669
- \end{document}
670
- """.strip()
671
-
672
- # Replace placeholders with actual patient data
673
- latex_content = latex_content.replace("patient.get(\"fhir_id\")", str(patient.get("fhir_id", "N/A")))
674
- latex_content = latex_content.replace("patient.get(\"full_name\")", str(patient.get("full_name", "N/A")))
675
- latex_content = latex_content.replace("patient.get(\"gender\")", str(patient.get("gender", "N/A")))
676
- latex_content = latex_content.replace("patient.get(\"date_of_birth\")", str(patient.get("date_of_birth", "N/A")))
677
- latex_content = latex_content.replace("calculate_age(patient.get(\"date_of_birth\"))", str(calculate_age(patient.get("date_of_birth")) or "N/A"))
678
- latex_content = latex_content.replace("patient.get(\"address\")", str(patient.get("address", "")))
679
- latex_content = latex_content.replace("patient.get(\"city\")", str(patient.get("city", "")))
680
- latex_content = latex_content.replace("patient.get(\"state\")", str(patient.get("state", "")))
681
- latex_content = latex_content.replace("patient.get(\"postal_code\")", str(patient.get("postal_code", "")))
682
- latex_content = latex_content.replace("patient.get(\"country\")", str(patient.get("country", "")))
683
- latex_content = latex_content.replace("patient.get(\"marital_status\")", str(patient.get("marital_status", "N/A")))
684
- latex_content = latex_content.replace("patient.get(\"language\")", str(patient.get("language", "N/A")))
685
- latex_content = latex_content.replace("patient.get(\"source\")", str(patient.get("source", "N/A")))
686
- latex_content = latex_content.replace("patient.get(\"import_date\")", str(patient.get("import_date", "N/A")))
687
- latex_content = latex_content.replace("patient.get(\"last_updated\")", str(patient.get("last_updated", "N/A")))
688
-
689
- # Handle lists (notes, conditions, medications, encounters)
690
- notes_rows = "\n".join([f"{note.get('date', 'N/A')} & {note.get('type', 'N/A')} & {note.get('text', 'N/A')} \\\\" for note in patient.get("notes", [])])
691
- conditions_rows = "\n".join([f"{cond.get('id', 'N/A')} & {cond.get('code', 'N/A')} & {cond.get('status', 'N/A')} & {cond.get('onset_date', 'N/A')} & {cond.get('verification_status', 'N/A')} \\\\" for cond in patient.get("conditions", [])])
692
- medications_rows = "\n".join([f"{med.get('id', 'N/A')} & {med.get('name', 'N/A')} & {med.get('status', 'N/A')} & {med.get('prescribed_date', 'N/A')} & {med.get('dosage', 'N/A')} \\\\" for med in patient.get("medications", [])])
693
- encounters_rows = "\n".join([f"{enc.get('id', 'N/A')} & {enc.get('type', 'N/A')} & {enc.get('status', 'N/A')} & {enc.get('period', {}).get('start', 'N/A')} & {enc.get('service_provider', 'N/A')} \\\\" for enc in patient.get("encounters", [])])
694
-
695
- latex_content = latex_content.replace("for note in patient.get(\"notes\", []):\n note.get(\"date\") & note.get(\"type\") & note.get(\"text\") \\\\", notes_rows)
696
- latex_content = latex_content.replace("for condition in patient.get(\"conditions\", []):\n condition.get(\"id\") & condition.get(\"code\") & condition.get(\"status\") & condition.get(\"onset_date\") & condition.get(\"verification_status\") \\\\", conditions_rows)
697
- latex_content = latex_content.replace("for medication in patient.get(\"medications\", []):\n medication.get(\"id\") & medication.get(\"name\") & medication.get(\"status\") & medication.get(\"prescribed_date\") & medication.get(\"dosage\") \\\\", medications_rows)
698
- latex_content = latex_content.replace("for encounter in patient.get(\"encounters\", []):\n encounter.get(\"id\") & encounter.get(\"type\") & encounter.get(\"status\") & encounter.get(\"period\", {}).get(\"start\") & encounter.get(\"service_provider\") \\\\", encounters_rows)
699
-
700
- # Write LaTeX to a temporary file
701
- with NamedTemporaryFile(mode='w', suffix='.tex', delete=False) as temp_file:
702
- temp_file.write(latex_content)
703
- temp_file_path = temp_file.name
704
-
705
- # Compile LaTeX to PDF using latexmk
706
  try:
707
- subprocess.run(['latexmk', '-pdf', temp_file_path], check=True, capture_output=True, text=True)
708
- pdf_path = temp_file_path.replace('.tex', '.pdf')
709
-
710
- # Read the generated PDF
711
- with open(pdf_path, 'rb') as pdf_file:
712
- pdf_content = pdf_file.read()
713
-
714
- # Clean up temporary files
715
- subprocess.run(['latexmk', '-c', temp_file_path], check=True, capture_output=True, text=True)
716
- for file in [temp_file_path, pdf_path]:
717
- if os.path.exists(file):
718
- os.unlink(file)
719
-
720
- # Return the PDF as a response
721
- response = Response(content=pdf_content, media_type="application/pdf")
722
  response.headers["Content-Disposition"] = f"attachment; filename=patient_{patient.get('fhir_id', 'unknown')}_record.pdf"
723
- logger.info(f"PDF generated successfully for patient {patient_id}")
724
  return response
725
 
726
  except subprocess.CalledProcessError as e:
727
- logger.error(f"Failed to compile LaTeX for patient {patient_id}: {str(e)}")
728
- raise HTTPException(
729
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
730
- detail=f"Failed to generate PDF: {str(e.output)}"
731
- )
732
- except Exception as e:
733
- logger.error(f"Unexpected error generating PDF for patient {patient_id}: {str(e)}")
734
- raise HTTPException(
735
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
736
- detail=f"Unexpected error generating PDF: {str(e)}"
737
- )
738
 
739
- except ValueError as ve:
740
- logger.error(f"Invalid patient ID format: {patient_id}, error: {str(ve)}")
741
- raise HTTPException(
742
- status_code=status.HTTP_400_BAD_REQUEST,
743
- detail="Invalid patient ID format"
744
- )
745
  except Exception as e:
746
- logger.error(f"Failed to retrieve patient {patient_id} for PDF generation: {str(e)}")
747
- raise HTTPException(
748
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
749
- detail=f"Failed to generate PDF: {str(e)}"
750
- )
751
-
752
  @router.post("/signup", status_code=status.HTTP_201_CREATED)
753
  async def signup(data: SignupForm):
754
  logger.info(f"Signup attempt for email: {data.email}")
 
548
  @router.get("/ehr/patients/{patient_id}/pdf", response_class=Response)
549
  async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get_current_user)):
550
  logger.info(f"Generating PDF for patient: {patient_id} by user {current_user.get('email')}")
551
+
552
  if current_user.get('role') not in ['doctor', 'admin']:
553
  logger.warning(f"Unauthorized PDF generation attempt by {current_user.get('email')}")
554
+ raise HTTPException(status_code=403, detail="Only clinicians can generate patient PDFs")
555
+
 
 
 
556
  try:
557
+ # Determine if input is ObjectId or fallback to fhir_id
558
+ try:
559
+ obj_id = ObjectId(patient_id)
560
+ query = { "$or": [ { "_id": obj_id }, { "fhir_id": patient_id } ] }
561
+ except InvalidId:
562
+ query = { "fhir_id": patient_id }
563
+
564
+ patient = await patients_collection.find_one(query)
565
+
566
  if not patient:
567
  logger.warning(f"Patient not found: {patient_id}")
568
+ raise HTTPException(status_code=404, detail="Patient not found")
569
+
570
+ # Generate LaTeX content
571
+ latex = f"""
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 {datetime.now().strftime('%A, %B %d, %Y at %I:%M %p')}}}
590
+ \\end{{center}}
591
+
592
+ \\section*{{Demographics}}
593
+ \\begin{{itemize}}
594
+ \\item \\textbf{{FHIR ID:}} {patient.get("fhir_id", "")}
595
+ \\item \\textbf{{Full Name:}} {patient.get("full_name", "")}
596
+ \\item \\textbf{{Gender:}} {patient.get("gender", "")}
597
+ \\item \\textbf{{Date of Birth:}} {patient.get("date_of_birth", "")}
598
+ \\item \\textbf{{Age:}} {calculate_age(patient.get("date_of_birth", ""))}
599
+ \\item \\textbf{{Address:}} {patient.get("address", "")}, {patient.get("city", "")}, {patient.get("state", "")}, {patient.get("postal_code", "")}, {patient.get("country", "")}
600
+ \\item \\textbf{{Marital Status:}} {patient.get("marital_status", "")}
601
+ \\item \\textbf{{Language:}} {patient.get("language", "")}
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
+ \\endhead
610
+ {" \\\\ \n".join([f"{n.get('date', '')} & {n.get('type', '')} & {n.get('text', '')}" for n in patient.get("notes", [])])}
611
+ \\bottomrule
612
+ \\end{{longtable}}
613
+
614
+ \\section*{{Conditions}}
615
+ \\begin{{longtable}}{{p{{2cm}}p{{3cm}}p{{2cm}}p{{2cm}}p{{3cm}}}}
616
+ \\toprule
617
+ \\textbf{{ID}} & \\textbf{{Code}} & \\textbf{{Status}} & \\textbf{{Onset}} & \\textbf{{Verification}} \\\\
618
+ \\midrule
619
+ \\endhead
620
+ {" \\\\ \n".join([f"{c.get('id','')} & {c.get('code','')} & {c.get('status','')} & {c.get('onset_date','')} & {c.get('verification_status','')}" for c in patient.get("conditions", [])])}
621
+ \\bottomrule
622
+ \\end{{longtable}}
623
+
624
+ \\section*{{Medications}}
625
+ \\begin{{longtable}}{{p{{2cm}}p{{4cm}}p{{2cm}}p{{2cm}}p{{2cm}}}}
626
+ \\toprule
627
+ \\textbf{{ID}} & \\textbf{{Name}} & \\textbf{{Status}} & \\textbf{{Date}} & \\textbf{{Dosage}} \\\\
628
+ \\midrule
629
+ \\endhead
630
+ {" \\\\ \n".join([f"{m.get('id','')} & {m.get('name','')} & {m.get('status','')} & {m.get('prescribed_date','')} & {m.get('dosage','')}" for m in patient.get("medications", [])])}
631
+ \\bottomrule
632
+ \\end{{longtable}}
633
+
634
+ \\section*{{Encounters}}
635
+ \\begin{{longtable}}{{p{{2cm}}p{{4cm}}p{{2cm}}p{{3cm}}p{{3cm}}}}
636
+ \\toprule
637
+ \\textbf{{ID}} & \\textbf{{Type}} & \\textbf{{Status}} & \\textbf{{Start}} & \\textbf{{Provider}} \\\\
638
+ \\midrule
639
+ \\endhead
640
+ {" \\\\ \n".join([f"{e.get('id','')} & {e.get('type','')} & {e.get('status','')} & {e.get('period',{}).get('start','')} & {e.get('service_provider','')}" for e in patient.get("encounters", [])])}
641
+ \\bottomrule
642
+ \\end{{longtable}}
643
+
644
+ \\end{{document}}
645
+ """
646
+
647
+ # Save and compile to PDF
648
+ with NamedTemporaryFile(suffix=".tex", mode="w", delete=False) as tex_file:
649
+ tex_file.write(latex)
650
+ tex_path = tex_file.name
651
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
  try:
653
+ subprocess.run(["latexmk", "-pdf", tex_path], check=True, capture_output=True)
654
+ pdf_path = tex_path.replace(".tex", ".pdf")
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
+ # Send PDF as response
664
+ response = Response(content=pdf_bytes, media_type="application/pdf")
 
 
 
665
  response.headers["Content-Disposition"] = f"attachment; filename=patient_{patient.get('fhir_id', 'unknown')}_record.pdf"
 
666
  return response
667
 
668
  except subprocess.CalledProcessError as e:
669
+ logger.error(f"LaTeX compilation failed: {e.stderr}")
670
+ raise HTTPException(status_code=500, detail="LaTeX compilation failed")
 
 
 
 
 
 
 
 
 
671
 
 
 
 
 
 
 
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}")