diff --git a/README.md b/README.md index f623da6a169b6fdf7dfe8d448407fa8594241d73..21b6e9bfa7bb2e0458d26d2a5810accc92518abb 100644 --- a/README.md +++ b/README.md @@ -51,21 +51,31 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False UPLOAD_FOLDER = 'uploads' ``` -5. Khởi tạo cơ sở dữ liệu: +5. Chạy ứng dụng sử dụng menu trợ giúp: ```bash -python init_database.py +# Trên Windows +run_ccu.bat ``` -6. Tạo tài khoản admin (nếu chưa có): -```bash -python create_admin.py -``` +Menu hiển thị các tùy chọn sau: +- **1. Initialize the database (includes migration)**: Khởi tạo cơ sở dữ liệu và thực hiện migration +- **2. Run the application**: Khởi động hệ thống CCU HTM +- **3. Create admin account**: Tạo tài khoản quản trị viên +- **4. Update database schema**: Kiểm tra và cập nhật cấu trúc CSDL +- **5. Exit**: Đóng chương trình -7. Chạy ứng dụng: +Để cài đặt hệ thống lần đầu, hãy chọn lần lượt tùy chọn 1 và 3 trước khi chạy ứng dụng. + +Hoặc chạy trực tiếp từ dòng lệnh: ```bash -python run.py -# Hoặc sử dụng file batch trên Windows -run_app.bat +# Khởi tạo cơ sở dữ liệu và migration +python run.py --init --migrate + +# Tạo tài khoản admin (nếu cần) +python run.py --admin + +# Chạy ứng dụng +python run.py --clean ``` ## Sử dụng @@ -110,11 +120,8 @@ ccu_bvnm/ ├── migrations/ # Các file migration cơ sở dữ liệu ├── uploads/ # Thư mục lưu trữ file tải lên ├── instance/ # Cấu hình riêng của từng máy -├── create_admin.py # Script tạo tài khoản admin -├── init_database.py # Script khởi tạo cơ sở dữ liệu ├── run.py # Điểm khởi chạy ứng dụng -├── run_app.bat # File batch để chạy trên Windows -├── clear_cache.bat # Script xóa cache +├── run_ccu.bat # File batch để chạy trên Windows └── requirements.txt # Danh sách các gói phụ thuộc ``` diff --git a/app/models/measurement.py b/app/models/measurement.py index b5bb5f00f9eb321701c68d34f64f777aafbfdd77..699d05236e200d63d5668f32cf2793be485b30c1 100644 --- a/app/models/measurement.py +++ b/app/models/measurement.py @@ -38,5 +38,12 @@ class PhysiologicalMeasurement(db.Model): tidal_volume = db.Column(db.Float) notes = db.Column(db.Text) + # Trường referral_score để lưu kết quả phân tích ML + referral_score = db.Column(db.Float, nullable=True, comment='Score from ML algorithm indicating referral recommendation (0-1)') + + # Timestamps + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + def __repr__(self): return f'<PhysiologicalMeasurement {self.id} for Encounter {self.encounter_id}>' \ No newline at end of file diff --git a/app/models/models.py b/app/models/models.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/app/models/patient.py b/app/models/patient.py index 60a06ef83dda20a014dab1840467959e36ebfecf..c4b24defbaf4c0957df1c1af78ff2c4aa6178588 100644 --- a/app/models/patient.py +++ b/app/models/patient.py @@ -1,6 +1,7 @@ from datetime import datetime from app import db from sqlalchemy.ext.hybrid import hybrid_property +from datetime import date class Patient(db.Model): """Patient model containing basic patient information""" @@ -10,7 +11,7 @@ class Patient(db.Model): firstName = db.Column(db.String(50)) lastName = db.Column(db.String(50)) age = db.Column(db.Integer) - gender = db.Column(db.Enum('Male', 'Female', 'Other')) + gender = db.Column(db.Enum('male', 'female', 'other', name='gender_types')) bmi = db.Column(db.Float) # From CSV status = db.Column(db.String(20), default='active', nullable=True) height = db.Column(db.Float, nullable=True) # Height in cm @@ -20,6 +21,7 @@ class Patient(db.Model): # Timestamps created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=True) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=True) # Relationships encounters = db.relationship('Encounter', backref='patient', lazy=True) @@ -47,6 +49,59 @@ class Patient(db.Model): if self.firstName and self.lastName: return f"{self.firstName} {self.lastName}" return f"Patient {self.id}" + + def calculate_age(self): + """Tính tuổi dựa trên ngày sinh""" + if self.date_of_birth: + today = date.today() + return today.year - self.date_of_birth.year - ((today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day)) + return self.age + + def calculate_bmi(self): + """Tính BMI dựa trên chiều cao và cân nặng""" + if self.height and self.weight and self.height > 0: + height_m = self.height / 100 # Chuyển từ cm sang m + bmi_value = round(self.weight / (height_m * height_m), 1) + self.bmi = bmi_value + return bmi_value + return self.bmi + + def get_bmi_category(self): + """Trả về phân loại BMI""" + if not self.bmi: + return "Not Available" + + if self.bmi < 18.5: + return "Thiếu cân" + elif self.bmi < 25: + return "Bình thường" + elif self.bmi < 30: + return "Thừa cân" + else: + return "Béo phì" + + def get_bmi_color_class(self): + """Trả về class màu cho BMI để hiển thị trên UI""" + if not self.bmi: + return "gray" + + if self.bmi < 18.5: + return "blue" + elif self.bmi < 25: + return "green" + elif self.bmi < 30: + return "yellow" + else: + return "red" + + def get_bmi_percentage(self): + """Trả về phần trăm để hiển thị thanh trượt BMI""" + if not self.bmi: + return 0 + + # BMI từ 0 đến 40 là thang đo phổ biến, quy về 0-100% + percentage = min(100, max(0, (self.bmi / 40) * 100)) + return percentage class Encounter(db.Model): diff --git a/app/models/procedure.py b/app/models/procedure.py index 5ffe1b7311f9f0d0f1ccb9019567eaf37da091eb..f2bb2a20305ddb0c491cc056f3496b12304b77aa 100644 --- a/app/models/procedure.py +++ b/app/models/procedure.py @@ -10,9 +10,18 @@ class Procedure(db.Model): patient_id = db.Column('patientID', db.String(20), db.ForeignKey('patients.patientID'), nullable=False) # Procedure details - procedureDateTime = db.Column(db.DateTime, nullable=False) procedureType = db.Column(db.String(100), nullable=False) - description = db.Column(db.Text) + procedureName = db.Column(db.String(255), nullable=True) # Cho phép NULL + procedureDateTime = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + procedureEndDateTime = db.Column(db.DateTime, nullable=True) + + # Results and description + procedureResults = db.Column(db.Text, nullable=True) + description = db.Column(db.Text, nullable=True) # Đồng bộ với SQL, thay vì "notes" + + # Timestamps + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) def __repr__(self): - return f'<Procedure {self.id} ({self.procedureType}) for Encounter {self.encounter_id}>' \ No newline at end of file + return f'<Procedure {self.id} - {self.procedureName}>' diff --git a/app/models/report.py b/app/models/report.py index cc424d88ac5d845b42b8e57177d81ef4b5be6391..3ad5ff47201acd78811624c691043dbb9bc0da24 100644 --- a/app/models/report.py +++ b/app/models/report.py @@ -14,6 +14,7 @@ class Report(db.Model): report_content = db.Column('reportContent', db.Text) status = db.Column(db.String(20), default='draft') created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Trường ảo để tương thích với code hiện tại @hybrid_property diff --git a/app/routes/patients.py b/app/routes/patients.py index 21ee034936218d250cfe20d7ee20c9abedb9253b..6d94893621ae0a9b5d545c803f9989a3999fb1a9 100644 --- a/app/routes/patients.py +++ b/app/routes/patients.py @@ -171,12 +171,22 @@ def edit_patient(patient_id): # Update patient patient.firstName = request.form.get('firstName') patient.lastName = request.form.get('lastName') - patient.date_of_birth = datetime.strptime(request.form.get('date_of_birth'), '%Y-%m-%d') if request.form.get('date_of_birth') else None - patient.gender = request.form.get('gender') + patient.age = int(request.form.get('age')) if request.form.get('age') else None + + # Chuyển đổi gender thành chữ thường để đảm bảo tính nhất quán + gender_value = request.form.get('gender') + if gender_value: + gender_value = gender_value.lower() # Chuyển đổi thành chữ thường + patient.gender = gender_value + patient.height = float(request.form.get('height')) if request.form.get('height') else None patient.weight = float(request.form.get('weight')) if request.form.get('weight') else None patient.blood_type = request.form.get('blood_type') + # Tính BMI nếu có chiều cao và cân nặng + if patient.height and patient.weight: + patient.bmi = patient.calculate_bmi() + db.session.commit() flash('Patient has been updated successfully.', 'success') diff --git a/app/templates/edit_patient.html b/app/templates/edit_patient.html index 46d1af1ae6c101398001e71fe1fab1c0dcb05474..5942eeefd00b0d79f62c78309d303402c30195f0 100644 --- a/app/templates/edit_patient.html +++ b/app/templates/edit_patient.html @@ -28,7 +28,8 @@ <svg class="w-3 h-3 text-gray-400 mx-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/> </svg> - <a href="{{ url_for('patients.patient_detail', patient_id=patient.patient_id) }}" class="ml-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ml-2">{{ patient.full_name }}</a> + {% set patient_number = patient.patient_id.split('-')[1]|int - 10000 %} + <a href="{{ url_for('patients.patient_detail', patient_id=patient.patient_id) }}" class="ml-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ml-2">Patient {{ patient_number }}</a> </div> </li> <li aria-current="page"> @@ -79,10 +80,10 @@ class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"> </div> - <!-- Date of Birth --> + <!-- Age --> <div> - <label for="date_of_birth" class="block text-sm font-medium text-gray-700 mb-1">Date of Birth</label> - <input type="date" id="date_of_birth" name="date_of_birth" value="{{ patient.date_of_birth.strftime('%Y-%m-%d') if patient.date_of_birth else '' }}" + <label for="age" class="block text-sm font-medium text-gray-700 mb-1">Tuổi</label> + <input type="number" id="age" name="age" min="0" max="120" value="{{ patient.age }}" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"> </div> diff --git a/app/templates/patient_detail.html b/app/templates/patient_detail.html index c4e7647d58e734ab5752e8e85d5a6dd888813ef3..12499638d21070e70032a86333ec20839226860d 100644 --- a/app/templates/patient_detail.html +++ b/app/templates/patient_detail.html @@ -17,14 +17,14 @@ <li> <div class="flex items-center"> <svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg> - <a href="{{ url_for('patients.index') }}" class="ml-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ml-2 transition duration-200">Bệnh nhân</a> + <a href="{{ url_for('patients.index') }}" class="ml-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ml-2 transition duration-200">Patient</a> </div> </li> <li aria-current="page"> <div class="flex items-center"> <svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg> {% set patient_number = patient.patient_id.split('-')[1]|int - 10000 %} - <span class="ml-1 text-sm font-medium text-gray-500 md:ml-2">Bệnh nhân {{ patient_number }}</span> + <span class="ml-1 text-sm font-medium text-gray-500 md:ml-2">Patient {{ patient_number }}</span> </div> </li> </ol> @@ -55,7 +55,7 @@ <div class="px-4 py-5 sm:p-6"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div class="flex flex-col"> - <div class="text-sm font-medium text-gray-500">ID Bệnh nhân</div> + <div class="text-sm font-medium text-gray-500">ID Patient</div> <div class="mt-1 text-lg font-semibold text-gray-900">P-{{ patient.id|default('10001') }}</div> </div> <div class="flex flex-col"> @@ -177,14 +177,16 @@ <div class="bg-white overflow-hidden rounded-lg border border-gray-200 transition-all duration-300 hover:shadow-md"> <div class="px-4 py-3 flex justify-between items-center border-b border-gray-200"> <span class="text-sm font-semibold text-gray-700">BMI</span> - <span class="px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800"> - Bình thường + <span class="px-2 py-1 text-xs font-semibold rounded-full {% if patient.get_bmi_color_class() %}bg-{{ patient.get_bmi_color_class() }}-100 text-{{ patient.get_bmi_color_class() }}-800{% else %}bg-green-100 text-green-800{% endif %}"> + {{ patient.get_bmi_category()|default('Bình thường') }} </span> </div> <div class="px-4 py-4 flex flex-col items-center"> <div class="text-3xl font-bold text-gray-900">{{ patient.bmi|default('23.5') }}</div> <div class="mt-1 w-full bg-gray-200 rounded-full h-2"> - <div class="bg-green-500 h-2 rounded-full" style="width: 45%"></div> + <div class="h-2 rounded-full" + style="width: {{ patient.get_bmi_percentage()|default(45) }}%; background-color: {% if patient.get_bmi_color_class() == 'green' %}#10b981{% elif patient.get_bmi_color_class() == 'blue' %}#3b82f6{% elif patient.get_bmi_color_class() == 'yellow' %}#facc15{% elif patient.get_bmi_color_class() == 'red' %}#ef4444{% else %}#10b981{% endif %};"> + </div> </div> <div class="mt-2 flex justify-between w-full text-xs text-gray-500"> <span>0</span> @@ -328,6 +330,285 @@ <!-- Phần còn lại của tab tổng quan sẽ được thêm vào phần 2 --> </div> + <!-- Tab đo lường sinh lý --> + <div id="measurements" class="tab-pane" style="display: none;"> + <div class="bg-white shadow rounded-lg"> + <div class="px-4 py-5 sm:px-6 flex justify-between items-center"> + <h3 class="text-lg leading-6 font-medium text-gray-900"> + Đo lường sinh lý + </h3> + <button type="button" class="inline-flex items-center px-3 py-1 border border-transparent rounded text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200"> + <i class="fas fa-plus mr-2"></i> + Thêm mới + </button> + </div> + + <!-- Bảng đo lường sinh lý --> + <div class="overflow-x-auto border-t border-gray-200"> + <table class="min-w-full divide-y divide-gray-200"> + <thead class="bg-gray-50"> + <tr> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> + Ngày/giờ + </th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> + Nhịp tim + </th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> + Huyết áp + </th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> + SpO2 + </th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> + Nhiệt độ + </th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> + FiO2 + </th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> + Tidal Volume + </th> + <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"> + Thao tác + </th> + </tr> + </thead> + <tbody class="bg-white divide-y divide-gray-200"> + <!-- Hiển thị dữ liệu từ CSDL --> + {% for measurement in measurements|default([]) %} + <tr class="hover:bg-gray-50"> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + {{ measurement.created_at.strftime('%d/%m/%Y %H:%M') }} + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + {{ measurement.heart_rate }} bpm + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + {{ measurement.blood_pressure }} + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + {{ measurement.spo2 }}% + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + {{ measurement.temperature }}°C + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + {{ measurement.fio2 }}% + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + {{ measurement.tidal_volume }} mL + </td> + <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> + <button class="text-primary-600 hover:text-primary-900 mr-3"> + <i class="fas fa-edit"></i> + </button> + <button class="text-red-600 hover:text-red-900"> + <i class="fas fa-trash"></i> + </button> + </td> + </tr> + {% else %} + <tr> + <td colspan="8" class="px-6 py-4 text-center text-sm text-gray-500"> + Không có dữ liệu đo lường + </td> + </tr> + {% endfor %} + + <!-- Dữ liệu mẫu --> + <tr class="hover:bg-gray-50"> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + 11/06/2023 08:30 + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + 85 bpm + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + 120/80 mmHg + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + 98% + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + 36.8°C + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + 60% + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + 450 mL + </td> + <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> + <button class="text-primary-600 hover:text-primary-900 mr-3"> + <i class="fas fa-edit"></i> + </button> + <button class="text-red-600 hover:text-red-900"> + <i class="fas fa-trash"></i> + </button> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + + <!-- Tab giới thiệu & đánh giá --> + <div id="referrals" class="tab-pane" style="display: none;"> + <div class="bg-white shadow rounded-lg"> + <div class="px-4 py-5 sm:px-6 flex justify-between items-center"> + <h3 class="text-lg leading-6 font-medium text-gray-900"> + Giới thiệu & Đánh giá + </h3> + <button type="button" class="inline-flex items-center px-3 py-1 border border-transparent rounded text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200"> + <i class="fas fa-plus mr-2"></i> + Thêm giới thiệu + </button> + </div> + + <div class="border-t border-gray-200 px-4 py-5 sm:p-6"> + <!-- Trạng thái giới thiệu hiện tại --> + <div class="mb-6 p-4 border border-gray-200 rounded-md bg-blue-50"> + <div class="flex items-center"> + <i class="fas fa-info-circle text-blue-600 text-xl mr-3"></i> + <div> + <h4 class="text-md font-medium text-gray-900">Trạng thái giới thiệu</h4> + <div class="mt-1 flex items-center"> + <span class="mr-2 font-medium">Mức điểm dự đoán:</span> + <div class="flex items-center"> + <span class="text-md font-bold text-blue-800">0.78</span> + <div class="ml-2 h-2 w-24 bg-gray-200 rounded-full overflow-hidden"> + <div class="h-full bg-blue-600" style="width: 78%"></div> + </div> + </div> + <span class="ml-4 px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800"> + Nên giới thiệu + </span> + </div> + </div> + </div> + </div> + + <!-- Lịch sử giới thiệu --> + <h4 class="text-md font-medium text-gray-900 mb-4">Lịch sử giới thiệu</h4> + + <div class="overflow-x-auto"> + <table class="min-w-full divide-y divide-gray-200"> + <thead class="bg-gray-50"> + <tr> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> + Ngày giới thiệu + </th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> + Bác sĩ + </th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> + Phòng ban + </th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> + Mức độ + </th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> + Trạng thái + </th> + <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"> + Thao tác + </th> + </tr> + </thead> + <tbody class="bg-white divide-y divide-gray-200"> + <!-- Hiển thị dữ liệu từ CSDL --> + {% for referral in referrals|default([]) %} + <tr class="hover:bg-gray-50"> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + {{ referral.created_at.strftime('%d/%m/%Y') }} + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + {{ referral.doctor_name }} + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + {{ referral.department }} + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + {{ referral.urgency }} + </td> + <td class="px-6 py-4 whitespace-nowrap"> + <span class="px-2 py-1 text-xs font-medium rounded-full + {% if referral.status == 'Đã chấp nhận' %} + bg-green-100 text-green-800 + {% elif referral.status == 'Đang xử lý' %} + bg-yellow-100 text-yellow-800 + {% else %} + bg-gray-100 text-gray-800 + {% endif %}"> + {{ referral.status }} + </span> + </td> + <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> + <button class="text-primary-600 hover:text-primary-900"> + <i class="fas fa-eye mr-1"></i> Xem + </button> + </td> + </tr> + {% else %} + <!-- Dữ liệu mẫu --> + <tr class="hover:bg-gray-50"> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + 15/05/2023 + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + Bs. Nguyễn Văn A + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + Tim mạch + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + Khẩn cấp + </td> + <td class="px-6 py-4 whitespace-nowrap"> + <span class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800"> + Đã chấp nhận + </span> + </td> + <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> + <button class="text-primary-600 hover:text-primary-900"> + <i class="fas fa-eye mr-1"></i> Xem + </button> + </td> + </tr> + <tr class="hover:bg-gray-50"> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + 10/06/2023 + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + Bs. Trần Thị B + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + Thần kinh + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + Thường quy + </td> + <td class="px-6 py-4 whitespace-nowrap"> + <span class="px-2 py-1 text-xs font-medium rounded-full bg-yellow-100 text-yellow-800"> + Đang xử lý + </span> + </td> + <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> + <button class="text-primary-600 hover:text-primary-900"> + <i class="fas fa-eye mr-1"></i> Xem + </button> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> + </div> + <!-- Tabs khác sẽ được thêm vào trong các phần tiếp theo --> </div> </div> @@ -340,29 +621,46 @@ const tabLinks = document.querySelectorAll('.tab-link'); const tabPanes = document.querySelectorAll('.tab-pane'); - // Hiển thị tab mặc định - showTab('overview'); + // Lấy tab từ hash URL hoặc hiển thị tab mặc định + const hash = window.location.hash.substring(1); + showTab(hash || 'overview'); + + // Cập nhật các tab link dựa vào tab hiện tại + updateTabLinks(hash || 'overview'); tabLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); - // Xóa lớp active từ tất cả các tab - tabLinks.forEach(el => { - el.classList.remove('border-primary-500', 'text-primary-600'); - el.classList.add('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300'); - }); + // Lấy target tab từ thuộc tính data-target + const target = link.getAttribute('data-target'); - // Thêm lớp active cho tab được chọn - link.classList.remove('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300'); - link.classList.add('border-primary-500', 'text-primary-600'); + // Cập nhật URL hash + window.location.hash = target; - // Hiển thị nội dung tab - const target = link.getAttribute('data-target'); + // Cập nhật các tab link + updateTabLinks(target); + + // Hiển thị tab được chọn showTab(target); }); }); + function updateTabLinks(activeTabId) { + // Xóa lớp active từ tất cả các tab + tabLinks.forEach(el => { + el.classList.remove('border-primary-500', 'text-primary-600'); + el.classList.add('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300'); + }); + + // Thêm lớp active cho tab được chọn + const activeLink = document.querySelector(`.tab-link[data-target="${activeTabId}"]`); + if (activeLink) { + activeLink.classList.remove('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300'); + activeLink.classList.add('border-primary-500', 'text-primary-600'); + } + } + function showTab(tabId) { // Ẩn tất cả các tab tabPanes.forEach(pane => { @@ -373,6 +671,16 @@ const activePane = document.getElementById(tabId); if (activePane) { activePane.style.display = 'block'; + + // Thêm hiệu ứng fade-in + activePane.classList.add('fade-in'); + + // Animation cho các phần tử trong tab + const animatedElements = activePane.querySelectorAll('.animate-on-tab-show'); + animatedElements.forEach((el, index) => { + el.style.animationDelay = `${index * 0.1}s`; + el.classList.add('animate-slide-in-bottom'); + }); } } }); diff --git a/migrations/versions/95cf7b8894bb_add_discharge_date_and_admission_date_.py b/migrations/versions/95cf7b8894bb_add_discharge_date_and_admission_date_.py deleted file mode 100644 index 58ab222d5879483e255b8faf72604fa3531446bf..0000000000000000000000000000000000000000 --- a/migrations/versions/95cf7b8894bb_add_discharge_date_and_admission_date_.py +++ /dev/null @@ -1,274 +0,0 @@ -"""Add discharge_date and admission_date to Patient model - -Revision ID: 95cf7b8894bb -Revises: -Create Date: 2025-04-09 22:52:07.294447 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = '95cf7b8894bb' -down_revision = None -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.drop_index('idx_patientID') - batch_op.drop_constraint('encounters_ibfk_2', type_='foreignkey') - batch_op.drop_constraint('encounters_ibfk_1', type_='foreignkey') - batch_op.create_foreign_key(None, 'patients', ['patientID'], ['patientID']) - batch_op.create_foreign_key(None, 'dietitians', ['dietitianID'], ['dietitianID']) - - with op.batch_alter_table('patients', schema=None) as batch_op: - batch_op.add_column(sa.Column('admission_date', sa.DateTime(), nullable=True)) - batch_op.add_column(sa.Column('discharge_date', sa.DateTime(), nullable=True)) - batch_op.alter_column('bmi', - existing_type=mysql.DECIMAL(precision=5, scale=2), - type_=sa.Float(), - existing_nullable=True) - - with op.batch_alter_table('physiologicalmeasurements', schema=None) as batch_op: - batch_op.alter_column('end_tidal_co2', - existing_type=mysql.DECIMAL(precision=5, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('feed_vol', - existing_type=mysql.DECIMAL(precision=7, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('feed_vol_adm', - existing_type=mysql.DECIMAL(precision=7, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('fio2', - existing_type=mysql.DECIMAL(precision=5, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('fio2_ratio', - existing_type=mysql.DECIMAL(precision=7, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('insp_time', - existing_type=mysql.DECIMAL(precision=4, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('oxygen_flow_rate', - existing_type=mysql.DECIMAL(precision=5, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('peep', - existing_type=mysql.DECIMAL(precision=5, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('pip', - existing_type=mysql.DECIMAL(precision=5, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('resp_rate', - existing_type=mysql.DECIMAL(precision=5, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('sip', - existing_type=mysql.DECIMAL(precision=5, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('tidal_vol', - existing_type=mysql.DECIMAL(precision=7, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('tidal_vol_actual', - existing_type=mysql.DECIMAL(precision=7, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('tidal_vol_kg', - existing_type=mysql.DECIMAL(precision=5, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.alter_column('tidal_vol_spon', - existing_type=mysql.DECIMAL(precision=7, scale=2), - type_=sa.Float(), - existing_nullable=True) - batch_op.drop_index('idx_encounterId') - batch_op.drop_index('idx_measurementDateTime') - batch_op.drop_constraint('physiologicalmeasurements_ibfk_1', type_='foreignkey') - batch_op.create_foreign_key(None, 'encounters', ['encounterId'], ['encounterId']) - - with op.batch_alter_table('procedures', schema=None) as batch_op: - batch_op.drop_index('idx_encounterId') - batch_op.drop_constraint('procedures_ibfk_1', type_='foreignkey') - batch_op.create_foreign_key(None, 'encounters', ['encounterId'], ['encounterId']) - - with op.batch_alter_table('referrals', schema=None) as batch_op: - batch_op.drop_index('idx_createdAt') - batch_op.drop_index('idx_dietitianID') - batch_op.drop_index('idx_encounterId') - batch_op.drop_index('idx_referralRequestedDateTime') - batch_op.drop_index('idx_referral_status') - batch_op.drop_constraint('referrals_ibfk_1', type_='foreignkey') - batch_op.drop_constraint('referrals_ibfk_2', type_='foreignkey') - batch_op.create_foreign_key(None, 'dietitians', ['dietitianID'], ['dietitianID']) - batch_op.create_foreign_key(None, 'encounters', ['encounterId'], ['encounterId']) - - with op.batch_alter_table('reports', schema=None) as batch_op: - batch_op.drop_index('idx_userID') - batch_op.drop_constraint('reports_ibfk_1', type_='foreignkey') - batch_op.create_foreign_key(None, 'users', ['userID'], ['userID']) - - with op.batch_alter_table('uploadedfiles', schema=None) as batch_op: - batch_op.add_column(sa.Column('original_filename', sa.String(length=256), nullable=False)) - batch_op.add_column(sa.Column('file_size', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('file_type', sa.String(length=64), nullable=True)) - batch_op.add_column(sa.Column('delimiter', sa.String(length=10), nullable=True)) - batch_op.add_column(sa.Column('encoding', sa.String(length=20), nullable=True)) - batch_op.add_column(sa.Column('status', sa.String(length=20), nullable=True)) - batch_op.add_column(sa.Column('process_start', sa.DateTime(), nullable=True)) - batch_op.add_column(sa.Column('process_end', sa.DateTime(), nullable=True)) - batch_op.add_column(sa.Column('total_records', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('processed_records', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('error_records', sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column('error_details', sa.Text(), nullable=True)) - batch_op.add_column(sa.Column('process_referrals', sa.Boolean(), nullable=True)) - batch_op.add_column(sa.Column('description', sa.Text(), nullable=True)) - batch_op.add_column(sa.Column('notes', sa.Text(), nullable=True)) - batch_op.add_column(sa.Column('created_at', sa.DateTime(), nullable=True)) - batch_op.add_column(sa.Column('updated_at', sa.DateTime(), nullable=True)) - batch_op.drop_index('idx_userID') - batch_op.drop_constraint('uploadedfiles_ibfk_1', type_='foreignkey') - batch_op.create_foreign_key(None, 'users', ['userID'], ['userID']) - - # ### 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.drop_constraint(None, type_='foreignkey') - batch_op.create_foreign_key('uploadedfiles_ibfk_1', 'users', ['userID'], ['userID'], ondelete='CASCADE') - batch_op.create_index('idx_userID', ['userID'], unique=False) - batch_op.drop_column('updated_at') - batch_op.drop_column('created_at') - batch_op.drop_column('notes') - batch_op.drop_column('description') - batch_op.drop_column('process_referrals') - batch_op.drop_column('error_details') - batch_op.drop_column('error_records') - batch_op.drop_column('processed_records') - batch_op.drop_column('total_records') - batch_op.drop_column('process_end') - batch_op.drop_column('process_start') - batch_op.drop_column('status') - batch_op.drop_column('encoding') - batch_op.drop_column('delimiter') - batch_op.drop_column('file_type') - batch_op.drop_column('file_size') - batch_op.drop_column('original_filename') - - with op.batch_alter_table('reports', schema=None) as batch_op: - batch_op.drop_constraint(None, type_='foreignkey') - batch_op.create_foreign_key('reports_ibfk_1', 'users', ['userID'], ['userID'], ondelete='CASCADE') - batch_op.create_index('idx_userID', ['userID'], unique=False) - - with op.batch_alter_table('referrals', schema=None) as batch_op: - batch_op.drop_constraint(None, type_='foreignkey') - batch_op.drop_constraint(None, type_='foreignkey') - batch_op.create_foreign_key('referrals_ibfk_2', 'dietitians', ['dietitianID'], ['dietitianID'], ondelete='SET NULL') - batch_op.create_foreign_key('referrals_ibfk_1', 'encounters', ['encounterId'], ['encounterId'], ondelete='CASCADE') - batch_op.create_index('idx_referral_status', ['referral_status'], unique=False) - batch_op.create_index('idx_referralRequestedDateTime', ['referralRequestedDateTime'], unique=False) - batch_op.create_index('idx_encounterId', ['encounterId'], unique=False) - batch_op.create_index('idx_dietitianID', ['dietitianID'], unique=False) - batch_op.create_index('idx_createdAt', ['createdAt'], unique=False) - - with op.batch_alter_table('procedures', schema=None) as batch_op: - batch_op.drop_constraint(None, type_='foreignkey') - batch_op.create_foreign_key('procedures_ibfk_1', 'encounters', ['encounterId'], ['encounterId'], ondelete='CASCADE') - batch_op.create_index('idx_encounterId', ['encounterId'], unique=False) - - with op.batch_alter_table('physiologicalmeasurements', schema=None) as batch_op: - batch_op.drop_constraint(None, type_='foreignkey') - batch_op.create_foreign_key('physiologicalmeasurements_ibfk_1', 'encounters', ['encounterId'], ['encounterId'], ondelete='CASCADE') - batch_op.create_index('idx_measurementDateTime', ['measurementDateTime'], unique=False) - batch_op.create_index('idx_encounterId', ['encounterId'], unique=False) - batch_op.alter_column('tidal_vol_spon', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=7, scale=2), - existing_nullable=True) - batch_op.alter_column('tidal_vol_kg', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=5, scale=2), - existing_nullable=True) - batch_op.alter_column('tidal_vol_actual', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=7, scale=2), - existing_nullable=True) - batch_op.alter_column('tidal_vol', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=7, scale=2), - existing_nullable=True) - batch_op.alter_column('sip', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=5, scale=2), - existing_nullable=True) - batch_op.alter_column('resp_rate', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=5, scale=2), - existing_nullable=True) - batch_op.alter_column('pip', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=5, scale=2), - existing_nullable=True) - batch_op.alter_column('peep', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=5, scale=2), - existing_nullable=True) - batch_op.alter_column('oxygen_flow_rate', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=5, scale=2), - existing_nullable=True) - batch_op.alter_column('insp_time', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=4, scale=2), - existing_nullable=True) - batch_op.alter_column('fio2_ratio', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=7, scale=2), - existing_nullable=True) - batch_op.alter_column('fio2', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=5, scale=2), - existing_nullable=True) - batch_op.alter_column('feed_vol_adm', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=7, scale=2), - existing_nullable=True) - batch_op.alter_column('feed_vol', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=7, scale=2), - existing_nullable=True) - batch_op.alter_column('end_tidal_co2', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=5, scale=2), - existing_nullable=True) - - with op.batch_alter_table('patients', schema=None) as batch_op: - batch_op.alter_column('bmi', - existing_type=sa.Float(), - type_=mysql.DECIMAL(precision=5, scale=2), - existing_nullable=True) - batch_op.drop_column('discharge_date') - batch_op.drop_column('admission_date') - - with op.batch_alter_table('encounters', schema=None) as batch_op: - batch_op.drop_constraint(None, type_='foreignkey') - batch_op.drop_constraint(None, type_='foreignkey') - batch_op.create_foreign_key('encounters_ibfk_1', 'patients', ['patientID'], ['patientID'], ondelete='CASCADE') - batch_op.create_foreign_key('encounters_ibfk_2', 'dietitians', ['dietitianID'], ['dietitianID'], ondelete='SET NULL') - batch_op.create_index('idx_patientID', ['patientID'], unique=False) - - # ### end Alembic commands ### diff --git a/migrations/versions/add_timestamps_and_referral_score.py b/migrations/versions/add_timestamps_and_referral_score.py new file mode 100644 index 0000000000000000000000000000000000000000..54a9d9b6cb9ac16ddbeeb214df46ef1145ef0bb4 --- /dev/null +++ b/migrations/versions/add_timestamps_and_referral_score.py @@ -0,0 +1,62 @@ +"""Add timestamps and referral_score fields + +Migration tùy chỉnh để thêm các trường timestamps vào các bảng quan trọng +và thêm trường referral_score vào bảng physiologicalmeasurements. + +Revision ID: add_timestamps_referral_score +Revises: +Create Date: 2024-06-15 10:00:00 +""" + +from alembic import op +import sqlalchemy as sa +from datetime import datetime + +# revision identifiers, used by Alembic +revision = 'add_timestamps_referral_score' +down_revision = None # Điều này sẽ được tự động điều chỉnh bởi Flask-Migrate +branch_labels = None +depends_on = None + +def upgrade(): + # Thêm trường referral_score vào bảng physiologicalmeasurements + op.add_column('physiologicalmeasurements', sa.Column('referral_score', sa.Float(), nullable=True, + comment='Score from ML algorithm indicating referral recommendation (0-1)')) + + # Thêm timestamps vào bảng physiologicalmeasurements nếu chưa có + op.add_column('physiologicalmeasurements', sa.Column('created_at', sa.DateTime(), nullable=True, + server_default=sa.text('CURRENT_TIMESTAMP'))) + op.add_column('physiologicalmeasurements', sa.Column('updated_at', sa.DateTime(), nullable=True, + server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))) + + # Thêm trường updated_at vào bảng patients nếu chưa có + op.add_column('patients', sa.Column('updated_at', sa.DateTime(), nullable=True, + server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))) + + # Thêm timestamps vào bảng procedures + op.add_column('procedures', sa.Column('created_at', sa.DateTime(), nullable=True, + server_default=sa.text('CURRENT_TIMESTAMP'))) + op.add_column('procedures', sa.Column('updated_at', sa.DateTime(), nullable=True, + server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))) + + # Thêm trường updated_at vào bảng reports + op.add_column('reports', sa.Column('updated_at', sa.DateTime(), nullable=True, + server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'))) + +def downgrade(): + # Xóa trường referral_score khỏi bảng physiologicalmeasurements + op.drop_column('physiologicalmeasurements', 'referral_score') + + # Xóa timestamps khỏi bảng physiologicalmeasurements + op.drop_column('physiologicalmeasurements', 'created_at') + op.drop_column('physiologicalmeasurements', 'updated_at') + + # Xóa trường updated_at khỏi bảng patients + op.drop_column('patients', 'updated_at') + + # Xóa timestamps khỏi bảng procedures + op.drop_column('procedures', 'created_at') + op.drop_column('procedures', 'updated_at') + + # Xóa trường updated_at khỏi bảng reports + op.drop_column('reports', 'updated_at') \ No newline at end of file diff --git a/run.py b/run.py index ad9bc5010d811626ff1f286ce6f59ce7af666053..17d1fc9659be287cabc5bad0cee1e2d665fa40a5 100644 --- a/run.py +++ b/run.py @@ -2,12 +2,14 @@ # -*- coding: utf-8 -*- import os +import sys import shutil import argparse import mysql.connector from flask_bcrypt import Bcrypt from flask import Flask from datetime import datetime +from flask_migrate import Migrate, init, migrate, upgrade def clear_cache(): print("\n[Cache] Cleaning __pycache__ and .pyc files...") @@ -76,27 +78,256 @@ def init_tables_and_admin(app, db): print(f"[Error] Init failed: {e}") return False +def run_migrations(app, db): + """ + Thực hiện migration để cập nhật cấu trúc cơ sở dữ liệu + """ + try: + print("Khởi tạo Migration...") + migrations_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'migrations') + migrate_instance = Migrate(app, db, directory=migrations_dir) + + with app.app_context(): + # Kiểm tra xem thư mục migrations đã tồn tại chưa + if not os.path.exists(migrations_dir): + print(f"Khởi tạo thư mục migrations tại {migrations_dir}...") + init(directory=migrations_dir) + + print("Tạo migration mới dựa trên model hiện tại...") + migrate(message="Update database schema to match models", directory=migrations_dir) + + print("Áp dụng migrations...") + upgrade(directory=migrations_dir) + + print("Migration hoàn tất!") + + return True + except Exception as e: + print(f"Lỗi khi thực hiện migration: {str(e)}") + return False + +def check_database_structure(app): + """ + Kiểm tra cấu trúc cơ sở dữ liệu hiện tại + """ + try: + # Đọc thông tin kết nối từ cấu hình + db_uri = app.config['SQLALCHEMY_DATABASE_URI'] + + if not db_uri.startswith('mysql'): + print("Không phải cấu hình MySQL, không thể kiểm tra cấu trúc.") + return False + + # Phân tích chuỗi kết nối + db_parts = db_uri.replace('mysql://', '').split('@') + auth_parts = db_parts[0].split(':') + host_parts = db_parts[1].split('/') + + username = auth_parts[0] + password = auth_parts[1] if len(auth_parts) > 1 else '' + host = host_parts[0] + database = host_parts[1] + + print(f"Đang kết nối đến MySQL với người dùng '{username}' trên máy chủ '{host}'") + + # Kết nối đến MySQL + connection = mysql.connector.connect( + host=host, + user=username, + password=password, + database=database + ) + + cursor = connection.cursor() + + # Lấy danh sách tất cả các bảng + cursor.execute("SHOW TABLES") + tables = cursor.fetchall() + + print("\n=== DANH SÁCH BẢNG TRONG CƠ SỞ DỮ LIỆU ===") + for (table_name,) in tables: + print(f"\n--- Bảng: {table_name} ---") + + # Lấy cấu trúc của bảng + cursor.execute(f"DESCRIBE {table_name}") + columns = cursor.fetchall() + + # Hiển thị thông tin cột + print("Tên cột\t\tLoại dữ liệu\tCó thể NULL\tKhóa\tMặc định\tThông tin khác") + print("-" * 100) + for column in columns: + field = column[0] + type = column[1] + null = column[2] + key = column[3] + default = column[4] + extra = column[5] + print(f"{field: <16} {type: <16} {null: <16} {key: <8} {default if default else 'NULL': <16} {extra}") + + # Đóng kết nối + cursor.close() + connection.close() + + return True + + except Exception as e: + print(f"Lỗi khi kiểm tra cấu trúc cơ sở dữ liệu: {str(e)}") + return False + +def update_database_schema(app, db): + """ + Cập nhật cấu trúc cơ sở dữ liệu trực tiếp từ models + Cách này nhanh hơn so với migration nhưng chỉ nên dùng trong môi trường phát triển + """ + try: + # Đọc thông tin kết nối từ cấu hình + db_uri = app.config['SQLALCHEMY_DATABASE_URI'] + + if not db_uri.startswith('mysql'): + print("Không phải cấu hình MySQL, không thể cập nhật cấu trúc.") + return False + + # Phân tích chuỗi kết nối + db_parts = db_uri.replace('mysql://', '').split('@') + auth_parts = db_parts[0].split(':') + host_parts = db_parts[1].split('/') + + username = auth_parts[0] + password = auth_parts[1] if len(auth_parts) > 1 else '' + host = host_parts[0] + database = host_parts[1] + + print(f"Đang kết nối đến MySQL với người dùng '{username}' trên máy chủ '{host}'") + + # Kết nối đến MySQL + connection = mysql.connector.connect( + host=host, + user=username, + password=password, + database=database + ) + + cursor = connection.cursor() + + # Import tất cả model để đảm bảo chúng được khai báo + from app.models.patient import Patient, Encounter + from app.models.user import User, Dietitian + from app.models.measurement import PhysiologicalMeasurement + from app.models.procedure import Procedure + from app.models.referral import Referral + from app.models.report import Report + from app.models.uploaded_file import UploadedFile + + with app.app_context(): + # Thực hiện các câu lệnh cập nhật + for table, statements in schema_updates.items(): + print(f"\nĐang cập nhật bảng {table}...") + + # Kiểm tra xem bảng có tồn tại không + cursor.execute(f"SHOW TABLES LIKE '{table}'") + table_exists = cursor.fetchone() + + if not table_exists: + print(f" - Bảng {table} không tồn tại, sẽ được tạo khi chạy db.create_all()") + continue + + # Lấy danh sách các cột hiện tại của bảng + cursor.execute(f"SHOW COLUMNS FROM {table}") + existing_columns = [column[0] for column in cursor.fetchall()] + + # Thực hiện các câu lệnh ALTER TABLE + for statement in statements: + try: + # Phân tích cú pháp để lấy tên cột + column_name = statement.split("ADD COLUMN")[1].strip().split(" ")[0] + + # Kiểm tra xem cột đã tồn tại chưa + if column_name in existing_columns: + print(f" - Bỏ qua: Cột {column_name} đã tồn tại trong bảng {table}") + continue + + cursor.execute(statement) + print(f" - Thành công: {statement}") + except Exception as e: + print(f" - Lỗi: {statement}") + print(f" {str(e)}") + + # Commit các thay đổi + connection.commit() + + # Tạo bảng mới nếu cần + print("\nĐang tạo các bảng mới (nếu có)...") + db.create_all() + print("Đã tạo các bảng mới (nếu có)") + + # Đóng kết nối + cursor.close() + connection.close() + + print("\nHoàn tất cập nhật cấu trúc cơ sở dữ liệu!") + return True + + except Exception as e: + print(f"Lỗi khi cập nhật cấu trúc cơ sở dữ liệu: {str(e)}") + return False + def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--init', action='store_true') - parser.add_argument('--clean', action='store_true') - parser.add_argument('--admin', action='store_true') - parser.add_argument('--port', type=int, default=5000) + # Phân tích tham số dòng lệnh + parser = argparse.ArgumentParser(description='CCU HTM Management System') + parser.add_argument('--init', action='store_true', help='Khởi tạo cơ sở dữ liệu') + parser.add_argument('--clean', action='store_true', help='Xóa cache trước khi chạy') + parser.add_argument('--admin', action='store_true', help='Tạo tài khoản admin') + parser.add_argument('--port', type=int, default=5000, help='Port để chạy ứng dụng') + parser.add_argument('--migrate', action='store_true', help='Thực hiện migration cơ sở dữ liệu') + parser.add_argument('--check-db', action='store_true', help='Kiểm tra cấu trúc cơ sở dữ liệu') + parser.add_argument('--update-db', action='store_true', help='Cập nhật cấu trúc cơ sở dữ liệu trực tiếp từ models') args = parser.parse_args() + # Import modules from app import create_app, db - app = create_app() + # Tạo ứng dụng + app = create_app() + + # Xóa cache nếu yêu cầu if args.clean: clear_cache() + + # Khởi tạo cơ sở dữ liệu nếu yêu cầu if args.init: + print("Bắt đầu khởi tạo cơ sở dữ liệu...") if create_mysql_database(app): - init_tables_and_admin(app, db) + if init_tables_and_admin(app, db): + print("Khởi tạo cơ sở dữ liệu hoàn tất!") + else: + print("Lỗi khi khởi tạo bảng và tài khoản admin.") + else: + print("Lỗi khi tạo cơ sở dữ liệu MySQL.") + + # Thực hiện migration nếu yêu cầu + if args.migrate: + print("Bắt đầu thực hiện migration...") + run_migrations(app, db) + + # Kiểm tra cấu trúc cơ sở dữ liệu nếu yêu cầu + if args.check_db: + print("Đang kiểm tra cấu trúc cơ sở dữ liệu...") + check_database_structure(app) + + # Cập nhật cấu trúc cơ sở dữ liệu trực tiếp nếu yêu cầu + if args.update_db: + print("Bắt đầu cập nhật cấu trúc cơ sở dữ liệu...") + update_database_schema(app, db) + + # Tạo tài khoản admin nếu yêu cầu if args.admin: - init_tables_and_admin(app, db) - - if not (args.init or args.admin): - print(f"[Run] Starting Flask app on port {args.port}...") + print("Bắt đầu tạo tài khoản admin...") + create_admin_user_direct() + + # Chạy ứng dụng nếu không có tham số nào khác hoặc chỉ có flag --clean + is_only_clean = args.clean and not any([args.init, args.admin, args.migrate, args.check_db, args.update_db]) + if not any([args.init, args.admin, args.migrate, args.check_db, args.update_db]) or is_only_clean: + print(f"Khởi chạy CCU HTM trên port {args.port}...") app.run(host='0.0.0.0', port=args.port, debug=True) if __name__ == '__main__': diff --git a/run_ccu.bat b/run_ccu.bat index e78ebac95421d226eb8005aca60c01033933e8fc..af81c1a9ab8cbc8663fab2d52eb872bfb87293ed 100644 --- a/run_ccu.bat +++ b/run_ccu.bat @@ -4,52 +4,55 @@ echo ------------------------- :menu cls -echo Pick an option: -echo 1. Database inittialization (for 1st time) -echo 2. App run -echo 3. Run app and clear cache -echo 4. Admin acc creation +echo Select a function: +echo 1. Initialize the database (includes migration) +echo 2. Run the application +echo 3. Create admin account +echo 4. Update database schema echo 5. Exit echo. set /p choice=Your choice (1-5): if "%choice%"=="1" ( - echo Data initializing... - python run.py --init + echo Initializing the database and performing migration... + python run.py --init --migrate echo. - echo Press any key to back to menu... + echo Press any key to return to the menu... pause >nul goto menu ) if "%choice%"=="2" ( - echo Running app... - python run.py - goto end -) -if "%choice%"=="3" ( - echo Deleting cache and running app... + echo Starting the application... python run.py --clean goto end ) -if "%choice%"=="4" ( +if "%choice%"=="3" ( echo Creating admin account... python run.py --admin echo. - echo Press any key to back to menu... + echo Press any key to return to the menu... + pause >nul + goto menu +) +if "%choice%"=="4" ( + echo Updating database schema... + python run.py --check-db --update-db + echo. + echo Press any key to return to the menu... pause >nul goto menu ) if "%choice%"=="5" ( goto end ) else ( - echo Choice invalid! + echo Invalid choice! echo. - echo Press any key to back to menu... + echo Press any key to try again... pause >nul goto menu ) :end -echo Thanks for using CCU HTM Management System! -exit \ No newline at end of file +echo Thank you for using the CCU HTM Management System! +exit \ No newline at end of file