From ad3dee4b598c8801d2844f94e070f1c509f276f4 Mon Sep 17 00:00:00 2001
From: muoimeo <bvnminh6a01@gmail.com>
Date: Tue, 22 Apr 2025 19:24:48 +0700
Subject: [PATCH] 99% maybe last one

---
 app/routes/patients.py                        | 130 +++++++++++++++---
 app/templates/encounter_measurements.html     |   6 +-
 app/templates/new_patient.html                |   1 +
 app/templates/patient_detail.html             |  11 +-
 app/templates/patients.html                   |   6 +-
 app/templates/report_form.html                |   2 +-
 migrations/versions/8116b7d4aede_initital.py  | 103 --------------
 .../versions/fb934f468320_add_ml_pred.py      |  50 -------
 migrations/versions/fe0cbf650625_add.py       |  64 ---------
 requirements.txt                              | Bin 755 -> 12936 bytes
 10 files changed, 126 insertions(+), 247 deletions(-)
 delete mode 100644 migrations/versions/8116b7d4aede_initital.py
 delete mode 100644 migrations/versions/fb934f468320_add_ml_pred.py
 delete mode 100644 migrations/versions/fe0cbf650625_add.py

diff --git a/app/routes/patients.py b/app/routes/patients.py
index bd4904b..98deb4d 100644
--- a/app/routes/patients.py
+++ b/app/routes/patients.py
@@ -1356,7 +1356,8 @@ def new_encounter(patient_id):
         create_notification_for_admins(
             f"New encounter ({custom_id}) for patient {patient.full_name} by {current_user.full_name}.",
             admin_link,
-            exclude_user_id=current_user.userID)
+            exclude_user_id=current_user.userID
+        )
 
         if (patient.assigned_dietitian_user_id
                 and patient.assigned_dietitian_user_id != current_user.userID):
@@ -1480,18 +1481,83 @@ def delete_encounter(patient_id, encounter_pk):
                  create_notification(dietitian_id_assigned, dietitian_message, admin_link)
 
             # --- THÊM LOGIC HỦY ASSIGNMENT --- 
+            from app.models.patient_dietitian_assignment import PatientDietitianAssignment # DI CHUYỂN IMPORT LÊN ĐÂY
+            unassignment_successful = False # Flag để kiểm tra
             if dietitian_id_assigned: # Nếu encounter bị xóa có gán dietitian
                 active_assignment = PatientDietitianAssignment.get_active_assignment(patient.id)
                 # Chỉ hủy assignment nếu dietitian của assignment đang active trùng với dietitian của encounter bị xóa
                 if active_assignment and active_assignment.dietitian_id == dietitian_id_assigned:
-                    current_app.logger.info(f"Deactivating assignment {active_assignment.id} for patient {patient.id} due to deletion of encounter {deleted_encounter_id}")
-                    active_assignment.is_active = False
-                    active_assignment.end_date = datetime.utcnow()
-                    db.session.add(active_assignment)
-                    # Gửi thêm thông báo về việc hủy assignment
-                    deactivation_message = f"Your assignment to patient {patient.full_name} was deactivated because the related encounter was deleted."
-                    deactivation_link = ('patients.patient_detail', {'patient_id': patient.id})
-                    create_notification(dietitian_id_assigned, deactivation_message, deactivation_link)
+                    current_app.logger.info(f"Examining assignment {active_assignment.id} for patient {patient.id} due to deletion of encounter {deleted_encounter_id}")
+                    
+                    # Kiểm tra xem có encounters nào khác của patient này đang gán cho CÙNG dietitian không
+                    other_same_dietitian_encounters = Encounter.query.filter(
+                        Encounter.patient_id == patient.id,
+                        Encounter.encounterID != deleted_encounter_id,
+                        Encounter.dietitian_id == dietitian_id_assigned
+                    ).all()
+                    
+                    # Nếu không còn encounter nào khác có cùng dietitian, hủy gán
+                    if not other_same_dietitian_encounters:
+                        current_app.logger.info(f"No other encounters with dietitian {dietitian_id_assigned} found. Attempting to deactivate assignment.")
+                        try:
+                            # --- BẮT ĐẦU KHỐI TRY RIÊNG CHO UNASSIGNMENT ---
+                            # 1. Hủy assignment hiện tại
+                            active_assignment.is_active = False
+                            active_assignment.end_date = datetime.utcnow()
+                            db.session.add(active_assignment)
+                            
+                            # 2. Cập nhật trường assigned_dietitian_user_id của patient về null
+                            patient.assigned_dietitian_user_id = None
+                            db.session.add(patient)
+                            
+                            # 3. (Số thứ tự cũ là 4) Đảm bảo không còn encounter nào liên kết với dietitian đã bị hủy
+                            encounter_updates = Encounter.query.filter(
+                                Encounter.patient_id == patient.id,
+                                Encounter.dietitian_id == dietitian_id_assigned
+                            ).update({"dietitian_id": None}, synchronize_session=False)
+                            if encounter_updates > 0:
+                                current_app.logger.info(f"Updated {encounter_updates} other encounters to remove dietitian link")
+                                
+                            # 4. (Số thứ tự cũ là 5) Cập nhật các assignment khác có thể vẫn đang active (phòng hờ)
+                            other_active_assignments = PatientDietitianAssignment.query.filter(
+                                PatientDietitianAssignment.patient_id == patient.id,
+                                PatientDietitianAssignment.dietitian_id == dietitian_id_assigned,
+                                PatientDietitianAssignment.is_active == True,
+                                PatientDietitianAssignment.id != active_assignment.id
+                            ).all()
+                            for other_assignment in other_active_assignments:
+                                other_assignment.is_active = False
+                                other_assignment.end_date = datetime.utcnow()
+                                db.session.add(other_assignment)
+                                current_app.logger.info(f"Deactivated additional assignment {other_assignment.id}")
+
+                            # 5. (Số thứ tự cũ là 6) Kiểm tra và cập nhật bảng Dietitian
+                            from app.models.dietitian import Dietitian
+                            dietitian = Dietitian.query.filter_by(user_id=dietitian_id_assigned).first()
+                            if dietitian:
+                                dietitian.update_status_based_on_patient_count() # Cần chạy sau khi patient.assigned_dietitian_user_id đã được commit
+                                db.session.add(dietitian)
+                                current_app.logger.info(f"Dietitian {dietitian.formattedID} status marked for update.")
+
+                            # *** COMMIT RIÊNG CHO UNASSIGNMENT ***
+                            db.session.commit() 
+                            unassignment_successful = True # Đánh dấu thành công
+                            current_app.logger.info(f"Successfully committed unassignment changes for patient {patient.id} and dietitian {dietitian_id_assigned}.")
+
+                            # Gửi thông báo *sau khi* commit thành công
+                            deactivation_message = f"Your assignment to patient {patient.full_name} was deactivated because the related encounter was deleted."
+                            deactivation_link = ('patients.patient_detail', {'patient_id': patient.id})
+                            create_notification(dietitian_id_assigned, deactivation_message, deactivation_link)
+
+                        except Exception as unassign_error:
+                            db.session.rollback() # Rollback CHỈ các thay đổi unassignment
+                            current_app.logger.error(f"Error during unassignment process for patient {patient.id}: {unassign_error}", exc_info=True)
+                            flash("An error occurred while unassigning the dietitian. Encounter deleted, but assignment might persist.", "warning")
+                        # --- KẾT THÚC KHỐI TRY RIÊNG CHO UNASSIGNMENT ---
+                            
+                    else:
+                        # Vẫn giữ assignment vì còn encounter khác với cùng dietitian
+                        current_app.logger.info(f"Patient {patient.id} still has {len(other_same_dietitian_encounters)} other encounters with dietitian {dietitian_id_assigned}. Assignment maintained.")
                 elif active_assignment:
                     current_app.logger.info(f"Deleted encounter {deleted_encounter_id} had dietitian {dietitian_id_assigned}, but active assignment {active_assignment.id} is for dietitian {active_assignment.dietitian_id}. Assignment not deactivated.")
                 else:
