commit 1312f21849477cdf1029d6c0f623edce23ed8b9c
Author: chinosk <2248589280@qq.com>
Date: Wed Apr 30 17:28:58 2025 +0100
init commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b85dc31
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.idea
+.vs
+.vscode
+__pycache__
+/HMS_Backend/HMS_Backend.zip
+/HMS_Frontend/HMS_Frontend.zip
+/HMS_Frontend/HMS_Frontend_2.zip
diff --git a/HMS_Backend/HMS_Backend/__init__.py b/HMS_Backend/HMS_Backend/__init__.py
new file mode 100644
index 0000000..e505ffe
--- /dev/null
+++ b/HMS_Backend/HMS_Backend/__init__.py
@@ -0,0 +1 @@
+from .hms import *
diff --git a/HMS_Backend/HMS_Backend/hms.py b/HMS_Backend/HMS_Backend/hms.py
new file mode 100644
index 0000000..c4d4b09
--- /dev/null
+++ b/HMS_Backend/HMS_Backend/hms.py
@@ -0,0 +1,1252 @@
+import datetime
+from typing import Callable, Optional, Dict
+import inspect
+from flask import Flask, request, jsonify
+from .models import *
+from . import utils
+
+
+def create_init_data():
+ if Doctor.query.count() == 0:
+ admin = Doctor(id=1, name="Admin", title="", birth_date=datetime.datetime(2000, 1, 1), gender="M",
+ phone="1234567890", email="Admin@example.com", grade=DoctorGrade.Unknown.value)
+ receptionist = Doctor(id=2, name="Receptionist", title="", birth_date=datetime.datetime(2000, 1, 1), gender="M",
+ phone="1234567891", email="Receptionist@example.com", grade=DoctorGrade.Unknown.value)
+ db.session.add(admin)
+ db.session.add(receptionist)
+ db.session.commit()
+
+ if Team.query.count() == 0:
+ admin_team = Team(id=1, department="Admin", consultant_id=1, is_admin_team=True)
+ reception_team = Team(id=2, department="Reception", consultant_id=2, is_admin_team=False)
+ db.session.add(admin_team)
+ db.session.add(reception_team)
+ db.session.commit()
+
+ if DoctorTeamRole.query.count() == 0:
+ admin_team_role = DoctorTeamRole(doctor_id=1, team_id=1)
+ reception_team_role = DoctorTeamRole(doctor_id=2, team_id=2)
+ db.session.add(admin_team_role)
+ db.session.add(reception_team_role)
+ db.session.commit()
+
+ if User.query.count() == 0:
+ admin_user = User(username="admin", role=UserType.DOCTOR.value, doctor_id=1)
+ admin_user.set_password("admin")
+ receptionist_user = User(username="receptionist", role=UserType.DOCTOR.value, doctor_id=2)
+ receptionist_user.set_password("receptionist")
+ db.session.add(admin_user)
+ db.session.add(receptionist_user)
+ db.session.commit()
+
+
+with app.app_context():
+ db.create_all()
+ create_init_data()
+
+
+def return_json(success=True, message: str = "", status_code: int = 200, data: dict = None):
+ return jsonify({
+ 'success': success,
+ 'message': message,
+ 'data': {} if data is None else data
+ }), status_code
+
+
+def permission_check(min_permission: UserPermissionLevel, receptionist_only: bool = False):
+ def outer(func: Callable):
+ def inner(*args, **kwargs):
+ token = request.headers.get('Authorization')
+ if not token:
+ return return_json(success=False, message="Unauthorized", status_code=401)
+
+ login_info = utils.get_user_info_with_permission_from_token(token)
+ if receptionist_only and (
+ login_info.user_type.value == UserType.DOCTOR.value) and utils.get_doctor_can_reception(
+ login_info.user_data.id):
+ if "pm_user_data" in inspect.signature(func).parameters:
+ return func(*args, pm_user_data=login_info, **kwargs)
+ else:
+ return func(*args, **kwargs)
+
+ elif login_info.user_permission.value >= min_permission.value:
+ if "pm_user_data" in inspect.signature(func).parameters:
+ return func(*args, pm_user_data=login_info, **kwargs)
+ else:
+ return func(*args, **kwargs)
+
+ return return_json(success=False, message="Permission denied", status_code=403)
+
+ inner.__name__ = func.__name__
+ return inner
+
+ return outer
+
+def _get_team_members(team_id: int) -> Optional[Dict[str, any]]:
+ team: Team = Team.query.filter_by(id=team_id).first()
+ if not team:
+ return None
+
+ members = DoctorTeamRole.query.filter_by(team_id=team.id).all()
+ if not members:
+ return {"team": team.to_dict(), "members": []}
+
+ member_list = []
+ for i in members:
+ doctor: Doctor = Doctor.query.filter_by(id=i.doctor_id).first()
+ if doctor:
+ member_list.append(doctor.to_dict())
+ return {
+ "team": team.to_dict(),
+ "members": member_list
+ }
+
+def get_formatted_user_info_data(pm_user_data: UserLoginInfo):
+ ret_data = {
+ "user_type": pm_user_data.user_type,
+ "user_permission": pm_user_data.user_permission,
+ "patient_data": None,
+ "doctor_data": None,
+ "doctor_teams": None
+ }
+ if pm_user_data.user_type == UserType.PATIENT.value:
+ ret_data["patient_data"] = pm_user_data.user_data.to_dict()
+ elif pm_user_data.user_type == UserType.DOCTOR.value:
+ ret_data["doctor_data"] = pm_user_data.user_data.to_dict()
+ # ret_data["doctor_teams"] = [i.to_dict() for i in utils.get_doctor_teams(pm_user_data.user_data.id)]
+ ret_data["doctor_teams"] = []
+ for i in utils.get_doctor_teams(pm_user_data.user_data.id):
+ team_data = i.to_dict()
+ team_data["members"] = _get_team_members(i.id)["members"]
+ ret_data["doctor_teams"].append(team_data)
+ else:
+ # return return_json(success=False, message="Unknown user type", status_code=500)
+ return None
+
+ return ret_data
+
+
+# Login
+@app.route('/login', methods=['POST'])
+def login():
+ data = request.get_json()
+ username = data.get('username')
+ password = data.get('password')
+
+ if not username or not password:
+ return return_json(success=False, message="Username and password are required", status_code=400)
+
+ user: User = User.query.filter_by(username=username).first()
+ if not user:
+ return return_json(success=False, message="User not found", status_code=404)
+ if not user.check_password(password):
+ return return_json(success=False, message="Invalid password", status_code=401)
+
+ token = utils.generate_randstring(62)
+ expire_at = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=14)
+ credentials = Credentials(token=token, expire_at=expire_at, role=user.role)
+ if user.role == UserType.DOCTOR.value:
+ credentials.doctor_id = user.doctor_id
+ else:
+ credentials.patient_id = user.patient_id
+ db.session.add(credentials)
+ db.session.commit()
+
+ user_login_info = utils.get_user_info_with_permission_from_token(token)
+
+ return return_json(success=True, message="Login successful",
+ data={"token": token, "expire_at": int(expire_at.timestamp()),
+ "user_info": get_formatted_user_info_data(user_login_info)})
+
+
+@app.route('/get_my_info', methods=['GET'])
+@permission_check(UserPermissionLevel.PATIENT)
+def get_user_info(pm_user_data: UserLoginInfo):
+ user_formatted_info = get_formatted_user_info_data(pm_user_data)
+
+ if user_formatted_info is None:
+ return return_json(success=False, message="Unknown user type", status_code=500)
+
+ if pm_user_data.user_type == UserType.DOCTOR:
+ user_formatted_info["doctor_data"]["is_admin"] = user_formatted_info.get("user_permission", UserPermissionLevel.UNAUTHORIZED.value) >= UserPermissionLevel.ADMIN.value
+ return return_json(success=True, message="User info retrieved successfully", data=user_formatted_info)
+
+
+@app.route('/change_password', methods=['POST'])
+@permission_check(UserPermissionLevel.PATIENT)
+def change_password(pm_user_data: UserLoginInfo):
+ data = request.get_json()
+ old_password = data.get('old_password')
+ new_password = data.get('new_password')
+
+ if not new_password:
+ return return_json(success=False, message="New passwords required", status_code=400)
+ if not old_password:
+ return return_json(success=False, message="Old passwords required", status_code=400)
+
+ if pm_user_data.user_type == UserType.PATIENT.value:
+ user: User = User.query.filter_by(patient_id=pm_user_data.user_data.id).first()
+ elif pm_user_data.user_type == UserType.DOCTOR.value:
+ user: User = User.query.filter_by(doctor_id=pm_user_data.user_data.id).first()
+ else:
+ return return_json(success=False, message="Unknown user type", status_code=500)
+
+ if not user:
+ return return_json(success=False, message="User not found", status_code=500)
+
+ if not user.check_password(old_password):
+ return return_json(success=False, message="Old password is incorrect", status_code=401)
+
+ user.set_password(new_password)
+
+ if pm_user_data.user_type == UserType.PATIENT.value:
+ Credentials.query.filter_by(patient_id=user.patient_id).delete()
+ elif pm_user_data.user_type == UserType.DOCTOR.value:
+ Credentials.query.filter_by(doctor_id=user.doctor_id).delete()
+
+ db.session.commit()
+
+ return return_json(success=True, message="Password changed successfully")
+
+
+# Register patient
+@app.route('/register_patient', methods=['POST'])
+def register():
+ data = request.get_json()
+ username = data.get('username')
+ password = data.get('password')
+
+ name = data.get('name')
+ title = data.get('title')
+ birth_date = datetime.datetime.fromtimestamp(data.get('birth_date'), datetime.timezone.utc)
+ gender = data.get('gender')
+ phone = data.get('phone')
+ email = data.get('email')
+ address = data.get('address')
+ postcode = data.get('postcode')
+
+ # Validate input
+ if not all([username, password, name, title, birth_date, gender, phone, email, address, postcode]):
+ return return_json(success=False, message="Username and password are required", status_code=400)
+
+ if User.query.filter_by(username=username).first():
+ return return_json(success=False, message="Username already exists", status_code=409)
+
+ patient = Patient(name=name, title=title, birth_date=birth_date, gender=gender, phone=phone, email=email,
+ postcode=postcode,
+ address=address)
+ db.session.add(patient)
+
+ db.session.flush()
+ if patient.id is None:
+ return return_json(success=False, message="Failed to create patient record", status_code=500)
+
+ user = User(username=username, role=UserType.PATIENT.value, patient_id=patient.id)
+ user.set_password(password)
+ db.session.add(user)
+
+ db.session.commit()
+
+ return return_json(success=True, message="User registered successfully")
+
+
+@app.route("/get_doctor_list", methods=["GET"])
+# @permission_check(UserPermissionLevel.PATIENT)
+def get_doctor_list():
+ count_limit = request.args.get("limit", BaseDefine.DEFAULT_PER_PAGE, type=int)
+ page = request.args.get("page", 1, type=int)
+
+ if page == -1:
+ doctors = Doctor.query.paginate(per_page=999999, max_per_page=None, error_out=False)
+ else:
+ doctors = Doctor.query.paginate(page=page, per_page=count_limit, max_per_page=BaseDefine.MAX_PER_PAGE,
+ error_out=False)
+ if not doctors.items:
+ return return_json(success=True, message="No doctors found", data={"doctors": [], "total_pages": 1})
+ doctor_list = []
+ for i in doctors.items:
+ doctor_data = i.to_dict()
+ doctor_data["is_admin"] = utils.get_doctor_is_admin(i.id)
+ doctor_list.append(doctor_data)
+
+ return return_json(success=True, message="Doctors retrieved successfully",
+ data={"doctors": doctor_list, "total_pages": doctors.pages})
+
+
+@app.route("/get_ward_list", methods=["GET"])
+@permission_check(UserPermissionLevel.PATIENT) # TODO 确认此 API 的权限
+def get_ward_list():
+ count_limit = request.args.get("limit", BaseDefine.DEFAULT_PER_PAGE, type=int)
+ page = request.args.get("page", 1, type=int)
+ if page < 1:
+ page = 1
+ no_limit = request.args.get("no_limit", "false").lower() == "true"
+
+ wards = Ward.query.paginate(page=page, per_page=count_limit, max_per_page=BaseDefine.MAX_PER_PAGE, error_out=False) \
+ if no_limit else Ward.query.paginate(max_per_page=None)
+ if not wards.items:
+ return return_json(success=True, message="No wards found", data={"wards": [], "total_pages": 1})
+ ward_list = [i.to_dict() for i in wards.items]
+ return return_json(success=True, message="Wards retrieved successfully",
+ data={"wards": ward_list, "total_pages": wards.pages})
+
+
+@app.route("/get_team_info", methods=["GET"])
+# @permission_check(UserPermissionLevel.DOCTOR)
+def get_team_members():
+ team_id = request.args.get("team_id", type=int)
+ team = _get_team_members(team_id)
+ if not team:
+ return return_json(success=False, message="Team not found", status_code=404)
+ team["team"]["members"] = team["members"]
+
+ return return_json(success=True, message="Members retrieved successfully", data=team)
+
+
+@app.route('/get_team_list', methods=['GET'])
+# @permission_check(UserPermissionLevel.DOCTOR)
+def get_team_list():
+ count_limit = request.args.get("limit", BaseDefine.DEFAULT_PER_PAGE, type=int)
+ page = request.args.get("page", 1, type=int)
+
+ if page == -1:
+ teams = Team.query.paginate(per_page=999999, max_per_page=None, error_out=False)
+ else:
+ teams = Team.query.paginate(page=page, per_page=count_limit, max_per_page=BaseDefine.MAX_PER_PAGE,
+ error_out=False)
+ if not teams.items:
+ return return_json(success=True, message="No teams found", data={"teams": [], "total_pages": 1})
+
+ team_list = []
+ for i in teams.items:
+ team_data = i.to_dict()
+ team_data["members"] = _get_team_members(i.id)["members"]
+ team_list.append(team_data)
+
+ return return_json(success=True, message="Teams retrieved successfully",
+ data={"teams": team_list, "total_pages": teams.pages})
+
+
+@app.route('/get_patient_info', methods=['GET'])
+@permission_check(UserPermissionLevel.DOCTOR)
+def get_patient_info():
+ patient_id = request.args.get("patient_id", type=int)
+ if not patient_id:
+ return return_json(success=False, message="Patient ID is required", status_code=400)
+
+ patient: Patient = Patient.query.filter_by(id=patient_id).first()
+ if not patient:
+ return return_json(success=False, message="Patient not found", status_code=404)
+
+ return return_json(success=True, message="Patient info retrieved successfully",
+ data={"patient": patient.to_dict()})
+
+
+@app.route('/patient/book', methods=['POST'])
+@permission_check(UserPermissionLevel.PATIENT)
+def book_appointment(pm_user_data: UserLoginInfo):
+ data = request.get_json()
+ appointment_time = datetime.datetime.fromtimestamp(data.get('appointment_time'))
+ description = data.get('description')
+ category = data.get('category')
+
+ if category not in BaseDefine.categories:
+ return return_json(success=False, message="Invalid category", status_code=400)
+
+ if not all([appointment_time, description, category]):
+ return return_json(success=False, message="Appointment time, description and category are required",
+ status_code=400)
+
+ appointment = Appointment(patient_id=pm_user_data.user_data.id, appointment_time=appointment_time,
+ description=description, category=category)
+ db.session.add(appointment)
+ db.session.commit()
+
+ return return_json(success=True, message="Appointment booked successfully", data={"appointment_id": appointment.id})
+
+
+@app.route('/patient/appointments', methods=['GET'])
+@permission_check(UserPermissionLevel.PATIENT)
+def get_appointments(pm_user_data: UserLoginInfo):
+ count_limit = request.args.get("limit", BaseDefine.DEFAULT_PER_PAGE, type=int)
+ page = request.args.get("page", 1, type=int)
+
+ appointments = (Appointment.query.order_by(Appointment.id.desc()).filter_by(patient_id=pm_user_data.user_data.id)
+ .paginate(page=page, per_page=count_limit, max_per_page=BaseDefine.MAX_PER_PAGE, error_out=False))
+ # appointments = Appointment.query.order_by(Appointment.id.desc()).filter_by(patient_id=pm_user_data.user_data.id).all()
+ if not appointments.items:
+ return return_json(success=True, message="Appointments retrieved successfully",
+ data={"appointments": [], "total_pages": 1})
+
+ appointment_list = [appointment.to_dict() for appointment in appointments.items]
+ return return_json(success=True, message="Appointments retrieved successfully",
+ data={"appointments": appointment_list, "total_pages": appointments.pages})
+
+
+@app.route('/patient/edit_appointment', methods=['POST'])
+@permission_check(UserPermissionLevel.PATIENT)
+def edit_appointment(pm_user_data: UserLoginInfo):
+ data = request.get_json()
+ appointment_id = data.get("appointment_id")
+ appointment_time = datetime.datetime.fromtimestamp(data.get('appointment_time'))
+ description = data.get('description')
+ category = data.get('category')
+
+ if not all([appointment_id, appointment_time, description, category]):
+ return return_json(success=False, message="Appointment ID, time, description and category are required",
+ status_code=400)
+
+ appointment: Appointment = Appointment.query.order_by(Appointment.id.desc()).filter_by(id=appointment_id).first()
+ if not appointment:
+ return return_json(success=False, message="Appointment not found", status_code=404)
+ if appointment.patient_id != pm_user_data.user_data.id:
+ return return_json(success=False, message="You are not authorized to edit this appointment", status_code=403)
+ if appointment.approved:
+ return return_json(success=False, message="Appointment already approved", status_code=403)
+
+ appointment.appointment_time = appointment_time
+ appointment.description = description
+ appointment.category = category
+ db.session.commit()
+
+ return return_json(success=True, message="Appointment edited successfully")
+
+
+@app.route('/patient/cancel_appointment', methods=['DELETE'])
+@permission_check(UserPermissionLevel.PATIENT)
+def cancel_appointment(pm_user_data: UserLoginInfo):
+ data = request.get_json()
+ appointment_id = data.get("appointment_id")
+
+ if not appointment_id:
+ return return_json(success=False, message="Appointment ID is required", status_code=400)
+
+ appointment: Appointment = Appointment.query.order_by(Appointment.id.desc()).filter_by(id=appointment_id).first()
+ if not appointment:
+ return return_json(success=False, message="Appointment not found", status_code=404)
+ if appointment.patient_id != pm_user_data.user_data.id:
+ return return_json(success=False, message="You are not authorized to cancel this appointment", status_code=403)
+ if appointment.approved:
+ return return_json(success=False, message="Appointment already approved", status_code=403)
+
+ db.session.delete(appointment)
+ db.session.commit()
+
+ return return_json(success=True, message="Appointment cancelled successfully")
+
+@app.route("/patient/get_my_ward", methods=["GET"])
+@permission_check(UserPermissionLevel.PATIENT)
+def get_my_ward(pm_user_data: UserLoginInfo):
+ admission: Admission = Admission.query.filter_by(patient_id=pm_user_data.user_data.id).first()
+ if not admission:
+ return return_json(success=True, message="Patient not admitted", status_code=404)
+
+ ward: Ward = Ward.query.filter_by(id=admission.ward_id).first()
+ if not ward:
+ return return_json(success=True, message="Ward not found", status_code=404)
+
+ return return_json(success=True, message="Ward retrieved successfully", data={"ward": ward.to_dict()})
+
+
+@app.route("/receptionist/appointments", methods=["GET"])
+@permission_check(UserPermissionLevel.DOCTOR, receptionist_only=True)
+def get_receptionist_appointments():
+ count_limit = request.args.get("limit", BaseDefine.DEFAULT_PER_PAGE, type=int)
+ page = request.args.get("page", 1, type=int)
+ include_discharged = request.args.get("include_discharged", "false").lower() == "true"
+
+ appointments_query = Appointment.query.order_by(Appointment.id.desc()).filter_by(discharged=False) if not include_discharged else Appointment.query.order_by(Appointment.id.desc())
+ appointments = appointments_query.paginate(page=page, per_page=count_limit, max_per_page=BaseDefine.MAX_PER_PAGE,
+ error_out=False)
+
+ if not appointments.items:
+ return return_json(success=True, message="Appointments retrieved successfully",
+ data={"appointments": [], "total_pages": 1})
+
+ appointment_list = [appointment.to_dict() for appointment in appointments.items]
+ return return_json(success=True, message="Appointments retrieved successfully",
+ data={"appointments": appointment_list, "total_pages": appointments.pages})
+
+
+@app.route("/receptionist/process_appointment", methods=["POST"])
+@permission_check(UserPermissionLevel.DOCTOR, receptionist_only=True)
+def process_appointment():
+ data = request.get_json()
+ appointment_id = data.get("appointment_id")
+ approved = data.get("approved", False)
+ feedback = data.get("feedback")
+ assigned_team = data.get("assigned_team")
+
+ appointment: Appointment = Appointment.query.order_by(Appointment.id.desc()).filter_by(id=appointment_id).first()
+ if not appointment:
+ return return_json(success=False, message="Appointment not found", status_code=404)
+
+ appointment.approved = approved
+ appointment.feedback = feedback
+ if approved:
+ appointment.assigned_team = assigned_team
+ else:
+ appointment.assigned_team = None
+ db.session.commit()
+
+ return return_json(success=True, message="Appointment processed successfully")
+
+
+@app.route("/receptionist/check_in", methods=["POST"])
+@permission_check(UserPermissionLevel.DOCTOR, receptionist_only=True)
+def check_in_appointment():
+ data = request.get_json()
+ appointment_id = data.get("appointment_id")
+ ward_id = data.get("ward_id")
+
+ appointment: Appointment = Appointment.query.order_by(Appointment.id.desc()).filter_by(id=appointment_id).first()
+ if not appointment:
+ return return_json(success=False, message="Appointment not found", status_code=500)
+ if appointment.admitted:
+ return return_json(success=False, message="Patient already admitted", status_code=403)
+ if Admission.query.filter_by(patient_id=appointment.patient_id).first():
+ return return_json(success=False, message="Patient already admitted", status_code=403)
+
+ team: Team = Team.query.filter_by(id=appointment.assigned_team).first()
+ if not team:
+ return return_json(success=False, message="Team not found", status_code=500)
+
+ ward: Ward = Ward.query.filter_by(id=ward_id).first()
+ if not ward:
+ return return_json(success=False, message="Ward not found", status_code=500)
+
+ if ward.total_capacity <= ward.current_occupancy:
+ return return_json(success=False, message="Ward is full", status_code=403)
+
+ ward.current_occupancy += 1
+ appointment.admitted = True
+ admission = Admission(appointment_id=appointment_id, patient_id=appointment.patient_id, ward_id=ward_id,
+ team_id=appointment.assigned_team, consultant_id=team.consultant_id,
+ admitted_at=datetime.datetime.now())
+ db.session.add(admission)
+ db.session.commit()
+
+ return return_json(success=True, message="Appointment checked in successfully")
+
+
+@app.route("/doctor/appointments", methods=["GET"])
+@permission_check(UserPermissionLevel.DOCTOR)
+def get_doctor_appointments(pm_user_data: UserLoginInfo):
+ target_team = request.args.get("target_team", type=int)
+
+ if target_team is not None:
+ if pm_user_data.user_permission < UserPermissionLevel.ADMIN:
+ return return_json(success=False, message="Permission denied", status_code=403)
+ if target_team == -1:
+ doctor_teams = Team.query.all()
+ else:
+ team = Team.query.filter_by(id=target_team).first()
+ doctor_teams = [team] if team else []
+ else:
+ doctor_teams = utils.get_doctor_teams(pm_user_data.user_data.id)
+ if not doctor_teams:
+ return return_json(success=False, message="Doctor teams not found", status_code=404)
+
+ appointments = []
+ for i in doctor_teams:
+ team_appointments = Appointment.query.order_by(Appointment.id.desc()).filter_by(assigned_team=i.id, admitted=True,
+ discharged=False
+ ).all()
+ if team_appointments:
+ appointments.extend(team_appointments)
+
+ if not appointments:
+ return return_json(success=True, message="Appointments retrieved successfully",
+ data={"appointments": []})
+
+ appointment_list = []
+ for appointment in appointments:
+ appointment_data = appointment.to_dict()
+ admission: Admission = Admission.query.filter_by(appointment_id=appointment.id).first()
+ if admission:
+ appointment_data["admission"] = admission.to_dict()
+ else:
+ appointment_data["admission"] = None
+ appointment_list.append(appointment_data)
+
+ return return_json(success=True, message="Appointments retrieved successfully",
+ data={"appointments": appointment_list})
+
+
+@app.route("/doctor/treat", methods=["POST"])
+@permission_check(UserPermissionLevel.DOCTOR)
+def treat_appointment(pm_user_data: UserLoginInfo):
+ data = request.get_json()
+ appointment_id = data.get("appointment_id")
+ treat_info = data.get("treat_info")
+ treated_at = datetime.datetime.fromtimestamp(data.get("treated_at", datetime.datetime.now().timestamp()))
+
+ if not all([appointment_id, treat_info]):
+ return return_json(success=False, message="Appointment ID and treatment info are required", status_code=400)
+
+ appointment: Appointment = Appointment.query.order_by(Appointment.id.desc()).filter_by(id=appointment_id).first()
+ if not appointment:
+ return return_json(success=False, message="Appointment not found", status_code=404)
+
+ treatment = Treatment(appointment_id=appointment.id, doctor_id=pm_user_data.user_data.id,
+ patient_id=appointment.patient_id, treated_at=treated_at, treat_info=treat_info)
+ db.session.add(treatment)
+ db.session.commit()
+
+ return return_json(success=True, message="Treatment recorded successfully")
+
+
+@app.route("/admin/transfer_ward", methods=["POST"])
+@permission_check(UserPermissionLevel.ADMIN)
+def transfer_ward():
+ data = request.get_json()
+ patient_id = data.get("patient_id")
+ target_ward_id = data.get("target_ward_id")
+
+ ward: Ward = Ward.query.filter_by(id=target_ward_id).first()
+ if not ward:
+ return return_json(success=False, message="Ward not found", status_code=404)
+ if ward.total_capacity <= ward.current_occupancy:
+ return return_json(success=False, message="Ward is full", status_code=403)
+
+ admission: Admission = Admission.query.filter_by(patient_id=patient_id).first()
+ if not admission:
+ return return_json(success=False, message="Admission not found", status_code=404)
+ orig_ward: Ward = Ward.query.filter_by(id=admission.ward_id).first()
+
+ if orig_ward:
+ orig_ward.current_occupancy -= 1
+ admission.ward_id = target_ward_id
+ ward.current_occupancy += 1
+ db.session.commit()
+
+ return return_json(success=True, message="Ward transferred successfully")
+
+
+@app.route("/admin/patient_discharge", methods=["POST"])
+@permission_check(UserPermissionLevel.ADMIN)
+def discharge_patient():
+ data = request.get_json()
+ admission_id = data.get("admission_id")
+
+ admission: Admission = Admission.query.filter_by(id=admission_id).first()
+ if not admission:
+ return return_json(success=False, message="Admission not found", status_code=404)
+
+ ward: Ward = Ward.query.filter_by(id=admission.ward_id).first()
+ if not ward:
+ return return_json(success=False, message="Ward not found", status_code=404)
+
+ appointment = Appointment.query.order_by(Appointment.id.desc()).filter_by(id=admission.appointment_id, admitted=True).all()
+ if not appointment:
+ return return_json(success=False, message="Appointment not found", status_code=404)
+
+ for n, _ in enumerate(appointment):
+ appointment[n].discharged = True
+
+ # appointment.discharged = True
+ ward.current_occupancy -= 1
+ db.session.delete(admission)
+ db.session.commit()
+
+ return return_json(success=True, message="Patient discharged successfully")
+
+
+@app.route("/admin/create_ward", methods=["POST"])
+@permission_check(UserPermissionLevel.ADMIN)
+def create_wards():
+ data = request.get_json()
+ ward_name = data.get("ward_name")
+ total_capacity = data.get("total_capacity")
+ ward_type = data.get("type")
+
+ if ward_type not in WardTypes._member_names_:
+ return return_json(success=False, message="Invalid ward type", status_code=400)
+
+ if not all([ward_name, total_capacity]):
+ return return_json(success=False, message="Ward name and total capacity are required", status_code=400)
+
+ if total_capacity > BaseDefine.MAX_PATIENTS_PER_WARD:
+ return return_json(success=False, message=f"Ward capacity exceeds maximum limit: "
+ f"{BaseDefine.MAX_PATIENTS_PER_WARD}", status_code=400)
+
+ ward = Ward(name=ward_name, type=ward_type, total_capacity=total_capacity, current_occupancy=0)
+ db.session.add(ward)
+ db.session.commit()
+
+ return return_json(success=True, message="Ward created successfully", data={"ward_id": ward.id})
+
+
+@app.route("/admin/edit_ward", methods=["POST"])
+@permission_check(UserPermissionLevel.ADMIN)
+def edit_wards():
+ data = request.get_json()
+ ward_id = data.get("ward_id")
+ ward_name = data.get("ward_name")
+ total_capacity = data.get("total_capacity")
+ ward_type = data.get("type")
+
+ if ward_type not in WardTypes._member_names_:
+ return return_json(success=False, message="Invalid ward type", status_code=400)
+
+ if not all([ward_id, ward_name, total_capacity]):
+ return return_json(success=False, message="Ward ID, name and total capacity are required", status_code=400)
+
+ if total_capacity > BaseDefine.MAX_PATIENTS_PER_WARD:
+ return return_json(success=False, message=f"Ward capacity exceeds maximum limit: "
+ f"{BaseDefine.MAX_PATIENTS_PER_WARD}", status_code=400)
+
+ ward: Ward = Ward.query.filter_by(id=ward_id).first()
+ if not ward:
+ return return_json(success=False, message="Ward not found", status_code=404)
+
+ ward.name = ward_name
+ ward.total_capacity = total_capacity
+ ward.type = ward_type
+
+ db.session.commit()
+
+ return return_json(success=True, message="Ward edited successfully")
+
+
+@app.route("/admin/delete_ward", methods=["DELETE"])
+@permission_check(UserPermissionLevel.ADMIN)
+def delete_wards():
+ data = request.get_json()
+ ward_id = data.get("ward_id")
+
+ if not ward_id:
+ return return_json(success=False, message="Ward ID is required", status_code=400)
+
+ ward: Ward = Ward.query.filter_by(id=ward_id).first()
+ if not ward:
+ return return_json(success=False, message="Ward not found", status_code=404)
+
+ db.session.delete(ward)
+ db.session.commit()
+
+ return return_json(success=True, message="Ward deleted successfully")
+
+
+@app.route("/admin/create_doctor_team", methods=["POST"])
+@permission_check(UserPermissionLevel.ADMIN)
+def create_doctor_team():
+ data = request.get_json()
+ department = data.get("department")
+ consultant_id = data.get("consultant_id")
+ is_admin_team = data.get("is_admin_team", False)
+ team_members = data.get("team_members", [])
+
+ if team_members is None:
+ team_members = []
+
+ if not all([department, consultant_id]):
+ return return_json(success=False, message="Department and consultant ID are required", status_code=400)
+
+ if consultant_id not in team_members:
+ return return_json(success=False, message="Consultant must be in team members", status_code=400)
+
+ if department not in Departments._member_names_:
+ return return_json(success=False, message="Invalid department", status_code=400)
+
+ if not Doctor.query.filter_by(id=consultant_id).first():
+ return return_json(success=False, message="Invalid consultant", status_code=400)
+
+ team = Team(department=department, consultant_id=consultant_id, is_admin_team=is_admin_team)
+ db.session.add(team)
+ db.session.flush()
+
+ if team.id is None:
+ return return_json(success=False, message="Failed to create team record", status_code=500)
+
+ for i in team_members:
+ doctor_team_role = DoctorTeamRole(doctor_id=i, team_id=team.id)
+ db.session.add(doctor_team_role)
+
+ db.session.commit()
+
+ return return_json(success=True, message="Doctor team created successfully", data={"team_id": team.id})
+
+
+@app.route("/admin/delete_doctor_team", methods=["DELETE"])
+@permission_check(UserPermissionLevel.ADMIN)
+def delete_doctor_team():
+ data = request.get_json()
+ team_id = data.get("team_id")
+
+ team: Team = Team.query.filter_by(id=team_id).first()
+ if not team:
+ return return_json(success=False, message="Team not found", status_code=404)
+
+ # Check if there are any admissions with the same team_id
+ admissions = Admission.query.filter_by(team_id=team.id).all()
+ if admissions:
+ return return_json(success=False, message="Cannot delete team with existing admissions", status_code=403)
+
+ DoctorTeamRole.query.filter_by(team_id=team.id).delete()
+ db.session.delete(team)
+ db.session.commit()
+
+ return return_json(success=True, message="Doctor team deleted successfully")
+
+
+@app.route("/admin/register", methods=["POST"])
+@permission_check(UserPermissionLevel.ADMIN)
+def admin_register():
+ data = request.get_json()
+ reg_type = data.get('reg_type') # Patient: 0, Doctor: 1, Receptionist: 99
+
+ username = data.get('username')
+ password = utils.generate_randstring(6)
+
+ name = data.get('name')
+ title = data.get('title')
+ birth_date = datetime.datetime.fromtimestamp(data.get('birth_date'), datetime.timezone.utc)
+ gender = data.get('gender')
+ phone = data.get('phone')
+ email = data.get('email')
+
+ address = data.get('address') # patient
+ postcode = data.get('postcode') # patient
+
+ grade = data.get('grade') # doctor
+ team_id = data.get('team_id') # doctor, 留 None 则不添加团队, Receptionist 不生效
+ is_admin = data.get('is_admin', False) # doctor
+
+ if User.query.filter_by(username=username).first():
+ return return_json(success=False, message="Username already exists", status_code=409)
+
+ if reg_type == 0: # Patient
+ patient = Patient(name=name, title=title, birth_date=birth_date, gender=gender, phone=phone, email=email,
+ postcode=postcode, address=address)
+ db.session.add(patient)
+
+ db.session.flush()
+ if patient.id is None:
+ return return_json(success=False, message="Failed to create patient record", status_code=500)
+
+ user = User(username=username, role=UserType.PATIENT.value, patient_id=patient.id)
+ user.set_password(password)
+ db.session.add(user)
+ elif (reg_type == 1) or (reg_type == 99): # Doctor or Receptionist
+ if grade not in DoctorGrade._value2member_map_:
+ return return_json(success=False, message="Invalid doctor grade", status_code=400)
+
+ doctor = Doctor(name=name, title=title, birth_date=birth_date, gender=gender, phone=phone, email=email,
+ grade=grade)
+ db.session.add(doctor)
+
+ db.session.flush()
+ if doctor.id is None:
+ return return_json(success=False, message="Failed to create doctor record", status_code=500)
+
+ user = User(username=username, role=UserType.DOCTOR.value, doctor_id=doctor.id)
+ user.set_password(password)
+
+ if is_admin:
+ admin_team = Team.query.filter_by(id=1).first() # Admin team must be No.1
+ if not admin_team:
+ return return_json(success=False, message="Admin team not found", status_code=500)
+ admin_team_role = DoctorTeamRole(doctor_id=doctor.id, team_id=admin_team.id)
+ db.session.add(admin_team_role)
+
+ if reg_type == 99:
+ team = Team.query.filter_by(department="Reception").first()
+ if not team:
+ return return_json(success=False, message="Reception team not found", status_code=500)
+ doctor_team_role = DoctorTeamRole(doctor_id=doctor.id, team_id=team.id)
+ db.session.add(doctor_team_role)
+ else:
+ if team_id is not None:
+ team = Team.query.filter_by(id=team_id).first()
+ if not team:
+ return return_json(success=False, message="Team not found", status_code=500)
+ doctor_team_role = DoctorTeamRole(doctor_id=doctor.id, team_id=team.id)
+ db.session.add(doctor_team_role)
+
+ db.session.add(user)
+ db.session.commit()
+
+ return return_json(success=True, message="User registered successfully", data={"username": username,
+ "password": password})
+
+
+@app.route("/admin/edit_team_info", methods=["POST"])
+@permission_check(UserPermissionLevel.ADMIN)
+def edit_team_info():
+ data = request.get_json()
+ team_id = data.get("team_id")
+ department = data.get("department")
+ consultant_id = data.get("consultant_id")
+ is_admin_team = data.get("is_admin_team", False)
+ team_members = data.get("team_members", None)
+
+ if not all([team_id, department, consultant_id]):
+ return return_json(success=False, message="Team ID, department and consultant ID are required", status_code=400)
+
+ if team_members is not None:
+ if consultant_id not in team_members:
+ return return_json(success=False, message="Consultant must be in team members", status_code=400)
+
+ if department not in Departments._member_names_:
+ return return_json(success=False, message="Invalid department", status_code=400)
+
+ if not Doctor.query.filter_by(id=consultant_id).first():
+ return return_json(success=False, message="Invalid consultant", status_code=400)
+
+ team: Team = Team.query.filter_by(id=team_id).first()
+ if not team:
+ return return_json(success=False, message="Team not found", status_code=404)
+
+ if team_members is not None:
+ DoctorTeamRole.query.filter_by(team_id=team.id).delete()
+
+ for i in team_members:
+ doctor_team_role = DoctorTeamRole(doctor_id=i, team_id=team.id)
+ db.session.add(doctor_team_role)
+
+ team.department = department
+ team.consultant_id = consultant_id
+ team.is_admin_team = is_admin_team
+
+ db.session.commit()
+
+ return return_json(success=True, message="Doctor team info updated successfully")
+
+
+@app.route("/admin/edit_doctor_info", methods=["POST"])
+@permission_check(UserPermissionLevel.PATIENT)
+def edit_doctor_info(pm_user_data: UserLoginInfo):
+ data = request.get_json()
+ doctor_id = data.get("doctor_id")
+
+ name = data.get('name')
+ title = data.get('title')
+ birth_date = datetime.datetime.fromtimestamp(data.get('birth_date'), datetime.timezone.utc)
+ gender = data.get('gender')
+ phone = data.get('phone')
+ email = data.get('email')
+ grade = data.get('grade', DoctorGrade.Unknown.value)
+
+ is_admin = data.get('is_admin', False)
+
+ if pm_user_data.user_permission < UserPermissionLevel.ADMIN:
+ if pm_user_data.user_type == UserType.DOCTOR:
+ if pm_user_data.user_data.id != doctor_id:
+ return return_json(success=False, message="You are not authorized to edit this doctor", status_code=403)
+ else:
+ return return_json(success=False, message="You are not authorized to edit this doctor", status_code=403)
+
+ doctor: Doctor = Doctor.query.filter_by(id=doctor_id).first()
+ if not doctor:
+ return return_json(success=False, message="Doctor not found", status_code=404)
+ if grade not in DoctorGrade._value2member_map_:
+ return return_json(success=False, message="Invalid doctor grade", status_code=400)
+ doctor.name = name
+ doctor.title = title
+ doctor.birth_date = birth_date
+ doctor.gender = gender
+ doctor.phone = phone
+ doctor.email = email
+ doctor.grade = grade
+
+ doctor_team_roles = DoctorTeamRole.query.filter_by(doctor_id=doctor_id, team_id=1).all()
+ if is_admin and not doctor_team_roles:
+ admin_team_role = DoctorTeamRole(doctor_id=doctor.id, team_id=1)
+ db.session.add(admin_team_role)
+ elif not is_admin and doctor_team_roles:
+ for team_role in doctor_team_roles:
+ db.session.delete(team_role)
+
+ db.session.commit()
+ return return_json(success=True, message="Doctor info updated successfully")
+
+
+@app.route("/admin/edit_patient_info", methods=["POST"])
+@permission_check(UserPermissionLevel.PATIENT)
+def edit_patient_info(pm_user_data: UserLoginInfo):
+ data = request.get_json()
+ patient_id = data.get("patient_id")
+
+ name = data.get('name')
+ title = data.get('title')
+ birth_date = datetime.datetime.fromtimestamp(data.get('birth_date'), datetime.timezone.utc)
+ gender = data.get("gender")
+ phone = data.get('phone')
+ email = data.get('email')
+ postcode = data.get('postcode')
+ address = data.get('address')
+
+ if pm_user_data.user_permission < UserPermissionLevel.ADMIN:
+ if pm_user_data.user_type.value == UserType.PATIENT.value:
+ if pm_user_data.user_data.id != patient_id:
+ return return_json(success=False, message="You are not authorized to edit this patient", status_code=403)
+ else:
+ return return_json(success=False, message="You are not authorized to edit this patient", status_code=403)
+
+ patient: Patient = Patient.query.filter_by(id=patient_id).first()
+ if not patient:
+ return return_json(success=False, message="Patient not found", status_code=404)
+
+ patient.name = name
+ patient.title = title
+ patient.birth_date = birth_date
+ patient.gender = gender
+ patient.phone = phone
+ patient.email = email
+ patient.postcode = postcode
+ patient.address = address
+
+ db.session.commit()
+ return return_json(success=True, message="Patient info updated successfully")
+
+
+
+@app.route("/admin/set_resign_doctor", methods=["POST"])
+@permission_check(UserPermissionLevel.ADMIN)
+def resign_doctor():
+ data = request.get_json()
+ doctor_id = data.get("doctor_id")
+ is_resigned = data.get("is_resigned")
+
+ doctor: Doctor = Doctor.query.filter_by(id=doctor_id).first()
+ if not doctor:
+ return return_json(success=False, message="Doctor not found", status_code=404)
+
+ doctor.is_resigned = is_resigned
+ db.session.commit()
+
+ return return_json(success=True, message="Doctor resigned successfully")
+
+
+@app.route("/admin/reset_user_password", methods=["POST"])
+@permission_check(UserPermissionLevel.ADMIN)
+def reset_user_password():
+ data = request.get_json()
+ username = data.get("username")
+ password = utils.generate_randstring(6)
+
+ user: User = User.query.filter_by(username=username).first()
+ if not user:
+ return return_json(success=False, message="User not found", status_code=404)
+
+ if user.role == UserType.PATIENT.value:
+ Credentials.query.filter_by(patient_id=user.patient_id).delete()
+ elif user.role == UserType.DOCTOR.value:
+ Credentials.query.filter_by(doctor_id=user.doctor_id).delete()
+
+ user.set_password(password)
+ db.session.commit()
+
+ return return_json(success=True, message="User password reset successfully", data={
+ "username": user.username,
+ "password": password
+ })
+
+
+@app.route("/admin/reset_user_password_from_role_id", methods=["POST"])
+@permission_check(UserPermissionLevel.ADMIN)
+def reset_user_password_from_id():
+ data = request.get_json()
+ user_id = data.get("user_id")
+ password = utils.generate_randstring(6)
+ user_type = data.get("user_type")
+
+ if user_type == UserType.PATIENT.value:
+ user: User = User.query.filter_by(patient_id=user_id).first()
+ elif user_type == UserType.DOCTOR.value:
+ user: User = User.query.filter_by(doctor_id=user_id).first()
+ else:
+ return return_json(success=False, message="Invalid user type", status_code=400)
+
+ if not user:
+ return return_json(success=False, message="User not found", status_code=404)
+
+ if user.role == UserType.PATIENT.value:
+ Credentials.query.filter_by(patient_id=user.patient_id).delete()
+ elif user.role == UserType.DOCTOR.value:
+ Credentials.query.filter_by(doctor_id=user.doctor_id).delete()
+
+ user.set_password(password)
+ db.session.commit()
+
+ return return_json(success=True, message="User password reset successfully", data={
+ "username": user.username,
+ "password": password
+ })
+
+
+@app.route("/admin/get_patient_list", methods=["GET"])
+@permission_check(UserPermissionLevel.PATIENT)
+def get_user_list():
+ count_limit = request.args.get("limit", BaseDefine.DEFAULT_PER_PAGE, type=int)
+ page = request.args.get("page", 1, type=int)
+
+ if page == -1:
+ patients = Patient.query.paginate(per_page=9999999, max_per_page=None, error_out=False)
+ else:
+ patients = Patient.query.paginate(page=page, per_page=count_limit, max_per_page=BaseDefine.MAX_PER_PAGE,
+ error_out=False)
+ if not patients.items:
+ return return_json(success=True, message="No patients found", data={"patients": [], "total_pages": 1})
+ patient_list = []
+ for i in patients.items:
+ admission: Admission = Admission.query.filter_by(patient_id=i.id).first()
+ ward: Ward = Ward.query.filter_by(id=admission.ward_id).first() if admission else None
+
+ add_data = i.to_dict()
+ add_data["admission"] = admission.to_dict() if admission else None
+ add_data["ward"] = ward.to_dict() if ward else None
+
+ patient_list.append(add_data)
+
+ return return_json(success=True, message="Patients retrieved successfully",
+ data={"patients": patient_list, "total_pages": patients.pages})
+
+
+@app.route("/data/ward_patients", methods=["GET"])
+@permission_check(UserPermissionLevel.DOCTOR)
+def get_ward_patients():
+ ward_id = request.args.get("ward_id", type=int)
+
+ if not ward_id:
+ return return_json(success=False, message="Ward ID is required", status_code=400)
+ ward: Ward = Ward.query.filter_by(id=ward_id).first()
+ if not ward:
+ return return_json(success=False, message="Ward not found", status_code=404)
+ admissions = Admission.query.filter_by(ward_id=ward_id).all()
+ if not admissions:
+ return return_json(success=True, message="No patients found",
+ data={"ward": ward.to_dict(), "patients": []})
+
+ patient_list = []
+ for i in admissions:
+ patient: Patient = Patient.query.filter_by(id=i.patient_id).first()
+ if patient:
+ patient_list.append(patient.to_dict())
+
+ return return_json(success=True, message="Patients retrieved successfully",
+ data={"ward": ward.to_dict(), "patients": patient_list})
+
+
+@app.route("/data/team_patients", methods=["GET"])
+@permission_check(UserPermissionLevel.DOCTOR)
+def get_team_patients():
+ count_limit = request.args.get("limit", BaseDefine.DEFAULT_PER_PAGE, type=int)
+ page = request.args.get("page", 1, type=int)
+ if page < 1:
+ page = 1
+ team_id = request.args.get("team_id", type=int)
+
+ if not team_id:
+ return return_json(success=False, message="Team ID is required", status_code=400)
+ team: Team = Team.query.filter_by(id=team_id).first()
+ if not team:
+ return return_json(success=False, message="Team not found", status_code=404)
+ admissions = Admission.query.filter_by(team_id=team_id).paginate(page=page, per_page=count_limit,
+ max_per_page=BaseDefine.MAX_PER_PAGE,
+ error_out=False)
+ if not admissions.items:
+ return return_json(success=True, message="No patients found",
+ data={"team": team.to_dict(), "patients": [], "total_pages": admissions.pages})
+
+ patient_list = []
+ for i in admissions.items:
+ patient: Patient = Patient.query.filter_by(id=i.patient_id).first()
+ ward: Ward = Ward.query.filter_by(id=i.ward_id).first()
+ if patient:
+ add_dict = patient.to_dict()
+ add_dict["admission"] = i.to_dict()
+ add_dict["ward"] = ward.to_dict() if ward else {}
+ patient_list.append(add_dict)
+
+ return return_json(success=True, message="Patients retrieved successfully",
+ data={"team": team.to_dict(), "patients": patient_list, "total_pages": admissions.pages})
+
+
+@app.route("/data/treatment_doctors", methods=["GET"])
+@permission_check(UserPermissionLevel.PATIENT)
+def get_treatment_doctors(pm_user_data: UserLoginInfo):
+ count_limit = request.args.get("limit", BaseDefine.DEFAULT_PER_PAGE, type=int)
+ page = request.args.get("page", 1, type=int)
+ if page < 1:
+ page = 1
+ patient_id = request.args.get("patient_id", type=int)
+ doctor_id = request.args.get("doctor_id", type=int)
+ team_id = request.args.get("team_id", type=int)
+ display_all = request.args.get("all", type=str).lower() == "true"
+
+ if pm_user_data.user_type.value == UserType.PATIENT.value:
+ if patient_id is not None:
+ if pm_user_data.user_data.id != patient_id:
+ return return_json(success=False, message="You are not authorized to view this patient's treatments",
+ status_code=403)
+ else:
+ return return_json(success=False, message="Permission denied", status_code=403)
+
+ if display_all:
+ if pm_user_data.user_permission < UserPermissionLevel.ADMIN:
+ return return_json(success=False, message="Permission denied", status_code=403)
+
+ if display_all:
+ treatments = Treatment.query.paginate(page=page, per_page=count_limit, max_per_page=BaseDefine.MAX_PER_PAGE,
+ error_out=False)
+
+ elif patient_id is not None:
+ patient: Patient = Patient.query.filter_by(id=patient_id).first()
+ if not patient:
+ return return_json(success=False, message="Patient not found", status_code=404)
+
+ treatments = Treatment.query.filter_by(patient_id=patient_id).paginate(page=page, per_page=count_limit,
+ max_per_page=BaseDefine.MAX_PER_PAGE,
+ error_out=False)
+ elif doctor_id is not None:
+ doctor: Doctor = Doctor.query.filter_by(id=doctor_id).first()
+ if not doctor:
+ return return_json(success=False, message="Doctor not found", status_code=404)
+ treatments = Treatment.query.filter_by(doctor_id=doctor_id).paginate(page=page, per_page=count_limit,
+ max_per_page=BaseDefine.MAX_PER_PAGE,
+ error_out=False)
+ elif team_id is not None:
+ appointments = Appointment.query.order_by(Appointment.id.desc()).filter_by(assigned_team=team_id
+ # , discharged=False
+ ).all()
+ if not appointments:
+ return return_json(success=True, message="No patients found", status_code=404, data={"treatments": [],
+ "total_pages": 1})
+ treatment_ids = [i.id for i in appointments]
+ treatments = (Treatment.query.filter(Treatment.appointment_id.in_(treatment_ids))
+ .paginate(page=page, per_page=count_limit, max_per_page=BaseDefine.MAX_PER_PAGE, error_out=False))
+ else:
+ return return_json(success=False, message="Patient ID or Doctor ID or Team ID is required", status_code=400)
+
+ if not treatments.items:
+ return return_json(success=True, message="No treatment found",
+ data={
+ # "patient": patient.to_dict(),
+ "treatments": [],
+ "total_pages": treatments.pages
+ })
+
+ treatment_data = []
+ for i in treatments.items:
+ doctor: Doctor = Doctor.query.filter_by(id=i.doctor_id).first()
+ add_dict = i.to_dict()
+ add_dict["doctor_info"] = doctor.to_dict() if doctor else None
+
+ appointment: Appointment = Appointment.query.order_by(Appointment.id.desc()).filter_by(id=i.appointment_id).first()
+ if appointment and (appointment.assigned_team is not None):
+ doctor_team: Optional[Team] = Team.query.filter_by(id=appointment.assigned_team).first()
+ else:
+ doctor_team = None
+
+ add_dict["team"] = doctor_team.to_dict() if doctor_team else None
+
+ treatment_data.append(add_dict)
+
+ return return_json(success=True, message="Doctors retrieved successfully",
+ data={
+ # "patient": patient.to_dict(),
+ "treatments": treatment_data,
+ "total_pages": treatments.pages
+ })
diff --git a/HMS_Backend/HMS_Backend/models.py b/HMS_Backend/HMS_Backend/models.py
new file mode 100644
index 0000000..4cfa248
--- /dev/null
+++ b/HMS_Backend/HMS_Backend/models.py
@@ -0,0 +1,271 @@
+from flask_sqlalchemy import SQLAlchemy
+from flask import Flask
+from flask_cors import CORS
+from werkzeug.security import generate_password_hash, check_password_hash
+from datetime import datetime as dt
+from datetime import date as dt_date
+from typing import Optional, Union
+from enum import Enum
+
+app = Flask(__name__)
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///hospital.db'
+CORS(app, supports_credentials=True)
+
+db = SQLAlchemy(app)
+
+
+class MIntEnum(int, Enum):
+ def __eq__(self, other):
+ if isinstance(other, Enum):
+ return self.value == other.value
+ if isinstance(other, int):
+ return self.value == other
+ return super().__eq__(other)
+
+ def __int__(self):
+ return int(self.value)
+
+
+class MStrEnum(str, Enum):
+ def __eq__(self, other):
+ if isinstance(other, Enum):
+ return self.value == other.value
+ if isinstance(other, str):
+ return self.value == other
+ return super().__eq__(other)
+
+ def __str__(self):
+ return str(self.value)
+
+
+class UserType(MIntEnum):
+ UNAUTHORIZED = -1
+ PATIENT = 0
+ DOCTOR = 1
+
+
+class UserPermissionLevel(MIntEnum):
+ UNAUTHORIZED = -1
+ PATIENT = 0
+ DOCTOR = 1
+ # CONSULTANT = 2
+ ADMIN = 3
+
+
+class DoctorTeamType(MIntEnum):
+ DOCTOR_TEAM = 0
+ RECEPTIONIST_TEAM = 2
+ ADMIN_TEAM = 4
+
+
+class DoctorGrade(MIntEnum):
+ Unknown = 0
+ Junior = 2
+ Registrar = 4
+ Consultant = 6
+
+
+class Departments(MStrEnum):
+ Internal_Medicine = "Internal Medicine" # 内科
+ Surgery = "Surgery" # 外科
+ Obstetrics_and_Gynecology = "Obstetrics and Gynecology" # 妇产科
+ Pediatrics = "Pediatrics" # 儿科
+ Ophthalmology = "Ophthalmology" # 眼科
+ Otolaryngology = "Otolaryngology (ENT)" # 耳鼻喉科
+ Dermatology = "Dermatology" # 皮肤科
+ Psychiatry = "Psychiatry" # 精神科
+ Neurology = "Neurology" # 神经科
+ Cardiology = "Cardiology" # 心内科
+ Radiology = "Radiology" # 放射科
+ Emergency_Department = "Emergency Department" # 急诊科
+ ICU = "Intensive Care Unit (ICU)" # 重症监护室
+ Oncology = "Oncology" # 肿瘤科
+ Orthopedics = "Orthopedics" # 骨科
+ Urology = "Urology" # 泌尿外科
+ Rehabilitation_Medicine = "Rehabilitation Medicine" # 康复医学科
+ Dentistry = "Dentistry / Oral and Maxillofacial Surgery" # 口腔科
+ Traditional_Chinese_Medicine = "Traditional Chinese Medicine" # 中医科
+
+ Reception = "Reception" # 接待
+ Admin = "Admin"
+
+
+class WardTypes(MStrEnum):
+ GENERAL = "General Ward" # 普通病房
+ ICU = "Intensive Care Unit" # 重症监护室
+ ISOLATION = "Isolation" # 隔离病房
+ MATERNITY = "Maternity" # 产科病房
+ PEDIATRIC = "Pediatric" # 儿科病房
+
+
+class BaseDefine:
+ categories = [
+ "consultation",
+ "follow_up",
+ "admission",
+ "diagnostic",
+ "therapy",
+ "surgery"
+ ]
+
+ MAX_PER_PAGE = 50
+ DEFAULT_PER_PAGE = 50 # 16
+ MAX_PATIENTS_PER_WARD = 12
+
+
+class BaseModel(db.Model):
+ __abstract__ = True
+
+ def to_dict(self, exclude: set = None):
+ exclude = exclude or set()
+ ret = {}
+ for c in self.__table__.columns:
+ if c.name not in exclude:
+ curr_item = getattr(self, c.name)
+ if isinstance(curr_item, dt):
+ ret[c.name] = int(curr_item.timestamp())
+ elif isinstance(curr_item, dt_date):
+ ret[c.name] = curr_item.strftime("%d/%m/%Y")
+ elif isinstance(curr_item, Enum):
+ ret[c.name] = curr_item.value
+ else:
+ ret[c.name] = curr_item
+
+ return ret
+
+
+# 病房
+class Ward(BaseModel):
+ __tablename__ = 'wards'
+ id: int = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ name: str = db.Column(db.Text, nullable=False)
+ type: str = db.Column(db.Text, nullable=False)
+ total_capacity: int = db.Column(db.Integer, nullable=False)
+ current_occupancy: int = db.Column(db.Integer, nullable=False)
+
+
+# 医生基础信息
+class Doctor(BaseModel):
+ __tablename__ = 'doctors'
+ id: int = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ name: str = db.Column(db.Text, nullable=False)
+ title: str = db.Column(db.Text, nullable=False)
+ birth_date: dt_date = db.Column(db.Date, nullable=False)
+ gender: str = db.Column(db.Text, nullable=False)
+ phone: str = db.Column(db.Text, nullable=False)
+ email: str = db.Column(db.Text, nullable=False)
+ grade: int = db.Column(db.Integer, nullable=False)
+ is_resigned: bool = db.Column(db.Boolean, default=0, nullable=False) # 是否离职
+
+
+# 医生团队
+class Team(BaseModel):
+ __tablename__ = 'teams'
+ id: int = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ department: str = db.Column(db.Text, nullable=False) # Departments
+ consultant_id: int = db.Column(db.Integer, db.ForeignKey('doctors.id'), nullable=False)
+ is_admin_team: bool = db.Column(db.Boolean, default=0, nullable=False) # 是否为管理员团队
+
+ def get_team_type(self) -> DoctorTeamType:
+ if self.is_admin_team:
+ return DoctorTeamType.ADMIN_TEAM
+ elif self.department == 'Reception':
+ return DoctorTeamType.RECEPTIONIST_TEAM
+ else:
+ return DoctorTeamType.DOCTOR_TEAM
+
+
+# 医生与团队的关联表
+class DoctorTeamRole(BaseModel):
+ __tablename__ = 'doctor_team_roles'
+ id: int = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ doctor_id: int = db.Column(db.Integer, db.ForeignKey('doctors.id'), nullable=False)
+ team_id: int = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
+
+
+# 患者
+class Patient(BaseModel):
+ __tablename__ = 'patients'
+ id: int = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ name: str = db.Column(db.Text, nullable=False)
+ title: str = db.Column(db.Text, nullable=False)
+ birth_date: dt_date = db.Column(db.Date, nullable=False)
+ gender: str = db.Column(db.Text, nullable=False)
+ phone: str = db.Column(db.Text, nullable=False)
+ email: str = db.Column(db.Text, nullable=False)
+ postcode: str = db.Column(db.Text, nullable=False)
+ address: str = db.Column(db.Text, nullable=False)
+
+
+# 患者住院信息
+class Admission(BaseModel):
+ __tablename__ = 'admissions'
+ id: int = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ appointment_id: int = db.Column(db.Integer, db.ForeignKey('appointments.id'), nullable=False)
+ patient_id: int = db.Column(db.Integer, db.ForeignKey('patients.id'), nullable=False)
+ ward_id: int = db.Column(db.Integer, db.ForeignKey('wards.id'), nullable=False)
+ team_id: int = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
+ consultant_id: int = db.Column(db.Integer, db.ForeignKey('doctors.id'), nullable=False)
+ admitted_at: dt = db.Column(db.DateTime, nullable=False)
+ # discharged_at: Optional[dt] = db.Column(db.DateTime)
+
+
+# 治疗记录
+class Treatment(BaseModel):
+ __tablename__ = 'treatments'
+ id: int = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ appointment_id: int = db.Column(db.Integer, db.ForeignKey('appointments.id'), nullable=False)
+ doctor_id: int = db.Column(db.Integer, db.ForeignKey('doctors.id'), nullable=False)
+ patient_id: int = db.Column(db.Integer, db.ForeignKey('patients.id'), nullable=False)
+ treated_at: dt = db.Column(db.DateTime, nullable=False)
+ treat_info: str = db.Column(db.Text, nullable=False)
+
+
+# 预约
+class Appointment(BaseModel):
+ __tablename__ = 'appointments'
+ id = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ patient_id: int = db.Column(db.Integer, db.ForeignKey('patients.id'), nullable=False)
+ category: str = db.Column(db.Text, nullable=False)
+ appointment_time: dt = db.Column(db.DateTime, nullable=False)
+ description: str = db.Column(db.Text, nullable=False)
+ approved: bool = db.Column(db.Boolean, default=False, nullable=False)
+ feedback: Optional[str] = db.Column(db.Text)
+ assigned_team: int = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True)
+ admitted: bool = db.Column(db.Boolean, default=False, nullable=False)
+ discharged: bool = db.Column(db.Boolean, default=False, nullable=False)
+
+
+# 用户表(用于存储登录信息,关联患者或医生信息)
+class User(BaseModel):
+ __tablename__ = 'users'
+ id: int = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ username: str = db.Column(db.Text, unique=True, nullable=False)
+ password_hash: str = db.Column(db.Text, nullable=False)
+ role: int = db.Column(db.Integer, nullable=False) # 0: 患者, 1: 医生
+ patient_id: Optional[int] = db.Column(db.Integer, db.ForeignKey('patients.id'))
+ doctor_id: Optional[int] = db.Column(db.Integer, db.ForeignKey('doctors.id'))
+
+ def set_password(self, password: str) -> None:
+ self.password_hash = generate_password_hash(password)
+
+ def check_password(self, password: str) -> bool:
+ return check_password_hash(self.password_hash, password)
+
+
+# 登录凭据表
+class Credentials(BaseModel):
+ __tablename__ = 'credentials'
+ token: str = db.Column(db.Text, primary_key=True)
+ expire_at: dt = db.Column(db.DateTime, nullable=False)
+ role: int = db.Column(db.Integer, nullable=False) # 0: 患者, 1: 医生
+ patient_id: Optional[int] = db.Column(db.Integer, db.ForeignKey('patients.id'))
+ doctor_id: Optional[int] = db.Column(db.Integer, db.ForeignKey('doctors.id'))
+
+
+class UserLoginInfo:
+ def __init__(self, user_type: UserType, user_permission: UserPermissionLevel,
+ user_data: Optional[Union[Patient, Doctor]]):
+ self.user_type = user_type
+ self.user_permission = user_permission
+ self.user_data = user_data
diff --git a/HMS_Backend/HMS_Backend/utils.py b/HMS_Backend/HMS_Backend/utils.py
new file mode 100644
index 0000000..7897604
--- /dev/null
+++ b/HMS_Backend/HMS_Backend/utils.py
@@ -0,0 +1,98 @@
+import string
+import random
+import datetime
+from typing import Optional, Union, List, Tuple
+from . import models as m
+
+
+def generate_randstring(num=8):
+ value = ''.join(random.sample(string.ascii_letters + string.digits, num))
+ return value
+
+def get_doctor_is_resigned(doctor_id: int) -> bool:
+ doctor: Optional[m.Doctor] = m.Doctor.query.filter_by(id=doctor_id).first()
+ if not doctor:
+ return True
+ if doctor.is_resigned:
+ return True
+ return False
+
+
+def get_doctor_teams(doctor_id: int) -> List[m.Team]:
+ if get_doctor_is_resigned(doctor_id):
+ return []
+ doctor_team: Optional[List[m.DoctorTeamRole]] = m.DoctorTeamRole.query.filter_by(doctor_id=doctor_id).all()
+ if not doctor_team:
+ return []
+
+ ret = []
+ for team in doctor_team:
+ curr_team: m.Team = m.Team.query.filter_by(id=team.team_id).first()
+ ret.append(curr_team)
+
+ return ret
+
+
+def get_team_can_reception(team: m.Team) -> bool:
+ team_type = team.get_team_type()
+ if team_type == m.DoctorTeamType.RECEPTIONIST_TEAM:
+ return True
+ elif team_type == m.DoctorTeamType.ADMIN_TEAM:
+ return True
+ return False
+
+def get_doctor_is_admin(doctor_id: int) -> bool:
+ if get_doctor_is_resigned(doctor_id):
+ return False
+ doctor_teams = get_doctor_teams(doctor_id)
+ for team in doctor_teams:
+ if team.is_admin_team:
+ return True
+ return False
+
+def get_doctor_can_reception(doctor_id: int) -> bool:
+ if get_doctor_is_resigned(doctor_id):
+ return False
+ doctor_teams = get_doctor_teams(doctor_id)
+ if not doctor_teams:
+ return False
+ for team in doctor_teams:
+ if get_team_can_reception(team):
+ return True
+ return False
+
+
+def get_user_info_from_token(token: str) -> Optional[Union[m.Patient, m.Doctor]]:
+ credentials = m.Credentials.query.filter_by(token=token).first()
+ if not credentials:
+ return None
+ if credentials.expire_at < datetime.datetime.now():
+ return None
+ if credentials.role == 0:
+ user: m.Patient = m.Patient.query.filter_by(id=credentials.patient_id).first()
+ else:
+ user: m.Doctor = m.Doctor.query.filter_by(id=credentials.doctor_id).first()
+ return user
+
+
+def get_user_info_with_permission_from_token(token: str) -> m.UserLoginInfo:
+ user_info = get_user_info_from_token(token)
+ if not user_info:
+ return m.UserLoginInfo(m.UserType.UNAUTHORIZED, m.UserPermissionLevel.UNAUTHORIZED, None)
+
+ if isinstance(user_info, m.Patient): # 病人
+ return m.UserLoginInfo(m.UserType.PATIENT, m.UserPermissionLevel.PATIENT, user_info)
+ elif isinstance(user_info, m.Doctor):
+ if user_info.is_resigned: # 离职医生
+ return m.UserLoginInfo(m.UserType.UNAUTHORIZED, m.UserPermissionLevel.UNAUTHORIZED, None)
+ user_permission = m.UserPermissionLevel.DOCTOR # 医生
+ doctor_teams = get_doctor_teams(user_info.id)
+ if doctor_teams:
+ for team in doctor_teams:
+ if team.is_admin_team: # 管理员团队
+ user_permission = m.UserPermissionLevel.ADMIN
+ break
+
+ return m.UserLoginInfo(m.UserType.DOCTOR, user_permission, user_info)
+ else:
+ return m.UserLoginInfo(m.UserType.UNAUTHORIZED, m.UserPermissionLevel.UNAUTHORIZED, None)
diff --git a/HMS_Backend/instance/hospital.db b/HMS_Backend/instance/hospital.db
new file mode 100644
index 0000000..50b30dc
Binary files /dev/null and b/HMS_Backend/instance/hospital.db differ
diff --git a/HMS_Backend/main.py b/HMS_Backend/main.py
new file mode 100644
index 0000000..3fa00d6
--- /dev/null
+++ b/HMS_Backend/main.py
@@ -0,0 +1,6 @@
+import HMS_Backend
+
+
+if __name__ == "__main__":
+ # Initialize the backend
+ HMS_Backend.app.run("0.0.0.0", port=721)
diff --git a/HMS_Backend/requirements.txt b/HMS_Backend/requirements.txt
new file mode 100644
index 0000000..cda3903
--- /dev/null
+++ b/HMS_Backend/requirements.txt
@@ -0,0 +1,4 @@
+Flask~=3.0.3
+flask-cors~=5.0.1
+Werkzeug~=3.1.1
+flask-sqlalchemy~=3.1.1
\ No newline at end of file
diff --git a/HMS_Frontend/.dockerignore b/HMS_Frontend/.dockerignore
new file mode 100644
index 0000000..9b8d514
--- /dev/null
+++ b/HMS_Frontend/.dockerignore
@@ -0,0 +1,4 @@
+.react-router
+build
+node_modules
+README.md
\ No newline at end of file
diff --git a/HMS_Frontend/.env b/HMS_Frontend/.env
new file mode 100644
index 0000000..21e7647
--- /dev/null
+++ b/HMS_Frontend/.env
@@ -0,0 +1,2 @@
+# VITE_API_ENDPOINT = "http://localhost:721"
+VITE_API_ENDPOINT = "https://hms_api.yanfeng.uk"
diff --git a/HMS_Frontend/.gitignore b/HMS_Frontend/.gitignore
new file mode 100644
index 0000000..383eebb
--- /dev/null
+++ b/HMS_Frontend/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+/node_modules/
+
+# React Router
+/.react-router/
+/build/
+.idea
+.vscode
diff --git a/HMS_Frontend/Dockerfile b/HMS_Frontend/Dockerfile
new file mode 100644
index 0000000..207bf93
--- /dev/null
+++ b/HMS_Frontend/Dockerfile
@@ -0,0 +1,22 @@
+FROM node:20-alpine AS development-dependencies-env
+COPY . /app
+WORKDIR /app
+RUN npm ci
+
+FROM node:20-alpine AS production-dependencies-env
+COPY ./package.json package-lock.json /app/
+WORKDIR /app
+RUN npm ci --omit=dev
+
+FROM node:20-alpine AS build-env
+COPY . /app/
+COPY --from=development-dependencies-env /app/node_modules /app/node_modules
+WORKDIR /app
+RUN npm run build
+
+FROM node:20-alpine
+COPY ./package.json package-lock.json /app/
+COPY --from=production-dependencies-env /app/node_modules /app/node_modules
+COPY --from=build-env /app/build /app/build
+WORKDIR /app
+CMD ["npm", "run", "start"]
\ No newline at end of file
diff --git a/HMS_Frontend/README.md b/HMS_Frontend/README.md
new file mode 100644
index 0000000..5c4780a
--- /dev/null
+++ b/HMS_Frontend/README.md
@@ -0,0 +1,87 @@
+# Welcome to React Router!
+
+A modern, production-ready template for building full-stack React applications using React Router.
+
+[](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
+
+## Features
+
+- 🚀 Server-side rendering
+- ⚡️ Hot Module Replacement (HMR)
+- 📦 Asset bundling and optimization
+- 🔄 Data loading and mutations
+- 🔒 TypeScript by default
+- 🎉 TailwindCSS for styling
+- 📖 [React Router docs](https://reactrouter.com/)
+
+## Getting Started
+
+### Installation
+
+Install the dependencies:
+
+```bash
+npm install
+```
+
+### Development
+
+Start the development server with HMR:
+
+```bash
+npm run dev
+```
+
+Your application will be available at `http://localhost:5173`.
+
+## Building for Production
+
+Create a production build:
+
+```bash
+npm run build
+```
+
+## Deployment
+
+### Docker Deployment
+
+To build and run using Docker:
+
+```bash
+docker build -t my-app .
+
+# Run the container
+docker run -p 3000:3000 my-app
+```
+
+The containerized application can be deployed to any platform that supports Docker, including:
+
+- AWS ECS
+- Google Cloud Run
+- Azure Container Apps
+- Digital Ocean App Platform
+- Fly.io
+- Railway
+
+### DIY Deployment
+
+If you're familiar with deploying Node applications, the built-in app server is production-ready.
+
+Make sure to deploy the output of `npm run build`
+
+```
+├── package.json
+├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
+├── build/
+│ ├── client/ # Static assets
+│ └── server/ # Server-side code
+```
+
+## Styling
+
+This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
+
+---
+
+Built with ❤️ using React Router.
diff --git a/HMS_Frontend/app/app.css b/HMS_Frontend/app/app.css
new file mode 100644
index 0000000..99345d8
--- /dev/null
+++ b/HMS_Frontend/app/app.css
@@ -0,0 +1,15 @@
+@import "tailwindcss";
+
+@theme {
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+}
+
+html,
+body {
+ @apply bg-white dark:bg-gray-950;
+
+ @media (prefers-color-scheme: dark) {
+ color-scheme: dark;
+ }
+}
diff --git a/HMS_Frontend/app/components/GlobalAffix.tsx b/HMS_Frontend/app/components/GlobalAffix.tsx
new file mode 100644
index 0000000..3a8567d
--- /dev/null
+++ b/HMS_Frontend/app/components/GlobalAffix.tsx
@@ -0,0 +1,47 @@
+import {Affix, Button, Group, rem, Stack} from "@mantine/core";
+import ArrowUpIcon from "mdi-react/ArrowUpIcon";
+import HomeIcon from "mdi-react/HomeIcon";
+import {useNavigate} from "react-router";
+import {useWindowScroll} from "@mantine/hooks";
+import {ThemeToggle} from "~/components/subs/ThemeToggle.tsx";
+import {roundThemeButton} from "~/styles.ts";
+
+export default function () {
+ const navigate = useNavigate();
+ const [scroll, scrollTo] = useWindowScroll();
+
+ const buttonSize = 23;
+ const circleSize = 55;
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/HMS_Frontend/app/components/PageHeader.tsx b/HMS_Frontend/app/components/PageHeader.tsx
new file mode 100644
index 0000000..d5fae30
--- /dev/null
+++ b/HMS_Frontend/app/components/PageHeader.tsx
@@ -0,0 +1,62 @@
+import {Burger, Group, Image, Text} from "@mantine/core";
+import {ThemeToggle} from "./subs/ThemeToggle.tsx";
+import React, {useEffect, useState} from "react";
+
+
+export function PageHeader({opened, toggle, appendTitle}: {opened: boolean, toggle: () => void, appendTitle?: string}) {
+ const [width, setWidth] = useState(window.innerWidth)
+ // const [height, setHeight] = useState(window.innerHeight)
+ const [title, setTitle] = useState('HMS Dashboard');
+
+ useEffect(() => {
+ const handleResize = () => {
+ setWidth(window.innerWidth);
+ // setHeight(window.innerHeight);
+ };
+
+ window.addEventListener('resize', handleResize);
+
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ };
+ }, []);
+
+ // useEffect(() => {
+ // const titleElement = document.querySelector('title');
+ // const observer = new MutationObserver((mutations) => {
+ // mutations.forEach((mutation) => {
+ // mutation.target.textContent && setTitle(mutation.target.textContent);
+ // });
+ // });
+ //
+ // if (titleElement) {
+ // observer.observe(titleElement, { childList: true });
+ // }
+ //
+ // return () => {
+ // observer.disconnect();
+ // };
+ // }, []);
+
+
+ return (
+
+
+
+
+ {width >= 290 && {title}}
+ {appendTitle && {appendTitle}}
+
+
+
+
+
+
+
+ )
+}
diff --git a/HMS_Frontend/app/components/PageNavbar.tsx b/HMS_Frontend/app/components/PageNavbar.tsx
new file mode 100644
index 0000000..f683757
--- /dev/null
+++ b/HMS_Frontend/app/components/PageNavbar.tsx
@@ -0,0 +1,98 @@
+import {Button, Divider, Flex, Tabs} from "@mantine/core";
+import {iconMStyle, maxWidth} from "../styles.ts";
+import React from "react";
+import {DashboardPageType, UserPermissionLevel} from "~/utils/hms_enums.ts";
+import HomeIcon from "mdi-react/HomeIcon";
+import InformationIcon from "mdi-react/InformationIcon";
+import LogoutIcon from "mdi-react/LogoutIcon";
+import AccountGroupIcon from "mdi-react/AccountGroupIcon";
+import BedIcon from "mdi-react/BedIcon";
+import AccountWrenchIcon from "mdi-react/AccountWrenchIcon";
+import BookAccountIcon from "mdi-react/BookAccountIcon";
+import HospitalBoxIcon from "mdi-react/HospitalBoxIcon";
+
+
+export function PageNavbar({currentStat, changePageStat, userPermission, canReception}:
+ {currentStat: DashboardPageType, changePageStat: (p: DashboardPageType) => any,
+ userPermission: UserPermissionLevel, canReception: boolean}) {
+
+ const onClickLogout = () => {
+ localStorage.removeItem("hms_token")
+ changePageStat(DashboardPageType.LoginPage)
+ }
+
+ return (
+ <>
+ {if (e) changePageStat(e as DashboardPageType)}}>
+
+ }>
+ Home
+
+
+ { userPermission == UserPermissionLevel.PATIENT && <>
+
+ }>
+ My Appointments
+
+ }>
+ My Treatments
+
+ >}
+
+ { canReception && <>
+
+ }>
+ Appointments Management
+
+ >}
+
+ { userPermission >= UserPermissionLevel.DOCTOR && <>
+
+ }>
+ Patient Treatment
+
+ >}
+
+ { userPermission >= UserPermissionLevel.ADMIN && <>
+
+ }>
+ Ward Management
+
+
+
+ }>
+ Medical Teams Management
+
+ }>
+ Staff Management
+
+ }>
+ Patients Management
+
+ > }
+
+ { userPermission >= UserPermissionLevel.DOCTOR && <>
+
+ }>
+ Treatment Records
+
+ >}
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/HMS_Frontend/app/components/subs/AppointmentManageFilter.tsx b/HMS_Frontend/app/components/subs/AppointmentManageFilter.tsx
new file mode 100644
index 0000000..5aebf6c
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/AppointmentManageFilter.tsx
@@ -0,0 +1,75 @@
+import {useEffect, useMemo, useState} from "react";
+import {Button, Checkbox, Group, MultiSelect, Select, Stack, Text} from "@mantine/core";
+import {BookingCategory} from "~/utils/hms_enums.ts";
+
+interface AppointmentManageFilterProps {
+ onChange: (categories: string[], status: number, discharged: boolean) => void;
+}
+
+export function AppointmentManageFilter({onChange}: AppointmentManageFilterProps) {
+ const [selectedCategories, setSelectedCategories] = useState([]);
+ const [filterStatus, setFilterStatus] = useState('-1');
+ const [includeDischarged, setIncludeDischarged] = useState(false);
+
+ const categoryOptions = useMemo(() => {
+ return Object.entries(BookingCategory).map(([key, value]) => ({
+ value,
+ label: key.replace(/_/g, ' ')
+ }));
+ }, []);
+
+ useEffect(() => {
+ onChange(
+ selectedCategories,
+ parseInt(filterStatus),
+ includeDischarged
+ )
+ }, [selectedCategories, filterStatus, includeDischarged]);
+
+ const statusOptions = useMemo(() => [
+ {value: '-1', label: 'All'},
+ {value: '0', label: 'Pending'},
+ {value: '1', label: 'Approved'},
+ {value: '2', label: 'Rejected'},
+ ], []);
+
+ const handleResetFilter = () => {
+ setSelectedCategories([]);
+ setFilterStatus('-1');
+ setIncludeDischarged(false);
+ onChange([], -1, false);
+ };
+
+ const handleStatusChange = (value: string | null) => {
+ setFilterStatus(value || '-1');
+ };
+
+ return (
+
+
+
+
+
+ setIncludeDischarged(e.currentTarget.checked)}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/HMS_Frontend/app/components/subs/BackButton.tsx b/HMS_Frontend/app/components/subs/BackButton.tsx
new file mode 100644
index 0000000..aa99682
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/BackButton.tsx
@@ -0,0 +1,40 @@
+import { useNavigate, useLocation } from 'react-router';
+import {useEffect, useMemo, useState} from "react";
+
+export default function BackButton() {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const [canBack, setCanBack] = useState(window.history.length > 1);
+
+ useEffect(() => {
+ setCanBack(window.history.length > 1);
+ }, [location.key]);
+
+ const supportsNavigationAPI = useMemo(
+ () =>
+ typeof window !== "undefined" &&
+ "navigation" in window &&
+ "canGoBack" in (window as any).navigation,
+ []
+ );
+
+ return (
+
+ );
+}
diff --git a/HMS_Frontend/app/components/subs/DoctorInfoDisplay.tsx b/HMS_Frontend/app/components/subs/DoctorInfoDisplay.tsx
new file mode 100644
index 0000000..c6db7b7
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/DoctorInfoDisplay.tsx
@@ -0,0 +1,173 @@
+import React, { useEffect, useState } from 'react';
+import { ActionIcon, Button, Group, Modal, Stack, Text, Loader } from '@mantine/core';
+import { showErrorMessage } from '~/utils/utils.ts';
+import { apiGetDoctorsList } from '~/utils/hms_api.ts';
+import type { DoctorData, DoctorDataWithPermission } from '~/utils/models.ts';
+import InfoIcon from 'mdi-react/InfoIcon';
+import { iconMStyle } from '~/styles.ts';
+import { DoctorGrade } from '~/utils/hms_enums.ts';
+
+// 缓存已加载的医生数据,避免重复请求
+const doctorCache = new Map();
+
+interface DoctorInfoDisplayProps {
+ doctorId: number;
+ showIcon?: boolean;
+}
+
+export function DoctorInfoDisplay({ doctorId, showIcon = true }: DoctorInfoDisplayProps) {
+ const [doctorInfo, setDoctorInfo] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [modalOpen, setModalOpen] = useState(false);
+ const [initialLoad, setInitialLoad] = useState(true);
+
+ useEffect(() => {
+ // 如果已经在缓存中,直接使用缓存数据
+ if (doctorCache.has(doctorId)) {
+ setDoctorInfo(doctorCache.get(doctorId) || null);
+ setLoading(false);
+ setInitialLoad(false);
+ return;
+ }
+
+ // 否则加载医生数据
+ fetchDoctorInfo();
+ }, [doctorId]);
+
+ const fetchDoctorInfo = async () => {
+ try {
+ setLoading(true);
+ // 获取所有医生信息,传递-1表示不分页,获取所有医生
+ const response = await apiGetDoctorsList(-1);
+
+ if (response.success) {
+ const doctors = response.data.doctors;
+ // 将所有医生添加到缓存
+ doctors.forEach(doctor => {
+ doctorCache.set(doctor.id, doctor);
+ });
+
+ // 查找当前需要的医生
+ const doctor = doctors.find(d => d.id === doctorId);
+
+ if (doctor) {
+ setDoctorInfo(doctor);
+ } else {
+ console.warn(`Doctor with ID ${doctorId} not found`);
+ }
+ } else {
+ showErrorMessage(response.message, "Failed to load doctor information");
+ }
+ } catch (error) {
+ showErrorMessage("Error loading doctor information", "Error");
+ console.error("Error fetching doctor information:", error);
+ } finally {
+ setLoading(false);
+ setInitialLoad(false);
+ }
+ };
+
+ const openModal = () => {
+ setModalOpen(true);
+ };
+
+ const closeModal = () => {
+ setModalOpen(false);
+ };
+
+ // 初始加载时显示加载状态
+ if (initialLoad) {
+ return ;
+ }
+
+ // 如果无法找到医生信息,显示 ID
+ if (!doctorInfo) {
+ return (
+
+ ID: {doctorId}
+ {showIcon && (
+
+
+
+ )}
+
+ );
+ }
+
+ return (
+ <>
+
+
+ {`${doctorInfo.title} ${doctorInfo.name}`}
+
+ {showIcon && (
+
+
+
+ )}
+
+
+
+
+
+ Name:
+ {`${doctorInfo.title} ${doctorInfo.name}`}
+
+
+ Gender:
+ {doctorInfo.gender}
+
+
+ Grade:
+ {DoctorGrade[doctorInfo.grade]}
+
+
+ Birth Date:
+ {doctorInfo.birth_date}
+
+
+ Email:
+ {doctorInfo.email}
+
+
+ Phone:
+ {doctorInfo.phone}
+
+
+ Admin:
+ {doctorInfo.is_admin ? 'Yes' : 'No'}
+
+
+ Status:
+
+ {doctorInfo.is_resigned ? 'Resigned' : 'Active'}
+
+
+
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/HMS_Frontend/app/components/subs/DoctorTeamsSimple.tsx b/HMS_Frontend/app/components/subs/DoctorTeamsSimple.tsx
new file mode 100644
index 0000000..2554c64
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/DoctorTeamsSimple.tsx
@@ -0,0 +1,73 @@
+import type {DoctorTeamInfo, OutletContextType} from "~/utils/models.ts";
+import {ActionIcon, Card, Group, Table, Text, Tooltip} from "@mantine/core";
+import HistoryIcon from "mdi-react/HistoryIcon";
+import {iconMStyle, marginRound} from "~/styles.ts";
+import EyeIcon from "mdi-react/EyeIcon";
+import {useOutletContext} from "react-router";
+import {DashboardPageType} from "~/utils/hms_enums.ts";
+import {confirmViewTeamMembers} from "~/components/subs/confirms.tsx";
+import {ResponsiveTableContainer} from "~/components/subs/ResponsiveTableContainer.tsx";
+
+
+export default function({doctorTeams}: {doctorTeams: DoctorTeamInfo[]}) {
+ const { changePage } = useOutletContext();
+
+ const handleViewTeamMembers = (team: DoctorTeamInfo) => {
+ confirmViewTeamMembers(team)
+ }
+
+ const teamRows = doctorTeams.map((team) => (
+
+ {team.id}
+ {team.department.replace(/_/g, ' ')}
+
+ {team.members.find(m => m.id === team.consultant_id)
+ ? `${team.members.find(m => m.id === team.consultant_id)?.title || ''} ${team.members.find(m => m.id === team.consultant_id)?.name || ''}`
+ : `ID: ${team.consultant_id}`}
+
+ {team.members.length}
+ {team.is_admin_team ? 'Yes' : 'No'}
+
+
+
+ changePage(DashboardPageType.TreatmentRecord, `/dashboard/treatment_record/t${team.id}`)}>
+
+
+
+ handleViewTeamMembers(team)}>
+
+
+
+
+
+ ));
+
+ return(
+
+ My Medical Teams
+
+
+ {doctorTeams.length > 0 ? (
+
+
+
+
+ Team ID
+ Department
+ Consultant
+ Members Count
+ Admin Team
+ Operations
+
+
+ {teamRows}
+
+
+ ) : (
+ You are not a member of any medical team.
+ )}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/HMS_Frontend/app/components/subs/MedicalTeamManageFilter.tsx b/HMS_Frontend/app/components/subs/MedicalTeamManageFilter.tsx
new file mode 100644
index 0000000..131ce67
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/MedicalTeamManageFilter.tsx
@@ -0,0 +1,56 @@
+import {useEffect, useMemo, useState} from "react";
+import {Button, Checkbox, Group, MultiSelect, RangeSlider, Select, Stack, Text} from "@mantine/core";
+import {Departments} from "~/utils/hms_enums.ts";
+
+interface MedicalTeamTableFilterProps {
+ onChange: (departments: string[], adminTeam: number) => void;
+}
+
+export function MedicalTeamManageFilter({onChange}: MedicalTeamTableFilterProps) {
+ const [selectedDepartments, setSelectedDepartments] = useState([]);
+ const [filterAdminTeam, setFilterAdminTeam] = useState('-1');
+
+ useEffect(() => {
+ onChange(selectedDepartments, parseInt(filterAdminTeam));
+ }, [selectedDepartments, filterAdminTeam]);
+
+ const departmentOptions = useMemo(() => {
+ return Object.entries(Departments).map(([key, value]) => ({
+ value,
+ label: key.replace(/_/g, ' ')
+ }));
+ }, []);
+
+ const adminOptions = useMemo(() => [
+ {value: '-1', label: 'All'},
+ {value: '1', label: 'Yes'},
+ {value: '0', label: 'No'},
+ ], []);
+
+ const handleAdminTeamChange = (value: string | null) => {
+ setFilterAdminTeam(value || '-1');
+ };
+
+ return (
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/HMS_Frontend/app/components/subs/PatientInfoDisplay.tsx b/HMS_Frontend/app/components/subs/PatientInfoDisplay.tsx
new file mode 100644
index 0000000..e5cf6d4
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/PatientInfoDisplay.tsx
@@ -0,0 +1,166 @@
+import React, { useEffect, useState } from 'react';
+import { ActionIcon, Button, Group, Modal, Stack, Text, Loader } from '@mantine/core';
+import { showErrorMessage } from '~/utils/utils.ts';
+import { apiGetPatientsList } from '~/utils/hms_api.ts';
+import type { PatientData } from '~/utils/models.ts';
+import InfoIcon from 'mdi-react/InfoIcon';
+import { iconMStyle } from '~/styles.ts';
+
+// 缓存已加载的患者数据,避免重复请求
+const patientCache = new Map();
+
+interface PatientInfoDisplayProps {
+ patientId: number;
+ showIcon?: boolean;
+}
+
+export function PatientInfoDisplay({ patientId, showIcon = true }: PatientInfoDisplayProps) {
+ const [patientInfo, setPatientInfo] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [modalOpen, setModalOpen] = useState(false);
+ const [initialLoad, setInitialLoad] = useState(true);
+
+ useEffect(() => {
+ // 如果已经在缓存中,直接使用缓存数据
+ if (patientCache.has(patientId)) {
+ setPatientInfo(patientCache.get(patientId) || null);
+ setLoading(false);
+ setInitialLoad(false);
+ return;
+ }
+
+ // 否则加载患者数据
+ fetchPatientInfo();
+ }, [patientId]);
+
+ const fetchPatientInfo = async () => {
+ try {
+ setLoading(true);
+ // 获取所有患者信息,传递-1表示不分页,获取所有患者
+ const response = await apiGetPatientsList(-1);
+
+ if (response.success) {
+ const patients = response.data.patients;
+ // 将所有患者添加到缓存
+ patients.forEach(patient => {
+ patientCache.set(patient.id, patient);
+ });
+
+ // 查找当前需要的患者
+ const patient = patients.find(p => p.id === patientId);
+
+ if (patient) {
+ setPatientInfo(patient);
+ } else {
+ console.warn(`Patient with ID ${patientId} not found`);
+ }
+ } else {
+ showErrorMessage(response.message, "Failed to load patient information");
+ }
+ } catch (error) {
+ showErrorMessage("Error loading patient information", "Error");
+ console.error("Error fetching patient information:", error);
+ } finally {
+ setLoading(false);
+ setInitialLoad(false);
+ }
+ };
+
+ const openModal = () => {
+ setModalOpen(true);
+ };
+
+ const closeModal = () => {
+ setModalOpen(false);
+ };
+
+ // 初始加载时显示加载状态
+ if (initialLoad) {
+ return ;
+ }
+
+ // 如果无法找到患者信息,显示 ID
+ if (!patientInfo) {
+ return (
+
+ ID: {patientId}
+ {showIcon && (
+
+
+
+ )}
+
+ );
+ }
+
+ return (
+ <>
+
+
+ {`${patientInfo.title} ${patientInfo.name}`}
+
+ {showIcon && (
+
+
+
+ )}
+
+
+
+
+
+ Name:
+ {`${patientInfo.title} ${patientInfo.name}`}
+
+
+ Gender:
+ {patientInfo.gender}
+
+
+ Birth Date:
+ {patientInfo.birth_date}
+
+
+ Email:
+ {patientInfo.email}
+
+
+ Phone:
+ {patientInfo.phone}
+
+
+ Address:
+ {patientInfo.address}
+
+
+ Postcode:
+ {patientInfo.postcode}
+
+
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/HMS_Frontend/app/components/subs/PatientManagementFilter.tsx b/HMS_Frontend/app/components/subs/PatientManagementFilter.tsx
new file mode 100644
index 0000000..995ca58
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/PatientManagementFilter.tsx
@@ -0,0 +1,124 @@
+import {type ComboboxItem, Grid, Group, MultiSelect, type OptionsFilter, Radio, Text, TextInput} from "@mantine/core";
+import React, {useEffect, useMemo, useState} from "react";
+import SearchIcon from "mdi-react/SearchIcon";
+import {iconMStyle} from "~/styles.ts";
+
+export interface PatientFilterProps {
+ onChange: (nameSearch: string, genders: string[], hasTeam: string, isAdmitted: string, teams: number[], wards: number[]) => void;
+ teamOptions: { value: string, label: string }[];
+ wardOptions: { value: string, label: string }[];
+}
+
+export function PatientManagementFilter({ onChange, teamOptions, wardOptions }: PatientFilterProps) {
+ const [nameSearch, setNameSearch] = useState("")
+ const [filterGenders, setFilterGenders] = useState([])
+ const [filterHasTeam, setFilterHasTeam] = useState("-1")
+ const [filterIsAdmitted, setFilterIsAdmitted] = useState("-1")
+ const [filterTeams, setFilterTeams] = useState([])
+ const [filterWards, setFilterWards] = useState([])
+
+ useEffect(() => {
+ onChange(
+ nameSearch,
+ filterGenders,
+ filterHasTeam,
+ filterIsAdmitted,
+ filterTeams.map(t => parseInt(t)),
+ filterWards.map(w => parseInt(w))
+ )
+ }, [nameSearch, filterGenders, filterHasTeam, filterIsAdmitted, filterTeams, filterWards]);
+
+ const genderOptions = useMemo(
+ () => [
+ { value: 'M', label: 'Male' },
+ { value: 'F', label: 'Female' },
+ { value: 'Intersex', label: 'Intersex' },
+ ],
+ [],
+ );
+
+ const optionsFilter: OptionsFilter = ({ options, search }) => {
+ const filtered = (options as ComboboxItem[]).filter((option) =>
+ option.label.toLowerCase().trim().includes(search.toLowerCase().trim())
+ );
+
+ filtered.sort((a, b) => a.label.localeCompare(b.label));
+ return filtered;
+ };
+
+ return (
+
+
+ setNameSearch(e.currentTarget.value)}
+ leftSection={}
+ />
+
+
+
+
+
+ Medical Team Status
+
+
+
+
+
+
+
+
+
+ Admission Status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/HMS_Frontend/app/components/subs/ResponsiveTableContainer.tsx b/HMS_Frontend/app/components/subs/ResponsiveTableContainer.tsx
new file mode 100644
index 0000000..ad0c4cb
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/ResponsiveTableContainer.tsx
@@ -0,0 +1,27 @@
+import { type ReactNode, forwardRef } from 'react';
+import { Table } from '@mantine/core';
+import { useMediaQuery } from '@mantine/hooks';
+
+type ResponsiveTableContainerProps = Omit<
+ React.ComponentProps,
+ 'children'
+> & {
+ children: ReactNode;
+};
+
+export const ResponsiveTableContainer = forwardRef<
+ HTMLDivElement,
+ ResponsiveTableContainerProps
+>(({ children, ...rest }, ref) => {
+ const isNarrow = useMediaQuery('(max-width: 700px)');
+
+ return isNarrow ? (
+
+ {children}
+
+ ) : (
+ <>{children}>
+ );
+});
+
+ResponsiveTableContainer.displayName = 'ResponsiveTableContainer';
diff --git a/HMS_Frontend/app/components/subs/StaffManageTableFilter.tsx b/HMS_Frontend/app/components/subs/StaffManageTableFilter.tsx
new file mode 100644
index 0000000..b2abed1
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/StaffManageTableFilter.tsx
@@ -0,0 +1,99 @@
+import {type ComboboxItem, Grid, Group, MultiSelect, type OptionsFilter, Radio, RangeSlider, Text} from "@mantine/core";
+import React, {useEffect, useMemo, useState} from "react";
+import {WardTypes} from "~/utils/models.ts";
+import {DoctorGrade} from "~/utils/hms_enums.ts";
+
+export function StaffTableFilter({onChange}: {onChange: (grades: string[], genders: string[], isAdmin: string, isTerminated: string) => void}) {
+ const [filterGrades, setFilterGrades] = useState([])
+ const [filterGenders, setFilterGenders] = useState([])
+ const [filterIsAdmin, setFilterIsAdmin] = useState("-1")
+ const [filterIsTerminated, setFilterIsTerminated] = useState("-1")
+
+ useEffect(() => {
+ onChange(filterGrades, filterGenders, filterIsAdmin, filterIsTerminated)
+ }, [filterGrades, filterGenders, filterIsAdmin, filterIsTerminated]);
+
+ const gradesTypesOptions = useMemo(() => {
+ return Object.entries(DoctorGrade)
+ .filter(([k]) => isNaN(Number(k)))
+ .map(([key, value]) => ({ value: value.toString(), label: key }))
+ }, [WardTypes])
+
+ const genderOptions = useMemo(
+ () => [
+ { value: 'M', label: 'Male' },
+ { value: 'F', label: 'Female' },
+ { value: 'Intersex', label: 'Intersex' },
+ ],
+ [],
+ );
+
+ const optionsFilter: OptionsFilter = ({ options, search }) => {
+ const filtered = (options as ComboboxItem[]).filter((option) =>
+ option.label.toLowerCase().trim().includes(search.toLowerCase().trim())
+ );
+
+ filtered.sort((a, b) => a.label.localeCompare(b.label));
+ return filtered;
+ }
+
+ return (
+
+
+ setFilterGrades(value)}
+ comboboxProps={{ transitionProps: { transition: 'fade', duration: 100, timingFunction: 'ease' } }}
+ />
+
+
+
+ setFilterGenders(value)}
+ comboboxProps={{ transitionProps: { transition: 'fade', duration: 100, timingFunction: 'ease' } }}
+ />
+
+
+
+ setFilterIsAdmin(value)}
+ >
+
+
+
+
+
+
+
+
+
+ setFilterIsTerminated(value)}
+ >
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/HMS_Frontend/app/components/subs/TeamInfoDisplay.tsx b/HMS_Frontend/app/components/subs/TeamInfoDisplay.tsx
new file mode 100644
index 0000000..46fad02
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/TeamInfoDisplay.tsx
@@ -0,0 +1,123 @@
+import React, { useEffect, useState } from 'react';
+import { ActionIcon, Group, Text, Loader } from '@mantine/core';
+import { showErrorMessage } from '~/utils/utils.ts';
+import { apiGetTeamList, get_team_info } from '~/utils/hms_api.ts';
+import type { DoctorTeamInfo } from '~/utils/models.ts';
+import InfoIcon from 'mdi-react/InfoIcon';
+import { iconMStyle } from '~/styles.ts';
+import { confirmViewTeamMembers } from '~/components/subs/confirms.tsx';
+import { Departments } from '~/utils/hms_enums.ts';
+
+const teamCache = new Map();
+
+interface TeamInfoDisplayProps {
+ teamId: number;
+ showIcon?: boolean;
+}
+
+export function TeamInfoDisplay({ teamId, showIcon = true }: TeamInfoDisplayProps) {
+ const [teamInfo, setTeamInfo] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [initialLoad, setInitialLoad] = useState(true);
+
+ useEffect(() => {
+ if (teamCache.has(teamId)) {
+ setTeamInfo(teamCache.get(teamId) || null);
+ setLoading(false);
+ setInitialLoad(false);
+ return;
+ }
+
+ fetchTeamInfo();
+ }, [teamId]);
+
+ const fetchTeamInfo = async () => {
+ try {
+ setLoading(true);
+ const detailResponse = await get_team_info(teamId);
+
+ if (detailResponse.success) {
+ const team = detailResponse.data.team;
+ teamCache.set(teamId, team);
+ setTeamInfo(team);
+ } else {
+ const listResponse = await apiGetTeamList(-1);
+
+ if (listResponse.success) {
+ const teams = listResponse.data.teams;
+ teams.forEach(team => {
+ teamCache.set(team.id, team);
+ });
+
+ const team = teams.find(t => t.id === teamId);
+
+ if (team) {
+ setTeamInfo(team);
+ } else {
+ console.warn(`Team with ID ${teamId} not found`);
+ }
+ } else {
+ showErrorMessage(listResponse.message, "Failed to load team information");
+ }
+ }
+ } catch (error) {
+ showErrorMessage("Error loading team information", "Error");
+ console.error("Error fetching team information:", error);
+ } finally {
+ setLoading(false);
+ setInitialLoad(false);
+ }
+ };
+
+ const handleViewTeamDetails = () => {
+ if (!teamInfo) {
+ showErrorMessage("Team information not available", "Error");
+ return;
+ }
+
+ confirmViewTeamMembers(teamInfo);
+ };
+
+ if (initialLoad) {
+ return ;
+ }
+
+ if (!teamInfo) {
+ return (
+
+ ID: {teamId}
+ {showIcon && (
+
+
+
+ )}
+
+ );
+ }
+
+ return (
+
+
+ {teamInfo.department.replace(/_/g, ' ')}
+
+ {showIcon && (
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/HMS_Frontend/app/components/subs/ThemeToggle.tsx b/HMS_Frontend/app/components/subs/ThemeToggle.tsx
new file mode 100644
index 0000000..4f05965
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/ThemeToggle.tsx
@@ -0,0 +1,41 @@
+import {
+ ActionIcon,
+ Group, rem,
+ Tooltip,
+ useComputedColorScheme,
+ useMantineColorScheme,
+ useMantineTheme
+} from "@mantine/core";
+import {iconMStyle} from "~/styles";
+
+import ThemeLightDarkIcon from "mdi-react/ThemeLightDarkIcon";
+import WeatherNightIcon from "mdi-react/WeatherNightIcon";
+import WeatherSunnyIcon from "mdi-react/WeatherSunnyIcon";
+import type {CSSProperties} from "react";
+
+
+export function ThemeToggle({extraStyle, iconStyle}: {extraStyle?: CSSProperties, iconStyle?: CSSProperties}) {
+ const { colorScheme, setColorScheme } = useMantineColorScheme();
+ // const [colorSchemeState, toggleColorSchemeState] = useToggle(['auto', 'dark', 'light'] as const);
+ const computedColorScheme = useComputedColorScheme('light');
+ const theme = useMantineTheme();
+
+ const nextScheme = colorScheme === "auto" ? "dark" : colorScheme === "dark" ? "light" : "auto";
+
+ const onClickChangeColorScheme = () => {
+ setColorScheme(nextScheme)
+ }
+
+ return (
+
+
+
+ {colorScheme === 'auto' ? :
+ colorScheme === 'dark' ? : }
+
+
+
+ );
+}
diff --git a/HMS_Frontend/app/components/subs/TruncatedText.tsx b/HMS_Frontend/app/components/subs/TruncatedText.tsx
new file mode 100644
index 0000000..9bf230e
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/TruncatedText.tsx
@@ -0,0 +1,69 @@
+import React, { useState } from 'react';
+import { ActionIcon, Group, Modal, Text, Tooltip } from '@mantine/core';
+import InformationOutlineIcon from 'mdi-react/InformationOutlineIcon';
+import { iconMStyle } from '~/styles.ts';
+
+interface TruncatedTextProps {
+ text: string;
+ maxLength?: number;
+ maxTooltipLength?: number;
+ title?: string;
+}
+
+export function TruncatedText({
+ text,
+ maxLength = 20,
+ maxTooltipLength = 200,
+ title = 'Details'
+}: TruncatedTextProps) {
+ const [opened, setOpened] = useState(false);
+
+ if (!text) return -;
+
+ if (text.length <= maxLength) {
+ return {text};
+ }
+
+ const truncatedText = `${text.substring(0, maxLength)}...`;
+
+ const tooltipText = text.length > maxTooltipLength
+ ? `${text.substring(0, maxTooltipLength)}...`
+ : text
+
+ return (
+ <>
+
+
+ {truncatedText}
+ setOpened(true)}
+ title="Show full content"
+ >
+
+
+
+
+
+ setOpened(false)}
+ title={title}
+ size="md"
+ centered
+ >
+
+ {text}
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/HMS_Frontend/app/components/subs/WardManageTableFilter.tsx b/HMS_Frontend/app/components/subs/WardManageTableFilter.tsx
new file mode 100644
index 0000000..043aa07
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/WardManageTableFilter.tsx
@@ -0,0 +1,78 @@
+import {type ComboboxItem, Grid, MultiSelect, type OptionsFilter, RangeSlider, Text} from "@mantine/core";
+import React, {useEffect, useMemo, useState} from "react";
+import {WardTypes} from "~/utils/models.ts";
+
+export function WardManageTableFilter({onChange}: {onChange: (types: string[], occupancy: [number, number], capacity: [number, number]) => void}) {
+ const [filterTypes, setFilterTypes] = useState([])
+ const [filterOccupancy, setFilterOccupancy] = useState<[number, number]>([0, 12])
+ const [filterCapacity, setFilterCapacity] = useState<[number, number]>([1, 12])
+
+ useEffect(() => {
+ onChange(filterTypes, filterOccupancy, filterCapacity)
+ }, [filterTypes, filterOccupancy, filterCapacity]);
+
+ const wardTypesOptions = useMemo(() => {
+ return Object.entries(WardTypes).map(([key, value]) => ({ value: key, label: value }))
+ }, [WardTypes])
+
+ const optionsFilter: OptionsFilter = ({ options, search }) => {
+ const filtered = (options as ComboboxItem[]).filter((option) =>
+ option.label.toLowerCase().trim().includes(search.toLowerCase().trim())
+ );
+
+ filtered.sort((a, b) => a.label.localeCompare(b.label));
+ return filtered;
+ }
+
+ return (
+
+
+ setFilterTypes(value)}
+ comboboxProps={{ transitionProps: { transition: 'fade', duration: 100, timingFunction: 'ease' } }}
+ />
+
+
+
+ Current Occupancy
+ ({
+ value: index,
+ label: String(index),
+ }))}
+ onChange={setFilterOccupancy}
+ onChangeEnd={setFilterOccupancy}
+ />
+
+
+
+ Capacity
+ ({
+ value: index + 1,
+ label: String(index + 1),
+ }))}
+ onChange={setFilterCapacity}
+ onChangeEnd={setFilterCapacity}
+ />
+
+
+ )
+}
\ No newline at end of file
diff --git a/HMS_Frontend/app/components/subs/WardPatients.tsx b/HMS_Frontend/app/components/subs/WardPatients.tsx
new file mode 100644
index 0000000..45603f1
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/WardPatients.tsx
@@ -0,0 +1,206 @@
+import {type PatientData, SORT_SYMBOLS, type WardInfo, WardTypes} from "~/utils/models.ts";
+import {ActionIcon, Group, Table, Text} from "@mantine/core";
+import {useEffect, useMemo, useState} from "react";
+import {apiGetWardPatients, apiTransferWard, apiGetWardList} from "~/utils/hms_api.ts";
+import {getUserDisplayFullName, showErrorMessage, showInfoMessage} from "~/utils/utils.ts";
+import {confirmCheckWardPatients, confirmDeleteWard, confirmEditOrCreateWard} from "~/components/subs/confirms.tsx";
+import EyeIcon from "mdi-react/EyeIcon";
+import {iconMStyle} from "~/styles.ts";
+import PencilIcon from "mdi-react/PencilIcon";
+import DeleteIcon from "mdi-react/DeleteIcon";
+import SwapHorizontalIcon from "mdi-react/SwapHorizontalIcon";
+import {modals} from "@mantine/modals";
+import {Button, Select, Stack} from "@mantine/core";
+import {useForm} from "@mantine/form";
+import { ResponsiveTableContainer } from "./ResponsiveTableContainer";
+
+function ConfirmTransferWard({patientId, currentWardId, onSuccess}: {patientId: number, currentWardId: number, onSuccess: () => void}) {
+ const [loading, setLoading] = useState(false);
+ const [wards, setWards] = useState([]);
+ const [loadingWards, setLoadingWards] = useState(true);
+
+ const form = useForm({
+ initialValues: {
+ patient_id: patientId,
+ target_ward_id: '',
+ },
+ validate: {
+ target_ward_id: (value) => (!value ? 'Please select a ward' : null),
+ },
+ });
+
+ useEffect(() => {
+ // Load wards for selection
+ apiGetWardList(1, true).then(res => {
+ if (res.success) {
+ setWards(res.data.wards);
+ } else {
+ showErrorMessage(res.message, "Failed to load wards");
+ }
+ })
+ .catch(err => {
+ showErrorMessage("Failed to load wards", "Error");
+ })
+ .finally(() => {
+ setLoadingWards(false);
+ });
+ }, []);
+
+ const wardOptions = useMemo(() => {
+ return wards
+ .filter(ward => ward.id !== currentWardId && ward.current_occupancy < ward.total_capacity) // Exclude current ward and show only those with available capacity
+ .map(ward => ({
+ value: ward.id.toString(),
+ label: `${ward.name} (${ward.current_occupancy}/${ward.total_capacity}) - ${WardTypes[ward.type]}`
+ }));
+ }, [wards, currentWardId]);
+
+ const handleFormSubmit = (values: typeof form.values) => {
+ setLoading(true);
+ apiTransferWard(
+ values.patient_id,
+ parseInt(values.target_ward_id)
+ ).then(res => {
+ if (res.success) {
+ showInfoMessage("Patient transferred successfully", "Success");
+ modals.closeAll();
+ onSuccess();
+ } else {
+ showErrorMessage(res.message, "Failed to transfer patient");
+ }
+ })
+ .catch(err => {
+ showErrorMessage("Failed to transfer patient", "Error");
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ };
+
+ return (
+
+ );
+}
+
+export default function WardPatients ({ward_id, onChanged}: {ward_id: number, onChanged: () => void}) {
+ const [wardPatients, setWardPatients] = useState([])
+
+ useEffect(() => {
+ refreshWardPatients()
+ }, []);
+
+ const refreshWardPatients = () => {
+ apiGetWardPatients(ward_id).then((result) => {
+ if (result.success) {
+ setWardPatients(result.data.patients)
+ }
+ else {
+ showErrorMessage(result.message, "Failed to get ward patients")
+ }
+ }).catch((err => { showErrorMessage(err.toString(), "Failed to get ward patients") }) )
+ }
+
+ const handleTransferWard = (patientId: number) => {
+ modals.open({
+ title: "Transfer Patient",
+ centered: true,
+ size: "md",
+ children: (
+ {
+ refreshWardPatients()
+ onChanged()
+ }}
+ />
+ )
+ });
+ };
+
+ const rows = wardPatients.map((patient) => (
+
+ {`${patient.title} ${patient.name}`}
+ {patient.gender}
+ {patient.birth_date}
+ {patient.email}
+ {patient.phone}
+
+
+ handleTransferWard(patient.id)} title="Transfer to another ward">
+
+
+
+
+
+ ))
+
+ return (
+ <>
+
+
+
+
+
+ Name
+
+
+ Gender
+
+
+ Date of Birth
+
+
+ Email
+
+
+ Phone
+
+
+ Operations
+
+
+
+ {rows}
+
+
+ >
+ )
+}
diff --git a/HMS_Frontend/app/components/subs/confirms.tsx b/HMS_Frontend/app/components/subs/confirms.tsx
new file mode 100644
index 0000000..8e5722a
--- /dev/null
+++ b/HMS_Frontend/app/components/subs/confirms.tsx
@@ -0,0 +1,1598 @@
+import {modals} from "@mantine/modals";
+import React, {useMemo, useState, useEffect} from "react";
+import {
+ ActionIcon,
+ Button, Checkbox,
+ type ComboboxItem, CopyButton,
+ Group,
+ NumberInput,
+ type OptionsFilter, Radio,
+ Select,
+ Stack,
+ Text,
+ TextInput, Tooltip,
+ Table,
+ MultiSelect,
+ Textarea
+} from "@mantine/core";
+import {
+ apiAdminRegister,
+ apiCreateWard,
+ apiDeleteWard,
+ apiEditDoctorInfo,
+ apiEditWard,
+ apiResetPasswordFromRoleID,
+ apiSetDoctorResigned,
+ apiDeleteTeam,
+ apiCreateTeam,
+ apiEditTeam,
+ apiPatientBooking,
+ apiEditPatientBooking,
+ apiDeletePatientBooking,
+ apiGetTeamList,
+ apiProcessAppointment,
+ apiPatientAdmission,
+ apiGetWardList,
+ apiEditPatientInfo,
+ apiPatientDischarge
+} from "~/utils/hms_api.ts";
+import {parseDmyToDate, showErrorMessage, showInfoMessage, timestampToDate} from "~/utils/utils.ts";
+import {
+ type DoctorDataWithPermission,
+ type WardInfo,
+ WardTypes,
+ type DoctorTeamInfo,
+ type PatientBookingInfo,
+ type PatientDataWithWardAndAdmission, type PatientData, type DoctorData
+} from "~/utils/models.ts";
+import {useForm} from "@mantine/form";
+import {iconMStyle} from "~/styles.ts";
+import RenameIcon from "mdi-react/RenameIcon";
+import HumanEditIcon from "mdi-react/HumanEditIcon";
+import WardPatients from "~/components/subs/WardPatients.tsx";
+import {DoctorGrade, Departments, BookingCategory} from "~/utils/hms_enums.ts";
+import {DateInput, DateTimePicker} from "@mantine/dates";
+import CalendarIcon from "mdi-react/CalendarIcon";
+import PhoneIcon from "mdi-react/PhoneIcon";
+import MailIcon from "mdi-react/MailIcon";
+import GenderMaleFemaleIcon from "mdi-react/GenderMaleFemaleIcon";
+import AlphaGIcon from "mdi-react/AlphaGIcon";
+import HomeIcon from "mdi-react/HomeIcon";
+import CheckIcon from "mdi-react/CheckIcon";
+import ContentCopyIcon from "mdi-react/ContentCopyIcon";
+
+
+export function confirmDeleteWard(wardId: number, wardName: String, onFinished: () => void = () => {}) {
+ const onClickConfirmDeleteWard = (wardId: number) => {
+ apiDeleteWard(wardId).then(res => {
+ if (res.success) {
+ showInfoMessage("", "Ward deleted successfully", 3000)
+ } else {
+ showErrorMessage(res.message, "Delete Ward Failed")
+ }
+ }).catch(err => {
+ showErrorMessage(err.toString(), "Delete Ward Failed")
+ }).finally(() => onFinished())
+ modals.closeAll()
+ }
+
+ modals.open({
+ title: "Delete Ward",
+ centered: true,
+ children: (
+
+ Are you sure you want to delete the ward: {wardName}?
+
+
+
+
+
+ )
+ })
+}
+
+export function confirmEditOrCreateWard(origWardInfo: WardInfo | null, onSucceed: () => void = () => {}) {
+
+ function ConfirmEditOrCreateWard({wardInfo}: {wardInfo: WardInfo | null}) {
+ const form = useForm({
+ initialValues: {
+ ward_id: wardInfo ? wardInfo.id : -1,
+ ward_name: wardInfo ? wardInfo.name : "",
+ total_capacity: wardInfo ? wardInfo.total_capacity : 0,
+ type: wardInfo ? wardInfo.type : "GENERAL"
+ },
+
+ validate: {
+ ward_name: (value) => (value.length == 0 ? "Input ward name." : null),
+ total_capacity: (value) => (value < 1 ? "Total capacity must be greater than 0." : null),
+ },
+ });
+
+ const optionsFilter: OptionsFilter = ({ options, search }) => {
+ const filtered = (options as ComboboxItem[]).filter((option) =>
+ option.label.toLowerCase().trim().includes(search.toLowerCase().trim())
+ );
+
+ filtered.sort((a, b) => a.label.localeCompare(b.label));
+ return filtered;
+ }
+
+ const wardTypesOptions = useMemo(() => {
+ return Object.entries(WardTypes).map(([key, value]) => ({ value: key, label: value }))
+ }, [WardTypes])
+
+ const onClickConfirmEditWard = (values: {ward_id: number, ward_name: string, total_capacity: number, type: string}) => {
+ if (origWardInfo) {
+ apiEditWard(values.ward_id, values.ward_name, values.total_capacity, values.type).then(res => {
+ if (res.success) {
+ modals.closeAll()
+ showInfoMessage("", "Ward edited successfully", 3000)
+ onSucceed()
+ } else {
+ showErrorMessage(res.message, "Edit Ward Failed")
+ }
+ }).catch(err => {
+ showErrorMessage(err.toString(), "Edit Ward Failed")
+ })
+ } else {
+ apiCreateWard(values.ward_name, values.total_capacity, values.type).then(res => {
+ if (res.success) {
+ modals.closeAll()
+ showInfoMessage("", "Ward created successfully", 3000)
+ onSucceed()
+ } else {
+ showErrorMessage(res.message, "Create Ward Failed")
+ }
+ }).catch(err => {
+ showErrorMessage(err.toString(), "Create Ward Failed")
+ })
+ }
+ }
+
+ return (
+
+ )
+ }
+
+ modals.open({
+ title: origWardInfo ? "Edit Ward" : "Create Ward",
+ centered: true,
+ children: (
+
+ )
+ })
+}
+
+export function confirmCheckWardPatients(origWardId: number, onChanged: () => void = () => {}) {
+ modals.open({
+ title: "Ward Patients",
+ centered: true,
+ size: "70%",
+ children: (
+
+ )
+ })
+}
+
+export function confirmEditDoctor(
+ origDoctorInfo: DoctorDataWithPermission,
+ onSucceed: () => void = () => {},
+) {
+ function ConfirmEditOrCreateDoctor({
+ doctorInfo,
+ }: {
+ doctorInfo: DoctorDataWithPermission;
+ }) {
+ const form = useForm({
+ initialValues: {
+ id: doctorInfo ? doctorInfo.id : -1,
+ name: doctorInfo ? doctorInfo.name : '',
+ email: doctorInfo ? doctorInfo.email : '',
+ phone: doctorInfo ? doctorInfo.phone : '',
+ gender: doctorInfo ? doctorInfo.gender : 'M',
+ birth_date: doctorInfo ? parseDmyToDate(doctorInfo.birth_date) : new Date(2000, 1, 1),
+ title: doctorInfo ? doctorInfo.title : '',
+ grade: doctorInfo ? doctorInfo.grade.toString() : DoctorGrade.Unknown.toString(),
+ is_admin: doctorInfo ? doctorInfo.is_admin : false
+ },
+
+ validate: {
+ name: (v) => (v.length === 0 ? 'Input doctor name.' : null),
+ email: (v) =>
+ /^\S+@\S+\.\S+$/.test(v) ? null : 'Incorrect email address.',
+ phone: (v) =>
+ v.length === 0 || /^\+?\d{6,15}$/.test(v)
+ ? null
+ : 'Invalid phone number.',
+ birth_date: (v) => (v > new Date() ? 'Date of birth cannot be in the future' : null),
+ gender: (v) => (v.length === 0 ? "Select a gender" : null)
+ },
+ });
+
+ const genderOptions = useMemo(
+ () => [
+ { value: 'M', label: 'Male' },
+ { value: 'F', label: 'Female' },
+ { value: 'Intersex', label: 'Intersex' },
+ ],
+ [],
+ );
+
+ const gradeOptions = useMemo(
+ () =>
+ Object.entries(DoctorGrade)
+ .filter(([k]) => isNaN(Number(k)))
+ .map(([k, v]) => ({
+ value: v.toString(),
+ label: k,
+ })),
+ [],
+ );
+
+ const optionsFilter: OptionsFilter = ({ options, search }) => {
+ const res = (options as ComboboxItem[]).filter((o) =>
+ o.label.toLowerCase().includes(search.toLowerCase().trim()),
+ );
+ res.sort((a, b) => a.label.localeCompare(b.label));
+ return res;
+ };
+
+ const onClickConfirmEditDoctor = (values: typeof form.values) => {
+ apiEditDoctorInfo(values).then((res) => {
+ if (res.success) {
+ modals.closeAll();
+ showInfoMessage('', 'Doctor edited successfully', 3000);
+ onSucceed();
+ } else showErrorMessage(res.message, 'Edit Doctor Failed');
+ }).catch((err) =>
+ showErrorMessage(err.toString(), 'Edit Doctor Failed'),
+ );
+ };
+
+ return (
+
+ );
+ }
+
+ modals.open({
+ title: origDoctorInfo ? 'Edit Doctor' : 'Create Doctor',
+ centered: true,
+ size: "xl",
+ children: ,
+ });
+}
+
+export function confirmSetResignedDoctor(doctor_id: number, doctor_name: string, is_resigned: boolean, onFinished: () => void = () => {}) {
+ const onClickConfirmSetResigned = (doctor: number, resigned: boolean) => {
+ apiSetDoctorResigned(doctor, resigned).then(res => {
+ if (res.success) {
+ showInfoMessage("", "The operation has been completed successfully.", 3000)
+ }
+ else {
+ showErrorMessage(res.message, "The doctor's resignation failed.")
+ }
+ }).catch(err => {
+ showErrorMessage(err.toString(), "The doctor's resignation failed.")
+ }).finally(() => onFinished())
+ modals.closeAll()
+ }
+
+ modals.open({
+ title: is_resigned ? "Termination" : "Reinstatement",
+ centered: true,
+ children: (
+
+ { is_resigned ?
+ Are you certain you want to proceed with the termination of {doctor_name}?
+ :
+ Are you certain you want to proceed with the reinstatement of {doctor_name}?
+ }
+
+
+
+
+
+
+ )
+ })
+}
+
+
+export function confirmAdminAddUser(
+ initRegType: number = 1, // Patient: 0, Doctor: 1, Receptionist: 99
+ onSucceed: () => void = () => {},
+) {
+ function ConfirmAddUser() {
+ const form = useForm({
+ initialValues: {
+ reg_type: initRegType.toString(),
+ username: '',
+ name: '',
+ email: '',
+ phone: '',
+ gender: '',
+ birth_date: new Date(2000, 1, 1),
+ title: '',
+
+ address: '',
+ postcode: '',
+
+ grade: DoctorGrade.Unknown.toString(),
+ is_admin: false
+ },
+
+ validate: {
+ name: (v) => (v.length === 0 ? 'Input name.' : null),
+ email: (v) =>
+ /^\S+@\S+\.\S+$/.test(v) ? null : 'Incorrect email address.',
+ phone: (v) =>
+ v.length === 0 || /^\+?\d{6,15}$/.test(v)
+ ? null
+ : 'Invalid phone number.',
+ birth_date: (v) => (v > new Date() ? 'Date of birth cannot be in the future' : null),
+ gender: (v) => (v.length === 0 ? "Select a gender" : null)
+ },
+ });
+
+ const genderOptions = useMemo(
+ () => [
+ { value: 'M', label: 'Male' },
+ { value: 'F', label: 'Female' },
+ { value: 'Intersex', label: 'Intersex' },
+ ],
+ [],
+ );
+
+ const gradeOptions = useMemo(
+ () =>
+ Object.entries(DoctorGrade)
+ .filter(([k]) => isNaN(Number(k)))
+ .map(([k, v]) => ({
+ value: v.toString(),
+ label: k,
+ })),
+ [],
+ );
+
+ const optionsFilter: OptionsFilter = ({ options, search }) => {
+ const res = (options as ComboboxItem[]).filter((o) =>
+ o.label.toLowerCase().includes(search.toLowerCase().trim()),
+ );
+ res.sort((a, b) => a.label.localeCompare(b.label));
+ return res;
+ };
+
+ const onClickRegSubmit = (values: typeof form.values) => {
+ apiAdminRegister(values).then((res) => {
+ if (res.success) {
+ // modals.closeAll();
+ showRegAccountPassword("Added User Succeed", res.data?.username, res.data?.password)
+ onSucceed()
+ } else showErrorMessage(res.message, 'Add User Failed');
+ }).catch((err) =>
+ showErrorMessage(err.toString(), 'Add User Failed'),
+ );
+ };
+
+ return (
+
+ );
+ }
+
+ modals.open({
+ title: "Add User",
+ centered: true,
+ size: "70%",
+ children: ,
+ });
+}
+
+export function showRegAccountPassword(title: string, username?: string, password?: string) {
+ modals.open({
+ title: title,
+ centered: true,
+ closeOnEscape: false,
+ closeOnClickOutside: false,
+ withCloseButton: false,
+ children: (
+
+
+ Username: {username}
+ { username &&
+
+ {({ copied, copy }) => (
+
+
+ {copied ? : }
+
+
+ )}
+
+ }
+
+
+ Password: {password}
+ { password &&
+
+ {({ copied, copy }) => (
+
+
+ {copied ? : }
+
+
+ )}
+
+ }
+
+
+
+ {({ copied, copy }) => (
+
+ )}
+
+
+
+
+
+ ),
+ });
+}
+
+export function confirmResetPassword(user_id: number, user_type: number) {
+ function ConfirmResetPWD() {
+ const [resetting, setResetting] = useState(false)
+
+ return(
+
+ Are you sure you want to reset the password?
+
+
+
+
+
+ )
+ }
+
+ modals.open({
+ title: "Reset Password",
+ centered: true,
+ children:
+ });
+}
+
+export function confirmDeleteTeam(teamId: number, department: String, onFinished: () => void = () => {}) {
+ const onClickConfirmDeleteTeam = (teamId: number) => {
+ apiDeleteTeam(teamId).then(res => {
+ if (res.success) {
+ showInfoMessage("", "Medical team deleted successfully", 3000)
+ } else {
+ showErrorMessage(res.message, "Failed to delete medical team")
+ }
+ }).catch(err => {
+ showErrorMessage(err.toString(), "Failed to delete medical team")
+ }).finally(() => onFinished())
+ modals.closeAll()
+ }
+
+ modals.open({
+ title: "Delete Medical Team",
+ centered: true,
+ children: (
+
+ Are you sure you want to delete this medical team: {department}?
+
+
+
+
+
+ )
+ })
+}
+
+export function confirmEditOrCreateTeam(
+ origTeamInfo: DoctorTeamInfo | null,
+ doctorsList: DoctorDataWithPermission[] = [],
+ onSucceed: () => void = () => {}
+) {
+ function ConfirmEditOrCreateTeam({
+ teamInfo,
+ doctorsList
+ }: {
+ teamInfo: DoctorTeamInfo | null;
+ doctorsList: DoctorDataWithPermission[];
+ }) {
+ const [loading, setLoading] = useState(false);
+ const [availableDoctors, setAvailableDoctors] = useState([]);
+
+ useEffect(() => {
+ // Filter out resigned doctors
+ const activeDoctors = doctorsList.filter(d => !d.is_resigned);
+ setAvailableDoctors(activeDoctors);
+ }, [doctorsList]);
+
+ const form = useForm({
+ initialValues: {
+ team_id: teamInfo ? teamInfo.id : -1,
+ department: teamInfo ? teamInfo.department : '',
+ consultant_id: teamInfo ? teamInfo.consultant_id.toString() : '-1',
+ is_admin_team: teamInfo ? teamInfo.is_admin_team : false,
+ team_members: teamInfo
+ ? teamInfo.members.map(member => member.id)
+ : []
+ },
+ validate: {
+ department: (value) => (!value ? 'Please select a department' : null),
+ consultant_id: (value) => (value === '-1' || !value ? 'Please select a consultant' : null),
+ },
+ });
+
+ const departmentOptions = useMemo(() => {
+ return Object.entries(Departments).map(([key, value]) => ({
+ value: value as string,
+ label: key.replace(/_/g, ' ')
+ }));
+ }, []);
+
+ const consultantOptions = useMemo(() => {
+ return availableDoctors
+ .map(d => ({
+ value: d.id.toString(),
+ label: `${d.title} ${d.name}`
+ }));
+ }, [availableDoctors]);
+
+ const teamMemberOptions = useMemo(() => {
+ return availableDoctors.map(d => ({
+ value: d.id.toString(),
+ label: `${d.title} ${d.name} (${DoctorGrade[d.grade]})`
+ }));
+ }, [availableDoctors]);
+
+ const handleFormSubmit = (values: typeof form.values) => {
+ setLoading(true);
+
+ const teamData = {
+ ...values,
+ consultant_id: Number(values.consultant_id),
+ team_members: values.team_members.map(Number)
+ };
+
+ const apiCall = teamInfo
+ ? apiEditTeam(teamData)
+ : apiCreateTeam(teamData);
+
+ apiCall.then(res => {
+ if (res.success) {
+ modals.closeAll();
+ showInfoMessage("", teamInfo ? "Medical team updated successfully" : "Medical team created successfully", 3000);
+ onSucceed();
+ } else {
+ showErrorMessage(res.message, teamInfo ? "Failed to update medical team" : "Failed to create medical team");
+ }
+ }).catch(err => {
+ showErrorMessage(err.toString(), teamInfo ? "Failed to update medical team" : "Failed to create medical team");
+ }).finally(() => {
+ setLoading(false);
+ });
+ };
+
+ return (
+
+ );
+ }
+
+ modals.open({
+ title: origTeamInfo ? "Edit Medical Team" : "Create Medical Team",
+ centered: true,
+ size: "lg",
+ children: (
+
+ )
+ });
+}
+
+export function confirmViewTeamMembers(teamInfo: DoctorTeamInfo) {
+ modals.open({
+ title: `Medical Team: ${teamInfo.department.replace(/_/g, ' ')}`,
+ centered: true,
+ size: "lg",
+ children: (
+
+ Team Information
+ Department: {teamInfo.department.replace(/_/g, ' ')}
+
+ Consultant: {
+ teamInfo.members.find(m => m.id === teamInfo.consultant_id)
+ ? `${teamInfo.members.find(m => m.id === teamInfo.consultant_id)?.title || ''} ${teamInfo.members.find(m => m.id === teamInfo.consultant_id)?.name || ''}`
+ : `ID: ${teamInfo.consultant_id}`
+ }
+
+ Admin Team: {teamInfo.is_admin_team ? 'Yes' : 'No'}
+
+ Team Members ({teamInfo.members.length})
+ {teamInfo.members.length > 0 ? (
+
+
+
+ ID
+ Name
+ Grade
+ Gender
+ Contact
+
+
+
+ {teamInfo.members.map(member => (
+
+ {member.id}
+ {`${member.title} ${member.name}`}
+ {DoctorGrade[member.grade]}
+ {member.gender}
+ {member.email}
+
+ ))}
+
+
+ ) : (
+ No team members
+ )}
+
+
+
+
+
+ )
+ });
+}
+
+export function confirmDeleteBooking(appointmentId: number, category: string, onFinished: () => void = () => {}) {
+ const onClickConfirmDeleteBooking = (appointmentId: number) => {
+ apiDeletePatientBooking(appointmentId).then(res => {
+ if (res.success) {
+ showInfoMessage("", "Appointment deleted successfully", 3000)
+ } else {
+ showErrorMessage(res.message, "Failed to delete appointment")
+ }
+ }).catch(err => {
+ showErrorMessage(err.toString(), "Failed to delete appointment")
+ }).finally(() => onFinished())
+ modals.closeAll()
+ }
+
+ modals.open({
+ title: "Delete Appointment",
+ centered: true,
+ children: (
+
+ Are you sure you want to delete this appointment: {category}?
+
+
+
+
+
+ )
+ })
+}
+
+export function confirmEditOrCreateBooking(
+ origBookingInfo: PatientBookingInfo | null,
+ onSucceed: () => void = () => {}
+) {
+ function ConfirmEditOrCreateBooking({
+ bookingInfo
+ }: {
+ bookingInfo: PatientBookingInfo | null;
+ }) {
+ const [loading, setLoading] = useState(false);
+
+ const form = useForm({
+ initialValues: {
+ appointment_id: bookingInfo ? bookingInfo.id : -1,
+ appointment_time: bookingInfo ? new Date(bookingInfo.appointment_time * 1000) : new Date(),
+ category: bookingInfo ? bookingInfo.category : BookingCategory.Consultation,
+ description: bookingInfo ? bookingInfo.description : '',
+ },
+ validate: {
+ appointment_time: (value) => (!value ? 'Please select appointment time' : null),
+ category: (value) => (!value ? 'Please select category' : null),
+ description: (value) => (!value || value.length < 5 ? 'Description must be at least 5 characters' : null),
+ },
+ });
+
+ const categoryOptions = useMemo(() => {
+ return Object.entries(BookingCategory).map(([key, value]) => ({
+ value: value,
+ label: key.replace(/_/g, ' ')
+ }));
+ }, []);
+
+ const handleFormSubmit = (values: typeof form.values) => {
+ setLoading(true);
+
+ const appointmentTime = Math.floor(values.appointment_time.getTime() / 1000);
+
+ const bookingData = {
+ ...values,
+ appointment_time: appointmentTime,
+ };
+
+ const apiCall = bookingInfo
+ ? apiEditPatientBooking(bookingData)
+ : apiPatientBooking(bookingData);
+
+ apiCall.then(res => {
+ if (res.success) {
+ modals.closeAll();
+ showInfoMessage("", bookingInfo ? "Appointment updated successfully" : "Appointment created successfully", 3000);
+ onSucceed();
+ } else {
+ showErrorMessage(res.message, bookingInfo ? "Failed to update appointment" : "Failed to create appointment");
+ }
+ }).catch(err => {
+ showErrorMessage(err.toString(), bookingInfo ? "Failed to update appointment" : "Failed to create appointment");
+ }).finally(() => {
+ setLoading(false);
+ });
+ };
+
+ return (
+
+ );
+ }
+
+ modals.open({
+ title: origBookingInfo ? "Edit Appointment" : "Create Appointment",
+ centered: true,
+ size: "lg",
+ children: (
+
+ )
+ });
+}
+
+export function confirmApproveAppointment(
+ appointmentId: number,
+ onSucceed: () => void = () => {}
+) {
+ function ConfirmApproveAppointment() {
+ const [loading, setLoading] = useState(false);
+ const [teams, setTeams] = useState([]);
+ const [loadingTeams, setLoadingTeams] = useState(true);
+
+ const form = useForm({
+ initialValues: {
+ appointment_id: appointmentId,
+ approved: true,
+ feedback: '',
+ assigned_team: '',
+ },
+ validate: {
+ feedback: (value) => (!value ? 'Please provide feedback' : null),
+ assigned_team: (value) => (!value ? 'Please select a medical team' : null),
+ },
+ });
+
+ useEffect(() => {
+ // Load teams for selection
+ apiGetTeamList(1).then(res => {
+ if (res.success) {
+ setTeams(res.data.teams);
+ } else {
+ showErrorMessage(res.message, "Failed to load medical teams");
+ }
+ })
+ .catch(err => {
+ showErrorMessage("Failed to load medical teams", "Error");
+ })
+ .finally(() => {
+ setLoadingTeams(false);
+ });
+ }, []);
+
+ const teamOptions = useMemo(() => {
+ return teams.map(team => ({
+ value: team.id.toString(),
+ label: `${team.department.replace(/_/g, ' ')} (ID: ${team.id})`
+ }));
+ }, [teams]);
+
+ const handleFormSubmit = (values: typeof form.values) => {
+ setLoading(true);
+
+ apiProcessAppointment(
+ values.appointment_id,
+ values.approved,
+ values.feedback,
+ parseInt(values.assigned_team)
+ ).then(res => {
+ if (res.success) {
+ modals.closeAll();
+ showInfoMessage("", "Appointment approved successfully", 3000);
+ onSucceed();
+ } else {
+ showErrorMessage(res.message, "Failed to approve appointment");
+ }
+ }).catch(err => {
+ showErrorMessage("Failed to approve appointment", "Error");
+ }).finally(() => {
+ setLoading(false);
+ });
+ };
+
+ return (
+
+ );
+ }
+
+ modals.open({
+ title: "Approve Appointment",
+ centered: true,
+ size: "md",
+ children:
+ });
+}
+
+export function confirmRejectAppointment(
+ appointmentId: number,
+ onSucceed: () => void = () => {}
+) {
+ function ConfirmRejectAppointment() {
+ const [loading, setLoading] = useState(false);
+
+ const form = useForm({
+ initialValues: {
+ appointment_id: appointmentId,
+ approved: true,
+ feedback: '',
+ assigned_team: null,
+ },
+ validate: {
+ feedback: (value) => (!value ? 'Please provide rejection reason' : null),
+ },
+ });
+
+ const handleFormSubmit = (values: typeof form.values) => {
+ setLoading(true);
+
+ apiProcessAppointment(
+ values.appointment_id,
+ values.approved,
+ values.feedback,
+ values.assigned_team
+ ).then(res => {
+ if (res.success) {
+ modals.closeAll();
+ showInfoMessage("", "Appointment rejected successfully", 3000);
+ onSucceed();
+ } else {
+ showErrorMessage(res.message, "Failed to reject appointment");
+ }
+ }).catch(err => {
+ showErrorMessage("Failed to reject appointment", "Error");
+ }).finally(() => {
+ setLoading(false);
+ });
+ };
+
+ return (
+
+ );
+ }
+
+ modals.open({
+ title: "Reject Appointment",
+ centered: true,
+ size: "md",
+ children:
+ });
+}
+
+export function confirmPatientAdmission(
+ appointmentId: number,
+ onSucceed: () => void = () => {}
+) {
+ function ConfirmPatientAdmission() {
+ const [loading, setLoading] = useState(false);
+ const [wards, setWards] = useState([]);
+ const [loadingWards, setLoadingWards] = useState(true);
+
+ const form = useForm({
+ initialValues: {
+ appointment_id: appointmentId,
+ ward_id: '',
+ },
+ validate: {
+ ward_id: (value) => (!value ? 'Please select a ward' : null),
+ },
+ });
+
+ useEffect(() => {
+ // Load wards for selection
+ apiGetWardList(1, true).then(res => {
+ if (res.success) {
+ setWards(res.data.wards);
+ } else {
+ showErrorMessage(res.message, "Failed to load wards");
+ }
+ })
+ .catch(err => {
+ showErrorMessage("Failed to load wards", "Error");
+ })
+ .finally(() => {
+ setLoadingWards(false);
+ });
+ }, []);
+
+ const wardOptions = useMemo(() => {
+ return wards
+ .filter(ward => ward.current_occupancy < ward.total_capacity) // Only show wards with available capacity
+ .map(ward => ({
+ value: ward.id.toString(),
+ label: `${ward.name} (${ward.current_occupancy}/${ward.total_capacity}) - ${WardTypes[ward.type]}`
+ }));
+ }, [wards]);
+
+ const handleFormSubmit = (values: typeof form.values) => {
+ setLoading(true);
+
+ apiPatientAdmission(
+ values.appointment_id,
+ parseInt(values.ward_id)
+ ).then(res => {
+ if (res.success) {
+ modals.closeAll();
+ showInfoMessage("", "Patient admitted successfully", 3000);
+ onSucceed();
+ } else {
+ showErrorMessage(res.message, "Failed to admit patient");
+ }
+ }).catch(err => {
+ showErrorMessage("Failed to admit patient", "Error");
+ }).finally(() => {
+ setLoading(false);
+ });
+ };
+
+ return (
+
+ );
+ }
+
+ modals.open({
+ title: "Patient Admission",
+ centered: true,
+ size: "md",
+ children:
+ });
+}
+
+export function confirmEditPatient(
+ origPatientInfo: PatientData,
+ onSucceed: () => void = () => {},
+) {
+ function ConfirmEditPatient({
+ patientInfo,
+ }: {
+ patientInfo: PatientData;
+ }) {
+ const form = useForm({
+ initialValues: {
+ patient_id: patientInfo.id,
+ title: patientInfo.title,
+ name: patientInfo.name,
+ gender: patientInfo.gender,
+ birth_date: parseDmyToDate(patientInfo.birth_date),
+ email: patientInfo.email,
+ phone: patientInfo.phone,
+ address: patientInfo.address,
+ postcode: patientInfo.postcode,
+ },
+ validate: {
+ name: (value) => (!value || value.trim() === "" ? "Name cannot be empty" : null),
+ title: (value) => (!value || value.trim() === "" ? "Title cannot be empty" : null),
+ email: (value) => (!/^\S+@\S+$/.test(value) ? "Please enter a valid email" : null),
+ phone: (value) => (!value || value.length < 6 ? "Please enter a valid phone number" : null),
+ address: (value) => (!value || value.trim() === "" ? "Address cannot be empty" : null),
+ postcode: (value) => (!value || value.trim() === "" ? "Postcode cannot be empty" : null),
+ },
+ });
+
+ const onClickConfirmEditPatient = (values: typeof form.values) => {
+ apiEditPatientInfo(values).then(res => {
+ if (res.success) {
+ modals.closeAll()
+ showInfoMessage("", "Patient information updated successfully", 3000)
+ onSucceed()
+ } else {
+ showErrorMessage(res.message, "Failed to update patient information")
+ }
+ }).catch(err => {
+ showErrorMessage(err.toString(), "Failed to update patient information")
+ })
+ }
+
+ return (
+
+ );
+ }
+
+ modals.open({
+ title: "Edit Patient Information",
+ centered: true,
+ size: "xl",
+ children: ,
+ });
+}
+
+export function confirmPatientDischarge(admissionId: number, patientName: string, onFinished: () => void = () => {}) {
+ const onClickConfirmDischarge = (admissionId: number) => {
+ apiPatientDischarge(admissionId).then(res => {
+ if (res.success) {
+ modals.closeAll()
+ showInfoMessage("", "Patient discharged successfully", 3000)
+ onFinished()
+ } else {
+ showErrorMessage(res.message, "Failed to discharge patient")
+ }
+ }).catch(err => {
+ showErrorMessage(err.toString(), "Failed to discharge patient")
+ })
+ }
+
+ modals.open({
+ title: "Confirm Patient Discharge",
+ centered: true,
+ children: (
+
+ Are you sure you want to discharge {patientName}?
+
+
+
+
+
+ ),
+ })
+}
diff --git a/HMS_Frontend/app/pages/Dashboard.tsx b/HMS_Frontend/app/pages/Dashboard.tsx
new file mode 100644
index 0000000..8fce117
--- /dev/null
+++ b/HMS_Frontend/app/pages/Dashboard.tsx
@@ -0,0 +1,103 @@
+import type {Route} from "../../.react-router/types/app/pages/+types/Dashboard";
+import {useDisclosure, useWindowScroll} from "@mantine/hooks";
+import {Affix, AppShell, Button, Space, Stack, Transition} from "@mantine/core";
+import {PageHeader} from "~/components/PageHeader.tsx";
+import {PageNavbar} from "~/components/PageNavbar.tsx";
+import {useEffect, useState} from "react";
+import {DashboardPageType, Departments, UserPermissionLevel} from "~/utils/hms_enums.ts";
+import {Outlet, useLocation, useNavigate} from "react-router";
+import type {LoginUserInfo} from "~/utils/models.ts";
+import {apiGetMyInfo} from "~/utils/hms_api.ts";
+import {getEnumKeyByValue, getUserDisplayFullName} from "~/utils/utils.ts";
+import ArrowUpIcon from "mdi-react/ArrowUpIcon";
+import HomeIcon from "mdi-react/HomeIcon";
+import GlobalAffix from "~/components/GlobalAffix.tsx";
+
+export function meta({}: Route.MetaArgs) {
+ return [
+ { title: "Dashboard" },
+ { name: "description", content: "Dashboard Main" },
+ ];
+}
+
+export default function Component() {
+ const navigate = useNavigate();
+ const [currentStat, setCurrentStat] = useState(DashboardPageType.Home)
+ const [loginUserInfo, setLoginUserInfo] = useState(null)
+ const [canReception, setCanReception] = useState(false)
+ const [opened, { toggle }] = useDisclosure()
+ const location = useLocation();
+
+ const changePage = (pageType: DashboardPageType, navigateTo?: string) => {
+ // if (currentStat == pageType) return
+ setCurrentStat(pageType)
+ navigate(navigateTo || pageType)
+ }
+
+ const refreshCanReception = (data: LoginUserInfo) => {
+ if (!data.doctor_teams) {
+ setCanReception(false)
+ return
+ }
+
+ for (const team of data.doctor_teams) {
+ if ((team.department == Departments.Reception) || team.is_admin_team) {
+ setCanReception(true)
+ return
+ }
+ }
+ }
+
+ const refreshMyInfo = () => {
+ apiGetMyInfo()
+ .then((res => {
+ if (res.success && res.data) {
+ setLoginUserInfo(res.data)
+ refreshCanReception(res.data)
+ }
+ else {
+ navigate(DashboardPageType.LoginPage)
+ }
+ }))
+ .catch(err => {
+ console.log("apiGetMyInfo failed", err)
+ navigate(DashboardPageType.LoginPage)
+ })
+ }
+
+ useEffect(() => {
+ refreshMyInfo()
+ let currPage = getEnumKeyByValue(DashboardPageType, location.pathname)
+ if (currPage) {
+ setCurrentStat(DashboardPageType[currPage])
+ }
+ else {
+ const pathParts = location.pathname.split("/")
+ const path = pathParts.slice(0, pathParts.length - 1).join("/")
+ currPage = getEnumKeyByValue(DashboardPageType, path + "/")
+ if (currPage) {
+ setCurrentStat(DashboardPageType[currPage])
+ }
+ }
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/HMS_Frontend/app/pages/HomePage.tsx b/HMS_Frontend/app/pages/HomePage.tsx
new file mode 100644
index 0000000..9dfce5e
--- /dev/null
+++ b/HMS_Frontend/app/pages/HomePage.tsx
@@ -0,0 +1,53 @@
+import type {Route} from "../../.react-router/types/app/pages/+types/HomePage";
+import {Box, Button, Flex, Group, Image, Text, Select, MantineProvider, em} from '@mantine/core';
+import {Link, useNavigate} from "react-router";
+import {marginRound, marginTopBottom, maxWidth} from "~/styles";
+import {Notifications} from "@mantine/notifications";
+import GlobalAffix from "~/components/GlobalAffix.tsx";
+
+export function meta({}: Route.MetaArgs) {
+ return [
+ { title: "HomePage" },
+ { name: "description", content: "Welcome!" },
+ ];
+}
+
+export default function Component() {
+ const navigate = useNavigate()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/HMS_Frontend/app/pages/Page2.tsx b/HMS_Frontend/app/pages/Page2.tsx
new file mode 100644
index 0000000..04ff2b1
--- /dev/null
+++ b/HMS_Frontend/app/pages/Page2.tsx
@@ -0,0 +1,12 @@
+import type {Route} from "../../.react-router/types/app/pages/+types/Page2";
+
+export function meta({}: Route.MetaArgs) {
+ return [
+ { title: "ExamplePage2" },
+ { name: "description", content: "Welcome Example2!" },
+ ];
+}
+
+export default function Component() {
+ return Example Page
;
+}
diff --git a/HMS_Frontend/app/pages/dashboard/AppointmentManagement.tsx b/HMS_Frontend/app/pages/dashboard/AppointmentManagement.tsx
new file mode 100644
index 0000000..8d59313
--- /dev/null
+++ b/HMS_Frontend/app/pages/dashboard/AppointmentManagement.tsx
@@ -0,0 +1,416 @@
+import type {Route} from "../../../.react-router/types/app/pages/dashboard/+types/AppointmentManagement";
+import {useDisclosure} from "@mantine/hooks";
+import {useEffect, useMemo, useState} from "react";
+import {BookingCategory, DashboardPageType} from "~/utils/hms_enums.ts";
+import {useNavigate, useOutletContext} from "react-router";
+import {
+ type OutletContextType,
+ type PatientBookingInfo,
+ type DoctorTeamInfo,
+ SORT_SYMBOLS
+} from "~/utils/models.ts";
+import {Accordion, ActionIcon, Badge, Button, Card, Group, Pagination, Stack, Table, Text} from "@mantine/core";
+import {apiGetAllAppointments, get_team_info} from "~/utils/hms_api.ts";
+import {showErrorMessage, timestampToDate} from "~/utils/utils.ts";
+import {iconMStyle, marginLeftRight, marginRightBottom, marginRound, marginTopBottom, textCenter} from "~/styles.ts";
+import CheckCircleIcon from "mdi-react/CheckCircleIcon";
+import CancelIcon from "mdi-react/CancelIcon";
+import HospitalIcon from "mdi-react/HospitalIcon";
+import {
+ confirmApproveAppointment,
+ confirmRejectAppointment,
+ confirmPatientAdmission,
+ confirmViewTeamMembers
+} from "~/components/subs/confirms.tsx";
+import {AppointmentManageFilter} from "~/components/subs/AppointmentManageFilter.tsx";
+import HomePlusIcon from "mdi-react/HomePlusIcon";
+import {TruncatedText} from "~/components/subs/TruncatedText.tsx";
+import InfoIcon from "mdi-react/InfoIcon";
+import {PatientInfoDisplay} from "~/components/subs/PatientInfoDisplay.tsx";
+import {ResponsiveTableContainer} from "~/components/subs/ResponsiveTableContainer.tsx";
+
+export function meta({}: Route.MetaArgs) {
+ return [
+ { title: "Appointment Management" },
+ { name: "description", content: "Appointment Management" },
+ ];
+}
+
+export default function Component() {
+ const navigate = useNavigate();
+ const [refreshingAppointmentList, setRefreshingAppointmentList] = useState(false);
+ const [appointmentInfo, setAppointmentInfo] = useState<{appointments: PatientBookingInfo[], total_pages: number}>({appointments: [], total_pages: 1});
+ const [teamInfoMap, setTeamInfoMap] = useState