Ali2206 commited on
Commit
770d5fe
·
verified ·
1 Parent(s): 4a9d8e9

Update api/routes.py

Browse files
Files changed (1) hide show
  1. api/routes.py +207 -2
api/routes.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import APIRouter, HTTPException, Depends, Body, Query, status
2
  from fastapi.security import OAuth2PasswordRequestForm
3
  from models.schemas import SignupForm, TokenResponse, PatientCreate, DoctorCreate, AppointmentCreate
4
  from db.mongo import users_collection, patients_collection, appointments_collection
@@ -19,6 +19,8 @@ import logging
19
  import time
20
  import uuid
21
  import re
 
 
22
 
23
  # Configure logging
24
  logging.basicConfig(
@@ -543,7 +545,210 @@ async def add_note(
543
  detail=f"Failed to add note: {str(e)}"
544
  )
545
 
546
- # Original Auth Routes (fully implemented)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  @router.post("/signup", status_code=status.HTTP_201_CREATED)
548
  async def signup(data: SignupForm):
549
  logger.info(f"Signup attempt for email: {data.email}")
 
1
+ from fastapi import APIRouter, HTTPException, Depends, Body, Query, status, Response
2
  from fastapi.security import OAuth2PasswordRequestForm
3
  from models.schemas import SignupForm, TokenResponse, PatientCreate, DoctorCreate, AppointmentCreate
4
  from db.mongo import users_collection, patients_collection, appointments_collection
 
19
  import time
20
  import uuid
21
  import re
22
+ import subprocess
23
+ from tempfile import NamedTemporaryFile
24
 
25
  # Configure logging
26
  logging.basicConfig(
 
545
  detail=f"Failed to add note: {str(e)}"
546
  )
547
 
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}")