@@ -1544,7 +1610,10 @@ def run_encounter_ml(patient_id, encounter_id):
             
             # Xử lý lỗi từ hàm dự đoán
             if prediction_error: # Sửa lỗi thụt lề
-                flash(f"ML Prediction Warning/Error: {prediction_error}", "warning") # Sửa lỗi thụt lề
+                # Format encounter ID as "E-XXXXX-XX" rather than just the numeric ID
+                encounter_display_id = encounter.custom_encounter_id or f"E-{encounter.encounterID:05d}-01"
+                error_message = prediction_error.replace(f"encounter {encounter.encounterID}", f"encounter {encounter_display_id}")
+                flash(f"ML Prediction Warning/Error: {error_message}", "warning") # Sửa lỗi thụt lề
                 # Vẫn tiếp tục để lưu kết quả (có thể là None)
             
             if needs_intervention_result is not None: # Chỉ lưu kết quả nếu ML chạy không lỗi cơ bản # Sửa lỗi thụt lề
@@ -1654,8 +1723,9 @@ def run_encounter_ml(patient_id, encounter_id):
                 db.session.commit() # Commit tất cả thay đổi (encounter, referral, report, patient status) # Sửa lỗi thụt lề
             
             else: # needs_intervention_result is None (lỗi ML) # Sửa lỗi thụt lề
-                flash("An error occurred during ML prediction. Please check logs.", "danger") # Sửa lỗi thụt lề
+                # Bỏ thông báo lỗi ở đây vì đã xử lý ở trên với mã lỗi cụ thể
                 # Không commit nếu ML lỗi
+                pass # Thêm pass để đảm bảo cú pháp đúng khi xóa nội dung của khối
 
             # Luôn redirect sau khi xử lý xong khối try (hoặc nếu ML lỗi)
             return redirect(url_for('patients.encounter_measurements', patient_id=patient_id, encounter_id=encounter_id)) # Sửa lỗi thụt lề
@@ -1795,12 +1865,32 @@ def assign_dietitian(patient_id):
                 
             # --- Update Patient and Related Objects --- 
             if assigned_dietitian:
+                # --- ADD: Deactivate existing assignments FIRST --- 
+                from app.models.patient_dietitian_assignment import PatientDietitianAssignment
+                deactivated_count = PatientDietitianAssignment.deactivate_existing_assignments(patient.id)
+                if deactivated_count:
+                    current_app.logger.info(f"Deactivated {deactivated_count} previous assignment(s) for patient {patient.id}.")
+                # --- END ADD ---
+                
+                # --- ADD: Create NEW active assignment record --- 
+                new_assignment = PatientDietitianAssignment(
+                    patient_id=patient.id,
+                    dietitian_id=assigned_dietitian.userID,
+                    assignment_date=datetime.utcnow(),
+                    is_active=True,
+                    notes=notes # Thêm ghi chú từ form vào assignment
+                )
+                db.session.add(new_assignment)
+                current_app.logger.info(f"Created new active assignment record for patient {patient.id} and dietitian {assigned_dietitian.userID}.")
+                # --- END ADD ---
+                
+                # Cập nhật thông tin Patient (giữ nguyên)
                 patient.assigned_dietitian_user_id = assigned_dietitian.userID
                 patient.assignment_date = datetime.utcnow()
                 patient.status = PatientStatus.ASSESSMENT_IN_PROGRESS # Update patient status
                 db.session.add(patient)
                 
