SmartHeal commited on
Commit
abdcfa7
·
verified ·
1 Parent(s): 49f9560

Update src/database.py

Browse files
Files changed (1) hide show
  1. src/database.py +433 -565
src/database.py CHANGED
@@ -1,565 +1,433 @@
1
- import mysql.connector
2
- from mysql.connector import Error
3
- import logging
4
- from datetime import datetime
5
- import json
6
- import uuid
7
- import os
8
- from PIL import Image
9
-
10
- class DatabaseManager:
11
- """Database operations manager for SmartHeal application"""
12
-
13
- def __init__(self, mysql_config):
14
- """Initialize database manager with MySQL configuration"""
15
- self.mysql_config = mysql_config
16
- self.test_connection()
17
-
18
- def test_connection(self):
19
- """Test database connection"""
20
- try:
21
- connection = self.get_connection()
22
- if connection:
23
- connection.close()
24
- logging.info("✅ Database connection successful")
25
- else:
26
- logging.error("❌ Database connection failed")
27
- except Exception as e:
28
- logging.error(f"Database connection test failed: {e}")
29
-
30
- def get_connection(self):
31
- """Get a database connection"""
32
- try:
33
- connection = mysql.connector.connect(**self.mysql_config)
34
- return connection
35
- except Error as e:
36
- logging.error(f"Error connecting to MySQL: {e}")
37
- return None
38
-
39
- def execute_query(self, query, params=None, fetch=False):
40
- """Execute a query and return results if fetch=True"""
41
- connection = self.get_connection()
42
- if not connection:
43
- return None
44
-
45
- cursor = None
46
- try:
47
- cursor = connection.cursor(dictionary=True)
48
- cursor.execute(query, params or ())
49
-
50
- if fetch:
51
- result = cursor.fetchall()
52
- else:
53
- connection.commit()
54
- result = cursor.rowcount
55
-
56
- return result
57
- except Error as e:
58
- logging.error(f"Error executing query: {e}")
59
- if connection:
60
- connection.rollback()
61
- return None
62
- finally:
63
- if cursor:
64
- cursor.close()
65
- if connection and connection.is_connected():
66
- connection.close()
67
-
68
- def execute_query_one(self, query, params=None):
69
- """Execute a query and return one result"""
70
- connection = self.get_connection()
71
- if not connection:
72
- return None
73
-
74
- cursor = None
75
- try:
76
- cursor = connection.cursor(dictionary=True)
77
- cursor.execute(query, params or ())
78
- result = cursor.fetchone()
79
- return result
80
- except Error as e:
81
- logging.error(f"Error executing query: {e}")
82
- return None
83
- finally:
84
- if cursor:
85
- cursor.close()
86
- if connection and connection.is_connected():
87
- connection.close()
88
-
89
- def create_tables(self):
90
- """Create all required database tables"""
91
- tables = {
92
- "users": """
93
- CREATE TABLE IF NOT EXISTS users (
94
- id INT AUTO_INCREMENT PRIMARY KEY,
95
- username VARCHAR(50) UNIQUE NOT NULL,
96
- email VARCHAR(100) UNIQUE NOT NULL,
97
- password VARCHAR(255) NOT NULL,
98
- name VARCHAR(100) NOT NULL,
99
- role ENUM('practitioner', 'organization') NOT NULL,
100
- org INT NULL,
101
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
102
- last_login TIMESTAMP NULL,
103
- is_active BOOLEAN DEFAULT TRUE,
104
- INDEX idx_username (username),
105
- INDEX idx_email (email),
106
- INDEX idx_role (role)
107
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
108
- """,
109
-
110
- "organizations": """
111
- CREATE TABLE IF NOT EXISTS organizations (
112
- id INT AUTO_INCREMENT PRIMARY KEY,
113
- name VARCHAR(200) NOT NULL,
114
- email VARCHAR(100) UNIQUE NOT NULL,
115
- phone VARCHAR(20),
116
- country_code VARCHAR(10),
117
- department VARCHAR(100),
118
- location VARCHAR(200),
119
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
120
- is_active BOOLEAN DEFAULT TRUE,
121
- INDEX idx_name (name),
122
- INDEX idx_email (email)
123
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
124
- """,
125
-
126
- "questionnaires": """
127
- CREATE TABLE IF NOT EXISTS questionnaires (
128
- id INT AUTO_INCREMENT PRIMARY KEY,
129
- user_id INT NOT NULL,
130
- patient_name VARCHAR(100) NOT NULL,
131
- patient_age INT,
132
- patient_gender ENUM('Male', 'Female', 'Other'),
133
- wound_location VARCHAR(200),
134
- wound_duration VARCHAR(100),
135
- pain_level INT DEFAULT 0,
136
- previous_treatment TEXT,
137
- medical_history TEXT,
138
- medications TEXT,
139
- allergies TEXT,
140
- additional_notes TEXT,
141
- moisture_level VARCHAR(50),
142
- infection_signs VARCHAR(50),
143
- diabetic_status VARCHAR(50),
144
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
145
- INDEX idx_user_id (user_id),
146
- INDEX idx_patient_name (patient_name),
147
- INDEX idx_created_at (created_at)
148
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
149
- """,
150
-
151
- "wound_images": """
152
- CREATE TABLE IF NOT EXISTS wound_images (
153
- id INT AUTO_INCREMENT PRIMARY KEY,
154
- questionnaire_id INT NOT NULL,
155
- image_url VARCHAR(500) NOT NULL,
156
- original_filename VARCHAR(255),
157
- file_size INT,
158
- image_width INT,
159
- image_height INT,
160
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
161
- INDEX idx_questionnaire_id (questionnaire_id),
162
- INDEX idx_created_at (created_at)
163
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
164
- """,
165
-
166
- "ai_analyses": """
167
- CREATE TABLE IF NOT EXISTS ai_analyses (
168
- id INT AUTO_INCREMENT PRIMARY KEY,
169
- questionnaire_id INT NOT NULL,
170
- image_id INT,
171
- analysis_data TEXT,
172
- summary TEXT,
173
- recommendations TEXT,
174
- risk_score INT DEFAULT 0,
175
- risk_level ENUM('Low', 'Moderate', 'High', 'Unknown') DEFAULT 'Unknown',
176
- wound_type VARCHAR(100),
177
- wound_dimensions VARCHAR(100),
178
- processing_time DECIMAL(5,2),
179
- model_version VARCHAR(50),
180
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
181
- INDEX idx_questionnaire_id (questionnaire_id),
182
- INDEX idx_risk_level (risk_level),
183
- INDEX idx_created_at (created_at)
184
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
185
- """,
186
-
187
- "analysis_sessions": """
188
- CREATE TABLE IF NOT EXISTS analysis_sessions (
189
- id INT AUTO_INCREMENT PRIMARY KEY,
190
- user_id INT NOT NULL,
191
- questionnaire_id INT NOT NULL,
192
- image_id INT,
193
- analysis_id INT,
194
- session_duration DECIMAL(5,2),
195
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
196
- INDEX idx_user_id (user_id),
197
- INDEX idx_created_at (created_at)
198
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
199
- """
200
- }
201
-
202
- for table_name, table_sql in tables.items():
203
- try:
204
- result = self.execute_query(table_sql)
205
- if result is not None:
206
- logging.info(f"Table '{table_name}' created or already exists")
207
- except Exception as e:
208
- logging.error(f"Error creating table '{table_name}': {e}")
209
-
210
- def save_questionnaire(self, questionnaire_data):
211
- """
212
- Save questionnaire response using the default questionnaire.
213
- This fixes the foreign key constraint issue by using an existing questionnaire ID.
214
- """
215
- connection = None
216
- cursor = None
217
- try:
218
- connection = self.get_connection()
219
- if not connection:
220
- return None
221
- cursor = connection.cursor()
222
-
223
- # (1) Create or get patient
224
- patient_id = self._create_or_get_patient(cursor, questionnaire_data)
225
- if not patient_id:
226
- raise Exception("Failed to get or create patient")
227
-
228
- # (2) Get default questionnaire ID
229
- cursor.execute("SELECT id FROM questionnaires WHERE name = 'Default Patient Assessment' LIMIT 1")
230
- questionnaire_row = cursor.fetchone()
231
-
232
- if not questionnaire_row:
233
- # Create default questionnaire if it doesn't exist
234
- cursor.execute("""
235
- INSERT INTO questionnaires (name, description, created_at)
236
- VALUES ('Default Patient Assessment', 'Standard patient wound assessment form', NOW())
237
- """)
238
- connection.commit()
239
- questionnaire_id = cursor.lastrowid
240
- else:
241
- questionnaire_id = questionnaire_row[0]
242
-
243
- # (3) Prepare response_data JSON
244
- response_data = {
245
- 'patient_info': {
246
- 'name': questionnaire_data['patient_name'],
247
- 'age': questionnaire_data['patient_age'],
248
- 'gender': questionnaire_data['patient_gender']
249
- },
250
- 'wound_details': {
251
- 'location': questionnaire_data['wound_location'],
252
- 'duration': questionnaire_data['wound_duration'],
253
- 'pain_level': questionnaire_data['pain_level'],
254
- 'moisture_level': questionnaire_data['moisture_level'],
255
- 'infection_signs': questionnaire_data['infection_signs'],
256
- 'diabetic_status': questionnaire_data['diabetic_status']
257
- },
258
- 'medical_history': {
259
- 'previous_treatment': questionnaire_data['previous_treatment'],
260
- 'medical_history': questionnaire_data['medical_history'],
261
- 'medications': questionnaire_data['medications'],
262
- 'allergies': questionnaire_data['allergies'],
263
- 'additional_notes': questionnaire_data['additional_notes']
264
- }
265
- }
266
-
267
- # (4) Insert into questionnaire_responses with correct foreign key
268
- insert_resp = """
269
- INSERT INTO questionnaire_responses
270
- (questionnaire_id, patient_id, practitioner_id, response_data, submitted_at)
271
- VALUES (%s, %s, %s, %s, %s)
272
- """
273
- cursor.execute(insert_resp, (
274
- questionnaire_id, # Use the existing questionnaire ID
275
- patient_id,
276
- questionnaire_data['user_id'],
277
- json.dumps(response_data),
278
- datetime.now()
279
- ))
280
- response_id = cursor.lastrowid
281
-
282
- connection.commit()
283
- logging.info(f"✅ Saved response ID {response_id} for questionnaire {questionnaire_id}")
284
- return response_id
285
-
286
- except Exception as e:
287
- logging.error(f"❌ Error saving questionnaire: {e}")
288
- if connection:
289
- connection.rollback()
290
- return None
291
- finally:
292
- if cursor:
293
- cursor.close()
294
- if connection:
295
- connection.close()
296
-
297
- def _create_or_get_patient(self, cursor, questionnaire_data):
298
- """Create or get existing patient record"""
299
- try:
300
- # Check if patient exists
301
- select_query = """
302
- SELECT id FROM patients
303
- WHERE name = %s AND age = %s AND gender = %s
304
- """
305
- cursor.execute(select_query, (
306
- questionnaire_data['patient_name'],
307
- questionnaire_data['patient_age'],
308
- questionnaire_data['patient_gender']
309
- ))
310
-
311
- existing_patient = cursor.fetchone()
312
- if existing_patient:
313
- return existing_patient[0]
314
-
315
- # Create new patient
316
- import uuid
317
- insert_query = """
318
- INSERT INTO patients (
319
- uuid, name, age, gender, illness, allergy, notes, created_at
320
- ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
321
- """
322
-
323
- patient_uuid = str(uuid.uuid4())
324
- cursor.execute(insert_query, (
325
- patient_uuid,
326
- questionnaire_data['patient_name'],
327
- questionnaire_data['patient_age'],
328
- questionnaire_data['patient_gender'],
329
- questionnaire_data.get('medical_history', ''),
330
- questionnaire_data.get('allergies', ''),
331
- questionnaire_data.get('additional_notes', ''),
332
- datetime.now()
333
- ))
334
-
335
- return cursor.lastrowid
336
-
337
- except Exception as e:
338
- logging.error(f"Error creating/getting patient: {e}")
339
- return None
340
-
341
- def _create_wound_record(self, cursor, patient_id, questionnaire_data):
342
- """Create wound record"""
343
- try:
344
- import uuid
345
- wound_uuid = str(uuid.uuid4())
346
-
347
- query = """
348
- INSERT INTO wounds (
349
- uuid, patient_id, position, category, moisture, infection,
350
- notes, created_at
351
- ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
352
- """
353
-
354
- cursor.execute(query, (
355
- wound_uuid,
356
- str(patient_id),
357
- questionnaire_data.get('wound_location', ''),
358
- 'Assessment', # Default category
359
- questionnaire_data.get('moisture_level', ''),
360
- questionnaire_data.get('infection_signs', ''),
361
- questionnaire_data.get('additional_notes', ''),
362
- datetime.now()
363
- ))
364
-
365
- return cursor.lastrowid
366
-
367
- except Exception as e:
368
- logging.error(f"Error creating wound record: {e}")
369
- return None
370
-
371
- def save_wound_image(self, patient_id, image):
372
- """Save wound image to filesystem and database"""
373
- try:
374
- import uuid
375
- import os
376
-
377
- # Generate unique filename
378
- image_id = str(uuid.uuid4())
379
- filename = f"wound_{image_id}.jpg"
380
- file_path = os.path.join("uploads", filename)
381
-
382
- # Ensure uploads directory exists
383
- os.makedirs("uploads", exist_ok=True)
384
-
385
- # Save image to disk
386
- if hasattr(image, 'save'):
387
- image.save(file_path, format='JPEG', quality=95)
388
-
389
- # Get image dimensions
390
- width, height = image.size
391
-
392
- # Save to database using proper wound_images table structure
393
- query = """
394
- INSERT INTO wound_images (
395
- uuid, patient_id, image, width, height, created_at
396
- ) VALUES (%s, %s, %s, %s, %s, %s)
397
- """
398
-
399
- params = (
400
- str(uuid.uuid4()),
401
- str(patient_id),
402
- file_path,
403
- str(width),
404
- str(height),
405
- datetime.now()
406
- )
407
-
408
- connection = self.get_connection()
409
- if not connection:
410
- return None
411
-
412
- try:
413
- cursor = connection.cursor()
414
- cursor.execute(query, params)
415
- connection.commit()
416
- image_db_id = cursor.lastrowid
417
-
418
- logging.info(f"Image saved with ID: {image_db_id}")
419
- return {
420
- 'id': image_db_id,
421
- 'filename': filename,
422
- 'path': file_path
423
- }
424
- except Error as e:
425
- logging.error(f"Error saving image to database: {e}")
426
- connection.rollback()
427
- return None
428
- finally:
429
- cursor.close()
430
- connection.close()
431
- else:
432
- logging.error("Invalid image object")
433
- return None
434
-
435
- except Exception as e:
436
- logging.error(f"Image save error: {e}")
437
- return None
438
-
439
- def save_analysis(self, questionnaire_id, image_id, analysis_data):
440
- """Save AI analysis results to database"""
441
- try:
442
- query = """
443
- INSERT INTO ai_analyses (
444
- questionnaire_id, image_id, analysis_data, summary,
445
- recommendations, risk_score, risk_level, wound_type,
446
- wound_dimensions, processing_time, model_version, created_at
447
- ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
448
- """
449
-
450
- params = (
451
- questionnaire_id,
452
- image_id,
453
- json.dumps(analysis_data) if analysis_data else None,
454
- analysis_data.get('summary', ''),
455
- analysis_data.get('recommendations', ''),
456
- analysis_data.get('risk_score', 0),
457
- analysis_data.get('risk_level', 'Unknown'),
458
- analysis_data.get('wound_type', ''),
459
- analysis_data.get('wound_dimensions', ''),
460
- analysis_data.get('processing_time', 0.0),
461
- analysis_data.get('model_version', 'v1.0'),
462
- datetime.now()
463
- )
464
-
465
- connection = self.get_connection()
466
- if not connection:
467
- return None
468
-
469
- try:
470
- cursor = connection.cursor()
471
- cursor.execute(query, params)
472
- connection.commit()
473
- analysis_id = cursor.lastrowid
474
- logging.info(f"Analysis saved with ID: {analysis_id}")
475
- return analysis_id
476
- except Error as e:
477
- logging.error(f"Error saving analysis: {e}")
478
- connection.rollback()
479
- return None
480
- finally:
481
- cursor.close()
482
- connection.close()
483
-
484
- except Exception as e:
485
- logging.error(f"Analysis save error: {e}")
486
- return None
487
-
488
- def get_user_history(self, user_id):
489
- """Get user's analysis history"""
490
- try:
491
- query = """
492
- SELECT
493
- q.patient_name,
494
- q.wound_location,
495
- q.created_at,
496
- a.risk_level,
497
- a.summary,
498
- a.recommendations
499
- FROM questionnaires q
500
- LEFT JOIN ai_analyses a ON q.id = a.questionnaire_id
501
- WHERE q.user_id = %s
502
- ORDER BY q.created_at DESC
503
- LIMIT 20
504
- """
505
-
506
- result = self.execute_query(query, (user_id,), fetch=True)
507
- return result or []
508
-
509
- except Exception as e:
510
- logging.error(f"Error fetching user history: {e}")
511
- return []
512
-
513
- def get_organizations(self):
514
- """Get list of all organizations"""
515
- try:
516
- query = "SELECT id, name as org_name, location FROM organizations ORDER BY name"
517
- result = self.execute_query(query, fetch=True)
518
- return result or [{'id': 1, 'org_name': 'Default Hospital', 'location': 'Default Location'}]
519
- except Exception as e:
520
- logging.error(f"Error getting organizations: {e}")
521
- return [{'id': 1, 'org_name': 'Default Hospital', 'location': 'Default Location'}]
522
-
523
- def create_organization(self, org_data):
524
- """Create a new organization"""
525
- try:
526
- query = """INSERT INTO organizations (name, email, phone, country_code, department, location, created_at)
527
- VALUES (%s, %s, %s, %s, %s, %s, %s)"""
528
- params = (
529
- org_data.get('org_name', ''),
530
- org_data.get('email', ''),
531
- org_data.get('phone', ''),
532
- org_data.get('country_code', ''),
533
- org_data.get('department', ''),
534
- org_data.get('location', ''),
535
- datetime.now()
536
- )
537
- result = self.execute_query(query, params)
538
- if result:
539
- # Get the created organization ID
540
- org_id = self.execute_query_one(
541
- "SELECT id FROM organizations WHERE name = %s ORDER BY created_at DESC LIMIT 1",
542
- (org_data.get('org_name', ''),)
543
- )
544
- return org_id['id'] if org_id else None
545
- return None
546
- except Exception as e:
547
- logging.error(f"Error creating organization: {e}")
548
- return None
549
-
550
- def save_analysis_result(self, questionnaire_id, analysis_result):
551
- """Save analysis result to database"""
552
- try:
553
- query = """INSERT INTO ai_analyses (questionnaire_id, analysis_data, summary, created_at)
554
- VALUES (%s, %s, %s, %s)"""
555
- params = (
556
- questionnaire_id,
557
- json.dumps(analysis_result) if analysis_result else None,
558
- analysis_result.get('summary', 'Analysis completed'),
559
- datetime.now()
560
- )
561
- result = self.execute_query(query, params)
562
- return result
563
- except Exception as e:
564
- logging.error(f"Error saving analysis result: {e}")
565
- return None
 
