SmartHeal commited on
Commit
3ed15a0
·
verified ·
1 Parent(s): 2db03e8

Update src/patient_history.py

Browse files
Files changed (1) hide show
  1. src/patient_history.py +194 -507
src/patient_history.py CHANGED
@@ -1,507 +1,194 @@
1
- import logging
2
- import json
3
- from datetime import datetime
4
- from typing import List, Dict, Optional
5
-
6
- class PatientHistoryManager:
7
- """Complete patient history and wound tracking system"""
8
-
9
- def __init__(self, database_manager):
10
- self.db = database_manager
11
-
12
- def get_patient_complete_history(self, user_id: int, patient_name: str = None) -> List[Dict]:
13
- """Get complete patient history with all wound analyses and images"""
14
- try:
15
- if patient_name:
16
- query = """
17
- SELECT
18
- qr.id as response_id,
19
- p.name as patient_name,
20
- p.age as patient_age,
21
- p.gender as patient_gender,
22
- qr.response_data,
23
- qr.submitted_at as visit_date,
24
- w.position as wound_location,
25
- w.moisture,
26
- w.infection,
27
- w.notes,
28
- wi.image as image_url,
29
- a.analysis_data,
30
- a.summary,
31
- a.recommendations,
32
- a.risk_score,
33
- a.risk_level
34
- FROM questionnaire_responses qr
35
- JOIN patients p ON qr.patient_id = p.id
36
- LEFT JOIN wounds w ON w.patient_id = CAST(p.id AS CHAR)
37
- LEFT JOIN wound_images wi ON wi.patient_id = CAST(p.id AS CHAR)
38
- LEFT JOIN ai_analyses a ON a.questionnaire_id = qr.id
39
- WHERE qr.practitioner_id = %s AND p.name = %s
40
- ORDER BY qr.submitted_at DESC
41
- """
42
- params = (user_id, patient_name)
43
- else:
44
- query = """
45
- SELECT
46
- qr.id as response_id,
47
- p.name as patient_name,
48
- p.age as patient_age,
49
- p.gender as patient_gender,
50
- qr.response_data,
51
- qr.submitted_at as visit_date,
52
- w.position as wound_location,
53
- w.moisture,
54
- w.infection,
55
- w.notes,
56
- wi.image as image_url,
57
- a.analysis_data,
58
- a.summary,
59
- a.recommendations,
60
- a.risk_score,
61
- a.risk_level
62
- FROM questionnaire_responses qr
63
- JOIN patients p ON qr.patient_id = p.id
64
- LEFT JOIN wounds w ON w.patient_id = CAST(p.id AS CHAR)
65
- LEFT JOIN wound_images wi ON wi.patient_id = CAST(p.id AS CHAR)
66
- LEFT JOIN ai_analyses a ON a.questionnaire_id = qr.id
67
- WHERE qr.practitioner_id = %s
68
- ORDER BY qr.submitted_at DESC
69
- """
70
- params = (user_id,)
71
-
72
- result = self.db.execute_query(query, params, fetch=True)
73
- return result or []
74
-
75
- except Exception as e:
76
- logging.error(f"Error fetching patient history: {e}")
77
- return []
78
-
79
- def get_patient_list(self, user_id: int) -> List[Dict]:
80
- """Get list of all patients for this practitioner"""
81
- try:
82
- query = """
83
- SELECT DISTINCT
84
- p.name as patient_name,
85
- p.age as patient_age,
86
- p.gender as patient_gender,
87
- COUNT(qr.id) as total_visits,
88
- MAX(qr.submitted_at) as last_visit,
89
- MIN(qr.submitted_at) as first_visit
90
- FROM questionnaire_responses qr
91
- JOIN patients p ON qr.patient_id = p.id
92
- WHERE qr.practitioner_id = %s
93
- GROUP BY p.name, p.age, p.gender
94
- ORDER BY last_visit DESC
95
- """
96
-
97
- result = self.db.execute_query(query, (user_id,), fetch=True)
98
- return result or []
99
-
100
- except Exception as e:
101
- logging.error(f"Error fetching patient list: {e}")
102
- return []
103
-
104
- def get_wound_progression(self, user_id: int, patient_name: str) -> List[Dict]:
105
- """Get wound progression data for timeline visualization"""
106
- try:
107
- query = """
108
- SELECT
109
- qr.submitted_at as visit_date,
110
- qr.response_data,
111
- w.position as wound_location,
112
- w.moisture,
113
- w.infection,
114
- a.risk_score,
115
- a.risk_level,
116
- a.summary,
117
- wi.image as image_url
118
- FROM questionnaire_responses qr
119
- JOIN patients p ON qr.patient_id = p.id
120
- LEFT JOIN wounds w ON w.patient_id = CAST(p.id AS CHAR)
121
- LEFT JOIN wound_images wi ON wi.patient_id = CAST(p.id AS CHAR)
122
- LEFT JOIN ai_analyses a ON a.questionnaire_id = qr.id
123
- WHERE qr.practitioner_id = %s AND p.name = %s
124
- ORDER BY qr.submitted_at ASC
125
- """
126
-
127
- result = self.db.execute_query(query, (user_id, patient_name), fetch=True)
128
- return result or []
129
-
130
- except Exception as e:
131
- logging.error(f"Error fetching wound progression: {e}")
132
- return []
133
-
134
- def save_patient_note(self, user_id: int, patient_name: str, note: str) -> bool:
135
- """Save a clinical note for a patient"""
136
- try:
137
- # Check if notes table exists, if not use questionnaires additional_notes
138
- query = """
139
- UPDATE questionnaires
140
- SET additional_notes = CONCAT(IFNULL(additional_notes, ''), '\n--- Clinical Note (', NOW(), ') ---\n', %s)
141
- WHERE user_id = %s AND patient_name = %s
142
- ORDER BY created_at DESC
143
- LIMIT 1
144
- """
145
-
146
- result = self.db.execute_query(query, (note, user_id, patient_name))
147
- return bool(result)
148
-
149
- except Exception as e:
150
- logging.error(f"Error saving patient note: {e}")
151
- return False
152
-
153
- class ReportGenerator:
154
- """Professional HTML report generator for wound analysis"""
155
-
156
- def __init__(self):
157
- pass
158
-
159
- def generate_analysis_report(self, patient_data: Dict, analysis_data: Dict, image_url: str = None) -> str:
160
- """Generate comprehensive HTML report for wound analysis"""
161
-
162
- report_html = f"""
163
- <!DOCTYPE html>
164
- <html lang="en">
165
- <head>
166
- <meta charset="UTF-8">
167
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
168
- <title>SmartHeal AI - Wound Analysis Report</title>
169
- <style>
170
- body {{
171
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
172
- line-height: 1.6;
173
- margin: 0;
174
- padding: 20px;
175
- background-color: #f8f9fa;
176
- }}
177
- .report-container {{
178
- max-width: 800px;
179
- margin: 0 auto;
180
- background: white;
181
- border-radius: 10px;
182
- box-shadow: 0 4px 20px rgba(0,0,0,0.1);
183
- overflow: hidden;
184
- }}
185
- .header {{
186
- background: linear-gradient(135deg, #3182ce 0%, #2c5aa0 100%);
187
- color: white;
188
- padding: 30px;
189
- text-align: center;
190
- }}
191
- .header h1 {{
192
- margin: 0;
193
- font-size: 28px;
194
- font-weight: 600;
195
- }}
196
- .header p {{
197
- margin: 10px 0 0 0;
198
- opacity: 0.9;
199
- font-size: 16px;
200
- }}
201
- .content {{
202
- padding: 30px;
203
- }}
204
- .section {{
205
- margin-bottom: 30px;
206
- border-left: 4px solid #3182ce;
207
- padding-left: 20px;
208
- }}
209
- .section h2 {{
210
- color: #2c5aa0;
211
- margin-top: 0;
212
- font-size: 20px;
213
- font-weight: 600;
214
- }}
215
- .patient-info {{
216
- display: grid;
217
- grid-template-columns: 1fr 1fr;
218
- gap: 20px;
219
- margin-bottom: 20px;
220
- }}
221
- .info-card {{
222
- background: #f8f9fa;
223
- padding: 15px;
224
- border-radius: 8px;
225
- border: 1px solid #e9ecef;
226
- }}
227
- .info-card h3 {{
228
- margin: 0 0 10px 0;
229
- color: #495057;
230
- font-size: 14px;
231
- font-weight: 600;
232
- text-transform: uppercase;
233
- }}
234
- .info-card p {{
235
- margin: 0;
236
- font-weight: 500;
237
- color: #212529;
238
- }}
239
- .risk-indicator {{
240
- display: inline-block;
241
- padding: 8px 16px;
242
- border-radius: 20px;
243
- font-weight: 600;
244
- text-transform: uppercase;
245
- font-size: 12px;
246
- letter-spacing: 0.5px;
247
- }}
248
- .risk-low {{ background: #d4edda; color: #155724; }}
249
- .risk-moderate {{ background: #fff3cd; color: #856404; }}
250
- .risk-high {{ background: #f8d7da; color: #721c24; }}
251
- .recommendations {{
252
- background: #e7f3ff;
253
- border: 1px solid #b3d9ff;
254
- border-radius: 8px;
255
- padding: 20px;
256
- margin-top: 15px;
257
- }}
258
- .recommendations ul {{
259
- margin: 10px 0;
260
- padding-left: 20px;
261
- }}
262
- .recommendations li {{
263
- margin-bottom: 8px;
264
- color: #0056b3;
265
- }}
266
- .image-section {{
267
- text-align: center;
268
- margin: 20px 0;
269
- }}
270
- .wound-image {{
271
- max-width: 100%;
272
- height: auto;
273
- border-radius: 8px;
274
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
275
- }}
276
- .footer {{
277
- background: #f8f9fa;
278
- padding: 20px 30px;
279
- text-align: center;
280
- color: #6c757d;
281
- border-top: 1px solid #e9ecef;
282
- }}
283
- .connect-clinician {{
284
- background: #28a745;
285
- color: white;
286
- padding: 12px 30px;
287
- border: none;
288
- border-radius: 25px;
289
- font-weight: 600;
290
- cursor: pointer;
291
- margin: 20px 0;
292
- font-size: 16px;
293
- }}
294
- .connect-clinician:hover {{
295
- background: #218838;
296
- }}
297
- @media print {{
298
- body {{ background: white; }}
299
- .report-container {{ box-shadow: none; }}
300
- .connect-clinician {{ display: none; }}
301
- }}
302
- </style>
303
- </head>
304
- <body>
305
- <div class="report-container">
306
- <div class="header">
307
- <h1>🩺 SmartHeal AI Wound Analysis Report</h1>
308
- <p>Advanced AI-Powered Clinical Assessment</p>
309
- </div>
310
-
311
- <div class="content">
312
- <div class="section">
313
- <h2>Patient Information</h2>
314
- <div class="patient-info">
315
- <div class="info-card">
316
- <h3>Patient Name</h3>
317
- <p>{patient_data.get('patient_name', 'N/A')}</p>
318
- </div>
319
- <div class="info-card">
320
- <h3>Age</h3>
321
- <p>{patient_data.get('patient_age', 'N/A')} years</p>
322
- </div>
323
- <div class="info-card">
324
- <h3>Gender</h3>
325
- <p>{patient_data.get('patient_gender', 'N/A')}</p>
326
- </div>
327
- <div class="info-card">
328
- <h3>Assessment Date</h3>
329
- <p>{datetime.now().strftime('%B %d, %Y at %I:%M %p')}</p>
330
- </div>
331
- </div>
332
- </div>
333
-
334
- <div class="section">
335
- <h2>Wound Assessment</h2>
336
- <div class="patient-info">
337
- <div class="info-card">
338
- <h3>Location</h3>
339
- <p>{patient_data.get('wound_location', 'N/A')}</p>
340
- </div>
341
- <div class="info-card">
342
- <h3>Duration</h3>
343
- <p>{patient_data.get('wound_duration', 'N/A')}</p>
344
- </div>
345
- <div class="info-card">
346
- <h3>Pain Level</h3>
347
- <p>{patient_data.get('pain_level', 'N/A')}/10</p>
348
- </div>
349
- <div class="info-card">
350
- <h3>Risk Assessment</h3>
351
- <p>
352
- <span class="risk-indicator risk-{analysis_data.get('risk_level', 'unknown').lower()}">
353
- {analysis_data.get('risk_level', 'Unknown')} Risk
354
- </span>
355
- </p>
356
- </div>
357
- </div>
358
- </div>
359
-
360
- {f'''
361
- <div class="section">
362
- <h2>Wound Image</h2>
363
- <div class="image-section">
364
- <img src="{image_url}" alt="Wound Image" class="wound-image">
365
- </div>
366
- </div>
367
- ''' if image_url else ''}
368
-
369
- <div class="section">
370
- <h2>AI Analysis Summary</h2>
371
- <p>{analysis_data.get('summary', 'No analysis summary available.')}</p>
372
-
373
- <div class="recommendations">
374
- <h3>🎯 Clinical Recommendations</h3>
375
- {self._format_recommendations(analysis_data.get('recommendations', ''))}
376
- </div>
377
- </div>
378
-
379
- <div class="section">
380
- <h2>Medical History</h2>
381
- <div class="patient-info">
382
- <div class="info-card">
383
- <h3>Medical History</h3>
384
- <p>{patient_data.get('medical_history', 'None reported')}</p>
385
- </div>
386
- <div class="info-card">
387
- <h3>Current Medications</h3>
388
- <p>{patient_data.get('medications', 'None reported')}</p>
389
- </div>
390
- <div class="info-card">
391
- <h3>Known Allergies</h3>
392
- <p>{patient_data.get('allergies', 'None reported')}</p>
393
- </div>
394
- <div class="info-card">
395
- <h3>Additional Notes</h3>
396
- <p>{patient_data.get('additional_notes', 'None')}</p>
397
- </div>
398
- </div>
399
- </div>
400
-
401
- <div style="text-align: center;">
402
- <button class="connect-clinician" onclick="connectToClinician()">
403
- 📞 Connect to Clinician
404
- </button>
405
- </div>
406
- </div>
407
-
408
- <div class="footer">
409
- <p><strong>SmartHeal AI</strong> - Advanced Wound Care Analysis & Clinical Support System</p>
410
- <p>Generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')} | For professional medical use only</p>
411
- <p>⚠️ This AI analysis is for clinical decision support only. Always consult with healthcare professionals for diagnosis and treatment.</p>
412
- </div>
413
- </div>
414
-
415
- <script>
416
- function connectToClinician() {{
417
- alert('🩺 Connecting to clinician...\n\nThis feature would connect you to a healthcare professional for consultation.\n\nIn a production environment, this would:\n• Initiate video call\n• Send analysis to specialist\n• Schedule consultation\n• Provide emergency contact');
418
- }}
419
- </script>
420
- </body>
421
- </html>
422
- """
423
-
424
- return report_html
425
-
426
- def _format_recommendations(self, recommendations: str) -> str:
427
- """Format recommendations as HTML list"""
428
- if not recommendations:
429
- return "<p>No specific recommendations available.</p>"
430
-
431
- # Split recommendations by common delimiters
432
- items = []
433
- for delimiter in ['\n', '. ', '; ']:
434
- if delimiter in recommendations:
435
- items = [item.strip() for item in recommendations.split(delimiter) if item.strip()]
436
- break
437
-
438
- if not items:
439
- items = [recommendations]
440
-
441
- html = "<ul>"
442
- for item in items:
443
- if item and len(item) > 3: # Avoid very short fragments
444
- html += f"<li>{item}</li>"
445
- html += "</ul>"
446
-
447
- return html
448
-
449
- def generate_patient_history_report(self, patient_history: List[Dict]) -> str:
450
- """Generate comprehensive patient history report"""
451
- if not patient_history:
452
- return "<p>No patient history available.</p>"
453
-
454
- patient_name = patient_history[0].get('patient_name', 'Unknown Patient')
455
-
456
- html = f"""
457
- <div style="max-width: 800px; margin: 0 auto; font-family: 'Segoe UI', sans-serif;">
458
- <div style="background: linear-gradient(135deg, #3182ce 0%, #2c5aa0 100%); color: white; padding: 20px; border-radius: 10px 10px 0 0;">
459
- <h2 style="margin: 0;">📋 Patient History: {patient_name}</h2>
460
- <p style="margin: 10px 0 0 0; opacity: 0.9;">Complete Treatment Timeline</p>
461
- </div>
462
- <div style="background: white; border: 1px solid #e9ecef; border-top: none; border-radius: 0 0 10px 10px;">
463
- """
464
-
465
- for i, visit in enumerate(patient_history):
466
- visit_date = visit.get('visit_date', '')
467
- if isinstance(visit_date, str):
468
- try:
469
- visit_date = datetime.fromisoformat(visit_date.replace('Z', '+00:00'))
470
- except:
471
- pass
472
-
473
- risk_class = f"risk-{visit.get('risk_level', 'unknown').lower()}"
474
-
475
- html += f"""
476
- <div style="padding: 20px; border-bottom: 1px solid #f0f0f0;">
477
- <div style="display: flex; justify-content: between; align-items: center; margin-bottom: 15px;">
478
- <h3 style="color: #2c5aa0; margin: 0;">Visit #{len(patient_history) - i}</h3>
479
- <span style="color: #6c757d; font-size: 14px;">{visit_date.strftime('%B %d, %Y') if hasattr(visit_date, 'strftime') else str(visit_date)}</span>
480
- </div>
481
-
482
- <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-bottom: 15px;">
483
- <div style="background: #f8f9fa; padding: 10px; border-radius: 5px;">
484
- <strong>Location:</strong> {visit.get('wound_location', 'N/A')}
485
- </div>
486
- <div style="background: #f8f9fa; padding: 10px; border-radius: 5px;">
487
- <strong>Pain Level:</strong> {visit.get('pain_level', 'N/A')}/10
488
- </div>
489
- <div style="background: #f8f9fa; padding: 10px; border-radius: 5px;">
490
- <strong>Risk:</strong>
491
- <span class="risk-indicator {risk_class}" style="background: {'#d4edda' if 'low' in risk_class else '#fff3cd' if 'moderate' in risk_class else '#f8d7da'}; color: {'#155724' if 'low' in risk_class else '#856404' if 'moderate' in risk_class else '#721c24'}; padding: 2px 8px; border-radius: 10px; font-size: 12px;">
492
- {visit.get('risk_level', 'Unknown')}
493
- </span>
494
- </div>
495
- </div>
496
-
497
- {f'<p><strong>Summary:</strong> {visit.get("summary", "No summary available")}</p>' if visit.get("summary") else ""}
498
- {f'<p><strong>Recommendations:</strong> {visit.get("recommendations", "No recommendations available")}</p>' if visit.get("recommendations") else ""}
499
- </div>
500
- """
501
-
502
- html += """
503
- </div>
504
- </div>
505
- """
506
-
507
- return html
 
