diff --git a/.gitignore b/.gitignore index c34cb7a092fa70a4ad3728c46a35ad4ecc173e33..371ea69c1fbe598872a0feb1e8ee2ef76735e272 100644 --- a/.gitignore +++ b/.gitignore @@ -145,4 +145,7 @@ logs/ Thumbs.db #json file -*.json \ No newline at end of file +*.json + +#csv file +*.csv \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 69d49d2f76674ae0f2dccdfd986a4c815d533e08..0ceb1d7cad06aacccef68f054d0ff4cca988a7af 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -36,7 +36,18 @@ def format_datetime_filter(value, format='%d/%m/%Y %H:%M:%S'): if not isinstance(value, datetime): return value # Trả về giá trị gốc nếu không phải datetime return value.strftime(format) -# --------------------------- + +# --- Thêm hàm helper cho report status badge --- +def get_report_status_badge(status): + status_lower = status.lower() if status else '' + if status_lower == 'draft': + return 'bg-yellow-100 text-yellow-800' + elif status_lower == 'finalized': + return 'bg-green-100 text-green-800' + # Thêm các trạng thái khác nếu cần + else: + return 'bg-gray-100 text-gray-800' # Mặc định +# ---------------------------------------------- def create_app(config_class=None): # Có thể bỏ config_class hoặc dùng cho cấu hình cơ bản app = Flask(__name__, instance_relative_config=True) @@ -69,8 +80,9 @@ def create_app(config_class=None): # Có thể bỏ config_class hoặc dùng ch migrate.init_app(app, db) csrf.init_app(app) - # Đăng ký Jinja filter + # Đăng ký Jinja filter và global function app.jinja_env.filters['format_datetime'] = format_datetime_filter + app.jinja_env.globals.update(get_report_status_badge=get_report_status_badge) # Đăng ký global function # -------------------- # Import và đăng ký Blueprints @@ -80,7 +92,8 @@ def create_app(config_class=None): # Có thể bỏ config_class hoặc dùng ch from .routes.upload import upload_bp from .routes.dietitians import dietitians_bp from .routes.dashboard import dashboard_bp - from .routes.notifications import notifications_bp + from .routes.notifications import notifications_bp as notifications_blueprint + # from .api import api_bp # Xóa hoặc comment out dòng này app.register_blueprint(auth_bp) app.register_blueprint(patients_bp) @@ -88,7 +101,8 @@ def create_app(config_class=None): # Có thể bỏ config_class hoặc dùng ch app.register_blueprint(upload_bp) app.register_blueprint(dietitians_bp) app.register_blueprint(dashboard_bp) - app.register_blueprint(notifications_bp) + app.register_blueprint(notifications_blueprint, url_prefix='/notifications') # Sử dụng tên đã đổi + # app.register_blueprint(api_bp, url_prefix='/api') # Xóa hoặc comment out dòng này # Import models để SQLAlchemy biết về chúng with app.app_context(): diff --git a/app/models/report.py b/app/models/report.py index 3ad5ff47201acd78811624c691043dbb9bc0da24..ec7fc8d9de904fe57bb1355b856589128011080f 100644 --- a/app/models/report.py +++ b/app/models/report.py @@ -1,31 +1,54 @@ from datetime import datetime from app import db -from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import relationship class Report(db.Model): """Model for dietitian reports and assessments""" __tablename__ = 'reports' id = db.Column('reportID', db.Integer, primary_key=True) - user_id = db.Column('userID', db.Integer, db.ForeignKey('users.userID'), nullable=False) + # Khóa ngoại tới User (người tạo báo cáo) + author_id = db.Column('authorID', db.Integer, db.ForeignKey('users.userID'), nullable=False) + # Khóa ngoại tới Patient patient_id = db.Column('patientID', db.String(20), db.ForeignKey('patients.patientID'), nullable=False) - report_date = db.Column('reportDateTime', db.DateTime, nullable=False) - report_title = db.Column('reportTitle', db.String(100), nullable=False) - report_content = db.Column('reportContent', db.Text) - status = db.Column(db.String(20), default='draft') - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + # Khóa ngoại tới Referral (tùy chọn) + referral_id = db.Column('referralID', db.Integer, db.ForeignKey('referrals.referralID'), nullable=True) + + report_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + report_type = db.Column(db.String(50), nullable=False) # e.g., 'initial_assessment', 'follow_up' + status = db.Column(db.String(20), default='draft') # e.g., 'draft', 'finalized' - # Trường ảo để tương thích với code hiện tại - @hybrid_property - def report_type(self): - """Trả về loại báo cáo, mặc định là 'nutrition_assessment'""" - return 'nutrition_assessment' + # Assessment Fields + nutritional_status = db.Column(db.String(100)) + nutrition_diagnosis = db.Column(db.Text) + assessment_details = db.Column(db.Text) # Thay cho report_content + nutritional_risk_score = db.Column(db.Float, nullable=True) + malnutrition_screening_result = db.Column(db.String(50), nullable=True) + + # Recommendation Fields + intervention_plan = db.Column(db.Text) # Thay cho report_content + intervention_summary = db.Column(db.Text, nullable=True) + dietary_recommendations = db.Column(db.Text, nullable=True) + supplement_recommendations = db.Column(db.Text, nullable=True) + enteral_feeding_recommendations = db.Column(db.Text, nullable=True) + parenteral_nutrition_recommendations = db.Column(db.Text, nullable=True) - @report_type.expression - def report_type(cls): - """Biểu thức SQL cho loại báo cáo""" - return db.literal('nutrition_assessment') + # Monitoring and Follow-up Fields + monitoring_plan = db.Column(db.Text) + follow_up_needed = db.Column(db.Boolean, default=False) + follow_up_date = db.Column(db.Date, nullable=True) + # Other Fields + notes = db.Column(db.Text, nullable=True) # Ghi chú nội bộ + finalized_at = db.Column(db.DateTime, nullable=True) # Thời điểm hoàn tất + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + author = relationship('User', backref='reports_authored', foreign_keys=[author_id]) + patient = relationship('Patient', backref='reports') + referral = relationship('Referral', backref='related_report') + # attachments = relationship('ReportAttachment', backref='report', cascade='all, delete-orphan') # Sẽ thêm model Attachment sau nếu cần + def __repr__(self): - return f'<Report {self.id} ({self.report_title})>' \ No newline at end of file + return f'<Report {self.id} for Patient {self.patient_id}>' \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py index 5d52a702e139faaba3c346ed24d7735cb8108746..2b7b4f57aeac9a1c5488bd2de5e090fb26619fa9 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -19,7 +19,6 @@ class User(UserMixin, db.Model): firstName = Column(String(50), nullable=False) lastName = Column(String(50), nullable=False) role = Column(Enum('Admin', 'Dietitian', name='user_roles_new'), default='Dietitian', nullable=False) - is_active = Column(Boolean, default=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) last_login = Column(DateTime(timezone=True)) @@ -44,6 +43,7 @@ class User(UserMixin, db.Model): @property def is_active(self): + # You might want to implement actual logic here later return True @property @@ -68,6 +68,25 @@ class User(UserMixin, db.Model): return f"AD-{self.userID:05d}" return f"U-{self.userID:05d}" + def can_edit(self, report): + """Check if the user can edit the given report.""" + if self.is_admin: + return True + # Check if the user is the author of the report + # Assumes the report object has an 'author_id' attribute + if hasattr(report, 'author_id') and report.author_id == self.userID: + return True + return False + + def is_authorized(self, permission): + """Generic permission check (placeholder).""" + # For now, only Admins are authorized for specific permissions + if self.is_admin: + if permission in ['view_report_notes', 'view_report_audit']: + return True + # Allow users to view their own audit trail/notes? Add logic here. + return False + @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) \ No newline at end of file diff --git a/app/routes/notifications.py b/app/routes/notifications.py index b8ffef00c00956668a4b2e12be4866ce13574e31..595d0d556b61c01b47e367f9052b34d208a477d6 100644 --- a/app/routes/notifications.py +++ b/app/routes/notifications.py @@ -1,63 +1,64 @@ # app/routes/notifications.py -from flask import Blueprint, jsonify +from flask import Blueprint, jsonify, url_for from flask_login import login_required, current_user from app.models.notification import Notification from app import db +from datetime import datetime, timedelta +import timeago notifications_bp = Blueprint('notifications', __name__, url_prefix='/notifications') +def format_timestamp_relative(timestamp): + """Format timestamp relative to now (e.g., '5 minutes ago').""" + now = datetime.utcnow() + # Handle potential timezone differences if necessary + # For simplicity, assume both are UTC + return timeago.format(timestamp, now) + @notifications_bp.route('/api/unread') @login_required def get_unread_notifications(): """API endpoint to get unread notifications for the current user.""" - # Lấy thông báo chưa đọc từ database - # unread_notifications = Notification.query.filter_by(user_id=current_user.userID, is_read=False).order_by(Notification.timestamp.desc()).limit(10).all() - # count = Notification.query.filter_by(user_id=current_user.userID, is_read=False).count() - - # Tạm thời trả về dữ liệu giả - unread_notifications_data = [ - { - 'id': 1, - 'message': f'Welcome {current_user.full_name}!', - 'timestamp': 'Just now', - 'link': '#', - 'icon': 'fa-info-circle', - 'icon_color': 'blue' - }, - { - 'id': 2, - 'message': 'New referral assigned.', - 'timestamp': '5 minutes ago', - 'link': '#', # Thay bằng url_for('referrals.detail', referral_id=...) - 'icon': 'fa-user-plus', - 'icon_color': 'green' - }, - { - 'id': 3, - 'message': 'Patient P-10001 has high HR.', - 'timestamp': '1 hour ago', - 'link': '#', # Thay bằng url_for('patients.patient_detail', patient_id='P-10001') - 'icon': 'fa-heartbeat', - 'icon_color': 'red' - } - ] - count = len(unread_notifications_data) + try: + unread_notifications = ( + Notification.query.filter_by(user_id=current_user.userID, is_read=False) + .order_by(Notification.timestamp.desc()) + .limit(10) + .all() + ) + count = Notification.query.filter_by( + user_id=current_user.userID, is_read=False + ).count() - # Format data for JSON response - # formatted_notifications = [ - # { - # 'id': n.id, - # 'message': n.message, - # 'timestamp': n.timestamp.isoformat(), # Cần xử lý hiển thị thời gian tương đối ở frontend - # 'link': n.link or '#' # Cần thêm logic để tạo link phù hợp - # } for n in unread_notifications - # ] + formatted_notifications = [] + for n in unread_notifications: + icon = "fa-bell" + icon_color = "gray" + if "referral" in n.message.lower(): + icon = "fa-user-plus" + icon_color = "green" + elif "patient" in n.message.lower() or "HR" in n.message or "SpO2" in n.message: + icon = "fa-exclamation-triangle" + icon_color = "red" + elif "welcome" in n.message.lower(): + icon = "fa-info-circle" + icon_color = "blue" - return jsonify({ - 'count': count, - 'notifications': unread_notifications_data # Sử dụng dữ liệu giả - # 'notifications': formatted_notifications # Sử dụng khi có dữ liệu thật - }) + formatted_notifications.append( + { + "id": n.id, + "message": n.message, + "timestamp": format_timestamp_relative(n.timestamp), + "link": n.link or "#", + "icon": icon, + "icon_color": icon_color, + } + ) + + return jsonify({"count": count, "notifications": formatted_notifications}) + except Exception as e: + current_app.logger.error(f"Error fetching notifications for user {current_user.userID}: {e}", exc_info=True) + return jsonify({"error": "Could not fetch notifications"}), 500 @notifications_bp.route('/api/<int:notification_id>/mark-read', methods=['POST']) @login_required @@ -85,6 +86,7 @@ def mark_all_notifications_as_read(): return jsonify({'success': True, 'message': f'{updated_count} notifications marked as read.'}) except Exception as e: db.session.rollback() + current_app.logger.error(f"Error marking all notifications read for user {current_user.userID}: {e}", exc_info=True) return jsonify({'success': False, 'message': f'Error marking notifications as read: {str(e)}'}), 500 # Có thể thêm route để xem tất cả thông báo (trang riêng) nếu cần diff --git a/app/routes/report.py b/app/routes/report.py index 600b7cf03e2eef02a2f53442c9d94daae700dd58..3780bacc87b5576eb623ed9d5119b7459dcec38b 100644 --- a/app/routes/report.py +++ b/app/routes/report.py @@ -6,6 +6,7 @@ from app.models.patient import Patient from app.models.referral import Referral from app.models.procedure import Procedure from app.models.measurement import PhysiologicalMeasurement +from app.models.user import User from app.utils.report_generator import generate_report, generate_excel_report from sqlalchemy import func, desc, case from datetime import datetime, timedelta @@ -13,6 +14,7 @@ import tempfile import os import io from app.forms.report import ReportForm +from sqlalchemy.orm import joinedload report_bp = Blueprint('report', __name__, url_prefix='/reports') @@ -23,7 +25,8 @@ def index(): page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 10, type=int) - query = Report.query + query = Report.query.join(Patient, Report.patient_id == Patient.id)\ + .join(User, Report.author_id == User.userID) # Apply filters if provided report_type = request.args.get('type') @@ -38,7 +41,8 @@ def index(): query = query.order_by(Report.report_date.desc()) # Paginate the results - reports = query.paginate(page=page, per_page=per_page) + reports = query.options(joinedload(Report.patient), joinedload(Report.author))\ + .paginate(page=page, per_page=per_page) # Sử dụng danh sách cứng thay vì truy vấn report_types = [('nutrition_assessment', 'Đánh giá dinh dưỡng')] @@ -73,113 +77,141 @@ def index(): @login_required def new_report(): form = ReportForm() - - if request.method == 'POST': - # Get patient - patient_id = request.form.get('patient_id') - patient = Patient.query.filter_by(patient_id=patient_id).first() - - if not patient: - flash('Patient not found.', 'error') - return redirect(url_for('report.new_report')) - - # Create new report + # Lấy prefill_patient_id từ args cho GET, hoặc từ form data cho POST (nếu user chọn lại) + # Ưu tiên lấy từ args nếu là GET, hoặc khi POST thất bại và muốn giữ giá trị ban đầu + prefill_patient_id = request.args.get('patient_id') if request.method == 'GET' else None + patient_id_from_form = form.patient_id.data # Lấy giá trị từ form (có thể rỗng) + + form.patient_id.choices = [(p.id, f"{p.full_name} ({p.id})") for p in Patient.query.order_by(Patient.lastName, Patient.firstName).all()] + form.patient_id.choices.insert(0, ('', '-- Select Patient --')) + + if form.validate_on_submit(): + print("--- Report form validation PASSED ---") + # patient_id_from_form đã được validate ở đây là hợp lệ + patient = Patient.query.get(patient_id_from_form) + # Kiểm tra lại patient để chắc chắn (dù validate đã làm) + if not patient: + flash('Invalid Patient ID submitted.', 'error') + # Load lại choices + form.patient_id.choices = [(p.id, f"{p.full_name} ({p.id})") for p in Patient.query.order_by(Patient.lastName, Patient.firstName).all()] + form.patient_id.choices.insert(0, ('', '-- Select Patient --')) + # Cố gắng giữ lại patient_id nếu có thể + current_patient_id = patient_id_from_form or prefill_patient_id + return render_template('report_form.html', form=form, report=None, edit_mode=False, prefill_patient_id=current_patient_id) + + print(f"--- Creating report for patient: {patient.id} ---") report = Report( - patient_id=patient.id, - author_id=current_user.id, - report_date=datetime.now(), - report_type=request.form.get('report_type'), - nutritional_status=request.form.get('nutritional_status'), - nutrition_diagnosis=request.form.get('nutrition_diagnosis'), - intervention_summary=request.form.get('intervention_summary'), - monitoring_plan=request.form.get('monitoring_plan'), - nutritional_risk_score=float(request.form.get('nutritional_risk_score')) if request.form.get('nutritional_risk_score') else None, - malnutrition_screening_result=request.form.get('malnutrition_screening_result'), - dietary_recommendations=request.form.get('dietary_recommendations'), - supplement_recommendations=request.form.get('supplement_recommendations'), - enteral_feeding_recommendations=request.form.get('enteral_feeding_recommendations'), - parenteral_nutrition_recommendations=request.form.get('parenteral_nutrition_recommendations'), - follow_up_needed=True if request.form.get('follow_up_needed') else False, - follow_up_date=datetime.strptime(request.form.get('follow_up_date'), '%Y-%m-%d') if request.form.get('follow_up_date') else None, - status='draft', - notes=request.form.get('notes') + author_id=current_user.userID, + patient_id=patient.id, + report_date=form.report_date.data, + report_type=form.report_type.data, + nutritional_status=form.nutritional_status.data, + nutrition_diagnosis=form.nutrition_diagnosis.data, + assessment_details=form.assessment_details.data, + intervention_plan=form.intervention_plan.data, + intervention_summary=form.intervention_summary.data, + monitoring_plan=form.monitoring_plan.data, + nutritional_risk_score=form.nutritional_risk_score.data, + malnutrition_screening_result=form.malnutrition_screening_result.data, + dietary_recommendations=form.dietary_recommendations.data, + supplement_recommendations=form.supplement_recommendations.data, + enteral_feeding_recommendations=form.enteral_feeding_recommendations.data, + parenteral_nutrition_recommendations=form.parenteral_nutrition_recommendations.data, + follow_up_needed=form.follow_up_needed.data, + follow_up_date=form.follow_up_date.data, + status=form.status.data, + notes=form.notes.data ) - # Link to referral if provided - referral_id = request.form.get('referral_id') - if referral_id: - report.referral_id = referral_id - - # Update referral status if needed - referral = Referral.query.get(referral_id) - if referral and referral.referral_status in ['new', 'in_review']: - referral.referral_status = 'confirmed' - referral.reviewed_by = current_user.id - referral.reviewed_at = datetime.now() - referral.action_taken = 'nutritional_assessment' + if form.referral_id.data: + report.referral_id = form.referral_id.data - db.session.add(report) - db.session.commit() + try: + print("--- Adding report to session... ---") + db.session.add(report) + print("--- Committing session... ---") + db.session.commit() + print("--- Commit successful ---") + flash('Report created successfully.', 'success') + print("--- Redirecting after successful creation... ---") + return redirect(url_for('patients.patient_detail', patient_id=patient.id, _anchor='reports')) + except Exception as e: + db.session.rollback() + print(f"--- DATABASE ERROR during commit: {e} ---") + flash(f'Database error occurred: {e}', 'error') + # Load lại choices + form.patient_id.choices = [(p.id, f"{p.full_name} ({p.id})") for p in Patient.query.order_by(Patient.lastName, Patient.firstName).all()] + form.patient_id.choices.insert(0, ('', '-- Select Patient --')) + # Truyền lại patient_id ban đầu khi render lại sau lỗi DB + current_patient_id = patient_id_from_form or prefill_patient_id + return render_template('report_form.html', form=form, report=None, edit_mode=False, prefill_patient_id=current_patient_id) + + # Xử lý khi validation thất bại (POST) hoặc là GET request + else: + # Xác định patient_id để điền lại vào form + current_patient_id_for_render = None + if request.method == 'POST': # Validation failed on POST + print("--- Report form validation FAILED ---") + print(f"Validation Errors: {form.errors}") + flash('Form validation failed. Please check the highlighted fields.', 'error') + # Khi POST lỗi, lấy ID từ form đã submit (nếu user có chọn) + # Hoặc thử lấy từ prefill_patient_id ban đầu (cần lấy lại từ đâu đó, ví dụ hidden field) + # Cách đơn giản nhất là lấy từ form.patient_id.data vì nó chứa giá trị user đã chọn (dù có thể rỗng) + current_patient_id_for_render = form.patient_id.data + elif request.method == 'GET': # Initial GET request + current_patient_id_for_render = prefill_patient_id + + # Gán lại giá trị cho form data để nó hiển thị đúng khi render lại + if current_patient_id_for_render: + form.patient_id.data = current_patient_id_for_render - flash('Report has been created successfully.', 'success') - return redirect(url_for('report.view_report', report_id=report.id)) - - # Get patients for dropdown - patients = Patient.query.all() - - # Get open referrals for dropdown - referrals = Referral.query.filter( - Referral.referral_status.in_(['new', 'in_review']) - ).all() - - return render_template( - 'report_form.html', - patients=patients, - referrals=referrals, - form=form - ) + # Render form tạo mới (hoặc render lại nếu validation thất bại) + print(f"--- Rendering report form template (Prefill ID: {form.patient_id.data}) ---") + return render_template('report_form.html', form=form, report=None, edit_mode=False) @report_bp.route('/<int:report_id>') @login_required def view_report(report_id): - report = Report.query.get_or_404(report_id) - - return render_template('report_detail.html', report=report) + # Sửa lại để load cả patient và author + report = Report.query.options(joinedload(Report.patient), joinedload(Report.author)).get_or_404(report_id) + # TODO: Lấy danh sách attachments nếu có + attachments = [] # Tạm thời + return render_template('report_detail.html', report=report, attachments=attachments) @report_bp.route('/<int:report_id>/edit', methods=['GET', 'POST']) @login_required def edit_report(report_id): - report = Report.query.get_or_404(report_id) - - # Only the author or admin can edit the report - if report.author_id != current_user.id and not current_user.is_admin: - flash('You do not have permission to edit this report.', 'error') - return redirect(url_for('report.view_report', report_id=report.id)) - - if request.method == 'POST': - # Update report - report.report_type = request.form.get('report_type') - report.nutritional_status = request.form.get('nutritional_status') - report.nutrition_diagnosis = request.form.get('nutrition_diagnosis') - report.intervention_summary = request.form.get('intervention_summary') - report.monitoring_plan = request.form.get('monitoring_plan') - report.nutritional_risk_score = float(request.form.get('nutritional_risk_score')) if request.form.get('nutritional_risk_score') else None - report.malnutrition_screening_result = request.form.get('malnutrition_screening_result') - report.dietary_recommendations = request.form.get('dietary_recommendations') - report.supplement_recommendations = request.form.get('supplement_recommendations') - report.enteral_feeding_recommendations = request.form.get('enteral_feeding_recommendations') - report.parenteral_nutrition_recommendations = request.form.get('parenteral_nutrition_recommendations') - report.follow_up_needed = True if request.form.get('follow_up_needed') else False - report.follow_up_date = datetime.strptime(request.form.get('follow_up_date'), '%Y-%m-%d') if request.form.get('follow_up_date') else None - report.status = request.form.get('status', 'draft') - report.notes = request.form.get('notes') + report = Report.query.options(joinedload(Report.patient)).get_or_404(report_id) + form = ReportForm(obj=report) # Load dữ liệu từ report vào form + + # ... (kiểm tra quyền edit giữ nguyên) ... + + # Lấy danh sách patients cho dropdown + form.patient_id.choices = [(p.id, f"{p.full_name} ({p.id})") for p in Patient.query.order_by(Patient.lastName, Patient.firstName).all()] + form.patient_id.choices.insert(0, ('', '-- Select Patient --')) + # Đặt lại giá trị đã chọn cho patient_id (vì obj có thể ghi đè) + form.patient_id.data = report.patient_id + + # Lấy danh sách referrals (nếu cần) + # form.referral_id.choices = ... + # form.referral_id.data = report.referral_id + + if form.validate_on_submit(): + # Cập nhật report từ form data + form.populate_obj(report) # Tự động cập nhật các trường khớp tên + # Cần xử lý riêng các trường không khớp hoặc cần logic đặc biệt (vd: attachments) db.session.commit() - flash('Report has been updated successfully.', 'success') + # TODO: Xử lý file attachments mới và xóa file cũ nếu cần + + flash('Report updated successfully.', 'success') return redirect(url_for('report.view_report', report_id=report.id)) - - return render_template('report_form.html', report=report, edit_mode=True) + + # Render form chỉnh sửa + # TODO: Lấy danh sách attachments nếu có + attachments = [] # Tạm thời + return render_template('report_form.html', form=form, report=report, edit_mode=True, attachments=attachments) @report_bp.route('/<int:report_id>/finalize', methods=['POST']) @login_required diff --git a/app/static/img/logo.png b/app/static/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..66882e7e04ebeb5992299c855c9f0eea03b899bb Binary files /dev/null and b/app/static/img/logo.png differ diff --git a/app/templates/base.html b/app/templates/base.html index 952dddc858c87a293517b9bcddaef55888f4de38..d31bc6d55bb5d9b99464440c1074640bc626f5b1 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -120,17 +120,18 @@ position: absolute; top: 20px; right: -15px; - background: #f3f4f6; - color: #3b82f6; + background: #2563eb; + color: white; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15); cursor: pointer; z-index: 50; + border: 2px solid #e5e7eb; } /* Navigation items */ diff --git a/app/templates/new_report.html b/app/templates/new_report.html deleted file mode 100644 index 9edcbd47ee19523dd3cb3aac2d235b1002de8b68..0000000000000000000000000000000000000000 --- a/app/templates/new_report.html +++ /dev/null @@ -1,257 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Create New Report{% endblock %} - -{% block content %} -<div class="container mx-auto px-4 py-6"> - <!-- Breadcrumbs --> - <nav class="flex mb-5" aria-label="Breadcrumb"> - <ol class="inline-flex items-center space-x-1 md:space-x-3"> - <li class="inline-flex items-center"> - <a href="{{ url_for('main.index') }}" class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-blue-600"> - <svg class="w-3 h-3 mr-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"> - <path d="m19.707 9.293-2-2-7-7a1 1 0 0 0-1.414 0l-7 7-2 2a1 1 0 0 0 1.414 1.414L2 10.414V18a2 2 0 0 0 2 2h3a1 1 0 0 0 1-1v-4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v4a1 1 0 0 0 1 1h3a2 2 0 0 0 2-2v-7.586l.293.293a1 1 0 0 0 1.414-1.414Z"/> - </svg> - Home - </a> - </li> - <li> - <div class="flex items-center"> - <svg class="w-3 h-3 text-gray-400 mx-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"> - <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/> - </svg> - <a href="{{ url_for('report.index') }}" class="ml-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ml-2">Reports</a> - </div> - </li> - <li aria-current="page"> - <div class="flex items-center"> - <svg class="w-3 h-3 text-gray-400 mx-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"> - <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/> - </svg> - <span class="ml-1 text-sm font-medium text-gray-500 md:ml-2">Create New Report</span> - </div> - </li> - </ol> - </nav> - - <!-- Page Title --> - <h1 class="text-2xl font-bold text-gray-900 mb-6 flex items-center"> - <svg class="w-6 h-6 text-gray-700 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> - </svg> - Create New Report - </h1> - - <!-- Form Card --> - <div class="bg-white rounded-lg shadow-md overflow-hidden"> - <div class="p-6"> - <form method="POST" action="{{ url_for('report.new_report') }}"> - <!-- Patient Selection Section --> - <div class="mb-8"> - <h2 class="text-lg font-semibold text-gray-800 mb-4">Patient Information</h2> - <div class="grid grid-cols-1 gap-6"> - <!-- Patient ID --> - <div> - <label for="patient_id" class="block text-sm font-medium text-gray-700 mb-1">Patient ID</label> - <input type="text" id="patient_id" name="patient_id" required - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition" - placeholder="Enter patient ID (e.g., P-10001)"> - <p class="mt-1 text-sm text-gray-500">Enter the patient's ID to generate a report for them</p> - </div> - - <!-- Referral ID (Optional) --> - <div> - <label for="referral_id" class="block text-sm font-medium text-gray-700 mb-1">Referral ID (Optional)</label> - <input type="text" id="referral_id" name="referral_id" - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition" - placeholder="Enter referral ID if applicable"> - </div> - </div> - </div> - - <!-- Report Type Section --> - <div class="mb-8"> - <h2 class="text-lg font-semibold text-gray-800 mb-4">Report Type</h2> - <div> - <label for="report_type" class="block text-sm font-medium text-gray-700 mb-1">Report Type</label> - <select id="report_type" name="report_type" required - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"> - <option value="">Select report type</option> - <option value="initial_assessment">Initial Assessment</option> - <option value="follow_up">Follow-up</option> - <option value="discharge">Discharge</option> - <option value="consultation">Consultation</option> - <option value="progress">Progress Note</option> - </select> - </div> - </div> - - <!-- Assessment Section --> - <div class="mb-8"> - <h2 class="text-lg font-semibold text-gray-800 mb-4">Nutritional Assessment</h2> - <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> - <!-- Nutritional Status --> - <div> - <label for="nutritional_status" class="block text-sm font-medium text-gray-700 mb-1">Nutritional Status</label> - <select id="nutritional_status" name="nutritional_status" required - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"> - <option value="">Select status</option> - <option value="well_nourished">Well Nourished</option> - <option value="at_risk">At Risk</option> - <option value="mild_malnutrition">Mild Malnutrition</option> - <option value="moderate_malnutrition">Moderate Malnutrition</option> - <option value="severe_malnutrition">Severe Malnutrition</option> - </select> - </div> - - <!-- Nutritional Risk Score --> - <div> - <label for="nutritional_risk_score" class="block text-sm font-medium text-gray-700 mb-1">Nutritional Risk Score</label> - <input type="number" id="nutritional_risk_score" name="nutritional_risk_score" min="0" max="10" step="0.1" - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"> - <p class="mt-1 text-sm text-gray-500">Score from 0-10, higher values indicate greater risk</p> - </div> - - <!-- Malnutrition Screening Result --> - <div> - <label for="malnutrition_screening_result" class="block text-sm font-medium text-gray-700 mb-1">Malnutrition Screening Result</label> - <select id="malnutrition_screening_result" name="malnutrition_screening_result" - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"> - <option value="">Select result</option> - <option value="negative">Negative</option> - <option value="at_risk">At Risk</option> - <option value="positive">Positive</option> - </select> - </div> - - <!-- Nutrition Diagnosis --> - <div class="md:col-span-2"> - <label for="nutrition_diagnosis" class="block text-sm font-medium text-gray-700 mb-1">Nutrition Diagnosis</label> - <textarea id="nutrition_diagnosis" name="nutrition_diagnosis" rows="3" - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"></textarea> - </div> - </div> - </div> - - <!-- Recommendation Section --> - <div class="mb-8"> - <h2 class="text-lg font-semibold text-gray-800 mb-4">Recommendations</h2> - <div class="grid grid-cols-1 gap-6"> - <!-- Dietary Recommendations --> - <div> - <label for="dietary_recommendations" class="block text-sm font-medium text-gray-700 mb-1">Dietary Recommendations</label> - <textarea id="dietary_recommendations" name="dietary_recommendations" rows="3" - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"></textarea> - </div> - - <!-- Supplement Recommendations --> - <div> - <label for="supplement_recommendations" class="block text-sm font-medium text-gray-700 mb-1">Supplement Recommendations</label> - <textarea id="supplement_recommendations" name="supplement_recommendations" rows="3" - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"></textarea> - </div> - - <!-- Enteral Feeding Recommendations --> - <div> - <label for="enteral_feeding_recommendations" class="block text-sm font-medium text-gray-700 mb-1">Enteral Feeding Recommendations</label> - <textarea id="enteral_feeding_recommendations" name="enteral_feeding_recommendations" rows="3" - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"></textarea> - </div> - - <!-- Parenteral Nutrition Recommendations --> - <div> - <label for="parenteral_nutrition_recommendations" class="block text-sm font-medium text-gray-700 mb-1">Parenteral Nutrition Recommendations</label> - <textarea id="parenteral_nutrition_recommendations" name="parenteral_nutrition_recommendations" rows="3" - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"></textarea> - </div> - </div> - </div> - - <!-- Plan Section --> - <div class="mb-8"> - <h2 class="text-lg font-semibold text-gray-800 mb-4">Plan</h2> - <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> - <!-- Intervention Summary --> - <div class="md:col-span-2"> - <label for="intervention_summary" class="block text-sm font-medium text-gray-700 mb-1">Intervention Summary</label> - <textarea id="intervention_summary" name="intervention_summary" rows="3" - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"></textarea> - </div> - - <!-- Monitoring Plan --> - <div class="md:col-span-2"> - <label for="monitoring_plan" class="block text-sm font-medium text-gray-700 mb-1">Monitoring Plan</label> - <textarea id="monitoring_plan" name="monitoring_plan" rows="3" - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"></textarea> - </div> - - <!-- Follow-up Needed --> - <div> - <div class="flex items-center"> - <input type="checkbox" id="follow_up_needed" name="follow_up_needed" value="1" - class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"> - <label for="follow_up_needed" class="ml-2 block text-sm font-medium text-gray-700">Follow-up Needed</label> - </div> - </div> - - <!-- Follow-up Date --> - <div> - <label for="follow_up_date" class="block text-sm font-medium text-gray-700 mb-1">Follow-up Date</label> - <input type="date" id="follow_up_date" name="follow_up_date" - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"> - </div> - </div> - </div> - - <!-- Additional Notes Section --> - <div class="mb-8"> - <h2 class="text-lg font-semibold text-gray-800 mb-4">Additional Notes</h2> - <div> - <label for="notes" class="block text-sm font-medium text-gray-700 mb-1">Notes</label> - <textarea id="notes" name="notes" rows="4" - class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"></textarea> - </div> - </div> - - <!-- Form Buttons --> - <div class="flex justify-end space-x-4 mt-8"> - <a href="{{ url_for('report.index') }}" class="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition"> - Cancel - </a> - <button type="submit" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition"> - Save as Draft - </button> - </div> - </form> - </div> - </div> -</div> -{% endblock %} - -{% block scripts %} -<script> - document.addEventListener('DOMContentLoaded', function() { - const followUpNeededCheckbox = document.getElementById('follow_up_needed'); - const followUpDateInput = document.getElementById('follow_up_date'); - - // Initialize the disabled state based on checkbox - function updateFollowUpDateState() { - followUpDateInput.disabled = !followUpNeededCheckbox.checked; - if (!followUpNeededCheckbox.checked) { - followUpDateInput.value = ''; - } - } - - // Set initial state - updateFollowUpDateState(); - - // Add event listener for checkbox changes - followUpNeededCheckbox.addEventListener('change', updateFollowUpDateState); - - // Set minimum date to today for follow-up date - const today = new Date(); - const dateString = today.toISOString().split('T')[0]; - followUpDateInput.setAttribute('min', dateString); - }); -</script> -{% endblock %} \ No newline at end of file diff --git a/app/templates/patient_detail.html b/app/templates/patient_detail.html index 98fd2efe9eb2bfbc9ce972456dd2e931efb74bee..ca7b6bcd62a6a41a4d4d51529385bdda70604a3c 100644 --- a/app/templates/patient_detail.html +++ b/app/templates/patient_detail.html @@ -278,161 +278,82 @@ </div> <!-- Tab giới thiệu & đánh giá --> - <div id="referrals" class="tab-pane" style="display: none;"> - <div class="bg-white shadow rounded-lg"> - <div class="px-4 py-5 sm:px-6 flex justify-between items-center"> - <h3 class="text-lg leading-6 font-medium text-gray-900"> - Lịch sử Giới thiệu & Đánh giá - </h3> - <button type="button" class="inline-flex items-center px-3 py-1 border border-transparent rounded text-sm font-medium text-white bg-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 giới thiệu - </button> - </div> - <div class="border-t border-gray-200 px-4 py-5 sm:p-6"> - {% if referrals %} - <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">ID</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ngày Yêu cầu</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">Nguồn</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ghi chú</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 referral in referrals %} - <tr class="hover:bg-gray-50"> - <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ referral.id }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ referral.referralRequestedDateTime.strftime('%d/%m/%Y %H:%M') if referral.referralRequestedDateTime else 'N/A' }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ referral.referral_status }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ 'ML' if referral.is_ml_recommended else 'Staff' if referral.is_staff_referred else 'Unknown' }}</td> - <td class="px-6 py-4 text-sm text-gray-500 max-w-xs truncate" title="{{ referral.notes }}">{{ referral.notes }}</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> - </td> - </tr> - {% endfor %} - </tbody> - </table> - </div> - {% else %} - <p class="text-center text-gray-500 py-4">Không có lịch sử giới thiệu nào.</p> - {% endif %} - </div> - </div> + <div id="referrals" class="tab-pane hidden"> + <!-- Nội dung tab Giới thiệu & Đánh giá --> + {% include 'patients/_referrals_tab.html' %} </div> - <!-- Tab Thủ thuật --> - <div id="procedures" class="tab-pane" style="display: none;"> - <div class="bg-white shadow rounded-lg"> - <div class="px-4 py-5 sm:px-6 flex justify-between items-center"> - <h3 class="text-lg leading-6 font-medium text-gray-900"> - Lịch sử Thủ thuật - </h3> - <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 thủ thuật - </button> - </div> - <div class="border-t border-gray-200 px-4 py-5 sm:p-6"> - {% if procedures %} - <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">ID</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tên Thủ thuật</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Loại</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Thời gian Bắt đầu</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Thời gian Kết thúc</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Kết quả</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 procedure in procedures %} - <tr class="hover:bg-gray-50"> - <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ procedure.id }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ procedure.procedureName }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ procedure.procedureType }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ procedure.procedureDateTime.strftime('%d/%m/%Y %H:%M') if procedure.procedureDateTime else 'N/A' }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ procedure.procedureEndDateTime.strftime('%d/%m/%Y %H:%M') if procedure.procedureEndDateTime else 'N/A' }}</td> - <td class="px-6 py-4 text-sm text-gray-500 max-w-xs truncate" title="{{ procedure.procedureResults }}">{{ procedure.procedureResults }}</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> - </td> - </tr> - {% endfor %} - </tbody> - </table> - </div> - {% else %} - <p class="text-center text-gray-500 py-4">Không có lịch sử thủ thuật nào.</p> - {% endif %} - </div> - </div> + <!-- Tab Thủ thuật --> + <div id="procedures" class="tab-pane hidden"> + <!-- Nội dung tab Thủ thuật --> + <h3 class="text-lg font-medium text-gray-900 mb-4">Thông tin Thủ thuật (Chưa có dữ liệu)</h3> + <p class="text-sm text-gray-500">Chức năng này đang được phát triển.</p> </div> - <!-- Tab Báo cáo --> - <div id="reports" class="tab-pane" style="display: none;"> - <div class="bg-white shadow rounded-lg"> - <div class="px-4 py-5 sm:px-6 flex justify-between items-center"> + <!-- Tab Báo cáo (Đã thêm) --> + <div id="reports" class="tab-pane hidden"> + <div class="bg-white shadow-md rounded-lg overflow-hidden"> + <div class="px-4 py-5 sm:px-6 border-b border-gray-200 flex justify-between items-center"> <h3 class="text-lg leading-6 font-medium text-gray-900"> - Lịch sử Báo cáo + Danh sách Báo cáo Dinh dưỡng </h3> - <a href="{{ url_for('report.new_report') }}" 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> - Tạo báo cáo mới + <a href="{{ url_for('report.new_report', patient_id=patient.id) }}" class="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"> + <i class="fas fa-plus mr-1"></i> Tạo báo cáo mới </a> </div> - <div class="border-t border-gray-200 px-4 py-5 sm:p-6"> - {% if reports %} - <div class="overflow-x-auto"> + <div class="px-4 py-5 sm:p-6"> + {% if patient.reports|length > 0 %} <table class="min-w-full divide-y divide-gray-200"> - <thead class="bg-gray-50"> + <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</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tiêu đề</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID Báo cáo</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ngày tạo</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Người tạo</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Trạng thái</th> - <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Thao tác</th> + <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"> - {% for report in reports %} - <tr class="hover:bg-gray-50"> - <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ report.id }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ report.report_title }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ report.report_date.strftime('%d/%m/%Y %H:%M') if report.report_date else 'N/A' }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ report.author_id }}</td> {# Cần join để lấy tên người tạo #} - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> - <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ 'bg-green-100 text-green-800' if report.status == 'completed' else 'bg-yellow-100 text-yellow-800' }}"> - {{ report.status|capitalize }} - </span> - </td> - <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> - <a href="{{ url_for('report.view_report', report_id=report.id) }}" class="text-blue-600 hover:text-blue-900 mr-2">Xem</a> - <a href="{{ url_for('report.edit_report', report_id=report.id) }}" class="text-indigo-600 hover:text-indigo-900">Sửa</a> - </td> - </tr> + <tbody class="bg-white divide-y divide-gray-200"> + {% for report in patient.reports|sort(attribute='report_date', reverse=True) %} + <tr class="hover:bg-gray-50 transition-colors duration-150"> + <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"> + <a href="{{ url_for('report.view_report', report_id=report.id) }}" class="text-primary-600 hover:text-primary-900 hover:underline">#{{ report.id }}</a> + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ report.report_date.strftime('%d/%m/%Y %H:%M') if report.report_date else 'N/A' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ report.author.username if report.author else 'N/A' }}</td> + <td class="px-6 py-4 whitespace-nowrap"> + <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ get_report_status_badge(report.status) }}"> + {{ report.status|capitalize }} + </span> + </td> + <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2"> + <a href="{{ url_for('report.view_report', report_id=report.id) }}" class="text-blue-600 hover:text-blue-900" title="Xem chi tiết"><i class="fas fa-eye"></i></a> + <a href="{{ url_for('report.edit_report', report_id=report.id) }}" class="text-indigo-600 hover:text-indigo-900" title="Chỉnh sửa"><i class="fas fa-edit"></i></a> + {% if report.status == 'finalized' %} + <span class="mx-1">|</span> + {# Thêm nút tải PDF nếu báo cáo đã hoàn thành #} + <a href="{{ url_for('report.download_report', report_id=report.id) }}" class="text-green-600 hover:text-green-900" title="Xuất PDF"><i class="fas fa-file-pdf"></i></a> + {% endif %} + {% if current_user.is_admin %} + <span class="mx-1">|</span> + <a href="{{ url_for('report.delete_report', report_id=report.id) }}" class="text-red-600 hover:text-red-900" title="Xóa" onclick="return confirm('Bạn có chắc chắn muốn xóa báo cáo này không?');"><i class="fas fa-trash"></i></a> + {% endif %} + </td> + </tr> {% endfor %} </tbody> </table> - </div> - {% else %} - <p class="text-center text-gray-500 py-4">Không có báo cáo nào.</p> - {% endif %} + {% else %} + <p class="text-center text-gray-500 py-6">Chưa có báo cáo nào cho bệnh nhân này.</p> + {% endif %} </div> </div> </div> <!-- Tab Lượt khám (Encounters) --> - <div id="encounters" class="tab-pane" style="display: none;"> + <div id="encounters" class="tab-pane hidden"> <div class="bg-white shadow rounded-lg"> <div class="px-4 py-5 sm:px-6 flex justify-between items-center"> <h3 class="text-lg leading-6 font-medium text-gray-900"> @@ -522,94 +443,92 @@ {% block scripts %} {{ super() }} {# Kế thừa scripts từ base.html nếu có #} <script> - document.addEventListener('DOMContentLoaded', () => { - // Quản lý tabs - const tabLinks = document.querySelectorAll('.tab-link'); - const tabPanes = document.querySelectorAll('.tab-pane'); - const statusCard = document.querySelector('.status-card'); // Thêm class 'status-card' vào thẻ trạng thái + document.addEventListener('DOMContentLoaded', function() { + const tabs = document.querySelectorAll('.tab-link'); + const panes = document.querySelectorAll('.tab-pane'); + const tabContent = document.getElementById('tabContent'); // Ensure the tab content container exists - // Lấy tab từ hash URL hoặc hiển thị tab mặc định - const currentHash = window.location.hash.substring(1); - const defaultTab = 'overview'; - const initialTab = document.getElementById(currentHash) ? currentHash : defaultTab; - - showTab(initialTab); - updateTabLinks(initialTab); + function activateTab(targetId) { + // Hide all panes + panes.forEach(pane => { + pane.classList.add('hidden'); + pane.classList.remove('animate-fade-in-fast'); // Ensure animation class is removed + }); - tabLinks.forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - const target = link.getAttribute('data-target'); - if (target !== window.location.hash.substring(1)) { - window.location.hash = target; - updateTabLinks(target); - showTab(target); - } + // Deactivate all tabs + tabs.forEach(tab => { + tab.classList.remove('border-primary-500', 'text-primary-600'); + tab.classList.add('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300'); }); - }); - // Lắng nghe sự kiện thay đổi hash (ví dụ: khi người dùng bấm nút back/forward) - window.addEventListener('hashchange', () => { - const newHash = window.location.hash.substring(1); - const targetTab = document.getElementById(newHash) ? newHash : defaultTab; - if (targetTab !== getCurrentActiveTab()) { // Chỉ cập nhật nếu hash thực sự thay đổi tab - updateTabLinks(targetTab); - showTab(targetTab); - } - }); + // Activate the target tab and pane + const activeTab = document.querySelector(`.tab-link[data-target="${targetId}"]`); + const activePane = document.getElementById(targetId); - function getCurrentActiveTab(){ - for(const pane of tabPanes){ - if(pane.style.display !== 'none'){ - return pane.id; + if (activeTab && activePane) { + activeTab.classList.add('border-primary-500', 'text-primary-600'); + activeTab.classList.remove('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300'); + activePane.classList.remove('hidden'); + // Add animation class with a slight delay to ensure it's visible + setTimeout(() => { + activePane.classList.add('animate-fade-in-fast'); + }, 10); // Small delay + } else if (targetId === 'overview') { + // Default to overview if target not found (might happen on initial load) + const overviewTab = document.querySelector(`.tab-link[data-target="overview"]`); + const overviewPane = document.getElementById('overview'); + if (overviewTab && overviewPane) { + overviewTab.classList.add('border-primary-500', 'text-primary-600'); + overviewTab.classList.remove('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300'); + overviewPane.classList.remove('hidden'); + setTimeout(() => { + overviewPane.classList.add('animate-fade-in-fast'); + }, 10); } - } - return defaultTab; - } + } - function updateTabLinks(activeTabId) { - tabLinks.forEach(el => { - const target = el.getAttribute('data-target'); - if (target === activeTabId) { - el.classList.remove('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300'); - el.classList.add('border-blue-500', 'text-blue-600'); - el.setAttribute('aria-current', 'page'); - } else { - el.classList.remove('border-blue-500', 'text-blue-600'); - el.classList.add('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300'); - el.removeAttribute('aria-current'); - } - }); + // Update URL hash without scrolling + if (history.replaceState) { + history.replaceState(null, null, `#${targetId}`); + } else { + window.location.hash = `#${targetId}`; + } } - function showTab(tabId) { - let tabFound = false; - tabPanes.forEach(pane => { - if (pane.id === tabId) { - pane.style.display = 'block'; - pane.classList.add('animate-fade-in'); // Thêm animation - tabFound = true; - } else { - pane.style.display = 'none'; - pane.classList.remove('animate-fade-in'); - } + tabs.forEach(tab => { + tab.addEventListener('click', function(e) { + e.preventDefault(); + const targetId = this.getAttribute('data-target'); + activateTab(targetId); }); - // Nếu không tìm thấy tab hợp lệ, hiển thị tab mặc định - if (!tabFound && tabId !== defaultTab) { - showTab(defaultTab); - updateTabLinks(defaultTab); - window.location.hash = defaultTab; // Reset hash về mặc định - } - toggleStatusCard(tabId); - } + }); - function toggleStatusCard(tabId) { - if (statusCard) { - statusCard.style.display = (tabId === 'overview') ? 'block' : 'none'; + // Activate tab based on URL hash on page load + const hash = window.location.hash.substring(1); // Remove '#' + if (hash) { + const targetPane = document.getElementById(hash); + if (targetPane && targetPane.classList.contains('tab-pane')) { + activateTab(hash); + } else { + activateTab('overview'); // Default to overview if hash is invalid } + } else { + activateTab('overview'); // Default to overview if no hash } - // --- Thêm các script khác nếu cần, ví dụ khởi tạo biểu đồ nhỏ nếu có --- + // Style the status card based on patient status (Example) + const statusCard = document.querySelector('.status-card'); // Select the card to style + const patientStatus = "{{ patient.status|default('Active')|lower }}"; // Get patient status + + if (statusCard) { + if (patientStatus === 'discharged') { + statusCard.classList.add('border-l-4', 'border-gray-400'); // Example style for discharged + } else if (patientStatus === 'in treatment') { + statusCard.classList.add('border-l-4', 'border-blue-500'); // Example style for in treatment + } else { + statusCard.classList.add('border-l-4', 'border-green-500'); // Default for active + } + } }); </script> diff --git a/app/templates/patients/_referrals_tab.html b/app/templates/patients/_referrals_tab.html new file mode 100644 index 0000000000000000000000000000000000000000..20f487fd70bd2611ace72cc721a94f97b17386c4 --- /dev/null +++ b/app/templates/patients/_referrals_tab.html @@ -0,0 +1,46 @@ +<div class="bg-white shadow rounded-lg"> + <div class="px-4 py-5 sm:px-6 flex justify-between items-center"> + <h3 class="text-lg leading-6 font-medium text-gray-900"> + Lịch sử Giới thiệu & Đánh giá + </h3> + {# Nút thêm giới thiệu có thể cần route riêng sau #} + {# <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 giới thiệu + </button> #} + </div> + <div class="border-t border-gray-200 px-4 py-5 sm:p-6"> + {% if referrals %} + <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">ID</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ngày Yêu cầu</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">Nguồn</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ghi chú</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 referral in referrals %} + <tr class="hover:bg-gray-50"> + <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ referral.id }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ referral.referralRequestedDateTime.strftime('%d/%m/%Y %H:%M') if referral.referralRequestedDateTime else 'N/A' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ referral.referral_status }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ 'ML' if referral.is_ml_recommended else 'Staff' if referral.is_staff_referred else 'Unknown' }}</td> + <td class="px-6 py-4 text-sm text-gray-500 max-w-xs truncate" title="{{ referral.notes }}">{{ referral.notes }}</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> {# Link chi tiết cần cập nhật sau #} + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% else %} + <p class="text-center text-gray-500 py-4">Không có lịch sử giới thiệu nào.</p> + {% endif %} + </div> +</div> \ No newline at end of file diff --git a/app/templates/report.html b/app/templates/report.html index 682a1400771356f4ffb55c13ad07fd14e6b0cb9c..d3b39b378b549027e04dbcd46031fa80f23989be 100644 --- a/app/templates/report.html +++ b/app/templates/report.html @@ -99,7 +99,7 @@ {{ report.id }} </td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> - <a href="{{ url_for('patient.detail', patient_id=report.patient.id) }}" class="text-primary-600 hover:text-primary-900"> + <a href="{{ url_for('patients.patient_detail', patient_id=report.patient.id) }}" class="text-primary-600 hover:text-primary-900"> {{ report.patient.full_name }} </a> </td> @@ -152,13 +152,13 @@ </svg> </a> {% if report.status == 'draft' %} - <a href="{{ url_for('report.edit', report_id=report.id) }}" class="text-indigo-600 hover:text-indigo-900 transform hover:scale-110 transition-transform duration-200"> + <a href="{{ url_for('report.edit_report', report_id=report.id) }}" class="text-indigo-600 hover:text-indigo-900 transform hover:scale-110 transition-transform duration-200"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> </svg> </a> {% endif %} - <a href="{{ url_for('report.download', report_id=report.id) }}" class="text-green-600 hover:text-green-900 transform hover:scale-110 transition-transform duration-200"> + <a href="{{ url_for('report.download_report', report_id=report.id) }}" class="text-green-600 hover:text-green-900 transform hover:scale-110 transition-transform duration-200"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /> </svg> diff --git a/app/templates/report_detail.html b/app/templates/report_detail.html index cc3ec81b88fe9c80217e21f116f7172da6e8d4ba..b96aca14ec9654d35eb0ca5ff218120ac1a1b323 100644 --- a/app/templates/report_detail.html +++ b/app/templates/report_detail.html @@ -78,13 +78,19 @@ <div class="border-b border-gray-200 px-6 py-4"> <div class="flex justify-between"> <div> - <img src="{{ url_for('static', filename='img/logo.png') }}" alt="CCU HTM Logo" class="h-12 mb-4"> - <h2 class="text-xl font-bold text-gray-800"> - {{ report.report_type }} Report - </h2> - <p class="text-gray-600"> - Report Date: {{ report.report_date.strftime('%B %d, %Y') }} - </p> + <div class="header"> + <div class="logo"> + <img src="{{ url_for('static', filename='img/logo.png') }}" alt="Logo Bệnh viện" class="h-16 w-auto"> + </div> + <div class="hospital-info"> + <h2 class="text-xl font-bold text-gray-800"> + {{ report.report_type }} Report + </h2> + <p class="text-gray-600"> + Report Date: {{ report.report_date.strftime('%B %d, %Y') }} + </p> + </div> + </div> </div> <div class="text-right"> <p class="text-sm text-gray-500">Report ID: #{{ report.id }}</p> @@ -104,11 +110,10 @@ <div> <p><span class="font-medium">Name:</span> {{ report.patient.full_name }}</p> <p><span class="font-medium">Hospital Number:</span> {{ report.patient.hospital_number }}</p> - <p><span class="font-medium">Date of Birth:</span> {{ report.patient.date_of_birth.strftime('%B %d, %Y') }}</p> + <p><span class="font-medium">Age:</span> {{ report.patient.age }} years</p> </div> <div> <p><span class="font-medium">Gender:</span> {{ report.patient.gender }}</p> - <p><span class="font-medium">Age:</span> {{ report.patient.age }} years</p> {% if report.referral %} <p><span class="font-medium">Referral:</span> {{ report.referral.referral_type }} ({{ report.referral.referralRequestedDateTime.strftime('%B %d, %Y') }})</p> {% endif %} diff --git a/app/templates/report_form.html b/app/templates/report_form.html index dc6b3f28d8567e7b22d6c1e849b72eeaaede629f..6ccd9b65c361d28709dc3e1a125aa610bf593ff2 100644 --- a/app/templates/report_form.html +++ b/app/templates/report_form.html @@ -5,7 +5,7 @@ {% block head %} {{ super() }} <!-- Include TinyMCE for rich text editing --> -<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script> +<script src="https://cdn.tiny.cloud/1/71efq8a2xi8d83xwde8yf39kwrgox6d6rk9ilxjqq7eyje6j/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script> <script> document.addEventListener('DOMContentLoaded', function() { tinymce.init({ @@ -30,7 +30,7 @@ {% block content %} <div class="fade-in"> <div class="bg-white shadow-md rounded-lg p-6 mb-8"> - <div class="flex justify-between items-center mb-6"> + <div class="flex justify-between items-center mb-6 pb-4 border-b border-gray-200"> <h1 class="text-2xl font-bold text-gray-800"> {% if report %}Edit Report #{{ report.id }}{% else %}Create New Report{% endif %} </h1> @@ -42,16 +42,20 @@ </a> </div> - <form method="POST" class="space-y-6" enctype="multipart/form-data" id="report-form"> + {% set current_patient_id = form.patient_id.data or request.args.get('patient_id') %} + <form method="POST" class="space-y-8" enctype="multipart/form-data" id="report-form" novalidate> {{ form.hidden_tag() }} + {% if current_patient_id and not report %} + <input type="hidden" name="patient_id" value="{{ current_patient_id }}"> + {% endif %} <!-- Report Information Section --> - <div class="bg-gray-50 p-4 rounded-md mb-6"> - <h2 class="text-lg font-semibold text-gray-700 mb-4">Report Information</h2> + <div class="border border-gray-300 rounded-md p-4"> + <h2 class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-200">Report Information</h2> <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div> <label for="report_type" class="block text-sm font-medium text-gray-700 mb-1">Report Type*</label> - {{ form.report_type(class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50") }} + {{ form.report_type(class="w-full rounded-md border-gray-400 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50") }} {% if form.report_type.errors %} <div class="text-red-500 text-sm mt-1"> {% for error in form.report_type.errors %} @@ -63,7 +67,7 @@ <div> <label for="report_date" class="block text-sm font-medium text-gray-700 mb-1">Report Date*</label> - {{ form.report_date(class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50", placeholder="MM/DD/YYYY") }} + {{ form.report_date(class="w-full rounded-md border-gray-400 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50", placeholder="MM/DD/YYYY") }} {% if form.report_date.errors %} <div class="text-red-500 text-sm mt-1"> {% for error in form.report_date.errors %} @@ -76,14 +80,18 @@ </div> <!-- Patient Selection Section --> - <div class="bg-gray-50 p-4 rounded-md mb-6"> - <h2 class="text-lg font-semibold text-gray-700 mb-4">Patient Information</h2> + <div class="border border-gray-300 rounded-md p-4"> + <h2 class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-200">Patient Information</h2> <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div> <label for="patient_id" class="block text-sm font-medium text-gray-700 mb-1">Patient*</label> <div class="relative"> - {{ form.patient_id(class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50") }} - <div id="patient-info" class="mt-2 p-2 bg-blue-50 rounded-md text-sm hidden"></div> + {{ form.patient_id(class="w-full rounded-md border-gray-400 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" ~ (' bg-gray-100 cursor-not-allowed' if report else ''), disabled=report) }} + <div id="patient-info" class="mt-2 p-2 bg-blue-50 rounded-md text-sm {{ 'hidden' if not report }}"> + {% if report %} + <strong>{{ report.patient.full_name }}</strong> | {{ report.patient.gender }}, {{ report.patient.age }} years old | MRN: {{ report.patient.hospital_number }} + {% endif %} + </div> </div> {% if form.patient_id.errors %} <div class="text-red-500 text-sm mt-1"> @@ -92,12 +100,15 @@ {% endfor %} </div> {% endif %} + {% if report %} + <p class="text-xs text-gray-500 mt-1">Patient cannot be changed when editing a report.</p> + {% endif %} </div> <div> <label for="referral_id" class="block text-sm font-medium text-gray-700 mb-1">Referral (Optional)</label> <div class="relative"> - {{ form.referral_id(class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50") }} + {{ form.referral_id(class="w-full rounded-md border-gray-400 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50") }} <div id="referral-info" class="mt-2 p-2 bg-blue-50 rounded-md text-sm hidden"></div> </div> {% if form.referral_id.errors %} @@ -112,12 +123,12 @@ </div> <!-- Assessment Section --> - <div class="bg-gray-50 p-4 rounded-md mb-6"> - <h2 class="text-lg font-semibold text-gray-700 mb-4">Assessment</h2> - <div class="space-y-4"> + <div class="border border-gray-300 rounded-md p-4"> + <h2 class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-200">Assessment</h2> + <div class="space-y-6"> <div> <label for="nutritional_status" class="block text-sm font-medium text-gray-700 mb-1">Nutritional Status*</label> - {{ form.nutritional_status(class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50") }} + {{ form.nutritional_status(class="w-full rounded-md border-gray-400 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50") }} {% if form.nutritional_status.errors %} <div class="text-red-500 text-sm mt-1"> {% for error in form.nutritional_status.errors %} @@ -129,7 +140,7 @@ <div> <label for="nutrition_diagnosis" class="block text-sm font-medium text-gray-700 mb-1">Nutrition Diagnosis*</label> - {{ form.nutrition_diagnosis(class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50") }} + {{ form.nutrition_diagnosis(class="w-full rounded-md border-gray-400 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50", rows=3) }} {% if form.nutrition_diagnosis.errors %} <div class="text-red-500 text-sm mt-1"> {% for error in form.nutrition_diagnosis.errors %} @@ -154,9 +165,9 @@ </div> <!-- Recommendations Section --> - <div class="bg-gray-50 p-4 rounded-md mb-6"> - <h2 class="text-lg font-semibold text-gray-700 mb-4">Recommendations</h2> - <div class="space-y-4"> + <div class="border border-gray-300 rounded-md p-4"> + <h2 class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-200">Recommendations</h2> + <div class="space-y-6"> <div> <label for="intervention_plan" class="block text-sm font-medium text-gray-700 mb-1">Intervention Plan*</label> {{ form.intervention_plan(class="rich-editor w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50") }} @@ -184,13 +195,13 @@ </div> <!-- Attachments Section --> - <div class="bg-gray-50 p-4 rounded-md mb-6"> - <h2 class="text-lg font-semibold text-gray-700 mb-4">Attachments</h2> + <div class="border border-gray-300 rounded-md p-4"> + <h2 class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-200">Attachments</h2> <div> - <label for="attachments" class="block text-sm font-medium text-gray-700 mb-1">Upload Files (Optional)</label> - {{ form.attachments(class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50") }} + <label for="attachments" class="block text-sm font-medium text-gray-700 mb-1">Upload New Files (Optional)</label> + {{ form.attachments(class="block w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-400 cursor-pointer focus:outline-none", multiple=True) }} <p class="text-xs text-gray-500 mt-1"> - Accepted file types: PDF, DOC, DOCX, XLS, XLSX, JPG, PNG (Max 5MB per file) + Accepted file types: PDF, DOC, DOCX, XLS, XLSX, JPG, PNG (Max 5MB per file). Hold Ctrl/Cmd to select multiple files. </p> {% if form.attachments.errors %} <div class="text-red-500 text-sm mt-1"> @@ -201,22 +212,22 @@ {% endif %} {% if attachments %} - <div class="mt-4"> - <h3 class="text-sm font-medium text-gray-700 mb-2">Current Attachments</h3> - <ul class="space-y-2"> + <div class="mt-6"> + <h3 class="text-base font-medium text-gray-700 mb-3">Current Attachments</h3> + <ul class="space-y-3"> {% for attachment in attachments %} - <li class="flex items-center justify-between p-2 bg-white rounded-md border border-gray-200"> - <span class="flex items-center"> - <svg class="w-4 h-4 mr-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <li class="flex items-center justify-between p-3 bg-gray-50 rounded-md border border-gray-200"> + <span class="flex items-center text-sm text-gray-800"> + <svg class="w-5 h-5 mr-2 text-gray-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path> </svg> {{ attachment.filename }} </span> - <div class="flex space-x-2"> - <a href="{{ url_for('report.download_attachment', attachment_id=attachment.id) }}" class="text-blue-600 hover:text-blue-800 text-sm"> + <div class="flex space-x-3"> + <a href="{{ url_for('report.download_attachment', attachment_id=attachment.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium hover:underline"> Download </a> - <button type="button" data-attachment-id="{{ attachment.id }}" class="delete-attachment text-red-600 hover:text-red-800 text-sm"> + <button type="button" data-attachment-id="{{ attachment.id }}" class="delete-attachment text-red-600 hover:text-red-800 text-sm font-medium hover:underline"> Remove </button> </div> @@ -229,12 +240,12 @@ </div> <!-- Status Section --> - <div class="bg-gray-50 p-4 rounded-md mb-6"> - <h2 class="text-lg font-semibold text-gray-700 mb-4">Report Status</h2> - <div class="space-y-4"> + <div class="border border-gray-300 rounded-md p-4"> + <h2 class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-200">Report Status</h2> + <div class="space-y-6"> <div> <label for="status" class="block text-sm font-medium text-gray-700 mb-1">Status*</label> - {{ form.status(class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50") }} + {{ form.status(class="w-full rounded-md border-gray-400 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50") }} {% if form.status.errors %} <div class="text-red-500 text-sm mt-1"> {% for error in form.status.errors %} @@ -246,7 +257,7 @@ <div> <label for="notes" class="block text-sm font-medium text-gray-700 mb-1">Internal Notes (Optional)</label> - {{ form.notes(class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50", rows=3) }} + {{ form.notes(class="w-full rounded-md border-gray-400 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50", rows=4) }} <p class="text-xs text-gray-500 mt-1"> These notes are for internal use only and won't be included in the final report. </p> @@ -261,22 +272,44 @@ </div> </div> + <!-- Intervention Summary Field --> + <div class="border border-gray-300 rounded-md p-4"> + <h2 class="text-lg font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-200">Intervention Summary</h2> + <div> + <label for="intervention_summary" class="block text-sm font-medium text-gray-700 mb-1">Intervention Summary {% if form.intervention_summary.flags.required %}*{% endif %}</label> + {{ form.intervention_summary(class="w-full rounded-md border-gray-400 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50", rows=4) }} + {% if form.intervention_summary.errors %} + <div class="text-red-500 text-sm mt-1"> + {% for error in form.intervention_summary.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% endif %} + </div> + </div> + <!-- Form Actions --> - <div class="flex justify-end space-x-4 pt-4 border-t border-gray-200"> - <button type="button" onclick="window.location.href='{{ url_for('report.index') }}'" class="bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 font-medium py-2 px-4 rounded-md transition duration-300"> + <div class="flex justify-end space-x-4 pt-6 border-t border-gray-200"> + {% set cancel_url = url_for('report.index') %} + {% if report %} + {% set cancel_url = url_for('report.view_report', report_id=report.id) %} + {% elif request.args.get('patient_id') %} + {% set cancel_url = url_for('patients.patient_detail', patient_id=request.args.get('patient_id'), _anchor='reports') %} + {% endif %} + <button type="button" onclick="window.location.href='{{ cancel_url }}'" class="bg-white border border-gray-300 hover:bg-gray-100 text-gray-700 font-medium py-2 px-5 rounded-md transition duration-300 shadow-sm hover:shadow"> Cancel </button> {% if report and report.status == 'draft' %} - <button type="submit" name="action" value="save_draft" class="bg-yellow-600 hover:bg-yellow-700 text-white font-medium py-2 px-4 rounded-md transition duration-300"> - Save as Draft + <button type="submit" name="action" value="save_draft" class="bg-yellow-500 hover:bg-yellow-600 text-white font-medium py-2 px-5 rounded-md transition duration-300 shadow hover:shadow-md"> + Save Draft </button> - <button type="submit" name="action" value="finalize" class="bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-md transition duration-300"> + <button type="submit" name="action" value="finalize" class="bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-5 rounded-md transition duration-300 shadow hover:shadow-md"> Finalize Report </button> {% else %} - <button type="submit" name="action" value="save" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition duration-300"> + <button type="submit" name="action" value="save" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-5 rounded-md transition duration-300 shadow hover:shadow-md"> {% if report %}Update Report{% else %}Create Report{% endif %} </button> {% endif %} @@ -287,117 +320,215 @@ {% endblock %} {% block scripts %} +{{ super() }} <script> document.addEventListener('DOMContentLoaded', function() { - // Patient selection change const patientSelect = document.getElementById('patient_id'); const patientInfo = document.getElementById('patient-info'); - + const referralSelect = document.getElementById('referral_id'); + const referralInfo = document.getElementById('referral-info'); + + function updateReferrals(patient_id) { + if (!referralSelect) return; + referralSelect.innerHTML = '<option value="">Loading referrals...</option>'; + referralInfo.classList.add('hidden'); + + fetch(`/api/patients/${patient_id}/referrals`) + .then(response => { + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.json(); + }) + .then(data => { + const currentReferralId = referralSelect.value; + referralSelect.innerHTML = '<option value="">-- Select Referral (Optional) --</option>'; + if (data && Array.isArray(data) && data.length > 0) { + data.forEach(referral => { + const option = document.createElement('option'); + option.value = referral.id; + option.textContent = `#${referral.id} - ${referral.referral_type || 'N/A'} (${referral.referral_date || 'N/A'})`; + if (String(referral.id) === String(currentReferralId)) { + option.selected = true; + } + referralSelect.appendChild(option); + }); + } else { + referralSelect.innerHTML = '<option value="">No referrals found</option>'; + } + referralSelect.dispatchEvent(new Event('change')); + }) + .catch(error => { + console.error('Error fetching referrals:', error); + referralSelect.innerHTML = '<option value="">Error loading referrals</option>'; + referralInfo.classList.add('hidden'); + }); + } + if (patientSelect) { patientSelect.addEventListener('change', function() { const patient_id = this.value; + patientInfo.classList.add('hidden'); if (patient_id) { + patientInfo.innerHTML = 'Loading patient info...'; + patientInfo.classList.remove('hidden'); fetch(`/api/patients/${patient_id}/summary`) - .then(response => response.json()) + .then(response => { + if (!response.ok) throw new Error('Patient not found or API error'); + return response.json(); + }) .then(data => { - patientInfo.innerHTML = `<strong>${data.full_name}</strong> | ${data.gender}, ${data.age} years old | MRN: ${data.hospital_number}`; - patientInfo.classList.remove('hidden'); - - // Update referral dropdown + if(data.error){ + patientInfo.innerHTML = `<span class="text-red-500">${data.error}</span>`; + } else { + patientInfo.innerHTML = `<strong>${data.full_name || 'N/A'}</strong> | ${data.gender || 'N/A'}, ${data.age || 'N/A'} years | MRN: ${data.hospital_number || 'N/A'}`; + } updateReferrals(patient_id); }) .catch(error => { console.error('Error fetching patient data:', error); - patientInfo.classList.add('hidden'); + patientInfo.innerHTML = '<span class="text-red-500">Error loading patient info</span>'; + if(referralSelect){ + referralSelect.innerHTML = '<option value="">Select referral</option>'; + referralInfo.classList.add('hidden'); + } }); } else { - patientInfo.classList.add('hidden'); + patientInfo.innerHTML = ''; + if(referralSelect){ + referralSelect.innerHTML = '<option value="">Select referral</option>'; + referralInfo.classList.add('hidden'); + } } }); - - // Trigger change event if a patient is already selected + if (patientSelect.value) { - patientSelect.dispatchEvent(new Event('change')); + const initialPatientId = patientSelect.value; + if (initialPatientId) { + setTimeout(() => { + if (patientSelect.disabled) { + fetch(`/api/patients/${initialPatientId}/summary`) + .then(response => response.json()).then(data => { + if (!data.error) { + patientInfo.innerHTML = `<strong>${data.full_name || 'N/A'}</strong> | ${data.gender || 'N/A'}, ${data.age || 'N/A'} years | MRN: ${data.hospital_number || 'N/A'}`; + patientInfo.classList.remove('hidden'); + } + }).catch(err => console.error("Error preloading patient info:", err)); + } + updateReferrals(initialPatientId); + }, 100); + } } } - // Referral selection change - const referralSelect = document.getElementById('referral_id'); - const referralInfo = document.getElementById('referral-info'); - if (referralSelect) { referralSelect.addEventListener('change', function() { const referralId = this.value; + referralInfo.classList.add('hidden'); + referralInfo.innerHTML = ''; if (referralId) { + referralInfo.innerHTML = 'Loading referral info...'; + referralInfo.classList.remove('hidden'); fetch(`/api/referrals/${referralId}/summary`) - .then(response => response.json()) + .then(response => { + if (!response.ok) throw new Error('Referral not found or API error'); + return response.json(); + }) .then(data => { - referralInfo.innerHTML = `<strong>${data.referral_type}</strong> | Date: ${data.referral_date} | Status: ${data.status}`; - referralInfo.classList.remove('hidden'); + if(data.error){ + referralInfo.innerHTML = `<span class="text-red-500">${data.error}</span>`; + } else { + referralInfo.innerHTML = `<strong>${data.referral_type || 'N/A'}</strong> | Date: ${data.referral_date || 'N/A'} | Status: ${data.status || 'N/A'}`; + } }) .catch(error => { console.error('Error fetching referral data:', error); - referralInfo.classList.add('hidden'); + referralInfo.innerHTML = '<span class="text-red-500">Error loading referral info</span>'; }); - } else { - referralInfo.classList.add('hidden'); - } + } }); - - // Trigger change event if a referral is already selected - if (referralSelect.value) { - referralSelect.dispatchEvent(new Event('change')); - } - } - - // Function to update referrals based on patient - function updateReferrals(patient_id) { - if (referralSelect) { - fetch(`/api/patients/${patient_id}/referrals`) - .then(response => response.json()) - .then(data => { - // Clear current options - referralSelect.innerHTML = '<option value="">Select a referral (optional)</option>'; - - // Add new options - data.forEach(referral => { - const option = document.createElement('option'); - option.value = referral.id; - option.textContent = `${referral.referral_type} (${referral.referral_date})`; - referralSelect.appendChild(option); - }); - }) - .catch(error => console.error('Error fetching referrals:', error)); - } + + var initialReferralId = null; + {% if report is defined and report and report.referral_id is defined and report.referral_id %} + initialReferralId = "{{ report.referral_id | string }}"; + {% endif %} + if (initialReferralId && patientSelect && patientSelect.value) { + setTimeout(() => { + if (referralSelect.value === initialReferralId) { + referralSelect.dispatchEvent(new Event('change')); + } + }, 700); + } } - // Handle attachment deletion document.querySelectorAll('.delete-attachment').forEach(button => { button.addEventListener('click', function() { const attachmentId = this.getAttribute('data-attachment-id'); - if (confirm('Are you sure you want to remove this attachment?')) { - fetch(`/reports/attachment/${attachmentId}/delete`, { method: 'POST' }) + if (confirm('Are you sure you want to remove this attachment? This action might be permanent.')) { + fetch(`/reports/attachment/${attachmentId}/delete`, { + method: 'POST', + headers: { + 'X-CSRFToken': document.querySelector('input[name="csrf_token"]').value + } + }) .then(response => response.json()) .then(data => { if (data.success) { this.closest('li').remove(); } else { - alert('Failed to delete attachment: ' + data.message); + alert('Failed to delete attachment: ' + (data.message || 'Unknown error')); } }) - .catch(error => console.error('Error deleting attachment:', error)); + .catch(error => { + console.error('Error deleting attachment:', error); + alert('An error occurred while deleting the attachment.'); + }); } }); }); - - // Form submission handling + const form = document.getElementById('report-form'); - form.addEventListener('submit', function(e) { - // Update TinyMCE content before submission - if (typeof tinymce !== 'undefined') { - tinymce.triggerSave(); - } - }); + if (form) { + form.addEventListener('submit', function(e) { + console.log("Form submit event triggered!"); + + if (typeof tinymce !== 'undefined') { + let allEditorsExist = true; + ['assessment_details', 'intervention_plan', 'monitoring_plan'].forEach(id => { + if (!tinymce.get(id)) { + console.warn(`TinyMCE editor with id '${id}' not found.`); + allEditorsExist = false; + } + }); + + if (allEditorsExist) { + console.log("Attempting tinymce.triggerSave()..."); + try { + tinymce.triggerSave(); + console.log("tinymce.triggerSave() completed."); + } catch (tinymceError) { + console.error("Error during tinymce.triggerSave():", tinymceError); + } + } else { + console.warn('One or more TinyMCE editors not found. Skipping triggerSave().'); + } + } else { + console.warn('TinyMCE is not defined. Skipping triggerSave().'); + } + + const submitButton = form.querySelector('button[type="submit"][name="action"][value="save"]'); + const updateButton = form.querySelector('button[type="submit"][name="action"][value="save_draft"]'); + const finalizeButton = form.querySelector('button[type="submit"][name="action"][value="finalize"]'); + + let clickedButton = submitButton || updateButton || finalizeButton; + + if (clickedButton) { + console.log("Disabling submit button..."); + clickedButton.disabled = true; + clickedButton.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Processing...'; + } + + console.log("Form submit proceeding..."); + }); + } }); </script> {% endblock %} \ No newline at end of file diff --git a/migrations/versions/9d82acfed8fa_update_report.py b/migrations/versions/9d82acfed8fa_update_report.py new file mode 100644 index 0000000000000000000000000000000000000000..ccf807f304f6facb9e3a51eff12ad7a00865b268 --- /dev/null +++ b/migrations/versions/9d82acfed8fa_update_report.py @@ -0,0 +1,96 @@ +"""update report + +Revision ID: 9d82acfed8fa +Revises: 1af2bf32a740 +Create Date: 2025-04-18 11:06:04.115320 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '9d82acfed8fa' +down_revision = '1af2bf32a740' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('reports', schema=None) as batch_op: + batch_op.add_column(sa.Column('authorID', sa.Integer(), nullable=False)) + batch_op.add_column(sa.Column('referralID', sa.Integer(), nullable=True)) + batch_op.add_column(sa.Column('report_date', sa.DateTime(), nullable=False)) + batch_op.add_column(sa.Column('report_type', sa.String(length=50), nullable=False)) + batch_op.add_column(sa.Column('nutritional_status', sa.String(length=100), nullable=True)) + batch_op.add_column(sa.Column('nutrition_diagnosis', sa.Text(), nullable=True)) + batch_op.add_column(sa.Column('assessment_details', sa.Text(), nullable=True)) + batch_op.add_column(sa.Column('nutritional_risk_score', sa.Float(), nullable=True)) + batch_op.add_column(sa.Column('malnutrition_screening_result', sa.String(length=50), nullable=True)) + batch_op.add_column(sa.Column('intervention_plan', sa.Text(), nullable=True)) + batch_op.add_column(sa.Column('intervention_summary', sa.Text(), nullable=True)) + batch_op.add_column(sa.Column('dietary_recommendations', sa.Text(), nullable=True)) + batch_op.add_column(sa.Column('supplement_recommendations', sa.Text(), nullable=True)) + batch_op.add_column(sa.Column('enteral_feeding_recommendations', sa.Text(), nullable=True)) + batch_op.add_column(sa.Column('parenteral_nutrition_recommendations', sa.Text(), nullable=True)) + batch_op.add_column(sa.Column('monitoring_plan', sa.Text(), nullable=True)) + batch_op.add_column(sa.Column('follow_up_needed', sa.Boolean(), nullable=True)) + batch_op.add_column(sa.Column('follow_up_date', sa.Date(), nullable=True)) + batch_op.add_column(sa.Column('notes', sa.Text(), nullable=True)) + batch_op.add_column(sa.Column('finalized_at', sa.DateTime(), nullable=True)) + batch_op.drop_constraint('reports_ibfk_1', type_='foreignkey') + batch_op.create_foreign_key(None, 'referrals', ['referralID'], ['referralID']) + batch_op.create_foreign_key(None, 'users', ['authorID'], ['userID']) + batch_op.drop_column('reportTitle') + batch_op.drop_column('reportContent') + batch_op.drop_column('reportDateTime') + batch_op.drop_column('userID') + + with op.batch_alter_table('uploadedfiles', schema=None) as batch_op: + batch_op.alter_column('error_details', + existing_type=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + type_=sa.Text(length=16777215), + existing_nullable=True) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('uploadedfiles', schema=None) as batch_op: + batch_op.alter_column('error_details', + existing_type=sa.Text(length=16777215), + type_=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + existing_nullable=True) + + with op.batch_alter_table('reports', schema=None) as batch_op: + batch_op.add_column(sa.Column('userID', mysql.INTEGER(), autoincrement=False, nullable=False)) + batch_op.add_column(sa.Column('reportDateTime', mysql.DATETIME(), nullable=False)) + batch_op.add_column(sa.Column('reportContent', mysql.TEXT(collation='utf8mb4_unicode_ci'), nullable=True)) + batch_op.add_column(sa.Column('reportTitle', mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=100), nullable=False)) + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.create_foreign_key('reports_ibfk_1', 'users', ['userID'], ['userID']) + batch_op.drop_column('finalized_at') + batch_op.drop_column('notes') + batch_op.drop_column('follow_up_date') + batch_op.drop_column('follow_up_needed') + batch_op.drop_column('monitoring_plan') + batch_op.drop_column('parenteral_nutrition_recommendations') + batch_op.drop_column('enteral_feeding_recommendations') + batch_op.drop_column('supplement_recommendations') + batch_op.drop_column('dietary_recommendations') + batch_op.drop_column('intervention_summary') + batch_op.drop_column('intervention_plan') + batch_op.drop_column('malnutrition_screening_result') + batch_op.drop_column('nutritional_risk_score') + batch_op.drop_column('assessment_details') + batch_op.drop_column('nutrition_diagnosis') + batch_op.drop_column('nutritional_status') + batch_op.drop_column('report_type') + batch_op.drop_column('report_date') + batch_op.drop_column('referralID') + batch_op.drop_column('authorID') + + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index 7ae5fa4265508eb556f3385d0fedcdd4fd25ef75..179452ae969ab425536ee5b021f782030cc15710 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,4 +43,5 @@ black==23.1.0 flake8==6.0.0 isort==5.12.0 mypy==0.991 -Faker \ No newline at end of file +Faker +timeago \ No newline at end of file diff --git a/run_ccu.bat b/run_ccu.bat index a5a171a71da98a7d21e305c48a62ed8180632cd3..969988bd80cfaccb772653f306b127d61cfe438f 100644 --- a/run_ccu.bat +++ b/run_ccu.bat @@ -31,7 +31,8 @@ goto menu :run_app echo Starting the application... python "run.py" --clean -goto end +pause +goto menu :apply_migrations echo Applying database migrations (upgrade)... @@ -47,7 +48,7 @@ set /p msg=Enter a short description for the migration: flask db migrate -m "%msg%" echo. echo Migration file generated (or message shown if no changes detected). -echo You may need to run option 2 (Apply migrations) afterwards. +echo You may need to run option 4 (Apply migrations) afterwards. echo. echo Press any key to return to the menu... pause >nul