medicafe 0.250814.3__py3-none-any.whl → 0.250816.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
MediBot/MediBot.py CHANGED
@@ -11,6 +11,56 @@ try:
11
11
  except ImportError:
12
12
  msvcrt = None # Not available on non-Windows systems
13
13
  from collections import OrderedDict
14
+ from datetime import datetime # Added for primary surgery date logic
15
+
16
+ # ============================================================================
17
+ # MINIMAL PROTECTION: Import State Validation
18
+ # ============================================================================
19
+
20
+ def validate_critical_imports():
21
+ """Validate that critical imports are in expected state before proceeding"""
22
+ critical_modules = {
23
+ 'MediBot_Preprocessor': None,
24
+ 'MediBot_Preprocessor_lib': None,
25
+ 'MediBot_UI': None,
26
+ 'MediBot_Crosswalk_Library': None
27
+ }
28
+
29
+ # Test imports and capture state
30
+ try:
31
+ print("Testing MediCafe.core_utils import...")
32
+ from MediCafe.core_utils import import_medibot_module_with_debug
33
+ print("MediCafe.core_utils import successful")
34
+
35
+ for module_name in critical_modules.keys():
36
+ print("Testing {} import...".format(module_name))
37
+ try:
38
+ module = import_medibot_module_with_debug(module_name)
39
+ critical_modules[module_name] = module
40
+ if module is None:
41
+ print(" WARNING: {} import returned None".format(module_name))
42
+ else:
43
+ print(" SUCCESS: {} import successful".format(module_name))
44
+ except Exception as e:
45
+ print(" ERROR: {} import failed with exception: {}".format(module_name, e))
46
+ critical_modules[module_name] = None
47
+ except Exception as e:
48
+ print("CRITICAL: Failed to import core utilities: {}".format(e))
49
+ return False, critical_modules
50
+
51
+ # Check for None imports (the specific failure pattern)
52
+ failed_imports = []
53
+ for module_name, module in critical_modules.items():
54
+ if module is None:
55
+ failed_imports.append(module_name)
56
+
57
+ if failed_imports:
58
+ print("CRITICAL: Import failures detected:")
59
+ for failed in failed_imports:
60
+ print(" - {}: Import returned None".format(failed))
61
+ return False, critical_modules
62
+
63
+ return True, critical_modules
14
64
 
15
65
  # Use core utilities for standardized imports