1
+ import logging
2
+ import json
3
+ from datetime import datetime
4
+ from typing import List, Dict
5
+
6
+ class PatientHistoryManager:
7
+ """Complete patient history and wound tracking system (schema-aligned)"""
8
+
9
+ def __init__(self, database_manager):
10
+ self.db = database_manager
11
+
12
+ # ---------- INTERNAL HELPERS ----------
13
+ def _parse_response_field(self, row: Dict, field_path: list, default=None):
14
+ try:
15
+ data = row.get("response_data")
16
+ if isinstance(data, str):
17
+ data = json.loads(data)
18
+ cur = data
19
+ for key in field_path:
20
+ if isinstance(cur, dict) and key in cur:
21
+ cur = cur[key]
22
+ else:
23
+ return default
24
+ return cur
25
+ except Exception:
26
+ return default
27
+
28
+ # ---------- CORE QUERIES (fixed joins) ----------
29
+ def get_patient_complete_history(self, user_id: int, patient_name: str = None) -> List[Dict]:
30
+ try:
31
+ sql = f"""
32
+ SELECT
33
+ qr.id AS response_id,
34
+ p.name AS patient_name,
35
+ p.age AS patient_age,
36
+ p.gender AS patient_gender,
37
+ qr.response_data,
38
+ qr.submitted_at AS visit_date,
39
+ w.position AS wound_location,
40
+ w.moisture,
41
+ w.infection,
42
+ w.notes,
43
+ wi.image AS image_url,
44
+ a.analysis_data,
45
+ a.summary,
46
+ a.recommendations,
47
+ a.risk_score,
48
+ a.risk_level
49
+ FROM questionnaire_responses qr
50
+ JOIN patients p ON p.id = qr.patient_id
51
+ LEFT JOIN wounds w
52
+ ON (w.patient_id = p.uuid OR w.patient_id = CAST(p.id AS CHAR))
53
+ LEFT JOIN wound_images wi
54
+ ON (wi.patient_id = p.uuid OR wi.patient_id = CAST(p.id AS CHAR))
55
+ LEFT JOIN ai_analyses a
56
+ ON a.questionnaire_id = qr.questionnaire_id
57
+ WHERE qr.practitioner_id = %s
58
+ { "AND p.name = %s" if patient_name else "" }
59
+ ORDER BY qr.submitted_at DESC
60
+ """
61
+ params = (user_id, patient_name) if patient_name else (user_id,)
62
+ rows = self.db.execute_query(sql, params, fetch=True) or []
63
+ for r in rows:
64
+ r["pain_level"] = self._parse_response_field(r, ["wound_details", "pain_level"])
65
+ wl = self._parse_response_field(r, ["wound_details", "location"])
66
+ if wl:
67
+ r["wound_location"] = wl
68
+ return rows
69
+ except Exception as e:
70
+ logging.error(f"Error fetching patient history: {e}")
71
+ return []
72
+
73
+ def get_patient_list(self, user_id: int) -> List[Dict]:
74
+ try:
75
+ sql = """
76
+ SELECT DISTINCT
77
+ p.name AS patient_name,
78
+ p.age AS patient_age,
79
+ p.gender AS patient_gender,
80
+ COUNT(qr.id) AS total_visits,
81
+ MAX(qr.submitted_at) AS last_visit,
82
+ MIN(qr.submitted_at) AS first_visit
83
+ FROM questionnaire_responses qr
84
+ JOIN patients p ON p.id = qr.patient_id
85
+ WHERE qr.practitioner_id = %s
86
+ GROUP BY p.name, p.age, p.gender
87
+ ORDER BY last_visit DESC
88
+ """
89
+ return self.db.execute_query(sql, (user_id,), fetch=True) or []
90
+ except Exception as e:
91
+ logging.error(f"Error fetching patient list: {e}")
92
+ return []
93
+
94
+ def get_wound_progression(self, user_id: int, patient_name: str) -> List[Dict]:
95
+ try:
96
+ sql = """
97
+ SELECT
98
+ qr.submitted_at AS visit_date,
99
+ qr.response_data,
100
+ w.position AS wound_location,
101
+ w.moisture,
102
+ w.infection,
103
+ a.risk_score,
104
+ a.risk_level,
105
+ a.summary,
106
+ wi.image AS image_url
107
+ FROM questionnaire_responses qr
108
+ JOIN patients p ON p.id = qr.patient_id
109
+ LEFT JOIN wounds w
110
+ ON (w.patient_id = p.uuid OR w.patient_id = CAST(p.id AS CHAR))
111
+ LEFT JOIN wound_images wi
112
+ ON (wi.patient_id = p.uuid OR wi.patient_id = CAST(p.id AS CHAR))
113
+ LEFT JOIN ai_analyses a
114
+ ON a.questionnaire_id = qr.questionnaire_id
115
+ WHERE qr.practitioner_id = %s
116
+ AND p.name = %s
117
+ ORDER BY qr.submitted_at ASC
118
+ """
119
+ rows = self.db.execute_query(sql, (user_id, patient_name), fetch=True) or []
120
+ for r in rows:
121
+ r["pain_level"] = self._parse_response_field(r, ["wound_details", "pain_level"])
122
+ wl = self._parse_response_field(r, ["wound_details", "location"])
123
+ if wl:
124
+ r["wound_location"] = wl
125
+ return rows
126
+ except Exception as e:
127
+ logging.error(f"Error fetching wound progression: {e}")
128
+ return []
129
+
130
+ # ---------- WRAPPERS EXPECTED BY UI ----------
131
+ def get_user_patient_history(self, user_id: int) -> List[Dict]:
132
+ """Alias for UI: returns all patients’ latest visits for this practitioner."""
133
+ return self.get_patient_complete_history(user_id=user_id)
134
+
135
+ def search_patient_by_name(self, user_id: int, patient_name: str) -> List[Dict]:
136
+ """Alias for UI: filtered history for a single patient."""
137
+ return self.get_patient_complete_history(user_id=user_id, patient_name=patient_name)
138
+
139
+ # ---------- SIMPLE HTML FORMATTERS FOR UI ----------
140
+ def format_history_for_display(self, rows: List[Dict]) -> str:
141
+ if not rows:
142
+ return "<div class='status-warning'>No history found.</div>"
143
+ html = ["<div style='display:flex;flex-direction:column;gap:12px;'>"]
144
+ for r in rows:
145
+ dt = r.get("visit_date")
146
+ try:
147
+ dt = dt.strftime('%b %d, %Y %I:%M %p') if hasattr(dt, "strftime") else str(dt)
148
+ except Exception:
149
+ dt = str(r.get("visit_date"))
150
+ html.append(f"""
151
+ <div style="padding:12px;border:1px solid #e2e8f0;border-radius:8px;background:#fff;">
152
+ <div><strong>{r.get('patient_name','')}</strong> • {r.get('patient_age','N/A')} • {r.get('patient_gender','')}</div>
153
+ <div style="color:#4a5568;">{dt}</div>
154
+ <div>Wound: {r.get('wound_location','N/A')} Pain: {r.get('pain_level','N/A')}</div>
155
+ <div>Risk: {r.get('risk_level','Unknown')}</div>
156
+ {f"<div style='margin-top:6px;color:#2d3748;'><em>{r.get('summary')}</em></div>" if r.get('summary') else ""}
157
+ </div>
158
+ """)
159
+ html.append("</div>")
160
+ return "".join(html)
161
+
162
+ def format_patient_data_for_display(self, rows: List[Dict]) -> str:
163
+ """Same renderer but for a single-patient search results."""
164
+ return self.format_history_for_display(rows)
165
+
166
+ # ---------- NOTES ----------
167
+ def save_patient_note(self, user_id: int, patient_name: str, note: str) -> bool:
168
+ try:
169
+ row = self.db.execute_query_one(
170
+ """
171
+ SELECT p.uuid
172
+ FROM questionnaire_responses qr
173
+ JOIN patients p ON p.id = qr.patient_id
174
+ WHERE qr.practitioner_id = %s AND p.name = %s
175
+ ORDER BY qr.submitted_at DESC
176
+ LIMIT 1
177
+ """,
178
+ (user_id, patient_name)
179
+ )
180
+ if not row or not row.get("uuid"):
181
+ logging.error("save_patient_note: could not resolve patient uuid")
182
+ return False
183
+
184
+ patient_uuid = row["uuid"]
185
+ return bool(self.db.execute_query(
186
+ """
187
+ INSERT INTO notes (uuid, patient_id, note, added_by, created_at, updated_at)
188
+ VALUES (UUID(), %s, %s, %s, NOW(), NOW())
189
+ """,
190
+ (patient_uuid, note, str(user_id))
191
+ ))
192
+ except Exception as e:
193
+ logging.error(f"Error saving patient note: {e}")
194
+ return False