-                # Find the latest relevant referral needing assignment and update it
+                # Cập nhật Referral (giữ nguyên)
                 latest_needing_referral = Referral.query.filter(
                     Referral.patient_id == patient.id,
                     Referral.referral_status == ReferralStatus.DIETITIAN_UNASSIGNED
@@ -1817,7 +1907,7 @@ def assign_dietitian(patient_id):
                 else:
                     current_app.logger.warning(f"Could not find a referral needing assignment for patient {patient.id} when assigning dietitian.")
                 
-                # --- ADD: Update latest ONGOING encounter with the assigned dietitian --- 
+                # Cập nhật Encounter (giữ nguyên)
                 latest_ongoing_encounter = Encounter.query.filter_by(
                     patient_id=patient.id,
                     status=EncounterStatus.ON_GOING
@@ -1826,17 +1916,14 @@ def assign_dietitian(patient_id):
                 if latest_ongoing_encounter:
                     latest_ongoing_encounter.dietitian_id = assigned_dietitian.userID
                     db.session.add(latest_ongoing_encounter)
-                    # Đảm bảo refresh lại object để SQLAlchemy cập nhật relationship
                     db.session.flush()
-                    # Nếu chạy mà vẫn không hiện tên dietitian, thử thêm dòng này:
                     db.session.refresh(latest_ongoing_encounter)
                     current_app.logger.info(f"Updated latest ongoing encounter {latest_ongoing_encounter.encounterID} with assigned dietitian {assigned_dietitian.userID}. Relationship loaded: {latest_ongoing_encounter.assigned_dietitian is not None}")
                 else:
                     current_app.logger.warning(f"Could not find an ongoing encounter for patient {patient.id} to assign dietitian to.")
-                # --- END ADD ---
                 
-                # --- ADD: Update dietitian_id for related PENDING reports --- 
-                if latest_ongoing_encounter: # Chỉ cập nhật report nếu có encounter liên quan
+                # Cập nhật Report (giữ nguyên)
+                if latest_ongoing_encounter: 
                     associated_pending_reports = Report.query.filter_by(
                         encounter_id=latest_ongoing_encounter.encounterID,
                         status=ReportStatus.PENDING
@@ -1844,16 +1931,16 @@ def assign_dietitian(patient_id):
                     
                     updated_report_count = 0
                     for report_to_update in associated_pending_reports:
-                        if report_to_update.dietitian_id is None: # Chỉ cập nhật nếu chưa có dietitian
+                        if report_to_update.dietitian_id is None: 
                             report_to_update.dietitian_id = assigned_dietitian.userID
                             db.session.add(report_to_update)
                             updated_report_count += 1
                             
                     if updated_report_count > 0:
                         current_app.logger.info(f"Updated dietitian_id for {updated_report_count} pending reports associated with encounter {latest_ongoing_encounter.encounterID}.")
-                # --- END ADD --- 
 
-                # --- Logging and Notifications --- 
+                # Logging và Notifications (giữ nguyên)
+                # ... (code logging và notification giữ nguyên)
                 # Log Activity
                 log_message = f"Assigned dietitian {assigned_dietitian.full_name} to patient {patient.full_name} ({assignment_type} assignment)."
                 activity = ActivityLog(
@@ -2293,3 +2380,4 @@ def download_ml_results(patient_id, encounter_id):
 
     return response
 # --- KẾT THÚC ROUTE TẢI KẾT QUẢ ML ---
+        
\ No newline at end of file
diff --git a/app/templates/encounter_measurements.html b/app/templates/encounter_measurements.html
index c3ea3ec..e9007b9 100644
--- a/app/templates/encounter_measurements.html
+++ b/app/templates/encounter_measurements.html
@@ -5,7 +5,7 @@
 
 {% block title %}Encounter #{{ display_encounter_id }} Measurements - {{ patient.full_name }}{% endblock %}
 
-{% block header %}Encounter #{{ display_encounter_id }} Measurements - {{ patient.full_name }}{% endblock %}
+{% block header %}Encounter {{ display_encounter_id }} Details{% endblock %}
 
 {% block content %}
 <div class="animate-slide-in container mx-auto px-4 py-8">
@@ -53,7 +53,7 @@
         <div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
             <div><strong>Start Time:</strong> {{ encounter.start_time.strftime('%d/%m/%Y %H:%M') if encounter.start_time else 'N/A' }}</div>
             {# Sửa cách hiển thị status #}
-            <div><strong>Status:</strong> <span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-{{ status.color if status and status.color else 'gray' }}-100 text-{{ status.color if status and status.color else 'gray' }}-800">{{ status.text if status and status.text else 'Unknown' }}</span></div>
+            <div><strong>Status:</strong> <span class="px-2 py-0.5 text-xs font-semibold rounded-full bg-{{ encounter_status.color if encounter_status and encounter_status.color else 'gray' }}-100 text-{{ encounter_status.color if encounter_status and encounter_status.color else 'gray' }}-800">{{ encounter_status.text if encounter_status and encounter_status.text else 'Unknown' }}</span></div>
             {# Chỉ hiển thị Dietitian ở đây #}
             <div><strong>Dietitian:</strong> {{ encounter.assigned_dietitian.full_name if encounter.assigned_dietitian else 'None' }}</div>
             
@@ -108,7 +108,7 @@
         <a href="{{ url_for('patients.patient_detail', patient_id=patient.id) }}#encounters" 
            class="inline-flex items-center px-3 py-1.5 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200">
             <i class="fas fa-arrow-left mr-1.5"></i>
-            Quay lại danh sách lượt khám
+            Return to Encounter List
         </a>
         
         <div class="flex items-center space-x-3"> {# Wrap remaining buttons #}
diff --git a/app/templates/new_patient.html b/app/templates/new_patient.html
index 69e49ab..90bab75 100644
--- a/app/templates/new_patient.html
+++ b/app/templates/new_patient.html
@@ -1,6 +1,7 @@
 {% extends "base.html" %}
 
 {% block title %}Add New Patient{% endblock %}
+{% block header %}Add Patient{% endblock %}
 
 {% block content %}
 <div class="container mx-auto px-4 py-6">
diff --git a/app/templates/patient_detail.html b/app/templates/patient_detail.html
index cac6f96..ff7c39c 100644
--- a/app/templates/patient_detail.html
+++ b/app/templates/patient_detail.html
@@ -208,7 +208,7 @@
                             <div class="flex justify-between">
                                 <dt class="text-sm font-medium text-gray-500">In-charge dietitian</dt>
                                 {# Cập nhật để dùng assigned_dietitian từ Patient #}
-                                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ patient.assigned_dietitian.full_name if patient.assigned_dietitian else 'Chưa gán' }}</dd>
+                                <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{{ patient.assigned_dietitian.full_name if patient.assigned_dietitian else 'Not yet assigned' }}</dd>
                             </div>
                         </dl>
                     </div>
@@ -393,7 +393,7 @@
                                     <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
                                     <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time</th>
                                     <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Dietitian</th>
-                                    <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Critical Indicators (Max 3)</th>
+                                    <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Critical Indicators</th>
                                     <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
                                     <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">ACTIONS</th>
                                 </tr>
@@ -1535,6 +1535,13 @@
         // Add event listeners for delete procedure buttons (if on this page)
         // ... (existing delete procedure logic) ...
 
+        // Scroll to top after initial tab activation to prevent jumping due to hash
+        // Use setTimeout to ensure scrolling happens after potential browser hash scroll
+        setTimeout(() => {
+            window.scrollTo(0, 0);
+            console.log("[DOMContentLoaded] Scrolled window to top (after timeout).");
+        }, 100); // Delay of 100ms
+
     });
 </script>
 
diff --git a/app/templates/patients.html b/app/templates/patients.html
index e2c91c7..f613aaf 100644
--- a/app/templates/patients.html
+++ b/app/templates/patients.html
@@ -133,17 +133,17 @@
                         <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ patient.admission_date.strftime('%Y-%m-%d') if patient.admission_date else 'N/A' }}</td>
                         <td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
                             <a href="{{ url_for('patients.patient_detail', patient_id=patient.patient_id) }}" class="text-blue-600 hover:text-blue-900 mr-3" title="View Details">
-                                <i class="fas fa-eye text-2xl"></i>
+                                <i class="fas fa-eye text-xl"></i>
                             </a>
                             {% if patient.status and patient.status.value != 'COMPLETED' %}
                             <a href="{{ url_for('patients.edit_patient', patient_id=patient.patient_id) }}" class="text-indigo-600 hover:text-indigo-900 mr-3" title="Edit Patient">
-                                <i class="fas fa-edit text-2xl"></i>
+                                <i class="fas fa-edit text-xl"></i>
                             </a>
                             {% endif %}
                             {# Add admin check for delete button #}
                             {% if current_user.is_admin %}
                             <button type="button" data-patient-id="{{ patient.patient_id }}" data-patient-name="{{ patient.full_name }}" class="text-red-600 hover:text-red-900 delete-patient" title="Delete Patient">
-                                <i class="fas fa-trash text-2xl"></i>
+                                <i class="fas fa-trash text-xl"></i>
                             </button>
                             {% endif %}
                         </td>
diff --git a/app/templates/report_form.html b/app/templates/report_form.html
index 8fad603..0e1de6e 100644
--- a/app/templates/report_form.html
+++ b/app/templates/report_form.html
@@ -402,7 +402,7 @@
                 e.preventDefault(); // Ngăn form submit ngay lập tức
                 
                 // Lấy thông báo xác nhận từ thuộc tính data
-                const confirmMessage = this.getAttribute('data-confirmation-message') || 'Bạn có chắc chắn muốn thực hiện?';
+                const confirmMessage = this.getAttribute('data-confirmation-message') || 'Are you certain the report has been fully completed?';
                 
                 // Hiển thị hộp thoại xác nhận
                 if (confirm(confirmMessage)) {
diff --git a/migrations/versions/8116b7d4aede_initital.py b/migrations/versions/8116b7d4aede_initital.py
deleted file mode 100644
index f61f116..0000000
--- a/migrations/versions/8116b7d4aede_initital.py
+++ /dev/null
@@ -1,103 +0,0 @@
-"""initital
-
-Revision ID: 8116b7d4aede
-Revises: 
-Create Date: 2025-04-21 13:04:43.442726
-
-"""
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import mysql
-
-# revision identifiers, used by Alembic.
-revision = '8116b7d4aede'
-down_revision = None
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    with op.batch_alter_table('support_message_read_status', schema=None) as batch_op:
-        # Drop constraints FIRST
-        try: # Use try-except in case constraints don't exist or have different names
-            batch_op.drop_constraint('support_message_read_status_ibfk_1', type_='foreignkey')
-        except Exception as e:
-            print(f"Info: Could not drop FK support_message_read_status_ibfk_1 (might not exist): {e}")
-        try:
-            batch_op.drop_constraint('support_message_read_status_ibfk_2', type_='foreignkey')
-        except Exception as e:
-            print(f"Info: Could not drop FK support_message_read_status_ibfk_2 (might not exist): {e}")
-
-        # Now drop the index
-        try:
-            batch_op.drop_index('uq_user_message_read')
-        except Exception as e:
-            print(f"Info: Could not drop Index uq_user_message_read (might not exist): {e}")
-
-    # Drop the table after constraints and indexes are gone
-    try:
-        op.drop_table('support_message_read_status')
-    except Exception as e:
-        print(f"Info: Could not drop table support_message_read_status (might not exist): {e}")
-    with op.batch_alter_table('reports', schema=None) as batch_op:
-        batch_op.alter_column('status',
-               existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=20),
-               type_=sa.Enum('DRAFT', 'PENDING', 'COMPLETED', 'CANCELLED', name='reportstatus'),
-               nullable=False)
-
-    with op.batch_alter_table('support_messages', schema=None) as batch_op:
-        batch_op.alter_column('timestamp',
-               existing_type=mysql.DATETIME(),
-               nullable=False)
-
-    with op.batch_alter_table('uploadedfiles', schema=None) as batch_op:
-        batch_op.alter_column('error_details',
-               existing_type=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'),
-               type_=sa.Text(length=16777215),
-               existing_nullable=True)
-
-    with op.batch_alter_table('users', schema=None) as batch_op:
-        batch_op.add_column(sa.Column('last_support_visit', sa.DateTime(timezone=True), nullable=True))
-
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    with op.batch_alter_table('users', schema=None) as batch_op:
-        batch_op.drop_column('last_support_visit')
-
-    with op.batch_alter_table('uploadedfiles', schema=None) as batch_op:
-        batch_op.alter_column('error_details',
-               existing_type=sa.Text(length=16777215),
-               type_=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'),
-               existing_nullable=True)
-
-    with op.batch_alter_table('support_messages', schema=None) as batch_op:
-        batch_op.alter_column('timestamp',
-               existing_type=mysql.DATETIME(),
-               nullable=True)
-
-    with op.batch_alter_table('reports', schema=None) as batch_op:
-        batch_op.alter_column('status',
-               existing_type=sa.Enum('DRAFT', 'PENDING', 'COMPLETED', 'CANCELLED', name='reportstatus'),
-               type_=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=20),
-               nullable=True)
-
-    op.create_table('support_message_read_status',
-    sa.Column('id', mysql.INTEGER(), autoincrement=True, nullable=False),
-    sa.Column('user_id', mysql.INTEGER(), autoincrement=False, nullable=False),
-    sa.Column('message_id', mysql.INTEGER(), autoincrement=False, nullable=False),
-    sa.Column('read_at', mysql.DATETIME(), nullable=True),
-    sa.ForeignKeyConstraint(['message_id'], ['support_messages.id'], name='support_message_read_status_ibfk_1'),
-    sa.ForeignKeyConstraint(['user_id'], ['users.userID'], name='support_message_read_status_ibfk_2'),
-    sa.PrimaryKeyConstraint('id'),
-    mysql_collate='utf8mb4_unicode_ci',
-    mysql_default_charset='utf8mb4',
-    mysql_engine='InnoDB'
-    )
-    with op.batch_alter_table('support_message_read_status', schema=None) as batch_op:
-        batch_op.create_index('uq_user_message_read', ['user_id', 'message_id'], unique=True)
-
-    # ### end Alembic commands ###
diff --git a/migrations/versions/fb934f468320_add_ml_pred.py b/migrations/versions/fb934f468320_add_ml_pred.py
deleted file mode 100644
index 7404e7c..0000000
--- a/migrations/versions/fb934f468320_add_ml_pred.py
+++ /dev/null
@@ -1,50 +0,0 @@
-"""add ml pred
-
-Revision ID: fb934f468320
-Revises: fe0cbf650625
-Create Date: 2025-04-22 01:02:07.781555
-
-"""
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import mysql
-
-# revision identifiers, used by Alembic.
-revision = 'fb934f468320'
-down_revision = 'fe0cbf650625'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    with op.batch_alter_table('encounters', schema=None) as batch_op:
-        batch_op.add_column(sa.Column('ml_needs_intervention', sa.Boolean(), nullable=True, comment='Result of ML prediction (True=Needs Intervention)'))
-        batch_op.add_column(sa.Column('ml_prediction_time', sa.DateTime(), nullable=True, comment='Timestamp of the last ML prediction run'))
-        batch_op.add_column(sa.Column('ml_top_features_json', sa.JSON(), nullable=True, comment='JSON array of top influential features from SHAP'))
-        batch_op.add_column(sa.Column('ml_limit_breaches_json', sa.JSON(), nullable=True, comment='JSON array of features breaching physiological limits'))
-
-    with op.batch_alter_table('uploadedfiles', schema=None) as batch_op:
-        batch_op.alter_column('error_details',
-               existing_type=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'),
-               type_=sa.Text(length=16777215),
-               existing_nullable=True)
-
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    with op.batch_alter_table('uploadedfiles', schema=None) as batch_op:
-        batch_op.alter_column('error_details',
-               existing_type=sa.Text(length=16777215),
-               type_=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'),
-               existing_nullable=True)
-
-    with op.batch_alter_table('encounters', schema=None) as batch_op:
-        batch_op.drop_column('ml_limit_breaches_json')
-        batch_op.drop_column('ml_top_features_json')
-        batch_op.drop_column('ml_prediction_time')
-        batch_op.drop_column('ml_needs_intervention')
-
-    # ### end Alembic commands ###
diff --git a/migrations/versions/fe0cbf650625_add.py b/migrations/versions/fe0cbf650625_add.py
deleted file mode 100644
index a517bcc..0000000
--- a/migrations/versions/fe0cbf650625_add.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""add
-
-Revision ID: fe0cbf650625
-Revises: 8116b7d4aede
-Create Date: 2025-04-21 20:14:07.900955
-
-"""
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import mysql
-
-# revision identifiers, used by Alembic.
-revision = 'fe0cbf650625'
-down_revision = '8116b7d4aede'
-branch_labels = None
-depends_on = None
-
-
-def upgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    op.create_table('patient_dietitian_assignments',
-    sa.Column('id', mysql.INTEGER(unsigned=True), nullable=False),
-    sa.Column('patient_id', sa.String(length=50), nullable=False),
-    sa.Column('dietitian_id', sa.Integer(), nullable=False),
-    sa.Column('assignment_date', sa.DateTime(), nullable=False),
-    sa.Column('end_date', sa.DateTime(), nullable=True),
-    sa.Column('is_active', sa.Boolean(), nullable=False),
-    sa.Column('notes', sa.Text(), nullable=True),
-    sa.ForeignKeyConstraint(['dietitian_id'], ['users.userID'], ondelete='CASCADE'),
-    sa.ForeignKeyConstraint(['patient_id'], ['patients.patientID'], ondelete='CASCADE'),
-    sa.PrimaryKeyConstraint('id'),
-    sa.UniqueConstraint('patient_id', 'dietitian_id', name='uq_patient_dietitian_assignment')
-    )
-    with op.batch_alter_table('patient_dietitian_assignments', schema=None) as batch_op:
-        batch_op.create_index(batch_op.f('ix_patient_dietitian_assignments_assignment_date'), ['assignment_date'], unique=False)
-        batch_op.create_index(batch_op.f('ix_patient_dietitian_assignments_dietitian_id'), ['dietitian_id'], unique=False)
-        batch_op.create_index(batch_op.f('ix_patient_dietitian_assignments_is_active'), ['is_active'], unique=False)
-        batch_op.create_index(batch_op.f('ix_patient_dietitian_assignments_patient_id'), ['patient_id'], unique=False)
-
-    with op.batch_alter_table('uploadedfiles', schema=None) as batch_op:
-        batch_op.alter_column('error_details',
-               existing_type=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'),
-               type_=sa.Text(length=16777215),
-               existing_nullable=True)
-
-    # ### end Alembic commands ###
-
-
-def downgrade():
-    # ### commands auto generated by Alembic - please adjust! ###
-    with op.batch_alter_table('uploadedfiles', schema=None) as batch_op:
-        batch_op.alter_column('error_details',
-               existing_type=sa.Text(length=16777215),
-               type_=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'),
-               existing_nullable=True)
-
-    with op.batch_alter_table('patient_dietitian_assignments', schema=None) as batch_op:
-        batch_op.drop_index(batch_op.f('ix_patient_dietitian_assignments_patient_id'))
-        batch_op.drop_index(batch_op.f('ix_patient_dietitian_assignments_is_active'))
-        batch_op.drop_index(batch_op.f('ix_patient_dietitian_assignments_dietitian_id'))
-        batch_op.drop_index(batch_op.f('ix_patient_dietitian_assignments_assignment_date'))
-
-    op.drop_table('patient_dietitian_assignments')
-    # ### end Alembic commands ###
diff --git a/requirements.txt b/requirements.txt
index 9fe069ec95febee3cd67ed36de2e4e5dd2468719..43852f9a197652224e64a4d6b7d93be7d0f44d64 100644
GIT binary patch
literal 12936
zcmezWFOeaMp_n0uL6@O`p_0Lt!Ir^@K@W;~8Mqh{8FCm>8FCqt7%~}>!SaR-dJKjP
zreIYN6?qJm44Dl1U|AEexE_N614OQXp#UsyzyJ~h=|qU9Fk~_mF%*O4L8gNAnn3jx
zF{Cr(GvqNCG3YWRGo&%3F=T?(88H}x?FX3#F$tSWU9fxe7(h1UF{DH7Fk%4N0`ecs
zbwvzC4EYS@V3R>|AhTg|#SH0S_oXtVf#pGR7GS#}_7*dgFqAOlGh{QQGUUN+&|?7k
z!Vu~UkeX74Jca^>M6gd&!6t!hGGowVFovtDWXNMkX3%BGVJKoK1*?I$5@Z_0HjrIK
zU>Q(Ig4_bL10n+nMM&sC^g{GiF_bbCF{FY+F(1qV#jzpSc92O%P&Xjdf!qvInaGe1
z)|bSP!;sIA1h(4{nleB-A>ri207^lr3^`zVkj;kR5P--gF(fi1Gh{OqFn~-fVJK$M
zV<-WK1t>m2DGTNrkWQGMWQHP!N`?Z460n(|@C3<1{F}s(%8<xV%22|P$&kiS%8<iQ
z%#hDe%22>y0#^-jB_wn~t_SHzX2<}`gTfu*dXQ`;LmoH|A)yNj8FO%21%+P{Lq0f;
zL9PV3!GysQL(Uj%E=Zp-*ykYs!|aCWP6qoP5%<{CRWN|ukq$Ns<UWvjki3w@P|A?W
zki(DymIH}{Tmr(NPzA*nBxIbyVV%K{%HRynIVIq<4T%?siI9*4`70T&7L<bX!J!E9
zBPdsc(iX)1$qcCsMbMCj<vs%jNE$N)=Osf1b8z}W&Y>U|8Ztn_8DdukIL1>LQlT*l
zvIXRRNPL5029`TY7<3u(z@e23E_pH;s=#glxeekjP}u}A6_k1)We&(JkgYKDa=>L(
zHaNT>IaUwuPEcAdWk_KtfT{tR0y7<C1|%#&F$Pju#E{4UatTNUC=5X%Xa@ChK0_`;
zF4zu`48#o(mx0PHP`H9}9;{RanGI3{b0f&)VulQc5{6U;P#hxL0CNK@7vwXfGl0|=
zFl0c>XcKTg1lel=bxkSQ6p))r7_z}>31l)PO~b+)lGl?NDjAZ&r8-0wWC|q5K<rHc
z$5u82B)(D^lA*B%QU?l6hz~%rpp=;ouQxzGM3o1HBFNpKuz=VFatTNs%qCFT3UYZW
zLkdF$LoT!w0oeq~gD_Q@V84N46;cX;<Uy&y66|+S84n6UP)QC7MUeT3T<XG*#gNDV
zsskWt1*8uY4@L~8P~8Z5U9d_}`UBNxAUz=SK(-k&fJjKpry!XKs+&M1TQP$!11RSu
zf<p~fr-AH3<O6hl#n74&qzaOYP*os8SQlIdfpRIRJcWcLNEgT-urNaQ1*kqM1J_-k
zl!)pFSnQ#yuK=e*P%Q?k$3Zkm7sw7!Xkya?DZdeE4CEG2?!cxtmmwb<E}&Khq~r#b
z+8`T1IzTpH^93m7fZ_m@Hgv)DKPaz)>M~HsfqYiX04Y;JaRV|1lD<(xv527%oJT?K
zgQPxC9z|6HN<E-hgw%MDniOOnBv(MnLR1})G+V>~N->~XGLHdNw}E^Fatp{;nBf3%
zc?m-rI4|ciq%xF)OFBq71qo{i32`ALy(=&{Fet!%Z^fVwhAs>sRwzR_gDZnRgAoI$
zrK8GV#9+V>39cWD&_qEY1+o$3Q&5?%&)~?A!cYcI@1RmXg#n`5ks$}%?kZx)WGG?C
zV8~_A2iGQ`k_==9%*FZ)DGU(z=`t9C>qH}H-3Q7M1q{Xvp!xxoT@Egl;=v_q3WFI~
zeK|u0Lk@#71E?fSWB}z!GX`4*Glnz<Lxw~KQ-)NAR0cx^GX`@8O9pcWQ-%~U%ap;0
zA&J3=!Ga-`!GHneeiH^z&1?$Rm%?DlkjP-pU=Fs!1WX%(+mNOV=HM_&V@P5!V=x1o
zmds$rV8~#>V8M{ckP0>hmdio8091N|!U2@pK(Px-MWB)i7Q6Wj$>6dH6kedT8jsY{
zgXB|CdI0$V5~oEBX$-myDGZ?Y9;jvkwW%R#8`OeIfwm$+dO#@wW(!1D8o11^V8E^(
zRN}!(G>Cdgx&*~`BDnm3l}hNQgW6D_xB=C}AU}fSjllH_q;3JV{u9A1ka%#40mVD0
zjzE;^pilu}h<ib`d_1%?hm;bKTnbSEYqzH|R4{<*7D#yj3OkV9Fg1|!0Hg*~Ps2hN
zRMNw0BT)VXsfOhFG;n(c)XD|rGElvf&X5OA!;ta}WHv|#B2)?K&}BfBT96U}6fOw;
zZVZV?twv)8P^tjg00{$->7cxp1#X>!QW7Y1At4EJ3&`b=d;{?*ESx}X4p4g;qy}Uc
zC}qMz45S8>&(YH)$X%dX22xJBG2}2LG88i)N)?biB$N<xx(rSXsI6>>+hP4PWR*S)
zp!Nr-bqaD7$TV!W=`#2-WJ231pjrW>7E}&G%mt->WHW;q0vUW5K&1^VTp)c<h@Y{!
zBAg+F!3|uJgG>a4AH+|fTmeeoptcC8mk3JlkoW?XCZN_L$TV22fbub@zm(4as!bv4
zK&}Lp5+Jo8ccw8EGNdr%!R<oS8le703A8^2@*l`0pzwg0o(>LoNS_5H3JN8dOezDY
zq=3|dka7W}*O&p+UV@|mP;VC$&Y(IDRI6k%=z{xSdEoXh$cLE>WelKFA`#r0DP@4v
zRv>r4Yy<VmK|Vk?3zUaIWihCZL*zx|^oLysa!Lc~2jzTF+XCW8P#l0tHe`Q-!ZjUS
zPJ#RY%G;1$7f7u!xJ-eC2sV|VHWVm!3ZQk15jYM&z60sUrW4eT1@-<?z%_9u14t(*
ztkS?WE2MP}3Tbn2Z9~`u&}aq7hoGK6DCBj)ecyEONC2#iGGqYt%0U=2tWn(q>SuuJ
zQC+Ahu($&G22xg|hC8xu(0EHaLm@*cIQ|j2!w6i`5b_DA1O%0YAR9rpfzmj(bcSpr
zsO&-YDI_<7!k>_RAiE&_@FH-13@SZA=@gPi5h)(jH-?pGxcv?)IYDU^q8sK1P+t?5
zUXZPz^o1Cq0_9q4u12jDA$bZ^yMb&0`4p5RA^mPt_kl)mK<-K;m?j}%Q3~!ig2oq8
z89<={YQ<(UKy-s*1X5q1r=Ub|zZBG)0L37ve+eqFbQuc3eR)ug3n{Y<8T1&;z$0Oh
zQV`KU1hxCCklYCJ6QpJUsR6a8^1%5Ml7}HN3@RZZE-7Lt0FOhV`W@u%Tn1f+G6qAq
z$%bItVQC7Y4^-EJ!V%&+<TQtn*9EtiKzS6@hDN9cg$c|rpb;}r%LtSPAmuQqWeaj8
zEdDdV?MskukpGZVRR)78yhjT09f*Xa1W<1q)W^&Pw?q-TK_)}&2B|0khXKfLNLdN;
z4agQqj)kcJm4%=-D9jW{iwR*5$dn3rXd&m?3<gNr0JRE0HD(@oqyy9{gtXf#!Sx7a
zBnV_TWYiMU*UbdiOpsIr@)xA7n#llbXMkJ-5(kAQB-S$-a^a(I5I4s&fO-O;Q8-8|
z4q_T8-GfSUNQ{DNdr-{^YN3Hf+CZrgWHuxmK;Z>SFCbNrJdp!VQ;>WPQUglekgx!i
z$(7KS5~xIlj7NcNG6MG-kX0eKv_bBH#5%GHP&+jRTra}<TOf6yP=dJ|R0=`-1&KY7
zYe2T4%7gkdpfm_dv8X8wWERXukR2eiKqWt@T>+|TAo&jzz95y5R0HZugL<o=JgWyD
zTLHDeK>a0%YEX=Tas$L%bk(4+1+|z!eQ;11gIo)8HN?fJszEIf&`2?;BtUJ!fLsGJ
zA5zwU%z~H$>Ki~z^8%-QkeQIW734CIPDonG0;dd6?-~>~keVAL3o!*0<5}R5U`RRw
znGcc$nE?w2Q0V6|n8MRN#AZl&ngwkOgGMbuDF@_dNQwc4CaRiZ23Wfgk~$%Fz;Zv#
zBxF^fa-{&=qX3NygGvvOA0Y7yi3Lz<f{k5*Y=neJ7I^Hq5<LC^aSg~%ARog*6EwCA
zlLxheK=B3gC#a<c3IkAlfiNr#QPrn{TWzT01E5gBZaQdO1TtR&av=mm!X4SvcyJoa
z1kXD_%5hLG0J#C1sUX#e(hO8Df^sJ|-^Vk6(rgjf#gMQ7xxf(IZiAG5sG)_bJ{}r|
zkkS=2Zv+W<P*{R&g{2W>pMd%*kTE?-{s5IM*jxrtr^|qu|3N7OWCNspM0FoT7pPBO
z$&e0C+o<sX(Tf?rFn!oU1J<hm=>pCCfbu=WpCHpfF$?i0NUo9r)^i1=6NpYkEP+O8
zLH>cHB~aTQ66T=v2MP^Hh(p{0YWIUuIH>ml>IFl_GC?XKu>%P+Txwyd7Geh?-+{sn
zgiRSBJ$>BzQOg^U9uS7)S&%MJjSb4NpwhC4A)6tGp^^dQUQnqCu@%Hif%Zy4sTGp4
zK%odJO+YPCP>T^(?}5}n{0C8!!%zku9|pCj5hXil+y_=FfZDI1zB8zw49lA!S3*Jv
z5-T~-aS2d-K++_v*XIlF&m+PW5}P2qK{6merGRG&A+-fa77}Kl*#Tr(UGS)bE=n5&
z<O)d6gqa1(1HlZ53~A62Oh{<J`V=5DLG1^Sk4m6@A&`q9w!&PDO%=#?Q2P#4z9K>i
z6poOVLM}rJxc>^Ng+cBD$w9&fly5<`IVdh6dO`Mq)WGC(7~&aH7)ro%bD&uZkefjA
zppb;5K+u>8sQn9a2PB=LrX1KfIxNm|89=kFptdzA=0N6w+ylap@XBSVWGH4R1kX={
zYS%mlP#Xd?1_Ub6Afb%jY5=(iRMvyy4xt~EzK}~yScpU14QhjdT1AK!8^{z$4CgT<
zVU)KZSx{bwxDuiQR4+i%I;2d7l(L|l3<*DwDp<c1R5yWq3ko@qIWQHVPzB9_f!YtC
zF~Vf{ygDdM5M>7_j!U4q2^32p8OR(0X!ZgW@<|M!9yVkY2;>rwi?OQ!mEsu;pmrk2
zMId#cuz}QOdEmAxXx0HH2O2Gfr9g-*Xp{gX3v!_mc-{}9H=hAiAA>>`GIt1)HwLd8
z0QDU}u>(^9GNA%G_XF|`NF^jhKq^WY;u%uG?UrJ&Ye05^RDnVk68;cXpmYaGw;(?l
zf^CAy7lG#=Kq^2Z1)y>r5=Rg-VEzHM?@Aaz=?s(>K=y!K0<sHaBB<R0${#Q@A#n{V
z;XyMskU9Vq(jW{`154MSek>?QLdrJCNIArRAQhl7Jy3kZ>;t6<NC?7YL1RadQVkTd
zuyI_F+dyg|DHx;{Bm*hIK<)*VpfGblYdk=C64d7awfvFW>!7>>shdDP1@*E)aT5R@
z@dcGYkkt{OR0^^U=Cf?*m<T9E!(46%ZYhA=1@j%KZUD`AfO0FS>_M#e0EIkACCt2h
z@a!;X#uH>3C<TIKVdfxYA$1ujCb6k2VgSv6f<~Z0vvHsvCP+P~h5?Q0K;j$}Hik%j
z4@ln{)Jp>8JXm=Js+}R~L7@WDV**{T0Z|W9rwbl$0@(s`3&g!3f5H3&at%lwWNZ?o
z1|$m$YfvZ`Fo4oDsD}k9kwGC03Ta5m1Cp%-r^f<@bnr?T(5Mh7M<It3NIj(WQvfd8
zLE{aeRwt+h2C@_6c2Ic2!V=QI%tPwSgGvvWn-Q|0ej;eZ5;Q*mDwkkpf&73_os2Q#
z0kI2~Dk~X4Z30lc3)ChDnE?tXNIif^YnbgbkSQQ}Sl$d^sATYEsAPbQ93#vDwHP5G
z30g6d2wr7_$ZMdugV={q18S2%Y6D118zc`3Lxi6oF`NbNO@K<D3}{&j$>SjPAlD((
z2Qj!YI5QZ7p(VKQ15$$s0Z_h#%weOZLQu|w)r27b1%pS4v!Sy<ATvN^3Bnv$O$QoV
z1(ny3Gy{rXko#eI9n@C=xi}9z(*&w%QlaZ*AnHMG2bB$o^nfrI)W!joF?kGSaDRZz
zNB9+?7S;>Xg^q55M%ExLSda}MU6A%6sB|x3fQ(6jd<+>WLG>A^9h}Di8o>j_tTBTD
z1EjqI(u<G-<z>v)2go*r-yy0&Ylk3XrXX36TMQW>{so0|B!eS^FM|)*hmf?7oQkR#
za^Z8vp!{eEUeN|hKZOjC+5uFGf_mSO+6JTsgdy=1$WX!nvIFD~klmo%0+9(~fQ;^?
zFt{<4GE_lB1d>8QBqWSM{RvPEfXXb$_%+1!pz%JC8$cx=s0RQ^iy(a<yCMDu$$?sw
zAR07EfXD|RGeGGRmfAonK=B1>??7S&<Wi7XpqdkA6R6GutpEa*<A{D5$X1A-K;}Tk
zs6jO&#7!VygXVvc?Lt<g3!aSxg&$}wR}OS_2`H37u7rg`8bdNTKN&Mvf=3=fr5R}S
z3zVKgwFxMELFz&JAn6P=dIr*C$zZ`?Mo<sT{h(3_HqrqK0g#z6Hxw|WFo4ElL8%a7
zA0p+#))s)=04vcTAy*6?=>?exDVss-`ar!BL>NKRV=;p<18AidsP9t@?hion5Xc=6
zGhymLZ9zz_qzfKx1C@1<-iRS|Mgrn)(5MJx)D7fjkQ^xO!(s!}_5rQf1BD?(H^e4T
zoWg7Z^)^8zRVKW>1@Z?Z-@@zw&1FJbKp<N{x<TeZOb7;#Or$UvF@%8EI)G;PL8?G*
zL6kfomw@U8P+g|WP|N`99fHyis62z&lmX5+ApIa4kZ0;Z=7RDR$i1k!9a4IL<Uw+v
zyaXD<1DOQLQy`as(mEvN6f<NpKym^|+z{M`h1m|$3tC}-h!v14AoX)Gc#at~>j)ZW
z0=W>R9}*UzbO7qvgK`gOJPy>V1o;*;*8u9nfa(O04v=p_p$oGQ)CL9B`=GuGD12dR
zAfo}0+7ENy6l4m>U7*|ou^H57g0%@D<rGLB<Y$;opmrdr*997R2IVTqNE=8Ms7!>E
zD4>!EQrZ<UfO-_5GzVFS2(l5m6@^_T!fa3sgKA!opAcaQ%1e+mgRCDk@(f#*i_adI
zI#A0tA3R?QT8#l&9|6g0pxz_MWJs)o{DxdIfn<spa=|SuP)ir$E=c^rY6O@XP+1CE
z-Gyjh8-izVA)yJX6+vYoNCl|O1eI=}UMi%<1(^u)2TU!f?gp(%1<kU6;un-^KyCx2
zJW%X`+Jm4q)ga##GeAaaL25u|K*}vp-w)&>nCn2f05tOhT2llw5j4IIF%8rv&H=9q
zgtTZOc0pVPQ&GeKYFC2le~^Aq3V`H|67Xy|B>X_>6tr#zHckO4w?HE@pwI@5mV@+y
z(imh$2If{!9|;j|kUk<TEFf~a4CM@|44_^LsILPWJ41FU$nOZV^1<Viptb<0Rw`xy
z<z7(l2$Z^Dr7LKaD##rmKN^DD`XCbGS5Vl2(lTgmE~wT5)wqb3AaYrWXlv+#$8$ij
z4C>{>QZ@E=704z~s}<x169xkYP$>>6k09oO;w>HArv;7pgUVTu4?yNZTn`FmLvT+D
zmNTlrZD>$_1%)r7wuXcWXeEFy10t_OQZXcsL8UDuq)Soeh(KWjau=*t0L_PhRvduR
z6R0Ny3Rw^vWFIK>LE#892NYu<IdpXpcf;}x$V5;tGm{~S0kJ|F68E4G0>uTW=LCv1
zP<s~C8UdA$kT3wHbP$Hv2};?ZaunoKP)dWfPC+37T15}?D@0{ELo#%X7!+3^*MsCh
zCc(xWAn6QL8bfkC#6FN8QB{EA1*8hp)&kWX5OpBaAYlh`Kg29hDGw?~LA_2$ssxqY
zAisd@2bqkBg>dj}el~bjb14I4JOC87AXShO7u0@5%y5Cmj6kcPK;aE4he7oWq}B$7
zHpn%Q)<rqEo&~jfVRj<-oyr+Nb2_lzC`<(?R3M=SSz7~Y5rD?yK=L3rflP;_Fv!YW
rSbHBN50XRZuVMhTHz0Wkq!Th@2{F40I^PQ!<p7PtAoPROLR0|&(+Yb#

literal 755
zcmY#ZaL&&yNG!=r%FM|usZ>bItl%n6Eh#N1$<NOzwlOo)Gte{O;v!mEcxsYDT2W$d
zYI%N9HkVsYVsW;ut&yIQo-u@@8yx84n3J55np<gWYpiDg7x&FfFG?&)wY4?TGte`E
zD)z}w&&;#6HPAEDGlB{^B^Ol|l-SxD>KW)6LdC*E+&}_`dWKx#A#V9axy2B(4Y^Wt
z6Ekyk%Mx=kQxZ$^i$G3+`o|@)Brz$mIF&26vbZoOIVUqUuLL9wG6dO;MquY8=A`B(
zWhR4dG0-yt1qjFhg@U5|<kaHg%)E4k#Jm)RvdrSr#GK5k#FEVXJg$Poyp+UZkRDS#
zW3If?+=5CF$H+v_h$}a-q#!51BquWo<WzG#L$3US)VzYqiX0H%P|t`fATuW?zueZ=
zQqNS+fU79AAit<2Cou^UK;S3|Edj+@W@<54L1jrsex7bheo1Ox8ORQhC&N>Vva3={
z(?Pxhg@1Z!US@KBQJ$@>k%69}o&i@<PHJLuhOI3mjteSFsz6dk#(KtFMX80Qsl_G5
zV7(T4MqC9cY1x_3U^n2Z$SJNUFUl-QEdsgLK+lpZDK)XQBr~lvr#Qc~zy#z-LnBDS
zaY-#p&B-swP0cG&P*Vs=EiTE-O9#0)wYbFA)?Ck6&k)MfO@kyCLp_kz<ovSKqQvx6
zkQyUBBd(;J#AHxfG6uUnEhjNM)dJ@B%;Nl_5?fnSJ+P&@m7st&(6h8ORN!(;%uX%h
RD#^@EO-#?{D$YnO008sV=J@~s

-- 
GitLab