16
66
  from MediCafe.core_utils import (
@@ -122,6 +172,68 @@ def identify_field(header, field_mapping):
122
172
  # Add this print to a function that is calling identify_field
123
173
  #print("Warning: No matching field found for CSV header '{}'".format(header))
124
174
 
175
+ def create_patient_entries_from_row(row, reverse_mapping):
176
+ """
177
+ Helper function to create patient entries from a row with surgery date handling.
178
+
179
+ Args:
180
+ row: The CSV row containing patient data
181
+ reverse_mapping: The reverse mapping for field lookups
182
+
183
+ Returns:
184
+ list: List of tuples (surgery_date, patient_name, patient_id, diagnosis_code, row)
185
+ """
186
+ patient_id = row.get(reverse_mapping['Patient ID #2'])
187
+ patient_name = row.get(reverse_mapping['Patient Name'])
188
+
189
+ # Get all surgery dates for this patient
190
+ all_surgery_dates = row.get('_all_surgery_dates', [row.get('Surgery Date')])
191
+ surgery_date_to_diagnosis = row.get('_surgery_date_to_diagnosis', {})
192
+
193
+ patient_entries = []
194
+
195
+ # Sort surgery dates chronologically to ensure proper ordering
196
+ sorted_surgery_dates = []
197
+ for surgery_date in all_surgery_dates:
198
+ try:
199
+ if hasattr(surgery_date, 'strftime'):
200
+ # Already a datetime object
201
+ sorted_surgery_dates.append(surgery_date)
202
+ elif isinstance(surgery_date, str):
203
+ # Convert string to datetime for sorting
204
+ surgery_date_dt = datetime.strptime(surgery_date, '%m-%d-%Y')
205
+ sorted_surgery_dates.append(surgery_date_dt)
206
+ else:
207
+ # Fallback - use as is
208
+ sorted_surgery_dates.append(surgery_date)
209
+ except (ValueError, TypeError):
210
+ # If parsing fails, use the original value
211
+ sorted_surgery_dates.append(surgery_date)
212
+
213
+ # Sort the dates chronologically
214
+ sorted_surgery_dates.sort()
215
+
216
+ # Create entries for each surgery date in chronological order
217
+ # The enhanced table display will group by patient_id and show dashed lines for secondary dates
218
+ for surgery_date in sorted_surgery_dates:
219
+ try:
220
+ if hasattr(surgery_date, 'strftime'):
221
+ surgery_date_str = surgery_date.strftime('%m-%d-%Y')
222
+ elif isinstance(surgery_date, str):
223
+ surgery_date_str = surgery_date
224
+ else:
225
+ surgery_date_str = str(surgery_date)
226
+ except Exception:
227
+ surgery_date_str = str(surgery_date)
228
+
229
+ # Get the diagnosis code for this surgery date
230
+ diagnosis_code = surgery_date_to_diagnosis.get(surgery_date_str, 'N/A')
231
+
232
+ # Add entry for this surgery date
233
+ patient_entries.append((surgery_date, patient_name, patient_id, diagnosis_code, row))
234
+
235
+ return patient_entries
236
+
125
237
  # Global flag to control AHK execution method - set to True to use optimized stdin method
126
238
  USE_AHK_STDIN_OPTIMIZATION = True
127
239
 
@@ -512,6 +624,19 @@ if __name__ == "__main__":
512
624
  try:
513
625
  if PERFORMANCE_LOGGING:
514
626
  print("Initializing. Loading configuration and preparing environment...")
627
+
628
+ # PROTECTION: Validate critical imports before proceeding
629
+ print("Validating critical imports...")
630
+ import_valid, import_state = validate_critical_imports()
631
+ if not import_valid:
632
+ print("CRITICAL: Import validation failed. Cannot continue.")
633
+ print("This indicates a fundamental system configuration issue.")
634
+ print("Please check:")
635
+ print(" 1. All MediBot modules exist in the MediBot directory")
636
+ print(" 2. Python path is correctly configured")
637
+ print(" 3. No syntax errors in MediBot modules")
638
+ sys.exit(1)
639
+
515
640
  # Use MediCafe configuration system
516
641
  try:
517
642
  config_loader = get_config_loader_with_fallback()
@@ -553,6 +678,22 @@ if __name__ == "__main__":
553
678
  print("Starting CSV preprocessing at: {}".format(time.strftime("%H:%M:%S")))
554
679
  MediLink_ConfigLoader.log("Starting CSV preprocessing at: {}".format(time.strftime("%H:%M:%S")), level="INFO")
555
680
 
681
+ # PROTECTION: Validate MediBot_Preprocessor before calling preprocess_csv_data
682
+ if MediBot_Preprocessor is None:
683
+ print("CRITICAL: MediBot_Preprocessor is None when trying to call preprocess_csv_data")
684
+ print("This indicates the import failed silently during execution.")
685
+ print("Import state at failure:")
686
+ for module_name, module in import_state.items():
687
+ status = "None" if module is None else "OK"
688
+ print(" - {}: {}".format(module_name, status))
689
+ print("Please check for syntax errors or missing dependencies in MediBot modules.")
690
+ sys.exit(1)
691
+
692
+ if not hasattr(MediBot_Preprocessor, 'preprocess_csv_data'):
693
+ print("CRITICAL: MediBot_Preprocessor missing preprocess_csv_data function")
694
+ print("Available functions: {}".format([attr for attr in dir(MediBot_Preprocessor) if not attr.startswith('_')]))
695
+ sys.exit(1)
696
+
556
697
  MediBot_Preprocessor.preprocess_csv_data(csv_data, e_state.crosswalk)
557
698
 
558
699
  # TIMING: End CSV preprocessing timing
@@ -617,14 +758,9 @@ if __name__ == "__main__":
617
758
  if patient_row is None:
618
759
  raise ValueError("Patient row not found for patient ID: {}".format(patient_id))
619
760
 
620
- # Get all surgery dates for this patient
621
- all_surgery_dates = patient_row.get('_all_surgery_dates', [patient_row.get('Surgery Date')])
622
- surgery_date_to_diagnosis = patient_row.get('_surgery_date_to_diagnosis', {})
623
-
624
- # Create entries for each surgery date with its specific diagnosis code
625
- for surgery_date in all_surgery_dates:
626
- diagnosis_code = surgery_date_to_diagnosis.get(surgery_date, 'N/A')
627
- patient_info.append((surgery_date, patient_name, patient_id, diagnosis_code, patient_row))
761
+ # Use helper function to create patient entries
762
+ patient_entries = create_patient_entries_from_row(patient_row, reverse_mapping)
763
+ patient_info.extend(patient_entries)
628
764
 
629
765
  except Exception as e:
630
766
  MediLink_ConfigLoader.log("Warning: Error retrieving data for patient ID '{}': {}".format(patient_id, e), level="WARNING")
@@ -646,17 +782,9 @@ if __name__ == "__main__":
646
782
  # Collect surgery dates and patient info for NEW patients
647
783
  new_patient_info = []
648
784
  for row in csv_data:
649
- patient_id = row.get(reverse_mapping['Patient ID #2'])
650
- patient_name = row.get(reverse_mapping['Patient Name'])
651
-
652
- # Get all surgery dates for this patient
653
- all_surgery_dates = row.get('_all_surgery_dates', [row.get('Surgery Date')])
654
- surgery_date_to_diagnosis = row.get('_surgery_date_to_diagnosis', {})
655
-
656
- # Create entries for each surgery date with its specific diagnosis code
657
- for surgery_date in all_surgery_dates:
658
- diagnosis_code = surgery_date_to_diagnosis.get(surgery_date, 'N/A')
659
- new_patient_info.append((surgery_date, patient_name, patient_id, diagnosis_code, row))
785
+ # Use helper function to create patient entries
786
+ patient_entries = create_patient_entries_from_row(row, reverse_mapping)
787
+ new_patient_info.extend(patient_entries)
660
788
 
661
789
  # Display new patients table using the enhanced display function
662
790
  MediBot_UI.display_enhanced_patient_table(
@@ -695,7 +823,41 @@ if __name__ == "__main__":
695
823
  if e_state:
696
824
  interaction_mode = 'error' # Switch to error mode
697
825
  error_message = str(e) # Capture the error message
698
- print("An error occurred while running MediBot: {}".format(e))
826
+
827
+ # ENHANCED ERROR DIAGNOSTICS
828
+ print("=" * 60)
829
+ print("MEDIBOT EXECUTION FAILURE")
830
+ print("=" * 60)
831
+ print("Error: {}".format(e))
832
+ print("Error type: {}".format(type(e).__name__))
833
+
834
+ # Check for the specific failure pattern
835
+ if "'NoneType' object has no attribute" in str(e):
836
+ print("DIAGNOSIS: This is the import failure pattern.")
837
+ print("A module import returned None, causing a method call to fail.")
838
+ print("This typically indicates:")
839
+ print(" 1. Syntax error in a MediBot module")
840
+ print(" 2. Missing dependency")
841
+ print(" 3. Import path issue")
842
+ print(" 4. Circular import problem")
843
+
844
+ # Show current import state
845
+ print("Current import state:")
846
+ try:
847
+ import_state = {
848
+ 'MediBot_Preprocessor': MediBot_Preprocessor,
849
+ 'MediBot_Preprocessor_lib': MediBot_Preprocessor_lib,
850
+ 'MediBot_UI': MediBot_UI,
851
+ 'MediBot_Crosswalk_Library': MediBot_Crosswalk_Library
852
+ }
853
+ for module_name, module in import_state.items():
854
+ status = "None" if module is None else "OK"
855
+ print(" - {}: {}".format(module_name, status))
856
+ except Exception as diag_e:
857
+ print(" - Unable to diagnose import state: {}".format(diag_e))
858
+
859
+ print("=" * 60)
860
+
699
861
  # Handle the error by calling user interaction with the error information
700
862
  if 'identified_fields' in locals():
701
863
  _ = user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
@@ -531,17 +531,41 @@ def sort_and_deduplicate(csv_data):
531
531
  existing_row = unique_patients[patient_id]
532
532
  existing_date = existing_row['Surgery Date']
533
533
 
534
+ # Ensure both dates are comparable by converting to datetime objects
535
+ def normalize_date_for_comparison(date_value):
536
+ if isinstance(date_value, datetime):
537
+ return date_value
538
+ elif isinstance(date_value, str) and date_value.strip():
539
+ try:
540
+ # Try to parse the string as a date
541
+ return datetime.strptime(date_value, '%m/%d/%Y')
542
+ except ValueError:
543
+ try:
544
+ return datetime.strptime(date_value, '%m-%d-%Y')
545
+ except ValueError:
546
+ # If parsing fails, return minimum datetime
547
+ return datetime.min
548
+ else:
549
+ # Empty or invalid values get minimum datetime
550
+ return datetime.min
551
+
552
+ normalized_surgery_date = normalize_date_for_comparison(surgery_date)
553
+ normalized_existing_date = normalize_date_for_comparison(existing_date)
554
+
534
555
  # Keep the most current demographic data (later surgery date takes precedence)
535
- if surgery_date > existing_date:
556
+ if normalized_surgery_date > normalized_existing_date:
536
557
  # Store the old row's surgery date before replacing
537
558
  old_date = existing_row['Surgery Date']
538
- patient_surgery_dates[patient_id].append(old_date)
559
+ # Add the old date to the list if it's not already there
560
+ if old_date not in patient_surgery_dates[patient_id]:
561
+ patient_surgery_dates[patient_id].append(old_date)
539
562
  # Replace with newer row (better demographics)
540
563
  unique_patients[patient_id] = row
541
- # Update the surgery dates list to reflect the new primary date
542
- patient_surgery_dates[patient_id] = [surgery_date] + [d for d in patient_surgery_dates[patient_id] if d != surgery_date]
564
+ # Add the new surgery date to the list if it's not already there
565
+ if surgery_date not in patient_surgery_dates[patient_id]:
566
+ patient_surgery_dates[patient_id].append(surgery_date)
543
567
  else:
544
- # Add this surgery date to the list for this patient
568
+ # Add this surgery date to the list for this patient if it's not already there
545
569
  if surgery_date not in patient_surgery_dates[patient_id]:
546
570
  patient_surgery_dates[patient_id].append(surgery_date)
547
571
 
@@ -558,11 +582,77 @@ def sort_and_deduplicate(csv_data):
558
582
  else:
559
583
  surgery_date_strings.append(str(date) if date else 'MISSING')
560
584
 
561
- row['_all_surgery_dates'] = sorted(surgery_date_strings)
585
+ # Remove duplicates and sort
586
+ unique_surgery_dates = list(set(surgery_date_strings))
587
+ sorted_surgery_dates = sorted(unique_surgery_dates, key=lambda x: datetime.strptime(x, '%m-%d-%Y') if x != 'MISSING' else datetime.min)
588
+ row['_all_surgery_dates'] = sorted_surgery_dates
562
589
  row['_primary_surgery_date'] = row['Surgery Date'] # Keep track of which date has the demographics
590
+ # Compute and store earliest surgery date for emission sort
591
+ earliest_dt = None
592
+ earliest_str = None
593
+ for d in sorted_surgery_dates:
594
+ if d and d != 'MISSING':
595
+ try:
596
+ earliest_dt = datetime.strptime(d, '%m-%d-%Y')
597
+ earliest_str = d
598
+ break
599
+ except Exception:
600
+ pass
601
+ # Fallback to demographics date if earliest could not be determined
602
+ if earliest_str is None:
603
+ try:
604
+ sd = row.get('Surgery Date')
605
+ if isinstance(sd, datetime) and sd != datetime.min:
606
+ earliest_dt = sd
607
+ earliest_str = sd.strftime('%m-%d-%Y')
608
+ elif isinstance(sd, str) and sd.strip():
609
+ try:
610
+ earliest_dt = datetime.strptime(sd, '%m/%d/%Y')
611
+ except Exception:
612
+ try:
613
+ earliest_dt = datetime.strptime(sd, '%m-%d-%Y')
614
+ except Exception:
615
+ earliest_dt = None
616
+ earliest_str = sd
617
+ except Exception:
618
+ earliest_dt = None
619
+ earliest_str = None
620
+ row['_earliest_surgery_date'] = earliest_str
621
+
622
+
563
623
 
564
624
  # Convert the unique_patients dictionary back to a list and sort it
565
- csv_data[:] = sorted(unique_patients.values(), key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip())) # TODO Does this need to be sorted twice? once before and once after?
625
+ # Use the same normalization function for consistent sorting
626
+ def sort_key(row):
627
+ # Prefer earliest surgery date across all known dates for the patient
628
+ earliest = row.get('_earliest_surgery_date')
629
+ if isinstance(earliest, str) and earliest and earliest != 'MISSING':
630
+ try:
631
+ normalized_date = datetime.strptime(earliest, '%m-%d-%Y')
632
+ except Exception:
633
+ normalized_date = datetime.min
634
+ else:
635
+ # Fallback to the single Surgery Date field
636
+ surgery_date = row.get('Surgery Date')
637
+ if isinstance(surgery_date, datetime):
638
+ normalized_date = surgery_date
639
+ elif isinstance(surgery_date, str) and surgery_date.strip():
640
+ try:
641
+ normalized_date = datetime.strptime(surgery_date, '%m/%d/%Y')
642
+ except ValueError:
643
+ try:
644
+ normalized_date = datetime.strptime(surgery_date, '%m-%d-%Y')
645
+ except ValueError:
646
+ normalized_date = datetime.min
647
+ else:
648
+ normalized_date = datetime.min
649
+ # Tie-break per requirement: last name (case-insensitive), then first name, then patient id
650
+ last_name = ((row.get('Patient Last') or '')).strip().upper()
651
+ first_name = ((row.get('Patient First') or '')).strip().upper()
652
+ patient_id_tiebreak = str(row.get('Patient ID') or '')
653
+ return (normalized_date, last_name, first_name, patient_id_tiebreak)
654
+
655
+ csv_data[:] = sorted(unique_patients.values(), key=sort_key) # TODO Does this need to be sorted twice? once before and once after?
566
656
 
567
657
  # TODO: Consider adding an option in the config to sort based on Surgery Schedules when available.
568
658
  # If no schedule is available, the current sorting strategy will be used.
@@ -1160,17 +1250,51 @@ def update_diagnosis_codes(csv_data):
1160
1250
  MediLink_ConfigLoader.log("Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="DEBUG")
1161
1251
 
1162
1252
  if surgery_date_str in all_patient_data[patient_id]:
1163
- diagnosis_code, left_or_right_eye, femto_yes_or_no = all_patient_data[patient_id][surgery_date_str]
1253
+ diagnosis_data = all_patient_data[patient_id][surgery_date_str]
1254
+ # XP SP3 + Py3.4.4 compatible tuple unpacking with safety check
1255
+ try:
1256
+ if isinstance(diagnosis_data, (list, tuple)) and len(diagnosis_data) >= 3:
1257
+ diagnosis_code, left_or_right_eye, femto_yes_or_no = diagnosis_data
1258
+ else:
1259
+ # Handle case where diagnosis_data is not a proper tuple
1260
+ diagnosis_code = diagnosis_data if diagnosis_data else None
1261
+ left_or_right_eye = None
1262
+ femto_yes_or_no = None
1263
+ except Exception as e:
1264
+ MediLink_ConfigLoader.log("Error unpacking diagnosis data for Patient ID: {}, Surgery Date: {}: {}".format(
1265
+ patient_id, surgery_date_str, str(e)), level="WARNING")
1266
+ diagnosis_code = None
1267
+ left_or_right_eye = None
1268
+ femto_yes_or_no = None
1269
+
1164
1270
  MediLink_ConfigLoader.log("Found diagnosis data for Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="DEBUG")
1165
1271
 
1166
- # Convert diagnosis code to Medisoft shorthand format.
1167
- medisoft_shorthand = diagnosis_to_medisoft.get(diagnosis_code, None)
1168
- if medisoft_shorthand is None and diagnosis_code:
1169
- # Use fallback logic for missing mapping
1170
- defaulted_code = diagnosis_code.lstrip('H').lstrip('T8').replace('.', '')[-5:]
1171
- medisoft_shorthand = defaulted_code
1172
- MediLink_ConfigLoader.log("Missing diagnosis mapping for '{}', using fallback code '{}'".format(
1173
- diagnosis_code, medisoft_shorthand), level="WARNING")
1272
+ # Convert diagnosis code to Medisoft shorthand format.
1273
+ # XP SP3 + Py3.4.4 compatible null check
1274
+ if diagnosis_code is None:
1275
+ medisoft_shorthand = 'N/A'
1276
+ MediLink_ConfigLoader.log("Diagnosis code is None for Patient ID: {}, Surgery Date: {}".format(
1277
+ patient_id, surgery_date_str), level="WARNING")
1278
+ else:
1279
+ medisoft_shorthand = diagnosis_to_medisoft.get(diagnosis_code, None)
1280
+ if medisoft_shorthand is None and diagnosis_code:
1281
+ # Use fallback logic for missing mapping (XP SP3 + Py3.4.4 compatible)
1282
+ try:
1283
+ defaulted_code = diagnosis_code.lstrip('H').lstrip('T8').replace('.', '')[-5:]
1284
+ # Basic validation: ensure code is not empty and has reasonable length
1285
+ if defaulted_code and len(defaulted_code) >= 3:
1286
+ medisoft_shorthand = defaulted_code
1287
+ MediLink_ConfigLoader.log("Missing diagnosis mapping for '{}', using fallback code '{}'".format(
1288
+ diagnosis_code, medisoft_shorthand), level="WARNING")
1289
+ else:
1290
+ medisoft_shorthand = 'N/A'
1291
+ MediLink_ConfigLoader.log("Fallback diagnosis code validation failed for '{}', using 'N/A'".format(
1292
+ diagnosis_code), level="WARNING")
1293
+ except Exception as e:
1294
+ medisoft_shorthand = 'N/A'
1295
+ MediLink_ConfigLoader.log("Error in fallback diagnosis code generation for '{}': {}".format(
1296
+ diagnosis_code, str(e)), level="WARNING")
1297
+
1174
1298
  MediLink_ConfigLoader.log("Converted diagnosis code to Medisoft shorthand: {}".format(medisoft_shorthand), level="DEBUG")
1175
1299
 
1176
1300
  surgery_date_to_diagnosis[surgery_date_str] = medisoft_shorthand
@@ -1488,12 +1612,28 @@ def capitalize_all_fields(csv_data):
1488
1612
  Returns:
1489
1613
  None: The function modifies the csv_data in place.
1490
1614
  """
1491
- # PERFORMANCE FIX: Optimize uppercase conversion using dict comprehension
1615
+ # PERFORMANCE FIX: Optimize uppercase conversion while preserving complex types
1492
1616
  for row in csv_data:
1493
- # Single-pass update using dict comprehension
1494
- row.update({
1495
- key: (value.upper() if isinstance(value, str)
1496
- else str(value).upper() if value is not None and not isinstance(value, datetime)
1497
- else value)
1498
- for key, value in row.items()
1499
- })
1617
+ updated_row = {}
1618
+ for key, value in row.items():
1619
+ # Preserve internal/derived fields intact (e.g., `_all_surgery_dates`, `_surgery_date_to_diagnosis`)
1620
+ if isinstance(key, str) and key.startswith('_'):
1621
+ updated_row[key] = value
1622
+ continue
1623
+ # Uppercase plain strings
1624
+ if isinstance(value, str):
1625
+ updated_row[key] = value.upper()
1626
+ continue
1627
+ # Preserve complex containers; optionally uppercase their string contents
1628
+ if isinstance(value, list):
1629
+ updated_row[key] = [elem.upper() if isinstance(elem, str) else elem for elem in value]
1630
+ continue
1631
+ if isinstance(value, dict):
1632
+ updated_row[key] = {k: (v.upper() if isinstance(v, str) else v) for k, v in value.items()}
1633
+ continue
1634
+ # Leave datetimes as-is; coerce simple scalars to string upper for consistency
1635
+ if isinstance(value, datetime):
1636
+ updated_row[key] = value
1637
+ else:
1638
+ updated_row[key] = str(value).upper() if value is not None else value
1639
+ row.update(updated_row)
MediBot/MediBot_UI.py CHANGED
@@ -43,64 +43,151 @@ def display_enhanced_patient_table(patient_info, title, show_line_numbers=True):
43
43
  print(title)
44
44
  print()
45
45
 
46
- # Sort by surgery date first and then by patient name
47
- patient_info.sort(key=lambda x: (x[0], x[1]))
46
+ # Normalize data to avoid None and unexpected container types in sort key
47
+ normalized_info = []
48
+ for surgery_date, patient_name, patient_id, diagnosis_code, patient_row in patient_info:
49
+ # Normalize date into comparable key and display string
50
+ display_date = None
51
+ current_date_dt = None
52
+ try:
53
+ if hasattr(surgery_date, 'strftime'):
54
+ display_date = surgery_date.strftime('%m-%d')
55
+ current_date_dt = surgery_date
56
+ elif isinstance(surgery_date, str):
57
+ # Date strings may be MM-DD-YYYY or already MM-DD
58
+ parts = surgery_date.split('-') if surgery_date else []
59
+ if len(parts) == 3 and all(parts):
60
+ display_date = "{}-{}".format(parts[0], parts[1])
61
+ try:
62
+ current_date_dt = datetime.strptime(surgery_date, '%m-%d-%Y')
63
+ except Exception:
64
+ current_date_dt = None
65
+ else:
66
+ display_date = surgery_date or 'Unknown Date'
67
+ current_date_dt = None
68
+ else:
69
+ display_date = str(surgery_date) if surgery_date is not None else 'Unknown Date'
70
+ current_date_dt = None
71
+ except Exception:
72
+ display_date = str(surgery_date) if surgery_date is not None else 'Unknown Date'
73
+ current_date_dt = None
74
+
75
+ # Normalize diagnosis display: only show "-Not Found-" when explicitly flagged as N/A
76
+ # XP SP3 + Py3.4.4 compatible error handling
77
+ display_diagnosis = diagnosis_code
78
+ try:
79
+ if diagnosis_code == "N/A":
80
+ display_diagnosis = "-Not Found-"
81
+ elif diagnosis_code is None:
82
+ display_diagnosis = "-Not Found-"
83
+ elif isinstance(diagnosis_code, str) and diagnosis_code.strip() == "":
84
+ display_diagnosis = "-Not Found-"
85
+ else:
86
+ display_diagnosis = str(diagnosis_code)
87
+ except (TypeError, ValueError) as e:
88
+ # Log the specific error for debugging (ASCII-only compatible)
89
+ try:
90
+ error_msg = "Error converting diagnosis code to string: {}".format(str(e))
91
+ MediLink_ConfigLoader.log(error_msg, level="WARNING")
92
+ except Exception:
93
+ # Fallback logging if string formatting fails
94
+ MediLink_ConfigLoader.log("Error converting diagnosis code to string", level="WARNING")
95
+ display_diagnosis = "-Not Found-"
96
+
97
+ # Grouping: place all dates for a patient together under their earliest date
98
+ primary_date_dt = None
99
+ within_index = 0
100
+ last_name_key = ''
101
+ first_name_key = ''
102
+ try:
103
+ all_dates = []
104
+ if patient_row is not None:
105
+ raw_dates = patient_row.get('_all_surgery_dates', [])
106
+ # Convert to datetime list and find primary
107
+ for d in raw_dates:
108
+ try:
109
+ if hasattr(d, 'strftime'):
110
+ all_dates.append(d)
111
+ elif isinstance(d, str):
112
+ all_dates.append(datetime.strptime(d, '%m-%d-%Y'))
113
+ except Exception:
114
+ pass
115
+ if all_dates:
116
+ all_dates.sort()
117
+ primary_date_dt = all_dates[0]
118
+ # Determine within-patient index of current date
119
+ if current_date_dt is not None:
120
+ # Find matching index by exact date
121
+ for idx, ad in enumerate(all_dates):
122
+ if current_date_dt == ad:
123
+ within_index = idx
124
+ break
125
+ # Prefer explicit last/first from row for sorting
126
+ try:
127
+ ln = patient_row.get('Patient Last')
128
+ fn = patient_row.get('Patient First')
129
+ if isinstance(ln, str):
130
+ last_name_key = ln.strip().upper()
131
+ if isinstance(fn, str):
132
+ first_name_key = fn.strip().upper()
133
+ except Exception:
134
+ pass
135
+ except Exception:
136
+ primary_date_dt = None
137
+ within_index = 0
138
+
139
+ # Fallbacks if parsing failed
140
+ if primary_date_dt is None:
141
+ primary_date_dt = current_date_dt
142
+ # If last/first not available from row, parse from display name "LAST, FIRST ..."
143
+ if not last_name_key and isinstance(patient_name, str):
144
+ try:
145
+ parts = [p.strip() for p in patient_name.split(',')]
146
+ if len(parts) >= 1:
147
+ last_name_key = parts[0].upper()
148
+ if len(parts) >= 2:
149
+ first_name_key = parts[1].split()[0].upper() if parts[1] else ''
150
+ except Exception:
151
+ last_name_key = ''
152
+ first_name_key = ''
153
+
154
+ # Build composite sort key per requirement: by earliest date, then last name within date,
155
+ # while keeping same patient's additional dates directly under the first line
156
+ composite_sort_key = (primary_date_dt, last_name_key, first_name_key, str(patient_id or ''), within_index)
157
+
158
+ normalized_info.append((composite_sort_key, display_date, str(patient_name or ''), str(patient_id or ''), display_diagnosis))
159
+
160
+ # Sort so that all entries for a patient are grouped under their earliest date
161
+ normalized_info.sort(key=lambda x: x[0])
48
162
 
49
163
  # Calculate column widths for proper alignment
50
- max_patient_id_len = max(len(str(pid)) for _, _, pid, _, _ in patient_info)
51
- max_patient_name_len = max(len(pname) for _, pname, _, _, _ in patient_info)
52
- max_diagnosis_len = max(len(dcode) for _, _, _, dcode, _ in patient_info)
164
+ max_patient_id_len = max(len(pid) for _, _, _, pid, _ in normalized_info)
165
+ max_patient_name_len = max(len(pname) for _, _, pname, _, _ in normalized_info)
166
+ max_diagnosis_len = max(len(dcode) for _, _, _, _, dcode in normalized_info)
53
167
 
54
168
  # Ensure minimum widths for readability
55
169
  max_patient_id_len = max(max_patient_id_len, 5) # 5-digit ID max
56
170
  max_patient_name_len = max(max_patient_name_len, 12) # "Patient Name" header
57
171
  max_diagnosis_len = max(max_diagnosis_len, 10) # "Diagnosis" header
58
172
 
59
- # Print the sorted patient info with multiple surgery dates
60
173
  current_patient = None
61
174
  line_number = 1
62
175
 
63
- for surgery_date, patient_name, patient_id, diagnosis_code, patient_row in patient_info:
64
- # Format surgery_date safely whether it's a datetime/date or a string
65
- try:
66
- if isinstance(surgery_date, datetime):
67
- formatted_date = surgery_date.strftime('%m-%d')
68
- else:
69
- # Handle string dates - this should be the Surgery Date Display field
70
- formatted_date = str(surgery_date)
71
- # If it's a date string like "12-25-2023", format it as "12-25"
72
- if '-' in formatted_date and len(formatted_date.split('-')) == 3:
73
- try:
74
- parts = formatted_date.split('-')
75
- formatted_date = "{}-{}".format(parts[0], parts[1])
76
- except:
77
- pass # Use original string if parsing fails
78
- except Exception:
79
- formatted_date = str(surgery_date)
80
-
81
- # Transform diagnosis code display: show "-Not Found-" instead of "N/A"
82
- display_diagnosis = "-Not Found-" if diagnosis_code == "N/A" else diagnosis_code
83
-
84
- # Check if this is the same patient as the previous line
176
+ for sort_key, formatted_date, patient_name, patient_id, display_diagnosis in normalized_info:
85
177
  if current_patient == patient_id:
86
- # Secondary surgery date - indent and show dashes for patient info
87
- # Use exact character count matching for dashes
88
- patient_id_dashes = '-' * len(str(patient_id))
178
+ patient_id_dashes = '-' * len(patient_id)
89
179
  patient_name_dashes = '-' * len(patient_name)
90
-
91
180
  secondary_format = " {:<6} | {:<" + str(max_patient_id_len) + "} | {:<" + str(max_patient_name_len) + "} | {:<" + str(max_diagnosis_len) + "}"
92
181
  print(secondary_format.format(formatted_date, patient_id_dashes, patient_name_dashes, display_diagnosis))
93
182
  else:
94
- # New patient - show full patient info
95
183
  current_patient = patient_id
96
184
  if show_line_numbers:
97
185
  primary_format = "{:03d}: {:<6} | {:<" + str(max_patient_id_len) + "} | {:<" + str(max_patient_name_len) + "} | {:<" + str(max_diagnosis_len) + "}"
98
- print(primary_format.format(line_number, formatted_date, str(patient_id), patient_name, display_diagnosis))
186
+ print(primary_format.format(line_number, formatted_date, patient_id, patient_name, display_diagnosis))
99
187
  line_number += 1
100
188
  else:
101
- # For existing patients, don't show line numbers
102
189
  primary_format = " {:<6} | {:<" + str(max_patient_id_len) + "} | {:<" + str(max_patient_name_len) + "} | {:<" + str(max_diagnosis_len) + "}"
103
- print(primary_format.format(formatted_date, str(patient_id), patient_name, display_diagnosis))
190
+ print(primary_format.format(formatted_date, patient_id, patient_name, display_diagnosis))
104
191
 
105
192
  # Function to check if a specific key is pressed
106
193
  def _get_vk_codes():