1
+ import mysql.connector
2
+ from mysql.connector import Error
3
+ import logging
4
+ from datetime import datetime
5
+ import json
6
+ import uuid
7
+ import os
8
+ from PIL import Image
9
+
10
+ class DatabaseManager:
11
+ """Database operations manager for SmartHeal application (aligned to existing schema)."""
12
+
13
+ def __init__(self, mysql_config):
14
+ self.mysql_config = mysql_config
15
+ self.test_connection()
16
+ self._ensure_default_questionnaire()
17
+
18
+ # ---------------------- Connection helpers ----------------------
19
+
20
+ def test_connection(self):
21
+ try:
22
+ conn = self.get_connection()
23
+ if conn:
24
+ conn.close()
25
+ logging.info("✅ Database connection successful")
26
+ else:
27
+ logging.error("❌ Database connection failed")
28
+ except Exception as e:
29
+ logging.error(f"Database connection test failed: {e}")
30
+
31
+ def get_connection(self):
32
+ try:
33
+ return mysql.connector.connect(**self.mysql_config)
34
+ except Error as e:
35
+ logging.error(f"Error connecting to MySQL: {e}")
36
+ return None
37
+
38
+ def execute_query(self, query, params=None, fetch=False):
39
+ conn = self.get_connection()
40
+ if not conn:
41
+ return None
42
+ cur = None
43
+ try:
44
+ cur = conn.cursor(dictionary=True)
45
+ cur.execute(query, params or ())
46
+ if fetch:
47
+ return cur.fetchall()
48
+ conn.commit()
49
+ return cur.rowcount
50
+ except Error as e:
51
+ logging.error(f"Error executing query: {e} | SQL: {query} | Params: {params}")
52
+ if conn:
53
+ conn.rollback()
54
+ return None
55
+ finally:
56
+ if cur: cur.close()
57
+ if conn and conn.is_connected(): conn.close()
58
+
59
+ def execute_query_one(self, query, params=None):
60
+ conn = self.get_connection()
61
+ if not conn:
62
+ return None
63
+ cur = None
64
+ try:
65
+ cur = conn.cursor(dictionary=True)
66
+ cur.execute(query, params or ())
67
+ return cur.fetchone()
68
+ except Error as e:
69
+ logging.error(f"Error executing query: {e} | SQL: {query} | Params: {params}")
70
+ return None
71
+ finally:
72
+ if cur: cur.close()
73
+ if conn and conn.is_connected(): conn.close()
74
+
75
+ # ---------------------- One-time ensures ----------------------
76
+
77
+ def _ensure_default_questionnaire(self):
78
+ """Ensure a 'Default Patient Assessment' row exists in questionnaires."""
79
+ try:
80
+ row = self.execute_query_one(
81
+ "SELECT id FROM questionnaires WHERE name = %s LIMIT 1",
82
+ ("Default Patient Assessment",)
83
+ )
84
+ if not row:
85
+ self.execute_query(
86
+ "INSERT INTO questionnaires (name, description, created_at, updated_at) VALUES (%s, %s, NOW(), NOW())",
87
+ ("Default Patient Assessment", "Standard patient wound assessment form")
88
+ )
89
+ logging.info("Created default questionnaire 'Default Patient Assessment'")
90
+ except Exception as e:
91
+ logging.error(f"Error ensuring default questionnaire: {e}")
92
+
93
+ # ---------------------- Business ops ----------------------
94
+
95
+ def save_questionnaire(self, questionnaire_data):
96
+ """
97
+ Creates/gets patient in EXISTING `patients` table, then creates a row in `questionnaire_responses`.
98
+ Returns the `questionnaire_response_id` (int) or None.
99
+ """
100
+ conn = None
101
+ cur = None
102
+ try:
103
+ conn = self.get_connection()
104
+ if not conn:
105
+ return None
106
+ cur = conn.cursor(dictionary=True)
107
+
108
+ # 1) Create or get patient (EXISTING `patients` table)
109
+ patient_id = self._create_or_get_patient(cur, questionnaire_data)
110
+ if not patient_id:
111
+ raise Exception("Failed to get or create patient")
112
+
113
+ # 2) Get template questionnaire id
114
+ cur.execute("SELECT id FROM questionnaires WHERE name = %s LIMIT 1",
115
+ ("Default Patient Assessment",))
116
+ row = cur.fetchone()
117
+ questionnaire_id = row["id"] if row else None
118
+ if not questionnaire_id:
119
+ raise Exception("Default questionnaire not found")
120
+
121
+ # 3) Build response_data
122
+ response_data = {
123
+ 'patient_info': {
124
+ 'name': questionnaire_data.get('patient_name'),
125
+ 'age': questionnaire_data.get('patient_age'),
126
+ 'gender': questionnaire_data.get('patient_gender')
127
+ },
128
+ 'wound_details': {
129
+ 'location': questionnaire_data.get('wound_location'),
130
+ 'duration': questionnaire_data.get('wound_duration'),
131
+ 'pain_level': questionnaire_data.get('pain_level'),
132
+ 'moisture_level': questionnaire_data.get('moisture_level'),
133
+ 'infection_signs': questionnaire_data.get('infection_signs'),
134
+ 'diabetic_status': questionnaire_data.get('diabetic_status')
135
+ },
136
+ 'medical_history': {
137
+ 'previous_treatment': questionnaire_data.get('previous_treatment'),
138
+ 'medical_history': questionnaire_data.get('medical_history'),
139
+ 'medications': questionnaire_data.get('medications'),
140
+ 'allergies': questionnaire_data.get('allergies'),
141
+ 'additional_notes': questionnaire_data.get('additional_notes')
142
+ }
143
+ }
144
+
145
+ practitioner_id = questionnaire_data.get('user_id')
146
+ if not practitioner_id:
147
+ # Fall back gracefully; your schema expects NOT NULL here.
148
+ practitioner_id = 1
149
+
150
+ # 4) Insert into `questionnaire_responses`
151
+ insert_sql = """
152
+ INSERT INTO questionnaire_responses
153
+ (questionnaire_id, patient_id, practitioner_id, response_data, submitted_at)
154
+ VALUES (%s, %s, %s, %s, %s)
155
+ """
156
+ cur.execute(insert_sql, (
157
+ questionnaire_id,
158
+ patient_id, # <-- This is patients.id (BIGINT) and WILL be filled now.
159
+ practitioner_id,
160
+ json.dumps(response_data, ensure_ascii=False),
161
+ datetime.now()
162
+ ))
163
+ response_id = cur.lastrowid
164
+ conn.commit()
165
+
166
+ logging.info(f" Saved questionnaire response ID {response_id} (patient_id={patient_id})")
167
+ return response_id
168
+
169
+ except Exception as e:
170
+ logging.error(f"❌ Error saving questionnaire: {e}")
171
+ if conn: conn.rollback()
172
+ return None
173
+ finally:
174
+ if cur: cur.close()
175
+ if conn: conn.close()
176
+
177
+ def _create_or_get_patient(self, cur, questionnaire_data):
178
+ """
179
+ Works against the EXISTING `patients` table:
180
+ columns include id (PK), uuid, name, age (int), gender (varchar), illness, allergy, notes, etc.
181
+ Returns patients.id (int).
182
+ """
183
+ try:
184
+ name = questionnaire_data.get('patient_name')
185
+ age = questionnaire_data.get('patient_age')
186
+ gender = questionnaire_data.get('patient_gender')
187
+
188
+ # Try to find an existing patient via (name, age, gender)
189
+ cur.execute("""
190
+ SELECT id FROM patients
191
+ WHERE name = %s AND (age = %s OR %s IS NULL) AND (gender = %s OR %s IS NULL)
192
+ LIMIT 1
193
+ """, (name, age, age, gender, gender))
194
+ row = cur.fetchone()
195
+ if row:
196
+ return row["id"]
197
+
198
+ # Create new patient
199
+ cur.execute("""
200
+ INSERT INTO patients (uuid, name, age, gender, illness, allergy, notes, created_at, updated_at)
201
+ VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
202
+ """, (
203
+ str(uuid.uuid4()),
204
+ name,
205
+ int(age) if (isinstance(age, (int, float, str)) and str(age).isdigit()) else None,
206
+ gender,
207
+ questionnaire_data.get('medical_history', ''),
208
+ questionnaire_data.get('allergies', ''),
209
+ questionnaire_data.get('additional_notes', '')
210
+ ))
211
+ return cur.lastrowid
212
+
213
+ except Exception as e:
214
+ logging.error(f"Error creating/getting patient: {e}")
215
+ return None
216
+
217
+ def _get_patient_uuid(self, patient_id, cur=None):
218
+ """Fetch patients.uuid for a given patients.id (helps when tables store patient_id as VARCHAR uuid)."""
219
+ owns_cursor = False
220
+ conn = None
221
+ try:
222
+ if cur is None:
223
+ conn = self.get_connection()
224
+ if not conn:
225
+ return None
226
+ cur = conn.cursor(dictionary=True)
227
+ owns_cursor = True
228
+
229
+ cur.execute("SELECT uuid FROM patients WHERE id = %s LIMIT 1", (patient_id,))
230
+ row = cur.fetchone()
231
+ return row["uuid"] if row else None
232
+ except Exception as e:
233
+ logging.error(f"Error fetching patient uuid: {e}")
234
+ return None
235
+ finally:
236
+ if owns_cursor and cur: cur.close()
237
+ if owns_cursor and conn: conn.close()
238
+
239
+ def save_wound_image(self, patient_id, image):
240
+ """
241
+ Save wound image to filesystem and EXISTING `wound_images` table.
242
+ Your `wound_images.patient_id` is VARCHAR, so we store patients.uuid there.
243
+ Returns dict {id, filename, path} or None.
244
+ """
245
+ try:
246
+ # 1) Persist to disk
247
+ image_uid = str(uuid.uuid4())
248
+ filename = f"wound_{image_uid}.jpg"
249
+ os.makedirs("uploads", exist_ok=True)
250
+ file_path = os.path.join("uploads", filename)
251
+
252
+ if hasattr(image, 'save'):
253
+ image.save(file_path, format='JPEG', quality=95)
254
+ width, height = image.size
255
+ file_size = os.path.getsize(file_path)
256
+ original_filename = getattr(image, "filename", filename)
257
+ elif isinstance(image, str) and os.path.exists(image):
258
+ with Image.open(image) as pil:
259
+ pil = pil.convert('RGB')
260
+ pil.save(file_path, format='JPEG', quality=95)
261
+ width, height = pil.size
262
+ file_size = os.path.getsize(file_path)
263
+ original_filename = os.path.basename(image)
264
+ else:
265
+ logging.error("Invalid image object/path")
266
+ return None
267
+
268
+ # 2) Resolve patients.uuid (VARCHAR target)
269
+ conn = self.get_connection()
270
+ if not conn: return None
271
+ cur = conn.cursor()
272
+ try:
273
+ patient_uuid = self._get_patient_uuid(patient_id)
274
+ if not patient_uuid:
275
+ raise Exception("Patient UUID not found for given patient_id")
276
+
277
+ # Insert into existing wound_images schema (uuid, patient_id (varchar), image, width, height, etc.)
278
+ cur.execute("""
279
+ INSERT INTO wound_images (
280
+ uuid, patient_id, image, width, height, area, notes, created_at, updated_at
281
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
282
+ """, (
283
+ str(uuid.uuid4()),
284
+ patient_uuid, # VARCHAR column → store patient's UUID
285
+ file_path,
286
+ str(width),
287
+ str(height),
288
+ None,
289
+ None
290
+ ))
291
+ conn.commit()
292
+ image_db_id = cur.lastrowid
293
+ logging.info(f"Image saved with ID: {image_db_id}")
294
+ return {'id': image_db_id, 'filename': filename, 'path': file_path}
295
+ finally:
296
+ cur.close()
297
+ conn.close()
298
+
299
+ except Exception as e:
300
+ logging.error(f"Image save error: {e}")
301
+ return None
302
+
303
+ def save_analysis(self, questionnaire_response_id, image_id, analysis_data):
304
+ """
305
+ Save AI analysis results.
306
+ Your live `ai_analyses` table expects `questionnaire_id` (the TEMPLATE), not the response id.
307
+ So we first look up the template id from questionnaire_responses and store that.
308
+ """
309
+ try:
310
+ # Resolve template questionnaire_id from the response
311
+ row = self.execute_query_one(
312
+ "SELECT questionnaire_id FROM questionnaire_responses WHERE id = %s LIMIT 1",
313
+ (questionnaire_response_id,)
314
+ )
315
+ if not row:
316
+ logging.error("No questionnaire_response found for analysis save")
317
+ return None
318
+ questionnaire_id = row["questionnaire_id"]
319
+
320
+ query = """
321
+ INSERT INTO ai_analyses (
322
+ questionnaire_id, image_id, analysis_data, summary,
323
+ recommendations, risk_score, risk_level,
324
+ processing_time, model_version, created_at
325
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
326
+ """
327
+ params = (
328
+ questionnaire_id,
329
+ image_id,
330
+ json.dumps(analysis_data) if analysis_data else None,
331
+ (analysis_data or {}).get('summary', ''),
332
+ (analysis_data or {}).get('recommendations', ''),
333
+ (analysis_data or {}).get('risk_score', 0),
334
+ (analysis_data or {}).get('risk_level', 'Unknown'),
335
+ (analysis_data or {}).get('processing_time', 0.0),
336
+ (analysis_data or {}).get('model_version', 'v1.0'),
337
+ datetime.now()
338
+ )
339
+ return self.execute_query(query, params, fetch=False)
340
+ except Exception as e:
341
+ logging.error(f"Analysis save error: {e}")
342
+ return None
343
+
344
+ def get_user_history(self, user_id):
345
+ """
346
+ Latest 20 submissions for a practitioner, with optional analysis summary if present.
347
+ Aligns to existing schema: joins questionnaire_responses -> questionnaires and LEFT joins ai_analyses (by template).
348
+ """
349
+ try:
350
+ query = """
351
+ SELECT
352
+ r.id AS response_id,
353
+ r.submitted_at,
354
+ p.name AS patient_name,
355
+ p.age AS patient_age,
356
+ p.gender AS patient_gender,
357
+ q.name AS questionnaire_name,
358
+ a.risk_level,
359
+ a.summary,
360
+ a.recommendations
361
+ FROM questionnaire_responses r
362
+ JOIN patients p ON p.id = r.patient_id
363
+ JOIN questionnaires q ON q.id = r.questionnaire_id
364
+ LEFT JOIN ai_analyses a ON a.questionnaire_id = r.questionnaire_id
365
+ WHERE r.practitioner_id = %s
366
+ ORDER BY r.submitted_at DESC
367
+ LIMIT 20
368
+ """
369
+ result = self.execute_query(query, (user_id,), fetch=True)
370
+ return result or []
371
+ except Exception as e:
372
+ logging.error(f"Error fetching user history: {e}")
373
+ return []
374
+
375
+ # Convenience for existing callers (kept signature)
376
+ def create_organization(self, org_data):
377
+ try:
378
+ query = """INSERT INTO organizations (name, email, phone, country_code, department, location, created_at, updated_at)
379
+ VALUES (%s, %s, %s, %s, %s, %s, NOW(), NOW())"""
380
+ params = (
381
+ org_data.get('org_name', ''),
382
+ org_data.get('email', ''),
383
+ org_data.get('phone', ''),
384
+ org_data.get('country_code', ''),
385
+ org_data.get('department', ''),
386
+ org_data.get('location', '')
387
+ )
388
+ rc = self.execute_query(query, params)
389
+ if rc:
390
+ row = self.execute_query_one(
391
+ "SELECT id FROM organizations WHERE name = %s ORDER BY created_at DESC LIMIT 1",
392
+ (org_data.get('org_name', ''),)
393
+ )
394
+ return row['id'] if row else None
395
+ return None
396
+ except Exception as e:
397
+ logging.error(f"Error creating organization: {e}")
398
+ return None
399
+
400
+ def get_organizations(self):
401
+ try:
402
+ query = "SELECT id, name as org_name, location FROM organizations ORDER BY name"
403
+ result = self.execute_query(query, fetch=True)
404
+ return result or [{'id': 1, 'org_name': 'Default Hospital', 'location': 'Default Location'}]
405
+ except Exception as e:
406
+ logging.error(f"Error getting organizations: {e}")
407
+ return [{'id': 1, 'org_name': 'Default Hospital', 'location': 'Default Location'}]
408
+
409
+ # Back-compat helper kept (writes same as save_analysis, minimal data)
410
+ def save_analysis_result(self, questionnaire_response_id, analysis_result):
411
+ try:
412
+ # store under template id like save_analysis()
413
+ row = self.execute_query_one(
414
+ "SELECT questionnaire_id FROM questionnaire_responses WHERE id = %s LIMIT 1",
415
+ (questionnaire_response_id,)
416
+ )
417
+ if not row:
418
+ logging.error("No questionnaire_response found for analysis_result save")
419
+ return None
420
+ questionnaire_id = row["questionnaire_id"]
421
+
422
+ query = """INSERT INTO ai_analyses (questionnaire_id, analysis_data, summary, created_at)
423
+ VALUES (%s, %s, %s, %s)"""
424
+ params = (
425
+ questionnaire_id,
426
+ json.dumps(analysis_result) if analysis_result else None,
427
+ (analysis_result or {}).get('summary', 'Analysis completed'),
428
+ datetime.now()
429
+ )
430
+ return self.execute_query(query, params)
431
+ except Exception as e:
432
+ logging.error(f"Error saving analysis result: {e}")
433
+ return None