File size: 10,091 Bytes
afcd57a
eccea1b
 
 
e55e0b4
 
 
44fe32c
afcd57a
 
 
e55e0b4
 
 
 
 
 
 
 
 
 
 
afcd57a
 
 
 
97e2e8f
 
afcd57a
 
97e2e8f
afcd57a
 
 
 
 
 
97e2e8f
44fe32c
 
 
97e2e8f
afcd57a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44fe32c
afcd57a
 
 
 
 
 
 
 
 
 
 
 
 
44fe32c
afcd57a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e55e0b4
afcd57a
 
 
 
 
 
 
ed7c34d
afcd57a
44fe32c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
from fastapi import APIRouter, HTTPException, Depends, Response
from db.mongo import patients_collection
from core.security import get_current_user
from utils.helpers import calculate_age, escape_latex_special_chars, hyphenate_long_strings, format_timestamp
from datetime import datetime
from bson import ObjectId
from bson.errors import InvalidId
import os
import subprocess
from tempfile import TemporaryDirectory
from string import Template
import logging

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(name)s - %(message)s'
)
logger = logging.getLogger(__name__)

router = APIRouter()

@router.get("/{patient_id}/pdf", response_class=Response)
async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get_current_user)):
    # Suppress logging for this route
    logger.setLevel(logging.CRITICAL)

    try:
        if current_user.get('role') not in ['doctor', 'admin']:
            raise HTTPException(status_code=403, detail="Only clinicians can generate patient PDFs")

        # Determine if patient_id is ObjectId or fhir_id
        try:
            obj_id = ObjectId(patient_id)
            query = {"$or": [{"_id": obj_id}, {"fhir_id": patient_id}]}
        except InvalidId:
            query = {"fhir_id": patient_id}

        patient = await patients_collection.find_one(query)
        if not patient:
            raise HTTPException(status_code=404, detail="Patient not found")

        # Prepare table content with proper LaTeX formatting
        def prepare_table_content(items, columns, default_message):
            if not items:
                return f"\\multicolumn{{{columns}}}{{l}}{{{default_message}}} \\\\"
            
            content = []
            for item in items:
                row = []
                for field in item:
                    value = item.get(field, "") or ""
                    row.append(escape_latex_special_chars(hyphenate_long_strings(value)))
                content.append(" & ".join(row) + " \\\\")
            return "\n".join(content)

        # Notes table
        notes = patient.get("notes", [])
        notes_content = prepare_table_content(
            [{
                "date": format_timestamp(n.get("date", "")),
                "type": n.get("type", ""),
                "text": n.get("text", "")
            } for n in notes],
            3,
            "No notes available"
        )

        # Conditions table
        conditions = patient.get("conditions", [])
        conditions_content = prepare_table_content(
            [{
                "id": c.get("id", ""),
                "code": c.get("code", ""),
                "status": c.get("status", ""),
                "onset": format_timestamp(c.get("onset_date", "")),
                "verification": c.get("verification_status", "")
            } for c in conditions],
            5,
            "No conditions available"
        )

        # Medications table
        medications = patient.get("medications", [])
        medications_content = prepare_table_content(
            [{
                "id": m.get("id", ""),
                "name": m.get("name", ""),
                "status": m.get("status", ""),
                "date": format_timestamp(m.get("prescribed_date", "")),
                "dosage": m.get("dosage", "")
            } for m in medications],
            5,
            "No medications available"
        )

        # Encounters table
        encounters = patient.get("encounters", [])
        encounters_content = prepare_table_content(
            [{
                "id": e.get("id", ""),
                "type": e.get("type", ""),
                "status": e.get("status", ""),
                "start": format_timestamp(e.get("period", {}).get("start", "")),
                "provider": e.get("service_provider", "")
            } for e in encounters],
            5,
            "No encounters available"
        )

        # LaTeX template with improved table formatting
        latex_template = Template(r"""
\documentclass[a4paper,12pt]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{geometry}
\geometry{margin=1in}
\usepackage{booktabs,longtable,fancyhdr}
\usepackage{array}
\usepackage{microtype}
\microtypesetup{expansion=false}
\setlength{\headheight}{14.5pt}
\pagestyle{fancy}
\fancyhf{}
\fancyhead[L]{Patient Report}
\fancyhead[R]{Generated: \today}
\fancyfoot[C]{\thepage}
\begin{document}
\begin{center}
    \Large\textbf{Patient Medical Report} \\
    \vspace{0.2cm}
    \textit{Generated on $generated_on}
\end{center}
\section*{Demographics}
\begin{itemize}
    \item \textbf{FHIR ID:} $fhir_id
    \item \textbf{Full Name:} $full_name
    \item \textbf{Gender:} $gender
    \item \textbf{Date of Birth:} $dob
    \item \textbf{Age:} $age
    \item \textbf{Address:} $address
    \item \textbf{Marital Status:} $marital_status
    \item \textbf{Language:} $language
\end{itemize}
\section*{Clinical Notes}
\begin{longtable}[l]{>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{6.5cm}}
\caption{Clinical Notes} \\
\toprule
\textbf{Date} & \textbf{Type} & \textbf{Text} \\
\midrule
$notes
\bottomrule
\end{longtable}
\section*{Conditions}
\begin{longtable}[l]{>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}}
\caption{Conditions} \\
\toprule
\textbf{ID} & \textbf{Code} & \textbf{Status} & \textbf{Onset} & \textbf{Verification} \\
\midrule
$conditions
\bottomrule
\end{longtable}
\section*{Medications}
\begin{longtable}[l]{>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{4cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}}
\caption{Medications} \\
\toprule
\textbf{ID} & \textbf{Name} & \textbf{Status} & \textbf{Date} & \textbf{Dosage} \\
\midrule
$medications
\bottomrule
\end{longtable}
\section*{Encounters}
\begin{longtable}[l]{>{\raggedright\arraybackslash}p{2.5cm}>{\raggedright\arraybackslash}p{4.5cm}>{\raggedright\arraybackslash}p{2.5cm}>{\raggedright\arraybackslash}p{4.5cm}>{\raggedright\arraybackslash}p{3.5cm}}
\caption{Encounters} \\
\toprule
\textbf{ID} & \textbf{Type} & \textbf{Status} & \textbf{Start} & \textbf{Provider} \\
\midrule
$encounters
\bottomrule
\end{longtable}
\end{document}
""")

        # Set the generated_on date to 02:54 PM CET, May 17, 2025
        generated_on = datetime.strptime("2025-05-17 14:54:00+02:00", "%Y-%m-%d %H:%M:%S%z").strftime("%A, %B %d, %Y at %I:%M %p %Z")

        latex_filled = latex_template.substitute(
            generated_on=generated_on,
            fhir_id=escape_latex_special_chars(hyphenate_long_strings(patient.get("fhir_id", "") or "")),
            full_name=escape_latex_special_chars(patient.get("full_name", "") or ""),
            gender=escape_latex_special_chars(patient.get("gender", "") or ""),
            dob=escape_latex_special_chars(patient.get("date_of_birth", "") or ""),
            age=escape_latex_special_chars(str(calculate_age(patient.get("date_of_birth", "")) or "N/A")),
            address=escape_latex_special_chars(", ".join(filter(None, [
                patient.get("address", ""),
                patient.get("city", ""),
                patient.get("state", ""),
                patient.get("postal_code", ""),
                patient.get("country", "")
            ]))),
            marital_status=escape_latex_special_chars(patient.get("marital_status", "") or ""),
            language=escape_latex_special_chars(patient.get("language", "") or ""),
            notes=notes_content,
            conditions=conditions_content,
            medications=medications_content,
            encounters=encounters_content
        )

        # Compile LaTeX in a temporary directory
        with TemporaryDirectory() as tmpdir:
            tex_path = os.path.join(tmpdir, "report.tex")
            pdf_path = os.path.join(tmpdir, "report.pdf")

            with open(tex_path, "w", encoding="utf-8") as f:
                f.write(latex_filled)

            try:
                # Run latexmk twice to ensure proper table rendering
                for _ in range(2):
                    result = subprocess.run(
                        ["latexmk", "-pdf", "-interaction=nonstopmode", tex_path],
                        cwd=tmpdir,
                        check=False,
                        capture_output=True,
                        text=True
                    )
                
                if result.returncode != 0:
                    raise HTTPException(
                        status_code=500,
                        detail=f"LaTeX compilation failed: stdout={result.stdout}, stderr={result.stderr}"
                    )

            except subprocess.CalledProcessError as e:
                raise HTTPException(
                    status_code=500,
                    detail=f"LaTeX compilation failed: stdout={e.stdout}, stderr={e.stderr}"
                )

            if not os.path.exists(pdf_path):
                raise HTTPException(
                    status_code=500,
                    detail="PDF file was not generated"
                )

            with open(pdf_path, "rb") as f:
                pdf_bytes = f.read()

            response = Response(
                content=pdf_bytes,
                media_type="application/pdf",
                headers={"Content-Disposition": f"attachment; filename=patient_{patient.get('fhir_id', 'unknown')}_report.pdf"}
            )
            return response

    except HTTPException as http_error:
        raise http_error
    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail=f"Unexpected error generating PDF: {str(e)}"
        )
    finally:
        # Restore the logger level for other routes
        logger.setLevel(logging.INFO)

# Export the router as 'pdf' for api.__init__.py
pdf = router