diff --git a/.gitignore b/.gitignore index b1766a09d531676fa9226c667ed403e1d6bafb69..c34cb7a092fa70a4ad3728c46a35ad4ecc173e33 100644 --- a/.gitignore +++ b/.gitignore @@ -56,7 +56,6 @@ local_settings.py db.sqlite3 # Flask stuff: -instance/ .webassets-cache # Scrapy stuff: @@ -124,9 +123,6 @@ dmypy.json out/ .idea_modules/ -# Tệp cấu hình cá nhân -instance/config.py - # Tệp tạm thời *.swp *~ diff --git a/app/__init__.py b/app/__init__.py index 929e985f92273c9a02bd8e30f349ed99f24c5ef0..397c934f5ef4e0f37fdcb51cd3a35c44cd2890d3 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -30,7 +30,6 @@ def create_app(config_class=None): # Có thể bỏ config_class hoặc dùng ch # --- Quan trọng: Đảm bảo các cấu hình cần thiết được thiết lập --- # Nếu không có trong file config.py, cần có giá trị mặc định ở đây app.config.setdefault('SECRET_KEY', 'a_default_secret_key_change_me') - # Sử dụng giá trị mặc định giống trong instance/config.py hoặc một placeholder an toàn hơn app.config.setdefault('SQLALCHEMY_DATABASE_URI', 'mysql://root:MinhDZ3009@localhost/ccu') app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', False) app.config.setdefault('UPLOAD_FOLDER', os.path.join(app.instance_path, 'uploads')) diff --git a/app/models/uploaded_file.py b/app/models/uploaded_file.py index 2230d4390dcf4087b062ab8998dc51eb5dbd7787..11cb090d31a27797c688e2a0fcd41f166f74cbdc 100644 --- a/app/models/uploaded_file.py +++ b/app/models/uploaded_file.py @@ -18,7 +18,7 @@ class UploadedFile(db.Model): file_encoding = db.Column(db.String(20), default='utf-8') # Status tracking - status = db.Column(db.String(20), default='pending') + status = db.Column(db.String(50), default='pending') process_start = db.Column(db.DateTime) process_end = db.Column(db.DateTime) @@ -26,7 +26,7 @@ class UploadedFile(db.Model): total_records = db.Column(db.Integer, default=0) processed_records = db.Column(db.Integer, default=0) error_records = db.Column(db.Integer, default=0) - error_details = db.Column(db.Text) + error_details = db.Column(db.Text(length=16777215)) process_referrals = db.Column(db.Boolean, default=False) description = db.Column(db.Text) diff --git a/app/routes/patients.py b/app/routes/patients.py index 429cc4d0d62112d6175041d2c29323b5d9786bb7..ffedbe5d48b10fd9f53fd1dacad51ff7854edccb 100644 --- a/app/routes/patients.py +++ b/app/routes/patients.py @@ -8,6 +8,7 @@ from app.models.referral import Referral from app.models.procedure import Procedure from app.models.report import Report from sqlalchemy import desc, or_, func +from sqlalchemy.orm import joinedload from datetime import datetime from app import csrf @@ -118,8 +119,10 @@ def patient_detail(patient_id): desc(Report.report_date) ).all() - # Get patient's encounters - encounters = Encounter.query.filter_by(patientID=patient_id).order_by( + # Get patient's encounters with dietitian info preloaded + encounters = Encounter.query.filter_by(patientID=patient_id).options( + joinedload(Encounter.dietitian) + ).order_by( desc(Encounter.admissionDateTime) ).all() diff --git a/app/routes/upload.py b/app/routes/upload.py index 6eaaad182e23aa21922956e8a055d5f4cb9e8465..517e0cb3cec535849ebf7d1015c2ae54c8183594 100644 --- a/app/routes/upload.py +++ b/app/routes/upload.py @@ -1,21 +1,29 @@ import os import csv import io -import pandas as pd -from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, send_from_directory +import json +import uuid +from datetime import datetime +from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, send_from_directory, session from flask_login import login_required, current_user from werkzeug.utils import secure_filename +from werkzeug.datastructures import FileStorage from app import db from app.models.uploaded_file import UploadedFile from app.models.patient import Patient, Encounter from app.models.measurement import PhysiologicalMeasurement from app.models.referral import Referral -from app.utils.csv_handler import process_csv, validate_csv -from datetime import datetime +from app.utils.csv_handler import process_csv from sqlalchemy import desc +from flask_wtf import FlaskForm +from flask_wtf.csrf import generate_csrf, validate_csrf upload_bp = Blueprint('upload', __name__, url_prefix='/upload') +# Tạo form rỗng để chứa CSRF token +class UploadForm(FlaskForm): + pass + def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ['csv'] @@ -23,134 +31,160 @@ def allowed_file(filename): @upload_bp.route('/', methods=['GET', 'POST']) @login_required def index(): + form = UploadForm() + + # Lấy 3 bản ghi upload gần nhất để hiển thị ở sidebar + recent_uploads = UploadedFile.query.order_by(UploadedFile.upload_date.desc()).limit(3).all() + if request.method == 'POST': - # Check if the post request has the file part + # Kiểm tra CSRF token + if not form.validate(): + flash('CSRF token không hợp lệ. Vui lòng thử lại.', 'error') + return redirect(url_for('upload.index')) + + # Kiểm tra xem có file được gửi lên hay không if 'file' not in request.files: - flash('No file part', 'error') + flash('Không có file nào được chọn', 'error') return redirect(request.url) file = request.files['file'] - # If the user does not select a file, the browser submits an empty file + # Nếu người dùng không chọn file if file.filename == '': - flash('No selected file', 'error') + flash('Không có file nào được chọn', 'error') return redirect(request.url) - if file and allowed_file(file.filename): - # Secure filename to prevent security issues + # Nếu file tồn tại và có đuôi là csv + if file and file.filename.lower().endswith('.csv'): + # Lưu file vào thư mục upload filename = secure_filename(file.filename) + unique_filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex}_{filename}" - # Create a unique filename - unique_filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{filename}" - - # Ensure upload folder exists - upload_folder = current_app.config['UPLOAD_FOLDER'] + # Tạo thư mục upload nếu chưa tồn tại + upload_folder = os.path.join(current_app.root_path, '..', 'uploads') os.makedirs(upload_folder, exist_ok=True) - # Save file to the upload folder file_path = os.path.join(upload_folder, unique_filename) file.save(file_path) - # Get file size - file_size = os.path.getsize(file_path) + # Lấy thông tin từ form + delimiter = request.form.get('delimiter', 'comma') + encoding = request.form.get('encoding', 'utf-8') + description = request.form.get('description', '') + process_referrals = 'process_referrals' in request.form - # Get form parameters - delimiter = { + delimiter_map = { 'comma': ',', 'semicolon': ';', 'tab': '\t' - }.get(request.form.get('delimiter', 'comma'), ',') + } - file_encoding = request.form.get('encoding', 'utf-8') - description = request.form.get('description', '') - process_referrals = True if request.form.get('process_referrals') else False + actual_delimiter = delimiter_map.get(delimiter, ',') - # Create uploaded file record - uploaded_file = UploadedFile( - filename=unique_filename, + # Tạo bản ghi lưu thông tin upload + upload_record = UploadedFile( original_filename=filename, - file_path=file_path, - file_size=file_size, - uploaded_by=current_user.id, - delimiter=delimiter, - file_encoding=file_encoding, - process_referrals=process_referrals, + fileName=unique_filename, + filePath=file_path, + file_type='csv', + file_size=os.path.getsize(file_path), + file_encoding=encoding, + delimiter=actual_delimiter, description=description, - status='pending' + user_id=current_user.userID, + upload_date=datetime.now(), + status='processing' ) - db.session.add(uploaded_file) - db.session.commit() - - # Validate the CSV file format - validation_result = validate_csv(file_path, delimiter, file_encoding) - - if validation_result['valid']: - # Process the CSV in the background (in a real app, this would be a Celery/RQ task) - try: - # Update file status to processing - uploaded_file.status = 'processing' - uploaded_file.process_start = datetime.now() - db.session.commit() - - # Process the CSV file - result = process_csv(uploaded_file.id) - - # Update file status based on processing result - if result['success']: - uploaded_file.status = 'completed' - uploaded_file.total_records = result['total_records'] - uploaded_file.processed_records = result['processed_records'] - uploaded_file.error_records = result['error_records'] - uploaded_file.error_details = result.get('error_details', '') - - flash(f'File uploaded and processed successfully. {result["processed_records"]} records processed.', 'success') + try: + db.session.add(upload_record) + db.session.commit() + + # Xử lý file CSV + result = process_csv( + uploaded_file_id=upload_record.id + ) + + # Cập nhật trạng thái + upload_record.status = 'completed' if result.get('success') else 'failed' + if result.get('success'): + upload_record.processed_records = result.get('processed_records', 0) + upload_record.error_records = result.get('error_records', 0) + upload_record.total_records = result.get('total_records', 0) + if result.get('errors'): + upload_record.error_details = json.dumps(result['errors']) + else: + upload_record.error_details = json.dumps([{'row': 0, 'error': result.get('error', 'Unknown error')}]) + + db.session.commit() + + if result.get('success'): + processed_count = result.get('processed_records', 0) + error_count = result.get('error_records', 0) + if error_count > 0: + # Hiển thị thông báo lỗi dễ hiểu cho người dùng + error_details = result.get('error_details', '{}') + try: + error_json = json.loads(error_details) + if 'summary' in error_json: + # Đây là lỗi đã được tóm tắt + flash(f"{error_json.get('summary')} {error_json.get('message', '')}", 'warning') + else: + flash(f'Đã tải lên và xử lý {processed_count} bản ghi, nhưng có {error_count} lỗi. Xem chi tiết trong lịch sử tải lên.', 'warning') + except: + flash(f'Đã tải lên và xử lý {processed_count} bản ghi, nhưng có {error_count} lỗi. Xem chi tiết trong lịch sử tải lên.', 'warning') + upload_record.status = 'completed_with_errors' else: - uploaded_file.status = 'failed' - uploaded_file.error_details = result.get('error', 'Unknown error') - - flash(f'Error processing file: {result.get("error", "Unknown error")}', 'error') - - uploaded_file.process_end = datetime.now() - db.session.commit() - - except Exception as e: - uploaded_file.status = 'failed' - uploaded_file.error_details = str(e) - uploaded_file.process_end = datetime.now() - db.session.commit() - - flash(f'Error processing file: {str(e)}', 'error') - else: - uploaded_file.status = 'failed' - uploaded_file.error_details = validation_result['error'] + flash(f'Đã tải lên và xử lý thành công {processed_count} bản ghi từ {filename}.', 'success') + upload_record.status = 'completed' + else: + error_msg = result.get('error', 'Unknown processing error') + # Đơn giản hóa thông báo lỗi cho người dùng cuối + if 'Data too long' in error_msg: + user_error_message = "Dữ liệu lỗi quá lớn để lưu chi tiết. Vui lòng kiểm tra tóm tắt lỗi trong lịch sử tải lên." + # Ghi log lỗi đầy đủ cho dev + current_app.logger.error(f"Data too long error during CSV processing for file ID {upload_record.id}: {error_msg}") + elif 'already exists' in error_msg: # Có thể bắt thêm các lỗi cụ thể khác + user_error_message = "Phát hiện dữ liệu bệnh nhân trùng lặp. Xem chi tiết trong lịch sử tải lên." + else: + user_error_message = f"Xử lý file thất bại. Vui lòng kiểm tra định dạng file hoặc xem chi tiết lỗi trong lịch sử tải lên." + # Ghi log lỗi không xác định + current_app.logger.error(f"Unknown error during CSV processing for file ID {upload_record.id}: {error_msg}") + + flash(user_error_message, 'error') + upload_record.status = 'failed' + db.session.commit() - flash(f'Invalid CSV file: {validation_result["error"]}', 'error') - - return redirect(url_for('upload.history')) + return redirect(url_for('upload.history')) + + except Exception as e: + db.session.rollback() + upload_record.status = 'failed' + upload_record.error_details = json.dumps([{"row": 0, "error": str(e)}]) + db.session.add(upload_record) + db.session.commit() + + flash(f'Lỗi khi xử lý file: {str(e)}', 'error') + return redirect(url_for('upload.index')) else: - flash('File type not allowed. Please upload a CSV file.', 'error') - - # Get recent upload history for sidebar - recent_uploads = UploadedFile.query.order_by( - UploadedFile.upload_date.desc() - ).limit(5).all() + flash('File không hợp lệ. Chỉ chấp nhận file .csv', 'error') + return redirect(request.url) - return render_template('upload.html', recent_uploads=recent_uploads) + return render_template('upload.html', form=form, recent_uploads=recent_uploads) @upload_bp.route('/history') @login_required def history(): - # Get upload history with pagination page = request.args.get('page', 1, type=int) - per_page = request.args.get('per_page', 10, type=int) + per_page = 10 - uploads = UploadedFile.query.order_by( - UploadedFile.upload_date.desc() - ).paginate(page=page, per_page=per_page) + # Lấy danh sách các file đã upload, sắp xếp theo thời gian gần nhất + uploads = UploadedFile.query.order_by(UploadedFile.upload_date.desc()).paginate( + page=page, per_page=per_page, error_out=False + ) - return render_template('upload_history.html', uploads=uploads, current_page=page, per_page=per_page) + return render_template('upload_history.html', uploads=uploads) @upload_bp.route('/preview/<int:file_id>') @login_required @@ -158,14 +192,14 @@ def preview(file_id): uploaded_file = UploadedFile.query.get_or_404(file_id) # Check if file exists - if not os.path.exists(uploaded_file.file_path): + if not os.path.exists(uploaded_file.filePath): flash('File not found.', 'error') return redirect(url_for('upload.history')) try: # Read the first 10 rows of the CSV file df = pd.read_csv( - uploaded_file.file_path, + uploaded_file.filePath, delimiter=uploaded_file.delimiter, encoding=uploaded_file.file_encoding, nrows=10 @@ -189,36 +223,72 @@ def preview(file_id): def download(file_id): uploaded_file = UploadedFile.query.get_or_404(file_id) - # Check if file exists - if not os.path.exists(uploaded_file.file_path): - flash('File not found.', 'error') + # Chỉ cho phép người tải lên hoặc admin tải xuống + if uploaded_file.user_id != current_user.userID and not current_user.is_admin: + flash('Bạn không có quyền tải xuống file này', 'error') return redirect(url_for('upload.history')) - # Return file for download + # Kiểm tra xem file có tồn tại không + if not os.path.exists(uploaded_file.filePath): + flash('File không tồn tại trên hệ thống', 'error') + return redirect(url_for('upload.history')) + + # Trả về file để tải xuống return send_from_directory( - os.path.dirname(uploaded_file.file_path), - os.path.basename(uploaded_file.file_path), + os.path.dirname(uploaded_file.filePath), + os.path.basename(uploaded_file.filePath), as_attachment=True, - attachment_filename=uploaded_file.original_filename + download_name=uploaded_file.original_filename, + mimetype='text/csv' ) @upload_bp.route('/delete/<int:file_id>', methods=['POST']) @login_required def delete(file_id): + # --- DEBUG CSRF --- + current_app.logger.info(f"--- Entering delete route for file {file_id} ---") + submitted_token = request.form.get('csrf_token') + session_token = session.get('csrf_token') # Lấy token từ session (nếu có) + current_app.logger.info(f"CSRF token received in form for file {file_id}: {submitted_token}") + current_app.logger.info(f"CSRF token from session: {session_token}") + form_data = ', '.join([f"{k}={v}" for k, v in request.form.items()]) + current_app.logger.info(f"All form data: {form_data}") + # --------------------- + + # Tạo form tạm thời để xác thực CSRF token + form = UploadForm() + + # Kiểm tra CSRF token với phương thức validate() của form + if not form.validate(): + current_app.logger.warning(f"Invalid CSRF token attempt for file {file_id}") + flash('CSRF token không hợp lệ. Vui lòng thử lại.', 'error') + return redirect(url_for('upload.history')) + + current_app.logger.info(f"CSRF token validated successfully for file {file_id}.") + + # Lấy thông tin file từ database uploaded_file = UploadedFile.query.get_or_404(file_id) - # Check if user has permission (admin or owner) - if not current_user.is_admin and uploaded_file.uploaded_by != current_user.id: - flash('You do not have permission to delete this file.', 'error') + # Chỉ cho phép người tải lên hoặc admin xóa + if uploaded_file.user_id != current_user.userID and not current_user.is_admin: + flash('Bạn không có quyền xóa file này', 'error') return redirect(url_for('upload.history')) - # Delete the file if it exists - if os.path.exists(uploaded_file.file_path): - os.remove(uploaded_file.file_path) + # Kiểm tra xem file có tồn tại không và xóa + if os.path.exists(uploaded_file.filePath): + try: + os.remove(uploaded_file.filePath) + except Exception as e: + # Không báo lỗi nếu không xóa được file, vẫn xóa bản ghi + pass - # Delete the database record - db.session.delete(uploaded_file) - db.session.commit() + # Xóa bản ghi trong database + try: + db.session.delete(uploaded_file) + db.session.commit() + flash('Đã xóa file thành công', 'success') + except Exception as e: + db.session.rollback() + flash(f'Lỗi khi xóa file: {str(e)}', 'error') - flash('File has been deleted.', 'success') return redirect(url_for('upload.history')) diff --git a/app/templates/base.html b/app/templates/base.html index f27b1f0497fa2af8682d336df1994b1aa8c3be40..0d607c9d774f7abd91092dbe0f92c44f1152a7de 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -382,10 +382,10 @@ </a> <a href="{{ url_for('auth.logout') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem"> <i class="fas fa-sign-out-alt mr-2"></i> Đăng xuất - </a> - </div> - </div> - </div> + </a> + </div> + </div> + </div> </div> </div> </div> @@ -397,13 +397,13 @@ <!-- Page Header --> <div class="page-header mb-8"> <h1 class="text-3xl font-bold text-gray-800 animate-fade-in">{% block header %}Dashboard{% endblock %}</h1> - </div> - + </div> + <!-- Flash Messages --> - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} <div class="mb-6 animate-slide-in-top"> - {% for category, message in messages %} + {% for category, message in messages %} <div class="alert-message py-3 px-4 mb-2 rounded-md flex items-center justify-between {% if category == 'error' %}bg-red-100 text-red-700{% elif category == 'warning' %}bg-yellow-100 text-yellow-700{% elif category == 'info' %}bg-blue-100 text-blue-700{% else %}bg-green-100 text-green-700{% endif %}"> <div class="flex items-center"> {% if category == 'error' %} @@ -415,20 +415,20 @@ {% else %} <i class="fas fa-check-circle mr-2"></i> {% endif %} - {{ message }} + {{ message }} </div> <button class="text-gray-500 hover:text-gray-700 focus:outline-none" onclick="this.parentElement.style.display='none'"> <i class="fas fa-times"></i> </button> - </div> - {% endfor %} - </div> - {% endif %} - {% endwith %} - + </div> + {% endfor %} + </div> + {% endif %} + {% endwith %} + <!-- Page Content --> <div class="animate-fade-in"> - {% block content %}{% endblock %} + {% block content %}{% endblock %} </div> </div> </div> diff --git a/app/templates/patient_detail.html b/app/templates/patient_detail.html index da9509ee4324370da1064d4fd1d1a22476e439c2..6c8c95b831cd57bbeadf429f8d76e8b3461d1ab5 100644 --- a/app/templates/patient_detail.html +++ b/app/templates/patient_detail.html @@ -438,6 +438,10 @@ Lịch sử Lượt khám (Encounters) </h3> <!-- Nút thêm encounter mới nếu cần --> + <button type="button" class="inline-flex items-center px-3 py-1 border border-transparent rounded text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"> + <i class="fas fa-plus mr-2"></i> + Thêm lượt khám + </button> </div> <div class="border-t border-gray-200 px-4 py-5 sm:p-6"> {% if encounters %} @@ -445,27 +449,30 @@ <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">ID Encounter</th> + <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">Ngày nhập viện</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ngày ra viện</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Chuyên gia DD</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ngày vào CCU</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ngày ra CCU</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Chuyên gia DD</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"> {% for encounter in encounters %} - <tr class="hover:bg-gray-50"> + <tr class="encounter-row hover:bg-gray-100 cursor-pointer transition duration-150 ease-in-out" data-encounter-id="{{ encounter.id }}"> <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ encounter.id }}</td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ encounter.admissionDateTime.strftime('%d/%m/%Y %H:%M') if encounter.admissionDateTime else 'N/A' }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ encounter.dischargeDateTime.strftime('%d/%m/%Y %H:%M') if encounter.dischargeDateTime else 'Đang điều trị' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> + {{ encounter.dischargeDateTime.strftime('%d/%m/%Y %H:%M') if encounter.dischargeDateTime else 'Đang điều trị' }} + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> + {{ encounter.dietitian.fullName if encounter.dietitian else 'Chưa gán' }} + </td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ encounter.ccuAdmissionDateTime.strftime('%d/%m/%Y %H:%M') if encounter.ccuAdmissionDateTime else 'N/A' }}</td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ encounter.ccuDischargeDateTime.strftime('%d/%m/%Y %H:%M') if encounter.ccuDischargeDateTime else 'N/A' }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ encounter.dietitian.fullName if encounter.dietitian else 'Chưa gán' }}</td> <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> - <a href="#" class="text-blue-600 hover:text-blue-900">Chi tiết</a> - {# Thêm nút sửa/xóa encounter nếu cần #} + <a href="#" class="text-blue-600 hover:text-blue-900 view-encounter-details" data-encounter-id="{{ encounter.id }}">Chi tiết</a> </td> </tr> {% endfor %} diff --git a/app/templates/upload.html b/app/templates/upload.html index 43863856301c8c5ba5fe7b4a633f458ee9ba8bce..0a18294f9e9684c6dc58b7a56e48af76f596daa7 100644 --- a/app/templates/upload.html +++ b/app/templates/upload.html @@ -21,6 +21,7 @@ <div class="bg-white shadow rounded-lg transition-all duration-300 hover:shadow-lg animate-fade-in"> <div class="px-4 py-5 sm:p-6"> <form action="{{ url_for('upload.index') }}" method="post" enctype="multipart/form-data" id="uploadForm"> + {{ form.csrf_token }} <div class="space-y-6"> <div> <h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Tải lên tệp CSV</h3> @@ -88,7 +89,9 @@ </div> <div> - <button type="submit" id="submitBtn" disabled class="w-full inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-gray-400 focus:outline-none transition-all duration-200"> + <button type="submit" id="submitBtn" disabled + style="background-color: #9ca3af;" + class="w-full inline-flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white transition-all duration-200 ease-in-out"> <i class="fas fa-cloud-upload-alt mr-2"></i> Tải lên </button> @@ -103,7 +106,7 @@ <div class="lg:col-span-1"> <div class="bg-white shadow rounded-lg transition-all duration-300 hover:shadow-lg animate-fade-in"> <div class="px-4 py-5 sm:p-6"> - <h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Hướng dẫn tải lên</h3> + <h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Hướng dẫn tải lên (Dữ liệu Bệnh nhân Ban đầu)</h3> <div class="space-y-4"> <div class="flex items-start"> <div class="flex-shrink-0"> @@ -122,7 +125,7 @@ </div> </div> <div class="ml-3 text-sm text-gray-600"> - <p>Kiểm tra xem tệp có các cột: <code>encounterId</code>, <code>bmi</code>, <code>fio2</code>, <code>tidal_vol</code>, <code>referral</code>.</p> + <p>Kiểm tra xem tệp có đầy đủ các cột theo thứ tự: <code>patientID</code>, <code>firstName</code>, <code>lastName</code>, <code>age</code>, <code>gender</code>, <code>height</code>, <code>weight</code>, <code>blood_type</code>, <code>measurementDateTime</code>, <code>temperature</code>, <code>heart_rate</code>, <code>blood_pressure_systolic</code>, ... (và các cột đo lường khác).</p> </div> </div> <div class="flex items-start"> @@ -148,20 +151,20 @@ </div> <div class="mt-6 border-t border-gray-200 pt-4"> - <h4 class="text-sm font-medium text-gray-900 mb-2">Mẫu định dạng CSV</h4> + <h4 class="text-sm font-medium text-gray-900 mb-2">Mẫu định dạng CSV (Bệnh nhân + Đo lường ban đầu)</h4> <div class="bg-gray-50 p-3 rounded-md overflow-x-auto"> <code class="text-xs"> - encounterId,bmi,fio2,tidal_vol,referral<br> - P-10001,23.5,60,450,yes<br> - P-10002,19.2,45,500,no<br> - P-10003,32.5,70,400,yes + patientID,firstName,lastName,age,gender,height,weight,blood_type,measurementDateTime,temperature,heart_rate,blood_pressure_systolic,blood_pressure_diastolic,oxygen_saturation,resp_rate,fio2,tidal_vol,...,bmi,referral<br> + P-00001,John,Doe,65,male,175.0,80.5,A+,2024-01-15T10:00:00,36.8,85,130,80,97.5,18,21.0,480,...,26.3,0<br> + P-00002,Jane,Smith,42,female,162.0,55.2,O-,2024-01-15T10:05:00,37.1,72,118,75,99.0,16,21.0,420,...,21.0,1<br> + ... </code> </div> </div> <div class="mt-6"> - <a href="{{ url_for('static', filename='templates/sample.csv') }}" class="text-primary-600 hover:text-primary-500 text-sm font-medium flex items-center"> - <i class="fas fa-download mr-1"></i> Tải xuống mẫu CSV + <a href="{{ url_for('static', filename='templates/initial_patient_data_sample.csv') }}" class="text-primary-600 hover:text-primary-500 text-sm font-medium flex items-center"> + <i class="fas fa-download mr-1"></i> Tải xuống mẫu CSV (Bệnh nhân ban đầu) </a> </div> </div> @@ -171,26 +174,42 @@ <div class="px-4 py-5 sm:p-6"> <h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Lịch sử tải lên</h3> <div class="space-y-3"> - {% for i in range(3) %} - <div class="border-b border-gray-200 pb-3 last:border-0 last:pb-0"> - <div class="flex justify-between items-start"> - <div> - <p class="text-sm font-medium text-gray-900">data_{{ 20230301 + i }}.csv</p> - <p class="text-xs text-gray-500">{{ (3-i) }} ngày trước</p> + {% if recent_uploads %} + {% for upload in recent_uploads %} + <div class="border-b border-gray-200 pb-3 last:border-0 last:pb-0"> + <div class="flex justify-between items-start"> + <div> + <p class="text-sm font-medium text-gray-900">{{ upload.original_filename }}</p> + <p class="text-xs text-gray-500">{{ upload.upload_date.strftime('%d/%m/%Y %H:%M') }}</p> + </div> + <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium + {% if upload.status == 'completed' %}bg-green-100 text-green-800 + {% elif upload.status == 'processing' %}bg-yellow-100 text-yellow-800 + {% elif upload.status == 'failed' %}bg-red-100 text-red-800 + {% elif upload.status == 'completed_with_errors' %}bg-orange-100 text-orange-800 + {% else %}bg-gray-100 text-gray-800{% endif %}"> + {% if upload.status == 'completed' %}Thành công + {% elif upload.status == 'processing' %}Đang xử lý + {% elif upload.status == 'failed' %}Thất bại + {% elif upload.status == 'completed_with_errors' %}Hoàn thành (có lỗi) + {% else %}{{ upload.status|capitalize }}{% endif %} + </span> + </div> + <div class="mt-1 text-xs text-gray-500"> + {% if upload.total_records %}Đã nhập {{ upload.processed_records }} / {{ upload.total_records }} bản ghi{% endif %} + {% if upload.error_records > 0 %} ({{ upload.error_records }} lỗi){% endif %} </div> - <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"> - Thành công - </span> </div> - <div class="mt-1 text-xs text-gray-500"> - Đã nhập {{ 120 + i * 15 }} bản ghi + {% endfor %} + {% else %} + <div class="text-sm text-gray-500 text-center py-4"> + Chưa có lịch sử tải lên </div> - </div> - {% endfor %} + {% endif %} </div> <div class="mt-4"> - <a href="#" class="text-primary-600 hover:text-primary-500 text-sm font-medium"> + <a href="{{ url_for('upload.history') }}" class="text-primary-600 hover:text-primary-500 text-sm font-medium"> Xem tất cả lịch sử </a> </div> @@ -202,6 +221,7 @@ {% endblock %} {% block scripts %} +{{ super() }} <script> document.addEventListener('DOMContentLoaded', () => { const fileInput = document.getElementById('file-upload'); @@ -257,11 +277,13 @@ function handleFiles(file) { if (file.type !== 'text/csv' && !file.name.endsWith('.csv')) { alert('Vui lòng chọn tệp CSV'); + resetFileInput(); return; } if (file.size > 10 * 1024 * 1024) { alert('Kích thước tệp quá lớn. Tối đa 10MB.'); + resetFileInput(); return; } @@ -271,18 +293,20 @@ function showFilePreview(file) { fileName.textContent = file.name; filePreview.classList.remove('hidden'); - submitBtn.disabled = false; - submitBtn.classList.remove('bg-gray-400'); - submitBtn.classList.add('bg-primary-600', 'hover:bg-primary-700', 'transform', 'hover:-translate-y-1'); + submitBtn.disabled = false; + submitBtn.style.backgroundColor = '#0284c7'; + submitBtn.style.cursor = 'pointer'; } - removeFile.addEventListener('click', function() { + function resetFileInput() { fileInput.value = ''; filePreview.classList.add('hidden'); - submitBtn.disabled = true; - submitBtn.classList.add('bg-gray-400'); - submitBtn.classList.remove('bg-primary-600', 'hover:bg-primary-700', 'transform', 'hover:-translate-y-1'); - }); + submitBtn.disabled = true; + submitBtn.style.backgroundColor = '#9ca3af'; + submitBtn.style.cursor = 'not-allowed'; + } + + removeFile.addEventListener('click', resetFileInput); // Xử lý submit form const uploadForm = document.getElementById('uploadForm'); diff --git a/app/templates/upload_history.html b/app/templates/upload_history.html new file mode 100644 index 0000000000000000000000000000000000000000..c5d083a66f65eb9cc5e3203d40c4308c79565d0e --- /dev/null +++ b/app/templates/upload_history.html @@ -0,0 +1,228 @@ +{% extends "base.html" %} + +{% block title %}Lịch sử Tải lên - CCU_BVNM{% endblock %} + +{% block content %} +<div class="animate-slide-in"> + <div class="md:flex md:items-center md:justify-between mb-6"> + <div class="flex-1 min-w-0"> + <h2 class="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate"> + Lịch sử Tải lên + </h2> + <p class="mt-1 text-sm text-gray-500"> + Xem lại các tệp đã tải lên và trạng thái xử lý. + </p> + </div> + <div class="mt-4 flex md:mt-0 md:ml-4"> + <a href="{{ url_for('upload.index') }}" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm 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 duration-200"> + <i class="fas fa-upload mr-2"></i> Tải lên tệp mới + </a> + </div> + </div> + + <div class="bg-white shadow overflow-hidden sm:rounded-lg"> + <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">Tên tệp gốc</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ngày tải lên</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Người tải</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-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tổng số dòng</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Đã xử lý</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lỗi</th> + <th scope="col" class="relative px-6 py-3"> + <span class="sr-only">Actions</span> + </th> + </tr> + </thead> + <tbody class="bg-white divide-y divide-gray-200"> + {% if uploads.items %} + {% for upload in uploads.items %} + <tr class="hover:bg-gray-50"> + <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ upload.original_filename }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ upload.upload_date.strftime('%d/%m/%Y %H:%M') if upload.upload_date else 'N/A' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ upload.uploader.email if upload.uploader else 'N/A' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm"> + {% if upload.status == 'completed' %} + <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Hoàn thành</span> + {% elif upload.status == 'processing' %} + <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">Đang xử lý</span> + {% elif upload.status == 'failed' %} + <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Thất bại</span> + {% elif upload.status == 'completed_with_errors' %} + <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-orange-100 text-orange-800">Hoàn thành (có lỗi)</span> + {% else %} + <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">{{ upload.status|capitalize }}</span> + {% endif %} + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center">{{ upload.total_records if upload.total_records is not none else '-' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center">{{ upload.processed_records if upload.processed_records is not none else '-' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-center {% if upload.error_records > 0 %}text-red-600 font-semibold{% else %}text-gray-500{% endif %}"> + {{ upload.error_records if upload.error_records is not none else '-' }} + </td> + <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2"> + {# Nút Xem trước nếu cần #} + {# <a href="{{ url_for('upload.preview', file_id=upload.id) }}" class="text-indigo-600 hover:text-indigo-900">Preview</a> #} + {% if upload.error_records > 0 and upload.error_details %} + <button type="button" class="text-yellow-600 hover:text-yellow-900 view-errors" data-errors='{{ upload.error_details }}'>Xem lỗi</button> + {% endif %} + <a href="{{ url_for('upload.download', file_id=upload.id) }}" class="text-blue-600 hover:text-blue-900">Tải xuống</a> + <form action="{{ url_for('upload.delete', file_id=upload.id) }}" method="POST" class="inline" onsubmit="return confirm('Bạn có chắc chắn muốn xóa tệp này và lịch sử của nó?');"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> + <button type="submit" class="text-red-600 hover:text-red-900">Xóa</button> + </form> + </td> + </tr> + {% endfor %} + {% else %} + <tr> + <td colspan="8" class="px-6 py-10 text-center text-sm text-gray-500"> + Chưa có lịch sử tải lên nào. + </td> + </tr> + {% endif %} + </tbody> + </table> + </div> + + <!-- Pagination --> + {% if uploads.pages > 1 %} + <nav class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6" aria-label="Pagination"> + <div class="hidden sm:block"> + <p class="text-sm text-gray-700"> + Hiển thị + <span class="font-medium">{{ uploads.offset + 1 }}</span> + đến + <span class="font-medium">{{ uploads.offset + uploads.items|length }}</span> + của + <span class="font-medium">{{ uploads.total }}</span> + kết quả + </p> + </div> + <div class="flex-1 flex justify-between sm:justify-end"> + <a href="{{ url_for('upload.history', page=uploads.prev_num) if uploads.has_prev else '#' }}" + class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 {% if not uploads.has_prev %}opacity-50 cursor-not-allowed{% endif %}"> + Trước + </a> + <a href="{{ url_for('upload.history', page=uploads.next_num) if uploads.has_next else '#' }}" + class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 {% if not uploads.has_next %}opacity-50 cursor-not-allowed{% endif %}"> + Sau + </a> + </div> + </nav> + {% endif %} + </div> + + <!-- Modal để hiển thị lỗi --> + <div id="errorModal" class="fixed z-10 inset-0 overflow-y-auto hidden" aria-labelledby="modal-title" role="dialog" aria-modal="true"> + <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> + <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div> + <span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span> + <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full"> + <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> + <div class="sm:flex sm:items-start"> + <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"> + <i class="fas fa-exclamation-triangle text-red-600"></i> + </div> + <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full"> + <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title"> + Chi tiết lỗi xử lý CSV + </h3> + <div class="mt-2"> + <div class="text-sm text-gray-500 max-h-96 overflow-y-auto"> + <pre id="errorDetailsContent" class="whitespace-pre-wrap"></pre> + </div> + </div> + </div> + </div> + </div> + <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse"> + <button type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" id="closeErrorModal"> + Đóng + </button> + </div> + </div> + </div> + </div> + +</div> +{% endblock %} + +{% block scripts %} +{{ super() }} +<script> +document.addEventListener('DOMContentLoaded', () => { + const errorModal = document.getElementById('errorModal'); + const errorDetailsContent = document.getElementById('errorDetailsContent'); + const closeErrorModalBtn = document.getElementById('closeErrorModal'); + const viewErrorButtons = document.querySelectorAll('.view-errors'); + + viewErrorButtons.forEach(button => { + button.addEventListener('click', () => { + try { + const errorsJson = button.getAttribute('data-errors'); + const errors = JSON.parse(errorsJson); + + // Kiểm tra xem lỗi có được tóm tắt không + if (errors.summary) { + // Đây là dạng tóm tắt lỗi + let formattedSummary = `${errors.summary}\n\n`; + + if (errors.sample_patients && errors.sample_patients.length) { + formattedSummary += `Ví dụ các ID bệnh nhân đã tồn tại: ${errors.sample_patients.join(', ')}\n\n`; + } + + if (errors.message) { + formattedSummary += errors.message; + } + + errorDetailsContent.textContent = formattedSummary; + } else { + // Dạng lỗi chi tiết theo từng dòng + let formattedErrors = ""; + + if (Array.isArray(errors)) { + errors.forEach(err => { + if (err.summary) { + // Đây là thông báo tóm tắt ở cuối + formattedErrors += `\n${err.summary}\n`; + } else { + formattedErrors += `Dòng ${err.row}: ${err.error}\n`; + if(err.data) { + formattedErrors += ` Dữ liệu: ${JSON.stringify(err.data)}\n\n`; + } else { + formattedErrors += '\n'; + } + } + }); + } else { + formattedErrors = "Định dạng lỗi không đúng."; + } + + errorDetailsContent.textContent = formattedErrors; + } + + errorModal.classList.remove('hidden'); + } catch (e) { + console.error("Error parsing or displaying errors:", e); + errorDetailsContent.textContent = "Không thể hiển thị chi tiết lỗi."; + errorModal.classList.remove('hidden'); + } + }); + }); + + closeErrorModalBtn.addEventListener('click', () => { + errorModal.classList.add('hidden'); + }); + + // Đóng modal khi click bên ngoài + errorModal.addEventListener('click', (event) => { + if (event.target === errorModal) { + errorModal.classList.add('hidden'); + } + }); +}); +</script> +{% endblock %} \ No newline at end of file diff --git a/app/utils/csv_handler.py b/app/utils/csv_handler.py index 06776857218a0210f12ff923d88b8f6335690341..ea80c0e7d62dc6894228618a44a0e455eadeef72 100644 --- a/app/utils/csv_handler.py +++ b/app/utils/csv_handler.py @@ -1,290 +1,285 @@ import os import csv -import pandas as pd +# import pandas as pd # Không dùng pandas nữa import json +from flask import current_app from datetime import datetime from app import db from app.models.patient import Patient, Encounter from app.models.measurement import PhysiologicalMeasurement from app.models.referral import Referral from app.models.uploaded_file import UploadedFile +from sqlalchemy.exc import IntegrityError +from dateutil.parser import parse as parse_datetime_string # Thư viện tốt hơn để parse datetime +# --- Helper Functions --- +def _parse_float(value): + if value is None or value == '': + return None + try: + return float(value) + except (ValueError, TypeError): + return None + +def _parse_int(value): + if value is None or value == '': + return None + try: + # Thử chuyển đổi sang float trước để xử lý số thập phân (vd: "1.0") + float_val = float(value) + # Sau đó chuyển sang int + return int(float_val) + except (ValueError, TypeError): + return None + +def _parse_datetime(value): + if value is None or value == '': + return None + try: + # Sử dụng dateutil.parser để linh hoạt hơn + return parse_datetime_string(value) + except (ValueError, TypeError): + # Thử định dạng cụ thể nếu parser chung thất bại (tùy chọn) + # try: + # return datetime.strptime(value, '%Y-%m-%d %H:%M:%S') + # except ValueError: + # return None + return None + +# --- Validation (Giữ nguyên hoặc đơn giản hóa nếu chỉ cần kiểm tra header) --- def validate_csv(file_path, delimiter=',', encoding='utf-8'): - """ - Validate if a CSV file has the required columns and format - - Args: - file_path (str): Path to the CSV file - delimiter (str): CSV delimiter character - encoding (str): File encoding - - Returns: - dict: Dictionary with validation result - { - 'valid': bool, - 'error': str or None - } - """ + """Validate CSV header for patient upload format.""" try: - # Check if file exists if not os.path.exists(file_path): return {'valid': False, 'error': 'File does not exist'} - - # Check file size (max 10MB) + file_size = os.path.getsize(file_path) - if file_size > 10 * 1024 * 1024: # 10MB + if file_size > 10 * 1024 * 1024: # 10MB return {'valid': False, 'error': 'File is too large (max 10MB)'} - - # Try to read the file - df = pd.read_csv(file_path, delimiter=delimiter, encoding=encoding, nrows=1) - - # Check if the required columns exist - required_columns = ['encounterId', 'patient_id', 'firstName', 'lastName', 'age', 'gender'] - optional_columns = ['bmi', 'admission_date', 'fio2', 'resp_rate', 'tidal_vol', 'pip', 'referral'] - - missing_columns = [col for col in required_columns if col not in df.columns] - - if missing_columns: - return { - 'valid': False, - 'error': f'Missing required columns: {", ".join(missing_columns)}' - } - - # All checks passed + + with open(file_path, 'r', encoding=encoding, newline='') as csvfile: + reader = csv.reader(csvfile, delimiter=delimiter) + header = next(reader, None) # Đọc dòng header + + if not header: + return {'valid': False, 'error': 'CSV file is empty or header is missing.'} + + # Kiểm tra các cột bắt buộc tối thiểu (có thể mở rộng) + required_patient_cols = ['patientID', 'firstName', 'lastName', 'age', 'gender'] + required_measurement_cols = ['measurementDateTime'] + required_cols = required_patient_cols + required_measurement_cols + + missing_cols = [col for col in required_cols if col not in header] + if missing_cols: + return {'valid': False, 'error': f'Missing required columns: {", ".join(missing_cols)}'} + return {'valid': True, 'error': None} - + except Exception as e: - return {'valid': False, 'error': str(e)} + return {'valid': False, 'error': f'CSV validation error: {str(e)}'} +# --- Rewritten process_csv function --- def process_csv(uploaded_file_id): """ - Process a CSV file and insert data into database - - Args: - uploaded_file_id (int): ID of the uploaded file record - - Returns: - dict: Processing result - { - 'success': bool, - 'total_records': int, - 'processed_records': int, - 'error_records': int, - 'error_details': str or None - } + Process the combined Patient + Initial Measurement CSV file. + Creates Patient, initial Encounter, initial Measurement, and initial Referral. """ + uploaded_file = UploadedFile.query.get(uploaded_file_id) + if not uploaded_file: + return {'success': False, 'error': f'Uploaded file record not found (ID: {uploaded_file_id})'} + + total_records = 0 + processed_records = 0 + error_records = 0 + errors = [] + try: - # Get the uploaded file record - uploaded_file = UploadedFile.query.get(uploaded_file_id) - - if not uploaded_file: - return { - 'success': False, - 'error': f'Uploaded file record not found (ID: {uploaded_file_id})' - } - - # Read the CSV file - df = pd.read_csv( - uploaded_file.file_path, - delimiter=uploaded_file.delimiter, - encoding=uploaded_file.file_encoding - ) - - # Initialize counters - total_records = len(df) - processed_records = 0 - error_records = 0 - errors = [] - - # Process each row - for index, row in df.iterrows(): - try: - # Process patient data - patient = process_patient_data(row) - - # Process encounter data - encounter = process_encounter_data(row, patient.id) - - # Process measurement data if columns exist - has_measurement_data = any(col in df.columns for col in [ - 'fio2', 'resp_rate', 'tidal_vol', 'pip', 'end_tidal_co2', - 'peep', 'tidal_vol_actual', 'tidal_vol_kg' - ]) - - if has_measurement_data: - measurement = process_measurement_data(row, patient.id, encounter.id) - - # Process referral if needed and column exists - if uploaded_file.process_referrals and 'referral' in df.columns: - process_referral_data(row, patient.id, encounter.id) - - processed_records += 1 - - except Exception as e: - error_records += 1 - errors.append({ - 'row': index + 1, - 'error': str(e), - 'data': row.to_dict() - }) - - # Update uploaded file record with results - uploaded_file.total_records = total_records - uploaded_file.processed_records = processed_records - uploaded_file.error_records = error_records - - # Store error details as JSON - if errors: - uploaded_file.error_details = json.dumps(errors) - - db.session.commit() - - return { - 'success': True, - 'total_records': total_records, - 'processed_records': processed_records, - 'error_records': error_records, - 'error_details': json.dumps(errors) if errors else None - } - - except Exception as e: - return { - 'success': False, - 'error': str(e) - } + with open(uploaded_file.filePath, 'r', encoding=uploaded_file.file_encoding, newline='') as csvfile: + # Sử dụng DictReader để dễ dàng truy cập cột bằng tên + reader = csv.DictReader(csvfile, delimiter=uploaded_file.delimiter) + + # Kiểm tra header có khớp không (an toàn hơn) + expected_headers = [ + 'patientID', 'firstName', 'lastName', 'age', 'gender', 'height', 'weight', 'blood_type', + 'measurementDateTime', 'temperature', 'heart_rate', 'blood_pressure_systolic', + 'blood_pressure_diastolic', 'oxygen_saturation', 'resp_rate', 'fio2', + 'tidal_vol', 'end_tidal_co2', 'feed_vol', 'feed_vol_adm', 'fio2_ratio', + 'insp_time', 'oxygen_flow_rate', 'peep', 'pip', 'sip', 'tidal_vol_actual', + 'tidal_vol_kg', 'tidal_vol_spon', 'bmi', 'referral' + ] + if not reader.fieldnames or any(h not in reader.fieldnames for h in expected_headers): + missing = [h for h in expected_headers if h not in (reader.fieldnames or [])] + # Cho phép thiếu các cột không bắt buộc nếu cần + # Đây là check nghiêm ngặt, yêu cầu mọi cột phải có + # if missing: + # raise ValueError(f"CSV header mismatch. Missing or incorrect columns: {', '.join(missing)}") + pass # Bỏ qua check nghiêm ngặt nếu muốn linh hoạt hơn -def process_patient_data(row): - """Process patient data from a CSV row""" - # Check if patient already exists - patient = None - - if 'patient_id' in row and pd.notna(row['patient_id']): - patient_id = str(row['patient_id']).strip() - patient = Patient.query.filter_by(patient_id=patient_id).first() - - if not patient: - # Create new patient - patient = Patient( - patient_id=str(row['patient_id']) if 'patient_id' in row and pd.notna(row['patient_id']) else f"P-{datetime.now().strftime('%Y%m%d%H%M%S')}", - firstName=str(row['firstName']) if 'firstName' in row and pd.notna(row['firstName']) else None, - lastName=str(row['lastName']) if 'lastName' in row and pd.notna(row['lastName']) else None, - gender=str(row['gender']) if 'gender' in row and pd.notna(row['gender']) else None, - date_of_birth=calculate_dob(row['age']) if 'age' in row and pd.notna(row['age']) else None, - height=float(row['height']) if 'height' in row and pd.notna(row['height']) else None, - weight=float(row['weight']) if 'weight' in row and pd.notna(row['weight']) else None, - status='active' - ) - - db.session.add(patient) - db.session.commit() - - return patient + for i, row in enumerate(reader): + total_records += 1 + row_num = i + 2 # +1 vì index bắt đầu từ 0, +1 nữa vì bỏ qua header + try: + # --- 1. Process Patient --- + patient_id = row.get('patientID') + if not patient_id: + raise ValueError("Missing patientID") + + # Kiểm tra xem Patient đã tồn tại chưa, nếu có thì bỏ qua (hoặc cập nhật tùy logic) + existing_patient = Patient.query.get(patient_id) + if existing_patient: + errors.append({'row': row_num, 'error': f'Patient {patient_id} already exists. Skipping.'}) + error_records += 1 + continue # Bỏ qua dòng này + + height = _parse_float(row.get('height')) + weight = _parse_float(row.get('weight')) + + new_patient = Patient( + id=patient_id, + firstName=row.get('firstName'), + lastName=row.get('lastName'), + age=_parse_int(row.get('age')), + gender=row.get('gender', '').lower() or None, # Đảm bảo lowercase hoặc None + height=height, + weight=weight, + blood_type=row.get('blood_type'), + # admission_date sẽ được xác định bởi Encounter đầu tiên + ) + new_patient.calculate_bmi() # Tự tính BMI + + # --- 2. Process Initial Encounter --- + measurement_time = _parse_datetime(row.get('measurementDateTime')) + if not measurement_time: + # Nếu không có thời gian đo lường, dùng thời gian hiện tại + measurement_time = datetime.utcnow() -def process_encounter_data(row, patient_id): - """Process encounter data from a CSV row""" - # Check if encounter already exists - encounter = None - - if 'encounterId' in row and pd.notna(row['encounterId']): - encounter_id = str(row['encounterId']).strip() - encounter = Encounter.query.filter_by(encounter_id=encounter_id).first() - - if not encounter: - # Create new encounter - encounter = Encounter( - encounter_id=str(row['encounterId']) if 'encounterId' in row and pd.notna(row['encounterId']) else f"E-{datetime.now().strftime('%Y%m%d%H%M%S')}", - patient_id=patient_id, - admission_date=parse_date(row['admission_date']) if 'admission_date' in row and pd.notna(row['admission_date']) else datetime.now(), - discharge_date=parse_date(row['discharge_date']) if 'discharge_date' in row and pd.notna(row['discharge_date']) else None, - encounter_type='inpatient', - primary_diagnosis=str(row['diagnosis']) if 'diagnosis' in row and pd.notna(row['diagnosis']) else None - ) - - db.session.add(encounter) + # Đặt admission_date của Patient bằng thời gian Encounter đầu tiên + new_patient.admission_date = measurement_time + + initial_encounter = Encounter( + patientID=new_patient.id, + admissionDateTime=measurement_time + # dischargeDateTime sẽ là None + ) + + # --- 3. Process Initial Measurement --- + initial_measurement = PhysiologicalMeasurement( + patient_id=new_patient.id, + # encounter_id sẽ được gán sau khi encounter được flush + measurementDateTime=measurement_time, + temperature=_parse_float(row.get('temperature')), + heart_rate=_parse_int(row.get('heart_rate')), + blood_pressure_systolic=_parse_int(row.get('blood_pressure_systolic')), + blood_pressure_diastolic=_parse_int(row.get('blood_pressure_diastolic')), + oxygen_saturation=_parse_float(row.get('oxygen_saturation')), + resp_rate=_parse_int(row.get('resp_rate')), + fio2=_parse_float(row.get('fio2')), + fio2_ratio=_parse_float(row.get('fio2_ratio')), + peep=_parse_float(row.get('peep')), + pip=_parse_float(row.get('pip')), + sip=_parse_float(row.get('sip')), + insp_time=_parse_float(row.get('insp_time')), + oxygen_flow_rate=_parse_float(row.get('oxygen_flow_rate')), + end_tidal_co2=_parse_float(row.get('end_tidal_co2')), + tidal_vol=_parse_float(row.get('tidal_vol')), + tidal_vol_kg=_parse_float(row.get('tidal_vol_kg')), + tidal_vol_actual=_parse_float(row.get('tidal_vol_actual')), + tidal_vol_spon=_parse_float(row.get('tidal_vol_spon')), + feed_vol=_parse_float(row.get('feed_vol')), + feed_vol_adm=_parse_float(row.get('feed_vol_adm')), + bmi=_parse_float(row.get('bmi')) # Có thể lấy BMI từ CSV hoặc để Patient tự tính + # notes có thể thêm sau này nếu cần + ) + + # --- 4. Process Initial Referral (Optional) --- + initial_referral = None + referral_flag = _parse_int(row.get('referral')) # Parse '1' hoặc '0' + if referral_flag == 1: + initial_referral = Referral( + patient_id=new_patient.id, + # encounter_id sẽ gán sau + is_ml_recommended=True, # Giả định referral từ CSV là do ML/auto + referral_status='ML Recommended', # Hoặc 'Pending Review' + referralRequestedDateTime=measurement_time + # dietitian_id, notes có thể thêm sau + ) + + # --- Add to Session and Commit (từng bản ghi hoặc theo batch) --- + db.session.add(new_patient) + db.session.add(initial_encounter) + db.session.flush() # Flush để lấy initial_encounter.id + + initial_measurement.encounter_id = initial_encounter.id + db.session.add(initial_measurement) + + if initial_referral: + initial_referral.encounter_id = initial_encounter.id + db.session.add(initial_referral) + + db.session.commit() # Commit cho mỗi bệnh nhân để tránh lỗi lớn + processed_records += 1 + + except IntegrityError as ie: + db.session.rollback() + error_records += 1 + errors.append({'row': row_num, 'error': f'Database integrity error (possibly duplicate ID?): {str(ie)}'}) + except Exception as e: + db.session.rollback() + error_records += 1 + # Ghi log lỗi chi tiết hơn + error_message = f'Error processing row: {str(e)}' + current_app.logger.error(f"CSV Processing Error (Row {row_num}): {error_message} Data: {row}", exc_info=True) + errors.append({'row': row_num, 'error': error_message}) + + except Exception as e: + # Lỗi đọc file hoặc lỗi không mong muốn khác + db.session.rollback() + uploaded_file.status = 'failed' + error_message = f'Failed to process CSV file: {str(e)}' + uploaded_file.error_details = error_message + current_app.logger.error(f"CSV File Processing Failed (File ID: {uploaded_file_id}): {error_message}", exc_info=True) db.session.commit() - - return encounter + return {'success': False, 'error': error_message} -def process_measurement_data(row, patient_id, encounter_id): - """Process physiological measurement data from a CSV row""" - # Create new measurement - measurement = PhysiologicalMeasurement( - patient_id=patient_id, - encounter_id=encounter_id, - temperature=float(row['temperature']) if 'temperature' in row and pd.notna(row['temperature']) else None, - heart_rate=int(row['heart_rate']) if 'heart_rate' in row and pd.notna(row['heart_rate']) else None, - respiratory_rate=float(row['resp_rate']) if 'resp_rate' in row and pd.notna(row['resp_rate']) else None, - blood_pressure_systolic=int(row['bp_systolic']) if 'bp_systolic' in row and pd.notna(row['bp_systolic']) else None, - blood_pressure_diastolic=int(row['bp_diastolic']) if 'bp_diastolic' in row and pd.notna(row['bp_diastolic']) else None, - oxygen_saturation=float(row['oxygen_saturation']) if 'oxygen_saturation' in row and pd.notna(row['oxygen_saturation']) else None, - bmi=float(row['bmi']) if 'bmi' in row and pd.notna(row['bmi']) else None, - fio2=float(row['fio2']) if 'fio2' in row and pd.notna(row['fio2']) else None, - tidal_volume=float(row['tidal_vol']) if 'tidal_vol' in row and pd.notna(row['tidal_vol']) else None, - peak_inspiratory_pressure=float(row['pip']) if 'pip' in row and pd.notna(row['pip']) else None, - measurement_date=datetime.now() - ) - - db.session.add(measurement) - db.session.commit() + # Cập nhật trạng thái file sau khi xử lý xong + uploaded_file.total_records = total_records + uploaded_file.processed_records = processed_records + uploaded_file.error_records = error_records + uploaded_file.status = 'completed' if error_records == 0 else 'completed_with_errors' - return measurement - -def process_referral_data(row, patient_id, encounter_id): - """Process referral data from a CSV row""" - # Check if referral is needed - if 'referral' in row and pd.notna(row['referral']): - referral_value = str(row['referral']).strip().lower() - - if referral_value in ['yes', 'true', '1']: - # Create new referral - referral = Referral( - patient_id=patient_id, - referralRequestedDateTime=datetime.now(), - referral_source='CSV Import', - reason='Automatically generated from imported data', - referral_status='new', - priority=3, # Medium priority - ml_prediction=True, - ml_confidence=float(row['ml_confidence']) if 'ml_confidence' in row and pd.notna(row['ml_confidence']) else 0.8 - ) - - db.session.add(referral) - db.session.commit() - - return referral + # Xử lý lưu error_details một cách thông minh để tránh quá lớn + if errors: + # Nếu có quá nhiều lỗi trùng lặp, hãy tóm tắt thay vì lưu tất cả + if len(errors) > 10 and all(e.get('error', '').startswith('Patient') and 'already exists' in e.get('error', '') for e in errors[:10]): + patient_ids = [e.get('error').split()[1] for e in errors if 'already exists' in e.get('error', '')] + error_summary = { + 'summary': f'Phát hiện {len(patient_ids)} bệnh nhân đã tồn tại trong hệ thống.', + 'sample_patients': patient_ids[:5], # Chỉ lấy 5 mẫu + 'message': f'Vui lòng kiểm tra lại dữ liệu hoặc sử dụng tính năng cập nhật thay vì thêm mới.' + } + uploaded_file.error_details = json.dumps(error_summary) + else: + # Chỉ lưu tối đa 20 lỗi đầu tiên để tránh quá lớn + limited_errors = errors[:20] + if len(errors) > 20: + limited_errors.append({'summary': f'... và {len(errors) - 20} lỗi khác không hiển thị.'}) + uploaded_file.error_details = json.dumps(limited_errors) - return None + uploaded_file.process_end = datetime.utcnow() + db.session.commit() -def calculate_dob(age): - """Calculate approximate date of birth from age""" - if pd.isna(age): - return None - - try: - age = int(age) - today = datetime.now().date() - year = today.year - age - return datetime(year, 1, 1).date() # January 1st as approximate birth date - except: - return None + return { + 'success': True, + 'total_records': total_records, + 'processed_records': processed_records, + 'error_records': error_records, + 'error_details': uploaded_file.error_details + } -def parse_date(date_str): - """Parse date string in various formats""" - if pd.isna(date_str): - return None - - try: - # Try multiple date formats - for fmt in ['%Y-%m-%d', '%Y/%m/%d', '%d-%m-%Y', '%d/%m/%Y', '%m-%d-%Y', '%m/%d/%Y']: - try: - return datetime.strptime(str(date_str).strip(), fmt) - except: - continue - - # If date is numeric (timestamp) - if isinstance(date_str, (int, float)): - return datetime.fromtimestamp(date_str) - - return None - except: - return None +# --- Xóa các hàm process_patient/encounter/measurement/referral_data cũ --- +# def process_patient_data(row): ... (đã xóa) +# def process_encounter_data(row, patient_id): ... (đã xóa) +# def process_measurement_data(row, patient_id, encounter_id): ... (đã xóa) +# def process_referral_data(row, patient_id, encounter_id): ... (đã xóa) +# def calculate_dob(age): ... (đã xóa - dùng trực tiếp age) +# def parse_date(date_str): ... (đã xóa - dùng helper _parse_datetime) diff --git a/generate_patients.py b/generate_patients.py index 93b30e25a4b92eece2328b69c5984025eec395f7..922baf6460d445f7970c128c3e3ef6809c8c8428 100644 --- a/generate_patients.py +++ b/generate_patients.py @@ -1,22 +1,23 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import json +import csv # Import thư viện csv import random # Bạn cần cài đặt thư viện Faker: pip install Faker from faker import Faker import os +from datetime import datetime # Cần datetime để thêm cột thời gian # Lấy đường dẫn thư mục của script hiện tại script_dir = os.path.dirname(os.path.abspath(__file__)) -output_file = os.path.join(script_dir, 'patients_data.json') +output_file = os.path.join(script_dir, 'patients_data.csv') # Đổi tên file thành .csv fake = Faker('en_US') # Có thể dùng 'vi_VN' nếu muốn tên Việt def generate_measurement(age): """Tạo dữ liệu đo lường ngẫu nhiên nhưng hợp lý theo độ tuổi""" measurements = {} - + measurements['measurementDateTime'] = datetime.utcnow().isoformat() # Thêm thời gian đo # Nhiệt độ (Temperature): 36.0 - 37.5, có thể cao hơn nếu già/bệnh base_temp = random.uniform(36.0, 37.2) if age > 70 or random.random() < 0.1: # 10% chance of higher temp @@ -66,20 +67,43 @@ def generate_measurement(age): measurements['tidal_vol'] = random.randint(350, 550) # Các chỉ số khác (tùy chọn, để None hoặc giá trị ngẫu nhiên cơ bản) - measurements['end_tidal_co2'] = round(random.uniform(35, 45), 1) if random.random() < 0.5 else None - measurements['feed_vol'] = random.randint(100, 500) if random.random() < 0.3 else None - measurements['peep'] = random.randint(4, 8) if random.random() < 0.4 else None - measurements['pip'] = random.randint(25, 35) if random.random() < 0.4 else None + measurements['end_tidal_co2'] = round(random.uniform(35, 45), 1) if random.random() < 0.5 else '' + measurements['feed_vol'] = random.randint(100, 500) if random.random() < 0.3 else '' + measurements['feed_vol_adm'] = random.randint(80, measurements['feed_vol']) if measurements['feed_vol'] and random.random() < 0.8 else '' + measurements['fio2_ratio'] = round(measurements['fio2'] / 100, 2) if measurements['fio2'] and measurements['fio2'] > 21 else '' + measurements['insp_time'] = round(random.uniform(0.8, 1.5), 1) if random.random() < 0.6 else '' + measurements['oxygen_flow_rate'] = random.randint(2, 10) if measurements['fio2'] > 21 else '' + measurements['peep'] = random.randint(4, 8) if random.random() < 0.4 else '' + measurements['pip'] = random.randint(25, 35) if random.random() < 0.4 else '' + measurements['sip'] = random.randint(20, measurements['pip']) if measurements['pip'] and random.random() < 0.7 else '' + measurements['tidal_vol_actual'] = random.randint(int(measurements['tidal_vol']*0.9), measurements['tidal_vol']) if measurements['tidal_vol'] else '' + measurements['tidal_vol_kg'] = '' # Sẽ tính toán sau dựa trên weight + measurements['tidal_vol_spon'] = random.randint(20, 80) if random.random() < 0.2 else '' + measurements['bmi'] = '' # Sẽ tính toán sau dựa trên height/weight + + # Thêm referral ngẫu nhiên (yes/no) + measurements['referral'] = 1 if random.random() < 0.25 else 0 # 25% cần referral ban đầu return measurements -def generate_patients(num_patients=50): - patients = [] +def generate_patient_rows(num_patients=50): + patient_data_rows = [] blood_types = ['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-'] genders = ['male', 'female', 'other'] + # Định nghĩa Header CSV cho file upload bệnh nhân + header = [ + 'patientID', 'firstName', 'lastName', 'age', 'gender', 'height', 'weight', 'blood_type', + # Thêm các trường đo lường ban đầu vào cùng hàng + 'measurementDateTime', 'temperature', 'heart_rate', 'blood_pressure_systolic', + 'blood_pressure_diastolic', 'oxygen_saturation', 'resp_rate', 'fio2', + 'tidal_vol', 'end_tidal_co2', 'feed_vol', 'feed_vol_adm', 'fio2_ratio', + 'insp_time', 'oxygen_flow_rate', 'peep', 'pip', 'sip', 'tidal_vol_actual', + 'tidal_vol_kg', 'tidal_vol_spon', 'bmi', 'referral' + ] + for i in range(num_patients): - patient_id_val = f"P-{i+1:05d}" # Sử dụng patient_id_val để tránh trùng tên biến + patient_id_val = f"P-{i+1:05d}" age = random.randint(18, 90) gender = random.choices(genders, weights=[48, 48, 4], k=1)[0] @@ -93,37 +117,64 @@ def generate_patients(num_patients=50): lastName = fake.last_name() height = round(random.uniform(150, 175), 1) weight = round(random.gauss(mu=height * 0.4, sigma=8), 1) - else: + else: # 'other' firstName = fake.first_name_nonbinary() lastName = fake.last_name() height = round(random.uniform(155, 180), 1) weight = round(random.gauss(mu=height * 0.42, sigma=9), 1) - weight = max(40, min(150, weight)) + weight = max(40, min(150, weight)) # Đảm bảo cân nặng hợp lý - patient = { - "patientID": patient_id_val, # Sử dụng patient_id_val + # Tạo thông tin bệnh nhân cơ bản + patient_info = { + "patientID": patient_id_val, "firstName": firstName, "lastName": lastName, "age": age, "gender": gender, "height": height, "weight": weight, - "blood_type": random.choice(blood_types), - "initial_measurement": generate_measurement(age) + "blood_type": random.choice(blood_types) } - patients.append(patient) - return patients + # Tạo thông tin đo lường ban đầu + initial_measurement = generate_measurement(age) + + # Tính toán các giá trị còn thiếu + if height > 0: + height_m = height / 100 + bmi_value = round(weight / (height_m * height_m), 1) + initial_measurement['bmi'] = bmi_value + if initial_measurement.get('tidal_vol'): # Dùng .get() để tránh lỗi nếu key không tồn tại + try: + ideal_body_weight = (height - 152.4) * 0.91 + (50 if gender == 'male' else 45.5) # Công thức tham khảo + if ideal_body_weight > 0: + initial_measurement['tidal_vol_kg'] = round(initial_measurement['tidal_vol'] / ideal_body_weight, 1) + except: # Bắt lỗi nếu tính toán thất bại + initial_measurement['tidal_vol_kg'] = '' + + + # Kết hợp thông tin bệnh nhân và đo lường thành một hàng dữ liệu CSV + row_data = [ + patient_info.get(col, '') or '' for col in header[:8] # Lấy thông tin bệnh nhân + ] + [ + initial_measurement.get(col, '') or '' for col in header[8:] # Lấy thông tin đo lường, dùng '' nếu None + ] + patient_data_rows.append(row_data) + + return header, patient_data_rows if __name__ == "__main__": num = 50 - print(f"Generating {num} patient records...") - patient_data = generate_patients(num) + print(f"Generating {num} patient records for CSV...") + csv_header, patient_rows = generate_patient_rows(num) try: - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(patient_data, f, indent=4, ensure_ascii=False) + # Ghi vào file CSV + with open(output_file, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(csv_header) # Ghi dòng header + writer.writerows(patient_rows) # Ghi tất cả các dòng dữ liệu print(f"Successfully generated {num} patient records to {output_file}") except IOError as e: print(f"Error writing to file {output_file}: {e}") diff --git a/instance/config.py b/instance/config.py new file mode 100644 index 0000000000000000000000000000000000000000..cf49ab180fc684da210474ad4d303aeae5a8dddd --- /dev/null +++ b/instance/config.py @@ -0,0 +1,31 @@ +# Production configuration settings +SECRET_KEY = 'replace_with_strong_key_in_production' +DEBUG = True # True for development + +# Database settings +import os +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Cấu hình MySQL +SQLALCHEMY_DATABASE_URI = 'mysql://root:MinhDZ3009@localhost/ccu' + +# Cấu hình SQLite - hiện đang bị comment +# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'instance', 'ccu_htlm.db') + +SQLALCHEMY_TRACK_MODIFICATIONS = False + +# Upload folder +UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'instance', 'uploads') +MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # 10MB upload limit + +# Application settings +APP_NAME = "CCU HTM - Critical Care Unit Patient Management" +ADMIN_EMAIL = "admin@ccuhtm.com" + +# Mail settings (if needed for reports or notifications) +MAIL_SERVER = 'smtp.example.com' +MAIL_PORT = 587 +MAIL_USE_TLS = True +MAIL_USERNAME = 'username' +MAIL_PASSWORD = 'password' +MAIL_DEFAULT_SENDER = 'admin@ccuhtm.com' diff --git a/instance/uploads/20250416150540_patients_data.csv b/instance/uploads/20250416150540_patients_data.csv new file mode 100644 index 0000000000000000000000000000000000000000..fcdcdec0745556220bb3e6897cf464a7c513b793 --- /dev/null +++ b/instance/uploads/20250416150540_patients_data.csv @@ -0,0 +1,51 @@ +patientID,firstName,lastName,age,gender,height,weight,blood_type,measurementDateTime,temperature,heart_rate,blood_pressure_systolic,blood_pressure_diastolic,oxygen_saturation,resp_rate,fio2,tidal_vol,end_tidal_co2,feed_vol,feed_vol_adm,fio2_ratio,insp_time,oxygen_flow_rate,peep,pip,sip,tidal_vol_actual,tidal_vol_kg,tidal_vol_spon,bmi,referral +P-00001,Alexandra,Hobbs,24,female,174.0,62.6,O-,2025-04-16T07:34:02.746051,36.7,75,125,71,92.6,13,21.0,492,39.7,,,,1.1,,,,,455,7.6,,20.7, +P-00002,Sean,Raymond,52,male,178.0,74.9,AB+,2025-04-16T07:34:02.746051,36.7,80,146,93,98.7,13,21.0,534,43.0,,,,1.3,,,,,526,7.3,26,23.6,1 +P-00003,Christina,Garcia,56,female,159.2,52.8,A+,2025-04-16T07:34:02.746051,36.3,70,130,93,97.0,19,33.4,419,37.6,,,0.33,1.3,9,,,,400,8.1,,20.8,1 +P-00004,Emily,Payne,63,female,167.6,66.6,AB+,2025-04-16T07:34:02.746051,36.7,97,151,81,98.0,17,21.0,402,,,,,,,,32,,387,6.8,,23.7,1 +P-00005,Jessica,Taylor,85,female,150.9,60.1,AB-,2025-04-16T07:34:02.746051,37.9,107,148,97,98.7,16,21.0,420,42.8,,,,1.1,,7,28,,412,9.5,22,26.4, +P-00006,Brianna,Jacobs,69,female,152.9,57.9,AB+,2025-04-16T07:34:02.746051,36.9,70,139,103,94.5,14,59.9,364,37.8,,,0.6,1.1,4,,,,345,7.9,48,24.8,1 +P-00007,Kevin,Martin,28,male,161.7,64.6,AB-,2025-04-16T07:34:02.746051,36.1,66,120,80,97.0,12,21.0,444,36.1,,,,1.4,,4,,,401,7.6,,24.7, +P-00008,Cheyenne,York,87,female,157.9,68.8,B-,2025-04-16T07:34:02.746051,36.9,79,125,90,98.8,14,21.0,423,36.5,,,,,,,30,30,397,8.4,28,27.6, +P-00009,Rebecca,Carpenter,49,female,166.8,73.3,AB+,2025-04-16T07:34:02.746051,36.6,50,140,83,94.9,12,21.0,415,36.3,,,,,,,,,403,7.1,,26.3,1 +P-00010,Carl,Bell,53,male,178.4,68.4,O-,2025-04-16T07:34:02.746051,37.2,84,135,89,97.1,16,21.0,520,,,,,1.5,,7,,,513,7.1,,21.5, +P-00011,Rachel,Butler,27,female,167.2,66.2,AB-,2025-04-16T07:34:02.746051,36.5,45,117,80,97.8,18,21.0,428,,,,,,,6,,,411,7.3,77,23.7, +P-00012,Megan,Soto,59,female,168.5,51.3,O+,2025-04-16T07:34:02.746051,36.5,80,138,86,97.5,20,21.0,489,,,,,,,,,,485,8.1,,18.1, +P-00013,Brandon,Horton,63,male,185.0,74.7,B+,2025-04-16T07:34:02.746051,37.4,79,130,81,96.5,20,40.4,387,39.6,,,0.4,0.9,8,,,,377,4.9,,21.8, +P-00014,Mandy,Perry,77,female,163.1,68.3,B-,2025-04-16T07:34:02.747129,37.0,105,132,83,94.9,18,21.0,430,,396,221,,1.5,,,,,402,7.8,,25.7,1 +P-00015,Melissa,Wallace,38,female,159.5,65.2,O+,2025-04-16T07:34:02.747129,37.3,60,113,75,97.0,12,21.0,383,41.1,,,,0.9,,,,,378,7.4,,25.6, +P-00016,Jeffery,Prince,45,male,177.0,65.1,A-,2025-04-16T07:34:02.747129,37.0,69,120,76,96.6,19,21.0,482,,,,,0.9,,8,,,481,6.7,,20.8, +P-00017,Dillon,Perez,24,male,169.6,89.1,O+,2025-04-16T07:34:02.747129,36.8,78,117,77,99.4,18,21.0,409,,473,82,,1.2,,5,29,27,383,6.2,79,31.0, +P-00018,Jessica,Peterson,85,female,168.7,74.5,B-,2025-04-16T07:34:02.747129,37.7,81,138,94,97.6,14,21.0,513,43.9,,,,1.1,,4,,,494,8.5,,26.2,1 +P-00019,Stephen,Wood,86,male,168.9,63.9,O+,2025-04-16T07:34:02.747129,36.9,81,148,91,96.2,13,52.7,365,42.3,,,0.53,1.4,4,8,27,23,331,5.6,,22.4, +P-00020,Jessica,Fields,66,female,159.1,65.8,A-,2025-04-16T07:34:02.747129,36.9,106,140,85,97.4,19,21.0,385,,358,156,,,,,,,385,7.5,,26.0, +P-00021,Kevin,Keller,24,male,174.1,53.5,A+,2025-04-16T07:34:02.747129,37.1,65,114,83,96.3,13,21.0,442,,,,,,,,,,423,6.3,,17.7, +P-00022,Susan,Tucker,42,other,177.0,81.3,A+,2025-04-16T07:34:02.747129,36.7,73,124,83,97.7,16,21.0,396,,,,,,,6,,,361,5.8,77,26.0, +P-00023,Justin,Miller,26,male,176.8,103.4,O+,2025-04-16T07:34:02.747129,37.0,80,123,71,97.3,12,21.0,382,42.1,349,318,,,,5,,,355,5.3,,33.1,1 +P-00024,Andrea,Webster,87,other,158.7,55.9,AB+,2025-04-16T07:34:02.747129,36.4,96,138,91,96.8,14,21.0,506,,248,208,,1.5,,,29,29,460,9.9,,22.2, +P-00025,Matthew,Griffin,58,male,168.1,76.1,AB+,2025-04-16T07:34:02.747129,37.1,84,147,101,96.6,15,21.0,362,40.9,,,,,,,35,30,355,5.6,,26.9, +P-00026,Andrew,Douglas,63,male,180.6,102.1,AB+,2025-04-16T07:34:02.747129,36.4,103,126,85,99.4,12,21.0,372,36.7,228,168,,1.1,,,31,22,346,4.9,,31.3, +P-00027,Anthony,Bruce,19,male,161.8,83.0,A-,2025-04-16T07:34:02.747129,36.2,40,116,81,98.4,17,48.1,397,36.5,287,273,0.48,0.8,7,8,,,388,6.8,,31.7, +P-00028,John,Gray,30,male,183.6,71.7,O+,2025-04-16T07:34:02.747129,36.1,71,113,78,99.0,20,21.0,467,40.5,,,,,,4,,,442,6.0,,21.3,1 +P-00029,Casey,Riley,86,male,179.9,90.6,B-,2025-04-16T07:34:02.747129,36.8,94,144,100,97.3,17,21.0,439,35.6,,,,1.4,,8,29,,434,5.9,,28.0, +P-00030,Jessica,Williams,69,female,171.3,63.3,AB+,2025-04-16T07:34:02.747129,37.0,99,143,91,98.3,14,21.0,369,35.7,,,,,,,,,356,5.9,,21.6, +P-00031,Madison,Bell,56,female,160.6,71.5,O+,2025-04-16T07:34:02.748156,36.3,65,137,95,96.2,16,53.4,468,44.9,,,0.53,,5,5,,,450,8.8,,27.7, +P-00032,Debra,Wilson,78,female,172.5,78.5,AB-,2025-04-16T07:34:02.748156,36.4,88,138,85,97.6,17,21.0,535,35.8,,,,1.2,,5,26,,499,8.4,,26.4, +P-00033,Andrew,Lewis,73,male,170.9,98.3,A+,2025-04-16T07:34:02.748156,38.0,90,142,87,99.3,15,21.0,456,44.2,,,,1.1,,,,,450,6.8,,33.7, +P-00034,Travis,Serrano,79,male,181.1,82.1,O-,2025-04-16T07:34:02.748156,37.1,91,141,85,98.0,14,21.0,537,38.1,,,,0.8,,,,,505,7.1,,25.0, +P-00035,Brian,Munoz,24,male,182.9,91.0,B+,2025-04-16T07:34:02.748156,36.7,53,118,70,97.7,14,28.7,376,43.0,,,0.29,1.4,5,5,,,339,4.8,56,27.2, +P-00036,Kenneth,Henry,37,male,169.8,73.9,B-,2025-04-16T07:34:02.748156,36.1,81,123,80,97.7,18,21.0,522,41.9,499,,,,,8,29,24,478,7.9,67,25.6, +P-00037,Andrea,Castaneda,27,female,154.7,74.5,B+,2025-04-16T07:34:02.748156,36.5,79,112,72,99.3,12,21.0,547,,462,138,,,,,,,510,11.5,70,31.1, +P-00038,Melissa,Riggs,50,female,166.2,64.8,AB-,2025-04-16T07:34:02.748156,37.0,68,137,85,99.2,17,21.0,364,,,,,,,,,,342,6.3,,23.5,1 +P-00039,Juan,Moore,88,other,177.3,73.5,AB+,2025-04-16T07:34:02.748156,36.9,80,131,96,93.2,20,21.0,485,,,,,,,,,,451,7.1,,23.4,1 +P-00040,Monique,Williamson,21,female,170.6,65.4,O-,2025-04-16T07:34:02.748156,36.7,67,121,80,99.3,18,21.0,357,,,,,,,5,,,349,5.8,,22.5, +P-00041,David,Stanley,46,other,173.5,70.9,O-,2025-04-16T07:34:02.748156,37.0,82,113,71,98.4,17,21.0,416,35.6,,,,,,,28,,405,6.4,,23.6, +P-00042,Ryan,Jenkins,19,male,176.1,89.8,A+,2025-04-16T07:34:02.748156,36.3,68,117,76,97.9,13,21.0,462,,,,,1.3,,,28,,459,6.5,,29.0, +P-00043,Kathleen,Jenkins,33,female,171.9,83.4,O-,2025-04-16T07:34:02.748156,36.4,67,125,76,96.4,15,21.0,435,35.7,,,,1.4,,5,,,404,6.9,,28.2, +P-00044,Sabrina,Ross,31,female,152.1,68.0,AB+,2025-04-16T07:34:02.748156,36.4,67,141,78,98.8,20,21.0,478,,,,,,,,26,25,444,10.6,,29.4,1 +P-00045,Michael,Castaneda,22,male,166.7,85.7,AB+,2025-04-16T07:34:02.748156,36.9,80,115,78,98.2,13,21.0,380,38.8,,,,,,,,,357,6.0,,30.8, +P-00046,Angela,Marsh,51,female,166.2,67.8,AB-,2025-04-16T07:34:02.749120,37.1,74,129,91,97.6,12,21.0,511,42.2,,,,1.1,,,,,460,8.8,,24.5, +P-00047,Latoya,Fox,76,other,164.9,55.7,A+,2025-04-16T07:34:02.749120,37.2,98,127,94,98.7,19,21.0,407,37.1,,,,1.2,,,32,32,399,7.2,,20.5,1 +P-00048,Patricia,Friedman,49,female,169.7,61.2,A+,2025-04-16T07:34:02.749120,36.8,40,121,71,99.0,13,21.0,369,35.8,,,,1.5,,,33,,334,6.0,,21.3, +P-00049,Adrian,Paul,22,male,170.9,92.7,O-,2025-04-16T07:34:02.749120,36.9,73,117,73,95.6,17,33.9,467,42.5,,,0.34,,6,,29,23,452,7.0,,31.7, +P-00050,Matthew,Morgan,67,male,187.4,90.1,O+,2025-04-16T07:34:02.749120,37.1,45,143,93,99.3,19,56.3,440,,,,0.56,,10,5,29,,409,5.4,,25.7,1 diff --git a/instance/uploads/20250416150623_patients_data.csv b/instance/uploads/20250416150623_patients_data.csv new file mode 100644 index 0000000000000000000000000000000000000000..fcdcdec0745556220bb3e6897cf464a7c513b793 --- /dev/null +++ b/instance/uploads/20250416150623_patients_data.csv @@ -0,0 +1,51 @@ +patientID,firstName,lastName,age,gender,height,weight,blood_type,measurementDateTime,temperature,heart_rate,blood_pressure_systolic,blood_pressure_diastolic,oxygen_saturation,resp_rate,fio2,tidal_vol,end_tidal_co2,feed_vol,feed_vol_adm,fio2_ratio,insp_time,oxygen_flow_rate,peep,pip,sip,tidal_vol_actual,tidal_vol_kg,tidal_vol_spon,bmi,referral +P-00001,Alexandra,Hobbs,24,female,174.0,62.6,O-,2025-04-16T07:34:02.746051,36.7,75,125,71,92.6,13,21.0,492,39.7,,,,1.1,,,,,455,7.6,,20.7, +P-00002,Sean,Raymond,52,male,178.0,74.9,AB+,2025-04-16T07:34:02.746051,36.7,80,146,93,98.7,13,21.0,534,43.0,,,,1.3,,,,,526,7.3,26,23.6,1 +P-00003,Christina,Garcia,56,female,159.2,52.8,A+,2025-04-16T07:34:02.746051,36.3,70,130,93,97.0,19,33.4,419,37.6,,,0.33,1.3,9,,,,400,8.1,,20.8,1 +P-00004,Emily,Payne,63,female,167.6,66.6,AB+,2025-04-16T07:34:02.746051,36.7,97,151,81,98.0,17,21.0,402,,,,,,,,32,,387,6.8,,23.7,1 +P-00005,Jessica,Taylor,85,female,150.9,60.1,AB-,2025-04-16T07:34:02.746051,37.9,107,148,97,98.7,16,21.0,420,42.8,,,,1.1,,7,28,,412,9.5,22,26.4, +P-00006,Brianna,Jacobs,69,female,152.9,57.9,AB+,2025-04-16T07:34:02.746051,36.9,70,139,103,94.5,14,59.9,364,37.8,,,0.6,1.1,4,,,,345,7.9,48,24.8,1 +P-00007,Kevin,Martin,28,male,161.7,64.6,AB-,2025-04-16T07:34:02.746051,36.1,66,120,80,97.0,12,21.0,444,36.1,,,,1.4,,4,,,401,7.6,,24.7, +P-00008,Cheyenne,York,87,female,157.9,68.8,B-,2025-04-16T07:34:02.746051,36.9,79,125,90,98.8,14,21.0,423,36.5,,,,,,,30,30,397,8.4,28,27.6, +P-00009,Rebecca,Carpenter,49,female,166.8,73.3,AB+,2025-04-16T07:34:02.746051,36.6,50,140,83,94.9,12,21.0,415,36.3,,,,,,,,,403,7.1,,26.3,1 +P-00010,Carl,Bell,53,male,178.4,68.4,O-,2025-04-16T07:34:02.746051,37.2,84,135,89,97.1,16,21.0,520,,,,,1.5,,7,,,513,7.1,,21.5, +P-00011,Rachel,Butler,27,female,167.2,66.2,AB-,2025-04-16T07:34:02.746051,36.5,45,117,80,97.8,18,21.0,428,,,,,,,6,,,411,7.3,77,23.7, +P-00012,Megan,Soto,59,female,168.5,51.3,O+,2025-04-16T07:34:02.746051,36.5,80,138,86,97.5,20,21.0,489,,,,,,,,,,485,8.1,,18.1, +P-00013,Brandon,Horton,63,male,185.0,74.7,B+,2025-04-16T07:34:02.746051,37.4,79,130,81,96.5,20,40.4,387,39.6,,,0.4,0.9,8,,,,377,4.9,,21.8, +P-00014,Mandy,Perry,77,female,163.1,68.3,B-,2025-04-16T07:34:02.747129,37.0,105,132,83,94.9,18,21.0,430,,396,221,,1.5,,,,,402,7.8,,25.7,1 +P-00015,Melissa,Wallace,38,female,159.5,65.2,O+,2025-04-16T07:34:02.747129,37.3,60,113,75,97.0,12,21.0,383,41.1,,,,0.9,,,,,378,7.4,,25.6, +P-00016,Jeffery,Prince,45,male,177.0,65.1,A-,2025-04-16T07:34:02.747129,37.0,69,120,76,96.6,19,21.0,482,,,,,0.9,,8,,,481,6.7,,20.8, +P-00017,Dillon,Perez,24,male,169.6,89.1,O+,2025-04-16T07:34:02.747129,36.8,78,117,77,99.4,18,21.0,409,,473,82,,1.2,,5,29,27,383,6.2,79,31.0, +P-00018,Jessica,Peterson,85,female,168.7,74.5,B-,2025-04-16T07:34:02.747129,37.7,81,138,94,97.6,14,21.0,513,43.9,,,,1.1,,4,,,494,8.5,,26.2,1 +P-00019,Stephen,Wood,86,male,168.9,63.9,O+,2025-04-16T07:34:02.747129,36.9,81,148,91,96.2,13,52.7,365,42.3,,,0.53,1.4,4,8,27,23,331,5.6,,22.4, +P-00020,Jessica,Fields,66,female,159.1,65.8,A-,2025-04-16T07:34:02.747129,36.9,106,140,85,97.4,19,21.0,385,,358,156,,,,,,,385,7.5,,26.0, +P-00021,Kevin,Keller,24,male,174.1,53.5,A+,2025-04-16T07:34:02.747129,37.1,65,114,83,96.3,13,21.0,442,,,,,,,,,,423,6.3,,17.7, +P-00022,Susan,Tucker,42,other,177.0,81.3,A+,2025-04-16T07:34:02.747129,36.7,73,124,83,97.7,16,21.0,396,,,,,,,6,,,361,5.8,77,26.0, +P-00023,Justin,Miller,26,male,176.8,103.4,O+,2025-04-16T07:34:02.747129,37.0,80,123,71,97.3,12,21.0,382,42.1,349,318,,,,5,,,355,5.3,,33.1,1 +P-00024,Andrea,Webster,87,other,158.7,55.9,AB+,2025-04-16T07:34:02.747129,36.4,96,138,91,96.8,14,21.0,506,,248,208,,1.5,,,29,29,460,9.9,,22.2, +P-00025,Matthew,Griffin,58,male,168.1,76.1,AB+,2025-04-16T07:34:02.747129,37.1,84,147,101,96.6,15,21.0,362,40.9,,,,,,,35,30,355,5.6,,26.9, +P-00026,Andrew,Douglas,63,male,180.6,102.1,AB+,2025-04-16T07:34:02.747129,36.4,103,126,85,99.4,12,21.0,372,36.7,228,168,,1.1,,,31,22,346,4.9,,31.3, +P-00027,Anthony,Bruce,19,male,161.8,83.0,A-,2025-04-16T07:34:02.747129,36.2,40,116,81,98.4,17,48.1,397,36.5,287,273,0.48,0.8,7,8,,,388,6.8,,31.7, +P-00028,John,Gray,30,male,183.6,71.7,O+,2025-04-16T07:34:02.747129,36.1,71,113,78,99.0,20,21.0,467,40.5,,,,,,4,,,442,6.0,,21.3,1 +P-00029,Casey,Riley,86,male,179.9,90.6,B-,2025-04-16T07:34:02.747129,36.8,94,144,100,97.3,17,21.0,439,35.6,,,,1.4,,8,29,,434,5.9,,28.0, +P-00030,Jessica,Williams,69,female,171.3,63.3,AB+,2025-04-16T07:34:02.747129,37.0,99,143,91,98.3,14,21.0,369,35.7,,,,,,,,,356,5.9,,21.6, +P-00031,Madison,Bell,56,female,160.6,71.5,O+,2025-04-16T07:34:02.748156,36.3,65,137,95,96.2,16,53.4,468,44.9,,,0.53,,5,5,,,450,8.8,,27.7, +P-00032,Debra,Wilson,78,female,172.5,78.5,AB-,2025-04-16T07:34:02.748156,36.4,88,138,85,97.6,17,21.0,535,35.8,,,,1.2,,5,26,,499,8.4,,26.4, +P-00033,Andrew,Lewis,73,male,170.9,98.3,A+,2025-04-16T07:34:02.748156,38.0,90,142,87,99.3,15,21.0,456,44.2,,,,1.1,,,,,450,6.8,,33.7, +P-00034,Travis,Serrano,79,male,181.1,82.1,O-,2025-04-16T07:34:02.748156,37.1,91,141,85,98.0,14,21.0,537,38.1,,,,0.8,,,,,505,7.1,,25.0, +P-00035,Brian,Munoz,24,male,182.9,91.0,B+,2025-04-16T07:34:02.748156,36.7,53,118,70,97.7,14,28.7,376,43.0,,,0.29,1.4,5,5,,,339,4.8,56,27.2, +P-00036,Kenneth,Henry,37,male,169.8,73.9,B-,2025-04-16T07:34:02.748156,36.1,81,123,80,97.7,18,21.0,522,41.9,499,,,,,8,29,24,478,7.9,67,25.6, +P-00037,Andrea,Castaneda,27,female,154.7,74.5,B+,2025-04-16T07:34:02.748156,36.5,79,112,72,99.3,12,21.0,547,,462,138,,,,,,,510,11.5,70,31.1, +P-00038,Melissa,Riggs,50,female,166.2,64.8,AB-,2025-04-16T07:34:02.748156,37.0,68,137,85,99.2,17,21.0,364,,,,,,,,,,342,6.3,,23.5,1 +P-00039,Juan,Moore,88,other,177.3,73.5,AB+,2025-04-16T07:34:02.748156,36.9,80,131,96,93.2,20,21.0,485,,,,,,,,,,451,7.1,,23.4,1 +P-00040,Monique,Williamson,21,female,170.6,65.4,O-,2025-04-16T07:34:02.748156,36.7,67,121,80,99.3,18,21.0,357,,,,,,,5,,,349,5.8,,22.5, +P-00041,David,Stanley,46,other,173.5,70.9,O-,2025-04-16T07:34:02.748156,37.0,82,113,71,98.4,17,21.0,416,35.6,,,,,,,28,,405,6.4,,23.6, +P-00042,Ryan,Jenkins,19,male,176.1,89.8,A+,2025-04-16T07:34:02.748156,36.3,68,117,76,97.9,13,21.0,462,,,,,1.3,,,28,,459,6.5,,29.0, +P-00043,Kathleen,Jenkins,33,female,171.9,83.4,O-,2025-04-16T07:34:02.748156,36.4,67,125,76,96.4,15,21.0,435,35.7,,,,1.4,,5,,,404,6.9,,28.2, +P-00044,Sabrina,Ross,31,female,152.1,68.0,AB+,2025-04-16T07:34:02.748156,36.4,67,141,78,98.8,20,21.0,478,,,,,,,,26,25,444,10.6,,29.4,1 +P-00045,Michael,Castaneda,22,male,166.7,85.7,AB+,2025-04-16T07:34:02.748156,36.9,80,115,78,98.2,13,21.0,380,38.8,,,,,,,,,357,6.0,,30.8, +P-00046,Angela,Marsh,51,female,166.2,67.8,AB-,2025-04-16T07:34:02.749120,37.1,74,129,91,97.6,12,21.0,511,42.2,,,,1.1,,,,,460,8.8,,24.5, +P-00047,Latoya,Fox,76,other,164.9,55.7,A+,2025-04-16T07:34:02.749120,37.2,98,127,94,98.7,19,21.0,407,37.1,,,,1.2,,,32,32,399,7.2,,20.5,1 +P-00048,Patricia,Friedman,49,female,169.7,61.2,A+,2025-04-16T07:34:02.749120,36.8,40,121,71,99.0,13,21.0,369,35.8,,,,1.5,,,33,,334,6.0,,21.3, +P-00049,Adrian,Paul,22,male,170.9,92.7,O-,2025-04-16T07:34:02.749120,36.9,73,117,73,95.6,17,33.9,467,42.5,,,0.34,,6,,29,23,452,7.0,,31.7, +P-00050,Matthew,Morgan,67,male,187.4,90.1,O+,2025-04-16T07:34:02.749120,37.1,45,143,93,99.3,19,56.3,440,,,,0.56,,10,5,29,,409,5.4,,25.7,1 diff --git a/instance/uploads/20250416152626_patients_data.csv b/instance/uploads/20250416152626_patients_data.csv new file mode 100644 index 0000000000000000000000000000000000000000..fcdcdec0745556220bb3e6897cf464a7c513b793 --- /dev/null +++ b/instance/uploads/20250416152626_patients_data.csv @@ -0,0 +1,51 @@ +patientID,firstName,lastName,age,gender,height,weight,blood_type,measurementDateTime,temperature,heart_rate,blood_pressure_systolic,blood_pressure_diastolic,oxygen_saturation,resp_rate,fio2,tidal_vol,end_tidal_co2,feed_vol,feed_vol_adm,fio2_ratio,insp_time,oxygen_flow_rate,peep,pip,sip,tidal_vol_actual,tidal_vol_kg,tidal_vol_spon,bmi,referral +P-00001,Alexandra,Hobbs,24,female,174.0,62.6,O-,2025-04-16T07:34:02.746051,36.7,75,125,71,92.6,13,21.0,492,39.7,,,,1.1,,,,,455,7.6,,20.7, +P-00002,Sean,Raymond,52,male,178.0,74.9,AB+,2025-04-16T07:34:02.746051,36.7,80,146,93,98.7,13,21.0,534,43.0,,,,1.3,,,,,526,7.3,26,23.6,1 +P-00003,Christina,Garcia,56,female,159.2,52.8,A+,2025-04-16T07:34:02.746051,36.3,70,130,93,97.0,19,33.4,419,37.6,,,0.33,1.3,9,,,,400,8.1,,20.8,1 +P-00004,Emily,Payne,63,female,167.6,66.6,AB+,2025-04-16T07:34:02.746051,36.7,97,151,81,98.0,17,21.0,402,,,,,,,,32,,387,6.8,,23.7,1 +P-00005,Jessica,Taylor,85,female,150.9,60.1,AB-,2025-04-16T07:34:02.746051,37.9,107,148,97,98.7,16,21.0,420,42.8,,,,1.1,,7,28,,412,9.5,22,26.4, +P-00006,Brianna,Jacobs,69,female,152.9,57.9,AB+,2025-04-16T07:34:02.746051,36.9,70,139,103,94.5,14,59.9,364,37.8,,,0.6,1.1,4,,,,345,7.9,48,24.8,1 +P-00007,Kevin,Martin,28,male,161.7,64.6,AB-,2025-04-16T07:34:02.746051,36.1,66,120,80,97.0,12,21.0,444,36.1,,,,1.4,,4,,,401,7.6,,24.7, +P-00008,Cheyenne,York,87,female,157.9,68.8,B-,2025-04-16T07:34:02.746051,36.9,79,125,90,98.8,14,21.0,423,36.5,,,,,,,30,30,397,8.4,28,27.6, +P-00009,Rebecca,Carpenter,49,female,166.8,73.3,AB+,2025-04-16T07:34:02.746051,36.6,50,140,83,94.9,12,21.0,415,36.3,,,,,,,,,403,7.1,,26.3,1 +P-00010,Carl,Bell,53,male,178.4,68.4,O-,2025-04-16T07:34:02.746051,37.2,84,135,89,97.1,16,21.0,520,,,,,1.5,,7,,,513,7.1,,21.5, +P-00011,Rachel,Butler,27,female,167.2,66.2,AB-,2025-04-16T07:34:02.746051,36.5,45,117,80,97.8,18,21.0,428,,,,,,,6,,,411,7.3,77,23.7, +P-00012,Megan,Soto,59,female,168.5,51.3,O+,2025-04-16T07:34:02.746051,36.5,80,138,86,97.5,20,21.0,489,,,,,,,,,,485,8.1,,18.1, +P-00013,Brandon,Horton,63,male,185.0,74.7,B+,2025-04-16T07:34:02.746051,37.4,79,130,81,96.5,20,40.4,387,39.6,,,0.4,0.9,8,,,,377,4.9,,21.8, +P-00014,Mandy,Perry,77,female,163.1,68.3,B-,2025-04-16T07:34:02.747129,37.0,105,132,83,94.9,18,21.0,430,,396,221,,1.5,,,,,402,7.8,,25.7,1 +P-00015,Melissa,Wallace,38,female,159.5,65.2,O+,2025-04-16T07:34:02.747129,37.3,60,113,75,97.0,12,21.0,383,41.1,,,,0.9,,,,,378,7.4,,25.6, +P-00016,Jeffery,Prince,45,male,177.0,65.1,A-,2025-04-16T07:34:02.747129,37.0,69,120,76,96.6,19,21.0,482,,,,,0.9,,8,,,481,6.7,,20.8, +P-00017,Dillon,Perez,24,male,169.6,89.1,O+,2025-04-16T07:34:02.747129,36.8,78,117,77,99.4,18,21.0,409,,473,82,,1.2,,5,29,27,383,6.2,79,31.0, +P-00018,Jessica,Peterson,85,female,168.7,74.5,B-,2025-04-16T07:34:02.747129,37.7,81,138,94,97.6,14,21.0,513,43.9,,,,1.1,,4,,,494,8.5,,26.2,1 +P-00019,Stephen,Wood,86,male,168.9,63.9,O+,2025-04-16T07:34:02.747129,36.9,81,148,91,96.2,13,52.7,365,42.3,,,0.53,1.4,4,8,27,23,331,5.6,,22.4, +P-00020,Jessica,Fields,66,female,159.1,65.8,A-,2025-04-16T07:34:02.747129,36.9,106,140,85,97.4,19,21.0,385,,358,156,,,,,,,385,7.5,,26.0, +P-00021,Kevin,Keller,24,male,174.1,53.5,A+,2025-04-16T07:34:02.747129,37.1,65,114,83,96.3,13,21.0,442,,,,,,,,,,423,6.3,,17.7, +P-00022,Susan,Tucker,42,other,177.0,81.3,A+,2025-04-16T07:34:02.747129,36.7,73,124,83,97.7,16,21.0,396,,,,,,,6,,,361,5.8,77,26.0, +P-00023,Justin,Miller,26,male,176.8,103.4,O+,2025-04-16T07:34:02.747129,37.0,80,123,71,97.3,12,21.0,382,42.1,349,318,,,,5,,,355,5.3,,33.1,1 +P-00024,Andrea,Webster,87,other,158.7,55.9,AB+,2025-04-16T07:34:02.747129,36.4,96,138,91,96.8,14,21.0,506,,248,208,,1.5,,,29,29,460,9.9,,22.2, +P-00025,Matthew,Griffin,58,male,168.1,76.1,AB+,2025-04-16T07:34:02.747129,37.1,84,147,101,96.6,15,21.0,362,40.9,,,,,,,35,30,355,5.6,,26.9, +P-00026,Andrew,Douglas,63,male,180.6,102.1,AB+,2025-04-16T07:34:02.747129,36.4,103,126,85,99.4,12,21.0,372,36.7,228,168,,1.1,,,31,22,346,4.9,,31.3, +P-00027,Anthony,Bruce,19,male,161.8,83.0,A-,2025-04-16T07:34:02.747129,36.2,40,116,81,98.4,17,48.1,397,36.5,287,273,0.48,0.8,7,8,,,388,6.8,,31.7, +P-00028,John,Gray,30,male,183.6,71.7,O+,2025-04-16T07:34:02.747129,36.1,71,113,78,99.0,20,21.0,467,40.5,,,,,,4,,,442,6.0,,21.3,1 +P-00029,Casey,Riley,86,male,179.9,90.6,B-,2025-04-16T07:34:02.747129,36.8,94,144,100,97.3,17,21.0,439,35.6,,,,1.4,,8,29,,434,5.9,,28.0, +P-00030,Jessica,Williams,69,female,171.3,63.3,AB+,2025-04-16T07:34:02.747129,37.0,99,143,91,98.3,14,21.0,369,35.7,,,,,,,,,356,5.9,,21.6, +P-00031,Madison,Bell,56,female,160.6,71.5,O+,2025-04-16T07:34:02.748156,36.3,65,137,95,96.2,16,53.4,468,44.9,,,0.53,,5,5,,,450,8.8,,27.7, +P-00032,Debra,Wilson,78,female,172.5,78.5,AB-,2025-04-16T07:34:02.748156,36.4,88,138,85,97.6,17,21.0,535,35.8,,,,1.2,,5,26,,499,8.4,,26.4, +P-00033,Andrew,Lewis,73,male,170.9,98.3,A+,2025-04-16T07:34:02.748156,38.0,90,142,87,99.3,15,21.0,456,44.2,,,,1.1,,,,,450,6.8,,33.7, +P-00034,Travis,Serrano,79,male,181.1,82.1,O-,2025-04-16T07:34:02.748156,37.1,91,141,85,98.0,14,21.0,537,38.1,,,,0.8,,,,,505,7.1,,25.0, +P-00035,Brian,Munoz,24,male,182.9,91.0,B+,2025-04-16T07:34:02.748156,36.7,53,118,70,97.7,14,28.7,376,43.0,,,0.29,1.4,5,5,,,339,4.8,56,27.2, +P-00036,Kenneth,Henry,37,male,169.8,73.9,B-,2025-04-16T07:34:02.748156,36.1,81,123,80,97.7,18,21.0,522,41.9,499,,,,,8,29,24,478,7.9,67,25.6, +P-00037,Andrea,Castaneda,27,female,154.7,74.5,B+,2025-04-16T07:34:02.748156,36.5,79,112,72,99.3,12,21.0,547,,462,138,,,,,,,510,11.5,70,31.1, +P-00038,Melissa,Riggs,50,female,166.2,64.8,AB-,2025-04-16T07:34:02.748156,37.0,68,137,85,99.2,17,21.0,364,,,,,,,,,,342,6.3,,23.5,1 +P-00039,Juan,Moore,88,other,177.3,73.5,AB+,2025-04-16T07:34:02.748156,36.9,80,131,96,93.2,20,21.0,485,,,,,,,,,,451,7.1,,23.4,1 +P-00040,Monique,Williamson,21,female,170.6,65.4,O-,2025-04-16T07:34:02.748156,36.7,67,121,80,99.3,18,21.0,357,,,,,,,5,,,349,5.8,,22.5, +P-00041,David,Stanley,46,other,173.5,70.9,O-,2025-04-16T07:34:02.748156,37.0,82,113,71,98.4,17,21.0,416,35.6,,,,,,,28,,405,6.4,,23.6, +P-00042,Ryan,Jenkins,19,male,176.1,89.8,A+,2025-04-16T07:34:02.748156,36.3,68,117,76,97.9,13,21.0,462,,,,,1.3,,,28,,459,6.5,,29.0, +P-00043,Kathleen,Jenkins,33,female,171.9,83.4,O-,2025-04-16T07:34:02.748156,36.4,67,125,76,96.4,15,21.0,435,35.7,,,,1.4,,5,,,404,6.9,,28.2, +P-00044,Sabrina,Ross,31,female,152.1,68.0,AB+,2025-04-16T07:34:02.748156,36.4,67,141,78,98.8,20,21.0,478,,,,,,,,26,25,444,10.6,,29.4,1 +P-00045,Michael,Castaneda,22,male,166.7,85.7,AB+,2025-04-16T07:34:02.748156,36.9,80,115,78,98.2,13,21.0,380,38.8,,,,,,,,,357,6.0,,30.8, +P-00046,Angela,Marsh,51,female,166.2,67.8,AB-,2025-04-16T07:34:02.749120,37.1,74,129,91,97.6,12,21.0,511,42.2,,,,1.1,,,,,460,8.8,,24.5, +P-00047,Latoya,Fox,76,other,164.9,55.7,A+,2025-04-16T07:34:02.749120,37.2,98,127,94,98.7,19,21.0,407,37.1,,,,1.2,,,32,32,399,7.2,,20.5,1 +P-00048,Patricia,Friedman,49,female,169.7,61.2,A+,2025-04-16T07:34:02.749120,36.8,40,121,71,99.0,13,21.0,369,35.8,,,,1.5,,,33,,334,6.0,,21.3, +P-00049,Adrian,Paul,22,male,170.9,92.7,O-,2025-04-16T07:34:02.749120,36.9,73,117,73,95.6,17,33.9,467,42.5,,,0.34,,6,,29,23,452,7.0,,31.7, +P-00050,Matthew,Morgan,67,male,187.4,90.1,O+,2025-04-16T07:34:02.749120,37.1,45,143,93,99.3,19,56.3,440,,,,0.56,,10,5,29,,409,5.4,,25.7,1 diff --git "a/migrations/versions/33d355767436_thay_\304\221\341\273\225i_\304\221\341\273\231_d\303\240i_c\341\273\231t_status_trong_b\341\272\243ng_.py" "b/migrations/versions/33d355767436_thay_\304\221\341\273\225i_\304\221\341\273\231_d\303\240i_c\341\273\231t_status_trong_b\341\272\243ng_.py" new file mode 100644 index 0000000000000000000000000000000000000000..d7527f1db477a4fa36f164f651a65d1b54870b38 --- /dev/null +++ "b/migrations/versions/33d355767436_thay_\304\221\341\273\225i_\304\221\341\273\231_d\303\240i_c\341\273\231t_status_trong_b\341\272\243ng_.py" @@ -0,0 +1,46 @@ +"""Thay đổi độ dài cột status trong bảng uploadedfiles + +Revision ID: 33d355767436 +Revises: 78006a2a15a0 +Create Date: 2025-04-16 18:32:35.386926 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '33d355767436' +down_revision = '78006a2a15a0' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('uploadedfiles', schema=None) as batch_op: + batch_op.alter_column('status', + existing_type=mysql.VARCHAR(length=20), + type_=sa.String(length=50), + existing_nullable=True) + batch_op.alter_column('error_details', + existing_type=mysql.LONGTEXT(), + 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(), + existing_nullable=True) + batch_op.alter_column('status', + existing_type=sa.String(length=50), + type_=mysql.VARCHAR(length=20), + existing_nullable=True) + + # ### end Alembic commands ### diff --git "a/migrations/versions/78006a2a15a0_t\341\272\241o_l\341\272\241i_migration.py" "b/migrations/versions/78006a2a15a0_t\341\272\241o_l\341\272\241i_migration.py" new file mode 100644 index 0000000000000000000000000000000000000000..9500b5951dfd3c724c811cef249c9d64353430e5 --- /dev/null +++ "b/migrations/versions/78006a2a15a0_t\341\272\241o_l\341\272\241i_migration.py" @@ -0,0 +1,38 @@ +"""Tạo lại migration + +Revision ID: 78006a2a15a0 +Revises: +Create Date: 2025-04-16 18:25:19.976243 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '78006a2a15a0' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### 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=mysql.LONGTEXT(), + 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(), + existing_nullable=True) + + # ### end Alembic commands ### diff --git a/migrations/versions/cbc9db9dd958_update_database_schema_to_match_models.py b/migrations/versions/cbc9db9dd958_update_database_schema_to_match_models.py deleted file mode 100644 index 929010b2abd919bb618b473a30207b5fbac5e3b9..0000000000000000000000000000000000000000 --- a/migrations/versions/cbc9db9dd958_update_database_schema_to_match_models.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Update database schema to match models - -Revision ID: cbc9db9dd958 -Revises: -Create Date: 2025-04-13 19:06:46.600555 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = 'cbc9db9dd958' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('activity_logs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('action', sa.String(length=255), nullable=False), - sa.Column('details', sa.Text(), nullable=True), - sa.Column('timestamp', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.userID'], ), - sa.PrimaryKeyConstraint('id') - ) - with op.batch_alter_table('users', schema=None) as batch_op: - batch_op.add_column(sa.Column('firstName', sa.String(length=50), nullable=False)) - batch_op.add_column(sa.Column('lastName', sa.String(length=50), nullable=False)) - batch_op.add_column(sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True)) - batch_op.add_column(sa.Column('last_login', sa.DateTime(timezone=True), nullable=True)) - batch_op.alter_column('password_hash', - existing_type=mysql.VARCHAR(length=255), - type_=sa.String(length=128), - nullable=True) - batch_op.drop_index('username') - batch_op.drop_column('username') - - # ### 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.add_column(sa.Column('username', mysql.VARCHAR(length=50), nullable=False)) - batch_op.create_index('username', ['username'], unique=True) - batch_op.alter_column('password_hash', - existing_type=sa.String(length=128), - type_=mysql.VARCHAR(length=255), - nullable=False) - batch_op.drop_column('last_login') - batch_op.drop_column('created_at') - batch_op.drop_column('lastName') - batch_op.drop_column('firstName') - - op.drop_table('activity_logs') - # ### end Alembic commands ### diff --git a/patients_data.csv b/patients_data.csv new file mode 100644 index 0000000000000000000000000000000000000000..fcdcdec0745556220bb3e6897cf464a7c513b793 --- /dev/null +++ b/patients_data.csv @@ -0,0 +1,51 @@ +patientID,firstName,lastName,age,gender,height,weight,blood_type,measurementDateTime,temperature,heart_rate,blood_pressure_systolic,blood_pressure_diastolic,oxygen_saturation,resp_rate,fio2,tidal_vol,end_tidal_co2,feed_vol,feed_vol_adm,fio2_ratio,insp_time,oxygen_flow_rate,peep,pip,sip,tidal_vol_actual,tidal_vol_kg,tidal_vol_spon,bmi,referral +P-00001,Alexandra,Hobbs,24,female,174.0,62.6,O-,2025-04-16T07:34:02.746051,36.7,75,125,71,92.6,13,21.0,492,39.7,,,,1.1,,,,,455,7.6,,20.7, +P-00002,Sean,Raymond,52,male,178.0,74.9,AB+,2025-04-16T07:34:02.746051,36.7,80,146,93,98.7,13,21.0,534,43.0,,,,1.3,,,,,526,7.3,26,23.6,1 +P-00003,Christina,Garcia,56,female,159.2,52.8,A+,2025-04-16T07:34:02.746051,36.3,70,130,93,97.0,19,33.4,419,37.6,,,0.33,1.3,9,,,,400,8.1,,20.8,1 +P-00004,Emily,Payne,63,female,167.6,66.6,AB+,2025-04-16T07:34:02.746051,36.7,97,151,81,98.0,17,21.0,402,,,,,,,,32,,387,6.8,,23.7,1 +P-00005,Jessica,Taylor,85,female,150.9,60.1,AB-,2025-04-16T07:34:02.746051,37.9,107,148,97,98.7,16,21.0,420,42.8,,,,1.1,,7,28,,412,9.5,22,26.4, +P-00006,Brianna,Jacobs,69,female,152.9,57.9,AB+,2025-04-16T07:34:02.746051,36.9,70,139,103,94.5,14,59.9,364,37.8,,,0.6,1.1,4,,,,345,7.9,48,24.8,1 +P-00007,Kevin,Martin,28,male,161.7,64.6,AB-,2025-04-16T07:34:02.746051,36.1,66,120,80,97.0,12,21.0,444,36.1,,,,1.4,,4,,,401,7.6,,24.7, +P-00008,Cheyenne,York,87,female,157.9,68.8,B-,2025-04-16T07:34:02.746051,36.9,79,125,90,98.8,14,21.0,423,36.5,,,,,,,30,30,397,8.4,28,27.6, +P-00009,Rebecca,Carpenter,49,female,166.8,73.3,AB+,2025-04-16T07:34:02.746051,36.6,50,140,83,94.9,12,21.0,415,36.3,,,,,,,,,403,7.1,,26.3,1 +P-00010,Carl,Bell,53,male,178.4,68.4,O-,2025-04-16T07:34:02.746051,37.2,84,135,89,97.1,16,21.0,520,,,,,1.5,,7,,,513,7.1,,21.5, +P-00011,Rachel,Butler,27,female,167.2,66.2,AB-,2025-04-16T07:34:02.746051,36.5,45,117,80,97.8,18,21.0,428,,,,,,,6,,,411,7.3,77,23.7, +P-00012,Megan,Soto,59,female,168.5,51.3,O+,2025-04-16T07:34:02.746051,36.5,80,138,86,97.5,20,21.0,489,,,,,,,,,,485,8.1,,18.1, +P-00013,Brandon,Horton,63,male,185.0,74.7,B+,2025-04-16T07:34:02.746051,37.4,79,130,81,96.5,20,40.4,387,39.6,,,0.4,0.9,8,,,,377,4.9,,21.8, +P-00014,Mandy,Perry,77,female,163.1,68.3,B-,2025-04-16T07:34:02.747129,37.0,105,132,83,94.9,18,21.0,430,,396,221,,1.5,,,,,402,7.8,,25.7,1 +P-00015,Melissa,Wallace,38,female,159.5,65.2,O+,2025-04-16T07:34:02.747129,37.3,60,113,75,97.0,12,21.0,383,41.1,,,,0.9,,,,,378,7.4,,25.6, +P-00016,Jeffery,Prince,45,male,177.0,65.1,A-,2025-04-16T07:34:02.747129,37.0,69,120,76,96.6,19,21.0,482,,,,,0.9,,8,,,481,6.7,,20.8, +P-00017,Dillon,Perez,24,male,169.6,89.1,O+,2025-04-16T07:34:02.747129,36.8,78,117,77,99.4,18,21.0,409,,473,82,,1.2,,5,29,27,383,6.2,79,31.0, +P-00018,Jessica,Peterson,85,female,168.7,74.5,B-,2025-04-16T07:34:02.747129,37.7,81,138,94,97.6,14,21.0,513,43.9,,,,1.1,,4,,,494,8.5,,26.2,1 +P-00019,Stephen,Wood,86,male,168.9,63.9,O+,2025-04-16T07:34:02.747129,36.9,81,148,91,96.2,13,52.7,365,42.3,,,0.53,1.4,4,8,27,23,331,5.6,,22.4, +P-00020,Jessica,Fields,66,female,159.1,65.8,A-,2025-04-16T07:34:02.747129,36.9,106,140,85,97.4,19,21.0,385,,358,156,,,,,,,385,7.5,,26.0, +P-00021,Kevin,Keller,24,male,174.1,53.5,A+,2025-04-16T07:34:02.747129,37.1,65,114,83,96.3,13,21.0,442,,,,,,,,,,423,6.3,,17.7, +P-00022,Susan,Tucker,42,other,177.0,81.3,A+,2025-04-16T07:34:02.747129,36.7,73,124,83,97.7,16,21.0,396,,,,,,,6,,,361,5.8,77,26.0, +P-00023,Justin,Miller,26,male,176.8,103.4,O+,2025-04-16T07:34:02.747129,37.0,80,123,71,97.3,12,21.0,382,42.1,349,318,,,,5,,,355,5.3,,33.1,1 +P-00024,Andrea,Webster,87,other,158.7,55.9,AB+,2025-04-16T07:34:02.747129,36.4,96,138,91,96.8,14,21.0,506,,248,208,,1.5,,,29,29,460,9.9,,22.2, +P-00025,Matthew,Griffin,58,male,168.1,76.1,AB+,2025-04-16T07:34:02.747129,37.1,84,147,101,96.6,15,21.0,362,40.9,,,,,,,35,30,355,5.6,,26.9, +P-00026,Andrew,Douglas,63,male,180.6,102.1,AB+,2025-04-16T07:34:02.747129,36.4,103,126,85,99.4,12,21.0,372,36.7,228,168,,1.1,,,31,22,346,4.9,,31.3, +P-00027,Anthony,Bruce,19,male,161.8,83.0,A-,2025-04-16T07:34:02.747129,36.2,40,116,81,98.4,17,48.1,397,36.5,287,273,0.48,0.8,7,8,,,388,6.8,,31.7, +P-00028,John,Gray,30,male,183.6,71.7,O+,2025-04-16T07:34:02.747129,36.1,71,113,78,99.0,20,21.0,467,40.5,,,,,,4,,,442,6.0,,21.3,1 +P-00029,Casey,Riley,86,male,179.9,90.6,B-,2025-04-16T07:34:02.747129,36.8,94,144,100,97.3,17,21.0,439,35.6,,,,1.4,,8,29,,434,5.9,,28.0, +P-00030,Jessica,Williams,69,female,171.3,63.3,AB+,2025-04-16T07:34:02.747129,37.0,99,143,91,98.3,14,21.0,369,35.7,,,,,,,,,356,5.9,,21.6, +P-00031,Madison,Bell,56,female,160.6,71.5,O+,2025-04-16T07:34:02.748156,36.3,65,137,95,96.2,16,53.4,468,44.9,,,0.53,,5,5,,,450,8.8,,27.7, +P-00032,Debra,Wilson,78,female,172.5,78.5,AB-,2025-04-16T07:34:02.748156,36.4,88,138,85,97.6,17,21.0,535,35.8,,,,1.2,,5,26,,499,8.4,,26.4, +P-00033,Andrew,Lewis,73,male,170.9,98.3,A+,2025-04-16T07:34:02.748156,38.0,90,142,87,99.3,15,21.0,456,44.2,,,,1.1,,,,,450,6.8,,33.7, +P-00034,Travis,Serrano,79,male,181.1,82.1,O-,2025-04-16T07:34:02.748156,37.1,91,141,85,98.0,14,21.0,537,38.1,,,,0.8,,,,,505,7.1,,25.0, +P-00035,Brian,Munoz,24,male,182.9,91.0,B+,2025-04-16T07:34:02.748156,36.7,53,118,70,97.7,14,28.7,376,43.0,,,0.29,1.4,5,5,,,339,4.8,56,27.2, +P-00036,Kenneth,Henry,37,male,169.8,73.9,B-,2025-04-16T07:34:02.748156,36.1,81,123,80,97.7,18,21.0,522,41.9,499,,,,,8,29,24,478,7.9,67,25.6, +P-00037,Andrea,Castaneda,27,female,154.7,74.5,B+,2025-04-16T07:34:02.748156,36.5,79,112,72,99.3,12,21.0,547,,462,138,,,,,,,510,11.5,70,31.1, +P-00038,Melissa,Riggs,50,female,166.2,64.8,AB-,2025-04-16T07:34:02.748156,37.0,68,137,85,99.2,17,21.0,364,,,,,,,,,,342,6.3,,23.5,1 +P-00039,Juan,Moore,88,other,177.3,73.5,AB+,2025-04-16T07:34:02.748156,36.9,80,131,96,93.2,20,21.0,485,,,,,,,,,,451,7.1,,23.4,1 +P-00040,Monique,Williamson,21,female,170.6,65.4,O-,2025-04-16T07:34:02.748156,36.7,67,121,80,99.3,18,21.0,357,,,,,,,5,,,349,5.8,,22.5, +P-00041,David,Stanley,46,other,173.5,70.9,O-,2025-04-16T07:34:02.748156,37.0,82,113,71,98.4,17,21.0,416,35.6,,,,,,,28,,405,6.4,,23.6, +P-00042,Ryan,Jenkins,19,male,176.1,89.8,A+,2025-04-16T07:34:02.748156,36.3,68,117,76,97.9,13,21.0,462,,,,,1.3,,,28,,459,6.5,,29.0, +P-00043,Kathleen,Jenkins,33,female,171.9,83.4,O-,2025-04-16T07:34:02.748156,36.4,67,125,76,96.4,15,21.0,435,35.7,,,,1.4,,5,,,404,6.9,,28.2, +P-00044,Sabrina,Ross,31,female,152.1,68.0,AB+,2025-04-16T07:34:02.748156,36.4,67,141,78,98.8,20,21.0,478,,,,,,,,26,25,444,10.6,,29.4,1 +P-00045,Michael,Castaneda,22,male,166.7,85.7,AB+,2025-04-16T07:34:02.748156,36.9,80,115,78,98.2,13,21.0,380,38.8,,,,,,,,,357,6.0,,30.8, +P-00046,Angela,Marsh,51,female,166.2,67.8,AB-,2025-04-16T07:34:02.749120,37.1,74,129,91,97.6,12,21.0,511,42.2,,,,1.1,,,,,460,8.8,,24.5, +P-00047,Latoya,Fox,76,other,164.9,55.7,A+,2025-04-16T07:34:02.749120,37.2,98,127,94,98.7,19,21.0,407,37.1,,,,1.2,,,32,32,399,7.2,,20.5,1 +P-00048,Patricia,Friedman,49,female,169.7,61.2,A+,2025-04-16T07:34:02.749120,36.8,40,121,71,99.0,13,21.0,369,35.8,,,,1.5,,,33,,334,6.0,,21.3, +P-00049,Adrian,Paul,22,male,170.9,92.7,O-,2025-04-16T07:34:02.749120,36.9,73,117,73,95.6,17,33.9,467,42.5,,,0.34,,6,,29,23,452,7.0,,31.7, +P-00050,Matthew,Morgan,67,male,187.4,90.1,O+,2025-04-16T07:34:02.749120,37.1,45,143,93,99.3,19,56.3,440,,,,0.56,,10,5,29,,409,5.4,,25.7,1 diff --git a/run.py b/run.py index b3246be65a01da032ca09b3283e4b03d225dedef..8429f4c9626d6e2124d2dbb804996fe58eeca774 100644 --- a/run.py +++ b/run.py @@ -229,7 +229,7 @@ def perform_database_reset(app, db, bcrypt): ) db.session.add(dietitian_profile) - # Commit all changes + # Commit all changes for users and dietitians db.session.commit() print("\n[DB] Database reset completed successfully.") print("[Admin] Default admin account created:") @@ -246,85 +246,13 @@ def perform_database_reset(app, db, bcrypt): print("\nImportant: Please change passwords after logging in!") - # --- Add Patients from JSON --- - print("[DB] Adding patients from JSON file...") - json_file_path = os.path.join(os.path.dirname(__file__), 'patients_data.json') - if not os.path.exists(json_file_path): - print(f"[Warning] patients_data.json not found at {json_file_path}. Skipping patient data loading.") - else: - try: - with open(json_file_path, 'r', encoding='utf-8') as f: - patients_json_data = json.load(f) - - patients_added = 0 - measurements_added = 0 - for patient_data in patients_json_data: - try: - # Create Patient - new_patient = Patient( - id=patient_data.get('patientID'), - firstName=patient_data.get('firstName'), - lastName=patient_data.get('lastName'), - age=patient_data.get('age'), - gender=patient_data.get('gender'), - height=patient_data.get('height'), - weight=patient_data.get('weight'), - blood_type=patient_data.get('blood_type'), - admission_date=datetime.utcnow() - ) - new_patient.calculate_bmi() - db.session.add(new_patient) - # Không cần flush vì khóa chính (id) đã có giá trị - - # Create an initial Encounter for the patient - initial_encounter = Encounter( - patientID=new_patient.id, # Link using patient's string ID - admissionDateTime=new_patient.admission_date # Use patient's admission time - # Các trường khác sẽ là NULL hoặc default - ) - db.session.add(initial_encounter) - db.session.flush() # Flush để lấy encounter.id (Integer) - - # Create Initial Measurement, linking to the new encounter - initial_measurement_data = patient_data.get('initial_measurement', {}) - if initial_measurement_data: - new_measurement = PhysiologicalMeasurement( - patient_id=new_patient.id, - encounter_id=initial_encounter.id, # Sử dụng ID của encounter vừa tạo - measurementDateTime=datetime.utcnow(), - temperature=initial_measurement_data.get('temperature'), - heart_rate=initial_measurement_data.get('heart_rate'), - blood_pressure_systolic=initial_measurement_data.get('blood_pressure_systolic'), - blood_pressure_diastolic=initial_measurement_data.get('blood_pressure_diastolic'), - oxygen_saturation=initial_measurement_data.get('oxygen_saturation'), - resp_rate=initial_measurement_data.get('resp_rate'), - fio2=initial_measurement_data.get('fio2'), - tidal_vol=initial_measurement_data.get('tidal_vol'), - end_tidal_co2=initial_measurement_data.get('end_tidal_co2'), - feed_vol=initial_measurement_data.get('feed_vol'), - peep=initial_measurement_data.get('peep'), - pip=initial_measurement_data.get('pip') - ) - db.session.add(new_measurement) - measurements_added += 1 - - patients_added += 1 - except Exception as patient_err: - print(f"[Error] Could not add patient {patient_data.get('patientID')}: {patient_err}") - db.session.rollback() - - db.session.commit() - print(f"[DB] Successfully added {patients_added} patients and {measurements_added} initial measurements.") - - except FileNotFoundError: - print(f"[Error] patients_data.json not found at {json_file_path}.") - except json.JSONDecodeError: - print(f"[Error] Could not decode JSON from {json_file_path}.") - except Exception as json_err: - print(f"[Error] Failed loading patient data from JSON: {json_err}") - db.session.rollback() - - # Kết thúc phần thêm bệnh nhân + # --- REMOVED Patient Data Loading --- + # Đoạn code tự động thêm bệnh nhân từ JSON/CSV đã bị xóa. + # Việc thêm bệnh nhân ban đầu sẽ được thực hiện qua trang Upload. + print("\n[DB] Patient data loading from file during reset has been removed.") + print("[DB] Please use the Upload page to add initial patient data.") + + # --- Kết thúc phần thêm bệnh nhân (đã xóa) --- return True except Exception as e: