Spaces:
Sleeping
Sleeping
| import logging | |
| import json | |
| from datetime import datetime | |
| from typing import List, Dict, Optional | |
| class PatientHistoryManager: | |
| """Complete patient history and wound tracking system""" | |
| def __init__(self, database_manager): | |
| self.db = database_manager | |
| def get_patient_complete_history(self, user_id: int, patient_name: str = None) -> List[Dict]: | |
| """Get complete patient history with all wound analyses and images""" | |
| try: | |
| if patient_name: | |
| query = """ | |
| SELECT | |
| qr.id as response_id, | |
| p.name as patient_name, | |
| p.age as patient_age, | |
| p.gender as patient_gender, | |
| qr.response_data, | |
| qr.submitted_at as visit_date, | |
| w.position as wound_location, | |
| w.moisture, | |
| w.infection, | |
| w.notes, | |
| wi.image as image_url, | |
| a.analysis_data, | |
| a.summary, | |
| a.recommendations, | |
| a.risk_score, | |
| a.risk_level | |
| FROM questionnaire_responses qr | |
| JOIN patients p ON qr.patient_id = p.id | |
| LEFT JOIN wounds w ON w.patient_id = CAST(p.id AS CHAR) | |
| LEFT JOIN wound_images wi ON wi.patient_id = CAST(p.id AS CHAR) | |
| LEFT JOIN ai_analyses a ON a.questionnaire_id = qr.id | |
| WHERE qr.practitioner_id = %s AND p.name = %s | |
| ORDER BY qr.submitted_at DESC | |
| """ | |
| params = (user_id, patient_name) | |
| else: | |
| query = """ | |
| SELECT | |
| qr.id as response_id, | |
| p.name as patient_name, | |
| p.age as patient_age, | |
| p.gender as patient_gender, | |
| qr.response_data, | |
| qr.submitted_at as visit_date, | |
| w.position as wound_location, | |
| w.moisture, | |
| w.infection, | |
| w.notes, | |
| wi.image as image_url, | |
| a.analysis_data, | |
| a.summary, | |
| a.recommendations, | |
| a.risk_score, | |
| a.risk_level | |
| FROM questionnaire_responses qr | |
| JOIN patients p ON qr.patient_id = p.id | |
| LEFT JOIN wounds w ON w.patient_id = CAST(p.id AS CHAR) | |
| LEFT JOIN wound_images wi ON wi.patient_id = CAST(p.id AS CHAR) | |
| LEFT JOIN ai_analyses a ON a.questionnaire_id = qr.id | |
| WHERE qr.practitioner_id = %s | |
| ORDER BY qr.submitted_at DESC | |
| """ | |
| params = (user_id,) | |
| result = self.db.execute_query(query, params, fetch=True) | |
| return result or [] | |
| except Exception as e: | |
| logging.error(f"Error fetching patient history: {e}") | |
| return [] | |
| def get_patient_list(self, user_id: int) -> List[Dict]: | |
| """Get list of all patients for this practitioner""" | |
| try: | |
| query = """ | |
| SELECT DISTINCT | |
| p.name as patient_name, | |
| p.age as patient_age, | |
| p.gender as patient_gender, | |
| COUNT(qr.id) as total_visits, | |
| MAX(qr.submitted_at) as last_visit, | |
| MIN(qr.submitted_at) as first_visit | |
| FROM questionnaire_responses qr | |
| JOIN patients p ON qr.patient_id = p.id | |
| WHERE qr.practitioner_id = %s | |
| GROUP BY p.name, p.age, p.gender | |
| ORDER BY last_visit DESC | |
| """ | |
| result = self.db.execute_query(query, (user_id,), fetch=True) | |
| return result or [] | |
| except Exception as e: | |
| logging.error(f"Error fetching patient list: {e}") | |
| return [] | |
| def get_wound_progression(self, user_id: int, patient_name: str) -> List[Dict]: | |
| """Get wound progression data for timeline visualization""" | |
| try: | |
| query = """ | |
| SELECT | |
| qr.submitted_at as visit_date, | |
| qr.response_data, | |
| w.position as wound_location, | |
| w.moisture, | |
| w.infection, | |
| a.risk_score, | |
| a.risk_level, | |
| a.summary, | |
| wi.image as image_url | |
| FROM questionnaire_responses qr | |
| JOIN patients p ON qr.patient_id = p.id | |
| LEFT JOIN wounds w ON w.patient_id = CAST(p.id AS CHAR) | |
| LEFT JOIN wound_images wi ON wi.patient_id = CAST(p.id AS CHAR) | |
| LEFT JOIN ai_analyses a ON a.questionnaire_id = qr.id | |
| WHERE qr.practitioner_id = %s AND p.name = %s | |
| ORDER BY qr.submitted_at ASC | |
| """ | |
| result = self.db.execute_query(query, (user_id, patient_name), fetch=True) | |
| return result or [] | |
| except Exception as e: | |
| logging.error(f"Error fetching wound progression: {e}") | |
| return [] | |
| def save_patient_note(self, user_id: int, patient_name: str, note: str) -> bool: | |
| """Save a clinical note for a patient""" | |
| try: | |
| # Check if notes table exists, if not use questionnaires additional_notes | |
| query = """ | |
| UPDATE questionnaires | |
| SET additional_notes = CONCAT(IFNULL(additional_notes, ''), '\n--- Clinical Note (', NOW(), ') ---\n', %s) | |
| WHERE user_id = %s AND patient_name = %s | |
| ORDER BY created_at DESC | |
| LIMIT 1 | |
| """ | |
| result = self.db.execute_query(query, (note, user_id, patient_name)) | |
| return bool(result) | |
| except Exception as e: | |
| logging.error(f"Error saving patient note: {e}") | |
| return False | |
| class ReportGenerator: | |
| """Professional HTML report generator for wound analysis""" | |
| def __init__(self): | |
| pass | |
| def generate_analysis_report(self, patient_data: Dict, analysis_data: Dict, image_url: str = None) -> str: | |
| """Generate comprehensive HTML report for wound analysis""" | |
| report_html = f""" | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SmartHeal AI - Wound Analysis Report</title> | |
| <style> | |
| body {{ | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| line-height: 1.6; | |
| margin: 0; | |
| padding: 20px; | |
| background-color: #f8f9fa; | |
| }} | |
| .report-container {{ | |
| max-width: 800px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.1); | |
| overflow: hidden; | |
| }} | |
| .header {{ | |
| background: linear-gradient(135deg, #3182ce 0%, #2c5aa0 100%); | |
| color: white; | |
| padding: 30px; | |
| text-align: center; | |
| }} | |
| .header h1 {{ | |
| margin: 0; | |
| font-size: 28px; | |
| font-weight: 600; | |
| }} | |
| .header p {{ | |
| margin: 10px 0 0 0; | |
| opacity: 0.9; | |
| font-size: 16px; | |
| }} | |
| .content {{ | |
| padding: 30px; | |
| }} | |
| .section {{ | |
| margin-bottom: 30px; | |
| border-left: 4px solid #3182ce; | |
| padding-left: 20px; | |
| }} | |
| .section h2 {{ | |
| color: #2c5aa0; | |
| margin-top: 0; | |
| font-size: 20px; | |
| font-weight: 600; | |
| }} | |
| .patient-info {{ | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 20px; | |
| margin-bottom: 20px; | |
| }} | |
| .info-card {{ | |
| background: #f8f9fa; | |
| padding: 15px; | |
| border-radius: 8px; | |
| border: 1px solid #e9ecef; | |
| }} | |
| .info-card h3 {{ | |
| margin: 0 0 10px 0; | |
| color: #495057; | |
| font-size: 14px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| }} | |
| .info-card p {{ | |
| margin: 0; | |
| font-weight: 500; | |
| color: #212529; | |
| }} | |
| .risk-indicator {{ | |
| display: inline-block; | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| font-size: 12px; | |
| letter-spacing: 0.5px; | |
| }} | |
| .risk-low {{ background: #d4edda; color: #155724; }} | |
| .risk-moderate {{ background: #fff3cd; color: #856404; }} | |
| .risk-high {{ background: #f8d7da; color: #721c24; }} | |
| .recommendations {{ | |
| background: #e7f3ff; | |
| border: 1px solid #b3d9ff; | |
| border-radius: 8px; | |
| padding: 20px; | |
| margin-top: 15px; | |
| }} | |
| .recommendations ul {{ | |
| margin: 10px 0; | |
| padding-left: 20px; | |
| }} | |
| .recommendations li {{ | |
| margin-bottom: 8px; | |
| color: #0056b3; | |
| }} | |
| .image-section {{ | |
| text-align: center; | |
| margin: 20px 0; | |
| }} | |
| .wound-image {{ | |
| max-width: 100%; | |
| height: auto; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
| }} | |
| .footer {{ | |
| background: #f8f9fa; | |
| padding: 20px 30px; | |
| text-align: center; | |
| color: #6c757d; | |
| border-top: 1px solid #e9ecef; | |
| }} | |
| .connect-clinician {{ | |
| background: #28a745; | |
| color: white; | |
| padding: 12px 30px; | |
| border: none; | |
| border-radius: 25px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| margin: 20px 0; | |
| font-size: 16px; | |
| }} | |
| .connect-clinician:hover {{ | |
| background: #218838; | |
| }} | |
| @media print {{ | |
| body {{ background: white; }} | |
| .report-container {{ box-shadow: none; }} | |
| .connect-clinician {{ display: none; }} | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="report-container"> | |
| <div class="header"> | |
| <h1>🩺 SmartHeal AI Wound Analysis Report</h1> | |
| <p>Advanced AI-Powered Clinical Assessment</p> | |
| </div> | |
| <div class="content"> | |
| <div class="section"> | |
| <h2>Patient Information</h2> | |
| <div class="patient-info"> | |
| <div class="info-card"> | |
| <h3>Patient Name</h3> | |
| <p>{patient_data.get('patient_name', 'N/A')}</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Age</h3> | |
| <p>{patient_data.get('patient_age', 'N/A')} years</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Gender</h3> | |
| <p>{patient_data.get('patient_gender', 'N/A')}</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Assessment Date</h3> | |
| <p>{datetime.now().strftime('%B %d, %Y at %I:%M %p')}</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="section"> | |
| <h2>Wound Assessment</h2> | |
| <div class="patient-info"> | |
| <div class="info-card"> | |
| <h3>Location</h3> | |
| <p>{patient_data.get('wound_location', 'N/A')}</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Duration</h3> | |
| <p>{patient_data.get('wound_duration', 'N/A')}</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Pain Level</h3> | |
| <p>{patient_data.get('pain_level', 'N/A')}/10</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Risk Assessment</h3> | |
| <p> | |
| <span class="risk-indicator risk-{analysis_data.get('risk_level', 'unknown').lower()}"> | |
| {analysis_data.get('risk_level', 'Unknown')} Risk | |
| </span> | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| {f''' | |
| <div class="section"> | |
| <h2>Wound Image</h2> | |
| <div class="image-section"> | |
| <img src="{image_url}" alt="Wound Image" class="wound-image"> | |
| </div> | |
| </div> | |
| ''' if image_url else ''} | |
| <div class="section"> | |
| <h2>AI Analysis Summary</h2> | |
| <p>{analysis_data.get('summary', 'No analysis summary available.')}</p> | |
| <div class="recommendations"> | |
| <h3>🎯 Clinical Recommendations</h3> | |
| {self._format_recommendations(analysis_data.get('recommendations', ''))} | |
| </div> | |
| </div> | |
| <div class="section"> | |
| <h2>Medical History</h2> | |
| <div class="patient-info"> | |
| <div class="info-card"> | |
| <h3>Medical History</h3> | |
| <p>{patient_data.get('medical_history', 'None reported')}</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Current Medications</h3> | |
| <p>{patient_data.get('medications', 'None reported')}</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Known Allergies</h3> | |
| <p>{patient_data.get('allergies', 'None reported')}</p> | |
| </div> | |
| <div class="info-card"> | |
| <h3>Additional Notes</h3> | |
| <p>{patient_data.get('additional_notes', 'None')}</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="text-align: center;"> | |
| <button class="connect-clinician" onclick="connectToClinician()"> | |
| 📞 Connect to Clinician | |
| </button> | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| <p><strong>SmartHeal AI</strong> - Advanced Wound Care Analysis & Clinical Support System</p> | |
| <p>Generated on {datetime.now().strftime('%B %d, %Y at %I:%M %p')} | For professional medical use only</p> | |
| <p>⚠️ This AI analysis is for clinical decision support only. Always consult with healthcare professionals for diagnosis and treatment.</p> | |
| </div> | |
| </div> | |
| <script> | |
| function connectToClinician() {{ | |
| 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'); | |
| }} | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| return report_html | |
| def _format_recommendations(self, recommendations: str) -> str: | |
| """Format recommendations as HTML list""" | |
| if not recommendations: | |
| return "<p>No specific recommendations available.</p>" | |
| # Split recommendations by common delimiters | |
| items = [] | |
| for delimiter in ['\n', '. ', '; ']: | |
| if delimiter in recommendations: | |
| items = [item.strip() for item in recommendations.split(delimiter) if item.strip()] | |
| break | |
| if not items: | |
| items = [recommendations] | |
| html = "<ul>" | |
| for item in items: | |
| if item and len(item) > 3: # Avoid very short fragments | |
| html += f"<li>{item}</li>" | |
| html += "</ul>" | |
| return html | |
| def generate_patient_history_report(self, patient_history: List[Dict]) -> str: | |
| """Generate comprehensive patient history report""" | |
| if not patient_history: | |
| return "<p>No patient history available.</p>" | |
| patient_name = patient_history[0].get('patient_name', 'Unknown Patient') | |
| html = f""" | |
| <div style="max-width: 800px; margin: 0 auto; font-family: 'Segoe UI', sans-serif;"> | |
| <div style="background: linear-gradient(135deg, #3182ce 0%, #2c5aa0 100%); color: white; padding: 20px; border-radius: 10px 10px 0 0;"> | |
| <h2 style="margin: 0;">📋 Patient History: {patient_name}</h2> | |
| <p style="margin: 10px 0 0 0; opacity: 0.9;">Complete Treatment Timeline</p> | |
| </div> | |
| <div style="background: white; border: 1px solid #e9ecef; border-top: none; border-radius: 0 0 10px 10px;"> | |
| """ | |
| for i, visit in enumerate(patient_history): | |
| visit_date = visit.get('visit_date', '') | |
| if isinstance(visit_date, str): | |
| try: | |
| visit_date = datetime.fromisoformat(visit_date.replace('Z', '+00:00')) | |
| except: | |
| pass | |
| risk_class = f"risk-{visit.get('risk_level', 'unknown').lower()}" | |
| html += f""" | |
| <div style="padding: 20px; border-bottom: 1px solid #f0f0f0;"> | |
| <div style="display: flex; justify-content: between; align-items: center; margin-bottom: 15px;"> | |
| <h3 style="color: #2c5aa0; margin: 0;">Visit #{len(patient_history) - i}</h3> | |
| <span style="color: #6c757d; font-size: 14px;">{visit_date.strftime('%B %d, %Y') if hasattr(visit_date, 'strftime') else str(visit_date)}</span> | |
| </div> | |
| <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-bottom: 15px;"> | |
| <div style="background: #f8f9fa; padding: 10px; border-radius: 5px;"> | |
| <strong>Location:</strong> {visit.get('wound_location', 'N/A')} | |
| </div> | |
| <div style="background: #f8f9fa; padding: 10px; border-radius: 5px;"> | |
| <strong>Pain Level:</strong> {visit.get('pain_level', 'N/A')}/10 | |
| </div> | |
| <div style="background: #f8f9fa; padding: 10px; border-radius: 5px;"> | |
| <strong>Risk:</strong> | |
| <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;"> | |
| {visit.get('risk_level', 'Unknown')} | |
| </span> | |
| </div> | |
| </div> | |
| {f'<p><strong>Summary:</strong> {visit.get("summary", "No summary available")}</p>' if visit.get("summary") else ""} | |
| {f'<p><strong>Recommendations:</strong> {visit.get("recommendations", "No recommendations available")}</p>' if visit.get("recommendations") else ""} | |
| </div> | |
| """ | |
| html += """ | |
| </div> | |
| </div> | |
| """ | |
| return html |