diff --git a/app/__init__.py b/app/__init__.py index 397c934f5ef4e0f37fdcb51cd3a35c44cd2890d3..69d49d2f76674ae0f2dccdfd986a4c815d533e08 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -5,6 +5,7 @@ from flask_login import LoginManager from flask_bcrypt import Bcrypt from flask_migrate import Migrate from flask_wtf.csrf import CSRFProtect +from datetime import datetime # Khởi tạo các extensions ở cấp độ module db = SQLAlchemy() @@ -17,6 +18,26 @@ login_manager.login_view = 'auth.login' login_manager.login_message = 'Vui lòng đăng nhập để truy cập trang này.' login_manager.login_message_category = 'info' +# --- Định nghĩa Jinja filter --- +def format_datetime_filter(value, format='%d/%m/%Y %H:%M:%S'): + """Format a datetime object for display.""" + if value is None: + return "N/A" + if isinstance(value, str): + # Cố gắng parse nếu là string (tùy chọn, nhưng hữu ích) + try: + value = datetime.fromisoformat(value) + except ValueError: + try: + # Thử định dạng phổ biến khác nếu cần + value = datetime.strptime(value, '%Y-%m-%d %H:%M:%S') + except ValueError: + return value # Trả về string gốc nếu không parse được + if not isinstance(value, datetime): + return value # Trả về giá trị gốc nếu không phải datetime + return value.strftime(format) +# --------------------------- + def create_app(config_class=None): # Có thể bỏ config_class hoặc dùng cho cấu hình cơ bản app = Flask(__name__, instance_relative_config=True) @@ -48,6 +69,10 @@ def create_app(config_class=None): # Có thể bỏ config_class hoặc dùng ch migrate.init_app(app, db) csrf.init_app(app) + # Đăng ký Jinja filter + app.jinja_env.filters['format_datetime'] = format_datetime_filter + # -------------------- + # Import và đăng ký Blueprints from .routes.auth import auth_bp from .routes.patients import patients_bp @@ -55,6 +80,7 @@ def create_app(config_class=None): # Có thể bỏ config_class hoặc dùng ch from .routes.upload import upload_bp from .routes.dietitians import dietitians_bp from .routes.dashboard import dashboard_bp + from .routes.notifications import notifications_bp app.register_blueprint(auth_bp) app.register_blueprint(patients_bp) @@ -62,6 +88,7 @@ def create_app(config_class=None): # Có thể bỏ config_class hoặc dùng ch app.register_blueprint(upload_bp) app.register_blueprint(dietitians_bp) app.register_blueprint(dashboard_bp) + app.register_blueprint(notifications_bp) # Import models để SQLAlchemy biết về chúng with app.app_context(): diff --git a/app/models/__init__.py b/app/models/__init__.py index 48524af6bad835e4acfdb50759d0f96923cbcdfa..7852de55131c4c3edfa3c2a7eadf115feded0449 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,6 +1,7 @@ # Import all models here to make them available to the app from .user import User -from .patient import Patient, Encounter +from .patient import Patient +from .encounter import Encounter from .measurement import PhysiologicalMeasurement from .referral import Referral from .procedure import Procedure @@ -8,6 +9,7 @@ from .report import Report from .uploaded_file import UploadedFile from .dietitian import Dietitian, DietitianStatus from .activity_log import ActivityLog +from .notification import Notification # Import bất kỳ model bổ sung nào ở đây @@ -22,5 +24,6 @@ __all__ = [ 'UploadedFile', 'Dietitian', 'DietitianStatus', - 'ActivityLog' + 'ActivityLog', + 'Notification' ] \ No newline at end of file diff --git a/app/models/encounter.py b/app/models/encounter.py new file mode 100644 index 0000000000000000000000000000000000000000..77bb0daa0bebe4a11795a902b17b8f8071cd33b5 --- /dev/null +++ b/app/models/encounter.py @@ -0,0 +1,25 @@ +from app import db +from datetime import datetime +from app.models.user import User +from app.models.patient import Patient + +class Encounter(db.Model): + __tablename__ = 'encounters' + + encounterID = db.Column(db.Integer, primary_key=True) + custom_encounter_id = db.Column(db.String(50), unique=True, nullable=True, index=True) + patient_id = db.Column(db.String(20), db.ForeignKey('patients.patientID'), nullable=False) + notes = db.Column(db.Text) + start_time = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True) + dietitian_id = db.Column(db.Integer, db.ForeignKey('users.userID'), nullable=True) + status = db.Column(db.String(50), default='Active', nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + patient = db.relationship('Patient', back_populates='encounters') + assigned_dietitian = db.relationship('User', foreign_keys=[dietitian_id], backref='assigned_encounters') + measurements = db.relationship('PhysiologicalMeasurement', backref='encounter', lazy='dynamic', cascade='all, delete-orphan') + + def __repr__(self): + display_id = self.custom_encounter_id if self.custom_encounter_id else self.encounterID + return f'<Encounter {display_id} for Patient {self.patient_id} started {self.start_time}>' \ No newline at end of file diff --git a/app/models/measurement.py b/app/models/measurement.py index 699d05236e200d63d5668f32cf2793be485b30c1..4b64800ff1dcc302d2706dcfbf50242e5dd0ee33 100644 --- a/app/models/measurement.py +++ b/app/models/measurement.py @@ -6,11 +6,11 @@ class PhysiologicalMeasurement(db.Model): __tablename__ = 'physiologicalmeasurements' id = db.Column('measurementID', db.Integer, primary_key=True) - encounter_id = db.Column('encounterId', db.Integer, db.ForeignKey('encounters.encounterId'), nullable=False) + encounter_id = db.Column('encounterID', db.Integer, db.ForeignKey('encounters.encounterID'), nullable=False, index=True) patient_id = db.Column('patientID', db.String(20), db.ForeignKey('patients.patientID'), nullable=False) # Các trường từ schema MySQL - measurementDateTime = db.Column(db.DateTime, default=datetime.utcnow) + measurementDateTime = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, index=True) end_tidal_co2 = db.Column(db.Float) feed_vol = db.Column(db.Float) feed_vol_adm = db.Column(db.Float) @@ -46,4 +46,4 @@ class PhysiologicalMeasurement(db.Model): updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) def __repr__(self): - return f'<PhysiologicalMeasurement {self.id} for Encounter {self.encounter_id}>' \ No newline at end of file + return f'<PhysiologicalMeasurement {self.id} for Patient {self.patient_id} in Encounter {self.encounter_id} at {self.measurementDateTime}>' \ No newline at end of file diff --git a/app/models/notification.py b/app/models/notification.py new file mode 100644 index 0000000000000000000000000000000000000000..7eeb82aaa0f46857bfb6305debd9b7f79d6cc8df --- /dev/null +++ b/app/models/notification.py @@ -0,0 +1,27 @@ +from app import db +from datetime import datetime +from sqlalchemy.dialects.mysql import INTEGER # Sử dụng INTEGER(unsigned=True) nếu cần + +class Notification(db.Model): + __tablename__ = 'notifications' # Tên bảng trong database + + id = db.Column(INTEGER(unsigned=True), primary_key=True) # Khóa chính (giữ unsigned vì là PK tự tăng) + user_id = db.Column(db.Integer, db.ForeignKey('users.userID'), nullable=False, index=True) # Khóa ngoại tới bảng users (người nhận) + message = db.Column(db.Text, nullable=False) # Nội dung thông báo + timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) # Thời gian tạo + is_read = db.Column(db.Boolean, default=False, nullable=False) # Trạng thái đã đọc + link = db.Column(db.String(255), nullable=True) # Đường dẫn liên kết (tùy chọn) + # type = db.Column(db.String(50), nullable=True, index=True) # Loại thông báo (vd: 'referral', 'system', 'alert', 'measurement_threshold') + + # Định nghĩa relationship tới User model + # 'user' sẽ là thuộc tính để truy cập đối tượng User từ Notification + # 'backref='notifications'' tạo thuộc tính user.notifications để truy cập các thông báo của user đó + user = db.relationship('User', backref=db.backref('notifications', lazy='dynamic', order_by=timestamp.desc())) + + def __repr__(self): + return f'<Notification {self.id} for User {self.user_id}>' + + # Có thể thêm các phương thức khác nếu cần + # def mark_as_read(self): + # self.is_read = True + # db.session.add(self) \ No newline at end of file diff --git a/app/models/patient.py b/app/models/patient.py index d47231f9b32dc243b22e48c5fd693390fbb85c41..da8b5db969506acba0941a31ffb444c67527b30a 100644 --- a/app/models/patient.py +++ b/app/models/patient.py @@ -1,7 +1,7 @@ from datetime import datetime, date from app import db from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, Date, ForeignKey, Enum, Text +from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, Date, ForeignKey, Enum, Text, desc from sqlalchemy.orm import relationship import math import enum @@ -32,8 +32,7 @@ class Patient(db.Model): updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relationships - encounters = relationship('Encounter', backref='patient_info', lazy=True, - foreign_keys='Encounter.patientID') + encounters = relationship('Encounter', back_populates='patient', lazy='dynamic', order_by=desc('Encounter.start_time')) dietitian_id = Column(Integer, ForeignKey('dietitians.dietitianID'), nullable=True) dietitian = relationship('Dietitian', backref='patients', lazy=True) @@ -99,37 +98,4 @@ class Patient(db.Model): # BMI từ 0 đến 40 là thang đo phổ biến, quy về 0-100% percentage = min(100, max(0, (self.bmi / 40) * 100)) - return percentage - - -class Encounter(db.Model): - """Encounter model representing a patient visit or stay""" - __tablename__ = 'encounters' - - id = Column('encounterId', Integer, primary_key=True) - patientID = Column(String(20), ForeignKey('patients.patientID'), nullable=False) - - # Encounter details - admissionDateTime = Column(DateTime, nullable=False) - dischargeDateTime = Column(DateTime) - ccuAdmissionDateTime = Column(DateTime) - ccuDischargeDateTime = Column(DateTime) - - sedated = Column(Boolean, default=False) - externalFeeding = Column(Boolean, default=False) - renalReplacementTherapy = Column(Boolean, default=False) - - dietitianID = Column(Integer, ForeignKey('dietitians.dietitianID'), nullable=True) - dietitian = relationship('Dietitian', backref='encounters', lazy=True) - - # Timestamps - createdAt = Column(DateTime, default=datetime.utcnow) - updatedAt = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - def __repr__(self): - return f'<Encounter {self.id}>' - - @property - def admission_date(self): - """Thuộc tính ảo để tương thích với code cũ. Trả về giá trị của admissionDateTime""" - return self.admissionDateTime \ No newline at end of file + return percentage \ No newline at end of file diff --git a/app/models/procedure.py b/app/models/procedure.py index f2bb2a20305ddb0c491cc056f3496b12304b77aa..6a7ee6d4504c40f399821beb184024e162c82c29 100644 --- a/app/models/procedure.py +++ b/app/models/procedure.py @@ -6,7 +6,7 @@ class Procedure(db.Model): __tablename__ = 'procedures' id = db.Column('procedureID', db.Integer, primary_key=True) - encounter_id = db.Column('encounterId', db.Integer, db.ForeignKey('encounters.encounterId'), nullable=False) + encounter_id = db.Column('encounterID', db.Integer, db.ForeignKey('encounters.encounterID'), nullable=False) patient_id = db.Column('patientID', db.String(20), db.ForeignKey('patients.patientID'), nullable=False) # Procedure details diff --git a/app/models/referral.py b/app/models/referral.py index d8280189e58fd67cc2c001f58f0f98649f88a8f3..7267a352467bcf0019fbf88799d3c58a699d98f2 100644 --- a/app/models/referral.py +++ b/app/models/referral.py @@ -7,7 +7,7 @@ class Referral(db.Model): __tablename__ = 'referrals' id = db.Column('referralID', db.Integer, primary_key=True) - encounter_id = db.Column('encounterId', db.Integer, db.ForeignKey('encounters.encounterId'), nullable=False) + encounter_id = db.Column('encounterID', db.Integer, db.ForeignKey('encounters.encounterID'), nullable=False) patient_id = db.Column('patientID', db.String(20), db.ForeignKey('patients.patientID'), nullable=False) # Referral fields from schema diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index 17a8eebb456a62731835d34ff46a1a4bb0739b70..f1e8b869d75491552fd6cb3f517bf24ab2bab9cc 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -1,7 +1,8 @@ from flask import Blueprint, render_template, jsonify, redirect, url_for from flask_login import login_required, current_user from app import db -from app.models.patient import Patient, Encounter +from app.models.patient import Patient +from app.models.encounter import Encounter from app.models.referral import Referral from app.models.procedure import Procedure from app.models.report import Report @@ -31,8 +32,8 @@ def index(): # Get recent referrals recent_referrals = (Referral.query - .join(Encounter, Referral.encounter_id == Encounter.id) - .join(Patient, Encounter.patientID == Patient.id) + .join(Encounter, Referral.encounter_id == Encounter.encounterID) + .join(Patient, Encounter.patient_id == Patient.id) .add_columns(Patient.firstName, Patient.lastName, Patient.id) .order_by(Referral.created_at.desc()) .limit(5) diff --git a/app/routes/dietitians.py b/app/routes/dietitians.py index 53a1a3438f41d29dea189c257f24de55a068b6c8..b77346d148595313972560079faa7be1545b0821 100644 --- a/app/routes/dietitians.py +++ b/app/routes/dietitians.py @@ -2,7 +2,8 @@ from flask import Blueprint, render_template, request, redirect, url_for, flash, from flask_login import login_required, current_user from app import db from app.models.dietitian import Dietitian, DietitianStatus -from app.models.patient import Patient, Encounter +from app.models.patient import Patient +from app.models.encounter import Encounter from sqlalchemy import desc, or_, func, text from datetime import datetime from app.models.user import User diff --git a/app/routes/notifications.py b/app/routes/notifications.py new file mode 100644 index 0000000000000000000000000000000000000000..b8ffef00c00956668a4b2e12be4866ce13574e31 --- /dev/null +++ b/app/routes/notifications.py @@ -0,0 +1,95 @@ +# app/routes/notifications.py +from flask import Blueprint, jsonify +from flask_login import login_required, current_user +from app.models.notification import Notification +from app import db + +notifications_bp = Blueprint('notifications', __name__, url_prefix='/notifications') + +@notifications_bp.route('/api/unread') +@login_required +def get_unread_notifications(): + """API endpoint to get unread notifications for the current user.""" + # Lấy thông báo chưa đọc từ database + # unread_notifications = Notification.query.filter_by(user_id=current_user.userID, is_read=False).order_by(Notification.timestamp.desc()).limit(10).all() + # count = Notification.query.filter_by(user_id=current_user.userID, is_read=False).count() + + # Tạm thời trả về dữ liệu giả + unread_notifications_data = [ + { + 'id': 1, + 'message': f'Welcome {current_user.full_name}!', + 'timestamp': 'Just now', + 'link': '#', + 'icon': 'fa-info-circle', + 'icon_color': 'blue' + }, + { + 'id': 2, + 'message': 'New referral assigned.', + 'timestamp': '5 minutes ago', + 'link': '#', # Thay bằng url_for('referrals.detail', referral_id=...) + 'icon': 'fa-user-plus', + 'icon_color': 'green' + }, + { + 'id': 3, + 'message': 'Patient P-10001 has high HR.', + 'timestamp': '1 hour ago', + 'link': '#', # Thay bằng url_for('patients.patient_detail', patient_id='P-10001') + 'icon': 'fa-heartbeat', + 'icon_color': 'red' + } + ] + count = len(unread_notifications_data) + + # Format data for JSON response + # formatted_notifications = [ + # { + # 'id': n.id, + # 'message': n.message, + # 'timestamp': n.timestamp.isoformat(), # Cần xử lý hiển thị thời gian tương đối ở frontend + # 'link': n.link or '#' # Cần thêm logic để tạo link phù hợp + # } for n in unread_notifications + # ] + + return jsonify({ + 'count': count, + 'notifications': unread_notifications_data # Sử dụng dữ liệu giả + # 'notifications': formatted_notifications # Sử dụng khi có dữ liệu thật + }) + +@notifications_bp.route('/api/<int:notification_id>/mark-read', methods=['POST']) +@login_required +def mark_notification_as_read(notification_id): + """API endpoint to mark a specific notification as read.""" + notification = Notification.query.filter_by(id=notification_id, user_id=current_user.userID).first() + + if notification: + if not notification.is_read: + notification.is_read = True + db.session.commit() + return jsonify({'success': True, 'message': 'Notification marked as read.'}) + else: + return jsonify({'success': True, 'message': 'Notification already read.'}) + else: + return jsonify({'success': False, 'message': 'Notification not found or access denied.'}), 404 + +@notifications_bp.route('/api/mark-all-read', methods=['POST']) +@login_required +def mark_all_notifications_as_read(): + """API endpoint to mark all unread notifications as read for the current user.""" + try: + updated_count = Notification.query.filter_by(user_id=current_user.userID, is_read=False).update({'is_read': True}) + db.session.commit() + return jsonify({'success': True, 'message': f'{updated_count} notifications marked as read.'}) + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'message': f'Error marking notifications as read: {str(e)}'}), 500 + +# Có thể thêm route để xem tất cả thông báo (trang riêng) nếu cần +# @notifications_bp.route('/') +# @login_required +# def index(): +# # Lấy tất cả thông báo, phân trang,... +# pass \ No newline at end of file diff --git a/app/routes/patients.py b/app/routes/patients.py index ffedbe5d48b10fd9f53fd1dacad51ff7854edccb..c9d17ae1757de36027815927f93c90775827e568 100644 --- a/app/routes/patients.py +++ b/app/routes/patients.py @@ -2,7 +2,8 @@ from flask import Blueprint, render_template, request, redirect, url_for, flash, from flask_login import login_required, current_user from flask_wtf import FlaskForm from app import db -from app.models.patient import Patient, Encounter +from app.models.patient import Patient +from app.models.encounter import Encounter from app.models.measurement import PhysiologicalMeasurement from app.models.referral import Referral from app.models.procedure import Procedure @@ -11,6 +12,11 @@ from sqlalchemy import desc, or_, func from sqlalchemy.orm import joinedload from datetime import datetime from app import csrf +from werkzeug.utils import secure_filename +import os +import uuid +# Thêm import cho hàm xử lý CSV mới +from app.utils.csv_handler import process_encounter_measurements_csv patients_bp = Blueprint('patients', __name__, url_prefix='/patients') @@ -89,52 +95,75 @@ def index(): @patients_bp.route('/<string:patient_id>') @login_required def patient_detail(patient_id): - """ - Route handler for viewing patient details - """ - patient = Patient.query.filter_by(id=patient_id).first_or_404() + patient = Patient.query.get_or_404(patient_id) - # Lấy thông tin đo lường sinh lý cuối cùng + # Lấy thông tin đo lường sinh lý cuối cùng (của tất cả encounter) latest_measurement = PhysiologicalMeasurement.query.filter_by(patient_id=patient_id).order_by( PhysiologicalMeasurement.measurementDateTime.desc() ).first() - # Lấy lịch sử các phép đo sinh lý học - measurements = PhysiologicalMeasurement.query.filter_by(patient_id=patient_id).order_by( - PhysiologicalMeasurement.measurementDateTime.desc() - ).all() - - # Get patient's referrals + # Lấy referrals, procedures, reports (như cũ) referrals = Referral.query.filter_by(patient_id=patient_id).order_by( desc(Referral.referralRequestedDateTime) ).all() - - # Get patient's procedures procedures = Procedure.query.filter_by(patient_id=patient_id).order_by( desc(Procedure.procedureDateTime) ).all() - - # Get patient's reports reports = Report.query.filter_by(patient_id=patient_id).order_by( desc(Report.report_date) ).all() - # Get patient's encounters with dietitian info preloaded - encounters = Encounter.query.filter_by(patientID=patient_id).options( - joinedload(Encounter.dietitian) + # Lấy tất cả encounters của bệnh nhân, sắp xếp mới nhất trước + all_encounters = Encounter.query.filter_by(patient_id=patient_id).options( + joinedload(Encounter.assigned_dietitian) # Tải sẵn thông tin dietitian ).order_by( - desc(Encounter.admissionDateTime) + desc(Encounter.start_time) # Hoặc admissionDateTime nếu dùng ).all() + latest_encounter = all_encounters[0] if all_encounters else None + + # Chuẩn bị dữ liệu encounters để hiển thị, bao gồm status và unstable metrics + display_encounters = [] + for enc in all_encounters: + # Xác định trạng thái hiển thị + if enc.status == 'Discharged': + status = {'text': 'Discharged', 'color': 'gray'} + elif enc.assigned_dietitian: + status = {'text': 'In Treatment', 'color': 'blue'} + else: + status = {'text': 'Active', 'color': 'green'} + + # TODO: Xác định các chỉ số bất ổn + # Logic này cần định nghĩa ngưỡng cho từng chỉ số + # Ví dụ đơn giản (tạm thời trả về list rỗng): + unstable_metrics_list = [] + # Ví dụ logic thực tế (cần hoàn thiện): + # latest_enc_measurement = PhysiologicalMeasurement.query.filter_by(encounter_id=enc.id).order_by(desc(PhysiologicalMeasurement.measurementDateTime)).first() + # if latest_enc_measurement: + # if latest_enc_measurement.heart_rate and latest_enc_measurement.heart_rate > 100: + # unstable_metrics_list.append("High HR") + # if latest_enc_measurement.oxygen_saturation and latest_enc_measurement.oxygen_saturation < 92: + # unstable_metrics_list.append("Low SpO2") + # # ... thêm các kiểm tra khác ... + # unstable_metrics_list = unstable_metrics_list[:3] # Giới hạn 3 + + display_encounters.append({ + 'encounter': enc, + 'status': status, + 'unstable_metrics': unstable_metrics_list + }) + return render_template( 'patient_detail.html', patient=patient, latest_measurement=latest_measurement, - measurements=measurements, + # measurements=measurements, # Không cần truyền list measurements cũ nữa referrals=referrals, procedures=procedures, reports=reports, - encounters=encounters + encounters_data=display_encounters, # Truyền dữ liệu đã xử lý + latest_encounter=latest_encounter, # Truyền encounter mới nhất + EmptyForm=EmptyForm # Thêm dòng này để truyền EmptyForm ) @patients_bp.route('/new', methods=['GET', 'POST']) @@ -275,14 +304,14 @@ def ajax_measurement(patient_id): # Lấy thông tin encounter hiện tại hoặc tạo mới encounter = Encounter.query.filter_by(patient_id=patient.id).order_by( - desc(Encounter.admissionDateTime) + desc(Encounter.start_time) ).first() if not encounter: # Tạo encounter mới nếu không có encounter = Encounter( patient_id=patient.id, - admissionDateTime=datetime.now() + start_time=datetime.now() ) db.session.add(encounter) db.session.commit() @@ -440,9 +469,9 @@ def new_physical_measurement(patient_id): if form.validate_on_submit(): encounter = Encounter.query.filter_by( - patientID=patient.id, + patient_id=patient.id, dischargeDateTime=None - ).order_by(desc(Encounter.admissionDateTime)).first() + ).order_by(desc(Encounter.start_time)).first() if not encounter: flash('Không tìm thấy encounter đang hoạt động cho bệnh nhân. Vui lòng kiểm tra lại.', 'error') @@ -591,91 +620,153 @@ def delete_physical_measurement(patient_id, measurement_id): return redirect(url_for('patients.physical_measurements', patient_id=patient.id)) -@patients_bp.route('/<string:patient_id>/measurements/quick_save', methods=['POST']) +@patients_bp.route('/<string:patient_id>/encounter/<int:encounter_id>/measurements') +@login_required +def encounter_measurements(patient_id, encounter_id): + """Hiển thị chi tiết các phép đo cho một encounter cụ thể.""" + patient = Patient.query.get_or_404(patient_id) + encounter = Encounter.query.filter_by(encounterID=encounter_id, patient_id=patient_id).first_or_404() + + # Lấy tất cả measurements cho encounter này, sắp xếp theo thời gian + measurements_query = PhysiologicalMeasurement.query.filter_by( + encounter_id=encounter.encounterID + ).order_by( + PhysiologicalMeasurement.measurementDateTime.asc() + ).all() + + # Chuẩn bị dữ liệu measurements cho template (tương tự physical_measurements cũ) + measurements_list = [] + latest_measurement = {} + latest_bp = None # Khởi tạo latest_bp + + for m in measurements_query: + measurement_data = { + 'measurementDateTime': m.measurementDateTime.isoformat() if m.measurementDateTime else None, + 'temperature': m.temperature, + 'heart_rate': m.heart_rate, + 'blood_pressure_systolic': m.blood_pressure_systolic, + 'blood_pressure_diastolic': m.blood_pressure_diastolic, + 'oxygen_saturation': m.oxygen_saturation, + 'resp_rate': m.resp_rate, # Sử dụng resp_rate từ model measurement + 'fio2': m.fio2, + 'fio2_ratio': m.fio2_ratio, + 'tidal_vol': m.tidal_vol, + 'tidal_vol_kg': m.tidal_vol_kg, + 'tidal_vol_actual': m.tidal_vol_actual, + 'tidal_vol_spon': m.tidal_vol_spon, + 'end_tidal_co2': m.end_tidal_co2, + 'feed_vol': m.feed_vol, + 'feed_vol_adm': m.feed_vol_adm, + 'peep': m.peep, + 'pip': m.pip, + 'sip': m.sip, + 'insp_time': m.insp_time, + 'oxygen_flow_rate': m.oxygen_flow_rate, + 'bmi': m.bmi, + 'notes': m.notes, + 'id': m.id # Thêm ID để có thể edit/delete + } + measurements_list.append(measurement_data) + + # Cập nhật latest values + for key, value in measurement_data.items(): + if key not in ['measurementDateTime', 'id', 'notes'] and value is not None: + latest_measurement[key] = value + + # Lưu giá trị huyết áp gần nhất + if m.blood_pressure_systolic is not None and m.blood_pressure_diastolic is not None: + latest_bp = { + 'systolicBP': m.blood_pressure_systolic, + 'diastolicBP': m.blood_pressure_diastolic, + 'time': m.measurementDateTime + } + + # TODO: Xác định chỉ số bất ổn (logic phức tạp, tạm bỏ qua) + unstable_metrics = [] + + # TODO: Xác định status encounter (logic phức tạp, dùng status hiện có) + encounter_status = encounter.status # Tạm dùng status có sẵn + + return render_template( + 'encounter_measurements.html', # Template mới sẽ tạo ở Bước 3 + patient=patient, + encounter=encounter, + measurements=measurements_list, + latest_measurement=latest_measurement, + latest_bp=latest_bp, + unstable_metrics=unstable_metrics, + encounter_status=encounter_status, + EmptyForm=EmptyForm # Truyền class EmptyForm vào context + ) + +@patients_bp.route('/<string:patient_id>/encounter/<int:encounter_id>/measurements/quick_save', methods=['POST']) @login_required -@csrf.exempt -def quick_save_measurement(patient_id): - """API endpoint để lưu nhanh một giá trị đo lường""" +@csrf.exempt # Giữ lại nếu bạn xử lý CSRF ở client-side +def quick_save_encounter_measurement(patient_id, encounter_id): + """API endpoint để lưu nhanh một giá trị đo lường cho encounter cụ thể""" try: + # Xác thực patient và encounter + patient = Patient.query.get_or_404(patient_id) + encounter = Encounter.query.filter_by(encounterID=encounter_id, patient_id=patient_id).first_or_404() + data = request.get_json() if not data or 'values' not in data: - current_app.logger.error(f"Quick save - Dữ liệu không hợp lệ hoặc thiếu key 'values': {request.data}") + # ... (xử lý lỗi như cũ) ... return jsonify({'success': False, 'message': 'Dữ liệu không hợp lệ'}), 400 values_dict = data['values'] - current_app.logger.info(f"Quick save - Nhận dữ liệu cho patient {patient_id}: {values_dict}") + current_app.logger.info(f"Quick save - Nhận dữ liệu cho patient {patient_id}, encounter {encounter_id}: {values_dict}") - patient = Patient.query.get_or_404(patient_id) - - # Tìm encounter đang hoạt động của bệnh nhân - encounter = Encounter.query.filter_by( - patientID=patient.id, - dischargeDateTime=None - ).order_by(desc(Encounter.admissionDateTime)).first() - - if not encounter: - current_app.logger.warning(f"Quick save - Không tìm thấy encounter hoạt động cho patient {patient_id}") - # Tạo encounter mới thay vì báo lỗi - encounter = Encounter( - patientID=patient.id, - admissionDateTime=datetime.now() - ) - db.session.add(encounter) - db.session.commit() - current_app.logger.info(f"Quick save - Đã tạo encounter mới cho patient {patient_id}: {encounter.id}") - # Tạo một đo lường mới với thời gian hiện tại measurement_time = datetime.now() new_measurement = PhysiologicalMeasurement( patient_id=patient.id, - encounter_id=encounter.id, + encounter_id=encounter.encounterID, # Sử dụng encounterID được cung cấp measurementDateTime=measurement_time ) - # Gán giá trị vào các trường tương ứng + # ... (Gán giá trị vào các trường tương ứng như cũ) ... field_map = { 'temperature': ('temperature', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'heart_rate': ('heart_rate', lambda key: int(values_dict.get(key)) if values_dict.get(key) else None), - 'blood_pressure_systolic': ('blood_pressure_systolic', lambda key: int(values_dict.get(key)) if values_dict.get(key) else None), - 'blood_pressure_diastolic': ('blood_pressure_diastolic', lambda key: int(values_dict.get(key)) if values_dict.get(key) else None), - 'oxygen_saturation': ('oxygen_saturation', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'resp_rate': ('resp_rate', lambda key: int(values_dict.get(key)) if values_dict.get(key) else None), - 'fio2': ('fio2', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'fio2_ratio': ('fio2_ratio', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'peep': ('peep', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'pip': ('pip', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'sip': ('sip', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'insp_time': ('insp_time', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'oxygen_flow_rate': ('oxygen_flow_rate', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'end_tidal_co2': ('end_tidal_co2', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'tidal_vol': ('tidal_vol', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'tidal_vol_kg': ('tidal_vol_kg', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'tidal_vol_actual': ('tidal_vol_actual', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'tidal_vol_spon': ('tidal_vol_spon', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'feed_vol': ('feed_vol', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), - 'feed_vol_adm': ('feed_vol_adm', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None) + # ... (thêm các field map khác tương tự như trong hàm quick_save cũ) ... + 'heart_rate': ('heart_rate', lambda key: int(values_dict.get(key)) if values_dict.get(key) else None), + 'blood_pressure_systolic': ('blood_pressure_systolic', lambda key: int(values_dict.get(key)) if values_dict.get(key) else None), + 'blood_pressure_diastolic': ('blood_pressure_diastolic', lambda key: int(values_dict.get(key)) if values_dict.get(key) else None), + 'oxygen_saturation': ('oxygen_saturation', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'resp_rate': ('resp_rate', lambda key: int(values_dict.get(key)) if values_dict.get(key) else None), # Đổi tên field model nếu cần + 'fio2': ('fio2', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'fio2_ratio': ('fio2_ratio', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'peep': ('peep', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'pip': ('pip', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'sip': ('sip', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'insp_time': ('insp_time', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'oxygen_flow_rate': ('oxygen_flow_rate', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'end_tidal_co2': ('end_tidal_co2', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'tidal_vol': ('tidal_vol', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'tidal_vol_kg': ('tidal_vol_kg', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'tidal_vol_actual': ('tidal_vol_actual', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'tidal_vol_spon': ('tidal_vol_spon', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'feed_vol': ('feed_vol', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None), + 'feed_vol_adm': ('feed_vol_adm', lambda key: float(values_dict.get(key)) if values_dict.get(key) else None) } - - # Cập nhật các giá trị từ dữ liệu gửi lên for key in values_dict: if key in field_map: field_name, conversion_func = field_map[key] setattr(new_measurement, field_name, conversion_func(key)) - + db.session.add(new_measurement) db.session.commit() - # Chuẩn bị dữ liệu trả về + # ... (Chuẩn bị và trả về dữ liệu như cũ) ... returned_data = {} for key in values_dict.keys(): if key in field_map: field_name = field_map[key][0] value = getattr(new_measurement, field_name) returned_data[key] = value - returned_data['measurementDateTime'] = measurement_time.isoformat() - current_app.logger.info(f"Quick save - Đã lưu thành công cho patient {patient_id}: {returned_data}") + current_app.logger.info(f"Quick save - Đã lưu thành công cho patient {patient_id}, encounter {encounter_id}: {returned_data}") return jsonify({ 'success': True, 'message': 'Đã lưu thành công', @@ -683,112 +774,52 @@ def quick_save_measurement(patient_id): }) except Exception as e: - # Ghi log lỗi chi tiết hơn - current_app.logger.error(f"Lỗi khi lưu đo lường nhanh cho patient {patient_id}. Data: {request.data}. Lỗi: {str(e)}", exc_info=True) + # ... (Xử lý lỗi như cũ) ... + current_app.logger.error(f"Lỗi khi lưu đo lường nhanh cho patient {patient_id}, encounter {encounter_id}. Data: {request.data}. Lỗi: {str(e)}", exc_info=True) db.session.rollback() return jsonify({'success': False, 'message': f'Lỗi máy chủ: {str(e)}'}), 500 -@patients_bp.route('/<string:patient_id>/measurements/data', methods=['GET']) +@patients_bp.route('/<string:patient_id>/encounter/<int:encounter_id>/measurements/data', methods=['GET']) @login_required -@csrf.exempt -def get_measurement_data(patient_id): - """API endpoint để lấy dữ liệu đo lường cho biểu đồ""" +# @csrf.exempt # Xem xét có cần exempt không nếu GET request +def get_encounter_measurement_data(patient_id, encounter_id): + """API endpoint để lấy dữ liệu đo lường cho biểu đồ của encounter cụ thể""" try: + # Xác thực patient và encounter patient = Patient.query.get_or_404(patient_id) + encounter = Encounter.query.filter_by(encounterID=encounter_id, patient_id=patient_id).first_or_404() - # Lấy tất cả đo lường của bệnh nhân, sắp xếp theo thời gian + # Lấy tất cả đo lường của encounter này, sắp xếp theo thời gian measurements = PhysiologicalMeasurement.query.filter_by( - patient_id=patient_id + encounter_id=encounter.encounterID ).order_by( PhysiologicalMeasurement.measurementDateTime ).all() - # Chuẩn bị dữ liệu cho từng loại biểu đồ - heart_rate_data = [] - temperature_data = [] - blood_pressure_data = [] - oxygen_saturation_data = [] - respiratory_rate_data = [] - fio2_data = [] - fio2_ratio_data = [] - peep_data = [] - pip_data = [] - sip_data = [] - insp_time_data = [] - oxygen_flow_rate_data = [] - end_tidal_co2_data = [] - tidal_vol_data = [] - tidal_vol_kg_data = [] - tidal_vol_actual_data = [] - tidal_vol_spon_data = [] - feed_vol_data = [] - feed_vol_adm_data = [] - - # Lọc và định dạng dữ liệu cho từng biểu đồ - for m in measurements: - time = m.measurementDateTime.isoformat() if m.measurementDateTime else None - - if m.heart_rate is not None: - heart_rate_data.append({'x': time, 'y': m.heart_rate}) - - if m.temperature is not None: - temperature_data.append({'x': time, 'y': m.temperature}) - - if m.blood_pressure_systolic is not None and m.blood_pressure_diastolic is not None: - blood_pressure_data.append({ - 'x': time, - 'systolic': m.blood_pressure_systolic, - 'diastolic': m.blood_pressure_diastolic - }) - - if m.oxygen_saturation is not None: - oxygen_saturation_data.append({'x': time, 'y': m.oxygen_saturation}) - - if m.resp_rate is not None: - respiratory_rate_data.append({'x': time, 'y': m.resp_rate}) - - if m.fio2 is not None: - fio2_data.append({'x': time, 'y': m.fio2}) - - if m.fio2_ratio is not None: - fio2_ratio_data.append({'x': time, 'y': m.fio2_ratio}) - - if m.peep is not None: - peep_data.append({'x': time, 'y': m.peep}) - - if m.pip is not None: - pip_data.append({'x': time, 'y': m.pip}) - - if m.sip is not None: - sip_data.append({'x': time, 'y': m.sip}) - - if m.insp_time is not None: - insp_time_data.append({'x': time, 'y': m.insp_time}) - - if m.oxygen_flow_rate is not None: - oxygen_flow_rate_data.append({'x': time, 'y': m.oxygen_flow_rate}) - - if m.end_tidal_co2 is not None: - end_tidal_co2_data.append({'x': time, 'y': m.end_tidal_co2}) - - if m.tidal_vol is not None: - tidal_vol_data.append({'x': time, 'y': m.tidal_vol}) - - if m.tidal_vol_kg is not None: - tidal_vol_kg_data.append({'x': time, 'y': m.tidal_vol_kg}) - - if m.tidal_vol_actual is not None: - tidal_vol_actual_data.append({'x': time, 'y': m.tidal_vol_actual}) - - if m.tidal_vol_spon is not None: - tidal_vol_spon_data.append({'x': time, 'y': m.tidal_vol_spon}) - - if m.feed_vol is not None: - feed_vol_data.append({'x': time, 'y': m.feed_vol}) - - if m.feed_vol_adm is not None: - feed_vol_adm_data.append({'x': time, 'y': m.feed_vol_adm}) - + # ... (Chuẩn bị dữ liệu cho từng loại biểu đồ như trong hàm get_measurement_data cũ) ... + # Ví dụ: + heart_rate_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.heart_rate} for m in measurements if m.heart_rate is not None] + temperature_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.temperature} for m in measurements if m.temperature is not None] + blood_pressure_data = [{'x': m.measurementDateTime.isoformat(), 'systolic': m.blood_pressure_systolic, 'diastolic': m.blood_pressure_diastolic} for m in measurements if m.blood_pressure_systolic is not None and m.blood_pressure_diastolic is not None] + # ... (thêm các loại dữ liệu khác) ... + oxygen_saturation_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.oxygen_saturation} for m in measurements if m.oxygen_saturation is not None] + respiratory_rate_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.resp_rate} for m in measurements if m.resp_rate is not None] # Sử dụng resp_rate + fio2_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.fio2} for m in measurements if m.fio2 is not None] + fio2_ratio_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.fio2_ratio} for m in measurements if m.fio2_ratio is not None] + peep_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.peep} for m in measurements if m.peep is not None] + pip_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.pip} for m in measurements if m.pip is not None] + sip_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.sip} for m in measurements if m.sip is not None] + insp_time_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.insp_time} for m in measurements if m.insp_time is not None] + oxygen_flow_rate_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.oxygen_flow_rate} for m in measurements if m.oxygen_flow_rate is not None] + end_tidal_co2_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.end_tidal_co2} for m in measurements if m.end_tidal_co2 is not None] + tidal_vol_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.tidal_vol} for m in measurements if m.tidal_vol is not None] + tidal_vol_kg_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.tidal_vol_kg} for m in measurements if m.tidal_vol_kg is not None] + tidal_vol_actual_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.tidal_vol_actual} for m in measurements if m.tidal_vol_actual is not None] + tidal_vol_spon_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.tidal_vol_spon} for m in measurements if m.tidal_vol_spon is not None] + feed_vol_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.feed_vol} for m in measurements if m.feed_vol is not None] + feed_vol_adm_data = [{'x': m.measurementDateTime.isoformat(), 'y': m.feed_vol_adm} for m in measurements if m.feed_vol_adm is not None] + + # Trả về dữ liệu cho tất cả các biểu đồ return jsonify({ 'heartRate': heart_rate_data, @@ -812,6 +843,124 @@ def get_measurement_data(patient_id): 'feedVolAdm': feed_vol_adm_data }) except Exception as e: - # Ghi log lỗi - current_app.logger.error(f"Lỗi khi lấy dữ liệu đo lường: {str(e)}") + # ... (Xử lý lỗi như cũ) ... + current_app.logger.error(f"Lỗi khi lấy dữ liệu đo lường cho encounter {encounter_id}: {str(e)}") return jsonify({'success': False, 'message': f'Lỗi máy chủ: {str(e)}'}), 500 + +@patients_bp.route('/<string:patient_id>/encounter/<int:encounter_id>/upload_measurements', methods=['POST']) +@login_required +@csrf.exempt # Tạm thời bỏ qua CSRF check cho route upload file này +def upload_encounter_measurements_csv(patient_id, encounter_id): + patient = Patient.query.filter_by(id=patient_id).first_or_404() + encounter = Encounter.query.filter_by(encounterID=encounter_id, patient_id=patient.id).first_or_404() + + if 'csv_file' not in request.files: + flash('Không tìm thấy file CSV trong yêu cầu.', 'danger') + return redirect(url_for('patients.encounter_measurements', patient_id=patient_id, encounter_id=encounter_id)) + + file = request.files['csv_file'] + + if file.filename == '': + flash('Không có file nào được chọn.', 'warning') + return redirect(url_for('patients.encounter_measurements', patient_id=patient_id, encounter_id=encounter_id)) + + if file and file.filename.endswith('.csv'): + try: + # Gọi hàm xử lý CSV từ utils - Sửa lại encounter.id thành encounter.encounterID + # Hàm giờ trả về (success, (message, category)) hoặc (False, error_message) + success, result_data = process_encounter_measurements_csv(file, patient.id, encounter.encounterID) + + if success: + message, category = result_data # Unpack message và category + flash(message, category) # Flash với category tương ứng + else: + # result_data lúc này là error_message + flash(result_data, 'danger') + except Exception as e: + current_app.logger.error(f"Lỗi khi xử lý file CSV cho encounter {encounter_id}: {e}", exc_info=True) # Thêm exc_info=True để log traceback + flash(f'Đã xảy ra lỗi không mong muốn khi xử lý file: {str(e)}', 'danger') + # db.session.rollback() # Rollback nên được xử lý trong hàm process_csv + else: + flash('Định dạng file không hợp lệ. Vui lòng tải lên file .csv.', 'warning') + + # Chuyển hướng về trang chi tiết encounter sau khi xử lý + return redirect(url_for('patients.encounter_measurements', patient_id=patient_id, encounter_id=encounter_id)) + +@patients_bp.route('/<string:patient_id>/encounters/new', methods=['POST']) +@login_required +def new_encounter(patient_id): + """Creates a new encounter for the patient.""" + patient = Patient.query.get_or_404(patient_id) + # form = EmptyForm() # Không cần form ở đây nữa + + try: + # 1. Đếm số encounter hiện có của bệnh nhân này để xác định số thứ tự tiếp theo + existing_encounter_count = Encounter.query.filter_by(patient_id=patient.id).count() + next_sequence_num = existing_encounter_count + 1 + + # 2. Tạo custom_encounter_id + # Lấy 5 số cuối từ patient.id (giả sử format là P-xxxxx) + patient_id_part = patient.id[-5:] if patient.id and patient.id.startswith('P-') and len(patient.id) >= 7 else patient.id + custom_id = f"E-{patient_id_part}-{next_sequence_num:02d}" # Format với 2 chữ số + + # Kiểm tra nếu ID này đã tồn tại (trường hợp hiếm) + while Encounter.query.filter_by(custom_encounter_id=custom_id).first(): + current_app.logger.warning(f"Generated custom encounter ID {custom_id} already exists. Incrementing sequence.") + next_sequence_num += 1 + custom_id = f"E-{patient_id_part}-{next_sequence_num:02d}" + + # 3. Create a new encounter instance + encounter = Encounter( + patient_id=patient.id, + start_time=datetime.utcnow(), + status='Active', # Initial status + custom_encounter_id=custom_id # Gán ID tùy chỉnh + ) + db.session.add(encounter) + db.session.commit() + # flash(f'New encounter (ID: {encounter.encounterID}) created successfully for {patient.full_name}.', 'success') # Xóa dòng flash này + # Redirect to the new encounter's measurement page + return redirect(url_for('patients.encounter_measurements', patient_id=patient.id, encounter_id=encounter.encounterID)) + # Option 2: Redirect back to patient detail, encounters tab + # return redirect(url_for('patients.patient_detail', patient_id=patient.id, _anchor='encounters')) + + except Exception as e: + db.session.rollback() + current_app.logger.error(f"Error creating new encounter for patient {patient.id}: {str(e)}", exc_info=True) + flash(f'Error creating new encounter: {str(e)}', 'error') + return redirect(url_for('patients.patient_detail', patient_id=patient.id, _anchor='encounters')) + +# Thêm route xóa encounter +@patients_bp.route('/<string:patient_id>/encounters/<int:encounter_pk>/delete', methods=['POST']) +@login_required +def delete_encounter(patient_id, encounter_pk): + """Deletes an encounter and its associated measurements.""" + # Dùng encounter_pk (khóa chính integer) để tìm encounter + encounter = Encounter.query.get_or_404(encounter_pk) + patient = Patient.query.get_or_404(patient_id) + + # Kiểm tra xem encounter có thuộc về bệnh nhân không + if encounter.patient_id != patient.id: + flash('Invalid encounter for this patient.', 'error') + return redirect(url_for('patients.patient_detail', patient_id=patient.id, _anchor='encounters')) + + # Xác thực CSRF token + form = EmptyForm() # Sử dụng form rỗng để validate CSRF + if not form.validate_on_submit(): # validate_on_submit sẽ tự kiểm tra CSRF + flash('Invalid CSRF token. Please try again.', 'error') + return redirect(url_for('patients.patient_detail', patient_id=patient.id, _anchor='encounters')) + + try: + # Lấy custom ID để hiển thị thông báo (nếu có) + custom_id_for_flash = encounter.custom_encounter_id if encounter.custom_encounter_id else f"(PK: {encounter.encounterID})" + + # Xóa encounter (và measurements do cascade) + db.session.delete(encounter) + db.session.commit() + flash(f'Encounter {custom_id_for_flash} deleted successfully.', 'success') + except Exception as e: + db.session.rollback() + current_app.logger.error(f"Error deleting encounter {encounter_pk} for patient {patient.id}: {str(e)}", exc_info=True) + flash(f'Error deleting encounter: {str(e)}', 'error') + + return redirect(url_for('patients.patient_detail', patient_id=patient.id, _anchor='encounters')) diff --git a/app/routes/upload.py b/app/routes/upload.py index 517e0cb3cec535849ebf7d1015c2ae54c8183594..cef5008a43e199d83ec9ff61c1ad823a64c7af30 100644 --- a/app/routes/upload.py +++ b/app/routes/upload.py @@ -10,10 +10,11 @@ from werkzeug.utils import secure_filename from werkzeug.datastructures import FileStorage from app import db from app.models.uploaded_file import UploadedFile -from app.models.patient import Patient, Encounter +from app.models.patient import Patient +from app.models.encounter import Encounter from app.models.measurement import PhysiologicalMeasurement from app.models.referral import Referral -from app.utils.csv_handler import process_csv +from app.utils.csv_handler import process_new_patients_csv, _process_combined_patient_measurement_csv, process_encounter_measurements_csv from sqlalchemy import desc from flask_wtf import FlaskForm from flask_wtf.csrf import generate_csrf, validate_csrf @@ -101,7 +102,7 @@ def index(): db.session.commit() # Xử lý file CSV - result = process_csv( + result = process_new_patients_csv( uploaded_file_id=upload_record.id ) diff --git a/app/templates/base.html b/app/templates/base.html index 0d607c9d774f7abd91092dbe0f92c44f1152a7de..952dddc858c87a293517b9bcddaef55888f4de38 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -96,9 +96,10 @@ position: fixed; height: 100vh; width: var(--sidebar-width); - transition: all 0.3s ease; + transition: width 0.3s ease; z-index: 40; box-shadow: 4px 0 15px rgba(0, 0, 0, 0.1); + overflow-x: hidden; } .sidebar.collapsed { @@ -107,7 +108,7 @@ .content-wrapper { margin-left: var(--sidebar-width); - transition: all 0.3s ease; + transition: margin-left 0.3s ease; min-height: 100vh; } @@ -119,8 +120,8 @@ position: absolute; top: 20px; right: -15px; - background: #2563eb; - color: white; + background: #f3f4f6; + color: #3b82f6; width: 30px; height: 30px; border-radius: 50%; @@ -134,16 +135,25 @@ /* Navigation items */ .nav-item { - transition: all 0.2s ease; border-radius: 8px; margin: 4px 0; overflow: hidden; white-space: nowrap; + display: flex; + align-items: center; + padding: 0.8rem 1rem; + height: 50px; } + .nav-item i { + margin-right: 0.75rem; + font-size: 1.2rem; + width: 24px; + text-align: center; + } + .nav-item:hover { background-color: var(--primary-dark); - transform: translateX(5px); } .nav-item.active { @@ -229,16 +239,25 @@ transition: all 0.5s ease; } - /* Sidebar icon style when collapsed */ + /* Style khi sidebar thu gọn */ .sidebar.collapsed .nav-item { - display: flex; justify-content: center; - padding: 1rem 0.5rem; + padding: 0.8rem 0; } .sidebar.collapsed .nav-item i { margin-right: 0; - font-size: 1.5rem; + font-size: 1.6rem; + } + + /* Ẩn text khi sidebar thu gọn */ + .sidebar.collapsed .nav-text { + display: none; + } + + /* Helper class để tạm thời vô hiệu hóa transition */ + .no-transition { + transition: none !important; } </style> {% block head_extra %}{% endblock %} @@ -249,9 +268,10 @@ <!-- Sidebar - Fixed, with collapse functionality --> <div id="sidebar" class="sidebar bg-blue-600 text-white py-4 px-6 flex-shrink-0"> <div class="flex items-center justify-between mb-10"> - <h2 class="text-xl font-bold animate-pulse-slow">CCU HTM</h2> + <span class="text-xl font-bold animate-pulse-slow sidebar-brand-full">CCU HTM</span> + <span class="text-xl font-bold sidebar-brand-mini" style="display: none;">C H</span> <div class="sidebar-toggle" id="sidebarToggle"> - <i class="fas fa-chevron-left text-blue-600"></i> + <i class="fas fa-chevron-left"></i> </div> </div> @@ -382,10 +402,10 @@ </a> <a href="{{ url_for('auth.logout') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" role="menuitem"> <i class="fas fa-sign-out-alt mr-2"></i> Đăng xuất - </a> - </div> - </div> - </div> + </a> + </div> + </div> + </div> </div> </div> </div> @@ -397,29 +417,29 @@ <!-- Page Header --> <div class="page-header mb-8"> <h1 class="text-3xl font-bold text-gray-800 animate-fade-in">{% block header %}Dashboard{% endblock %}</h1> - </div> + </div> <!-- Flash Messages --> - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - <div class="mb-6 animate-slide-in-top"> - {% for category, message in messages %} - <div class="alert-message py-3 px-4 mb-2 rounded-md flex items-center justify-between {% if category == 'error' %}bg-red-100 text-red-700{% elif category == 'warning' %}bg-yellow-100 text-yellow-700{% elif category == 'info' %}bg-blue-100 text-blue-700{% else %}bg-green-100 text-green-700{% endif %}"> - <div class="flex items-center"> - {% if category == 'error' %} - <i class="fas fa-exclamation-circle mr-2"></i> - {% elif category == 'warning' %} - <i class="fas fa-exclamation-triangle mr-2"></i> - {% elif category == 'info' %} - <i class="fas fa-info-circle mr-2"></i> - {% else %} - <i class="fas fa-check-circle mr-2"></i> - {% endif %} - {{ message }} - </div> - <button class="text-gray-500 hover:text-gray-700 focus:outline-none" onclick="this.parentElement.style.display='none'"> - <i class="fas fa-times"></i> - </button> + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + <div class="mb-6 animate-slide-in-top"> + {% for category, message in messages %} + <div class="alert-message py-3 px-4 mb-2 rounded-md flex items-center justify-between {% if category == 'error' %}bg-red-100 text-red-700{% elif category == 'warning' %}bg-yellow-100 text-yellow-700{% elif category == 'info' %}bg-blue-100 text-blue-700{% else %}bg-green-100 text-green-700{% endif %}"> + <div class="flex items-center"> + {% if category == 'error' %} + <i class="fas fa-exclamation-circle mr-2"></i> + {% elif category == 'warning' %} + <i class="fas fa-exclamation-triangle mr-2"></i> + {% elif category == 'info' %} + <i class="fas fa-info-circle mr-2"></i> + {% else %} + <i class="fas fa-check-circle mr-2"></i> + {% endif %} + {{ message }} + </div> + <button class="text-gray-500 hover:text-gray-700 focus:outline-none" onclick="this.parentElement.style.display='none'"> + <i class="fas fa-times"></i> + </button> </div> {% endfor %} </div> @@ -442,106 +462,230 @@ </div> </footer> + <!-- Core plugin JavaScript--> + <script src="{{ url_for('static', filename='vendor/jquery-easing/jquery.easing.min.js') }}"></script> + + <!-- Custom scripts for all pages--> + <script src="{{ url_for('static', filename='js/sb-admin-2.min.js') }}"></script> + + <!-- Page level plugins --> + <script src="{{ url_for('static', filename='vendor/chart.js/Chart.min.js') }}"></script> + + <!-- Page level custom scripts --> + <!-- <script src="{{ url_for('static', filename='js/demo/chart-area-demo.js') }}"></script> + <script src="{{ url_for('static', filename='js/demo/chart-pie-demo.js') }}"></script> --> + + <!-- Sidebar State Persistence Script --> <script> - // Common JS for all pages - document.addEventListener('DOMContentLoaded', function() { - // Flash message auto-hide - var flashMessages = document.querySelectorAll('.alert-message'); - flashMessages.forEach(function(message) { - setTimeout(function() { - message.style.opacity = '0'; - message.style.transform = 'translateY(-10px)'; - setTimeout(function() { - message.style.display = 'none'; - }, 500); - }, 5000); - }); - - // Sidebar toggle functionality + // --- START: Apply Sidebar State Immediately --- + (function() { const sidebar = document.getElementById('sidebar'); const contentWrapper = document.getElementById('content-wrapper'); + const toggleIcon = document.querySelector('#sidebarToggle i'); + const sidebarBrandFull = document.querySelector('.sidebar-brand-full'); + const sidebarBrandMini = document.querySelector('.sidebar-brand-mini'); + const sidebarState = localStorage.getItem('sidebarState'); + + if (!sidebar || !contentWrapper || !toggleIcon || !sidebarBrandFull || !sidebarBrandMini) return; + + // Thêm class no-transition trước khi thay đổi + sidebar.classList.add('no-transition'); + contentWrapper.classList.add('no-transition'); + + // Áp dụng class trạng thái + if (sidebarState === 'collapsed') { + sidebar.classList.add('collapsed'); + contentWrapper.classList.add('expanded'); + toggleIcon.classList.remove('fa-chevron-left'); + toggleIcon.classList.add('fa-chevron-right'); + sidebarBrandFull.style.display = 'none'; + sidebarBrandMini.style.display = 'inline'; + } else { // Default to expanded + sidebar.classList.remove('collapsed'); + contentWrapper.classList.remove('expanded'); + toggleIcon.classList.remove('fa-chevron-right'); + toggleIcon.classList.add('fa-chevron-left'); + sidebarBrandFull.style.display = 'inline'; + sidebarBrandMini.style.display = 'none'; + } + + // Xóa class no-transition sau một khoảng trễ rất ngắn + // Dùng requestAnimationFrame để đảm bảo trình duyệt đã kịp xử lý thay đổi class + requestAnimationFrame(() => { + requestAnimationFrame(() => { // Thêm một frame nữa cho chắc chắn + sidebar.classList.remove('no-transition'); + contentWrapper.classList.remove('no-transition'); + }); + }); + // Hoặc dùng setTimeout: + // setTimeout(() => { + // sidebar.classList.remove('no-transition'); + // contentWrapper.classList.remove('no-transition'); + // }, 50); // 50ms delay + + })(); + // --- END: Apply Sidebar State Immediately --- + + document.addEventListener('DOMContentLoaded', function() { + const sidebar = document.getElementById('sidebar'); const sidebarToggle = document.getElementById('sidebarToggle'); - const navTexts = document.querySelectorAll('.nav-text'); - const navItems = document.querySelectorAll('.nav-item'); + const contentWrapper = document.getElementById('content-wrapper'); + const toggleIcon = sidebarToggle ? sidebarToggle.querySelector('i') : null; + const sidebarBrandFull = document.querySelector('.sidebar-brand-full'); + const sidebarBrandMini = document.querySelector('.sidebar-brand-mini'); - if (sidebarToggle) { - sidebarToggle.addEventListener('click', function() { + // Add event listener to the toggle button + if (sidebarToggle && sidebar && contentWrapper && toggleIcon && sidebarBrandFull && sidebarBrandMini) { + sidebarToggle.addEventListener('click', function(e) { + e.preventDefault(); + + const isCollapsed = sidebar.classList.contains('collapsed'); + const newState = isCollapsed ? 'expanded' : 'collapsed'; + localStorage.setItem('sidebarState', newState); + + // Toggle classes để CSS transitions hoạt động sidebar.classList.toggle('collapsed'); contentWrapper.classList.toggle('expanded'); - - // Toggle icon direction and text display - if (sidebar.classList.contains('collapsed')) { - sidebarToggle.innerHTML = '<i class="fas fa-chevron-right"></i>'; - navTexts.forEach(text => text.style.display = 'none'); - navItems.forEach(item => item.classList.add('justify-center')); + + // Cập nhật icon và brand + if (newState === 'collapsed') { + toggleIcon.classList.remove('fa-chevron-left'); + toggleIcon.classList.add('fa-chevron-right'); + sidebarBrandFull.style.display = 'none'; + sidebarBrandMini.style.display = 'inline'; } else { - sidebarToggle.innerHTML = '<i class="fas fa-chevron-left"></i>'; - navTexts.forEach(text => text.style.display = 'inline'); - navItems.forEach(item => item.classList.remove('justify-center')); + toggleIcon.classList.remove('fa-chevron-right'); + toggleIcon.classList.add('fa-chevron-left'); + sidebarBrandFull.style.display = 'inline'; + sidebarBrandMini.style.display = 'none'; } }); } - // User dropdown menu functionality - const dropdownButton = document.getElementById('dropdownMenuButton'); - const dropdownMenu = document.getElementById('userDropdownMenu'); // Use specific ID + // Dropdown toggle functionality (giữ nguyên) + const dropdownButtons = document.querySelectorAll('.dropdown button'); + dropdownButtons.forEach(button => { + button.addEventListener('click', function(event) { + const dropdownMenu = this.nextElementSibling; + // Close other open dropdowns + document.querySelectorAll('.dropdown-menu.visible').forEach(menu => { + if (menu !== dropdownMenu) { + menu.classList.remove('visible'); + } + }); + // Toggle current dropdown + dropdownMenu.classList.toggle('visible'); + event.stopPropagation(); + }); + }); + + // Close dropdowns when clicking outside (giữ nguyên) + document.addEventListener('click', function(event) { + document.querySelectorAll('.dropdown-menu.visible').forEach(menu => { + const button = menu.previousElementSibling; + if (!menu.contains(event.target) && !button.contains(event.target)) { + menu.classList.remove('visible'); + } + }); + }); - // Notification dropdown menu functionality + // --- Notification Logic --- (giữ nguyên phần này) const notificationButton = document.getElementById('notificationButton'); const notificationMenu = document.getElementById('notificationMenu'); - - // Function to close all dropdowns - function closeAllDropdowns() { - if (dropdownMenu) dropdownMenu.classList.remove('visible'); - if (notificationMenu) notificationMenu.classList.remove('visible'); - } - - // Toggle user dropdown - if (dropdownButton && dropdownMenu) { - dropdownButton.addEventListener('click', function(event) { - event.stopPropagation(); - const isVisible = dropdownMenu.classList.contains('visible'); - closeAllDropdowns(); // Close others first - if (!isVisible) { - dropdownMenu.classList.add('visible'); - } - }); + const notificationBadge = notificationButton ? notificationButton.querySelector('.notification-badge') : null; + const notificationList = notificationMenu ? notificationMenu.querySelector('.max-h-64') : null; // Div chứa danh sách thông báo + const notificationCountSpan = notificationMenu ? notificationMenu.querySelector('.flex span') : null; // Span hiển thị số lượng + + function fetchNotifications() { + if (!notificationButton || !notificationMenu || !notificationBadge || !notificationList || !notificationCountSpan) return; + + fetch('{{ url_for("notifications.get_unread_notifications") }}') // Gọi API endpoint + .then(response => response.json()) + .then(data => { + // Cập nhật badge + if (data.count > 0) { + notificationBadge.textContent = data.count; + notificationBadge.style.display = 'flex'; // Hiển thị badge nếu có thông báo + notificationCountSpan.textContent = `${data.count} mới`; // Cập nhật số lượng trong dropdown + } else { + notificationBadge.style.display = 'none'; // Ẩn badge nếu không có + notificationCountSpan.textContent = 'Không có thông báo mới'; + } + + // Xóa danh sách cũ và hiển thị danh sách mới + notificationList.innerHTML = ''; // Xóa nội dung cũ + if (data.notifications && data.notifications.length > 0) { + data.notifications.forEach(noti => { + const notificationItem = ` + <a href="${noti.link || '#'}" class="block px-4 py-3 hover:bg-gray-50 transition-colors duration-200 notification-item" data-id="${noti.id}"> + <div class="flex items-start"> + <div class="flex-shrink-0 bg-${noti.icon_color || 'blue'}-100 p-2 rounded-full"> + <i class="fas ${noti.icon || 'fa-bell'} text-${noti.icon_color || 'blue'}-600"></i> + </div> + <div class="ml-3"> + <p class="text-sm font-medium text-gray-900">${noti.message}</p> + <p class="text-xs text-gray-500">${noti.timestamp}</p> + </div> + </div> + </a> + `; + notificationList.innerHTML += notificationItem; + }); + + // Thêm sự kiện click để đánh dấu đã đọc (ví dụ) + notificationList.querySelectorAll('.notification-item').forEach(item => { + item.addEventListener('click', function(e) { + const notificationId = this.dataset.id; + // Tùy chọn: Ngăn chuyển hướng nếu chỉ muốn đánh dấu đã đọc? + // e.preventDefault(); + markNotificationRead(notificationId); + // Có thể đóng dropdown sau khi click + // notificationMenu.classList.remove('visible'); + }); + }); + + } else { + notificationList.innerHTML = '<p class="text-sm text-gray-500 px-4 py-3">Không có thông báo nào.</p>'; + } + }) + .catch(error => { + console.error('Error fetching notifications:', error); + }); } - - // Toggle notification dropdown - if (notificationButton && notificationMenu) { - notificationButton.addEventListener('click', function(event) { - event.stopPropagation(); - const isVisible = notificationMenu.classList.contains('visible'); - closeAllDropdowns(); // Close others first - if (!isVisible) { - notificationMenu.classList.add('visible'); + + function markNotificationRead(notificationId) { + if (!notificationId) return; + fetch(`/notifications/api/${notificationId}/mark-read`, { + method: 'POST', + headers: { + // Gửi kèm CSRF token nếu cần và đã cấu hình + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + console.log('Notification marked as read:', notificationId); + // Giảm số lượng trên badge và cập nhật lại danh sách + fetchNotifications(); + } else { + console.error('Failed to mark notification as read:', data.message); } + }) + .catch(error => { + console.error('Error marking notification as read:', error); }); } - - // Close dropdowns when clicking outside - document.addEventListener('click', function(event) { - // Check if the click is outside both dropdowns and their buttons - const isOutsideUser = dropdownButton && !dropdownButton.contains(event.target) && dropdownMenu && !dropdownMenu.contains(event.target); - const isOutsideNotification = notificationButton && !notificationButton.contains(event.target) && notificationMenu && !notificationMenu.contains(event.target); - - // Only close if the click is outside the currently open dropdown - if ((dropdownMenu && dropdownMenu.classList.contains('visible') && isOutsideUser) || - (notificationMenu && notificationMenu.classList.contains('visible') && isOutsideNotification)) { - // Check again to be sure, might not be needed but safer - if (isOutsideUser && isOutsideNotification) { - closeAllDropdowns(); - } else if (dropdownMenu.classList.contains('visible') && isOutsideUser) { - closeAllDropdowns(); // Close all if clicking outside user dropdown - } else if (notificationMenu.classList.contains('visible') && isOutsideNotification) { - closeAllDropdowns(); // Close all if clicking outside notification dropdown - } - } - }); + + // Gọi hàm fetch lần đầu khi trang tải xong + fetchNotifications(); + + // Tùy chọn: Tự động cập nhật thông báo sau mỗi X giây + // setInterval(fetchNotifications, 30000); // Ví dụ: cập nhật mỗi 30 giây + }); </script> - + {% block scripts %}{% endblock %} </body> </html> diff --git a/app/templates/encounter_measurements.html b/app/templates/encounter_measurements.html new file mode 100644 index 0000000000000000000000000000000000000000..3a36dcd8467da1fa6d0c12dcb3bd7bd2a0da8200 --- /dev/null +++ b/app/templates/encounter_measurements.html @@ -0,0 +1,625 @@ +{% extends "base.html" %} + +{# Sử dụng custom_encounter_id nếu có, nếu không thì dùng encounterID #} +{% set display_encounter_id = encounter.custom_encounter_id if encounter.custom_encounter_id else encounter.encounterID %} + +{% block title %}Encounter #{{ display_encounter_id }} Measurements - {{ patient.full_name }}{% endblock %} + +{% block header %}Encounter #{{ display_encounter_id }} Measurements - {{ patient.full_name }}{% endblock %} + +{% block content %} +<div class="animate-slide-in container mx-auto px-4 py-8"> + <!-- Breadcrumb --> + <div class="mb-6"> + <nav class="flex" aria-label="Breadcrumb"> + <ol class="inline-flex items-center space-x-1 md:space-x-3"> + <li class="inline-flex items-center"> + <a href="{{ url_for('handle_root') }}" class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-blue-600 transition duration-200"> + <i class="fas fa-home mr-2"></i> Home + </a> + </li> + <li> + <div class="flex items-center"> + <i class="fas fa-chevron-right text-gray-400 mx-2"></i> + <a href="{{ url_for('patients.index') }}" class="text-sm font-medium text-gray-700 hover:text-blue-600 transition duration-200">Patients</a> + </div> + </li> + <li> + <div class="flex items-center"> + <i class="fas fa-chevron-right text-gray-400 mx-2"></i> + <a href="{{ url_for('patients.patient_detail', patient_id=patient.id) }}" class="text-sm font-medium text-gray-700 hover:text-blue-600 transition duration-200">{{ patient.full_name }} ({{ patient.id }})</a> + </div> + </li> + <li aria-current="page"> + <div class="flex items-center"> + <i class="fas fa-chevron-right text-gray-400 mx-2"></i> + {# Hiển thị custom_encounter_id trong breadcrumb #} + <span class="text-sm font-medium text-gray-500">Encounter #{{ display_encounter_id }} Measurements</span> + </div> + </li> + </ol> + </nav> + </div> + + <!-- Thông tin Encounter --> + <div class="bg-white shadow rounded-lg p-4 mb-6"> + {# Hiển thị custom_encounter_id trong tiêu đề section #} + <h3 class="text-lg font-semibold mb-2">Encounter Details (ID: {{ display_encounter_id }})</h3> + <div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm"> + <p><strong>Start Time:</strong> {{ encounter.start_time.strftime('%d/%m/%Y %H:%M') if encounter.start_time else 'N/A' }}</p> + <p><strong>Status:</strong> {{ encounter_status }}</p> <!-- Sử dụng biến encounter_status --> + <p><strong>Dietitian:</strong> {{ encounter.assigned_dietitian.full_name if encounter.assigned_dietitian else 'None' }}</p> + </div> + </div> + + <!-- Các nút thao tác (Upload CSV cho encounter này) --> + <div class="flex justify-end items-center mb-6 space-x-3"> + <!-- Nút Upload CSV --> + <form id="uploadCsvForm" action="{{ url_for('patients.upload_encounter_measurements_csv', patient_id=patient.id, encounter_id=encounter.encounterID) }}" method="post" enctype="multipart/form-data" class="inline-flex"> + {# Sử dụng form rỗng để lấy CSRF token #} + {% set form = EmptyForm() %} + {{ form.csrf_token }} + {# Thêm input ẩn nếu cần gửi delimiter/encoding từ client #} + {# <input type="hidden" name="delimiter" value=","> #} + {# <input type="hidden" name="encoding" value="utf-8"> #} + <label for="csvFile" class="cursor-pointer inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-all duration-200"> + <i class="fas fa-file-csv -ml-1 mr-2 text-gray-500"></i> + Upload Measurements CSV + </label> + <input type="file" id="csvFile" name="csv_file" class="hidden" accept=".csv"> + </form> + + <!-- Nút Thêm đo lường thủ công (nếu có) --> + <!-- <a href="#" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:-translate-y-1"> + <i class="fas fa-plus -ml-1 mr-2"></i> + Add Measurement Manually + </a> --> + </div> + + <!-- Container thông báo --> + <div id="toast-container" class="fixed top-4 right-4 z-50 flex flex-col items-end space-y-2"></div> + + <!-- Macro tạo card biểu đồ (giống physical_measurements.html) --> + {% macro chart_card(chart_id, title, latest_value, unit, metric_name) %} + <div class="bg-white p-4 rounded-lg shadow-md transition hover:shadow-lg h-full flex flex-col"> + <h3 class="font-semibold text-gray-800 mb-2">{{ title }}</h3> + <div class="flex-grow"> + <canvas id="{{ chart_id }}" width="100%" height="200"></canvas> + </div> + <div class="mt-3 text-center"> + <p class="text-sm text-gray-600">Giá trị gần nhất</p> + <p class="text-xl font-bold latest-value" data-metric-name="{{ metric_name }}">{{ latest_value if latest_value is not none else 'N/A' }} {{ unit }}</p> + </div> + <div class="mt-3 pt-3 border-t border-gray-200"> + <div class="quick-input-container flex items-stretch gap-2" data-metric="{{ metric_name }}"> + <input type="number" step="any" placeholder="Giá trị mới..." + class="quick-measurement-input flex-grow p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-300 ease-in-out" + data-metric="{{ metric_name }}"> + <button type="button" + class="quick-save-btn flex-shrink-0 bg-blue-500 hover:bg-blue-600 text-white px-3 py-2 rounded-lg shadow-md flex items-center justify-center transform transition-all duration-300 ease-in-out will-change-transform" + style="transform: scale(0); opacity: 0; pointer-events: none;" + data-metric="{{ metric_name }}"> + <i class="fas fa-save text-sm mr-1"></i> + <span class="save-text whitespace-nowrap">Lưu</span> + </button> + </div> + </div> + </div> + {% endmacro %} + + {% macro bp_chart_card() %} + <div class="bg-white p-4 rounded-lg shadow-md transition hover:shadow-lg h-full flex flex-col"> + <h3 class="font-semibold text-gray-800 mb-2">Huyết áp</h3> + <div class="flex-grow"> + <canvas id="bpChart" width="100%" height="200"></canvas> + </div> + <div class="mt-3 text-center"> + <p class="text-sm text-gray-600">Giá trị gần nhất</p> + <p class="text-xl font-bold latest-value" data-metric-name="blood_pressure"> + {% if latest_bp %} + {{ latest_bp.systolicBP }}/{{ latest_bp.diastolicBP }} mmHg + {% else %} + N/A + {% endif %} + </p> + </div> + <div class="mt-3 pt-3 border-t border-gray-200"> + <div class="quick-input-container bp-container flex flex-col gap-2" data-metric="bloodPressure"> + <div class="flex gap-2 w-full"> + <input type="number" step="1" placeholder="Tâm thu..." + class="quick-measurement-input w-1/2 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-300 ease-in-out" + data-metric="systolicBP"> + <input type="number" step="1" placeholder="Tâm trương..." + class="quick-measurement-input w-1/2 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-300 ease-in-out" + data-metric="diastolicBP"> + </div> + <button type="button" + class="quick-save-btn w-full opacity-0 transform scale-95 pointer-events-none bg-blue-500 hover:bg-blue-600 text-white px-3 py-2 rounded-lg transition-all duration-300 ease-in-out shadow-md flex items-center justify-center" + data-metric="bloodPressure"> + <i class="fas fa-save mr-2"></i> Lưu huyết áp + </button> + </div> + </div> + </div> + {% endmacro %} + + {% if measurements %} + <!-- Grid layout for charts --> + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-6"> + {{ chart_card('temperatureChart', 'Nhiệt độ', latest_measurement.temperature, '°C', 'temperature') }} + {{ chart_card('heartRateChart', 'Nhịp tim', latest_measurement.heart_rate, 'bpm', 'heart_rate') }} + {{ bp_chart_card() }} + {{ chart_card('oxygenSaturationChart', 'SpO₂', latest_measurement.oxygen_saturation, '%', 'oxygen_saturation') }} + {{ chart_card('respRateChart', 'Nhịp thở', latest_measurement.resp_rate, 'bpm', 'resp_rate') }} {# Đổi tên chart_id và metric #} + {{ chart_card('fio2Chart', 'FiO₂', latest_measurement.fio2, '%', 'fio2') }} + {{ chart_card('fio2RatioChart', 'Tỷ lệ FiO₂', latest_measurement.fio2_ratio, '', 'fio2_ratio') }} + {{ chart_card('tidalVolChart', 'Thể tích khí lưu thông (Tidal Vol)', latest_measurement.tidal_vol, 'mL', 'tidal_vol') }} + {{ chart_card('tidalVolKgChart', 'Tidal Vol/kg', latest_measurement.tidal_vol_kg, 'mL/kg', 'tidal_vol_kg') }} + {{ chart_card('tidalVolActualChart', 'Tidal Vol thực tế (Actual)', latest_measurement.tidal_vol_actual, 'mL', 'tidal_vol_actual') }} + {{ chart_card('tidalVolSponChart', 'Tidal Vol tự phát (Spon)', latest_measurement.tidal_vol_spon, 'mL', 'tidal_vol_spon') }} + {{ chart_card('endTidalCO2Chart', 'End Tidal CO₂', latest_measurement.end_tidal_co2, 'mmHg', 'end_tidal_co2') }} + {{ chart_card('feedVolChart', 'Thể tích thức ăn (Feed Vol)', latest_measurement.feed_vol, 'mL', 'feed_vol') }} + {{ chart_card('feedVolAdmChart', 'Feed Vol Adm', latest_measurement.feed_vol_adm, 'mL', 'feed_vol_adm') }} + {{ chart_card('peepChart', 'PEEP', latest_measurement.peep, 'cmH₂O', 'peep') }} + {{ chart_card('pipChart', 'PIP', latest_measurement.pip, 'cmH₂O', 'pip') }} + {{ chart_card('sipChart', 'SIP', latest_measurement.sip, 'cmH₂O', 'sip') }} + {{ chart_card('inspTimeChart', 'Thời gian hít vào (Insp Time)', latest_measurement.insp_time, 's', 'insp_time') }} + {# {{ chart_card('oxygenFlowRateChart', 'Oxygen Flow Rate', latest_measurement.oxygen_flow_rate, 'L/min', 'oxygen_flow_rate') }} #} {# Thêm nếu cần #} + </div> + {% else %} + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-6"> + {{ chart_card('temperatureChart', 'Nhiệt độ', 'N/A', '°C', 'temperature') }} + {{ chart_card('heartRateChart', 'Nhịp tim', 'N/A', 'bpm', 'heart_rate') }} + {{ bp_chart_card() }} + {{ chart_card('oxygenSaturationChart', 'SpO₂', 'N/A', '%', 'oxygen_saturation') }} + {{ chart_card('respRateChart', 'Nhịp thở', 'N/A', 'bpm', 'resp_rate') }} {# Đổi tên chart_id và metric #} + {{ chart_card('fio2Chart', 'FiO₂', 'N/A', '%', 'fio2') }} + {{ chart_card('fio2RatioChart', 'Tỷ lệ FiO₂', 'N/A', '', 'fio2_ratio') }} + {{ chart_card('tidalVolChart', 'Thể tích khí lưu thông (Tidal Vol)', 'N/A', 'mL', 'tidal_vol') }} + {{ chart_card('tidalVolKgChart', 'Tidal Vol/kg', 'N/A', 'mL/kg', 'tidal_vol_kg') }} + {{ chart_card('tidalVolActualChart', 'Tidal Vol thực tế (Actual)', 'N/A', 'mL', 'tidal_vol_actual') }} + {{ chart_card('tidalVolSponChart', 'Tidal Vol tự phát (Spon)', 'N/A', 'mL', 'tidal_vol_spon') }} + {{ chart_card('endTidalCO2Chart', 'End Tidal CO₂', 'N/A', 'mmHg', 'end_tidal_co2') }} + {{ chart_card('feedVolChart', 'Thể tích thức ăn (Feed Vol)', 'N/A', 'mL', 'feed_vol') }} + {{ chart_card('feedVolAdmChart', 'Feed Vol Adm', 'N/A', 'mL', 'feed_vol_adm') }} + {{ chart_card('peepChart', 'PEEP', 'N/A', 'cmH₂O', 'peep') }} + {{ chart_card('pipChart', 'PIP', 'N/A', 'cmH₂O', 'pip') }} + {{ chart_card('sipChart', 'SIP', 'N/A', 'cmH₂O', 'sip') }} + {{ chart_card('inspTimeChart', 'Thời gian hít vào (Insp Time)', 'N/A', 's', 'insp_time') }} + </div> + <div class="text-center p-6 bg-gray-50 rounded-lg border border-gray-200"> + <p class="text-gray-600 mb-4">Chưa có dữ liệu đo lường nào cho lượt khám này.</p> + <!-- Nút thêm đo lường thủ công có thể đặt ở đây --> + </div> + {% endif %} + + <!-- Bảng dữ liệu đo lường (Optional) --> + <div class="mt-8"> + <h3 class="text-xl font-semibold text-gray-800 mb-4">Measurement History for this Encounter</h3> + <div class="bg-white shadow overflow-hidden sm:rounded-lg"> + <div class="overflow-x-auto"> + <table class="min-w-full divide-y divide-gray-200"> + <thead class="bg-gray-50"> + <tr> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Temp (°C)</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">HR (bpm)</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">BP (mmHg)</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">SpO₂ (%)</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Resp Rate</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">FiO₂ (%)</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Notes</th> + <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> + </tr> + </thead> + <tbody class="bg-white divide-y divide-gray-200"> + {% if measurements %} + {% for m in measurements|reverse %}{# Hiển thị gần nhất trước #} + <tr> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ m.measurementDateTime|format_datetime if m.measurementDateTime else 'N/A' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ "{:.1f}".format(m.temperature) if m.temperature is not none else '-' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ m.heart_rate if m.heart_rate is not none else '-' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ '%d/%d'|format(m.blood_pressure_systolic, m.blood_pressure_diastolic) if m.blood_pressure_systolic is not none and m.blood_pressure_diastolic is not none else '-' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ "{:.1f}".format(m.oxygen_saturation) if m.oxygen_saturation is not none else '-' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ m.resp_rate if m.resp_rate is not none else '-' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ "{:.1f}".format(m.fio2) if m.fio2 is not none else '-' }}</td> + <td class="px-6 py-4 text-sm text-gray-500 max-w-xs truncate" title="{{ m.notes }}">{{ m.notes or '-' }}</td> + <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> + {# <a href="{{ url_for('patients.edit_physical_measurement', patient_id=patient.id, measurement_id=m.id) }}" class="text-indigo-600 hover:text-indigo-900 mr-2">Edit</a> #} + {# <form action="{{ url_for('patients.delete_physical_measurement', patient_id=patient.id, measurement_id=m.id) }}" method="post" class="inline" onsubmit="return confirm('Are you sure you want to delete this measurement?');"> #} + {# {{ csrf_token() }} #} + {# <button type="submit" class="text-red-600 hover:text-red-900">Delete</button> #} + {# </form> #} + <span class="text-gray-400">N/A</span> {# Tạm thời chưa có action #} + </td> + </tr> + {% endfor %} + {% else %} + <tr> + <td colspan="9" class="px-6 py-10 text-center text-gray-500 text-sm"> + No measurement records for this encounter. + </td> + </tr> + {% endif %} + </tbody> + </table> + </div> + </div> + </div> + +</div> +{% endblock %} + +{% block scripts %} +<!-- Chart.js & Adapter --> +<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js"></script> +<script src="https://cdn.jsdelivr.net/npm/luxon@2.3.1/build/global/luxon.min.js"></script> +<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.1.0/dist/chartjs-adapter-luxon.min.js"></script> + +<script> +// Biến lưu các đối tượng biểu đồ (giống physical_measurements.html) +let heartRateChart, temperatureChart, bpChart, oxygenSaturationChart, respRateChart; // Đổi tên respiratoryRateChart thành respRateChart +let fio2Chart, fio2RatioChart, tidalVolChart, tidalVolKgChart, tidalVolActualChart; +let tidalVolSponChart, endTidalCO2Chart, feedVolChart, feedVolAdmChart; +let peepChart, pipChart, sipChart, inspTimeChart; + +// Hàm tạo biểu đồ (giống physical_measurements.html) +function createChart(canvasId, label, data, unit, chartType = 'line', color = 'rgba(54, 162, 235, 1)') { + const canvas = document.getElementById(canvasId); + if (!canvas) return null; + const ctx = canvas.getContext('2d'); + const backgroundColor = color.replace(", 1)", ", 0.2)"); + const chartOptions = { + type: chartType, + data: { + datasets: [{ + label: label, + data: data, + backgroundColor: backgroundColor, + borderColor: color, + borderWidth: chartType === 'line' ? 2 : 1, + pointBackgroundColor: color, + pointBorderColor: '#fff', + pointRadius: chartType === 'line' ? 3 : 0, + pointHoverRadius: chartType === 'line' ? 5 : 0, + tension: 0.1, + fill: chartType === 'line' + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + type: 'time', + time: { + unit: 'hour', + displayFormats: { hour: 'HH:mm' }, + tooltipFormat: 'dd/MM/yyyy HH:mm' + }, + title: { display: true, text: 'Thời gian' } + }, + y: { + title: { display: true, text: unit }, + beginAtZero: chartType === 'bar' + } + }, + plugins: { + tooltip: { + callbacks: { + label: function(context) { + return `${label}: ${context.parsed.y} ${unit}`; + } + } + } + } + } + }; + if (chartType === 'bar') { + chartOptions.options.scales.x.time = undefined; + chartOptions.options.scales.x.type = 'category'; + } + return new Chart(ctx, chartOptions); +} + +// Hàm tạo biểu đồ huyết áp (giống physical_measurements.html) +function createBPChart(canvasId, data) { + const canvas = document.getElementById(canvasId); + if (!canvas) return null; + const ctx = canvas.getContext('2d'); + return new Chart(ctx, { + type: 'line', + data: { + datasets: [ + { + label: 'Tâm thu', + data: data.map(d => ({x: d.x, y: d.systolic})), + backgroundColor: 'rgba(255, 99, 132, 0.2)', + borderColor: 'rgba(255, 99, 132, 1)', + borderWidth: 2, pointBackgroundColor: 'rgba(255, 99, 132, 1)', pointBorderColor: '#fff', pointRadius: 3, pointHoverRadius: 5, tension: 0.1, fill: false + }, + { + label: 'Tâm trương', + data: data.map(d => ({x: d.x, y: d.diastolic})), + backgroundColor: 'rgba(54, 162, 235, 0.2)', + borderColor: 'rgba(54, 162, 235, 1)', + borderWidth: 2, pointBackgroundColor: 'rgba(54, 162, 235, 1)', pointBorderColor: '#fff', pointRadius: 3, pointHoverRadius: 5, tension: 0.1, fill: false + } + ] + }, + options: { + responsive: true, maintainAspectRatio: false, + scales: { + x: { + type: 'time', + time: { unit: 'hour', displayFormats: { hour: 'HH:mm' }, tooltipFormat: 'dd/MM/yyyy HH:mm' }, + title: { display: true, text: 'Thời gian' } + }, + y: { title: { display: true, text: 'mmHg' } } + }, + plugins: { + tooltip: { + callbacks: { + label: function(context) { return `${context.dataset.label}: ${context.parsed.y} mmHg`; } + } + } + } + } + }); +} + +// Hàm hiện toast thông báo (giống physical_measurements.html) +function showToast(message, type = 'success') { + const toastContainer = document.getElementById('toast-container'); + const toast = document.createElement('div'); + toast.className = `flex items-center px-4 py-3 rounded shadow-md max-w-sm transition-all duration-300 ease-out ${ type === 'success' ? 'bg-green-50 text-green-800 border-l-4 border-green-500' : type === 'error' ? 'bg-red-50 text-red-800 border-l-4 border-red-500' : 'bg-blue-50 text-blue-800 border-l-4 border-blue-500' }`; + toast.innerHTML = `<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'} mr-2"></i><span class="text-sm">${message}</span>`; + toastContainer.appendChild(toast); + setTimeout(() => { toast.classList.add('opacity-0'); setTimeout(() => { toast.remove(); }, 300); }, 3000); +} + +// Hàm cập nhật biểu đồ đơn lẻ (thêm 1 điểm) (giống physical_measurements.html) +function addDataPointToChart(chart, newDataPoint) { + if (!chart || !newDataPoint || !newDataPoint.measurementDateTime || !chart.metricKey || !(chart.metricKey in newDataPoint)) { + console.error(`Invalid data or chart config for adding point:`, chart, newDataPoint); + return; + } + chart.data.datasets[0].data.push({ x: newDataPoint.measurementDateTime, y: newDataPoint[chart.metricKey] }); + chart.update(); +} + +// Hàm cập nhật biểu đồ huyết áp (thêm 1 điểm) (giống physical_measurements.html) +function addDataPointToBPChart(chart, newDataPoint) { + if (!chart || !newDataPoint || !newDataPoint.measurementDateTime || newDataPoint.blood_pressure_systolic === undefined || newDataPoint.blood_pressure_diastolic === undefined) return; + const time = newDataPoint.measurementDateTime; + chart.data.datasets[0].data.push({ x: time, y: newDataPoint.blood_pressure_systolic }); + chart.data.datasets[1].data.push({ x: time, y: newDataPoint.blood_pressure_diastolic }); + chart.update(); +} + +// Hàm cập nhật giá trị mới nhất hiển thị (giống physical_measurements.html) +function updateLatestValues(newData) { + Object.keys(newData).forEach(key => { + if (key === 'measurementDateTime') return; + try { + let valueToDisplay = newData[key]; + let metricKey = key; + let unit = ''; + const unitMap = {'temperature': ' °C', 'heart_rate': ' bpm', 'oxygen_saturation': ' %', 'resp_rate': ' bpm', 'fio2': ' %', 'fio2_ratio': '', 'tidal_vol': ' mL', 'tidal_vol_kg': ' mL/kg', 'tidal_vol_actual': ' mL', 'tidal_vol_spon': ' mL', 'end_tidal_co2': ' mmHg', 'feed_vol': ' mL', 'feed_vol_adm': ' mL', 'peep': ' cmH₂O', 'pip': ' cmH₂O', 'sip': ' cmH₂O', 'insp_time': ' s'}; + if (key === 'blood_pressure_systolic') { + if (newData['blood_pressure_diastolic'] !== undefined) { + valueToDisplay = `${newData.blood_pressure_systolic}/${newData.blood_pressure_diastolic}`; + metricKey = 'blood_pressure'; unit = ' mmHg'; + } else return; + } else if (key === 'blood_pressure_diastolic') return; + else unit = unitMap[key] || ''; + const valueDisplay = document.querySelector(`.latest-value[data-metric-name="${metricKey}"]`); + if (valueDisplay) valueDisplay.textContent = valueToDisplay !== null ? `${valueToDisplay}${unit}` : 'N/A'; + } catch (e) { console.error('Lỗi khi cập nhật giá trị hiển thị:', key, e); } + }); +} + +// Hàm xóa giá trị trong ô input (giống physical_measurements.html) +function clearInputs() { + document.querySelectorAll('.quick-measurement-input').forEach(input => { + input.value = ''; + const container = input.closest('.quick-input-container'); + const saveBtn = container.querySelector('.quick-save-btn'); + const isBP = container.classList.contains('bp-container'); + if (saveBtn) { + if (!isBP) { + saveBtn.style.transform = 'scale(0)'; saveBtn.style.opacity = '0'; saveBtn.style.pointerEvents = 'none'; + } else { + saveBtn.classList.add('opacity-0', 'scale-95', 'pointer-events-none'); + saveBtn.classList.remove('opacity-100', 'scale-100', 'pointer-events-auto'); + } + } + }); +} + +document.addEventListener('DOMContentLoaded', function() { + // Lấy CSRF token (giống physical_measurements.html) + let csrfToken; + try { csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); } catch (e) { csrfToken = ""; } + + // Store chart instances (giống physical_measurements.html) + const chartObjects = {}; + + var measurementsData = []; + try { + // Dữ liệu measurements được truyền từ route vào biến 'measurements' + measurementsData = JSON.parse('{{ measurements|tojson|safe }}'); + } catch (e) { + console.error('Lỗi khi phân tích dữ liệu đo lường:', e); + } + + initializeCharts(measurementsData); + + // Hàm khởi tạo biểu đồ từ dữ liệu (giống physical_measurements.html, nhưng dùng dữ liệu 'measurements') + function initializeCharts(data) { + const heartRateData = data.map(m => m.heart_rate !== null && m.heart_rate !== undefined ? {x: m.measurementDateTime, y: m.heart_rate} : null).filter(Boolean); + const temperatureData = data.map(m => m.temperature !== null && m.temperature !== undefined ? {x: m.measurementDateTime, y: m.temperature} : null).filter(Boolean); + const bloodPressureData = data.map(m => (m.blood_pressure_systolic !== null && m.blood_pressure_systolic !== undefined && m.blood_pressure_diastolic !== null && m.blood_pressure_diastolic !== undefined) ? {x: m.measurementDateTime, systolic: m.blood_pressure_systolic, diastolic: m.blood_pressure_diastolic} : null).filter(Boolean); + const oxygenSaturationData = data.map(m => m.oxygen_saturation !== null && m.oxygen_saturation !== undefined ? {x: m.measurementDateTime, y: m.oxygen_saturation} : null).filter(Boolean); + const respRateData = data.map(m => m.resp_rate !== null && m.resp_rate !== undefined ? {x: m.measurementDateTime, y: m.resp_rate} : null).filter(Boolean); + const fio2Data = data.map(m => m.fio2 !== null && m.fio2 !== undefined ? {x: m.measurementDateTime, y: m.fio2} : null).filter(Boolean); + const fio2RatioData = data.map(m => m.fio2_ratio !== null && m.fio2_ratio !== undefined ? {x: m.measurementDateTime, y: m.fio2_ratio} : null).filter(Boolean); + const tidalVolData = data.map(m => m.tidal_vol !== null && m.tidal_vol !== undefined ? {x: m.measurementDateTime, y: m.tidal_vol} : null).filter(Boolean); + const tidalVolKgData = data.map(m => m.tidal_vol_kg !== null && m.tidal_vol_kg !== undefined ? {x: m.measurementDateTime, y: m.tidal_vol_kg} : null).filter(Boolean); + const tidalVolActualData = data.map(m => m.tidal_vol_actual !== null && m.tidal_vol_actual !== undefined ? {x: m.measurementDateTime, y: m.tidal_vol_actual} : null).filter(Boolean); + const tidalVolSponData = data.map(m => m.tidal_vol_spon !== null && m.tidal_vol_spon !== undefined ? {x: m.measurementDateTime, y: m.tidal_vol_spon} : null).filter(Boolean); + const endTidalCO2Data = data.map(m => m.end_tidal_co2 !== null && m.end_tidal_co2 !== undefined ? {x: m.measurementDateTime, y: m.end_tidal_co2} : null).filter(Boolean); + const feedVolData = data.map(m => m.feed_vol !== null && m.feed_vol !== undefined ? {x: m.measurementDateTime, y: m.feed_vol} : null).filter(Boolean); + const feedVolAdmData = data.map(m => m.feed_vol_adm !== null && m.feed_vol_adm !== undefined ? {x: m.measurementDateTime, y: m.feed_vol_adm} : null).filter(Boolean); + const peepData = data.map(m => m.peep !== null && m.peep !== undefined ? {x: m.measurementDateTime, y: m.peep} : null).filter(Boolean); + const pipData = data.map(m => m.pip !== null && m.pip !== undefined ? {x: m.measurementDateTime, y: m.pip} : null).filter(Boolean); + const sipData = data.map(m => m.sip !== null && m.sip !== undefined ? {x: m.measurementDateTime, y: m.sip} : null).filter(Boolean); + const inspTimeData = data.map(m => m.insp_time !== null && m.insp_time !== undefined ? {x: m.measurementDateTime, y: m.insp_time} : null).filter(Boolean); + + const colors = ['rgba(54, 162, 235, 1)', 'rgba(255, 99, 132, 1)', 'rgba(75, 192, 192, 1)', 'rgba(255, 206, 86, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)', 'rgba(100, 181, 246, 1)', 'rgba(240, 98, 146, 1)', 'rgba(129, 199, 132, 1)', 'rgba(255, 241, 118, 1)', 'rgba(179, 157, 219, 1)', 'rgba(255, 183, 77, 1)', 'rgba(34, 150, 243, 1)', 'rgba(233, 30, 99, 1)', 'rgba(0, 150, 136, 1)', 'rgba(255, 193, 7, 1)', 'rgba(103, 58, 183, 1)', 'rgba(255, 87, 34, 1)']; + let colorIndex = 0; + const nextColor = () => colors[colorIndex++ % colors.length]; + + const chartConfig = [ + { id: 'heartRateChart', label: 'Nhịp tim', data: heartRateData, unit: 'bpm', type: 'line', metric: 'heart_rate' }, + { id: 'temperatureChart', label: 'Nhiệt độ', data: temperatureData, unit: '°C', type: 'line', metric: 'temperature' }, + { id: 'oxygenSaturationChart', label: 'SpO₂', data: oxygenSaturationData, unit: '%', type: 'line', metric: 'oxygen_saturation' }, + { id: 'respRateChart', label: 'Nhịp thở', data: respRateData, unit: 'bpm', type: 'line', metric: 'resp_rate' }, // Đổi tên chart id/metric + { id: 'fio2Chart', label: 'FiO₂', data: fio2Data, unit: '%', type: 'bar', metric: 'fio2' }, + { id: 'fio2RatioChart', label: 'Tỷ lệ FiO₂', data: fio2RatioData, unit: '', type: 'line', metric: 'fio2_ratio' }, + { id: 'tidalVolChart', label: 'Thể tích khí lưu thông', data: tidalVolData, unit: 'mL', type: 'line', metric: 'tidal_vol' }, + { id: 'tidalVolKgChart', label: 'Tidal Vol/kg', data: tidalVolKgData, unit: 'mL/kg', type: 'line', metric: 'tidal_vol_kg' }, + { id: 'tidalVolActualChart', label: 'Tidal Vol thực tế', data: tidalVolActualData, unit: 'mL', type: 'line', metric: 'tidal_vol_actual' }, + { id: 'tidalVolSponChart', label: 'Tidal Vol tự phát', data: tidalVolSponData, unit: 'mL', type: 'line', metric: 'tidal_vol_spon' }, + { id: 'endTidalCO2Chart', label: 'End Tidal CO₂', data: endTidalCO2Data, unit: 'mmHg', type: 'line', metric: 'end_tidal_co2' }, + { id: 'feedVolChart', label: 'Thể tích thức ăn', data: feedVolData, unit: 'mL', type: 'bar', metric: 'feed_vol' }, + { id: 'feedVolAdmChart', label: 'Feed Vol Adm', data: feedVolAdmData, unit: 'mL', type: 'bar', metric: 'feed_vol_adm' }, + { id: 'peepChart', label: 'PEEP', data: peepData, unit: 'cmH₂O', type: 'bar', metric: 'peep' }, + { id: 'pipChart', label: 'PIP', data: pipData, unit: 'cmH₂O', type: 'bar', metric: 'pip' }, + { id: 'sipChart', label: 'SIP', data: sipData, unit: 'cmH₂O', type: 'bar', metric: 'sip' }, + { id: 'inspTimeChart', label: 'Thời gian hít vào', data: inspTimeData, unit: 's', type: 'bar', metric: 'insp_time' } + ]; + + chartConfig.forEach(config => { + if (document.getElementById(config.id)) { + const chart = createChart(config.id, config.label, config.data, config.unit, config.type, nextColor()); + if (chart) { chart.metricKey = config.metric; chartObjects[config.metric] = chart; } + } + }); + + if (document.getElementById('bpChart')) { + bpChart = createBPChart('bpChart', bloodPressureData); + if (bpChart) { bpChart.metricKey = 'blood_pressure'; chartObjects['blood_pressure'] = bpChart; colorIndex += 2; } + } + } + + // Xử lý ô nhập liệu nhanh (giống physical_measurements.html) + const quickInputs = document.querySelectorAll('.quick-measurement-input'); + quickInputs.forEach(input => { + const container = input.closest('.quick-input-container'); + const saveBtn = container.querySelector('.quick-save-btn'); + const isBP = container.classList.contains('bp-container'); + let otherInput = isBP ? container.querySelector(`.quick-measurement-input:not([data-metric="${input.dataset.metric}"])`) : null; + + const showSaveButton = () => { + if (!isBP) { saveBtn.style.transform = 'scale(1)'; saveBtn.style.opacity = '1'; saveBtn.style.pointerEvents = 'auto'; } + else { saveBtn.classList.remove('opacity-0', 'scale-95', 'pointer-events-none'); saveBtn.classList.add('opacity-100', 'scale-100', 'pointer-events-auto'); } + }; + const hideSaveButton = () => { + if (!isBP) { saveBtn.style.transform = 'scale(0)'; saveBtn.style.opacity = '0'; saveBtn.style.pointerEvents = 'none'; } + else { saveBtn.classList.add('opacity-0', 'scale-95', 'pointer-events-none'); saveBtn.classList.remove('opacity-100', 'scale-100', 'pointer-events-auto'); } + }; + + if (input.value.trim() !== '' || (isBP && otherInput && otherInput.value.trim() !== '')) { showSaveButton(); } + + input.addEventListener('focus', showSaveButton); + input.addEventListener('input', showSaveButton); + input.addEventListener('blur', function(e) { + setTimeout(() => { + const relatedTargetIsSaveButton = document.activeElement === saveBtn; + const thisInputHasValue = this.value.trim() !== ''; + let otherInputHasFocus = isBP && document.activeElement === otherInput; + let otherInputHasValue = isBP && otherInput.value.trim() !== ''; + if (!relatedTargetIsSaveButton && !thisInputHasValue && (!isBP || (!otherInputHasFocus && !otherInputHasValue))) { hideSaveButton(); } + }, 150); + }); + }); + + // Xử lý sự kiện click vào nút lưu (giống, nhưng dùng endpoint mới) + const quickSaveButtons = document.querySelectorAll('.quick-save-btn'); + quickSaveButtons.forEach(btn => { + btn.addEventListener('click', function() { + const metric = this.dataset.metric; + const container = this.closest('.quick-input-container'); + let values = {}; + let chartToUpdate = null; + let updateFunction = null; + let targetMetric = metric; + + if (metric === 'bloodPressure') { + targetMetric = 'blood_pressure'; + const systolicInput = container.querySelector('input[data-metric="systolicBP"]'); + const diastolicInput = container.querySelector('input[data-metric="diastolicBP"]'); + if (!systolicInput.value || !diastolicInput.value) { showToast('Vui lòng nhập cả giá trị tâm thu và tâm trương', 'error'); return; } + values = { 'blood_pressure_systolic': parseInt(systolicInput.value), 'blood_pressure_diastolic': parseInt(diastolicInput.value) }; + chartToUpdate = chartObjects[targetMetric]; updateFunction = addDataPointToBPChart; + } else { + const input = container.querySelector(`.quick-measurement-input[data-metric="${metric}"]`); + if (!input.value) { showToast('Vui lòng nhập giá trị', 'error'); return; } + // Xử lý giá trị nhập vào, dùng parseFloat cho các số thập phân + const numberValue = parseFloat(input.value); + if (isNaN(numberValue)) { showToast('Giá trị không hợp lệ', 'error'); return; } + values[metric] = numberValue; + chartToUpdate = chartObjects[targetMetric]; updateFunction = addDataPointToChart; + } + + showToast('Đang lưu dữ liệu...', 'info'); + const originalText = this.innerHTML; this.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Đang lưu...'; this.disabled = true; + + let currentCsrfToken; try { currentCsrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); } catch(e) { currentCsrfToken = ""; } + + // *** Sử dụng endpoint mới cho encounter cụ thể *** + fetch(`/patients/{{ patient.id }}/encounter/{{ encounter.encounterID }}/measurements/quick_save`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRFToken': currentCsrfToken }, + credentials: 'same-origin', + body: JSON.stringify({ values: values }) + }) + .then(response => { + if (!response.ok) { return response.text().then(text => { throw new Error(text || `Lỗi Server: ${response.status}`); }); } + return response.json(); + }) + .then(data => { + this.innerHTML = originalText; this.disabled = false; + if (data.success) { + showToast('Đã lưu dữ liệu thành công', 'success'); + if (data.new_measurement_data) { + updateLatestValues(data.new_measurement_data); + if (chartToUpdate && updateFunction) { updateFunction(chartToUpdate, data.new_measurement_data); } + else { console.warn('Không tìm thấy biểu đồ/hàm cập nhật:', targetMetric); } + } + clearInputs(); + } else { showToast(`Lỗi: ${data.message || 'Lỗi không xác định'}`, 'error'); } + }) + .catch(error => { + console.error('Quick Save Error:', error); this.innerHTML = originalText; this.disabled = false; + showToast(error.message || 'Đã xảy ra lỗi khi lưu dữ liệu', 'error'); + }); + }); + }); + + // Xử lý sự kiện tải file CSV (giống upload.html, nhưng submit đến route mới) + const csvFileInput = document.getElementById('csvFile'); + if (csvFileInput) { + csvFileInput.addEventListener('change', function() { + if (this.files.length > 0) { + // Hiển thị loader nếu có + const loader = document.getElementById('pageLoader'); + if (loader) { loader.style.display = 'flex'; loader.style.opacity = '1'; } + document.getElementById('uploadCsvForm').submit(); + } + }); + } +}); +</script> +{% endblock %} \ No newline at end of file diff --git a/app/templates/patient_detail.html b/app/templates/patient_detail.html index 6c8c95b831cd57bbeadf429f8d76e8b3461d1ab5..98fd2efe9eb2bfbc9ce972456dd2e931efb74bee 100644 --- a/app/templates/patient_detail.html +++ b/app/templates/patient_detail.html @@ -45,9 +45,10 @@ <i class="fas fa-edit -ml-1 mr-2 text-gray-500"></i> Edit Patient </a> - <a href="{{ url_for('patients.physical_measurements', patient_id=patient.id) }}" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-blue-700 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200 transform hover:-translate-y-1"> + <a href="{{ url_for('patients.encounter_measurements', patient_id=patient.id, encounter_id=latest_encounter.encounterID) if latest_encounter else '#' }}" + class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-blue-700 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200 transform hover:-translate-y-1 {{ 'opacity-50 cursor-not-allowed' if not latest_encounter }}"> <i class="fas fa-chart-line -ml-1 mr-2 text-blue-500"></i> - Physical Statistics + Latest Statistics </a> <!-- Nút cập nhật trạng thái có thể thêm chức năng sau --> <!-- <button type="button" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 transform hover:-translate-y-1"> @@ -437,42 +438,69 @@ <h3 class="text-lg leading-6 font-medium text-gray-900"> Lịch sử Lượt khám (Encounters) </h3> - <!-- Nút thêm encounter mới nếu cần --> - <button type="button" class="inline-flex items-center px-3 py-1 border border-transparent rounded text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"> - <i class="fas fa-plus mr-2"></i> - Thêm lượt khám - </button> + {# Thay thế button bằng form #} + <form action="{{ url_for('patients.new_encounter', patient_id=patient.id) }}" method="POST" class="inline-block"> + {# Thêm CSRF token nếu bạn muốn xác thực trong route #} + {# BỎ COMMENT DÒNG DƯỚI ĐỂ THÊM CSRF TOKEN #} + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> + <button type="submit" class="inline-flex items-center px-3 py-1 border border-transparent rounded text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200"> + <i class="fas fa-plus mr-2"></i> + Thêm lượt khám + </button> + </form> </div> <div class="border-t border-gray-200 px-4 py-5 sm:p-6"> - {% if encounters %} + {% if encounters_data %} <div class="overflow-x-auto"> <table class="min-w-full divide-y divide-gray-200"> <thead class="bg-gray-50"> <tr> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ngày nhập viện</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ngày ra viện</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Thời gian khám</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Chuyên gia DD</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ngày vào CCU</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ngày ra CCU</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Chỉ số bất ổn (Max 3)</th> + <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Trạng thái</th> <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Thao tác</th> </tr> </thead> <tbody class="bg-white divide-y divide-gray-200"> - {% for encounter in encounters %} - <tr class="encounter-row hover:bg-gray-100 cursor-pointer transition duration-150 ease-in-out" data-encounter-id="{{ encounter.id }}"> - <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ encounter.id }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ encounter.admissionDateTime.strftime('%d/%m/%Y %H:%M') if encounter.admissionDateTime else 'N/A' }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> - {{ encounter.dischargeDateTime.strftime('%d/%m/%Y %H:%M') if encounter.dischargeDateTime else 'Đang điều trị' }} - </td> + {% for data in encounters_data %} + {% set encounter = data.encounter %} + {% set status = data.status %} + {% set unstable_metrics = data.unstable_metrics %} + <tr class="hover:bg-gray-100 transition duration-150 ease-in-out"> + <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ encounter.custom_encounter_id if encounter.custom_encounter_id else encounter.encounterID }}</td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ encounter.start_time.strftime('%d/%m/%Y %H:%M') if encounter.start_time else 'N/A' }}</td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> - {{ encounter.dietitian.fullName if encounter.dietitian else 'Chưa gán' }} + {{ encounter.assigned_dietitian.full_name if encounter.assigned_dietitian else 'Chưa gán' }} </td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ encounter.ccuAdmissionDateTime.strftime('%d/%m/%Y %H:%M') if encounter.ccuAdmissionDateTime else 'N/A' }}</td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ encounter.ccuDischargeDateTime.strftime('%d/%m/%Y %H:%M') if encounter.ccuDischargeDateTime else 'N/A' }}</td> - <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> - <a href="#" class="text-blue-600 hover:text-blue-900 view-encounter-details" data-encounter-id="{{ encounter.id }}">Chi tiết</a> + <td class="px-6 py-4 whitespace-nowrap text-sm text-red-600"> + {% if unstable_metrics %} + {{ unstable_metrics | join(', ') }} + {% else %} + <span class="text-gray-500">-</span> + {% endif %} + </td> + <td class="px-6 py-4 whitespace-nowrap"> + <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-{{ status.color }}-100 text-{{ status.color }}-800"> + <span class="w-2 h-2 mr-1.5 rounded-full bg-{{ status.color }}-500"></span> + {{ status.text }} + </span> + </td> + <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2"> + {# Nút xem chi tiết #} + <a href="{{ url_for('patients.encounter_measurements', patient_id=patient.id, encounter_id=encounter.encounterID) }}" class="text-blue-600 hover:text-blue-800" title="View Details"> + <i class="fas fa-eye"></i> + </a> + {# Nút xóa encounter #} + <form action="{{ url_for('patients.delete_encounter', patient_id=patient.id, encounter_pk=encounter.encounterID) }}" method="POST" class="inline-block" onsubmit="return confirm('Bạn có chắc chắn muốn xóa lượt khám này và tất cả dữ liệu đo lường liên quan không?');"> + {# Sử dụng EmptyForm để tạo CSRF token #} + {% set form = EmptyForm() %} + {{ form.csrf_token }} + <button type="submit" class="text-red-600 hover:text-red-800" title="Delete Encounter"> + <i class="fas fa-trash-alt"></i> + </button> + </form> </td> </tr> {% endfor %} diff --git a/app/utils/csv_handler.py b/app/utils/csv_handler.py index ea80c0e7d62dc6894228618a44a0e455eadeef72..936aea433089d91fcbd75a958297f92c8ee57762 100644 --- a/app/utils/csv_handler.py +++ b/app/utils/csv_handler.py @@ -5,12 +5,21 @@ import json from flask import current_app from datetime import datetime from app import db -from app.models.patient import Patient, Encounter +from app.models.patient import Patient +from app.models.encounter import Encounter from app.models.measurement import PhysiologicalMeasurement from app.models.referral import Referral from app.models.uploaded_file import UploadedFile from sqlalchemy.exc import IntegrityError +from sqlalchemy import desc # Thêm import desc from dateutil.parser import parse as parse_datetime_string # Thư viện tốt hơn để parse datetime +import logging +from io import TextIOWrapper +from werkzeug.utils import secure_filename +import io + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) # --- Helper Functions --- def _parse_float(value): @@ -46,43 +55,150 @@ def _parse_datetime(value): # return None return None -# --- Validation (Giữ nguyên hoặc đơn giản hóa nếu chỉ cần kiểm tra header) --- +# --- Function to generate next patient ID --- +def _get_next_patient_id(): + """Generates the next patient ID in the format P-xxxxx.""" + last_patient = Patient.query.order_by(desc(Patient.id)).first() + if last_patient and last_patient.id.startswith('P-'): + try: + last_num = int(last_patient.id.split('-')[1]) + new_id_num = last_num + 1 + return f'P-{new_id_num:05d}' # Đảm bảo có 5 chữ số + except (ValueError, IndexError): + # Fallback nếu ID cuối cùng không đúng định dạng + pass + # Default starting ID or if recovery needed + return 'P-10001' + +# --- Validation (Có thể giữ nguyên hoặc xóa nếu không dùng) --- def validate_csv(file_path, delimiter=',', encoding='utf-8'): """Validate CSV header for patient upload format.""" + # ... (giữ nguyên hoặc xóa/sửa đổi nếu cần) ... + pass # Tạm thời bỏ qua validation phức tạp này + +# --- NEW function for processing New Patients CSV --- +def process_new_patients_csv(uploaded_file_id): + """ + Process a CSV file containing only new patient information. + Automatically generates a new patientID for each row. + Expected headers: firstName, lastName, age, gender, height, weight, blood_type + """ + uploaded_file = UploadedFile.query.get(uploaded_file_id) + if not uploaded_file: + return {'success': False, 'error': f'Uploaded file record not found (ID: {uploaded_file_id})'} + + total_records = 0 + processed_records = 0 + error_records = 0 + errors = [] + patient_ids_created = [] + + expected_headers = ['firstName', 'lastName', 'age', 'gender', 'height', 'weight', 'blood_type'] + try: - if not os.path.exists(file_path): - return {'valid': False, 'error': 'File does not exist'} + with open(uploaded_file.filePath, 'r', encoding=uploaded_file.file_encoding, newline='') as csvfile: + reader = csv.DictReader(csvfile, delimiter=uploaded_file.delimiter) + + # Basic header check + if not reader.fieldnames or any(h not in reader.fieldnames for h in expected_headers): + missing = [h for h in expected_headers if h not in (reader.fieldnames or [])] + raise ValueError(f"CSV header mismatch. Missing or incorrect columns: {', '.join(missing)}. Required: {', '.join(expected_headers)}") - file_size = os.path.getsize(file_path) - if file_size > 10 * 1024 * 1024: # 10MB - return {'valid': False, 'error': 'File is too large (max 10MB)'} + for i, row in enumerate(reader): + total_records += 1 + row_num = i + 2 # Row number in the original file - with open(file_path, 'r', encoding=encoding, newline='') as csvfile: - reader = csv.reader(csvfile, delimiter=delimiter) - header = next(reader, None) # Đọc dòng header + try: + # Generate new Patient ID + new_patient_id = _get_next_patient_id() + # Check if somehow this ID already exists (extremely unlikely but safe) + while Patient.query.get(new_patient_id): + current_app.logger.warning(f"Generated patient ID {new_patient_id} already exists. Regenerating.") + new_patient_id = _get_next_patient_id() # Regenerate - if not header: - return {'valid': False, 'error': 'CSV file is empty or header is missing.'} + height = _parse_float(row.get('height')) + weight = _parse_float(row.get('weight')) - # Kiểm tra các cột bắt buộc tối thiểu (có thể mở rộng) - required_patient_cols = ['patientID', 'firstName', 'lastName', 'age', 'gender'] - required_measurement_cols = ['measurementDateTime'] - required_cols = required_patient_cols + required_measurement_cols + new_patient = Patient( + id=new_patient_id, # Use generated ID + firstName=row.get('firstName'), + lastName=row.get('lastName'), + age=_parse_int(row.get('age')), + gender=row.get('gender', '').lower() or None, + height=height, + weight=weight, + blood_type=row.get('blood_type'), + admission_date=datetime.utcnow(), # Set admission date on creation + status='Active' # Default status + ) + new_patient.calculate_bmi() # Calculate BMI if possible - missing_cols = [col for col in required_cols if col not in header] - if missing_cols: - return {'valid': False, 'error': f'Missing required columns: {", ".join(missing_cols)}'} + db.session.add(new_patient) + db.session.commit() # Commit each patient + processed_records += 1 + patient_ids_created.append(new_patient_id) - return {'valid': True, 'error': None} + except IntegrityError as ie: + db.session.rollback() + error_records += 1 + errors.append({'row': row_num, 'error': f'Database integrity error (check logs): {str(ie)}', 'data': dict(row)}) + current_app.logger.error(f"IntegrityError on row {row_num} for file {uploaded_file_id}: {ie}", exc_info=True) + except Exception as e: + db.session.rollback() + error_records += 1 + error_message = f'Error processing row: {str(e)}' + errors.append({'row': row_num, 'error': error_message, 'data': dict(row)}) + current_app.logger.error(f"CSV Processing Error (Row {row_num}, File {uploaded_file_id}): {error_message}", exc_info=True) except Exception as e: - return {'valid': False, 'error': f'CSV validation error: {str(e)}'} + db.session.rollback() # Rollback any potential partial commits if file reading fails + uploaded_file.status = 'failed' + error_message = f'Failed to read or process CSV file: {str(e)}' + uploaded_file.error_details = json.dumps([{'row': 0, 'error': error_message}]) # Store file-level error + current_app.logger.error(f"CSV File Processing Failed (File ID: {uploaded_file_id}): {error_message}", exc_info=True) + db.session.commit() + return {'success': False, 'error': error_message} -# --- Rewritten process_csv function --- -def process_csv(uploaded_file_id): + # Update uploaded file record + uploaded_file.total_records = total_records + uploaded_file.processed_records = processed_records + uploaded_file.error_records = error_records + if errors: + # Limit the size of error details stored in DB + MAX_ERROR_DETAIL_LENGTH = 16000000 # Slightly less than 16MB to be safe + error_details_json = json.dumps(errors) + if len(error_details_json) > MAX_ERROR_DETAIL_LENGTH: + # Provide a summary instead of truncated data + summary_error = { + "summary": f"Too many errors ({len(errors)}) to store details.", + "message": "Check application logs for full error details.", + "first_few_errors": errors[:5] # Store first few errors as sample + } + uploaded_file.error_details = json.dumps(summary_error) + current_app.logger.warning(f"Error details for file {uploaded_file_id} exceeded length limit. Storing summary.") + else: + uploaded_file.error_details = error_details_json + + uploaded_file.status = 'completed_with_errors' if error_records > 0 else 'completed' + uploaded_file.process_end = datetime.utcnow() # Mark end time + + try: + db.session.commit() + except Exception as commit_error: + current_app.logger.error(f"Failed to commit final status for uploaded file {uploaded_file_id}: {commit_error}", exc_info=True) + # Even if final commit fails, processing might have partially succeeded. + return {'success': error_records == 0, 'processed_records': processed_records, 'error_records': error_records, 'total_records': total_records, 'errors': errors} + + return {'success': error_records == 0, 'processed_records': processed_records, 'error_records': error_records, 'total_records': total_records, 'errors': errors, 'patient_ids_created': patient_ids_created} + + +# --- Renamed old process_csv function --- +def _process_combined_patient_measurement_csv(uploaded_file_id): """ + (Internal use or for specific combined format) Process the combined Patient + Initial Measurement CSV file. Creates Patient, initial Encounter, initial Measurement, and initial Referral. + Requires patientID in the CSV. """ uploaded_file = UploadedFile.query.get(uploaded_file_id) if not uploaded_file: @@ -93,12 +209,18 @@ def process_csv(uploaded_file_id): error_records = 0 errors = [] + # Cập nhật thời gian bắt đầu xử lý + uploaded_file.process_start = datetime.utcnow() + uploaded_file.status = 'processing' + db.session.commit() + try: with open(uploaded_file.filePath, 'r', encoding=uploaded_file.file_encoding, newline='') as csvfile: # Sử dụng DictReader để dễ dàng truy cập cột bằng tên reader = csv.DictReader(csvfile, delimiter=uploaded_file.delimiter) # Kiểm tra header có khớp không (an toàn hơn) + # (Giữ nguyên header check của hàm cũ) expected_headers = [ 'patientID', 'firstName', 'lastName', 'age', 'gender', 'height', 'weight', 'blood_type', 'measurementDateTime', 'temperature', 'heart_rate', 'blood_pressure_systolic', @@ -109,11 +231,10 @@ def process_csv(uploaded_file_id): ] if not reader.fieldnames or any(h not in reader.fieldnames for h in expected_headers): missing = [h for h in expected_headers if h not in (reader.fieldnames or [])] - # Cho phép thiếu các cột không bắt buộc nếu cần - # Đây là check nghiêm ngặt, yêu cầu mọi cột phải có + # Bỏ qua check nghiêm ngặt nếu muốn linh hoạt hơn # if missing: # raise ValueError(f"CSV header mismatch. Missing or incorrect columns: {', '.join(missing)}") - pass # Bỏ qua check nghiêm ngặt nếu muốn linh hoạt hơn + pass for i, row in enumerate(reader): total_records += 1 @@ -122,12 +243,13 @@ def process_csv(uploaded_file_id): # --- 1. Process Patient --- patient_id = row.get('patientID') if not patient_id: + # This function REQUIRES patientID, so raise error raise ValueError("Missing patientID") - # Kiểm tra xem Patient đã tồn tại chưa, nếu có thì bỏ qua (hoặc cập nhật tùy logic) + # Kiểm tra xem Patient đã tồn tại chưa, nếu có thì bỏ qua existing_patient = Patient.query.get(patient_id) if existing_patient: - errors.append({'row': row_num, 'error': f'Patient {patient_id} already exists. Skipping.'}) + errors.append({'row': row_num, 'error': f'Patient {patient_id} already exists. Skipping row for combined processing.'}) error_records += 1 continue # Bỏ qua dòng này @@ -150,16 +272,14 @@ def process_csv(uploaded_file_id): # --- 2. Process Initial Encounter --- measurement_time = _parse_datetime(row.get('measurementDateTime')) if not measurement_time: - # Nếu không có thời gian đo lường, dùng thời gian hiện tại measurement_time = datetime.utcnow() # Đặt admission_date của Patient bằng thời gian Encounter đầu tiên new_patient.admission_date = measurement_time initial_encounter = Encounter( - patientID=new_patient.id, - admissionDateTime=measurement_time - # dischargeDateTime sẽ là None + patient_id=new_patient.id, + start_time=measurement_time ) # --- 3. Process Initial Measurement --- @@ -187,99 +307,223 @@ def process_csv(uploaded_file_id): tidal_vol_spon=_parse_float(row.get('tidal_vol_spon')), feed_vol=_parse_float(row.get('feed_vol')), feed_vol_adm=_parse_float(row.get('feed_vol_adm')), - bmi=_parse_float(row.get('bmi')) # Có thể lấy BMI từ CSV hoặc để Patient tự tính - # notes có thể thêm sau này nếu cần + bmi=_parse_float(row.get('bmi')) ) # --- 4. Process Initial Referral (Optional) --- initial_referral = None - referral_flag = _parse_int(row.get('referral')) # Parse '1' hoặc '0' + referral_flag = _parse_int(row.get('referral')) if referral_flag == 1: initial_referral = Referral( patient_id=new_patient.id, # encounter_id sẽ gán sau - is_ml_recommended=True, # Giả định referral từ CSV là do ML/auto - referral_status='ML Recommended', # Hoặc 'Pending Review' + is_ml_recommended=True, + referral_status='ML Recommended', referralRequestedDateTime=measurement_time - # dietitian_id, notes có thể thêm sau ) - # --- Add to Session and Commit (từng bản ghi hoặc theo batch) --- + # --- Add to Session and Commit --- db.session.add(new_patient) db.session.add(initial_encounter) - db.session.flush() # Flush để lấy initial_encounter.id + db.session.flush() - initial_measurement.encounter_id = initial_encounter.id + initial_measurement.encounter_id = initial_encounter.encounterID db.session.add(initial_measurement) if initial_referral: - initial_referral.encounter_id = initial_encounter.id + initial_referral.encounter_id = initial_encounter.encounterID db.session.add(initial_referral) - db.session.commit() # Commit cho mỗi bệnh nhân để tránh lỗi lớn + db.session.commit() processed_records += 1 except IntegrityError as ie: db.session.rollback() error_records += 1 - errors.append({'row': row_num, 'error': f'Database integrity error (possibly duplicate ID?): {str(ie)}'}) + errors.append({'row': row_num, 'error': f'Database integrity error: {str(ie)}'}) except Exception as e: db.session.rollback() error_records += 1 - # Ghi log lỗi chi tiết hơn error_message = f'Error processing row: {str(e)}' - current_app.logger.error(f"CSV Processing Error (Row {row_num}): {error_message} Data: {row}", exc_info=True) + current_app.logger.error(f"Combined CSV Processing Error (Row {row_num}, File {uploaded_file_id}): {error_message} Data: {row}", exc_info=True) errors.append({'row': row_num, 'error': error_message}) except Exception as e: - # Lỗi đọc file hoặc lỗi không mong muốn khác db.session.rollback() uploaded_file.status = 'failed' - error_message = f'Failed to process CSV file: {str(e)}' - uploaded_file.error_details = error_message - current_app.logger.error(f"CSV File Processing Failed (File ID: {uploaded_file_id}): {error_message}", exc_info=True) + error_message = f'Failed to process combined CSV file: {str(e)}' + uploaded_file.error_details = json.dumps([{'row': 0, 'error': error_message}]) + current_app.logger.error(f"Combined CSV File Processing Failed (File ID: {uploaded_file_id}): {error_message}", exc_info=True) db.session.commit() return {'success': False, 'error': error_message} - # Cập nhật trạng thái file sau khi xử lý xong + # Update uploaded file record uploaded_file.total_records = total_records uploaded_file.processed_records = processed_records uploaded_file.error_records = error_records - uploaded_file.status = 'completed' if error_records == 0 else 'completed_with_errors' - - # Xử lý lưu error_details một cách thông minh để tránh quá lớn - if errors: - # Nếu có quá nhiều lỗi trùng lặp, hãy tóm tắt thay vì lưu tất cả - if len(errors) > 10 and all(e.get('error', '').startswith('Patient') and 'already exists' in e.get('error', '') for e in errors[:10]): - patient_ids = [e.get('error').split()[1] for e in errors if 'already exists' in e.get('error', '')] - error_summary = { - 'summary': f'Phát hiện {len(patient_ids)} bệnh nhân đã tồn tại trong hệ thống.', - 'sample_patients': patient_ids[:5], # Chỉ lấy 5 mẫu - 'message': f'Vui lòng kiểm tra lại dữ liệu hoặc sử dụng tính năng cập nhật thay vì thêm mới.' - } - uploaded_file.error_details = json.dumps(error_summary) + uploaded_file.error_details = json.dumps(errors) if errors else None + uploaded_file.status = 'completed_with_errors' if error_records > 0 else 'completed' + uploaded_file.process_end = datetime.utcnow() # Mark end time + + try: + db.session.commit() + except Exception as commit_error: + current_app.logger.error(f"Failed to commit final status for combined file {uploaded_file_id}: {commit_error}", exc_info=True) + + return {'success': error_records == 0, 'processed_records': processed_records, 'error_records': error_records, 'total_records': total_records, 'errors': errors} + +# --- Function for processing Encounter Measurements CSV --- +def process_encounter_measurements_csv(file_storage, patient_id, encounter_id): + """ + Xử lý file CSV chứa dữ liệu đo lường sinh lý cho một encounter cụ thể. + + Args: + file_storage: Đối tượng FileStorage từ request. + patient_id: ID của bệnh nhân. + encounter_id: ID của encounter. + + Returns: + Tuple (bool, str): (True, success_message) nếu thành công, + (False, error_message) nếu thất bại. + """ + try: + # Đảm bảo patient và encounter tồn tại + patient = Patient.query.get(patient_id) + encounter = Encounter.query.get(encounter_id) + if not patient or not encounter or encounter.patient_id != patient.id: + return False, "Bệnh nhân hoặc encounter không hợp lệ." + + # Đọc nội dung file CSV + stream = io.StringIO(file_storage.stream.read().decode("UTF8"), newline=None) + csv_reader = csv.DictReader(stream) + + required_headers = [ + 'measurementDateTime', 'temperature', 'heart_rate', + 'blood_pressure_systolic', 'blood_pressure_diastolic', + 'oxygen_saturation', 'resp_rate' # Thêm các header bắt buộc khác nếu cần + ] + optional_headers = [ + 'fio2', 'tidal_vol', 'end_tidal_co2', 'feed_vol', 'feed_vol_adm', + 'fio2_ratio', 'insp_time', 'oxygen_flow_rate', 'peep', 'pip', 'sip', + 'tidal_vol_actual', 'tidal_vol_kg', 'tidal_vol_spon', 'bmi' + ] + + # Kiểm tra header + if not all(header in csv_reader.fieldnames for header in required_headers): + missing = [h for h in required_headers if h not in csv_reader.fieldnames] + return False, f"File CSV thiếu các cột bắt buộc: {', '.join(missing)}" + + measurements_to_add = [] + errors = [] + row_count = 0 + skipped_mismatch_count = 0 # Đếm số dòng bị bỏ qua do ID không khớp + + for row in csv_reader: + row_count += 1 + try: + # Lấy patientID và encounterID từ dòng CSV + csv_patient_id = row.get('patientID') + csv_encounter_id = row.get('encounterID') + + # Kiểm tra xem ID trong CSV có khớp với ID từ route không + if csv_patient_id != patient_id or csv_encounter_id != encounter.custom_encounter_id: + # Ghi log hoặc bỏ qua nhẹ nhàng nếu ID không khớp + current_app.logger.warning(f"Dòng {row_count + 1}: Bỏ qua do PatientID/EncounterID không khớp (CSV: {csv_patient_id}/{csv_encounter_id}, Target: {patient_id}/{encounter.custom_encounter_id})") + skipped_mismatch_count += 1 + continue # Bỏ qua dòng này và xử lý dòng tiếp theo + + # Sử dụng tên thuộc tính Python cho keys + measurement_data = { + 'patient_id': patient_id, + 'encounter_id': encounter_id + } + + # Xử lý các trường bắt buộc + try: + measurement_data['measurementDateTime'] = datetime.fromisoformat(row['measurementDateTime']) + except ValueError: + raise ValueError(f"Định dạng measurementDateTime không hợp lệ: {row['measurementDateTime']}") + + # Gán các giá trị từ CSV vào dictionary measurement_data dùng tên thuộc tính model + measurement_data['temperature'] = float(row['temperature']) if row.get('temperature') else None + measurement_data['heart_rate'] = int(row['heart_rate']) if row.get('heart_rate') else None + measurement_data['blood_pressure_systolic'] = int(row['blood_pressure_systolic']) if row.get('blood_pressure_systolic') else None + measurement_data['blood_pressure_diastolic'] = int(row['blood_pressure_diastolic']) if row.get('blood_pressure_diastolic') else None + measurement_data['oxygen_saturation'] = float(row['oxygen_saturation']) if row.get('oxygen_saturation') else None + # Đảm bảo tên key khớp với tên thuộc tính trong model measurement.py (vd: resp_rate) + measurement_data['resp_rate'] = int(row['resp_rate']) if row.get('resp_rate') else None + + # Xử lý các trường tùy chọn (dùng tên thuộc tính model) + for header in optional_headers: + model_attribute_name = header # Giả sử tên header CSV khớp tên thuộc tính model + # Nếu tên header CSV khác tên thuộc tính, cần có map chuyển đổi ở đây + if header in row and row[header]: + try: + try: measurement_data[model_attribute_name] = float(row[header]) + except ValueError: measurement_data[model_attribute_name] = int(row[header]) + except (ValueError, TypeError): + current_app.logger.warning(f"Giá trị không hợp lệ cho cột '{header}' ở dòng {row_count+1}: {row[header]}. Gán None.") + measurement_data[model_attribute_name] = None + else: + measurement_data[model_attribute_name] = None + + # Lọc dictionary dựa trên các thuộc tính hợp lệ của model + valid_attributes = {key for key in PhysiologicalMeasurement.__mapper__.attrs.keys()} + # Thêm cả các key relationship nếu cần (nhưng ở đây không cần gán relationship khi tạo) + # valid_attributes.update({rel.key for rel in PhysiologicalMeasurement.__mapper__.relationships}) + + filtered_data = {k: v for k, v in measurement_data.items() if k in valid_attributes} + + # Kiểm tra xem encounter_id và patient_id có trong filtered_data không + if 'encounter_id' not in filtered_data or 'patient_id' not in filtered_data: + current_app.logger.error(f"Lỗi lọc key: encounter_id hoặc patient_id bị thiếu trong filtered_data. Keys: {filtered_data.keys()}") + # Có thể raise lỗi ở đây nếu muốn dừng xử lý + # raise ValueError("Lỗi nội bộ: Không thể lọc đúng encounter_id/patient_id") + + measurement = PhysiologicalMeasurement(**filtered_data) + measurements_to_add.append(measurement) + + except (ValueError, TypeError) as e: + errors.append(f"Dòng {row_count + 1}: Lỗi dữ liệu - {e}") + except Exception as e: + errors.append(f"Dòng {row_count + 1}: Lỗi không xác định - {e}") + + if errors: + # Nếu có lỗi, không commit và báo lỗi + db.session.rollback() + error_message = f"Đã xảy ra lỗi khi xử lý file CSV. {len(errors)} dòng bị lỗi. Lỗi đầu tiên: {errors[0]}" + current_app.logger.error(f"Lỗi xử lý CSV cho encounter {encounter_id}: {error_message}") + return False, error_message + + if not measurements_to_add: + return False, "Không có dữ liệu hợp lệ nào được tìm thấy trong file CSV cho encounter này." + + # Thêm thông báo nếu có dòng bị bỏ qua do không khớp ID + if skipped_mismatch_count > 0: + final_message = f"Đã nhập thành công {len(measurements_to_add)} bản ghi đo lường. Bỏ qua {skipped_mismatch_count} dòng do PatientID/EncounterID không khớp." + flash_category = 'warning' # Dùng warning nếu có dòng bị bỏ qua else: - # Chỉ lưu tối đa 20 lỗi đầu tiên để tránh quá lớn - limited_errors = errors[:20] - if len(errors) > 20: - limited_errors.append({'summary': f'... và {len(errors) - 20} lỗi khác không hiển thị.'}) - uploaded_file.error_details = json.dumps(limited_errors) - - uploaded_file.process_end = datetime.utcnow() - db.session.commit() + final_message = f"Đã nhập thành công {len(measurements_to_add)} bản ghi đo lường." + flash_category = 'success' + + # Thêm tất cả các measurement hợp lệ vào session và commit + if measurements_to_add: + db.session.bulk_save_objects(measurements_to_add) + db.session.commit() + current_app.logger.info(f"Đã thêm {len(measurements_to_add)} bản ghi cho encounter {encounter_id} từ CSV. Bỏ qua {skipped_mismatch_count} dòng không khớp.") + # Trả về message và category để route có thể flash đúng loại thông báo + return True, (final_message, flash_category) + else: + # Trường hợp không có lỗi nhưng cũng không có bản ghi nào được thêm (và không có dòng nào bị skip mismatch) + return False, "Không có dữ liệu hợp lệ nào được tìm thấy trong file CSV cho encounter này." + + except UnicodeDecodeError: + db.session.rollback() + return False, "Lỗi mã hóa file. Vui lòng đảm bảo file được lưu dưới dạng UTF-8." + except Exception as e: + db.session.rollback() + current_app.logger.error(f"Lỗi nghiêm trọng khi xử lý CSV cho encounter {encounter_id}: {e}", exc_info=True) + return False, f"Đã xảy ra lỗi không mong muốn: {str(e)}" - return { - 'success': True, - 'total_records': total_records, - 'processed_records': processed_records, - 'error_records': error_records, - 'error_details': uploaded_file.error_details - } - -# --- Xóa các hàm process_patient/encounter/measurement/referral_data cũ --- -# def process_patient_data(row): ... (đã xóa) -# def process_encounter_data(row, patient_id): ... (đã xóa) -# def process_measurement_data(row, patient_id, encounter_id): ... (đã xóa) -# def process_referral_data(row, patient_id, encounter_id): ... (đã xóa) -# def calculate_dob(age): ... (đã xóa - dùng trực tiếp age) -# def parse_date(date_str): ... (đã xóa - dùng helper _parse_datetime) +# Bạn có thể thêm các hàm xử lý CSV khác ở đây nếu cần +# Ví dụ: process_patient_csv, process_encounter_csv, ... diff --git a/encounter_measurements_sample.csv b/encounter_measurements_sample.csv new file mode 100644 index 0000000000000000000000000000000000000000..d4c26c169f6f70893a331023efa219eaf72a6c79 --- /dev/null +++ b/encounter_measurements_sample.csv @@ -0,0 +1,1065 @@ +patientID,encounterID,measurementDateTime,temperature,heart_rate,blood_pressure_systolic,blood_pressure_diastolic,oxygen_saturation,resp_rate,fio2,tidal_vol,end_tidal_co2,feed_vol,feed_vol_adm,fio2_ratio,insp_time,oxygen_flow_rate,peep,pip,sip,tidal_vol_actual,tidal_vol_kg,tidal_vol_spon,bmi,referral +P-10001,E-10001-01,2025-04-16T17:27:19.747106,37.1,72,142,91,99.2,17,21.0,484,,,,,,,6,28,25,440,8.4,,37.0,1 +P-10001,E-10001-01,2025-04-16T18:39:57.166076,36.0,77,136,81,96.3,12,21.0,354,43.9,221,138,,1.1,,,35,27,339,6.1,,37.0, +P-10001,E-10001-01,2025-04-16T20:54:25.424493,36.3,75,133,86,98.5,20,21.0,366,,,,,1.3,,,28,,329,6.9,,37.0, +P-10001,E-10001-01,2025-04-16T19:06:33.949065,36.9,68,132,79,99.3,14,21.0,446,37.1,216,165,,,,4,,,446,7.7,,37.0,1 +P-10001,E-10001-01,2025-04-16T19:35:37.207372,37.0,83,125,85,96.6,18,21.0,393,,,,,,,,32,32,384,6.8,,37.0, +P-10001,E-10001-01,2025-04-17T02:22:53.394803,36.2,73,134,75,99.5,12,21.0,450,39.4,,,,,,4,,,425,8.4,,37.0,1 +P-10001,E-10001-01,2025-04-16T21:10:12.239405,36.0,108,158,105,99.3,15,30.7,448,,,,0.31,0.9,8,,,,415,7.8,,37.0, +P-10001,E-10001-01,2025-04-17T06:51:08.825106,36.8,72,128,90,98.3,17,21.0,414,,,,,,,,26,26,376,7.8,,37.0, +P-10001,E-10001-01,2025-04-17T09:21:54.347076,36.9,68,141,82,98.3,19,30.1,543,,,,0.3,,7,6,27,,506,10.2,,37.0, +P-10001,E-10001-01,2025-04-16T22:12:50.484655,36.8,76,137,77,91.1,18,21.0,352,39.7,,,,1.2,,8,,,326,6.1,77,37.0, +P-10001,E-10001-02,2025-04-14T17:27:19.747106,36.1,71,136,85,96.4,17,21.0,471,43.4,465,399,,1.4,,,,,433,8.8,,37.0, +P-10001,E-10001-02,2025-04-14T18:46:50.881197,37.0,76,135,84,99.4,15,21.0,367,,,,,,,5,31,30,342,6.9,,37.0, +P-10001,E-10001-02,2025-04-14T19:02:20.006804,36.2,73,130,81,97.4,18,21.0,478,44.8,,,,1.0,,4,,,460,9.0,,37.0,1 +P-10001,E-10001-02,2025-04-14T19:28:39.705507,36.9,70,142,85,97.4,15,21.0,531,,,,,,,,26,,491,9.2,,37.0, +P-10001,E-10001-02,2025-04-14T22:18:49.517753,36.6,81,140,84,96.3,17,21.0,429,,,,,,,,,,420,7.4,,37.0, +P-10001,E-10001-02,2025-04-14T21:51:32.760000,36.4,76,128,76,98.4,12,36.9,368,,,,0.37,1.2,6,,35,,351,6.4,,37.0,1 +P-10001,E-10001-02,2025-04-14T22:51:15.262578,37.0,82,158,86,96.6,18,21.0,523,,,,,1.2,,,,,520,9.1,80,37.0,1 +P-10001,E-10001-02,2025-04-15T06:03:41.769816,36.9,77,133,86,98.1,19,21.0,405,,,,,1.5,,,,,372,7.6,62,37.0,1 +P-10001,E-10001-02,2025-04-15T09:16:19.918117,37.0,83,128,92,97.0,15,21.0,408,38.8,,,,0.9,,,,,391,7.7,,37.0,1 +P-10001,E-10001-02,2025-04-15T00:55:54.497538,36.1,105,133,94,98.7,20,21.0,480,40.1,,,,0.9,,,29,,479,9.0,,37.0, +P-10001,E-10001-02,2025-04-15T07:14:21.192850,36.4,84,135,85,97.0,17,49.0,520,42.4,154,108,0.49,1.3,9,4,34,27,517,9.8,,37.0, +P-10001,E-10001-02,2025-04-14T23:06:52.500388,37.2,70,140,89,97.7,14,21.0,395,,,,,,,8,,,374,7.4,,37.0,1 +P-10001,E-10001-02,2025-04-15T17:06:53.969753,37.0,88,135,81,97.2,15,21.0,442,,316,176,,0.8,,5,,,405,8.3,,37.0, +P-10001,E-10001-02,2025-04-15T10:49:14.132686,37.0,77,138,89,98.7,20,21.0,524,,,,,,,,,,484,9.8,,37.0, +P-10002,E-10002-01,2025-04-13T17:27:19.747106,37.2,72,153,104,99.1,18,21.0,373,37.8,,,,,,,,,370,5.8,,28.1, +P-10002,E-10002-01,2025-04-13T19:16:13.202700,37.1,84,149,91,96.2,17,21.0,380,,,,,,,,,,355,5.5,,28.1, +P-10002,E-10002-01,2025-04-13T21:06:53.070893,37.1,111,134,79,98.3,19,21.0,527,41.6,321,101,,1.3,,6,27,21,509,8.1,,28.1, +P-10002,E-10002-01,2025-04-13T22:53:14.146976,37.0,70,141,82,97.4,13,50.9,450,,456,244,0.51,1.4,4,,,,427,6.5,,28.1,1 +P-10002,E-10002-01,2025-04-13T22:18:42.092586,37.2,70,141,89,97.5,18,21.0,461,,118,81,,,,5,,,414,7.1,,28.1,1 +P-10002,E-10002-01,2025-04-13T20:58:25.302447,36.6,77,135,85,99.0,14,21.0,432,38.3,,,,,,5,25,,422,6.7,,28.1,1 +P-10002,E-10002-01,2025-04-13T20:33:50.275619,37.0,85,143,95,98.1,15,21.0,486,35.5,,,,1.4,,,32,30,448,7.0,25,28.1, +P-10002,E-10002-01,2025-04-14T04:36:55.698193,36.1,76,132,95,96.5,14,41.2,498,44.3,,,0.41,1.4,5,,30,,451,7.2,78,28.1,1 +P-10002,E-10002-01,2025-04-13T23:22:11.113842,36.8,75,147,105,98.9,17,21.0,490,38.6,,,,,,,,,466,7.1,36,28.1, +P-10002,E-10002-01,2025-04-14T06:41:25.217321,37.4,83,129,95,97.7,17,21.0,361,,,,,,,,,,339,5.2,,28.1, +P-10002,E-10002-01,2025-04-14T12:10:55.960699,36.4,83,141,95,97.7,12,35.8,412,,,,0.36,1.2,7,5,,,377,5.9,,28.1, +P-10002,E-10002-01,2025-04-14T12:15:15.766251,36.9,73,126,88,98.0,18,21.0,454,,,,,1.5,,,,,450,6.5,27,28.1, +P-10002,E-10002-01,2025-04-14T07:01:01.472873,36.8,77,135,79,96.5,17,21.0,507,36.2,237,168,,,,,29,,466,7.3,41,28.1, +P-10002,E-10002-01,2025-04-14T02:37:16.570498,36.1,85,126,98,97.5,18,21.0,498,38.8,,,,1.0,,,32,24,465,7.7,,28.1, +P-10002,E-10002-01,2025-04-14T09:52:44.209457,36.6,74,135,99,99.4,12,21.0,357,44.7,362,205,,1.3,,8,,,352,5.5,,28.1, +P-10002,E-10002-02,2025-04-12T17:27:19.747106,36.3,66,123,98,96.8,15,21.0,511,41.5,,,,,,4,,,499,7.9,80,28.1, +P-10002,E-10002-02,2025-04-12T19:19:29.410120,36.3,73,131,84,98.7,16,21.0,411,42.2,,,,,,,30,,410,5.9,,28.1,1 +P-10002,E-10002-02,2025-04-12T21:11:38.693335,36.4,69,124,89,97.3,19,21.0,367,41.8,,,,,,7,25,,342,5.7,,28.1,1 +P-10002,E-10002-02,2025-04-12T20:16:56.543139,36.1,79,135,82,97.0,14,21.0,370,41.0,256,141,,1.4,,,,,350,5.3,,28.1,1 +P-10002,E-10002-02,2025-04-12T23:03:50.762045,36.7,79,132,94,98.4,13,21.0,467,,,,,1.3,,8,26,,449,6.7,,28.1,1 +P-10002,E-10002-02,2025-04-12T22:25:53.808120,36.1,79,141,86,98.3,15,21.0,495,41.8,,,,1.4,,,,,466,7.1,31,28.1, +P-10002,E-10002-02,2025-04-12T21:55:54.626184,37.1,77,130,93,96.3,15,21.0,437,44.9,,,,1.0,,,26,,403,6.7,,28.1, +P-10002,E-10002-02,2025-04-13T03:31:17.213800,37.1,76,137,81,96.8,17,21.0,395,,163,111,,,,4,33,30,394,5.7,,28.1, +P-10002,E-10002-02,2025-04-12T23:58:30.896879,36.9,66,144,93,96.5,20,21.0,425,,300,,,,,,,,424,6.1,,28.1,1 +P-10002,E-10002-02,2025-04-13T04:26:15.590689,37.0,66,146,90,96.8,13,21.0,541,,,,,,,,,,502,8.3,26,28.1,1 +P-10002,E-10002-02,2025-04-13T06:16:42.089605,36.6,77,140,88,97.5,15,21.0,439,,385,249,,,,6,25,,439,6.3,,28.1, +P-10003,E-10003-01,2025-04-13T17:27:19.747106,37.0,92,136,79,97.6,14,21.0,439,35.0,,,,1.2,,8,,,421,8.2,,37.3, +P-10003,E-10003-01,2025-04-13T18:39:58.476831,37.6,76,153,90,96.3,18,21.0,367,39.4,,,,,,6,,,338,6.3,,37.3, +P-10003,E-10003-01,2025-04-13T18:50:30.435986,36.3,91,134,90,99.5,12,21.0,549,,264,,,,,,,,543,10.2,,37.3, +P-10003,E-10003-01,2025-04-13T21:11:42.228199,37.1,111,165,89,96.2,13,21.0,417,42.9,,,,1.5,,,,,389,7.8,75,37.3, +P-10003,E-10003-01,2025-04-14T00:35:55.679047,37.4,94,122,90,98.7,13,21.0,539,37.3,,,,,,,,,504,10.1,,37.3,1 +P-10003,E-10003-01,2025-04-13T20:10:18.731487,37.0,75,150,92,96.4,12,21.0,391,,,,,1.3,,,,,367,6.7,,37.3,1 +P-10003,E-10003-01,2025-04-14T01:59:21.203252,37.6,57,132,76,96.3,12,21.0,459,38.9,,,,,,,,,448,8.6,80,37.3, +P-10003,E-10003-01,2025-04-13T23:46:52.481977,37.4,99,148,90,96.2,14,21.0,498,,,,,1.4,,,,,471,8.6,,37.3, +P-10004,E-10004-01,2025-04-15T17:27:19.747106,36.7,74,112,71,99.0,14,21.0,367,37.0,,,,,,8,25,,356,5.6,,19.3, +P-10004,E-10004-01,2025-04-15T18:48:40.454703,36.3,82,121,83,98.2,20,21.0,371,,,,,1.0,,5,,,339,6.1,21,19.3,1 +P-10004,E-10004-01,2025-04-15T19:37:27.918546,36.8,83,116,70,97.8,15,21.0,356,,,,,1.0,,,,,342,5.4,,19.3, +P-10004,E-10004-01,2025-04-15T22:45:06.361775,36.5,83,144,81,96.2,18,21.0,371,,,,,1.5,,4,32,22,362,6.1,,19.3, +P-10004,E-10004-01,2025-04-15T21:51:09.428096,37.0,72,118,75,95.2,16,21.0,462,37.4,453,297,,,,4,,,422,7.6,,19.3,1 +P-10004,E-10004-01,2025-04-16T01:52:49.078022,36.6,74,125,75,97.8,15,52.3,476,,,,0.52,0.8,10,,,,475,7.3,,19.3, +P-10004,E-10004-01,2025-04-15T23:03:34.833966,37.1,74,114,80,98.5,16,21.0,365,37.3,,,,1.3,,,29,28,346,6.0,,19.3, +P-10004,E-10004-01,2025-04-16T05:08:53.420918,36.6,70,135,82,97.3,19,21.0,486,40.2,,,,,,,,,455,7.4,,19.3, +P-10004,E-10004-01,2025-04-16T06:15:16.064019,36.4,68,112,72,99.4,18,21.0,500,,,,,,,,,,499,7.6,42,19.3, +P-10004,E-10004-01,2025-04-16T10:02:52.920807,36.1,79,112,79,97.9,19,21.0,436,39.5,,,,1.4,,8,,,398,7.1,,19.3, +P-10004,E-10004-01,2025-04-16T11:44:12.677564,36.9,66,120,77,98.0,13,21.0,368,,,,,0.9,,6,25,,349,5.6,,19.3, +P-10004,E-10004-01,2025-04-16T05:41:19.979333,37.0,83,134,92,98.5,19,21.0,420,,241,212,,,,4,,,404,6.4,,19.3, +P-10004,E-10004-01,2025-04-16T07:19:11.320735,37.0,66,130,86,98.2,13,21.0,464,,,,,0.9,,5,,,423,7.6,66,19.3, +P-10004,E-10004-02,2025-04-07T17:27:19.747106,36.5,65,137,82,96.8,14,21.0,485,39.9,,,,,,,,,485,7.9,,19.3, +P-10004,E-10004-02,2025-04-07T18:50:55.303194,36.0,83,125,71,94.0,17,21.0,392,,,,,0.8,,,,,370,6.4,,19.3, +P-10004,E-10004-02,2025-04-07T20:11:39.640862,37.2,77,110,79,98.2,16,21.0,538,38.9,,,,1.4,,6,30,29,536,8.2,77,19.3, +P-10004,E-10004-02,2025-04-07T21:07:51.609397,36.5,83,121,76,97.7,13,47.3,371,42.9,,,0.47,,5,,31,26,362,5.7,,19.3, +P-10004,E-10004-02,2025-04-08T00:46:46.738650,36.8,77,142,78,96.6,17,21.0,515,38.0,,,,,,,,,468,8.4,,19.3, +P-10004,E-10004-02,2025-04-07T20:01:01.319450,37.1,65,125,75,99.4,19,21.0,421,39.1,,,,0.9,,4,,,416,6.4,,19.3, +P-10004,E-10004-02,2025-04-08T04:06:50.993367,36.0,65,119,76,99.2,13,21.0,382,,,,,,,,27,,367,6.2,,19.3,1 +P-10004,E-10004-02,2025-04-07T21:39:51.194043,36.8,75,137,91,97.9,20,21.0,440,42.4,,,,,,,,,420,6.7,30,19.3, +P-10004,E-10004-02,2025-04-08T06:56:10.753944,36.7,67,111,79,96.4,19,21.0,478,41.4,,,,,,,,,456,7.3,,19.3, +P-10004,E-10004-02,2025-04-08T05:43:26.343170,36.2,83,122,80,97.8,12,21.0,361,,,,,,,,35,28,324,5.9,,19.3, +P-10004,E-10004-02,2025-04-08T06:12:07.056263,36.1,79,113,74,96.5,15,21.0,385,,,,,1.4,,,,,379,6.3,,19.3, +P-10004,E-10004-02,2025-04-08T08:05:09.717776,36.4,82,124,70,92.6,13,21.0,401,43.0,,,,1.4,,,,,387,6.6,,19.3, +P-10004,E-10004-02,2025-04-08T11:01:05.591907,36.5,81,118,79,99.0,19,21.0,356,,322,222,,,,,,,327,5.8,,19.3, +P-10004,E-10004-03,2025-04-07T17:27:19.747106,36.3,83,123,75,99.5,16,21.0,447,36.1,,,,,,8,33,25,421,7.3,,19.3, +P-10004,E-10004-03,2025-04-07T18:00:47.081728,37.8,71,119,76,97.3,18,21.0,430,43.2,,,,,,,34,32,414,6.6,,19.3,1 +P-10004,E-10004-03,2025-04-07T21:03:56.619664,36.3,67,113,74,94.4,13,27.8,417,,,,0.28,,6,,,,377,6.8,,19.3, +P-10004,E-10004-03,2025-04-07T20:16:20.304088,37.0,82,111,85,98.1,13,21.0,421,,358,99,,1.3,,7,27,21,414,6.4,,19.3,1 +P-10004,E-10004-03,2025-04-07T22:47:28.736180,36.9,75,116,84,96.1,17,21.0,496,37.7,,,,1.1,,8,,,465,8.1,,19.3,1 +P-10004,E-10004-03,2025-04-07T23:27:41.131222,37.0,70,110,82,97.3,16,21.0,459,41.2,398,263,,1.4,,,25,22,452,7.5,,19.3, +P-10004,E-10004-03,2025-04-07T22:47:25.320066,36.1,72,111,75,98.1,16,21.0,455,40.7,,,,0.9,,,,,428,7.4,,19.3, +P-10004,E-10004-03,2025-04-07T20:58:45.554946,36.2,69,118,80,99.1,15,21.0,426,,,,,,,,,,413,7.0,,19.3, +P-10005,E-10005-01,2025-04-14T17:27:19.747106,36.8,73,139,81,98.5,12,53.2,420,36.4,430,393,0.53,,4,,,,400,6.7,,31.7, +P-10005,E-10005-01,2025-04-14T19:05:38.695067,36.1,78,126,86,96.6,12,21.0,484,36.1,,,,0.8,,,26,,453,7.7,,31.7, +P-10005,E-10005-01,2025-04-14T21:22:49.994556,36.4,70,138,95,97.2,20,21.0,532,,,,,,,,,,528,7.9,,31.7, +P-10005,E-10005-01,2025-04-14T21:46:27.464175,36.5,82,142,85,97.2,19,21.0,459,38.6,304,298,,0.9,,,,,415,6.8,,31.7,1 +P-10005,E-10005-01,2025-04-14T20:52:47.796484,36.2,84,135,83,98.9,19,21.0,477,,,,,0.9,,,,,432,7.1,,31.7,1 +P-10005,E-10005-01,2025-04-15T01:05:27.095615,36.2,70,140,80,97.1,15,26.5,438,,407,,0.27,1.1,2,6,25,25,429,6.9,,31.7, +P-10005,E-10005-01,2025-04-14T23:12:20.366645,36.5,102,141,90,94.9,20,56.6,545,36.9,350,,0.57,1.5,9,5,25,24,495,8.6,,31.7, +P-10005,E-10005-01,2025-04-15T03:32:17.883164,36.0,70,135,96,98.7,13,21.0,479,41.2,160,106,,1.1,,,26,25,460,7.6,,31.7,1 +P-10005,E-10005-01,2025-04-15T00:11:22.668805,36.6,77,139,76,99.0,12,37.2,419,,,,0.37,,7,,30,24,398,6.6,,31.7, +P-10005,E-10005-01,2025-04-15T01:52:32.416451,36.6,50,131,90,97.3,13,21.0,490,41.3,109,101,,1.4,,,,,467,7.8,29,31.7, +P-10005,E-10005-01,2025-04-15T12:15:23.882939,36.9,80,130,95,96.5,16,21.0,443,40.4,,,,,,,,,441,6.6,70,31.7, +P-10005,E-10005-01,2025-04-15T12:29:09.846631,36.0,72,133,84,99.4,13,21.0,441,36.1,,,,1.3,,8,,,409,7.0,,31.7, +P-10005,E-10005-02,2025-04-09T17:27:19.747106,36.4,83,133,85,97.5,19,21.0,500,41.6,237,101,,,,8,,,450,7.9,,31.7, +P-10005,E-10005-02,2025-04-09T18:32:07.040044,37.3,79,159,95,97.1,20,21.0,542,,,,,0.9,,5,34,,509,8.6,,31.7, +P-10005,E-10005-02,2025-04-09T20:04:05.150213,36.7,82,140,82,96.9,15,21.0,464,35.3,273,183,,,,4,25,,451,7.4,,31.7, +P-10005,E-10005-02,2025-04-09T22:50:42.367632,36.3,85,137,81,98.3,17,21.0,459,39.5,,,,1.2,,6,,,415,7.3,,31.7, +P-10005,E-10005-02,2025-04-09T21:26:58.272310,36.7,71,126,86,95.6,17,21.0,390,,,,,1.3,,4,26,,354,5.8,,31.7, +P-10005,E-10005-02,2025-04-10T02:10:27.335042,37.0,79,167,96,98.6,19,21.0,453,,,,,,,8,,,440,6.7,,31.7,1 +P-10005,E-10005-02,2025-04-09T23:39:07.526507,36.3,77,136,86,99.1,17,21.0,469,35.0,,,,1.4,,6,,,426,6.9,,31.7, +P-10005,E-10005-02,2025-04-10T05:00:31.520130,36.8,70,141,93,97.6,12,21.0,435,,,,,1.1,,,,,403,6.4,,31.7, +P-10005,E-10005-02,2025-04-10T01:21:35.067375,36.5,52,137,80,98.5,15,21.0,366,,,,,,,,28,27,365,5.4,,31.7, +P-10005,E-10005-02,2025-04-10T03:57:46.429069,36.7,82,163,97,93.8,17,21.0,402,41.1,,,,1.2,,6,,,379,6.4,,31.7, +P-10005,E-10005-02,2025-04-10T08:35:40.403966,36.0,73,130,87,96.2,12,46.2,359,,478,432,0.46,,5,,,,351,5.7,,31.7, +P-10006,E-10006-01,2025-04-15T17:27:19.748194,36.5,84,116,73,96.4,12,21.0,542,,349,192,,,,,,,512,8.2,25,30.2,1 +P-10006,E-10006-01,2025-04-15T18:51:03.918037,37.0,68,116,78,96.2,19,21.0,371,37.3,,,,1.4,,5,,,360,5.6,,30.2, +P-10006,E-10006-01,2025-04-15T19:48:44.126579,36.5,67,115,79,96.5,14,21.0,459,,225,86,,1.5,,6,,,423,7.5,,30.2,1 +P-10006,E-10006-01,2025-04-15T21:25:38.463183,36.2,100,117,71,97.4,15,27.1,488,,,,0.27,1.0,8,,35,29,446,7.9,,30.2,1 +P-10006,E-10006-01,2025-04-15T19:33:43.236016,37.6,79,130,84,97.9,19,21.0,360,,,,,1.4,,,,,342,5.8,,30.2,1 +P-10006,E-10006-01,2025-04-16T02:34:36.821707,36.9,82,130,93,96.4,18,21.0,473,,153,112,,,,,27,20,444,7.2,,30.2,1 +P-10006,E-10006-01,2025-04-16T05:00:03.780489,36.2,57,112,79,98.0,19,21.0,368,,334,118,,,,,,,334,6.0,,30.2, +P-10006,E-10006-01,2025-04-15T23:04:33.867668,36.2,67,141,90,98.6,20,21.0,436,,331,199,,,,,,,435,6.6,,30.2, +P-10006,E-10006-01,2025-04-15T22:40:56.316486,36.9,71,116,76,96.1,20,21.0,531,,,,,1.4,,6,,,509,8.6,,30.2, +P-10006,E-10006-01,2025-04-16T06:48:06.927685,36.6,66,117,78,98.8,18,42.3,483,,106,101,0.42,1.1,3,,,,481,7.3,,30.2, +P-10007,E-10007-01,2025-04-16T17:27:19.748194,37.1,93,129,80,98.9,17,21.0,365,,,,,1.1,,,,,342,6.2,,21.7, +P-10007,E-10007-01,2025-04-16T18:48:45.604061,37.8,103,130,86,98.7,15,26.0,397,,195,97,0.26,,4,,30,25,373,6.3,,21.7, +P-10007,E-10007-01,2025-04-16T18:48:46.099958,36.7,72,129,94,96.8,17,21.0,436,,,,,1.0,,5,,,432,6.9,,21.7, +P-10007,E-10007-01,2025-04-16T22:21:27.613249,36.4,100,137,83,97.9,14,21.0,455,40.4,,,,,,7,,,414,7.2,,21.7, +P-10007,E-10007-01,2025-04-17T00:02:04.985052,37.6,94,127,78,97.7,17,21.0,538,44.1,,,,1.1,,4,27,21,496,9.1,,21.7,1 +P-10007,E-10007-02,2025-04-09T17:27:19.748194,36.9,79,136,89,96.8,17,28.9,487,35.9,,,0.29,,9,,,,474,7.7,,21.7, +P-10007,E-10007-02,2025-04-09T18:16:51.501748,37.1,94,162,98,97.5,13,21.0,405,,,,,1.4,,,,,375,6.9,27,21.7, +P-10007,E-10007-02,2025-04-09T21:25:17.236279,37.1,98,141,80,96.5,12,29.0,498,,,,0.29,1.4,3,4,29,20,470,8.4,,21.7, +P-10007,E-10007-02,2025-04-09T21:29:46.406558,37.1,99,157,88,98.4,19,21.0,388,40.6,,,,,,7,,,355,6.6,,21.7, +P-10007,E-10007-02,2025-04-09T20:32:50.421538,37.2,115,152,91,93.5,19,21.0,486,,363,347,,1.4,,6,26,,454,7.7,,21.7, +P-10007,E-10007-02,2025-04-10T02:13:50.406457,37.2,93,130,85,97.3,15,21.0,402,35.8,,,,1.0,,5,34,,388,6.8,,21.7, +P-10007,E-10007-02,2025-04-10T01:37:53.721006,36.9,86,140,94,97.5,16,21.0,518,,,,,0.9,,,28,26,482,8.8,,21.7, +P-10007,E-10007-02,2025-04-10T02:08:37.690064,37.4,93,125,87,97.7,15,21.0,385,,,,,,,,,,350,6.1,,21.7,1 +P-10007,E-10007-02,2025-04-10T01:46:02.055191,37.3,100,142,107,96.6,12,21.0,447,37.0,,,,1.1,,8,,,446,7.6,70,21.7,1 +P-10007,E-10007-02,2025-04-10T03:27:34.399902,37.5,92,140,82,96.6,13,42.5,546,37.7,,,0.42,0.9,10,4,,,544,8.6,,21.7, +P-10007,E-10007-02,2025-04-10T06:17:30.382456,37.1,99,141,96,97.6,17,21.0,515,43.1,,,,1.4,,4,31,21,485,8.7,,21.7, +P-10007,E-10007-02,2025-04-09T23:34:30.360540,38.0,78,147,93,97.5,16,21.0,457,,,,,1.1,,,,,441,7.2,,21.7, +P-10007,E-10007-02,2025-04-10T06:16:36.024930,38.1,102,140,86,96.5,19,21.0,452,,374,,,1.3,,6,,,438,7.7,75,21.7, +P-10007,E-10007-03,2025-04-14T17:27:19.748194,37.1,97,147,97,98.1,15,21.0,450,38.0,,,,,,7,,,421,7.6,,21.7,1 +P-10007,E-10007-03,2025-04-14T19:26:10.131785,36.9,95,132,92,97.6,19,21.0,479,,,,,1.3,,,,,437,8.1,,21.7,1 +P-10007,E-10007-03,2025-04-14T18:32:34.800811,36.8,74,137,91,96.3,12,21.0,532,36.6,,,,1.3,,,,,484,8.4,,21.7,1 +P-10007,E-10007-03,2025-04-14T22:45:17.076323,36.6,89,142,88,98.4,18,56.7,447,,,,0.57,,4,,,,440,7.0,,21.7,1 +P-10007,E-10007-03,2025-04-14T20:39:45.799677,37.1,99,141,91,98.1,19,21.0,415,,,,,1.2,,4,28,,374,6.5,24,21.7,1 +P-10007,E-10007-03,2025-04-15T02:21:47.692214,37.4,93,130,90,97.5,20,21.0,356,38.0,284,104,,,,6,35,,324,6.0,59,21.7, +P-10007,E-10007-03,2025-04-14T20:46:45.417214,37.2,76,127,94,96.8,18,21.0,535,44.6,,,,,,,,,509,8.4,,21.7, +P-10007,E-10007-03,2025-04-15T03:23:23.810195,37.5,92,157,103,97.0,16,21.0,517,40.7,,,,1.1,,8,25,,488,8.8,,21.7, +P-10007,E-10007-03,2025-04-15T04:58:56.991440,36.7,80,156,98,94.6,20,45.8,416,37.1,,,0.46,0.8,7,,,,388,6.6,57,21.7, +P-10007,E-10007-03,2025-04-15T08:53:44.624573,36.6,98,133,92,99.2,12,21.0,472,,,,,,,,29,29,432,8.0,,21.7,1 +P-10007,E-10007-03,2025-04-15T09:14:39.073886,37.9,96,131,94,99.0,14,24.7,491,43.1,,,0.25,1.2,2,7,,,462,7.7,,21.7,1 +P-10007,E-10007-03,2025-04-15T11:09:43.252696,37.9,82,138,85,97.0,20,21.0,388,,,,,1.4,,,,,378,6.6,,21.7, +P-10007,E-10007-03,2025-04-15T02:39:55.104195,36.9,89,135,84,99.2,14,21.0,535,,,,,1.2,,,,,490,9.1,,21.7, +P-10007,E-10007-03,2025-04-15T08:47:38.124961,37.4,82,143,91,97.5,16,21.0,509,,247,206,,1.1,,,,,488,8.6,33,21.7, +P-10007,E-10007-03,2025-04-15T05:01:14.530239,37.7,86,136,97,99.4,14,21.0,383,,,,,1.2,,,35,32,383,6.0,,21.7, +P-10008,E-10008-01,2025-04-12T17:27:19.748194,36.4,72,137,90,98.8,18,46.4,359,40.8,,,0.46,1.5,4,,,,353,5.8,,27.2, +P-10008,E-10008-01,2025-04-12T19:26:33.640965,36.6,44,136,81,96.1,12,21.0,524,,,,,1.4,,6,,,511,9.1,77,27.2,1 +P-10008,E-10008-01,2025-04-12T21:15:16.330576,37.1,79,136,88,99.4,16,21.0,382,,,,,,,,,,375,6.1,,27.2,1 +P-10008,E-10008-01,2025-04-12T22:43:46.594738,37.0,99,138,87,98.5,13,21.0,484,,,,,1.5,,,,,472,8.4,,27.2, +P-10008,E-10008-01,2025-04-12T21:32:25.118724,36.8,107,126,83,99.4,19,21.0,412,36.1,,,,1.4,,,34,,388,7.1,,27.2,1 +P-10008,E-10008-01,2025-04-13T02:35:54.828592,37.0,77,134,96,97.3,12,58.0,378,,,,0.58,,3,,,,372,6.1,,27.2, +P-10008,E-10008-01,2025-04-13T00:46:23.255113,36.2,81,131,91,96.1,19,21.0,483,,,,,,,,,,462,8.4,,27.2, +P-10008,E-10008-01,2025-04-12T23:14:55.391644,36.7,68,144,91,99.2,20,21.0,369,,,,,1.1,,,35,26,352,5.9,,27.2,1 +P-10008,E-10008-01,2025-04-13T03:20:04.490666,36.7,63,123,79,95.4,14,21.0,525,42.4,,,,1.5,,7,,,521,9.1,28,27.2, +P-10008,E-10008-01,2025-04-13T01:23:36.108683,36.1,67,138,77,98.0,15,35.6,396,,112,81,0.36,1.3,8,5,30,25,365,6.9,,27.2, +P-10008,E-10008-01,2025-04-13T12:48:07.568961,36.7,78,136,90,98.7,12,21.0,403,,,,,,,,,,381,6.5,,27.2, +P-10008,E-10008-01,2025-04-13T13:09:12.168943,37.0,69,142,89,98.2,18,21.0,421,,,,,,,,,,406,7.3,76,27.2, +P-10008,E-10008-01,2025-04-13T12:15:53.701221,37.1,76,141,91,96.8,20,21.0,354,,,,,1.2,,4,,,331,5.7,,27.2, +P-10008,E-10008-01,2025-04-13T02:45:26.750368,36.2,73,151,89,98.2,19,34.7,530,37.6,333,228,0.35,1.1,2,,34,28,521,9.2,,27.2, +P-10008,E-10008-02,2025-04-15T17:27:19.748194,36.9,79,140,94,95.6,17,21.0,417,41.8,,,,,,,,,408,7.2,,27.2, +P-10008,E-10008-02,2025-04-15T18:22:21.762124,36.6,68,143,86,98.9,19,21.0,403,,,,,,,,33,27,376,6.5,66,27.2, +P-10008,E-10008-02,2025-04-15T20:39:41.304839,36.4,92,125,79,98.6,19,21.0,419,40.2,,,,,,,28,24,419,7.3,,27.2,1 +P-10008,E-10008-02,2025-04-15T20:20:37.803661,36.6,83,136,82,97.0,18,21.0,350,38.5,,,,0.9,,,30,25,339,5.6,,27.2, +P-10008,E-10008-02,2025-04-15T19:59:23.089488,37.0,71,141,86,98.9,18,40.4,494,44.4,,,0.4,,2,,31,21,474,8.6,30,27.2, +P-10008,E-10008-02,2025-04-16T02:50:50.849868,36.5,72,131,84,98.8,18,21.0,364,,,,,1.4,,,,,337,5.8,,27.2, +P-10008,E-10008-02,2025-04-16T01:52:27.546679,37.2,97,144,89,97.9,17,21.0,455,,,,,,,,,,431,7.9,,27.2, +P-10008,E-10008-02,2025-04-16T04:29:02.054951,36.5,76,134,88,98.2,16,21.0,393,43.2,,,,,,8,,,361,6.3,,27.2, +P-10008,E-10008-02,2025-04-16T05:41:10.234350,36.7,88,129,82,97.0,13,21.0,361,,489,124,,1.1,,7,,,324,6.3,,27.2, +P-10008,E-10008-02,2025-04-16T07:54:18.921224,37.2,66,130,81,97.7,12,54.6,390,,,,0.55,,2,,,,370,6.3,,27.2, +P-10009,E-10009-01,2025-04-13T17:27:19.748194,36.3,86,141,86,99.4,13,26.8,358,36.6,439,210,0.27,1.5,9,,,,340,6.8,,28.0,1 +P-10009,E-10009-01,2025-04-13T18:52:32.921610,36.7,112,147,98,98.3,13,21.0,452,,,,,,,,,,410,8.0,,28.0,1 +P-10009,E-10009-01,2025-04-13T19:37:18.443523,36.6,94,125,76,97.5,13,21.0,515,40.0,,,,,,,,,508,9.9,,28.0,1 +P-10009,E-10009-01,2025-04-13T19:08:22.825773,36.7,83,132,92,99.1,16,26.5,466,,,,0.27,,5,,25,,446,8.2,,28.0, +P-10009,E-10009-01,2025-04-13T23:55:10.276250,36.9,62,136,80,98.9,16,21.0,511,38.2,293,145,,,,6,26,20,478,9.0,,28.0, +P-10009,E-10009-01,2025-04-14T02:50:23.252512,37.1,69,132,89,99.3,18,26.8,416,,,,0.27,1.3,5,,31,26,400,8.0,,28.0, +P-10009,E-10009-02,2025-04-11T17:27:19.748194,36.1,94,136,79,98.1,20,21.0,378,42.9,,,,0.9,,,,,350,7.2,,28.0, +P-10009,E-10009-02,2025-04-11T19:20:56.669349,36.7,95,157,100,98.7,18,21.0,503,,,,,1.4,,5,32,24,452,8.9,,28.0,1 +P-10009,E-10009-02,2025-04-11T18:51:21.068556,36.5,69,132,83,96.8,19,21.0,521,37.8,,,,1.0,,8,,,516,10.0,,28.0, +P-10009,E-10009-02,2025-04-11T19:47:00.527159,36.8,84,139,90,98.5,17,21.0,517,37.0,410,345,,1.3,,,,,512,9.9,58,28.0, +P-10009,E-10009-02,2025-04-11T22:51:38.344269,36.2,104,126,88,96.1,15,21.0,382,44.2,,,,1.3,,,,,366,7.3,,28.0, +P-10009,E-10009-02,2025-04-11T23:05:16.170033,37.0,97,135,83,99.2,15,54.4,415,36.0,,,0.54,0.9,9,,,,410,7.9,,28.0, +P-10009,E-10009-02,2025-04-12T04:00:27.280384,36.2,93,134,91,99.2,14,21.0,432,,,,,,,5,33,27,404,7.6,43,28.0, +P-10009,E-10009-02,2025-04-11T22:52:01.603438,36.8,68,136,91,96.7,13,21.0,492,,227,104,,1.0,,6,,,457,8.7,,28.0, +P-10009,E-10009-02,2025-04-12T05:58:07.306845,36.9,96,133,78,94.0,17,21.0,380,36.1,,,,1.0,,8,,,372,6.7,,28.0, +P-10010,E-10010-01,2025-04-15T17:27:19.748194,36.1,75,110,74,98.5,14,21.0,426,35.6,,,,,,8,,,397,6.4,,26.2, +P-10010,E-10010-01,2025-04-15T18:24:26.921813,37.1,82,115,75,97.0,12,21.0,470,,,,,1.2,,,,,433,7.1,,26.2, +P-10010,E-10010-01,2025-04-15T21:06:18.515605,36.0,77,140,80,98.6,18,21.0,546,44.1,,,,1.1,,5,,,540,7.7,,26.2, +P-10010,E-10010-01,2025-04-15T21:05:13.485304,36.3,78,116,73,97.9,20,21.0,517,,,,,,,8,,,511,7.3,,26.2, +P-10010,E-10010-01,2025-04-15T22:06:57.848341,36.2,68,110,77,98.5,14,21.0,426,37.5,143,142,,0.8,,6,,,425,6.4,,26.2,1 +P-10010,E-10010-01,2025-04-15T22:27:59.699458,36.1,72,113,73,96.0,17,43.3,473,42.1,,,0.43,1.4,9,,,,464,6.7,,26.2, +P-10010,E-10010-01,2025-04-16T02:17:49.416465,36.3,77,124,90,93.1,15,21.0,536,43.7,,,,1.0,,6,26,23,523,8.1,,26.2, +P-10010,E-10010-01,2025-04-15T22:14:54.336273,36.2,84,116,73,96.5,19,21.0,401,,443,,,1.0,,6,,,391,6.0,55,26.2, +P-10010,E-10010-01,2025-04-16T02:27:16.788369,36.0,81,117,77,96.7,19,21.0,542,,,,,1.0,,,,,493,7.6,,26.2, +P-10010,E-10010-01,2025-04-16T09:32:15.102702,36.4,51,118,85,97.2,18,21.0,423,,,,,1.1,,6,27,21,393,6.0,,26.2,1 +P-10010,E-10010-01,2025-04-16T00:17:06.577729,36.1,81,132,82,98.6,16,21.0,406,,,,,,,,30,20,389,5.7,,26.2, +P-10010,E-10010-01,2025-04-16T11:14:22.531196,36.9,102,115,83,98.0,20,21.0,541,,,,,,,,,,498,8.1,,26.2, +P-10010,E-10010-01,2025-04-16T16:18:46.682448,37.1,79,124,75,96.5,14,21.0,371,37.3,485,362,,,,,,,367,5.6,,26.2, +P-10010,E-10010-02,2025-04-12T17:27:19.748194,36.7,72,110,73,98.0,12,21.0,423,44.7,,,,0.9,,4,30,22,421,6.4,,26.2, +P-10010,E-10010-02,2025-04-12T19:14:15.669444,37.7,84,118,84,99.4,18,21.0,381,41.6,205,162,,1.1,,,,,374,5.7,,26.2,1 +P-10010,E-10010-02,2025-04-12T19:32:34.174031,37.2,82,127,94,97.9,13,21.0,492,,,,,1.3,,,,,451,6.9,61,26.2,1 +P-10010,E-10010-02,2025-04-12T20:52:02.508165,36.1,80,116,76,97.1,13,21.0,392,,236,157,,1.1,,,31,,354,5.5,,26.2,1 +P-10010,E-10010-02,2025-04-12T21:43:02.131305,36.4,74,119,75,99.3,18,21.0,537,,,,,,,8,35,31,491,7.6,79,26.2, +P-10010,E-10010-02,2025-04-12T20:07:45.841710,36.8,75,110,73,97.8,14,21.0,386,,,,,1.5,,7,,,381,5.8,,26.2, +P-10011,E-10011-01,2025-04-14T17:27:19.749207,36.7,68,135,81,98.4,16,21.0,528,,,,,,,4,26,24,512,10.3,73,41.9, +P-10011,E-10011-01,2025-04-14T18:07:55.091545,36.0,90,133,88,95.3,14,21.0,531,,155,139,,,,8,,,481,10.4,,41.9, +P-10011,E-10011-01,2025-04-14T20:19:30.662501,37.2,81,138,76,96.3,18,48.8,470,,,,0.49,,8,,,,427,10.1,,41.9, +P-10011,E-10011-01,2025-04-14T21:35:07.327916,37.2,73,133,78,99.3,18,21.0,353,,,,,1.4,,,,,352,6.9,,41.9,1 +P-10011,E-10011-01,2025-04-14T22:11:23.371517,36.7,70,142,93,98.4,14,21.0,479,,,,,,,,30,30,452,9.4,,41.9, +P-10011,E-10011-02,2025-04-07T17:27:19.749207,36.5,68,132,97,97.7,17,21.0,546,,,,,,,,,,509,11.7,77,41.9, +P-10011,E-10011-02,2025-04-07T18:36:54.028261,36.4,65,134,92,97.3,19,21.0,535,,,,,,,,,,516,10.5,21,41.9, +P-10011,E-10011-02,2025-04-07T18:29:20.936804,37.2,85,144,82,96.3,12,44.8,418,,,,0.45,,9,,27,27,388,8.2,,41.9, +P-10011,E-10011-02,2025-04-07T20:08:49.648898,37.1,73,131,91,96.7,18,21.0,523,,,,,1.3,,,28,,484,11.2,80,41.9, +P-10011,E-10011-02,2025-04-08T00:41:25.908821,36.2,70,142,97,93.4,15,21.0,476,40.2,,,,0.9,,,,,441,9.3,,41.9, +P-10011,E-10011-02,2025-04-07T23:45:27.519022,36.0,77,137,93,96.8,17,21.0,434,35.0,397,210,,,,5,,,419,9.3,,41.9,1 +P-10011,E-10011-02,2025-04-08T01:39:16.643642,37.1,78,146,102,98.5,16,21.0,403,36.6,290,243,,0.8,,4,29,27,379,7.9,,41.9,1 +P-10011,E-10011-02,2025-04-08T02:18:33.191139,36.8,75,137,80,94.2,18,51.0,452,42.2,477,,0.51,0.9,2,7,,,427,8.8,49,41.9,1 +P-10011,E-10011-02,2025-04-07T22:42:55.365822,36.9,67,128,96,97.8,14,21.0,422,,,,,,,,,,403,9.0,68,41.9, +P-10011,E-10011-02,2025-04-08T08:27:56.425802,36.3,79,123,90,98.3,18,56.8,528,36.7,,,0.57,1.2,3,4,,,525,11.3,49,41.9, +P-10011,E-10011-02,2025-04-08T10:12:27.277527,36.3,78,129,91,97.0,15,21.0,389,40.9,,,,1.2,,4,26,24,371,7.6,67,41.9, +P-10011,E-10011-02,2025-04-08T06:13:53.053972,36.9,58,131,78,98.3,16,46.2,417,,151,,0.46,,7,,33,26,407,8.2,,41.9,1 +P-10011,E-10011-02,2025-04-08T04:30:30.530358,36.4,84,142,80,97.1,14,21.0,513,42.5,,,,1.3,,6,,,502,11.0,43,41.9, +P-10011,E-10011-02,2025-04-08T15:55:15.694567,36.0,78,127,78,97.1,12,21.0,415,,,,,,,5,,,386,8.9,,41.9, +P-10011,E-10011-02,2025-04-08T01:42:14.320929,36.4,70,136,94,97.3,13,21.0,501,41.0,,,,,,,,,458,10.7,,41.9, +P-10012,E-10012-01,2025-04-14T17:27:19.749207,36.2,55,113,72,99.0,19,21.0,407,,,,,1.2,,,,,391,7.2,,30.6, +P-10012,E-10012-01,2025-04-14T18:41:17.373968,36.7,79,111,83,98.8,17,21.0,474,,256,247,,1.1,,,,,465,7.7,,30.6, +P-10012,E-10012-01,2025-04-14T19:49:35.532714,37.2,75,112,73,96.6,15,21.0,405,,,,,1.1,,8,,,390,6.6,40,30.6, +P-10012,E-10012-01,2025-04-14T19:19:32.490689,36.2,79,117,83,93.1,16,21.0,523,41.8,,,,1.4,,,,,520,8.5,,30.6, +P-10012,E-10012-01,2025-04-15T01:10:14.475717,36.6,75,118,82,99.3,19,51.0,485,,484,378,0.51,1.0,8,,,,480,8.6,27,30.6, +P-10012,E-10012-01,2025-04-15T01:37:02.293831,36.7,85,120,70,94.7,19,21.0,541,,,,,1.3,,,,,487,8.8,79,30.6, +P-10012,E-10012-01,2025-04-15T00:34:59.015904,36.7,85,111,82,98.0,13,21.0,439,,,,,0.9,,,,,406,7.7,,30.6, +P-10012,E-10012-01,2025-04-14T21:48:54.966390,36.3,77,122,73,97.0,12,21.0,540,41.7,,,,1.1,,,,,521,9.5,43,30.6,1 +P-10012,E-10012-01,2025-04-15T04:18:33.894015,36.1,65,132,90,96.2,18,21.0,505,43.0,,,,1.4,,7,,,475,8.9,,30.6, +P-10012,E-10012-01,2025-04-15T01:10:02.682121,37.0,71,117,78,97.4,13,21.0,377,,,,,,,,33,,374,6.2,,30.6, +P-10012,E-10012-01,2025-04-14T23:06:38.015205,37.1,67,112,85,98.3,16,21.0,425,,,,,1.1,,4,,,393,7.5,52,30.6, +P-10012,E-10012-01,2025-04-15T12:36:06.624320,36.1,73,133,82,96.4,12,21.0,540,44.4,149,118,,,,,,,497,8.8,23,30.6, +P-10012,E-10012-01,2025-04-15T04:06:26.998866,36.5,84,125,74,97.7,18,21.0,472,,,,,1.1,,,28,21,440,8.3,,30.6, +P-10012,E-10012-01,2025-04-15T13:56:27.270137,37.1,80,128,79,97.0,18,21.0,505,,,,,1.2,,8,,,493,8.9,50,30.6, +P-10013,E-10013-01,2025-04-15T17:27:19.749207,37.6,76,142,83,98.9,13,21.0,416,44.7,288,176,,1.4,,8,25,23,375,7.7,,30.2, +P-10013,E-10013-01,2025-04-15T18:59:05.953405,36.5,77,123,88,99.5,14,21.0,483,,,,,,,,29,,437,9.7,50,30.2,1 +P-10013,E-10013-01,2025-04-15T20:07:48.267279,37.4,89,159,100,96.4,19,21.0,368,,,,,,,,33,32,363,6.8,38,30.2, +P-10013,E-10013-01,2025-04-15T21:22:46.908509,37.4,66,153,94,97.6,14,21.0,352,40.4,,,,1.3,,,28,22,330,7.1,,30.2, +P-10013,E-10013-01,2025-04-15T21:06:52.498226,37.4,112,134,96,98.1,12,21.0,484,38.4,437,,,,,7,,,463,9.7,,30.2, +P-10013,E-10013-01,2025-04-15T21:35:07.203009,36.9,83,131,89,97.3,14,21.0,405,39.0,,,,1.1,,,,,380,8.1,32,30.2, +P-10013,E-10013-01,2025-04-16T01:03:33.021064,36.5,80,131,83,99.0,14,21.0,517,41.6,210,,,,,,30,,468,10.4,,30.2, +P-10013,E-10013-01,2025-04-15T22:43:00.056357,38.0,102,123,90,96.9,15,21.0,370,35.7,,,,,,,,,346,6.8,,30.2, +P-10013,E-10013-01,2025-04-16T02:58:21.411949,37.7,91,149,92,94.8,13,21.0,367,,,,,0.8,,,32,26,335,7.4,52,30.2, +P-10014,E-10014-01,2025-04-13T17:27:19.749207,36.7,96,136,89,97.2,14,21.0,523,38.3,,,,0.9,,6,26,26,520,7.3,,32.0, +P-10014,E-10014-01,2025-04-13T19:25:58.214211,37.5,107,129,82,93.9,18,25.6,465,35.6,,,0.26,,10,5,,,462,6.9,,32.0, +P-10014,E-10014-01,2025-04-13T19:38:56.455843,36.9,84,147,96,97.4,20,21.0,451,,357,89,,0.9,,,,,436,6.3,46,32.0, +P-10014,E-10014-01,2025-04-13T20:46:48.983706,37.2,74,130,83,97.9,14,21.0,487,,253,,,,,,,,480,7.3,,32.0,1 +P-10014,E-10014-01,2025-04-13T20:48:10.933778,37.2,84,128,96,98.8,17,21.0,459,42.1,475,225,,1.4,,,,,438,6.4,59,32.0,1 +P-10014,E-10014-01,2025-04-14T01:56:41.145457,37.4,74,136,91,96.6,20,21.0,469,43.3,,,,,,8,,,448,7.0,,32.0,1 +P-10014,E-10014-01,2025-04-13T20:32:01.282023,37.0,85,134,97,98.7,18,21.0,387,38.5,,,,,,,,,352,5.8,,32.0, +P-10014,E-10014-01,2025-04-14T02:48:27.693893,37.7,89,124,85,99.0,16,37.9,488,43.2,131,,0.38,0.8,2,,34,25,484,7.3,28,32.0, +P-10014,E-10014-01,2025-04-14T03:38:54.310294,37.6,82,136,85,98.4,17,21.0,413,,186,168,,,,5,28,28,397,6.2,,32.0, +P-10014,E-10014-01,2025-04-14T08:07:47.327602,37.7,89,142,83,97.5,16,21.0,410,43.0,,,,1.1,,,,,381,6.1,,32.0,1 +P-10014,E-10014-02,2025-04-13T17:27:19.749207,36.5,88,132,76,97.8,19,21.0,449,,,,,1.0,,6,34,29,408,6.3,,32.0,1 +P-10014,E-10014-02,2025-04-13T18:26:32.914303,37.3,83,132,76,97.7,14,21.0,531,,,,,1.3,,,,,508,7.4,,32.0, +P-10014,E-10014-02,2025-04-13T20:54:22.243295,37.5,96,144,82,96.2,15,21.0,488,,,,,0.8,,5,27,27,484,6.8,,32.0, +P-10014,E-10014-02,2025-04-13T20:59:13.693898,37.7,93,133,83,96.6,19,21.0,521,39.0,,,,,,,,,471,7.3,,32.0, +P-10014,E-10014-02,2025-04-13T23:22:41.223775,37.0,84,135,90,96.7,16,44.7,404,41.7,377,142,0.45,,3,,26,24,390,6.0,,32.0,1 +P-10014,E-10014-02,2025-04-14T00:48:00.737952,37.1,113,136,79,96.6,17,21.0,357,43.6,334,,,1.4,,,32,,321,5.0,,32.0, +P-10014,E-10014-02,2025-04-14T02:58:47.834628,37.1,64,146,90,96.4,20,21.0,459,37.1,,,,,,4,32,29,444,6.4,,32.0, +P-10014,E-10014-02,2025-04-14T06:37:21.468066,36.9,85,139,82,94.6,20,21.0,536,,358,,,,,,26,20,498,7.5,,32.0, +P-10014,E-10014-02,2025-04-14T03:57:45.125545,37.4,87,139,92,98.5,17,35.5,361,36.9,,,0.35,,6,,35,20,338,5.4,60,32.0,1 +P-10014,E-10014-02,2025-04-13T23:19:03.064552,36.9,88,133,93,98.4,20,21.0,449,,,,,1.4,,,,,449,6.3,,32.0, +P-10014,E-10014-02,2025-04-14T07:32:33.362241,37.1,82,129,96,97.9,15,21.0,428,,343,182,,1.1,,,28,27,423,6.0,,32.0, +P-10014,E-10014-02,2025-04-14T04:24:30.566404,37.6,100,160,102,98.8,15,21.0,374,,,,,,,,,,363,5.2,,32.0, +P-10014,E-10014-02,2025-04-14T06:43:43.205763,36.6,101,140,91,97.7,17,21.0,363,43.6,,,,1.2,,4,33,,359,5.4,,32.0, +P-10014,E-10014-02,2025-04-14T19:09:43.380957,37.3,91,131,91,99.3,17,21.0,408,44.4,,,,1.0,,,29,24,401,6.1,,32.0, +P-10014,E-10014-02,2025-04-14T13:18:13.477773,36.7,80,134,97,99.3,18,27.7,501,39.0,264,173,0.28,,9,,,,493,7.5,62,32.0, +P-10014,E-10014-03,2025-04-06T17:27:19.749207,37.7,88,137,97,96.1,20,25.1,465,,305,193,0.25,1.4,4,,,,447,6.9,,32.0, +P-10014,E-10014-03,2025-04-06T19:09:10.079247,37.5,88,145,98,97.5,20,55.1,413,,,,0.55,1.4,7,4,26,24,395,5.8,,32.0,1 +P-10014,E-10014-03,2025-04-06T21:19:01.001388,36.9,93,164,99,96.9,15,21.0,523,37.0,152,89,,1.4,,4,,,486,7.8,,32.0, +P-10014,E-10014-03,2025-04-06T22:06:40.543211,37.4,81,139,82,98.1,16,21.0,370,,,,,,,,25,21,338,5.5,,32.0, +P-10014,E-10014-03,2025-04-06T20:14:29.534147,37.3,78,155,102,98.8,12,42.8,538,,,,0.43,1.4,4,,,,524,8.0,,32.0, +P-10014,E-10014-03,2025-04-07T02:02:53.761639,38.0,78,147,82,96.6,15,21.0,399,39.3,,,,,,,,,392,6.0,,32.0,1 +P-10014,E-10014-03,2025-04-06T20:38:16.245842,37.2,99,143,88,96.2,14,21.0,480,,204,189,,1.0,,5,32,,458,7.2,,32.0, +P-10014,E-10014-03,2025-04-07T05:31:11.513745,37.2,89,151,95,98.8,16,21.0,386,44.0,,,,1.0,,,,,377,5.4,,32.0, +P-10014,E-10014-03,2025-04-07T01:39:32.706514,37.3,112,157,93,96.7,14,21.0,522,42.9,,,,1.4,,7,30,25,498,7.8,,32.0, +P-10014,E-10014-03,2025-04-06T22:19:34.392479,37.2,85,154,102,96.8,14,21.0,377,39.0,130,98,,1.1,,6,31,22,371,5.6,,32.0,1 +P-10014,E-10014-03,2025-04-07T06:58:39.088062,37.3,79,133,84,93.4,13,21.0,495,,396,293,,,,,,,474,6.9,,32.0, +P-10015,E-10015-01,2025-04-12T17:27:19.749207,37.0,108,123,86,99.0,16,21.0,385,,,,,0.9,,,,,365,7.4,,34.0, +P-10015,E-10015-01,2025-04-12T18:17:56.441727,36.3,50,134,99,98.5,19,21.0,513,36.8,,,,,,5,30,26,496,9.1,,34.0, +P-10015,E-10015-01,2025-04-12T19:25:15.437555,37.1,85,123,91,94.5,12,21.0,381,44.3,,,,1.3,,5,,,358,7.3,38,34.0,1 +P-10015,E-10015-01,2025-04-12T22:15:13.350725,36.5,85,126,91,97.8,12,21.0,441,36.5,217,110,,,,,,,406,8.5,,34.0, +P-10015,E-10015-01,2025-04-12T22:58:47.595750,36.8,82,144,77,96.9,20,21.0,424,,145,92,,,,7,26,26,394,8.2,,34.0, +P-10015,E-10015-01,2025-04-12T23:40:51.425292,37.2,74,135,92,99.2,20,21.0,432,,,,,1.3,,8,,,393,7.6,,34.0, +P-10015,E-10015-01,2025-04-13T00:54:42.579720,36.5,69,133,89,98.4,19,21.0,442,,,,,1.3,,,,,419,7.8,,34.0, +P-10015,E-10015-01,2025-04-13T04:32:20.179355,36.9,69,147,90,97.9,13,55.8,546,40.7,149,106,0.56,,6,5,33,33,541,10.5,,34.0, +P-10015,E-10015-01,2025-04-13T07:04:07.356907,37.0,78,126,93,98.8,19,21.0,411,,,,,0.9,,4,,,392,7.9,,34.0, +P-10015,E-10015-01,2025-04-13T01:18:29.838518,36.4,72,138,95,92.7,13,59.7,456,,460,385,0.6,,7,,,,441,8.1,,34.0, +P-10015,E-10015-01,2025-04-13T12:52:05.019989,36.8,84,137,89,96.3,14,21.0,408,,,,,,,,,,370,7.8,,34.0, +P-10015,E-10015-01,2025-04-13T01:32:02.492109,36.3,74,137,97,96.7,18,21.0,479,,231,,,1.5,,4,,,438,9.2,,34.0, +P-10015,E-10015-01,2025-04-13T14:55:19.821770,36.0,78,127,87,97.3,19,21.0,511,40.9,222,155,,,,5,,,492,9.0,,34.0, +P-10015,E-10015-01,2025-04-13T00:28:38.371851,36.2,82,151,103,99.1,16,21.0,426,,,,,,,6,30,26,422,7.5,,34.0, +P-10015,E-10015-01,2025-04-13T17:14:28.707103,36.3,48,143,100,97.5,15,30.8,462,,234,225,0.31,,6,,,,417,8.9,36,34.0, +P-10015,E-10015-02,2025-04-11T17:27:19.749207,36.4,83,125,90,99.1,19,21.0,521,41.2,311,143,,,,7,35,,473,9.2,,34.0,1 +P-10015,E-10015-02,2025-04-11T19:16:59.953815,37.1,74,143,79,96.9,12,21.0,451,,253,171,,,,4,26,,427,8.0,,34.0, +P-10015,E-10015-02,2025-04-11T19:55:47.931018,36.1,65,138,91,98.6,14,21.0,472,37.4,,,,,,5,,,461,8.4,,34.0,1 +P-10015,E-10015-02,2025-04-11T20:22:50.385396,36.4,103,132,77,96.7,14,21.0,523,37.3,101,88,,1.3,,,25,25,502,10.1,,34.0, +P-10015,E-10015-02,2025-04-11T19:47:19.972733,36.4,94,142,87,98.5,16,21.0,478,41.1,,,,,,,,,448,9.2,,34.0, +P-10015,E-10015-02,2025-04-11T22:24:48.434931,37.0,80,138,95,98.2,20,21.0,380,41.3,,,,,,,,,347,6.7,,34.0, +P-10015,E-10015-02,2025-04-12T04:41:35.454831,36.5,74,134,87,97.1,13,21.0,421,,,,,,,8,30,25,418,8.1,51,34.0, +P-10015,E-10015-02,2025-04-11T23:22:55.362584,37.0,73,130,80,99.0,12,21.0,354,39.2,,,,1.4,,4,27,22,337,6.3,,34.0,1 +P-10015,E-10015-02,2025-04-12T05:15:56.878496,37.2,97,133,85,96.2,14,21.0,471,40.7,,,,1.2,,,34,25,430,9.1,,34.0, +P-10015,E-10015-02,2025-04-11T22:02:28.752946,36.2,84,138,83,96.9,16,21.0,538,,,,,,,8,27,22,533,10.3,,34.0, +P-10015,E-10015-02,2025-04-12T02:10:03.481920,37.3,69,162,103,97.3,16,21.0,417,,,,,1.2,,,,,404,7.4,,34.0,1 +P-10016,E-10016-01,2025-04-15T17:27:19.750190,37.0,45,115,79,96.9,19,21.0,466,,135,,,1.1,,7,,,426,9.3,39,24.5,1 +P-10016,E-10016-01,2025-04-15T18:22:22.279233,36.4,68,110,73,99.3,19,21.0,476,,472,231,,,,,,,447,8.7,49,24.5,1 +P-10016,E-10016-01,2025-04-15T20:07:35.370341,37.1,73,119,78,99.0,12,21.0,355,38.5,,,,0.8,,,,,353,6.5,,24.5, +P-10016,E-10016-01,2025-04-15T19:37:29.431495,36.6,67,113,75,97.6,14,21.0,377,,,,,,,4,,,372,7.5,57,24.5, +P-10016,E-10016-01,2025-04-15T21:53:50.285307,37.1,69,111,81,98.8,20,21.0,450,,,,,,,8,29,20,443,8.2,,24.5,1 +P-10016,E-10016-01,2025-04-16T00:13:21.030228,37.2,77,124,72,97.8,16,21.0,545,,,,,1.5,,,,,526,9.9,,24.5,1 +P-10016,E-10016-01,2025-04-15T22:20:26.629572,36.1,72,120,78,97.5,13,44.3,397,36.5,,,0.44,,6,,34,30,361,7.9,44,24.5,1 +P-10016,E-10016-01,2025-04-16T01:07:27.235805,36.9,102,116,70,96.2,16,21.0,480,35.7,199,102,,1.3,,5,30,,478,8.8,,24.5, +P-10016,E-10016-01,2025-04-15T22:48:09.977800,37.1,70,140,81,91.9,14,21.0,409,38.5,,,,,,,27,23,380,8.1,,24.5, +P-10016,E-10016-01,2025-04-16T08:54:28.627214,36.7,85,120,72,96.8,20,21.0,407,37.4,,,,,,6,31,,373,8.1,48,24.5, +P-10016,E-10016-01,2025-04-16T00:32:05.114928,36.9,77,141,91,98.9,13,40.5,510,39.3,,,0.41,1.2,9,8,,,492,10.1,,24.5, +P-10016,E-10016-01,2025-04-16T05:36:48.157303,36.7,80,121,79,97.8,20,21.0,407,,,,,1.3,,,32,,367,8.1,,24.5, +P-10016,E-10016-02,2025-04-16T17:27:19.750190,36.8,71,114,76,98.7,14,45.7,532,36.8,,,0.46,1.5,2,7,31,,489,10.6,21,24.5, +P-10016,E-10016-02,2025-04-16T18:14:53.996825,36.3,76,112,76,96.3,12,21.0,367,,291,235,,,,,,,348,7.3,48,24.5, +P-10016,E-10016-02,2025-04-16T21:18:15.388436,36.1,79,122,75,97.4,13,21.0,453,38.3,393,306,,1.4,,,,,427,8.3,33,24.5,1 +P-10016,E-10016-02,2025-04-16T20:57:22.580803,36.8,70,122,77,99.2,12,21.0,371,,483,193,,1.0,,6,,,353,7.4,22,24.5,1 +P-10016,E-10016-02,2025-04-16T20:50:42.325469,36.4,76,134,92,97.3,19,21.0,371,,,,,,,7,,,367,7.4,37,24.5,1 +P-10016,E-10016-02,2025-04-17T00:31:08.635473,36.0,52,122,70,98.6,17,21.0,542,,134,132,,,,5,35,25,505,9.9,,24.5,1 +P-10017,E-10017-01,2025-04-13T17:27:19.750190,37.2,94,130,90,96.0,18,21.0,363,,140,110,,1.4,,,,,332,5.3,,16.7, +P-10017,E-10017-01,2025-04-13T17:59:00.236870,36.7,64,134,83,92.7,16,21.0,449,,,,,1.2,,8,,,409,6.5,,16.7,1 +P-10017,E-10017-01,2025-04-13T19:16:38.414170,37.1,112,149,80,98.8,14,21.0,351,,,,,,,,,,333,4.8,,16.7, +P-10017,E-10017-01,2025-04-13T21:35:16.521403,37.1,88,146,93,97.7,21,21.0,528,40.3,,,,,,6,30,,494,7.2,70,16.7,1 +P-10017,E-10017-01,2025-04-14T00:47:35.121170,37.1,89,138,78,98.7,16,26.8,396,38.6,,,0.27,,4,,27,,393,5.8,,16.7, +P-10017,E-10017-02,2025-04-15T17:27:19.750190,36.3,100,128,94,96.1,15,42.8,436,38.9,,,0.43,1.4,10,,25,21,393,6.0,,16.7, +P-10017,E-10017-02,2025-04-15T19:12:12.436817,36.8,87,130,87,99.1,17,21.0,372,38.2,,,,1.2,,4,,,365,5.4,75,16.7,1 +P-10017,E-10017-02,2025-04-15T19:50:44.822517,36.8,84,135,85,96.7,12,58.6,493,,,,0.59,1.3,10,8,,,464,7.2,,16.7,1 +P-10017,E-10017-02,2025-04-15T20:29:30.443754,36.9,85,153,92,96.2,13,21.0,464,,164,126,,,,6,31,,445,6.3,,16.7,1 +P-10017,E-10017-02,2025-04-16T01:25:00.868633,36.3,89,138,95,97.5,20,21.0,408,35.8,,,,1.4,,7,25,25,399,5.9,73,16.7, +P-10017,E-10017-02,2025-04-16T02:39:10.157545,36.3,90,138,83,98.5,13,21.0,365,39.9,,,,,,,32,30,340,5.3,,16.7, +P-10017,E-10017-02,2025-04-16T02:44:06.183544,36.1,85,150,89,97.9,17,21.0,368,,406,263,,1.3,,,,,334,5.0,,16.7, +P-10018,E-10018-01,2025-04-16T17:27:19.750190,36.0,58,137,75,92.3,20,21.0,518,40.5,,,,,,5,,,478,7.1,63,17.2, +P-10018,E-10018-01,2025-04-16T19:06:49.929686,36.3,85,131,86,97.3,14,21.0,449,,,,,0.8,,8,25,22,414,6.1,,17.2, +P-10018,E-10018-01,2025-04-16T21:02:52.395807,36.5,91,141,90,99.4,13,21.0,549,,,,,1.3,,,,,534,7.5,,17.2, +P-10018,E-10018-01,2025-04-16T20:44:29.478544,36.6,78,138,89,97.3,18,21.0,482,38.8,183,173,,1.4,,6,,,440,7.0,,17.2, +P-10018,E-10018-01,2025-04-16T23:33:01.654979,37.1,81,131,80,96.3,18,21.0,492,,308,302,,1.4,,,28,,443,7.1,,17.2, +P-10018,E-10018-01,2025-04-16T20:44:55.945217,36.6,79,144,97,98.2,15,21.0,405,,169,158,,,,,33,29,382,5.5,73,17.2, +P-10018,E-10018-01,2025-04-17T02:37:42.152990,36.1,95,137,92,98.7,20,21.0,443,,,,,,,,,,434,6.0,37,17.2, +P-10018,E-10018-01,2025-04-17T04:55:48.537240,36.5,88,133,95,97.5,20,21.0,367,41.0,,,,,,,,,334,5.3,68,17.2,1 +P-10018,E-10018-01,2025-04-16T22:24:11.256383,36.2,94,127,82,98.4,17,21.0,451,,,,,,,6,,,440,6.6,,17.2, +P-10018,E-10018-01,2025-04-17T09:25:45.988371,36.5,73,146,83,98.7,14,21.0,487,,492,405,,1.1,,5,30,30,444,6.6,73,17.2, +P-10018,E-10018-02,2025-04-12T17:27:19.750190,36.7,88,136,84,96.8,13,21.0,526,37.6,299,180,,,,,,,473,7.2,,17.2,1 +P-10018,E-10018-02,2025-04-12T18:57:35.569213,37.0,82,148,89,99.4,20,49.6,521,,,,0.5,1.2,3,,,,488,7.6,,17.2,1 +P-10018,E-10018-02,2025-04-12T20:54:54.302933,36.8,58,135,88,94.1,20,21.0,449,40.6,470,,,1.0,,7,,,434,6.1,,17.2,1 +P-10018,E-10018-02,2025-04-12T22:51:30.030655,36.2,92,138,91,97.0,14,21.0,487,42.9,,,,0.9,,7,,,444,6.6,,17.2, +P-10018,E-10018-02,2025-04-13T00:37:34.853349,36.8,69,143,92,97.2,15,50.2,377,,,,0.5,,3,5,33,,349,5.1,,17.2,1 +P-10018,E-10018-02,2025-04-12T21:29:49.574771,37.1,108,150,84,98.2,13,24.1,536,40.2,348,168,0.24,1.2,6,,33,,520,7.3,,17.2, +P-10018,E-10018-02,2025-04-13T04:37:02.898349,37.0,95,146,79,98.7,16,21.0,544,,,,,,,,,,532,7.4,,17.2, +P-10018,E-10018-03,2025-04-11T17:27:19.750190,36.1,64,137,103,97.8,19,33.9,396,,,,0.34,,3,,,,367,5.4,,17.2, +P-10018,E-10018-03,2025-04-11T18:48:01.814405,37.0,81,125,89,97.4,14,21.0,403,42.5,,,,1.4,,6,34,30,399,5.9,33,17.2,1 +P-10018,E-10018-03,2025-04-11T19:30:00.098602,36.5,82,146,87,98.2,19,21.0,378,,,,,1.1,,5,30,24,364,5.2,,17.2, +P-10018,E-10018-03,2025-04-11T20:57:43.824857,36.5,83,146,84,97.4,12,21.0,374,,,,,1.3,,,35,35,341,5.4,,17.2, +P-10018,E-10018-03,2025-04-11T21:30:19.927411,36.7,74,138,85,97.8,13,21.0,351,,,,,0.9,,,,,326,4.8,,17.2, +P-10018,E-10018-03,2025-04-12T02:43:58.199012,36.1,111,132,82,96.9,13,21.0,437,,,,,1.3,,,,,404,6.0,,17.2, +P-10018,E-10018-03,2025-04-12T01:44:37.818316,36.8,72,132,89,99.2,20,21.0,472,,351,149,,1.4,,,,,443,6.9,,17.2, +P-10018,E-10018-03,2025-04-12T03:12:40.101296,36.2,88,138,91,96.6,13,21.0,457,40.6,,,,0.9,,7,33,31,457,6.6,41,17.2, +P-10018,E-10018-03,2025-04-12T02:42:11.499545,36.5,100,150,85,99.4,19,21.0,439,,,,,1.4,,6,,,411,6.0,,17.2, +P-10018,E-10018-03,2025-04-12T06:03:54.778240,36.6,73,131,92,96.6,23,21.0,490,,375,269,,,,,,,467,7.1,,17.2, +P-10018,E-10018-03,2025-04-12T11:32:49.977191,36.6,91,147,100,99.4,13,21.0,450,37.2,,,,0.8,,8,34,20,421,6.5,44,17.2, +P-10018,E-10018-03,2025-04-12T11:59:28.532971,37.2,79,127,84,96.8,13,42.3,416,36.8,152,121,0.42,,8,,32,26,400,6.0,,17.2, +P-10018,E-10018-03,2025-04-12T10:16:57.509157,36.1,97,143,79,98.4,19,42.7,387,44.1,,,0.43,1.0,10,4,,,360,5.3,,17.2, +P-10019,E-10019-01,2025-04-13T17:27:19.750190,37.1,72,149,91,97.7,12,21.0,376,44.5,469,,,1.4,,,,,339,5.5,,20.7,1 +P-10019,E-10019-01,2025-04-13T18:53:23.312759,36.9,55,137,86,98.3,17,21.0,395,,208,183,,,,7,30,25,369,5.8,,20.7, +P-10019,E-10019-01,2025-04-13T19:39:18.811491,36.4,77,124,88,99.3,20,21.0,422,,,,,,,,,,398,5.8,70,20.7, +P-10019,E-10019-01,2025-04-13T19:06:33.698904,36.8,66,135,89,97.9,14,21.0,537,42.7,,,,0.9,,,29,29,506,7.4,,20.7, +P-10019,E-10019-01,2025-04-13T22:50:26.736911,36.8,71,124,90,98.8,13,21.0,400,37.5,417,311,,,,7,26,,368,5.5,,20.7, +P-10019,E-10019-01,2025-04-13T21:57:36.274622,36.7,53,135,77,96.2,12,21.0,400,44.0,,,,,,,,,380,5.5,28,20.7, +P-10019,E-10019-01,2025-04-14T01:22:13.372445,36.2,80,137,83,97.9,13,21.0,445,,,,,,,,,,413,6.1,,20.7, +P-10019,E-10019-01,2025-04-14T00:44:48.040465,36.8,65,148,90,96.8,15,21.0,384,37.5,,,,,,,,,380,5.6,,20.7, +P-10019,E-10019-01,2025-04-14T02:47:12.370980,36.9,72,142,97,98.6,19,21.0,535,,,,,1.4,,,,,532,7.4,29,20.7,1 +P-10019,E-10019-01,2025-04-14T02:52:54.244928,36.8,82,128,90,96.1,15,21.0,370,37.9,,,,0.9,,,,,347,5.1,,20.7, +P-10019,E-10019-01,2025-04-14T09:06:08.028122,36.8,76,132,95,98.9,12,21.0,539,,278,,,1.2,,8,,,520,7.9,,20.7, +P-10019,E-10019-01,2025-04-13T23:10:36.831236,36.9,70,137,80,94.6,19,21.0,397,40.2,,,,,,,29,,397,5.5,,20.7, +P-10019,E-10019-01,2025-04-14T06:39:11.806655,37.5,72,142,89,97.4,16,21.0,537,41.4,,,,0.9,,8,,,517,7.9,,20.7, +P-10019,E-10019-01,2025-04-14T08:00:34.113118,36.5,82,132,97,97.7,14,35.7,505,,334,216,0.36,,6,,,,477,7.0,,20.7, +P-10019,E-10019-01,2025-04-14T20:24:23.053607,36.8,75,153,96,96.5,16,21.0,452,40.5,277,112,,1.0,,,,,445,6.2,60,20.7, +P-10020,E-10020-01,2025-04-15T17:27:19.750190,36.3,47,128,86,96.9,19,21.0,516,,347,,,1.0,,,29,,499,8.2,,33.8, +P-10020,E-10020-01,2025-04-15T18:23:22.609006,36.3,70,120,79,98.5,20,37.3,400,39.2,,,0.37,0.9,9,,26,25,369,6.8,,33.8, +P-10020,E-10020-01,2025-04-15T19:23:41.995775,36.9,78,116,80,97.7,17,21.0,385,,110,101,,,,4,33,,374,6.6,51,33.8,1 +P-10020,E-10020-01,2025-04-15T20:43:04.388126,36.2,80,140,88,98.1,19,21.0,506,,,,,,,,26,21,492,8.0,,33.8, +P-10020,E-10020-01,2025-04-15T22:57:23.499681,36.6,77,118,72,99.0,19,21.0,362,,,,,1.1,,,,,332,6.2,,33.8, +P-10020,E-10020-01,2025-04-15T22:24:57.781352,36.5,71,136,82,98.4,18,21.0,432,35.4,,,,1.0,,6,,,416,7.4,,33.8, +P-10020,E-10020-01,2025-04-16T04:38:31.209774,36.3,67,115,80,93.3,17,21.0,549,,,,,0.8,,4,,,494,8.7,,33.8, +P-10020,E-10020-02,2025-04-07T17:27:19.750190,36.1,107,124,80,98.7,19,46.3,509,43.3,321,209,0.46,0.9,10,,,,473,8.1,,33.8, +P-10020,E-10020-02,2025-04-07T18:02:19.821299,36.4,78,113,82,97.7,12,21.0,446,,323,,,1.1,,,28,,411,7.6,,33.8, +P-10020,E-10020-02,2025-04-07T19:11:47.946886,36.9,67,122,81,94.5,15,41.9,381,,,,0.42,,2,,33,29,365,6.5,,33.8, +P-10020,E-10020-02,2025-04-07T21:14:15.954584,36.7,104,110,82,96.1,20,21.0,462,,,,,1.1,,6,,,441,7.3,,33.8, +P-10020,E-10020-02,2025-04-07T23:57:13.953946,36.3,85,123,84,96.8,14,21.0,450,,247,218,,0.8,,7,,,422,7.1,27,33.8, +P-10020,E-10020-02,2025-04-07T22:04:50.758523,36.2,78,118,71,96.5,20,21.0,351,,385,233,,0.9,,,,,342,6.0,57,33.8, +P-10020,E-10020-02,2025-04-08T02:58:23.167947,37.4,80,132,86,96.5,20,21.0,432,38.4,182,150,,1.3,,,29,25,398,6.9,,33.8, +P-10020,E-10020-02,2025-04-08T04:22:06.679032,37.4,69,123,77,97.5,12,21.0,413,44.3,,,,1.3,,,,,398,6.6,,33.8,1 +P-10020,E-10020-02,2025-04-08T07:53:21.430617,36.7,68,125,78,99.3,14,21.0,404,35.0,,,,1.3,,,30,27,401,6.9,20,33.8, +P-10020,E-10020-02,2025-04-07T23:55:08.294885,36.6,78,115,84,98.6,12,21.0,460,44.4,,,,,,7,25,24,423,7.3,,33.8, +P-10020,E-10020-03,2025-04-14T17:27:19.750190,37.2,67,113,70,98.4,15,21.0,478,43.5,166,122,,,,,,,441,8.2,77,33.8, +P-10020,E-10020-03,2025-04-14T19:16:37.541635,36.7,80,122,81,99.1,15,21.0,533,,,,,1.5,,4,34,30,505,8.5,,33.8, +P-10020,E-10020-03,2025-04-14T19:12:31.026449,36.5,67,138,81,99.2,14,29.3,365,35.2,392,360,0.29,,6,4,33,28,353,5.8,23,33.8,1 +P-10020,E-10020-03,2025-04-14T21:17:30.183289,36.6,74,116,79,96.6,12,21.0,541,,,,,1.0,,,,,494,9.2,42,33.8, +P-10020,E-10020-03,2025-04-15T00:07:16.469530,36.8,66,113,76,98.2,16,21.0,372,,,,,1.4,,7,,,344,5.9,,33.8,1 +P-10020,E-10020-03,2025-04-15T03:09:20.028347,36.9,74,123,75,98.5,19,55.4,412,39.8,,,0.55,,7,,,,371,6.5,57,33.8, +P-10020,E-10020-03,2025-04-15T01:39:59.525793,37.5,80,114,82,95.9,18,58.7,442,,,,0.59,1.2,3,,33,32,407,7.6,,33.8, +P-10020,E-10020-03,2025-04-14T23:48:06.887355,37.1,108,141,83,98.3,16,21.0,463,,,,,1.2,,5,,,453,7.3,,33.8,1 +P-10020,E-10020-03,2025-04-15T00:48:56.360422,37.1,72,120,76,98.4,14,21.0,447,42.3,329,275,,,,,,,424,7.6,,33.8, +P-10020,E-10020-03,2025-04-15T09:46:42.901826,36.4,84,113,79,99.3,20,21.0,387,40.4,,,,1.2,,5,,,358,6.1,,33.8, +P-10020,E-10020-03,2025-04-15T11:37:59.185573,36.4,78,120,73,98.2,18,21.0,488,44.4,,,,,,,,,485,7.7,,33.8, +P-10020,E-10020-03,2025-04-15T04:32:20.452829,36.6,69,113,75,97.4,20,34.8,485,35.7,424,127,0.35,,5,4,,,482,8.3,,33.8, +P-10020,E-10020-03,2025-04-15T12:01:24.882129,36.1,66,115,79,99.1,18,40.6,508,,,,0.41,,7,4,,,495,8.1,,33.8, +P-10021,E-10021-01,2025-04-15T17:27:19.751196,37.2,65,136,88,99.5,19,21.0,470,38.7,217,90,,1.5,,4,26,24,448,9.3,,32.9,1 +P-10021,E-10021-01,2025-04-15T18:55:34.030933,37.2,108,126,82,96.2,12,21.0,508,44.4,301,,,1.3,,,,,463,10.1,56,32.9, +P-10021,E-10021-01,2025-04-15T20:16:08.728405,37.8,78,137,91,95.0,17,36.8,506,,187,124,0.37,1.3,8,,,,493,11.1,,32.9, +P-10021,E-10021-01,2025-04-15T22:55:40.041013,36.8,74,133,92,98.4,15,21.0,376,40.2,,,,1.0,,,,,374,8.2,,32.9, +P-10021,E-10021-01,2025-04-16T01:02:50.280751,36.7,76,128,98,94.2,15,21.0,539,,,,,1.1,,,27,,501,10.7,,32.9, +P-10021,E-10021-01,2025-04-16T00:47:09.623773,37.5,100,150,93,98.4,16,21.0,380,44.3,372,370,,,,6,25,22,361,8.3,61,32.9, +P-10021,E-10021-01,2025-04-16T00:28:37.190415,37.6,98,131,86,96.0,19,46.3,477,41.4,252,134,0.46,,2,,31,,452,9.5,,32.9, +P-10021,E-10021-01,2025-04-16T04:11:56.127420,37.4,57,134,89,98.3,20,21.0,460,36.4,375,145,,,,8,26,,434,9.2,,32.9, +P-10021,E-10021-01,2025-04-16T05:15:37.811481,37.2,80,139,95,98.4,16,21.0,362,38.3,,,,,,,,,352,7.9,,32.9, +P-10021,E-10021-01,2025-04-15T22:34:43.219258,37.6,108,142,97,96.1,20,21.0,508,36.5,,,,,,5,,,506,11.1,,32.9, +P-10021,E-10021-01,2025-04-16T07:50:51.980472,37.9,83,127,80,96.6,15,34.1,376,,,,0.34,1.2,8,,,,355,7.5,57,32.9,1 +P-10021,E-10021-02,2025-04-07T17:27:19.751196,38.1,103,137,84,97.8,13,21.0,423,35.0,,,,1.1,,4,27,25,409,8.4,,32.9, +P-10021,E-10021-02,2025-04-07T18:19:54.402507,37.0,97,124,93,96.5,19,21.0,386,42.7,,,,1.4,,,,,359,7.7,,32.9,1 +P-10021,E-10021-02,2025-04-07T21:02:26.689271,36.8,58,134,83,96.7,13,21.0,547,42.6,,,,,,,29,26,540,12.0,,32.9, +P-10021,E-10021-02,2025-04-07T20:45:17.584472,36.5,91,131,81,96.2,14,21.0,473,,,,,1.0,,7,28,27,446,9.4,,32.9, +P-10021,E-10021-02,2025-04-07T20:59:36.247564,37.5,90,131,86,94.7,19,21.0,496,,485,431,,1.5,,4,29,,496,10.8,,32.9,1 +P-10021,E-10021-02,2025-04-07T23:48:26.636261,36.6,88,141,90,97.5,17,21.0,457,,,,,0.9,,,,,430,9.1,53,32.9,1 +P-10021,E-10021-02,2025-04-07T21:35:01.167699,36.8,91,134,85,98.5,16,21.0,429,37.5,,,,0.9,,,,,416,9.4,,32.9,1 +P-10021,E-10021-02,2025-04-07T22:42:54.170048,37.2,85,133,86,98.3,16,21.0,518,40.6,,,,,,5,,,482,11.3,54,32.9, +P-10021,E-10021-02,2025-04-08T01:43:44.169025,37.6,85,132,84,97.2,18,21.0,382,42.7,,,,0.9,,,,,374,7.6,,32.9, +P-10021,E-10021-02,2025-04-08T03:42:05.607220,37.3,81,148,79,97.0,19,21.0,461,,,,,1.1,,5,34,,433,9.2,,32.9, +P-10021,E-10021-02,2025-04-07T23:17:23.636900,37.3,100,135,96,97.4,19,21.0,418,41.0,336,314,,,,,,,390,8.3,20,32.9, +P-10021,E-10021-02,2025-04-08T08:15:07.470707,37.3,84,139,97,98.0,15,21.0,499,,,,,1.3,,,,,493,10.9,,32.9, +P-10021,E-10021-02,2025-04-08T15:53:42.849709,37.4,80,128,96,97.4,20,24.7,358,39.5,421,,0.25,,9,5,26,22,334,7.8,72,32.9, +P-10021,E-10021-02,2025-04-08T18:08:57.849231,37.6,77,152,91,98.1,18,21.0,514,41.7,,,,,,4,,,482,10.2,31,32.9, +P-10022,E-10022-01,2025-04-12T17:27:19.751196,36.5,65,120,86,98.6,14,21.0,537,42.2,,,,1.0,,4,,,521,9.2,,34.9, +P-10022,E-10022-01,2025-04-12T18:20:10.716718,37.1,94,138,97,98.3,13,21.0,401,,,,,1.2,,6,34,22,395,6.4,,34.9,1 +P-10022,E-10022-01,2025-04-12T19:04:06.291992,36.3,58,140,93,98.4,14,21.0,550,,,,,,,6,31,31,504,9.4,,34.9, +P-10022,E-10022-01,2025-04-12T22:38:14.472483,36.7,68,135,84,96.3,13,21.0,453,39.9,392,338,,1.4,,,34,34,411,7.2,61,34.9, +P-10022,E-10022-01,2025-04-13T00:02:30.694196,37.0,69,133,90,97.8,16,21.0,543,44.7,,,,,,7,32,32,523,9.3,,34.9, +P-10022,E-10022-01,2025-04-12T22:22:40.554211,36.1,72,146,83,96.2,18,21.0,412,38.3,,,,,,8,,,412,7.1,,34.9,1 +P-10023,E-10023-01,2025-04-12T17:27:19.751196,36.6,73,124,84,92.2,19,28.2,504,40.3,493,300,0.28,1.1,2,6,26,26,464,9.8,,21.2,1 +P-10023,E-10023-01,2025-04-12T19:26:25.835639,36.6,48,126,78,98.7,16,21.0,359,37.2,158,114,,,,5,,,356,7.0,,21.2, +P-10023,E-10023-01,2025-04-12T21:11:17.028126,36.5,65,124,79,97.2,12,21.0,424,,,,,1.4,,,29,20,414,8.3,,21.2,1 +P-10023,E-10023-01,2025-04-12T21:40:41.400407,36.0,84,125,85,99.0,19,21.0,517,,,,,1.4,,,32,23,473,10.1,,21.2,1 +P-10023,E-10023-01,2025-04-12T20:05:39.961568,36.5,77,136,83,96.3,20,21.0,535,,,,,,,4,33,,518,11.5,75,21.2, +P-10023,E-10023-01,2025-04-12T20:50:00.696754,37.1,82,111,75,96.4,20,21.0,433,40.0,,,,1.4,,5,35,34,401,9.3,38,21.2, +P-10024,E-10024-01,2025-04-13T17:27:19.751196,36.5,74,137,91,97.3,17,38.1,485,,,,0.38,1.0,3,,28,,447,8.9,,25.7, +P-10024,E-10024-01,2025-04-13T19:23:24.994585,36.8,96,131,89,94.4,19,21.0,402,35.7,238,94,,1.0,,8,26,,376,6.8,,25.7, +P-10024,E-10024-01,2025-04-13T20:03:08.170544,36.2,65,128,85,96.2,17,21.0,474,,,,,,,,29,21,455,8.0,,25.7, +P-10024,E-10024-01,2025-04-13T21:13:29.204422,36.3,85,133,87,98.7,12,21.0,372,41.9,,,,,,,,,363,6.3,,25.7,1 +P-10024,E-10024-01,2025-04-13T22:36:52.142433,36.7,65,141,81,98.4,12,21.0,484,,299,153,,0.8,,,,,480,8.2,,25.7, +P-10024,E-10024-01,2025-04-14T01:33:53.146956,37.2,77,142,94,98.2,15,21.0,526,,,,,,,,,,522,8.9,,25.7, +P-10024,E-10024-01,2025-04-13T21:13:04.670941,37.0,53,135,92,97.0,16,21.0,374,,,,,1.0,,4,30,30,358,6.3,,25.7, +P-10024,E-10024-01,2025-04-14T03:31:16.693393,37.1,82,127,92,97.3,15,21.0,538,44.0,,,,1.3,,,,,486,9.9,,25.7,1 +P-10024,E-10024-01,2025-04-14T05:41:30.557298,37.1,83,161,94,96.9,17,37.6,388,40.1,222,213,0.38,1.5,3,5,,,378,6.6,,25.7, +P-10024,E-10024-01,2025-04-14T08:16:47.356752,36.9,66,153,83,99.0,15,21.0,484,,153,96,,1.1,,,,,462,8.9,79,25.7,1 +P-10024,E-10024-01,2025-04-14T00:49:49.184595,36.6,80,131,92,96.4,13,21.0,457,40.6,,,,,,8,,,442,8.4,,25.7, +P-10024,E-10024-02,2025-04-16T17:27:19.751196,36.4,90,136,89,97.7,22,21.0,403,42.8,,,,0.9,,6,,,368,7.4,,25.7, +P-10024,E-10024-02,2025-04-16T18:32:01.858225,36.8,78,140,89,98.0,19,21.0,429,,,,,1.1,,,,,399,7.9,,25.7, +P-10024,E-10024-02,2025-04-16T21:23:00.877940,37.1,77,131,77,98.4,19,21.0,490,,497,280,,1.3,,6,35,20,454,8.3,,25.7, +P-10024,E-10024-02,2025-04-16T21:57:29.037218,37.1,82,136,96,97.6,14,21.0,514,,,,,,,,,,473,9.4,,25.7,1 +P-10024,E-10024-02,2025-04-16T23:00:32.123355,36.3,112,124,76,99.5,17,21.0,392,39.4,,,,,,,34,30,355,7.2,,25.7, +P-10024,E-10024-02,2025-04-17T01:24:58.593021,36.6,64,134,84,96.2,13,21.0,500,,,,,0.9,,,33,29,474,8.5,,25.7, +P-10024,E-10024-02,2025-04-16T22:44:09.955515,37.1,84,134,94,98.1,18,21.0,390,37.3,,,,1.2,,6,,,368,6.6,69,25.7, +P-10024,E-10024-02,2025-04-16T23:05:53.674590,36.1,83,126,88,99.3,12,21.0,446,43.9,,,,0.8,,,33,31,432,8.2,,25.7, +P-10024,E-10024-02,2025-04-17T03:56:52.432679,36.4,81,145,88,92.5,18,21.0,367,41.2,,,,1.0,,,30,,333,6.2,,25.7,1 +P-10024,E-10024-02,2025-04-17T00:15:05.321208,36.8,79,136,80,99.3,19,21.0,374,42.8,,,,1.0,,,,,361,6.3,42,25.7, +P-10024,E-10024-02,2025-04-17T00:48:20.519453,36.5,104,154,96,96.5,15,26.1,447,37.9,,,0.26,,4,,,,414,7.6,66,25.7, +P-10024,E-10024-02,2025-04-17T08:35:48.975521,36.4,73,136,77,96.3,16,21.0,518,41.8,,,,1.2,,6,30,28,471,9.5,,25.7, +P-10024,E-10024-02,2025-04-17T14:59:50.435543,37.7,85,132,91,99.2,19,21.0,432,,,,,0.9,,,,,406,7.3,,25.7, +P-10024,E-10024-03,2025-04-11T17:27:19.751196,36.8,71,139,94,98.2,19,21.0,477,,,,,,,,,,475,8.1,,25.7, +P-10024,E-10024-03,2025-04-11T18:45:00.791271,36.9,76,162,89,92.4,12,24.0,439,,339,155,0.24,1.0,7,,28,22,433,8.0,,25.7, +P-10024,E-10024-03,2025-04-11T19:25:50.082980,36.5,80,133,85,96.6,18,21.0,396,,,,,,,7,29,26,356,6.7,,25.7, +P-10024,E-10024-03,2025-04-11T22:35:43.487975,36.7,70,158,103,97.4,18,21.0,418,,,,,1.2,,,,,388,7.1,,25.7, +P-10024,E-10024-03,2025-04-11T21:26:22.359458,36.7,78,131,93,99.4,13,21.0,409,,205,94,,1.0,,6,26,23,371,6.9,,25.7,1 +P-10024,E-10024-03,2025-04-11T23:04:00.434198,36.8,78,134,84,98.7,16,47.9,430,38.4,,,0.48,,9,,25,24,414,7.9,,25.7, +P-10025,E-10025-01,2025-04-15T17:27:19.751196,37.6,88,129,87,96.1,20,21.0,525,,432,362,,0.8,,,,,521,8.7,,31.6, +P-10025,E-10025-01,2025-04-15T19:20:25.232062,36.4,82,131,90,99.5,15,21.0,540,,,,,,,,35,25,530,8.9,,31.6, +P-10025,E-10025-01,2025-04-15T19:41:06.619047,37.4,82,135,93,97.0,14,40.7,506,35.6,346,144,0.41,1.1,6,,33,27,489,8.4,,31.6, +P-10025,E-10025-01,2025-04-15T23:13:40.169488,37.9,78,125,93,96.3,15,21.0,398,,,,,,,6,,,366,6.6,,31.6,1 +P-10025,E-10025-01,2025-04-15T20:57:33.833596,37.6,52,132,88,96.0,16,21.0,508,39.4,402,143,,,,,28,20,473,8.4,,31.6, +P-10025,E-10025-01,2025-04-16T02:20:56.709700,37.3,81,127,92,97.4,20,21.0,358,42.9,,,,1.1,,,34,26,327,6.4,,31.6,1 +P-10025,E-10025-01,2025-04-15T21:25:57.089457,37.1,91,155,87,97.2,20,21.0,372,,,,,1.3,,,30,22,354,6.6,,31.6, +P-10025,E-10025-01,2025-04-15T22:56:52.953007,38.0,85,132,75,96.8,18,21.0,458,,,,,0.9,,,29,25,414,8.2,,31.6,1 +P-10025,E-10025-01,2025-04-16T08:26:53.986804,37.3,86,149,96,96.8,12,21.0,368,35.3,,,,1.4,,7,,,338,6.6,,31.6, +P-10025,E-10025-01,2025-04-16T09:04:51.965007,37.4,95,145,98,93.2,15,43.5,475,36.7,,,0.43,1.1,2,,34,32,466,8.5,,31.6, +P-10025,E-10025-01,2025-04-16T03:40:32.613850,36.6,115,134,85,97.1,16,21.0,431,,183,86,,,,,,,417,7.7,43,31.6, +P-10025,E-10025-01,2025-04-16T12:32:01.584276,36.7,49,141,89,97.6,20,21.0,380,41.1,397,292,,1.3,,,,,343,6.3,,31.6,1 +P-10025,E-10025-01,2025-04-16T05:30:48.685859,37.5,63,137,86,97.5,18,21.0,468,,,,,1.3,,,,,467,7.7,,31.6, +P-10025,E-10025-01,2025-04-16T17:48:16.708288,38.0,66,141,94,98.9,20,41.7,449,,,,0.42,0.9,9,7,35,30,425,7.4,55,31.6, +P-10026,E-10026-01,2025-04-16T17:27:19.751196,36.2,78,152,82,99.2,20,21.0,444,38.9,,,,1.2,,,,,420,6.5,,21.5, +P-10026,E-10026-01,2025-04-16T18:03:48.439272,36.5,69,126,84,98.8,12,21.0,373,37.5,,,,1.1,,,,,362,5.1,,21.5, +P-10026,E-10026-01,2025-04-16T20:57:55.528795,36.3,71,130,84,99.1,14,21.0,427,37.5,,,,,,,,,407,5.9,,21.5, +P-10026,E-10026-01,2025-04-16T22:39:15.079066,36.2,72,131,93,96.5,15,21.0,379,,,,,1.0,,,,,379,5.5,,21.5,1 +P-10026,E-10026-01,2025-04-16T22:32:36.891623,37.6,78,140,94,97.6,15,21.0,501,45.0,,,,1.4,,5,32,20,467,6.9,,21.5, +P-10026,E-10026-01,2025-04-17T03:18:11.574171,36.0,70,126,88,96.2,15,21.0,518,,275,137,,1.1,,,33,20,476,7.1,,21.5, +P-10026,E-10026-01,2025-04-17T03:03:05.976210,36.7,74,131,99,99.2,19,21.0,541,42.0,,,,,,,,,524,7.9,,21.5, +P-10026,E-10026-02,2025-04-07T17:27:19.751196,36.3,66,145,93,97.6,15,21.0,482,42.9,,,,,,,,,478,6.6,,21.5, +P-10026,E-10026-02,2025-04-07T18:21:11.751663,36.1,91,151,93,99.0,24,21.0,547,40.5,,,,1.3,,7,,,506,8.0,,21.5, +P-10026,E-10026-02,2025-04-07T20:44:33.174771,36.2,71,134,85,98.5,19,21.0,364,,,,,,,,32,,334,5.0,,21.5, +P-10026,E-10026-02,2025-04-07T19:00:53.782419,36.5,83,138,97,96.6,21,43.8,488,,,,0.44,1.2,2,6,,,443,6.7,,21.5, +P-10026,E-10026-02,2025-04-08T00:02:08.979241,36.8,74,136,86,97.4,20,21.0,479,39.9,,,,1.4,,,,,435,7.0,,21.5, +P-10026,E-10026-02,2025-04-07T23:58:14.763181,36.3,78,133,85,98.0,17,21.0,414,39.9,,,,1.2,,,32,20,380,6.0,80,21.5, +P-10026,E-10026-03,2025-04-09T17:27:19.751196,36.9,78,137,97,97.9,12,21.0,476,,,,,0.9,,4,29,25,435,6.5,,21.5,1 +P-10026,E-10026-03,2025-04-09T18:45:27.166283,36.3,73,132,81,98.8,16,21.0,390,,102,100,,1.1,,,,,352,5.3,,21.5, +P-10026,E-10026-03,2025-04-09T21:21:30.973051,36.9,76,135,88,98.5,19,21.0,457,,,,,1.1,,5,26,25,428,6.3,,21.5, +P-10026,E-10026-03,2025-04-09T22:54:01.500900,37.0,65,133,84,96.0,15,21.0,478,44.9,,,,1.3,,6,28,,471,6.6,,21.5, +P-10026,E-10026-03,2025-04-09T21:41:55.331649,37.1,76,136,86,97.9,17,21.0,368,36.4,350,243,,1.5,,6,34,32,334,5.0,,21.5,1 +P-10026,E-10026-03,2025-04-09T22:03:56.547894,36.5,98,131,85,99.1,20,21.0,528,43.8,,,,,,,,,517,7.2,68,21.5,1 +P-10026,E-10026-03,2025-04-09T23:47:37.384700,36.9,70,131,83,97.0,17,21.0,500,40.2,,,,1.5,,6,28,,460,6.9,,21.5, +P-10026,E-10026-03,2025-04-10T01:38:12.399776,36.7,73,145,84,97.5,16,21.0,518,,,,,1.2,,,26,21,479,7.1,59,21.5, +P-10026,E-10026-03,2025-04-10T06:36:56.501865,37.0,78,129,97,99.3,16,21.0,421,,126,124,,,,,,,388,6.1,,21.5,1 +P-10026,E-10026-03,2025-04-10T02:07:51.985994,37.0,77,127,94,98.8,18,21.0,382,39.3,,,,,,4,34,30,355,5.6,,21.5, +P-10027,E-10027-01,2025-04-12T17:27:19.752189,36.5,76,111,83,98.6,12,21.0,420,39.8,,,,1.0,,,,,390,8.9,,30.8, +P-10027,E-10027-01,2025-04-12T18:02:58.494873,37.0,44,111,82,92.0,16,21.0,393,,,,,1.4,,,33,,383,8.3,,30.8, +P-10027,E-10027-01,2025-04-12T19:15:58.666331,37.2,83,122,83,98.1,16,30.6,369,,303,93,0.31,,7,6,,,349,7.8,63,30.8, +P-10027,E-10027-01,2025-04-12T22:40:33.285680,37.1,83,115,80,99.1,19,21.0,511,,,,,1.1,,4,,,488,10.8,,30.8, +P-10027,E-10027-01,2025-04-13T00:16:07.577870,36.2,82,120,72,97.5,19,21.0,449,43.9,414,320,,,,7,27,25,416,9.5,,30.8, +P-10027,E-10027-01,2025-04-13T01:44:14.242814,36.2,79,118,80,98.2,12,21.0,368,43.1,,,,1.2,,7,27,20,350,7.1,,30.8,1 +P-10027,E-10027-01,2025-04-13T00:38:43.248631,37.0,49,120,81,97.6,20,21.0,518,35.7,,,,1.1,,7,,,474,10.9,,30.8, +P-10027,E-10027-01,2025-04-13T06:27:48.172949,36.0,66,123,70,97.6,19,44.9,536,,,,0.45,1.1,9,,,,530,10.3,,30.8, +P-10027,E-10027-01,2025-04-13T04:42:17.593844,37.0,80,119,84,98.6,12,59.1,417,39.0,,,0.59,,7,6,,,404,8.0,,30.8, +P-10027,E-10027-02,2025-04-11T17:27:19.752189,36.3,83,125,76,98.0,18,21.0,371,43.5,,,,1.5,,,,,340,7.1,61,30.8, +P-10027,E-10027-02,2025-04-11T18:35:37.310988,36.9,53,120,80,98.5,16,21.0,501,35.5,239,148,,0.8,,,,,451,9.7,58,30.8, +P-10027,E-10027-02,2025-04-11T19:58:31.069582,36.2,65,121,77,99.0,18,21.0,538,,,,,1.3,,,35,32,524,10.4,,30.8, +P-10027,E-10027-02,2025-04-11T22:40:15.429260,36.6,72,117,78,97.9,18,21.0,526,,,,,,,,,,494,10.1,,30.8, +P-10027,E-10027-02,2025-04-11T19:40:25.892841,37.1,67,117,74,97.1,18,21.0,382,,,,,1.1,,,32,20,355,7.4,,30.8, +P-10027,E-10027-02,2025-04-11T21:37:31.041183,36.9,85,120,71,99.5,18,21.0,363,40.7,,,,1.1,,6,,,341,7.7,71,30.8, +P-10027,E-10027-02,2025-04-12T04:22:59.832774,37.0,68,121,70,98.8,14,21.0,459,41.8,,,,0.8,,,30,30,454,9.7,64,30.8, +P-10027,E-10027-02,2025-04-12T00:55:02.719836,36.3,69,110,70,98.2,15,21.0,359,,,,,1.3,,,28,23,329,7.6,,30.8, +P-10027,E-10027-02,2025-04-12T02:26:41.230631,36.8,68,116,81,97.0,20,42.6,361,41.8,461,430,0.43,1.2,7,,,,343,7.0,,30.8, +P-10027,E-10027-02,2025-04-12T06:58:59.505925,37.4,74,122,72,96.5,14,21.0,363,,,,,1.3,,,,,330,7.7,,30.8, +P-10027,E-10027-02,2025-04-12T10:48:20.830448,36.4,106,125,72,99.3,19,21.0,523,,111,82,,1.4,,,33,32,484,11.0,,30.8, +P-10027,E-10027-02,2025-04-12T02:31:45.853027,37.0,67,117,70,98.4,15,21.0,490,35.5,,,,0.9,,,31,24,473,10.3,,30.8, +P-10027,E-10027-02,2025-04-12T07:17:22.980145,36.1,74,116,73,99.5,20,21.0,540,,272,92,,,,7,,,489,10.4,,30.8,1 +P-10027,E-10027-02,2025-04-12T19:06:28.947958,36.7,68,110,72,96.6,12,21.0,398,,,,,,,,,,377,7.7,,30.8, +P-10027,E-10027-02,2025-04-12T01:34:34.616674,36.4,70,115,78,98.2,14,21.0,362,,188,100,,,,,,,327,7.6,,30.8,1 +P-10028,E-10028-01,2025-04-13T17:27:19.752189,36.4,78,162,88,97.6,19,21.0,518,,,,,0.8,,5,35,21,486,10.4,,37.2, +P-10028,E-10028-01,2025-04-13T19:19:14.584942,36.1,95,130,89,97.9,13,21.0,417,,395,287,,0.9,,,,,416,9.2,,37.2, +P-10028,E-10028-01,2025-04-13T21:13:29.900145,36.2,84,138,90,97.5,13,21.0,405,,,,,,,8,,,394,8.2,,37.2,1 +P-10028,E-10028-01,2025-04-13T22:28:02.420309,36.6,78,142,88,97.5,12,32.3,413,,,,0.32,,4,,,,382,8.3,,37.2, +P-10028,E-10028-01,2025-04-14T01:01:32.711851,36.5,66,132,84,98.3,19,21.0,546,40.1,,,,1.3,,4,,,525,12.1,,37.2, +P-10028,E-10028-01,2025-04-14T02:30:18.069319,37.1,67,129,77,97.0,18,21.0,494,35.5,,,,,,4,,,462,10.9,,37.2,1 +P-10028,E-10028-01,2025-04-14T04:00:25.344094,36.5,74,142,94,96.3,16,21.0,405,38.6,343,290,,0.9,,7,,,399,8.2,,37.2, +P-10028,E-10028-01,2025-04-14T05:05:01.780360,37.1,69,131,78,99.1,20,21.0,387,,,,,,,,32,28,353,7.8,,37.2, +P-10028,E-10028-01,2025-04-14T02:23:32.949100,37.1,66,130,92,98.7,19,21.0,380,35.2,,,,0.9,,,34,,351,8.4,,37.2, +P-10028,E-10028-01,2025-04-14T11:21:54.575035,36.4,81,122,95,98.7,19,21.0,508,44.2,,,,0.8,,7,,,469,10.2,,37.2, +P-10028,E-10028-01,2025-04-14T02:02:48.387135,36.7,82,127,92,97.2,15,21.0,460,44.5,,,,0.9,,,31,21,446,10.2,,37.2,1 +P-10028,E-10028-01,2025-04-14T12:59:56.549861,36.3,79,136,89,96.8,15,21.0,421,,,,,1.3,,7,,,399,9.3,,37.2, +P-10028,E-10028-02,2025-04-10T17:27:19.752189,37.0,66,129,98,96.7,15,21.0,500,,,,,1.0,,5,27,21,492,10.1,,37.2, +P-10028,E-10028-02,2025-04-10T18:01:04.724644,36.5,65,133,90,96.6,19,21.0,492,37.5,,,,,,,34,25,475,9.9,30,37.2, +P-10028,E-10028-02,2025-04-10T19:30:56.267624,36.6,81,137,88,98.1,13,21.0,540,,,,,0.8,,,27,20,495,12.0,,37.2,1 +P-10028,E-10028-02,2025-04-10T22:06:29.762897,36.2,66,137,91,93.9,18,21.0,515,39.7,253,203,,,,6,30,27,484,10.4,79,37.2, +P-10028,E-10028-02,2025-04-10T21:17:24.502521,36.1,79,143,96,96.2,15,30.0,545,,,,0.3,,3,8,,,493,11.0,67,37.2, +P-10028,E-10028-02,2025-04-10T21:15:35.872774,36.3,69,126,87,96.0,14,21.0,478,43.1,330,147,,1.0,,,,,458,10.6,27,37.2, +P-10028,E-10028-02,2025-04-11T04:16:07.673254,37.1,83,129,87,97.1,17,21.0,375,38.3,,,,,,,,,346,8.3,,37.2, +P-10028,E-10028-03,2025-04-15T17:27:19.752189,37.1,81,131,94,97.7,20,21.0,387,40.0,,,,1.2,,5,,,351,7.8,,37.2, +P-10028,E-10028-03,2025-04-15T18:12:00.460754,37.0,80,139,94,97.9,18,21.0,489,42.7,,,,1.2,,7,29,,486,10.8,,37.2, +P-10028,E-10028-03,2025-04-15T21:22:49.049253,37.0,84,127,80,96.7,13,21.0,526,,,,,1.4,,4,30,,497,10.6,,37.2, +P-10028,E-10028-03,2025-04-15T23:25:34.569239,36.3,72,135,81,99.4,12,21.0,407,,441,207,,,,7,31,,395,8.2,,37.2, +P-10028,E-10028-03,2025-04-15T21:04:29.418194,36.1,80,132,80,97.4,14,21.0,395,41.5,,,,0.9,,4,27,22,374,8.0,,37.2, +P-10028,E-10028-03,2025-04-16T01:21:03.704439,36.2,101,128,86,98.9,16,21.0,387,,,,,1.4,,5,,,364,7.8,,37.2, +P-10028,E-10028-03,2025-04-16T02:07:23.743829,37.1,66,142,79,97.1,16,42.4,471,35.1,209,137,0.42,,10,,25,23,438,10.4,,37.2, +P-10028,E-10028-03,2025-04-16T02:19:33.752214,36.5,84,135,93,97.8,14,21.0,480,42.2,,,,,,,26,,452,9.7,,37.2, +P-10029,E-10029-01,2025-04-14T17:27:19.752189,36.9,75,124,72,97.3,19,31.2,352,38.2,,,0.31,1.0,9,,,,323,5.8,,26.4, +P-10029,E-10029-01,2025-04-14T18:09:35.605384,36.2,71,125,73,96.8,17,21.0,369,43.4,,,,0.8,,7,,,354,5.6,20,26.4, +P-10029,E-10029-01,2025-04-14T19:04:27.675627,36.7,82,142,89,96.1,18,21.0,478,,,,,0.8,,,,,460,7.3,,26.4, +P-10029,E-10029-01,2025-04-14T21:12:59.851892,36.9,84,120,73,96.2,18,21.0,485,41.8,,,,,,5,,,449,8.0,,26.4, +P-10029,E-10029-01,2025-04-14T23:37:52.824442,37.7,56,117,80,94.0,16,21.0,541,35.3,,,,1.2,,5,,,503,8.9,,26.4, +P-10029,E-10029-01,2025-04-14T22:48:01.735364,36.4,71,112,78,98.2,18,21.0,499,39.4,,,,,,5,,,482,8.2,,26.4,1 +P-10029,E-10029-01,2025-04-14T21:48:25.598378,36.8,66,115,83,96.7,22,21.0,414,,,,,,,,33,,389,6.8,,26.4, +P-10029,E-10029-01,2025-04-15T05:40:21.148427,36.5,66,128,80,98.8,20,21.0,526,40.0,327,308,,1.2,,5,,,474,8.0,,26.4,1 +P-10029,E-10029-01,2025-04-14T23:50:25.709797,38.1,46,113,79,98.2,16,21.0,537,39.1,274,187,,,,,,,516,8.2,,26.4, +P-10029,E-10029-01,2025-04-15T10:32:42.902808,36.6,85,124,80,99.4,20,29.8,447,,,,0.3,,2,,,,408,7.3,,26.4, +P-10029,E-10029-01,2025-04-15T07:53:12.060234,38.1,74,118,77,98.1,19,25.1,397,43.6,,,0.25,,2,8,27,23,359,6.5,,26.4, +P-10029,E-10029-01,2025-04-15T05:07:23.093499,36.5,94,111,71,93.1,17,21.0,549,43.2,344,203,,,,,34,21,505,8.4,,26.4, +P-10029,E-10029-01,2025-04-15T00:49:53.090233,36.1,81,112,79,98.5,12,21.0,404,37.1,287,230,,,,,30,,377,6.2,,26.4, +P-10029,E-10029-01,2025-04-15T14:49:04.064276,36.7,84,117,82,99.4,19,21.0,532,,,,,1.0,,,29,27,522,8.7,,26.4, +P-10029,E-10029-01,2025-04-15T01:12:36.704403,36.9,76,112,71,98.9,15,21.0,484,,,,,,,,,,450,7.4,,26.4, +P-10029,E-10029-02,2025-04-15T17:27:19.752189,36.6,79,139,84,98.5,13,21.0,546,44.1,,,,0.9,,,27,,500,8.3,,26.4, +P-10029,E-10029-02,2025-04-15T18:12:11.562854,36.3,70,113,81,97.0,12,21.0,517,,,,,,,,32,28,465,7.9,,26.4, +P-10029,E-10029-02,2025-04-15T19:27:36.347890,37.2,79,132,88,96.7,20,21.0,518,,,,,1.4,,,,,516,7.9,,26.4, +P-10029,E-10029-02,2025-04-15T21:36:16.847512,37.0,74,121,89,98.0,18,21.0,522,37.7,,,,,,,,,512,8.0,,26.4, +P-10029,E-10029-02,2025-04-15T22:46:38.827449,37.1,72,125,73,96.1,15,21.0,355,,,,,,,5,28,,324,5.8,,26.4,1 +P-10029,E-10029-02,2025-04-15T23:13:54.031940,36.6,77,125,74,96.5,18,21.0,532,,336,309,,0.9,,,30,27,487,8.1,68,26.4, +P-10029,E-10029-02,2025-04-16T02:56:59.137745,36.4,73,125,83,97.5,12,26.3,390,40.7,,,0.26,1.5,9,4,35,,378,6.0,,26.4,1 +P-10029,E-10029-02,2025-04-16T04:47:45.589534,36.1,53,111,70,98.8,16,21.0,515,,,,,,,5,33,,509,8.4,,26.4, +P-10029,E-10029-02,2025-04-16T03:25:25.596839,36.5,77,121,73,98.0,14,21.0,550,,,,,,,,32,20,506,8.4,,26.4, +P-10029,E-10029-02,2025-04-16T06:38:29.319293,36.8,97,112,73,99.0,19,21.0,469,40.3,,,,1.5,,5,,,435,7.7,,26.4, +P-10029,E-10029-02,2025-04-16T08:18:10.652236,36.2,80,121,77,96.2,17,21.0,430,,,,,1.1,,,,,409,7.0,,26.4,1 +P-10029,E-10029-02,2025-04-16T02:58:45.918224,36.8,72,111,73,96.7,17,21.0,414,,193,95,,,,,,,377,6.3,,26.4, +P-10029,E-10029-02,2025-04-16T14:48:42.028050,37.1,46,114,76,98.9,15,21.0,389,,,,,,,,,,383,6.4,,26.4, +P-10029,E-10029-02,2025-04-16T01:29:45.904376,36.9,70,124,85,99.0,19,21.0,457,,,,,1.3,,,,,417,7.0,,26.4, +P-10030,E-10030-01,2025-04-12T17:27:19.752189,36.4,67,140,99,97.0,20,21.0,410,37.0,,,,1.1,,,,,370,6.0,,18.2, +P-10030,E-10030-01,2025-04-12T19:09:07.422640,36.3,75,139,90,99.5,12,21.0,421,,,,,,,5,,,389,6.6,41,18.2, +P-10030,E-10030-01,2025-04-12T20:43:08.163367,37.1,78,140,80,98.3,14,31.1,417,38.8,481,441,0.31,,4,8,32,24,417,6.1,,18.2, +P-10030,E-10030-01,2025-04-12T20:31:42.094432,36.2,84,140,77,97.5,19,21.0,373,38.0,306,287,,1.1,,5,,,357,5.8,,18.2, +P-10030,E-10030-01,2025-04-13T01:11:16.039705,37.1,75,138,91,98.9,20,21.0,537,,112,98,,1.3,,,,,504,7.8,,18.2, +P-10031,E-10031-01,2025-04-13T17:27:19.752189,37.2,67,110,83,95.8,20,21.0,514,35.3,,,,,,8,,,509,9.9,,30.9, +P-10031,E-10031-01,2025-04-13T18:32:53.451301,37.1,45,118,77,98.8,19,21.0,418,38.0,,,,,,7,,,407,7.4,,30.9, +P-10031,E-10031-01,2025-04-13T20:33:50.659105,36.9,71,127,91,99.2,18,21.0,461,38.2,268,127,,1.3,,,,,449,8.9,,30.9,1 +P-10031,E-10031-01,2025-04-13T23:20:16.506544,36.9,65,124,83,96.9,12,21.0,508,,,,,,,6,30,22,469,9.0,,30.9, +P-10031,E-10031-01,2025-04-13T20:41:39.790650,37.1,80,133,84,96.8,20,21.0,525,38.9,,,,1.2,,,,,478,10.1,32,30.9, +P-10031,E-10031-01,2025-04-13T20:45:53.951701,36.6,75,120,75,97.3,19,43.9,366,40.8,,,0.44,,4,5,,,364,7.0,,30.9,1 +P-10031,E-10031-01,2025-04-13T20:35:33.581029,36.4,68,118,76,98.4,14,21.0,511,44.3,,,,0.9,,5,25,21,487,9.8,,30.9, +P-10031,E-10031-01,2025-04-13T23:25:02.360027,37.0,69,129,78,97.9,18,21.0,438,36.0,,,,0.9,,4,35,27,408,8.4,,30.9, +P-10031,E-10031-01,2025-04-14T05:13:56.906167,37.2,71,112,74,96.0,15,21.0,387,,,,,1.0,,,,,372,7.5,,30.9, +P-10031,E-10031-01,2025-04-14T02:28:59.062311,36.3,69,125,83,99.3,14,21.0,544,,242,,,1.0,,8,34,31,525,10.5,,30.9, +P-10031,E-10031-01,2025-04-14T03:35:25.081840,36.3,71,134,76,98.9,18,21.0,530,38.7,422,276,,1.4,,,34,29,524,10.2,,30.9, +P-10031,E-10031-01,2025-04-14T09:21:14.146873,37.1,73,125,85,98.9,12,21.0,493,44.5,,,,1.0,,7,25,22,469,9.5,,30.9, +P-10031,E-10031-01,2025-04-14T07:36:30.137304,37.0,43,112,75,97.9,17,21.0,540,44.9,,,,,,7,,,499,9.6,23,30.9, +P-10031,E-10031-02,2025-04-12T17:27:19.752189,36.4,76,120,70,97.3,16,21.0,483,,,,,0.9,,6,27,,474,9.3,33,30.9, +P-10031,E-10031-02,2025-04-12T19:11:53.821308,36.1,84,113,72,91.9,17,21.0,542,39.0,,,,,,,,,497,10.4,,30.9, +P-10031,E-10031-02,2025-04-12T21:11:08.429864,36.9,65,142,92,97.0,17,21.0,427,37.9,488,423,,,,,,,403,8.2,32,30.9, +P-10031,E-10031-02,2025-04-12T20:56:01.049005,36.0,76,140,88,98.1,19,21.0,494,,420,105,,,,,33,31,492,9.5,40,30.9, +P-10031,E-10031-02,2025-04-12T21:25:13.560916,36.2,66,117,76,96.0,12,21.0,448,37.6,186,,,0.8,,,,,404,7.9,,30.9, +P-10031,E-10031-02,2025-04-12T20:20:41.070191,36.6,76,130,79,93.1,12,21.0,363,43.7,,,,,,,,,340,6.4,,30.9, +P-10031,E-10031-02,2025-04-12T21:40:54.224393,36.9,66,111,83,99.0,13,21.0,495,,,,,,,,,,452,9.5,26,30.9,1 +P-10031,E-10031-02,2025-04-13T05:40:48.411598,37.5,66,110,80,96.8,19,44.8,419,41.1,,,0.45,0.9,6,,,,409,7.4,,30.9,1 +P-10031,E-10031-03,2025-04-10T17:27:19.752189,36.6,50,116,84,97.4,14,21.0,509,35.3,,,,,,,,,474,9.8,29,30.9,1 +P-10031,E-10031-03,2025-04-10T18:23:49.002378,37.1,79,119,84,96.6,19,21.0,466,37.2,365,332,,,,,29,29,451,9.0,,30.9, +P-10031,E-10031-03,2025-04-10T20:24:52.840820,36.9,77,138,85,96.9,13,21.0,485,,299,191,,,,4,29,21,469,9.3,,30.9, +P-10031,E-10031-03,2025-04-10T22:33:22.312407,36.7,97,125,93,97.2,16,21.0,461,35.2,312,,,,,6,30,25,440,8.9,,30.9, +P-10031,E-10031-03,2025-04-10T19:49:56.178752,36.6,71,111,74,97.2,20,21.0,549,44.8,152,,,1.0,,4,,,495,10.6,,30.9, +P-10031,E-10031-03,2025-04-10T22:53:57.556315,36.5,67,122,74,96.7,24,21.0,519,,,,,1.1,,,,,504,10.0,,30.9, +P-10032,E-10032-01,2025-04-15T17:27:19.753192,37.2,68,117,72,98.3,20,21.0,460,,389,198,,1.1,,,,,432,9.3,,22.4, +P-10032,E-10032-01,2025-04-15T18:40:23.328937,37.0,50,118,71,97.2,20,55.9,404,,386,298,0.56,0.9,8,8,30,20,402,9.0,,22.4, +P-10032,E-10032-01,2025-04-15T19:08:39.538593,36.0,85,137,79,96.9,17,21.0,420,41.2,,,,1.1,,,32,24,410,9.4,,22.4,1 +P-10032,E-10032-01,2025-04-15T23:02:31.030038,36.2,74,119,71,98.0,18,21.0,526,36.1,257,110,,1.4,,,,,473,10.7,,22.4, +P-10032,E-10032-01,2025-04-16T01:00:28.841983,37.2,112,125,93,97.5,14,21.0,504,,,,,1.4,,,,,488,10.2,,22.4, +P-10032,E-10032-01,2025-04-15T23:00:48.123843,36.5,83,125,85,96.0,19,21.0,398,44.0,,,,1.4,,,,,358,8.1,,22.4, +P-10032,E-10032-01,2025-04-16T03:02:03.510563,37.1,65,121,85,97.0,12,21.0,436,,,,,0.9,,,,,410,8.8,,22.4, +P-10032,E-10032-01,2025-04-15T22:13:11.363137,36.9,67,120,85,96.4,16,21.0,507,,374,350,,1.4,,6,30,25,476,11.3,74,22.4, +P-10032,E-10032-02,2025-04-07T17:27:19.753192,36.8,68,116,70,97.3,15,21.0,545,44.6,,,,,,5,26,25,532,12.2,38,22.4, +P-10032,E-10032-02,2025-04-07T18:40:01.520388,36.6,70,112,83,98.1,16,21.0,466,,445,288,,1.3,,8,35,29,431,10.4,,22.4, +P-10032,E-10032-02,2025-04-07T20:48:46.436609,36.3,79,111,72,97.2,18,21.0,534,,120,90,,0.9,,,,,492,11.9,,22.4,1 +P-10032,E-10032-02,2025-04-07T21:48:29.646668,36.4,80,125,70,98.0,19,21.0,504,36.9,,,,1.4,,7,32,24,468,10.2,,22.4, +P-10032,E-10032-02,2025-04-07T20:27:49.880477,37.1,79,116,72,99.5,18,21.0,397,,,,,1.0,,,,,362,8.1,,22.4,1 +P-10032,E-10032-02,2025-04-08T00:22:08.364297,37.1,68,132,89,96.6,17,29.3,366,,199,156,0.29,,4,,,,352,8.2,,22.4, +P-10032,E-10032-02,2025-04-08T00:19:50.445850,37.4,82,125,74,97.7,15,21.0,475,,,,,1.1,,5,31,,460,9.6,,22.4, +P-10032,E-10032-02,2025-04-08T01:14:50.281587,36.0,74,118,81,99.1,16,21.0,384,,178,162,,0.8,,5,,,376,7.8,,22.4, +P-10032,E-10032-02,2025-04-08T07:04:08.378520,36.9,108,136,86,98.9,16,21.0,483,36.2,,,,1.3,,,27,,480,10.8,,22.4, +P-10032,E-10032-02,2025-04-08T04:18:25.371347,36.3,73,118,78,96.9,19,21.0,412,,,,,,,4,,,407,8.4,,22.4, +P-10033,E-10033-01,2025-04-13T17:27:19.753192,36.5,79,124,79,97.4,19,21.0,385,42.8,,,,1.4,,,,,366,5.3,41,24.3,1 +P-10033,E-10033-01,2025-04-13T19:04:41.711329,36.3,75,124,70,96.1,14,21.0,457,38.1,,,,1.1,,,27,22,420,6.7,78,24.3, +P-10033,E-10033-01,2025-04-13T21:02:49.705788,36.7,65,131,82,96.1,14,21.0,474,,,,,,,,,,450,6.9,,24.3,1 +P-10033,E-10033-01,2025-04-13T22:05:25.147156,36.8,105,111,73,98.4,12,37.8,393,40.0,,,0.38,,8,,29,24,360,5.4,29,24.3, +P-10033,E-10033-01,2025-04-13T21:35:08.200387,37.0,78,124,82,99.2,20,21.0,439,36.0,,,,0.8,,,,,410,6.4,55,24.3, +P-10033,E-10033-01,2025-04-14T02:57:27.870535,36.8,75,136,93,95.0,14,21.0,405,,,,,,,,34,20,367,5.5,,24.3, +P-10034,E-10034-01,2025-04-12T17:27:19.753192,36.2,85,113,81,96.2,15,21.0,460,35.2,,,,1.3,,,32,,422,7.6,63,24.4, +P-10034,E-10034-01,2025-04-12T19:07:17.801061,36.4,84,121,81,98.8,19,21.0,455,39.8,437,405,,,,4,,,414,8.1,,24.4, +P-10034,E-10034-01,2025-04-12T19:41:59.998864,36.1,74,113,81,96.9,19,21.0,397,,,,,1.4,,,32,29,393,6.5,,24.4, +P-10034,E-10034-01,2025-04-12T22:07:06.034756,37.0,65,114,70,98.6,13,21.0,423,35.3,,,,,,,32,25,408,7.0,,24.4, +P-10034,E-10034-01,2025-04-13T01:15:48.175595,36.4,78,116,71,97.0,19,21.0,528,,,,,,,5,,,485,8.7,,24.4, +P-10034,E-10034-01,2025-04-12T23:06:53.900282,36.0,85,110,77,96.2,13,25.0,526,35.6,197,177,0.25,1.4,2,,28,20,474,8.7,,24.4,1 +P-10034,E-10034-01,2025-04-12T21:17:40.729715,36.2,53,125,89,97.4,13,21.0,390,,,,,1.5,,6,32,32,351,6.9,72,24.4,1 +P-10034,E-10034-02,2025-04-14T17:27:19.753192,36.5,70,118,74,96.0,15,21.0,467,43.0,,,,1.3,,5,,,428,8.3,,24.4, +P-10034,E-10034-02,2025-04-14T19:05:00.108482,37.0,76,125,84,98.0,16,21.0,522,,155,,,1.5,,6,32,,509,9.3,,24.4, +P-10034,E-10034-02,2025-04-14T19:03:40.410471,36.7,74,122,88,98.5,16,21.0,408,,131,121,,1.0,,6,,,381,6.7,,24.4, +P-10034,E-10034-02,2025-04-14T19:56:17.009505,36.8,71,117,83,98.6,18,21.0,364,40.9,,,,1.1,,4,,,333,6.5,,24.4, +P-10034,E-10034-02,2025-04-15T01:05:46.312296,37.0,80,122,71,95.6,19,21.0,518,,,,,0.8,,6,31,31,485,9.2,,24.4, +P-10034,E-10034-02,2025-04-14T23:31:00.398266,36.8,82,121,76,97.0,13,21.0,527,,,,,1.0,,,35,26,499,9.4,,24.4, +P-10034,E-10034-02,2025-04-15T01:03:30.525516,36.7,67,122,79,96.4,17,21.0,401,,,,,1.3,,6,25,,372,6.6,,24.4, +P-10034,E-10034-02,2025-04-14T22:52:00.875145,36.7,62,133,86,97.7,20,21.0,534,36.5,257,,,1.1,,,,,526,9.5,,24.4, +P-10034,E-10034-03,2025-04-07T17:27:19.753192,37.3,81,130,85,96.7,14,21.0,531,40.6,,,,1.3,,7,,,478,8.8,,24.4,1 +P-10034,E-10034-03,2025-04-07T18:17:05.502845,36.2,82,121,78,99.0,15,21.0,426,,,,,,,7,26,25,401,7.6,33,24.4, +P-10034,E-10034-03,2025-04-07T20:34:33.315553,36.4,71,114,83,99.5,20,21.0,399,42.0,,,,1.4,,8,,,396,7.1,,24.4,1 +P-10034,E-10034-03,2025-04-07T22:29:34.429972,36.5,80,135,93,97.8,18,21.0,449,41.3,328,169,,1.0,,5,28,24,446,7.4,,24.4, +P-10034,E-10034-03,2025-04-07T22:33:22.648169,36.6,76,119,71,97.0,13,21.0,497,,364,220,,,,,32,22,456,8.2,,24.4, +P-10034,E-10034-03,2025-04-08T00:37:56.293811,37.1,79,133,90,97.8,15,21.0,358,43.1,,,,1.2,,6,,,356,6.4,,24.4, +P-10034,E-10034-03,2025-04-08T01:12:48.113136,36.0,83,114,79,96.4,16,21.0,412,,194,138,,,,8,29,26,384,7.3,,24.4,1 +P-10034,E-10034-03,2025-04-08T06:16:16.444319,37.0,68,115,80,94.4,16,21.0,369,,,,,,,5,,,364,6.1,34,24.4, +P-10034,E-10034-03,2025-04-08T07:06:17.990851,36.1,81,114,72,97.8,18,44.4,509,,,,0.44,,5,5,,,495,9.1,,24.4, +P-10034,E-10034-03,2025-04-08T01:56:30.703876,36.4,45,125,82,97.4,16,33.1,502,36.8,,,0.33,,2,,,,499,8.3,,24.4, +P-10034,E-10034-03,2025-04-08T02:43:30.542837,36.5,77,123,73,97.7,17,21.0,398,42.9,,,,1.3,,7,,,388,6.6,,24.4,1 +P-10034,E-10034-03,2025-04-08T00:21:21.933629,37.6,83,115,71,96.8,13,21.0,535,42.6,228,118,,1.1,,,,,508,8.8,,24.4,1 +P-10035,E-10035-01,2025-04-12T17:27:19.753192,36.7,69,142,79,97.9,12,21.0,468,,,,,1.3,,,,,457,6.5,,19.4, +P-10035,E-10035-01,2025-04-12T19:22:19.620117,36.5,69,150,99,98.3,18,56.0,426,36.9,,,0.56,1.2,2,7,27,27,408,5.9,,19.4, +P-10035,E-10035-01,2025-04-12T19:40:42.186314,36.9,85,132,96,98.3,15,51.6,359,,,,0.52,0.9,6,,29,26,343,5.3,68,19.4, +P-10035,E-10035-01,2025-04-12T19:04:59.873819,37.5,81,141,85,99.5,18,21.0,437,,,,,,,4,,,436,6.5,,19.4, +P-10035,E-10035-01,2025-04-12T23:03:04.364018,37.7,77,140,93,98.9,20,36.5,397,42.7,,,0.36,1.1,9,,29,25,373,5.5,,19.4, +P-10035,E-10035-01,2025-04-13T02:48:42.814990,36.9,60,135,96,96.4,13,21.0,397,,,,,1.3,,,,,394,5.5,,19.4, +P-10035,E-10035-01,2025-04-12T23:25:00.867295,36.8,79,131,94,97.8,16,21.0,503,38.2,213,137,,,,7,,,484,7.4,,19.4, +P-10035,E-10035-01,2025-04-13T02:35:20.918475,36.4,70,138,82,98.4,18,21.0,451,38.8,406,102,,,,,28,26,438,6.3,,19.4, +P-10035,E-10035-01,2025-04-13T00:00:30.165587,36.2,79,145,88,99.0,14,21.0,496,,,,,1.4,,4,,,460,7.3,68,19.4, +P-10035,E-10035-01,2025-04-12T23:42:08.086226,36.9,82,128,95,96.0,16,21.0,371,,,,,1.2,,6,,,343,5.1,48,19.4, +P-10035,E-10035-01,2025-04-13T10:24:55.557881,36.0,63,142,95,94.5,15,21.0,540,43.6,,,,1.2,,,,,515,7.5,,19.4, +P-10035,E-10035-01,2025-04-13T13:32:23.912850,37.4,71,128,95,98.1,17,21.0,392,39.0,,,,1.1,,,,,352,5.8,44,19.4, +P-10035,E-10035-02,2025-04-12T17:27:19.753192,36.1,50,152,92,98.0,20,21.0,380,36.5,,,,1.0,,,,,355,5.6,,19.4, +P-10035,E-10035-02,2025-04-12T18:01:17.291180,37.0,65,121,88,97.2,20,21.0,407,43.0,410,163,,1.1,,,32,,374,5.6,,19.4, +P-10035,E-10035-02,2025-04-12T18:38:12.040450,37.1,83,131,94,98.3,16,21.0,390,,380,280,,1.2,,,30,26,373,5.4,37,19.4, +P-10035,E-10035-02,2025-04-12T21:19:58.060536,36.2,67,152,95,96.5,12,21.0,507,44.0,337,297,,,,,25,25,473,7.5,,19.4, +P-10035,E-10035-02,2025-04-12T22:37:02.490023,36.3,83,139,82,97.4,20,21.0,434,43.4,,,,0.9,,4,,,412,6.0,,19.4, +P-10035,E-10035-02,2025-04-13T02:24:19.815876,37.0,67,129,89,96.3,18,21.0,467,,,,,0.8,,,35,32,449,6.9,,19.4, +P-10035,E-10035-02,2025-04-13T01:16:55.835763,37.1,68,136,76,97.6,18,21.0,527,38.7,,,,,,,,,490,7.3,,19.4,1 +P-10035,E-10035-02,2025-04-13T02:44:46.146325,37.4,88,132,88,98.7,16,21.0,423,,,,,,,,,,416,6.3,,19.4,1 +P-10035,E-10035-02,2025-04-12T22:59:18.594728,36.3,82,134,95,98.6,14,21.0,424,,365,224,,1.1,,,,,411,5.9,,19.4, +P-10035,E-10035-02,2025-04-13T00:10:20.418403,36.2,66,146,86,98.8,16,21.0,525,,456,354,,,,,,,523,7.3,,19.4, +P-10035,E-10035-02,2025-04-13T10:07:40.235962,36.9,78,135,93,97.7,17,21.0,504,38.3,,,,,,,,,481,7.0,59,19.4,1 +P-10035,E-10035-02,2025-04-12T23:55:14.095272,37.1,58,133,96,93.7,20,48.5,376,,,,0.48,1.1,4,8,,,367,5.2,,19.4,1 +P-10035,E-10035-03,2025-04-15T17:27:19.753192,36.9,83,136,89,96.5,19,21.0,426,,367,269,,1.2,,,,,393,6.3,52,19.4, +P-10035,E-10035-03,2025-04-15T18:05:37.024091,36.8,53,149,90,96.8,12,21.0,503,,389,,,0.8,,5,,,500,7.0,,19.4,1 +P-10035,E-10035-03,2025-04-15T19:28:51.557133,36.8,71,160,91,97.4,19,21.0,396,36.1,,,,1.4,,,29,22,371,5.9,,19.4, +P-10035,E-10035-03,2025-04-15T19:10:43.108022,36.3,79,133,88,98.7,19,21.0,383,43.0,,,,1.2,,,,,344,5.3,42,19.4,1 +P-10035,E-10035-03,2025-04-15T20:15:31.650657,36.1,85,136,97,97.9,13,47.5,548,40.5,131,107,0.47,,2,5,,,495,8.1,,19.4,1 +P-10035,E-10035-03,2025-04-16T02:03:51.510598,36.0,75,139,95,96.7,20,21.0,393,,,,,1.5,,,,,393,5.8,,19.4, +P-10035,E-10035-03,2025-04-15T23:32:48.808448,36.7,73,152,102,98.2,19,21.0,370,43.9,,,,,,8,,,335,5.1,,19.4, +P-10035,E-10035-03,2025-04-15T23:35:06.797812,36.1,68,128,83,99.0,20,21.0,358,,,,,,,,,,336,5.0,,19.4,1 +P-10035,E-10035-03,2025-04-15T21:38:35.678633,36.5,84,128,88,97.4,16,21.0,406,39.2,,,,,,,34,,402,6.0,,19.4, +P-10035,E-10035-03,2025-04-16T02:10:39.411782,37.1,77,142,100,96.9,17,21.0,350,44.2,297,283,,1.4,,,,,317,4.9,,19.4, +P-10036,E-10036-01,2025-04-14T17:27:19.753192,36.9,77,123,83,97.5,19,21.0,492,,276,272,,,,8,,,450,9.9,62,27.7, +P-10036,E-10036-01,2025-04-14T19:05:00.534254,36.4,67,124,73,97.0,17,21.0,529,,342,137,,1.2,,,,,508,10.6,,27.7, +P-10036,E-10036-01,2025-04-14T20:19:02.259263,36.7,85,126,91,97.2,18,21.0,418,40.2,,,,1.3,,6,,,406,8.4,,27.7, +P-10036,E-10036-01,2025-04-14T21:59:15.487418,37.6,71,117,71,96.6,15,21.0,364,,,,,1.1,,,,,339,6.7,,27.7, +P-10036,E-10036-01,2025-04-15T00:55:10.850008,37.2,67,124,78,97.4,19,21.0,408,,,,,0.9,,,,,391,7.5,24,27.7, +P-10036,E-10036-01,2025-04-15T00:11:59.745773,36.7,77,130,86,98.5,16,21.0,368,,,,,0.9,,8,,,359,6.8,,27.7,1 +P-10036,E-10036-01,2025-04-15T00:35:03.886474,36.8,66,113,74,93.7,16,21.0,419,41.9,407,,,1.3,,4,,,402,8.4,,27.7, +P-10036,E-10036-01,2025-04-15T01:13:09.282158,36.3,68,110,74,98.2,14,45.0,382,42.9,,,0.45,,6,7,31,,347,7.0,74,27.7, +P-10036,E-10036-01,2025-04-15T00:18:45.615144,36.3,72,124,81,98.0,20,21.0,377,43.0,,,,1.2,,,25,,354,7.6,,27.7, +P-10036,E-10036-01,2025-04-15T06:35:00.171551,37.3,73,112,76,99.0,14,21.0,504,40.9,303,130,,1.3,,8,,,459,10.1,,27.7, +P-10036,E-10036-01,2025-04-15T09:58:15.672107,36.4,75,135,91,99.2,18,21.0,437,,242,132,,1.0,,5,28,28,434,8.0,,27.7, +P-10036,E-10036-01,2025-04-15T06:28:52.241061,36.1,71,115,72,96.3,15,21.0,537,44.5,,,,,,,26,,502,10.8,,27.7,1 +P-10036,E-10036-01,2025-04-15T16:50:19.770970,36.4,84,124,72,95.7,14,21.0,461,,237,177,,,,,,,425,8.5,,27.7,1 +P-10036,E-10036-01,2025-04-15T04:30:41.302630,37.1,77,115,80,96.4,20,21.0,363,,,,,1.2,,5,,,330,6.7,,27.7, +P-10036,E-10036-02,2025-04-09T17:27:19.753192,36.3,65,130,86,97.8,16,21.0,512,44.4,,,,0.8,,4,,,462,10.3,60,27.7, +P-10036,E-10036-02,2025-04-09T19:23:43.691534,36.4,84,121,73,97.5,17,21.0,391,36.4,428,337,,,,,30,30,375,7.9,,27.7, +P-10036,E-10036-02,2025-04-09T18:53:24.101072,36.9,47,118,76,98.5,14,21.0,428,39.0,,,,,,7,,,396,8.6,,27.7, +P-10036,E-10036-02,2025-04-09T22:24:40.502062,36.4,101,115,82,96.5,15,21.0,390,,,,,,,7,,,384,7.2,,27.7, +P-10036,E-10036-02,2025-04-09T19:36:36.713874,36.1,40,114,85,95.4,18,21.0,486,,228,81,,1.2,,4,,,476,9.0,,27.7,1 +P-10036,E-10036-02,2025-04-10T01:43:21.492868,36.2,83,120,77,98.2,19,21.0,514,,,,,,,,26,20,497,9.5,,27.7,1 +P-10036,E-10036-02,2025-04-10T01:40:18.309479,37.0,56,123,81,97.3,13,21.0,413,,,,,1.5,,8,,,387,8.3,,27.7, +P-10036,E-10036-02,2025-04-09T21:26:59.053415,36.8,65,114,76,98.2,17,35.9,537,,,,0.36,,10,,,,501,9.9,,27.7, +P-10036,E-10036-03,2025-04-07T17:27:19.753192,37.1,74,112,82,97.2,19,21.0,473,,,,,1.4,,,,,430,9.5,69,27.7,1 +P-10036,E-10036-03,2025-04-07T18:04:45.021773,37.0,66,138,78,98.0,17,21.0,494,43.4,,,,,,,,,472,9.1,,27.7, +P-10036,E-10036-03,2025-04-07T19:46:15.288532,37.2,60,124,76,96.7,22,50.3,545,44.3,,,0.5,,2,,,,499,10.9,,27.7, +P-10036,E-10036-03,2025-04-07T19:16:27.073097,36.8,73,139,88,97.1,14,21.0,370,44.5,,,,,,,25,21,339,6.8,,27.7, +P-10036,E-10036-03,2025-04-07T23:03:30.064341,36.9,73,120,80,97.6,17,21.0,448,,,,,,,,,,444,9.0,66,27.7, +P-10036,E-10036-03,2025-04-07T20:53:42.610527,36.6,85,115,80,94.5,12,21.0,389,38.7,,,,0.9,,,,,381,7.2,,27.7, +P-10036,E-10036-03,2025-04-07T22:23:08.514785,36.7,85,122,70,98.4,20,21.0,412,40.3,,,,1.0,,,28,23,412,8.3,,27.7, +P-10036,E-10036-03,2025-04-08T05:13:35.400819,36.3,102,112,76,98.7,20,27.8,384,,,,0.28,1.4,9,6,33,23,357,7.7,,27.7, +P-10036,E-10036-03,2025-04-08T02:19:19.227236,37.1,78,139,86,96.7,12,21.0,377,,,,,1.3,,6,33,,344,6.9,29,27.7,1 +P-10036,E-10036-03,2025-04-08T02:52:23.846543,36.5,68,119,70,98.7,15,21.0,546,,448,325,,0.9,,4,,,506,10.1,,27.7, +P-10037,E-10037-01,2025-04-15T17:27:19.754410,37.0,103,132,89,96.7,14,21.0,443,40.5,111,90,,,,,,,404,7.8,,36.6,1 +P-10037,E-10037-01,2025-04-15T18:14:32.400058,36.9,83,128,84,97.4,15,21.0,467,,395,,,,,7,33,25,445,8.2,77,36.6, +P-10037,E-10037-01,2025-04-15T18:47:52.267994,37.6,101,149,96,97.0,12,32.7,491,,,,0.33,0.9,10,7,,,484,9.4,,36.6, +P-10037,E-10037-01,2025-04-15T19:17:21.049260,37.3,78,126,86,97.6,20,21.0,519,,,,,1.5,,,,,502,9.2,,36.6,1 +P-10037,E-10037-01,2025-04-16T00:25:51.617456,37.1,87,153,96,97.5,17,21.0,363,39.3,,,,1.4,,6,,,352,7.0,,36.6, +P-10037,E-10037-01,2025-04-16T00:01:31.746831,37.9,90,152,102,97.4,14,35.4,526,,180,125,0.35,1.2,9,7,,,518,10.1,,36.6,1 +P-10037,E-10037-02,2025-04-13T17:27:19.754410,37.1,83,144,83,96.5,22,21.0,510,39.8,,,,1.0,,,,,506,9.8,,36.6, +P-10037,E-10037-02,2025-04-13T18:08:52.220768,36.8,76,138,90,98.3,16,21.0,537,,366,126,,1.2,,,33,,511,10.3,,36.6, +P-10037,E-10037-02,2025-04-13T19:56:47.052721,37.1,80,140,92,97.7,15,21.0,489,,,,,,,,,,455,8.6,,36.6, +P-10037,E-10037-02,2025-04-13T19:20:36.520914,37.6,96,136,88,97.5,13,21.0,449,,128,108,,1.2,,,,,409,8.6,,36.6, +P-10037,E-10037-02,2025-04-13T21:35:39.600602,37.4,86,135,84,99.1,17,21.0,537,40.7,469,446,,1.2,,6,29,27,499,9.5,,36.6,1 +P-10037,E-10037-02,2025-04-13T22:21:35.601498,36.8,91,130,86,98.2,13,59.0,395,40.7,,,0.59,0.9,8,,,,357,7.0,,36.6, +P-10037,E-10037-02,2025-04-14T00:06:41.675929,37.6,102,145,86,97.9,17,58.8,517,,,,0.59,1.4,3,,,,485,9.9,,36.6,1 +P-10037,E-10037-02,2025-04-14T00:24:53.469900,36.8,97,138,94,97.9,15,24.5,508,,353,,0.24,,3,,,,491,9.0,44,36.6,1 +P-10037,E-10037-02,2025-04-14T08:59:24.629141,36.7,90,143,79,96.6,15,33.4,421,,377,264,0.33,1.5,9,,,,398,7.4,,36.6, +P-10037,E-10037-02,2025-04-14T08:32:29.380294,36.4,101,142,91,99.1,17,21.0,421,,,,,0.9,,,,,389,8.1,,36.6, +P-10037,E-10037-02,2025-04-13T22:59:18.044853,37.9,82,146,77,98.1,13,21.0,496,,,,,0.9,,8,,,482,9.5,40,36.6, +P-10037,E-10037-02,2025-04-13T23:05:38.730370,37.5,88,141,79,95.8,16,21.0,512,,,,,,,,26,26,493,9.0,,36.6, +P-10037,E-10037-02,2025-04-14T15:25:50.440754,37.0,110,132,86,97.9,20,21.0,531,,,,,,,6,35,,507,9.4,,36.6, +P-10037,E-10037-02,2025-04-14T17:51:31.882414,36.9,100,153,86,97.5,12,21.0,445,42.4,106,83,,1.0,,,31,30,432,8.5,,36.6, +P-10037,E-10037-02,2025-04-14T09:35:53.757980,37.8,92,138,83,98.7,13,21.0,355,44.6,,,,0.8,,,25,21,327,6.8,77,36.6, +P-10037,E-10037-03,2025-04-16T17:27:19.754410,37.5,81,144,102,96.5,13,21.0,389,,,,,1.1,,8,,,381,7.5,,36.6,1 +P-10037,E-10037-03,2025-04-16T18:54:39.002160,37.5,79,142,89,97.3,17,21.0,530,,,,,,,6,31,29,518,10.2,,36.6,1 +P-10037,E-10037-03,2025-04-16T20:16:15.974576,37.5,82,135,90,98.0,14,21.0,436,44.6,,,,1.4,,,26,25,436,7.7,,36.6, +P-10037,E-10037-03,2025-04-16T23:09:05.704481,37.3,118,126,96,97.7,12,21.0,538,,123,90,,1.4,,,28,,532,9.5,,36.6, +P-10037,E-10037-03,2025-04-16T19:54:34.971063,37.4,89,132,81,96.2,17,21.0,439,38.6,,,,1.1,,,,,397,8.4,38,36.6,1 +P-10037,E-10037-03,2025-04-16T20:24:12.184293,37.2,80,135,89,97.4,17,21.0,379,,398,340,,1.3,,,,,352,7.3,,36.6,1 +P-10037,E-10037-03,2025-04-17T00:42:29.648369,37.4,83,138,93,97.6,20,21.0,519,40.9,,,,0.9,,,,,486,10.0,,36.6, +P-10037,E-10037-03,2025-04-16T23:11:24.930966,36.5,81,124,83,96.8,12,21.0,350,,488,136,,0.9,,,,,328,6.2,58,36.6, +P-10037,E-10037-03,2025-04-16T22:31:33.014746,36.7,122,148,97,95.0,22,49.5,505,,,,0.49,,7,7,,,469,9.7,,36.6, +P-10037,E-10037-03,2025-04-17T09:35:18.810943,36.8,87,132,90,98.4,15,21.0,444,43.1,291,279,,1.4,,4,27,,438,7.8,49,36.6, +P-10038,E-10038-01,2025-04-15T17:27:19.754410,36.1,82,144,90,92.6,12,21.0,425,43.8,,,,1.1,,,26,20,425,6.9,,28.3, +P-10038,E-10038-01,2025-04-15T18:00:37.915450,36.4,81,134,84,96.4,18,21.0,375,44.2,,,,1.2,,,,,368,5.7,51,28.3, +P-10038,E-10038-01,2025-04-15T20:16:14.938769,37.5,74,138,95,96.4,12,21.0,546,39.2,,,,,,,,,536,8.3,,28.3, +P-10038,E-10038-01,2025-04-15T20:19:46.783377,37.5,71,128,85,96.0,18,21.0,381,,,,,1.0,,,,,360,5.8,,28.3,1 +P-10038,E-10038-01,2025-04-15T23:58:38.760443,36.5,85,136,81,97.8,18,21.0,474,,373,364,,1.0,,4,,,473,7.2,,28.3, +P-10038,E-10038-01,2025-04-16T01:53:25.267234,36.2,84,140,95,97.4,16,21.0,438,37.7,,,,,,,,,436,7.1,,28.3,1 +P-10038,E-10038-01,2025-04-15T22:20:52.523666,37.0,89,149,91,97.8,12,28.5,510,,,,0.28,,10,,,,478,8.3,42,28.3,1 +P-10038,E-10038-01,2025-04-16T05:21:36.162174,36.3,42,125,91,99.2,15,21.0,495,42.4,180,119,,0.9,,5,27,27,478,7.5,,28.3, +P-10038,E-10038-01,2025-04-16T06:38:39.986531,36.3,76,136,90,98.9,12,21.0,414,,137,107,,,,,,,393,6.3,56,28.3,1 +P-10038,E-10038-01,2025-04-16T05:40:50.018229,36.1,71,145,89,97.6,16,21.0,524,40.5,,,,,,7,,,493,7.9,62,28.3, +P-10038,E-10038-01,2025-04-16T13:20:01.330562,36.1,80,132,85,97.7,15,21.0,387,44.5,168,124,,,,,35,,352,5.9,77,28.3, +P-10038,E-10038-01,2025-04-15T23:10:03.470020,37.0,75,129,89,99.3,19,21.0,425,,,,,1.4,,,29,24,412,6.9,63,28.3, +P-10038,E-10038-02,2025-04-13T17:27:19.754410,37.9,58,136,104,98.0,13,21.0,390,,188,125,,0.8,,6,,,352,5.9,,28.3, +P-10038,E-10038-02,2025-04-13T18:23:24.079661,36.9,83,121,88,98.4,14,21.0,491,41.5,,,,,,,,,471,8.0,,28.3,1 +P-10038,E-10038-02,2025-04-13T18:34:44.620167,36.3,83,147,90,98.0,15,41.4,543,,498,140,0.41,1.3,2,,,,540,8.2,,28.3,1 +P-10038,E-10038-02,2025-04-13T23:09:11.245734,37.1,92,130,76,97.4,19,21.0,512,,115,,,,,4,,,509,7.7,,28.3,1 +P-10038,E-10038-02,2025-04-13T20:52:40.886939,36.4,71,139,93,99.0,17,21.0,422,43.5,,,,1.5,,,31,,408,6.9,53,28.3,1 +P-10038,E-10038-02,2025-04-13T21:37:04.116369,36.3,72,131,88,97.2,17,21.0,361,38.5,,,,1.3,,,,,325,5.9,,28.3, +P-10038,E-10038-02,2025-04-14T05:26:30.048268,36.2,79,141,91,97.0,14,40.2,468,,,,0.4,,7,,32,24,465,7.6,,28.3, +P-10038,E-10038-02,2025-04-14T02:58:14.672395,36.1,71,141,99,99.2,20,21.0,500,,,,,0.8,,,34,,473,7.6,,28.3, +P-10038,E-10038-02,2025-04-14T01:55:37.899183,36.3,66,137,80,97.0,17,21.0,383,39.8,,,,,,,33,27,359,6.2,,28.3, +P-10038,E-10038-02,2025-04-14T00:21:08.894488,37.3,83,163,91,96.5,19,21.0,452,,,,,1.3,,7,28,25,442,7.3,,28.3, +P-10038,E-10038-02,2025-04-14T05:12:12.531932,36.2,70,132,79,98.8,20,21.0,453,,,,,1.1,,,,,419,6.9,,28.3, +P-10039,E-10039-01,2025-04-13T17:27:19.754410,36.8,66,115,81,97.0,18,21.0,540,35.1,,,,,,,,,496,10.6,80,39.0, +P-10039,E-10039-01,2025-04-13T18:16:21.206080,36.6,66,120,75,96.4,16,21.0,436,36.2,,,,,,,30,25,395,9.4,,39.0,1 +P-10039,E-10039-01,2025-04-13T20:25:54.324158,36.5,79,121,73,96.6,12,21.0,492,,446,282,,1.4,,7,,,456,9.7,,39.0,1 +P-10039,E-10039-01,2025-04-13T22:49:52.420304,36.5,70,115,70,98.9,13,21.0,534,,,,,1.1,,,,,511,10.5,,39.0, +P-10039,E-10039-01,2025-04-13T20:32:20.129028,37.0,72,115,79,99.2,14,21.0,415,,343,210,,,,,,,406,8.2,,39.0, +P-10039,E-10039-01,2025-04-13T22:43:13.865054,36.5,87,110,73,96.2,13,21.0,532,40.9,441,379,,,,,30,29,520,10.5,,39.0,1 +P-10039,E-10039-01,2025-04-14T02:12:13.398508,37.2,75,130,89,97.6,15,21.0,483,,243,,,1.3,,,,,448,9.5,47,39.0, +P-10039,E-10039-01,2025-04-14T02:29:57.747523,36.4,68,126,88,96.3,18,21.0,423,,424,231,,1.4,,8,,,386,9.1,,39.0, +P-10039,E-10039-01,2025-04-14T01:10:43.983137,36.5,73,127,82,98.7,14,21.0,458,,,,,,,4,,,444,9.9,,39.0, +P-10039,E-10039-01,2025-04-14T04:55:42.360304,36.2,79,117,79,97.9,19,21.0,404,,,,,,,7,,,396,8.7,,39.0,1 +P-10039,E-10039-01,2025-04-14T06:52:45.717502,36.9,54,118,84,92.3,13,56.2,438,40.8,,,0.56,,4,8,31,22,413,8.6,,39.0, +P-10039,E-10039-02,2025-04-09T17:27:19.754410,37.5,85,118,74,97.2,19,21.0,417,38.6,,,,,,7,35,,395,9.0,,39.0, +P-10039,E-10039-02,2025-04-09T18:00:28.575420,37.9,46,114,76,97.0,17,58.1,486,,252,243,0.58,1.3,3,4,33,,477,10.5,,39.0, +P-10039,E-10039-02,2025-04-09T20:19:36.028543,37.1,70,114,80,96.8,18,21.0,418,37.6,,,,1.0,,5,,,386,9.0,,39.0, +P-10039,E-10039-02,2025-04-09T21:13:32.014087,37.3,72,119,78,97.5,17,21.0,449,,,,,1.2,,,,,416,8.8,,39.0, +P-10039,E-10039-02,2025-04-09T21:13:39.510270,37.1,80,121,72,96.2,18,21.0,388,,144,,,,,,,,354,7.6,,39.0, +P-10039,E-10039-02,2025-04-10T00:59:30.002814,36.2,82,123,73,97.1,16,21.0,474,35.6,,,,,,,,,454,9.3,,39.0,1 +P-10039,E-10039-02,2025-04-09T21:48:31.189474,37.0,72,111,82,97.3,14,21.0,414,35.2,,,,0.9,,,,,372,8.1,77,39.0, +P-10039,E-10039-02,2025-04-10T02:08:36.243544,36.8,76,120,75,96.9,13,21.0,528,37.6,,,,,,5,,,502,11.4,79,39.0, +P-10039,E-10039-02,2025-04-10T05:44:49.766159,36.4,84,112,74,99.4,15,21.0,503,,,,,,,,,,456,9.9,,39.0, +P-10039,E-10039-02,2025-04-10T03:08:19.959992,36.8,65,119,82,96.3,17,21.0,535,38.4,,,,,,,,,524,10.5,,39.0,1 +P-10039,E-10039-02,2025-04-10T04:23:06.890425,36.4,112,121,78,96.3,13,21.0,535,,492,326,,0.9,,,35,20,516,11.5,33,39.0, +P-10039,E-10039-03,2025-04-09T17:27:19.754410,36.2,85,137,92,95.2,16,21.0,410,40.8,161,95,,1.2,,4,,,398,8.1,,39.0, +P-10039,E-10039-03,2025-04-09T18:39:20.853407,36.2,67,120,78,98.6,18,21.0,445,,228,223,,,,,31,22,440,8.8,,39.0, +P-10039,E-10039-03,2025-04-09T19:47:45.824678,37.6,91,112,75,96.6,18,33.5,435,,,,0.34,1.1,5,6,33,21,397,8.6,,39.0, +P-10039,E-10039-03,2025-04-09T20:04:50.818597,36.0,75,131,82,98.1,20,21.0,534,37.2,352,122,,,,,28,21,513,11.5,,39.0, +P-10039,E-10039-03,2025-04-09T20:54:46.179140,36.9,75,121,81,97.0,16,21.0,451,35.2,,,,,,,,,448,8.9,,39.0, +P-10040,E-10040-01,2025-04-12T17:27:19.754410,36.4,69,145,88,96.5,18,21.0,521,37.8,422,365,,0.9,,4,,,484,8.3,38,23.8,1 +P-10040,E-10040-01,2025-04-12T18:47:21.504758,37.1,53,158,102,97.4,13,21.0,447,,,,,,,6,,,415,6.7,,23.8,1 +P-10040,E-10040-01,2025-04-12T20:05:53.947249,37.1,77,148,84,97.0,16,21.0,495,36.7,,,,,,6,26,20,451,7.4,,23.8, +P-10040,E-10040-01,2025-04-12T22:57:28.679010,36.5,77,125,91,96.9,15,26.4,396,38.3,,,0.26,1.2,9,4,28,27,383,6.3,,23.8, +P-10040,E-10040-01,2025-04-12T21:11:09.901372,37.0,79,130,93,97.3,12,51.0,463,,,,0.51,1.2,3,,,,445,6.9,,23.8, +P-10040,E-10040-01,2025-04-13T02:07:46.970444,37.2,68,125,81,94.6,20,21.0,410,,,,,1.0,,,,,379,6.1,,23.8, +P-10040,E-10040-01,2025-04-13T04:54:21.146457,36.4,54,127,90,97.0,16,21.0,463,,,,,0.9,,,32,27,446,7.4,,23.8,1 +P-10041,E-10041-01,2025-04-15T17:27:19.754410,36.6,77,124,80,97.8,15,21.0,391,41.2,,,,0.8,,,26,,357,6.2,,24.4, +P-10041,E-10041-01,2025-04-15T18:08:31.689935,36.6,80,117,78,96.2,14,21.0,461,38.3,,,,1.3,,5,,,421,6.8,,24.4, +P-10041,E-10041-01,2025-04-15T20:26:51.622632,36.8,72,110,71,98.5,15,21.0,527,42.3,,,,1.4,,,,,519,7.8,,24.4, +P-10041,E-10041-01,2025-04-15T21:54:13.247710,37.0,103,119,70,99.4,13,21.0,390,,,,,1.1,,5,33,,380,6.2,,24.4, +P-10041,E-10041-01,2025-04-15T19:53:49.349029,37.1,69,115,80,97.3,12,21.0,493,,,,,,,,,,486,7.3,,24.4, +P-10041,E-10041-01,2025-04-15T21:02:28.984830,36.4,96,120,76,99.2,18,21.0,376,,434,279,,1.0,,8,,,339,5.6,,24.4, +P-10041,E-10041-01,2025-04-15T20:27:45.118304,36.5,65,115,82,96.5,14,21.0,397,39.4,457,,,,,5,,,383,5.9,,24.4, +P-10041,E-10041-01,2025-04-15T22:54:17.646524,37.1,74,124,84,96.3,17,21.0,515,40.9,,,,,,8,,,513,7.7,,24.4, +P-10041,E-10041-01,2025-04-15T23:45:25.137244,36.1,66,115,77,99.2,14,48.7,513,39.5,143,90,0.49,,3,4,25,20,503,8.2,,24.4, +P-10041,E-10041-01,2025-04-16T06:38:09.555704,36.8,43,124,82,97.8,20,21.0,431,36.4,,,,,,5,,,416,6.9,,24.4, +P-10041,E-10041-01,2025-04-16T00:28:06.857694,37.2,75,124,79,96.9,19,21.0,375,,150,131,,,,5,,,362,5.6,,24.4, +P-10041,E-10041-02,2025-04-09T17:27:19.754410,36.9,71,118,79,96.9,17,21.0,446,36.6,,,,,,4,33,,432,6.6,,24.4,1 +P-10041,E-10041-02,2025-04-09T18:42:59.373446,36.2,69,115,76,96.5,19,21.0,391,,259,145,,1.4,,,,,359,6.2,,24.4, +P-10041,E-10041-02,2025-04-09T18:50:34.295856,36.4,82,131,77,96.5,20,21.0,382,35.4,,,,1.0,,6,29,21,370,6.1,,24.4, +P-10041,E-10041-02,2025-04-09T21:22:19.331337,36.9,69,111,85,92.4,16,21.0,538,,270,183,,1.2,,8,33,,503,8.0,26,24.4, +P-10041,E-10041-02,2025-04-09T23:38:18.701170,37.5,84,137,89,97.4,17,21.0,368,,382,194,,1.3,,4,34,,362,5.5,,24.4, +P-10041,E-10041-02,2025-04-09T23:43:15.323762,36.2,67,113,83,96.6,20,24.4,474,,,,0.24,1.4,6,6,,,437,7.5,,24.4, +P-10041,E-10041-02,2025-04-10T00:55:11.632042,36.7,70,110,80,98.3,17,58.5,472,44.0,,,0.58,,6,8,34,20,453,7.5,78,24.4, +P-10042,E-10042-01,2025-04-16T17:27:19.755409,37.5,99,121,89,94.9,15,21.0,518,,391,173,,,,,26,,483,9.0,,20.3, +P-10042,E-10042-01,2025-04-16T18:57:51.208105,37.7,86,137,96,98.4,17,21.0,516,41.8,176,,,1.0,,,,,467,8.3,71,20.3, +P-10042,E-10042-01,2025-04-16T20:32:01.951223,36.7,88,141,80,96.2,12,21.0,358,41.9,,,,1.1,,,30,20,330,5.7,,20.3, +P-10042,E-10042-01,2025-04-16T19:07:39.661575,36.6,86,124,82,97.8,15,21.0,374,,,,,,,7,,,362,6.5,,20.3, +P-10042,E-10042-01,2025-04-16T23:09:45.091127,37.5,67,155,94,99.4,19,21.0,402,41.8,226,98,,1.4,,8,,,402,6.9,,20.3, +P-10042,E-10042-01,2025-04-17T01:10:48.251794,37.5,87,160,96,98.2,18,21.0,432,35.7,,,,1.3,,8,,,412,6.9,,20.3, +P-10042,E-10042-01,2025-04-17T05:26:32.132249,37.9,79,144,96,96.7,17,21.0,427,,,,,1.1,,,29,25,414,6.8,,20.3, +P-10042,E-10042-01,2025-04-17T04:03:05.058234,37.1,95,134,81,98.5,15,21.0,523,,,,,0.9,,,35,29,481,8.4,,20.3, +P-10042,E-10042-02,2025-04-16T17:27:19.755409,37.2,78,137,90,92.6,17,21.0,540,39.2,,,,1.4,,6,,,511,9.3,,20.3, +P-10042,E-10042-02,2025-04-16T19:18:43.481964,37.7,78,135,95,99.3,22,21.0,374,40.3,,,,,,5,29,,363,6.5,,20.3, +P-10042,E-10042-02,2025-04-16T20:35:46.060798,37.5,75,163,97,96.2,14,21.0,356,43.3,,,,1.0,,,28,,342,6.2,,20.3,1 +P-10042,E-10042-02,2025-04-16T19:52:03.383915,36.7,72,134,87,97.2,16,21.0,500,42.4,299,278,,,,,,,476,8.6,,20.3, +P-10042,E-10042-02,2025-04-16T22:19:56.806031,37.1,78,137,90,95.9,13,21.0,350,40.4,,,,,,,32,28,335,5.6,,20.3, +P-10042,E-10042-02,2025-04-16T21:57:53.915212,37.3,94,142,97,96.1,20,21.0,408,,160,147,,,,5,,,371,6.5,,20.3, +P-10042,E-10042-02,2025-04-17T04:08:00.742800,37.1,71,138,91,96.8,14,21.0,547,,,,,0.8,,,30,30,547,9.5,43,20.3,1 +P-10042,E-10042-02,2025-04-17T06:24:44.423416,37.4,79,140,77,96.6,15,21.0,467,,,,,,,8,,,462,8.1,,20.3, +P-10042,E-10042-02,2025-04-16T22:07:00.305440,37.2,100,137,89,98.0,18,21.0,392,,275,142,,0.8,,,,,367,6.3,,20.3, +P-10042,E-10042-02,2025-04-17T03:58:51.831903,37.4,82,135,96,99.2,16,21.0,364,,,,,0.9,,6,25,22,343,5.8,,20.3, +P-10042,E-10042-02,2025-04-17T03:50:52.250757,36.5,82,151,99,96.8,17,21.0,429,40.3,,,,1.4,,,,,409,6.9,,20.3, +P-10042,E-10042-02,2025-04-17T11:32:24.696274,37.2,78,129,83,94.3,17,21.0,462,,,,,,,,,,462,7.4,,20.3, +P-10042,E-10042-03,2025-04-13T17:27:19.755409,37.4,106,136,86,97.9,12,21.0,359,,,,,1.0,,,,,337,5.8,43,20.3,1 +P-10042,E-10042-03,2025-04-13T18:06:27.673119,36.9,92,140,100,98.4,17,21.0,386,40.2,,,,,,,,,381,6.2,,20.3,1 +P-10042,E-10042-03,2025-04-13T18:43:57.350051,36.8,88,129,86,96.1,20,21.0,439,,,,,1.1,,,34,20,437,7.6,,20.3, +P-10042,E-10042-03,2025-04-13T19:55:07.861902,37.0,47,141,84,93.3,21,21.0,477,35.3,494,171,,0.9,,7,34,23,469,8.2,,20.3, +P-10042,E-10042-03,2025-04-13T23:06:50.795569,38.0,85,134,80,96.0,18,21.0,360,41.7,,,,0.8,,,34,,352,6.2,,20.3, +P-10042,E-10042-03,2025-04-14T01:41:45.922226,37.7,111,158,96,97.9,19,21.0,456,35.5,,,,,,5,,,433,7.9,,20.3,1 +P-10042,E-10042-03,2025-04-14T03:34:18.019155,37.2,95,125,89,97.7,19,21.0,461,39.3,461,,,0.9,,5,,,415,7.4,,20.3, +P-10042,E-10042-03,2025-04-13T22:10:06.002741,37.5,85,148,86,97.4,19,21.0,400,37.5,119,107,,1.4,,8,,,363,6.9,,20.3, +P-10042,E-10042-03,2025-04-14T07:04:11.041565,37.2,94,141,91,96.8,13,21.0,538,,,,,1.3,,,26,25,504,9.3,66,20.3, +P-10042,E-10042-03,2025-04-14T02:28:11.250970,37.1,77,133,86,99.4,14,21.0,354,,,,,1.3,,5,35,28,354,6.1,45,20.3,1 +P-10042,E-10042-03,2025-04-14T07:45:26.121624,37.1,121,137,88,98.0,20,21.0,355,,122,83,,,,4,,,344,6.1,,20.3, +P-10042,E-10042-03,2025-04-14T04:45:32.292306,37.1,91,139,91,96.5,20,34.9,367,,,,0.35,,6,,,,349,6.3,79,20.3, +P-10042,E-10042-03,2025-04-14T02:25:30.590940,37.3,103,145,100,98.0,20,21.0,429,,400,128,,1.4,,4,31,,429,7.4,,20.3, +P-10043,E-10043-01,2025-04-12T17:27:19.755409,36.9,69,112,79,97.8,18,21.0,450,35.6,,,,1.4,,6,,,432,8.5,,35.9, +P-10043,E-10043-01,2025-04-12T19:06:33.731562,37.3,96,114,80,96.0,12,21.0,403,,,,,,,4,,,380,7.6,,35.9, +P-10043,E-10043-01,2025-04-12T19:02:53.123502,37.0,77,119,77,97.7,18,58.1,515,36.8,,,0.58,0.9,5,,33,,492,9.7,34,35.9, +P-10043,E-10043-01,2025-04-12T22:02:19.197379,37.0,76,116,79,96.2,13,21.0,506,44.9,450,391,,,,,31,21,458,10.4,,35.9, +P-10043,E-10043-01,2025-04-12T20:57:26.719568,36.3,76,125,70,98.8,15,21.0,363,35.6,,,,,,,30,22,331,6.8,,35.9,1 +P-10043,E-10043-01,2025-04-13T02:53:42.646467,37.1,77,136,91,97.4,14,46.6,362,35.2,343,87,0.47,1.1,6,,,,342,6.8,,35.9, +P-10043,E-10043-01,2025-04-13T02:01:17.765939,36.5,83,125,74,97.2,14,21.0,467,38.4,175,82,,1.3,,5,,,438,9.6,,35.9, +P-10043,E-10043-01,2025-04-13T06:35:27.589582,36.1,56,114,83,98.8,20,57.3,369,,383,345,0.57,,2,5,,,362,7.6,32,35.9, +P-10043,E-10043-01,2025-04-12T22:24:24.446871,36.9,74,115,76,97.2,17,21.0,502,,,,,1.3,,7,31,23,501,9.4,50,35.9, +P-10043,E-10043-01,2025-04-13T03:24:10.768258,37.0,76,110,76,97.7,18,28.7,455,,,,0.29,,10,,25,,416,8.5,,35.9, +P-10043,E-10043-01,2025-04-13T01:49:35.575856,36.9,77,115,79,97.2,14,21.0,401,,,,,1.1,,,25,20,392,7.5,,35.9, +P-10043,E-10043-02,2025-04-07T17:27:19.755409,36.8,84,111,84,98.4,12,21.0,484,37.6,398,154,,1.1,,7,,,460,9.9,,35.9,1 +P-10043,E-10043-02,2025-04-07T19:13:28.868499,36.1,74,116,76,99.3,19,21.0,474,42.1,,,,1.0,,,34,31,457,8.9,,35.9, +P-10043,E-10043-02,2025-04-07T18:44:53.134386,36.3,72,126,88,97.2,17,21.0,527,,441,299,,1.2,,,30,21,480,10.8,,35.9, +P-10043,E-10043-02,2025-04-07T19:03:32.968967,36.1,83,116,80,99.0,17,21.0,412,40.1,,,,1.0,,5,,,395,7.7,,35.9, +P-10043,E-10043-02,2025-04-07T20:13:25.973204,36.0,77,128,78,98.3,19,21.0,409,37.6,,,,1.5,,,,,392,7.7,23,35.9, +P-10043,E-10043-02,2025-04-08T00:05:15.007218,37.6,66,117,81,98.5,13,21.0,521,,108,86,,1.4,,8,25,20,491,9.8,,35.9, +P-10043,E-10043-02,2025-04-07T23:57:58.225571,36.8,71,130,91,97.1,12,21.0,522,,,,,1.2,,6,31,24,500,9.8,,35.9, +P-10043,E-10043-02,2025-04-07T21:37:43.870582,36.1,65,124,84,99.1,18,54.3,441,,,,0.54,,4,,,,403,9.0,,35.9, +P-10043,E-10043-02,2025-04-08T08:35:08.034079,36.9,69,125,74,96.2,15,21.0,403,,,,,1.0,,7,26,,394,7.6,,35.9, +P-10044,E-10044-01,2025-04-15T17:27:19.755409,36.5,82,155,88,97.3,18,21.0,423,43.7,,,,1.0,,,33,,406,8.1,,28.8, +P-10044,E-10044-01,2025-04-15T18:38:32.129588,37.1,81,161,92,98.6,19,21.0,426,,222,194,,1.3,,,,,409,7.5,,28.8,1 +P-10044,E-10044-01,2025-04-15T19:37:07.542568,36.8,74,141,102,96.4,20,21.0,508,,,,,,,7,,,495,9.0,27,28.8,1 +P-10044,E-10044-01,2025-04-15T20:22:08.220878,37.0,74,158,89,96.3,19,21.0,406,44.3,,,,1.3,,,,,390,7.8,69,28.8,1 +P-10044,E-10044-01,2025-04-15T22:00:46.986945,37.6,95,138,92,96.6,12,21.0,440,35.5,,,,,,,29,26,398,8.5,43,28.8, +P-10044,E-10044-01,2025-04-16T03:14:12.547042,36.8,79,142,90,99.2,19,34.3,517,,,,0.34,,9,,,,470,9.2,,28.8, +P-10044,E-10044-01,2025-04-16T01:23:03.581838,36.8,72,154,94,99.5,20,21.0,528,42.1,,,,,,,,,500,10.2,,28.8,1 +P-10044,E-10044-01,2025-04-16T06:03:43.481917,37.0,95,145,87,92.4,18,21.0,539,,428,134,,1.1,,,35,23,523,10.4,,28.8, +P-10044,E-10044-01,2025-04-15T22:45:41.004358,36.1,89,155,98,99.3,15,57.4,417,,294,237,0.57,1.2,2,5,35,,377,7.4,,28.8, +P-10044,E-10044-01,2025-04-16T00:25:07.088526,36.2,97,127,84,99.0,16,21.0,498,35.2,,,,1.4,,,,,479,8.8,,28.8,1 +P-10044,E-10044-01,2025-04-16T13:14:25.382481,36.9,102,127,89,99.3,18,21.0,421,,,,,1.5,,7,,,382,8.1,68,28.8, +P-10044,E-10044-01,2025-04-16T12:47:18.296633,37.0,72,159,96,97.5,17,21.0,500,,337,304,,1.3,,4,,,455,8.9,,28.8, +P-10044,E-10044-01,2025-04-16T10:50:00.144147,37.2,114,142,98,98.6,20,21.0,449,44.8,,,,1.1,,,,,421,8.6,,28.8,1 +P-10044,E-10044-01,2025-04-16T11:11:04.873950,36.1,105,123,89,98.9,18,21.0,433,36.2,131,97,,,,7,34,,410,8.3,,28.8, +P-10044,E-10044-01,2025-04-16T02:19:46.582088,36.9,118,136,93,92.4,12,42.5,474,41.4,479,272,0.42,1.5,5,,,,461,8.4,,28.8, +P-10044,E-10044-02,2025-04-11T17:27:19.755409,36.8,81,123,92,99.0,13,21.0,544,41.2,360,154,,1.3,,8,,,513,9.6,54,28.8, +P-10044,E-10044-02,2025-04-11T18:22:50.148848,36.6,85,121,77,92.9,17,21.0,437,,,,,1.2,,,,,418,8.4,,28.8, +P-10044,E-10044-02,2025-04-11T21:19:21.819036,37.6,84,135,89,99.0,17,21.0,438,,,,,1.2,,,,,435,8.4,,28.8,1 +P-10044,E-10044-02,2025-04-11T21:20:05.506029,36.8,89,131,96,97.8,20,56.8,418,40.4,349,240,0.57,,7,5,,,400,7.4,,28.8,1 +P-10044,E-10044-02,2025-04-11T23:49:12.038444,36.1,94,124,94,97.3,15,21.0,472,37.1,,,,,,,,,466,9.1,,28.8, +P-10044,E-10044-02,2025-04-11T22:31:08.125022,36.2,96,125,88,96.9,15,21.0,532,,318,,,1.4,,,29,29,487,10.2,68,28.8, +P-10044,E-10044-02,2025-04-12T03:44:44.542831,37.0,80,137,84,99.1,12,21.0,431,,,,,,,,32,32,425,7.6,,28.8, +P-10044,E-10044-02,2025-04-11T21:37:04.705562,36.1,81,162,93,99.1,15,21.0,479,40.8,240,,,0.9,,,,,467,8.5,,28.8, +P-10044,E-10044-02,2025-04-12T04:11:38.263104,36.6,99,154,94,97.9,14,21.0,443,44.7,327,,,,,,,,432,7.9,,28.8, +P-10044,E-10044-02,2025-04-12T07:35:40.279926,36.5,76,138,92,99.3,13,47.8,474,37.5,,,0.48,,5,8,,,430,8.4,,28.8, +P-10044,E-10044-02,2025-04-12T00:59:37.145528,36.4,93,133,84,99.5,19,21.0,507,40.7,237,202,,,,,,,484,9.8,,28.8, +P-10044,E-10044-02,2025-04-12T00:27:57.589855,36.2,87,128,84,97.1,20,21.0,446,44.1,,,,1.3,,,,,406,7.9,,28.8, +P-10044,E-10044-02,2025-04-12T10:13:59.341455,36.7,85,124,91,98.8,15,21.0,523,,,,,1.3,,,,,512,10.1,,28.8, +P-10044,E-10044-02,2025-04-12T19:18:36.308284,37.2,107,129,85,98.4,18,21.0,465,,194,,,,,,,,462,9.0,,28.8, +P-10045,E-10045-01,2025-04-13T17:27:19.755409,36.3,84,140,95,97.4,16,21.0,391,,236,,,1.5,,,27,,365,6.8,,22.1, +P-10045,E-10045-01,2025-04-13T19:21:13.294996,36.8,85,144,92,95.3,12,21.0,537,,,,,0.8,,,,,489,8.7,,22.1, +P-10045,E-10045-01,2025-04-13T18:56:55.454125,36.9,73,141,83,97.4,17,21.0,358,,,,,1.3,,,31,26,328,5.8,,22.1, +P-10045,E-10045-01,2025-04-13T22:18:50.911999,36.5,98,137,93,98.2,15,47.9,433,,,,0.48,0.8,8,,,,403,7.0,,22.1, +P-10045,E-10045-01,2025-04-13T22:51:08.108372,36.7,100,130,84,96.4,20,21.0,419,36.3,,,,1.3,,,,,417,6.8,,22.1, +P-10045,E-10045-01,2025-04-14T02:04:26.014522,36.7,98,131,88,91.7,14,21.0,350,,321,303,,0.8,,7,35,35,332,6.1,,22.1,1 +P-10045,E-10045-01,2025-04-13T22:29:11.172190,36.2,80,137,89,97.5,19,40.3,407,,,,0.4,1.1,6,,25,,386,6.6,,22.1, +P-10045,E-10045-01,2025-04-14T04:52:13.203650,36.9,92,138,90,98.5,19,21.0,467,,333,132,,1.2,,,,,463,8.2,,22.1,1 +P-10045,E-10045-01,2025-04-13T23:31:26.471587,36.0,88,135,87,97.4,13,29.5,377,35.6,,,0.29,1.3,10,,31,29,359,6.1,,22.1, +P-10045,E-10045-01,2025-04-14T07:40:04.693106,36.8,81,141,80,96.5,17,21.0,372,,,,,,,,,,357,6.0,,22.1, +P-10045,E-10045-01,2025-04-14T07:51:37.257554,36.7,88,138,86,97.9,15,21.0,524,37.1,,,,0.9,,8,,,505,8.5,,22.1, +P-10045,E-10045-01,2025-04-13T23:41:50.246971,37.1,75,128,89,97.8,12,21.0,532,,,,,,,,28,26,486,8.6,,22.1, +P-10045,E-10045-01,2025-04-14T10:05:14.219024,37.1,83,130,91,98.8,16,21.0,507,,205,,,1.0,,,31,30,477,8.9,,22.1, +P-10045,E-10045-01,2025-04-14T19:01:05.143919,36.9,76,130,78,99.0,13,21.0,366,42.3,,,,,,7,,,363,5.9,,22.1, +P-10046,E-10046-01,2025-04-13T17:27:19.756409,36.2,93,135,92,96.2,20,48.4,474,39.4,,,0.48,1.3,6,6,25,,462,7.2,,16.5, +P-10046,E-10046-01,2025-04-13T18:47:16.297265,37.2,89,147,93,99.1,20,24.2,424,39.6,,,0.24,1.1,2,,,,417,6.0,66,16.5,1 +P-10046,E-10046-01,2025-04-13T18:28:01.288440,36.4,86,157,90,96.3,16,37.5,371,41.7,,,0.38,,8,7,,,368,5.6,,16.5, +P-10046,E-10046-01,2025-04-13T19:50:37.631456,36.3,105,147,103,98.2,19,21.0,409,,210,190,,1.3,,,,,397,6.2,,16.5,1 +P-10046,E-10046-01,2025-04-14T01:20:49.135493,37.1,77,136,91,96.2,19,21.0,456,,,,,,,,,,449,6.5,48,16.5, +P-10046,E-10046-01,2025-04-14T02:14:31.979744,36.1,87,140,104,96.3,17,21.0,396,,,,,0.9,,,25,21,389,5.6,,16.5,1 +P-10046,E-10046-02,2025-04-14T17:27:19.756409,36.4,72,125,86,99.4,19,21.0,426,,,,,1.2,,,,,423,6.1,,16.5, +P-10046,E-10046-02,2025-04-14T19:15:54.778518,36.6,99,137,85,96.1,13,21.0,446,,327,,,1.3,,,31,,438,6.4,,16.5, +P-10046,E-10046-02,2025-04-14T21:26:30.560148,36.2,85,135,96,98.9,17,21.0,416,,,,,1.0,,8,,,403,5.9,,16.5,1 +P-10046,E-10046-02,2025-04-14T19:38:28.778039,36.4,50,128,88,98.7,19,21.0,526,35.9,,,,,,5,,,500,7.5,,16.5, +P-10046,E-10046-02,2025-04-14T20:19:03.577627,36.1,79,148,78,92.5,12,21.0,455,42.1,,,,,,,31,31,422,6.9,,16.5,1 +P-10046,E-10046-02,2025-04-14T23:57:19.276196,36.9,84,139,80,96.3,18,21.0,383,,125,124,,1.5,,,31,29,370,5.5,,16.5, +P-10046,E-10046-02,2025-04-15T00:26:09.142811,36.9,86,132,90,97.9,13,21.0,383,42.7,,,,1.2,,4,,,371,5.5,,16.5, +P-10046,E-10046-02,2025-04-15T04:43:41.380680,36.3,115,142,90,97.3,20,21.0,523,39.4,,,,,,,,,501,7.4,28,16.5, +P-10046,E-10046-02,2025-04-15T08:34:18.609578,37.1,74,137,86,98.8,12,21.0,470,44.3,,,,,,,,,423,7.2,,16.5, +P-10046,E-10046-02,2025-04-15T08:57:22.515609,36.1,85,133,96,99.2,13,21.0,504,,,,,,,6,35,26,465,7.2,79,16.5, +P-10046,E-10046-02,2025-04-14T23:45:30.219162,37.0,84,139,86,97.5,20,21.0,522,43.5,,,,1.1,,,,,519,7.9,,16.5,1 +P-10046,E-10046-02,2025-04-15T01:25:13.827384,36.2,81,123,82,97.7,16,21.0,509,38.2,,,,,,7,35,34,479,7.7,77,16.5,1 +P-10046,E-10046-02,2025-04-15T11:16:45.629222,36.7,59,127,89,98.9,17,28.1,449,44.1,,,0.28,1.5,5,6,,,432,6.4,,16.5, +P-10046,E-10046-02,2025-04-15T15:05:01.152516,37.1,86,139,102,98.2,18,21.0,490,44.6,360,183,,,,,26,25,445,7.5,,16.5, +P-10046,E-10046-02,2025-04-15T07:45:20.315012,37.5,96,133,93,98.8,13,21.0,366,39.7,,,,,,,,,335,5.2,74,16.5, +P-10046,E-10046-03,2025-04-05T17:27:19.756409,36.0,99,126,93,99.0,12,21.0,379,36.7,233,210,,,,,35,,362,5.4,,16.5,1 +P-10046,E-10046-03,2025-04-05T19:01:53.643112,37.6,90,144,91,98.8,16,21.0,360,,264,138,,1.5,,,,,337,5.1,,16.5, +P-10046,E-10046-03,2025-04-05T20:11:54.788485,36.7,99,146,84,92.3,20,21.0,474,,,,,1.0,,,,,463,6.7,,16.5, +P-10046,E-10046-03,2025-04-05T19:19:02.018935,36.8,86,142,77,93.9,17,21.0,480,,286,173,,1.0,,,28,26,457,6.8,,16.5,1 +P-10046,E-10046-03,2025-04-05T20:18:54.564017,36.7,75,123,84,96.1,19,21.0,381,,451,133,,,,,25,,350,5.8,,16.5, +P-10046,E-10046-03,2025-04-05T21:20:37.460581,37.1,105,138,76,96.3,17,40.2,372,36.2,,,0.4,,5,,35,24,346,5.7,,16.5, +P-10046,E-10046-03,2025-04-05T20:43:35.438607,37.1,81,132,88,92.4,17,21.0,422,42.7,,,,1.5,,,,,414,6.4,,16.5,1 +P-10046,E-10046-03,2025-04-06T05:11:57.588895,37.1,77,126,89,96.8,18,21.0,443,,270,121,,,,5,30,25,439,6.3,33,16.5, +P-10046,E-10046-03,2025-04-06T05:01:21.351436,36.6,93,152,98,99.0,19,21.0,390,39.3,,,,,,,26,,378,5.9,50,16.5, +P-10046,E-10046-03,2025-04-06T08:06:23.265156,36.3,74,134,85,98.0,18,21.0,364,44.1,,,,,,8,25,25,352,5.2,,16.5, +P-10046,E-10046-03,2025-04-06T12:46:25.199035,36.9,90,135,94,98.9,17,21.0,384,,,,,,,5,,,345,5.8,52,16.5, +P-10046,E-10046-03,2025-04-06T06:18:56.839601,36.6,85,148,96,99.4,13,21.0,478,,,,,1.3,,,,,473,6.8,,16.5,1 +P-10046,E-10046-03,2025-04-06T07:19:29.573933,36.1,78,140,93,98.3,15,21.0,399,36.4,,,,1.0,,8,,,384,6.1,69,16.5, +P-10046,E-10046-03,2025-04-06T14:28:52.253703,37.1,95,137,81,97.0,12,21.0,544,44.9,,,,,,5,,,537,7.7,46,16.5,1 +P-10047,E-10047-01,2025-04-15T17:27:19.756409,37.1,78,117,85,98.0,16,21.0,452,,157,92,,0.8,,5,,,426,10.3,,28.5, +P-10047,E-10047-01,2025-04-15T18:23:25.153899,37.0,68,117,78,98.0,17,21.0,365,,,,,1.3,,,,,351,7.5,,28.5,1 +P-10047,E-10047-01,2025-04-15T19:50:03.729428,36.0,74,122,83,96.4,18,21.0,422,,,,,0.9,,,26,23,415,9.6,37,28.5, +P-10047,E-10047-01,2025-04-15T21:59:02.021188,36.9,75,118,79,98.7,14,36.2,516,,335,309,0.36,,8,6,,,482,11.7,,28.5, +P-10047,E-10047-01,2025-04-15T23:20:17.289356,36.0,81,110,75,96.5,18,21.0,353,,234,233,,,,,35,29,341,8.0,,28.5, +P-10047,E-10047-01,2025-04-15T21:22:48.709640,37.0,76,118,71,96.3,14,21.0,454,37.8,,,,1.0,,6,25,25,426,9.3,,28.5, +P-10047,E-10047-01,2025-04-15T21:23:50.078434,37.1,66,115,71,96.4,15,37.3,477,,197,120,0.37,,2,,,,453,9.8,30,28.5, +P-10047,E-10047-01,2025-04-15T23:06:31.032132,36.4,74,111,80,98.9,18,21.0,531,,317,210,,1.4,,,,,519,12.0,71,28.5, +P-10047,E-10047-01,2025-04-15T21:50:32.014787,36.2,82,114,72,99.0,19,21.0,370,38.5,,,,,,6,,,362,8.4,52,28.5, +P-10047,E-10047-01,2025-04-15T23:53:15.251495,36.1,83,116,70,96.9,20,21.0,454,38.8,308,166,,1.0,,7,,,454,9.3,,28.5, +P-10047,E-10047-01,2025-04-16T11:23:18.022355,36.6,70,115,70,96.5,19,21.0,456,,477,422,,1.4,,,,,428,9.4,,28.5, +P-10048,E-10048-01,2025-04-16T17:27:19.756409,36.7,74,129,98,97.7,19,21.0,494,,418,,,1.4,,6,,,479,8.8,,31.2, +P-10048,E-10048-01,2025-04-16T18:15:56.673906,36.7,71,164,101,96.6,13,21.0,464,35.9,,,,1.5,,8,,,436,7.7,,31.2, +P-10048,E-10048-01,2025-04-16T20:26:22.878767,36.7,103,130,91,97.9,19,21.0,480,,278,232,,1.2,,,31,21,439,8.6,44,31.2, +P-10048,E-10048-01,2025-04-16T22:19:02.317582,36.1,72,141,86,96.5,18,21.0,524,,203,,,1.0,,,,,477,9.4,,31.2, +P-10048,E-10048-01,2025-04-16T21:36:59.982927,36.4,85,159,97,98.4,20,21.0,454,35.8,,,,1.0,,6,32,28,431,7.5,69,31.2, +P-10048,E-10048-01,2025-04-16T22:01:48.977643,36.2,86,134,90,97.4,13,21.0,424,,,,,1.4,,,,,424,7.0,29,31.2, +P-10049,E-10049-01,2025-04-15T17:27:19.756409,36.9,84,125,87,97.2,19,29.4,540,,353,80,0.29,1.1,10,8,,,489,11.4,,22.0, +P-10049,E-10049-01,2025-04-15T19:21:08.896623,36.7,71,136,85,96.9,19,21.0,515,,175,135,,1.1,,,29,22,472,10.8,27,22.0, +P-10049,E-10049-01,2025-04-15T20:46:50.993000,37.0,84,156,89,96.3,16,21.0,363,43.8,,,,,,,,,335,7.0,50,22.0,1 +P-10049,E-10049-01,2025-04-15T22:23:00.436601,36.0,75,129,90,99.1,20,21.0,418,39.9,,,,0.9,,,30,27,381,8.0,,22.0, +P-10049,E-10049-01,2025-04-16T00:38:56.060053,36.1,70,128,94,99.5,19,21.0,428,36.3,224,181,,,,6,,,394,9.0,47,22.0, +P-10049,E-10049-01,2025-04-15T22:08:26.869261,37.1,75,132,94,99.1,20,21.0,471,,,,,,,6,,,425,9.9,,22.0, +P-10049,E-10049-02,2025-04-11T17:27:19.756409,36.5,81,144,87,96.6,12,30.8,456,37.2,,,0.31,,5,,,,432,8.8,,22.0, +P-10049,E-10049-02,2025-04-11T17:59:32.465482,36.9,83,142,86,98.7,12,21.0,438,44.6,402,352,,,,4,,,429,9.2,,22.0,1 +P-10049,E-10049-02,2025-04-11T18:43:09.177832,37.0,81,129,96,99.0,16,21.0,416,,,,,0.8,,,,,380,8.7,,22.0, +P-10049,E-10049-02,2025-04-11T22:03:54.681589,36.9,78,138,85,98.9,14,21.0,420,,,,,1.2,,5,32,21,381,8.8,48,22.0,1 +P-10049,E-10049-02,2025-04-11T21:43:49.007104,36.5,80,158,98,97.7,18,21.0,500,,,,,1.0,,5,,,460,9.6,,22.0,1 +P-10049,E-10049-02,2025-04-12T00:21:12.016920,36.6,65,133,84,96.4,13,21.0,496,39.5,150,129,,,,4,,,494,10.4,,22.0, +P-10049,E-10049-02,2025-04-11T22:36:35.791168,36.3,65,145,95,98.0,15,21.0,439,41.0,475,,,1.4,,8,,,427,9.2,,22.0, +P-10049,E-10049-02,2025-04-11T23:14:54.212843,36.6,83,143,91,99.1,20,21.0,399,39.6,,,,0.9,,,,,369,7.7,,22.0, +P-10049,E-10049-02,2025-04-12T08:54:33.886339,37.6,83,142,84,99.2,12,21.0,427,36.5,,,,1.0,,,,,424,9.0,,22.0, +P-10049,E-10049-02,2025-04-11T22:12:45.446807,36.5,111,154,100,99.4,18,49.1,408,,364,229,0.49,1.1,5,,,,401,7.8,,22.0, +P-10049,E-10049-02,2025-04-12T06:41:15.888266,37.1,66,155,97,97.2,17,21.0,401,41.9,,,,1.1,,8,,,398,8.4,,22.0, +P-10050,E-10050-01,2025-04-12T17:27:19.756409,36.1,76,134,82,92.7,12,56.6,492,,,,0.57,0.8,6,,27,22,463,7.4,,18.1, +P-10050,E-10050-01,2025-04-12T18:45:46.130869,36.6,83,130,85,96.5,14,21.0,415,44.5,,,,1.3,,,,,393,6.7,65,18.1, +P-10050,E-10050-01,2025-04-12T18:42:08.714914,36.1,74,119,77,98.5,14,21.0,496,35.2,,,,1.2,,,27,,474,8.1,,18.1,1 +P-10050,E-10050-01,2025-04-12T22:16:13.814296,36.1,77,112,80,96.7,15,21.0,487,,,,,,,7,,,454,7.4,26,18.1, +P-10050,E-10050-01,2025-04-12T23:11:34.546531,36.9,73,117,74,96.7,19,42.7,528,,,,0.43,1.3,4,,,,479,8.6,53,18.1, +P-10050,E-10050-01,2025-04-12T23:31:17.863378,36.4,71,130,90,96.6,19,44.9,493,,117,92,0.45,0.9,4,,,,488,7.5,,18.1,1 +P-10050,E-10050-01,2025-04-13T00:17:07.905047,36.6,78,132,81,97.3,17,21.0,376,38.1,321,147,,1.1,,5,27,,356,5.7,,18.1, +P-10050,E-10050-01,2025-04-13T00:22:23.368560,36.6,43,120,85,96.4,12,21.0,353,41.6,,,,0.9,,,,,338,5.7,,18.1, +P-10050,E-10050-01,2025-04-12T23:17:53.094328,36.8,81,123,75,96.0,20,21.0,457,,,,,,,,30,27,431,7.4,68,18.1, +P-10050,E-10050-01,2025-04-13T06:00:52.925135,37.0,83,112,82,96.5,14,21.0,381,36.6,,,,,,5,,,378,6.2,,18.1, +P-10050,E-10050-01,2025-04-13T09:17:50.787463,36.0,75,140,80,96.9,14,21.0,387,43.5,,,,1.1,,,33,26,387,5.9,,18.1, +P-10050,E-10050-01,2025-04-13T00:02:24.713092,37.9,78,120,83,99.3,15,21.0,455,39.6,,,,,,4,,,436,7.4,,18.1, +P-10050,E-10050-01,2025-04-13T09:40:45.315421,36.3,75,114,81,99.2,16,21.0,431,35.1,210,144,,,,,27,23,400,7.0,69,18.1, +P-10050,E-10050-01,2025-04-13T09:37:40.982467,36.9,66,131,76,98.9,18,21.0,355,43.4,,,,1.0,,6,26,20,331,5.8,,18.1, +P-10050,E-10050-02,2025-04-14T17:27:19.756409,36.0,70,134,85,98.2,19,21.0,392,,,,,1.5,,8,34,,368,5.9,27,18.1,1 +P-10050,E-10050-02,2025-04-14T19:11:06.736715,36.8,85,112,80,97.6,19,21.0,513,38.3,196,137,,0.9,,,,,466,8.3,55,18.1,1 +P-10050,E-10050-02,2025-04-14T19:42:53.928151,36.5,73,119,72,92.3,19,21.0,456,,443,436,,1.4,,7,,,447,6.9,,18.1, +P-10050,E-10050-02,2025-04-14T19:55:12.157542,36.9,52,135,87,98.7,15,21.0,510,,127,111,,,,,34,,500,7.7,45,18.1, +P-10050,E-10050-02,2025-04-14T21:10:14.961959,37.2,78,125,82,98.9,12,42.8,519,,,,0.43,1.0,3,6,,,476,8.4,51,18.1, +P-10050,E-10050-02,2025-04-15T00:55:32.738461,36.9,70,125,82,98.1,18,21.0,542,,,,,,,,,,510,8.2,,18.1, +P-10050,E-10050-02,2025-04-14T23:23:08.853697,37.1,84,117,78,94.1,16,21.0,534,,258,249,,,,6,30,26,521,8.7,,18.1, +P-10050,E-10050-02,2025-04-14T22:02:34.956920,36.8,83,119,78,99.0,20,21.0,449,,,,,,,,32,29,449,7.3,,18.1, +P-10050,E-10050-03,2025-04-11T17:27:19.756409,36.1,73,119,75,99.3,14,21.0,426,,269,241,,1.2,,,26,,391,6.9,,18.1, +P-10050,E-10050-03,2025-04-11T18:19:57.951234,36.9,75,130,79,92.3,16,21.0,391,,,,,1.2,,,27,,353,6.3,,18.1,1 +P-10050,E-10050-03,2025-04-11T20:52:03.710132,37.0,79,129,90,97.1,19,21.0,380,44.8,,,,1.1,,7,27,,354,5.7,,18.1, +P-10050,E-10050-03,2025-04-11T22:32:21.148940,36.5,81,120,83,99.0,12,21.0,478,44.9,,,,,,,29,,464,7.2,,18.1,1 +P-10050,E-10050-03,2025-04-11T19:57:31.859392,36.5,66,113,81,97.5,13,21.0,474,,,,,,,8,34,34,457,7.2,,18.1,1 +P-10050,E-10050-03,2025-04-11T21:33:42.305602,37.1,85,122,75,96.8,12,21.0,365,35.1,159,,,,,,29,21,337,5.9,75,18.1, +P-10050,E-10050-03,2025-04-12T05:17:09.407181,36.5,83,113,81,96.0,13,21.0,525,39.3,,,,1.5,,,,,480,8.5,,18.1, +P-10050,E-10050-03,2025-04-12T01:32:09.593991,36.2,81,123,90,99.3,20,21.0,468,39.6,106,80,,1.4,,7,,,465,7.1,,18.1, +P-10050,E-10050-03,2025-04-12T01:00:12.260038,36.5,82,124,74,93.8,20,45.5,491,,,,0.46,0.9,2,,26,21,484,7.4,,18.1,1 +P-10050,E-10050-03,2025-04-12T02:19:41.907356,36.5,79,125,83,98.7,18,42.2,391,,,,0.42,1.1,7,,26,26,353,6.3,63,18.1, +P-10050,E-10050-03,2025-04-11T23:08:27.745643,36.3,78,114,83,98.4,16,21.0,380,,,,,1.3,,,,,365,6.2,,18.1, +P-10050,E-10050-03,2025-04-12T03:03:32.252722,37.9,72,122,73,93.3,19,21.0,369,,,,,,,7,,,365,6.0,,18.1, diff --git a/generate_patients.py b/generate_patients.py index 922baf6460d445f7970c128c3e3ef6809c8c8428..25abf0c7414ff24975fd986234e67fa1ec492cf1 100644 --- a/generate_patients.py +++ b/generate_patients.py @@ -6,18 +6,22 @@ import random # Bạn cần cài đặt thư viện Faker: pip install Faker from faker import Faker import os -from datetime import datetime # Cần datetime để thêm cột thời gian +from datetime import datetime, timedelta # Thêm timedelta # Lấy đường dẫn thư mục của script hiện tại script_dir = os.path.dirname(os.path.abspath(__file__)) -output_file = os.path.join(script_dir, 'patients_data.csv') # Đổi tên file thành .csv + +# Đổi tên file output mặc định +new_patients_output_file = os.path.join(script_dir, 'new_patients_upload.csv') +encounter_output_file = os.path.join(script_dir, 'encounter_measurements_sample.csv') fake = Faker('en_US') # Có thể dùng 'vi_VN' nếu muốn tên Việt def generate_measurement(age): """Tạo dữ liệu đo lường ngẫu nhiên nhưng hợp lý theo độ tuổi""" measurements = {} - measurements['measurementDateTime'] = datetime.utcnow().isoformat() # Thêm thời gian đo + # Bỏ measurementDateTime ở đây, sẽ thêm vào file encounter + # measurements['measurementDateTime'] = datetime.utcnow().isoformat() # Nhiệt độ (Temperature): 36.0 - 37.5, có thể cao hơn nếu già/bệnh base_temp = random.uniform(36.0, 37.2) if age > 70 or random.random() < 0.1: # 10% chance of higher temp @@ -77,33 +81,32 @@ def generate_measurement(age): measurements['pip'] = random.randint(25, 35) if random.random() < 0.4 else '' measurements['sip'] = random.randint(20, measurements['pip']) if measurements['pip'] and random.random() < 0.7 else '' measurements['tidal_vol_actual'] = random.randint(int(measurements['tidal_vol']*0.9), measurements['tidal_vol']) if measurements['tidal_vol'] else '' - measurements['tidal_vol_kg'] = '' # Sẽ tính toán sau dựa trên weight + measurements['tidal_vol_kg'] = '' # Sẽ tính toán sau dựa trên weight/height thực tế khi xử lý measurements['tidal_vol_spon'] = random.randint(20, 80) if random.random() < 0.2 else '' - measurements['bmi'] = '' # Sẽ tính toán sau dựa trên height/weight - - # Thêm referral ngẫu nhiên (yes/no) + measurements['bmi'] = '' # Sẽ tính toán sau dựa trên height/weight thực tế khi xử lý measurements['referral'] = 1 if random.random() < 0.25 else 0 # 25% cần referral ban đầu return measurements -def generate_patient_rows(num_patients=50): +def generate_new_patient_upload_rows(num_patients=50): + """Tạo dữ liệu cho file upload bệnh nhân mới (không ID, không đo lường)""" patient_data_rows = [] blood_types = ['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-'] genders = ['male', 'female', 'other'] - # Định nghĩa Header CSV cho file upload bệnh nhân + # Định nghĩa Header CSV cho file upload bệnh nhân mới header = [ - 'patientID', 'firstName', 'lastName', 'age', 'gender', 'height', 'weight', 'blood_type', - # Thêm các trường đo lường ban đầu vào cùng hàng - 'measurementDateTime', 'temperature', 'heart_rate', 'blood_pressure_systolic', - 'blood_pressure_diastolic', 'oxygen_saturation', 'resp_rate', 'fio2', - 'tidal_vol', 'end_tidal_co2', 'feed_vol', 'feed_vol_adm', 'fio2_ratio', - 'insp_time', 'oxygen_flow_rate', 'peep', 'pip', 'sip', 'tidal_vol_actual', - 'tidal_vol_kg', 'tidal_vol_spon', 'bmi', 'referral' + 'firstName', 'lastName', 'age', 'gender', 'height', 'weight', 'blood_type' ] + patient_ids_generated = [] # Lưu lại ID để dùng cho file encounter + for i in range(num_patients): - patient_id_val = f"P-{i+1:05d}" + # Sửa lại để bắt đầu từ 10001 + patient_id_num = i + 10001 + patient_id_val = f"P-{patient_id_num:05d}" # Tạo ID dạng P-10001, P-10002... + patient_ids_generated.append(patient_id_val) + age = random.randint(18, 90) gender = random.choices(genders, weights=[48, 48, 4], k=1)[0] @@ -125,9 +128,8 @@ def generate_patient_rows(num_patients=50): weight = max(40, min(150, weight)) # Đảm bảo cân nặng hợp lý - # Tạo thông tin bệnh nhân cơ bản + # Tạo thông tin bệnh nhân cơ bản (không có patientID) patient_info = { - "patientID": patient_id_val, "firstName": firstName, "lastName": lastName, "age": age, @@ -137,44 +139,119 @@ def generate_patient_rows(num_patients=50): "blood_type": random.choice(blood_types) } - # Tạo thông tin đo lường ban đầu - initial_measurement = generate_measurement(age) - - # Tính toán các giá trị còn thiếu - if height > 0: - height_m = height / 100 - bmi_value = round(weight / (height_m * height_m), 1) - initial_measurement['bmi'] = bmi_value - if initial_measurement.get('tidal_vol'): # Dùng .get() để tránh lỗi nếu key không tồn tại - try: - ideal_body_weight = (height - 152.4) * 0.91 + (50 if gender == 'male' else 45.5) # Công thức tham khảo - if ideal_body_weight > 0: - initial_measurement['tidal_vol_kg'] = round(initial_measurement['tidal_vol'] / ideal_body_weight, 1) - except: # Bắt lỗi nếu tính toán thất bại - initial_measurement['tidal_vol_kg'] = '' - - - # Kết hợp thông tin bệnh nhân và đo lường thành một hàng dữ liệu CSV + # Tạo hàng dữ liệu CSV row_data = [ - patient_info.get(col, '') or '' for col in header[:8] # Lấy thông tin bệnh nhân - ] + [ - initial_measurement.get(col, '') or '' for col in header[8:] # Lấy thông tin đo lường, dùng '' nếu None + patient_info.get(col, '') or '' for col in header ] patient_data_rows.append(row_data) - return header, patient_data_rows + return header, patient_data_rows, patient_ids_generated # Trả về cả list patient IDs đã tạo + +# --- Hàm tạo CSV đo lường theo encounter (giữ nguyên logic, chỉ đổi tên file output) --- +def generate_encounter_measurements_csv(output_filename, patient_ids, num_encounters_per_patient=1, num_measurements_per_encounter=10): + encounter_data_rows = [] + print(f"Generating encounter measurements for {len(patient_ids)} patients...") + + # Định nghĩa Header CSV cho file đo lường encounter + header = [ + 'patientID', 'encounterID', 'measurementDateTime', 'temperature', 'heart_rate', + 'blood_pressure_systolic', 'blood_pressure_diastolic', 'oxygen_saturation', + 'resp_rate', 'fio2', 'tidal_vol', 'end_tidal_co2', 'feed_vol', 'feed_vol_adm', + 'fio2_ratio', 'insp_time', 'oxygen_flow_rate', 'peep', 'pip', 'sip', + 'tidal_vol_actual', 'tidal_vol_kg', 'tidal_vol_spon', 'bmi', 'referral' + ] + + for patient_id in patient_ids: + # Lấy tuổi, cân nặng, chiều cao giả định (giữ nguyên logic này vì không có dữ liệu gốc) + age = random.randint(30, 80) + weight = random.uniform(50, 100) + height = random.uniform(150, 180) + + # Tạo số encounter ngẫu nhiên cho mỗi bệnh nhân + actual_encounters = random.randint(1, num_encounters_per_patient) + + for i in range(actual_encounters): + # Lấy 5 số cuối từ patient_id (giả sử format là P-xxxxx) + patient_id_part = patient_id[-5:] if patient_id and patient_id.startswith('P-') and len(patient_id) >= 7 else patient_id + # Tạo encounterID dạng E-<5_số_cuối_patientID>-<số_thứ_tự_2_chữ_số> + encounter_id = f"E-{patient_id_part}-{i+1:02d}" # Format với 2 chữ số + start_time = datetime.utcnow() - timedelta(days=random.randint(1, 5 + i*5)) # Encounter cũ hơn + + # Tạo số measurement ngẫu nhiên cho mỗi encounter + actual_measurements = random.randint(5, num_measurements_per_encounter) + + for j in range(actual_measurements): + # Thời điểm đo tăng dần + measurement_time = start_time + timedelta(hours=j * random.uniform(0.5, 2.0)) + + # Tạo dữ liệu đo lường + measurement = generate_measurement(age) + measurement['measurementDateTime'] = measurement_time.isoformat() # Thêm thời gian đo + + # Cập nhật BMI, tidal_vol_kg (giữ nguyên logic) + if height > 0: + height_m = height / 100 + bmi_value = round(weight / (height_m * height_m), 1) + measurement['bmi'] = bmi_value + if measurement.get('tidal_vol'): + try: + ideal_body_weight = (height - 152.4) * 0.91 + (50 if random.random() > 0.5 else 45.5) # Giới tính ngẫu nhiên + if ideal_body_weight > 0: + measurement['tidal_vol_kg'] = round(measurement['tidal_vol'] / ideal_body_weight, 1) + except: + measurement['tidal_vol_kg'] = '' + + # Điều chỉnh referral (giữ nguyên logic) + if j > actual_measurements // 2: + if random.random() < 0.4: + measurement['referral'] = 0 + else: + measurement['referral'] = 1 if random.random() < 0.3 else 0 + + # Tạo dòng dữ liệu cho CSV + row_data = [ + patient_id, + encounter_id + ] + [ + measurement.get(col, '') or '' for col in header[2:] # Lấy thông tin đo lường + ] + encounter_data_rows.append(row_data) + + # Ghi vào file CSV + try: + with open(output_filename, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(header) + writer.writerows(encounter_data_rows) + print(f"Successfully generated encounter measurements to {output_filename}") + except IOError as e: + print(f"Error writing to file {output_filename}: {e}") if __name__ == "__main__": - num = 50 - print(f"Generating {num} patient records for CSV...") - csv_header, patient_rows = generate_patient_rows(num) + num_patients_to_generate = 50 + # Lấy đường dẫn file từ biến đã định nghĩa ở đầu file + # initial_output_file = os.path.join(script_dir, 'new_patients_upload.csv') + # encounter_output_file = os.path.join(script_dir, 'encounter_measurements_sample.csv') + + print(f"Generating {num_patients_to_generate} new patient records for upload...") + # Gọi hàm mới generate_new_patient_upload_rows + csv_header_new_patients, patient_rows_new, generated_patient_ids = generate_new_patient_upload_rows(num_patients_to_generate) + # Ghi file CSV bệnh nhân mới try: - # Ghi vào file CSV - with open(output_file, 'w', newline='', encoding='utf-8') as f: + with open(new_patients_output_file, 'w', newline='', encoding='utf-8') as f: writer = csv.writer(f) - writer.writerow(csv_header) # Ghi dòng header - writer.writerows(patient_rows) # Ghi tất cả các dòng dữ liệu - print(f"Successfully generated {num} patient records to {output_file}") + writer.writerow(csv_header_new_patients) + writer.writerows(patient_rows_new) + print(f"Successfully generated {num_patients_to_generate} new patient records to {new_patients_output_file}") except IOError as e: - print(f"Error writing to file {output_file}: {e}") + print(f"Error writing to file {new_patients_output_file}: {e}") + + # Tạo file CSV đo lường cho các bệnh nhân đã tạo (dùng ID đã tạo) + if generated_patient_ids: + generate_encounter_measurements_csv( + output_filename=encounter_output_file, + patient_ids=generated_patient_ids, + num_encounters_per_patient=3, # Mỗi BN có tối đa 3 encounter + num_measurements_per_encounter=15 # Mỗi encounter có tối đa 15 lần đo + ) diff --git a/migrations/versions/11d6fcd3a14d_refactor_encounter_and_measurement_.py b/migrations/versions/11d6fcd3a14d_refactor_encounter_and_measurement_.py new file mode 100644 index 0000000000000000000000000000000000000000..eca7ecbccbb641849874cc23b639bceeb161a0bb --- /dev/null +++ b/migrations/versions/11d6fcd3a14d_refactor_encounter_and_measurement_.py @@ -0,0 +1,229 @@ +"""Refactor Encounter and Measurement models with relationships + +Revision ID: 11d6fcd3a14d +Revises: +Create Date: 2025-04-17 13:48:23.589559 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '11d6fcd3a14d' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('userID', sa.Integer(), nullable=False), + sa.Column('email', sa.String(length=100), nullable=False), + sa.Column('password_hash', sa.String(length=128), nullable=True), + sa.Column('firstName', sa.String(length=50), nullable=False), + sa.Column('lastName', sa.String(length=50), nullable=False), + sa.Column('role', sa.Enum('Admin', 'Dietitian', name='user_roles_new'), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('last_login', sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint('userID'), + sa.UniqueConstraint('email') + ) + op.create_table('activity_logs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('action', sa.String(length=255), nullable=False), + sa.Column('details', sa.Text(), nullable=True), + sa.Column('timestamp', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.userID'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('dietitians', + sa.Column('dietitianID', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('firstName', sa.String(length=50), nullable=False), + sa.Column('lastName', sa.String(length=50), nullable=False), + sa.Column('status', sa.Enum('AVAILABLE', 'UNAVAILABLE', 'ON_LEAVE', name='dietitianstatus'), nullable=False), + sa.Column('email', sa.String(length=100), nullable=True), + sa.Column('phone', sa.String(length=20), nullable=True), + sa.Column('specialization', sa.String(length=100), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.userID'], ), + sa.PrimaryKeyConstraint('dietitianID'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('user_id') + ) + op.create_table('uploadedfiles', + sa.Column('fileID', sa.Integer(), nullable=False), + sa.Column('fileName', sa.String(length=255), nullable=False), + sa.Column('filePath', sa.String(length=255), nullable=False), + sa.Column('userID', sa.Integer(), nullable=False), + sa.Column('original_filename', sa.String(length=256), nullable=False), + sa.Column('file_size', sa.Integer(), nullable=True), + sa.Column('file_type', sa.String(length=64), nullable=True), + sa.Column('delimiter', sa.String(length=10), nullable=True), + sa.Column('file_encoding', sa.String(length=20), nullable=True), + sa.Column('status', sa.String(length=50), nullable=True), + sa.Column('process_start', sa.DateTime(), nullable=True), + sa.Column('process_end', sa.DateTime(), nullable=True), + sa.Column('total_records', sa.Integer(), nullable=True), + sa.Column('processed_records', sa.Integer(), nullable=True), + sa.Column('error_records', sa.Integer(), nullable=True), + sa.Column('error_details', sa.Text(length=16777215), nullable=True), + sa.Column('process_referrals', sa.Boolean(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('upload_date', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['userID'], ['users.userID'], ), + sa.PrimaryKeyConstraint('fileID') + ) + op.create_table('patients', + sa.Column('patientID', sa.String(length=20), nullable=False), + sa.Column('firstName', sa.String(length=50), nullable=False), + sa.Column('lastName', sa.String(length=50), nullable=False), + sa.Column('age', sa.Integer(), nullable=False), + sa.Column('gender', sa.Enum('male', 'female', 'other', name='gender_types'), nullable=False), + sa.Column('bmi', sa.Float(), nullable=True), + sa.Column('status', sa.String(length=20), nullable=True), + sa.Column('height', sa.Float(), nullable=True), + sa.Column('weight', sa.Float(), nullable=True), + sa.Column('blood_type', sa.String(length=10), nullable=True), + sa.Column('admission_date', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('dietitian_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['dietitian_id'], ['dietitians.dietitianID'], ), + sa.PrimaryKeyConstraint('patientID') + ) + op.create_table('encounters', + sa.Column('encounterID', sa.Integer(), nullable=False), + sa.Column('patient_id', sa.String(length=20), nullable=False), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('start_time', sa.DateTime(), nullable=False), + sa.Column('dietitian_id', sa.Integer(), nullable=True), + sa.Column('status', sa.String(length=50), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['dietitian_id'], ['users.userID'], ), + sa.ForeignKeyConstraint(['patient_id'], ['patients.patientID'], ), + sa.PrimaryKeyConstraint('encounterID') + ) + with op.batch_alter_table('encounters', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_encounters_start_time'), ['start_time'], unique=False) + + op.create_table('reports', + sa.Column('reportID', sa.Integer(), nullable=False), + sa.Column('userID', sa.Integer(), nullable=False), + sa.Column('patientID', sa.String(length=20), nullable=False), + sa.Column('reportDateTime', sa.DateTime(), nullable=False), + sa.Column('reportTitle', sa.String(length=100), nullable=False), + sa.Column('reportContent', sa.Text(), nullable=True), + sa.Column('status', sa.String(length=20), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['patientID'], ['patients.patientID'], ), + sa.ForeignKeyConstraint(['userID'], ['users.userID'], ), + sa.PrimaryKeyConstraint('reportID') + ) + op.create_table('physiologicalmeasurements', + sa.Column('measurementID', sa.Integer(), nullable=False), + sa.Column('encounterID', sa.Integer(), nullable=False), + sa.Column('patientID', sa.String(length=20), nullable=False), + sa.Column('measurementDateTime', sa.DateTime(), nullable=False), + sa.Column('end_tidal_co2', sa.Float(), nullable=True), + sa.Column('feed_vol', sa.Float(), nullable=True), + sa.Column('feed_vol_adm', sa.Float(), nullable=True), + sa.Column('fio2', sa.Float(), nullable=True), + sa.Column('fio2_ratio', sa.Float(), nullable=True), + sa.Column('insp_time', sa.Float(), nullable=True), + sa.Column('oxygen_flow_rate', sa.Float(), nullable=True), + sa.Column('peep', sa.Float(), nullable=True), + sa.Column('pip', sa.Float(), nullable=True), + sa.Column('resp_rate', sa.Float(), nullable=True), + sa.Column('sip', sa.Float(), nullable=True), + sa.Column('tidal_vol', sa.Float(), nullable=True), + sa.Column('tidal_vol_actual', sa.Float(), nullable=True), + sa.Column('tidal_vol_kg', sa.Float(), nullable=True), + sa.Column('tidal_vol_spon', sa.Float(), nullable=True), + sa.Column('temperature', sa.Float(), nullable=True), + sa.Column('heart_rate', sa.Integer(), nullable=True), + sa.Column('respiratory_rate', sa.Integer(), nullable=True), + sa.Column('blood_pressure_systolic', sa.Integer(), nullable=True), + sa.Column('blood_pressure_diastolic', sa.Integer(), nullable=True), + sa.Column('oxygen_saturation', sa.Float(), nullable=True), + sa.Column('bmi', sa.Float(), nullable=True), + sa.Column('tidal_volume', sa.Float(), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('referral_score', sa.Float(), nullable=True, comment='Score from ML algorithm indicating referral recommendation (0-1)'), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['encounterID'], ['encounters.encounterID'], ), + sa.ForeignKeyConstraint(['patientID'], ['patients.patientID'], ), + sa.PrimaryKeyConstraint('measurementID') + ) + with op.batch_alter_table('physiologicalmeasurements', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_physiologicalmeasurements_encounterID'), ['encounterID'], unique=False) + batch_op.create_index(batch_op.f('ix_physiologicalmeasurements_measurementDateTime'), ['measurementDateTime'], unique=False) + + op.create_table('procedures', + sa.Column('procedureID', sa.Integer(), nullable=False), + sa.Column('encounterID', sa.Integer(), nullable=False), + sa.Column('patientID', sa.String(length=20), nullable=False), + sa.Column('procedureType', sa.String(length=100), nullable=False), + sa.Column('procedureName', sa.String(length=255), nullable=True), + sa.Column('procedureDateTime', sa.DateTime(), nullable=False), + sa.Column('procedureEndDateTime', sa.DateTime(), nullable=True), + sa.Column('procedureResults', sa.Text(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['encounterID'], ['encounters.encounterID'], ), + sa.ForeignKeyConstraint(['patientID'], ['patients.patientID'], ), + sa.PrimaryKeyConstraint('procedureID') + ) + op.create_table('referrals', + sa.Column('referralID', sa.Integer(), nullable=False), + sa.Column('encounterID', sa.Integer(), nullable=False), + sa.Column('patientID', sa.String(length=20), nullable=False), + sa.Column('is_ml_recommended', sa.Boolean(), nullable=True), + sa.Column('is_staff_referred', sa.Boolean(), nullable=True), + sa.Column('referral_status', sa.Enum('Not Needed', 'ML Recommended', 'Pending Review', 'Staff Referred', 'Completed', 'Rejected'), nullable=True), + sa.Column('referralRequestedDateTime', sa.DateTime(), nullable=True), + sa.Column('referralCompletedDateTime', sa.DateTime(), nullable=True), + sa.Column('dietitianID', sa.Integer(), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('createdAt', sa.DateTime(), nullable=True), + sa.Column('updatedAt', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['dietitianID'], ['dietitians.dietitianID'], ), + sa.ForeignKeyConstraint(['encounterID'], ['encounters.encounterID'], ), + sa.ForeignKeyConstraint(['patientID'], ['patients.patientID'], ), + sa.PrimaryKeyConstraint('referralID') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('referrals') + op.drop_table('procedures') + with op.batch_alter_table('physiologicalmeasurements', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_physiologicalmeasurements_measurementDateTime')) + batch_op.drop_index(batch_op.f('ix_physiologicalmeasurements_encounterID')) + + op.drop_table('physiologicalmeasurements') + op.drop_table('reports') + with op.batch_alter_table('encounters', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_encounters_start_time')) + + op.drop_table('encounters') + op.drop_table('patients') + op.drop_table('uploadedfiles') + op.drop_table('dietitians') + op.drop_table('activity_logs') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/migrations/versions/1af2bf32a740_add_noti.py b/migrations/versions/1af2bf32a740_add_noti.py new file mode 100644 index 0000000000000000000000000000000000000000..3c19b2ec7b1527eafe6c7ddb5b739aaa811aef2d --- /dev/null +++ b/migrations/versions/1af2bf32a740_add_noti.py @@ -0,0 +1,57 @@ +"""add noti + +Revision ID: 1af2bf32a740 +Revises: 8ea1605c5393 +Create Date: 2025-04-17 23:29:42.398550 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '1af2bf32a740' +down_revision = '8ea1605c5393' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('notifications', + sa.Column('id', mysql.INTEGER(unsigned=True), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('message', sa.Text(), nullable=False), + sa.Column('timestamp', sa.DateTime(), nullable=True), + sa.Column('is_read', sa.Boolean(), nullable=False), + sa.Column('link', sa.String(length=255), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.userID'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('notifications', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_notifications_timestamp'), ['timestamp'], unique=False) + batch_op.create_index(batch_op.f('ix_notifications_user_id'), ['user_id'], unique=False) + + with op.batch_alter_table('uploadedfiles', schema=None) as batch_op: + batch_op.alter_column('error_details', + existing_type=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + type_=sa.Text(length=16777215), + existing_nullable=True) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('uploadedfiles', schema=None) as batch_op: + batch_op.alter_column('error_details', + existing_type=sa.Text(length=16777215), + type_=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), + existing_nullable=True) + + with op.batch_alter_table('notifications', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_notifications_user_id')) + batch_op.drop_index(batch_op.f('ix_notifications_timestamp')) + + op.drop_table('notifications') + # ### end Alembic commands ### diff --git "a/migrations/versions/78006a2a15a0_t\341\272\241o_l\341\272\241i_migration.py" "b/migrations/versions/78006a2a15a0_t\341\272\241o_l\341\272\241i_migration.py" deleted file mode 100644 index 9500b5951dfd3c724c811cef249c9d64353430e5..0000000000000000000000000000000000000000 --- "a/migrations/versions/78006a2a15a0_t\341\272\241o_l\341\272\241i_migration.py" +++ /dev/null @@ -1,38 +0,0 @@ -"""Tạo lại migration - -Revision ID: 78006a2a15a0 -Revises: -Create Date: 2025-04-16 18:25:19.976243 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = '78006a2a15a0' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('uploadedfiles', schema=None) as batch_op: - batch_op.alter_column('error_details', - existing_type=mysql.LONGTEXT(), - type_=sa.Text(length=16777215), - existing_nullable=True) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('uploadedfiles', schema=None) as batch_op: - batch_op.alter_column('error_details', - existing_type=sa.Text(length=16777215), - type_=mysql.LONGTEXT(), - existing_nullable=True) - - # ### end Alembic commands ### diff --git "a/migrations/versions/33d355767436_thay_\304\221\341\273\225i_\304\221\341\273\231_d\303\240i_c\341\273\231t_status_trong_b\341\272\243ng_.py" b/migrations/versions/8ea1605c5393_add_encounter_id.py similarity index 51% rename from "migrations/versions/33d355767436_thay_\304\221\341\273\225i_\304\221\341\273\231_d\303\240i_c\341\273\231t_status_trong_b\341\272\243ng_.py" rename to migrations/versions/8ea1605c5393_add_encounter_id.py index d7527f1db477a4fa36f164f651a65d1b54870b38..119aacb0c51654f7703624b6ad6f01eddbd0eec2 100644 --- "a/migrations/versions/33d355767436_thay_\304\221\341\273\225i_\304\221\341\273\231_d\303\240i_c\341\273\231t_status_trong_b\341\272\243ng_.py" +++ b/migrations/versions/8ea1605c5393_add_encounter_id.py @@ -1,8 +1,8 @@ -"""Thay đổi độ dài cột status trong bảng uploadedfiles +"""add encounter_id -Revision ID: 33d355767436 -Revises: 78006a2a15a0 -Create Date: 2025-04-16 18:32:35.386926 +Revision ID: 8ea1605c5393 +Revises: 11d6fcd3a14d +Create Date: 2025-04-17 22:35:02.844489 """ from alembic import op @@ -10,21 +10,21 @@ import sqlalchemy as sa from sqlalchemy.dialects import mysql # revision identifiers, used by Alembic. -revision = '33d355767436' -down_revision = '78006a2a15a0' +revision = '8ea1605c5393' +down_revision = '11d6fcd3a14d' branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('encounters', schema=None) as batch_op: + batch_op.add_column(sa.Column('custom_encounter_id', sa.String(length=50), nullable=True)) + batch_op.create_index(batch_op.f('ix_encounters_custom_encounter_id'), ['custom_encounter_id'], unique=True) + with op.batch_alter_table('uploadedfiles', schema=None) as batch_op: - batch_op.alter_column('status', - existing_type=mysql.VARCHAR(length=20), - type_=sa.String(length=50), - existing_nullable=True) batch_op.alter_column('error_details', - existing_type=mysql.LONGTEXT(), + existing_type=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), type_=sa.Text(length=16777215), existing_nullable=True) @@ -36,11 +36,11 @@ def downgrade(): with op.batch_alter_table('uploadedfiles', schema=None) as batch_op: batch_op.alter_column('error_details', existing_type=sa.Text(length=16777215), - type_=mysql.LONGTEXT(), - existing_nullable=True) - batch_op.alter_column('status', - existing_type=sa.String(length=50), - type_=mysql.VARCHAR(length=20), + type_=mysql.LONGTEXT(collation='utf8mb4_unicode_ci'), existing_nullable=True) + with op.batch_alter_table('encounters', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_encounters_custom_encounter_id')) + batch_op.drop_column('custom_encounter_id') + # ### end Alembic commands ### diff --git a/new_patients_upload.csv b/new_patients_upload.csv new file mode 100644 index 0000000000000000000000000000000000000000..a437f8de4d5817d8edc405b65f697387d9584adc --- /dev/null +++ b/new_patients_upload.csv @@ -0,0 +1,51 @@ +firstName,lastName,age,gender,height,weight,blood_type +Monica,Jenkins,34,female,150.7,47.7,B- +Angela,Collins,41,female,154.7,73.2,A+ +John,Webb,89,male,160.5,76.5,A- +Cheryl,Williams,48,female,166.3,76.9,O+ +Teresa,Mcmillan,64,female,171.5,63.5,A- +Victoria,Kim,21,female,162.4,55.5,B- +David,Hunt,47,male,182.1,96.4,O- +Kelly,Osborne,85,female,154.6,60.4,B- +Nicholas,Smith,62,male,185.1,88.2,A- +Anthony,Cohen,79,male,182.6,67.8,O+ +Teresa,Smith,78,female,154.0,60.7,B+ +Paul,Price,54,male,171.2,82.1,AB- +Ryan,Sullivan,78,male,185.7,96.6,AB+ +Timothy,Gutierrez,63,male,170.2,83.5,O+ +Stephanie,Pacheco,76,female,159.0,67.2,O+ +Susan,Wagner,58,female,159.2,63.3,B+ +Michael,Frank,81,male,165.0,63.0,O- +Collin,Williams,48,male,178.6,80.6,AB+ +Lindsay,Dunn,48,female,164.6,66.8,AB- +Kenneth,Mitchell,42,male,177.8,75.9,B- +Alyssa,Chan,69,female,157.5,63.2,O- +Kimberly,Smith,39,female,170.8,65.4,AB- +Tanya,Smith,66,female,173.4,76.3,B- +Sean,Craig,76,male,187.1,89.6,AB- +Robert,Travis,26,male,182.8,77.0,B- +Michelle,Mason,76,female,158.0,60.1,A+ +Kristin,Pennington,33,female,167.6,59.3,A+ +Robert,Callahan,74,male,163.0,78.3,AB+ +Gloria,Carter,81,female,156.0,69.1,O+ +Jennifer,Smith,54,female,170.7,74.2,O- +Courtney,Lee,32,female,154.6,58.2,O- +Andrew,Potter,30,male,185.1,77.5,A+ +Alexander,Shannon,70,male,165.6,61.1,A- +Carlos,Hoover,62,male,160.8,78.2,B- +Robert,Smith,87,male,174.9,73.1,B- +Katie,Arnold,63,female,168.4,74.4,A+ +Keith,Knight,47,male,167.4,89.1,B+ +Colleen,Reyes,62,female,170.8,75.8,B- +Kyle,Aguirre,49,other,166.0,79.8,O+ +Morgan,Mckee,73,female,160.2,62.5,O+ +Nicholas,Ramirez,42,other,165.0,66.3,AB+ +Ashley,Hebert,87,female,155.1,50.1,A+ +Stanley,Thomas,41,male,188.6,85.8,AB- +Ashley,Williams,20,female,167.2,62.5,O+ +Brittany,Cherry,55,female,175.0,78.5,B- +Robert,Bowen,43,male,181.2,89.9,B+ +Judith,Phelps,53,female,165.0,72.3,B- +Ryan,Baker,27,other,167.9,66.4,A+ +Nathan,Jones,72,male,160.1,74.9,AB+ +Zachary,Gentry,63,male,163.1,72.8,A- diff --git a/patients_data.csv b/patients_data.csv deleted file mode 100644 index fcdcdec0745556220bb3e6897cf464a7c513b793..0000000000000000000000000000000000000000 --- a/patients_data.csv +++ /dev/null @@ -1,51 +0,0 @@ -patientID,firstName,lastName,age,gender,height,weight,blood_type,measurementDateTime,temperature,heart_rate,blood_pressure_systolic,blood_pressure_diastolic,oxygen_saturation,resp_rate,fio2,tidal_vol,end_tidal_co2,feed_vol,feed_vol_adm,fio2_ratio,insp_time,oxygen_flow_rate,peep,pip,sip,tidal_vol_actual,tidal_vol_kg,tidal_vol_spon,bmi,referral -P-00001,Alexandra,Hobbs,24,female,174.0,62.6,O-,2025-04-16T07:34:02.746051,36.7,75,125,71,92.6,13,21.0,492,39.7,,,,1.1,,,,,455,7.6,,20.7, -P-00002,Sean,Raymond,52,male,178.0,74.9,AB+,2025-04-16T07:34:02.746051,36.7,80,146,93,98.7,13,21.0,534,43.0,,,,1.3,,,,,526,7.3,26,23.6,1 -P-00003,Christina,Garcia,56,female,159.2,52.8,A+,2025-04-16T07:34:02.746051,36.3,70,130,93,97.0,19,33.4,419,37.6,,,0.33,1.3,9,,,,400,8.1,,20.8,1 -P-00004,Emily,Payne,63,female,167.6,66.6,AB+,2025-04-16T07:34:02.746051,36.7,97,151,81,98.0,17,21.0,402,,,,,,,,32,,387,6.8,,23.7,1 -P-00005,Jessica,Taylor,85,female,150.9,60.1,AB-,2025-04-16T07:34:02.746051,37.9,107,148,97,98.7,16,21.0,420,42.8,,,,1.1,,7,28,,412,9.5,22,26.4, -P-00006,Brianna,Jacobs,69,female,152.9,57.9,AB+,2025-04-16T07:34:02.746051,36.9,70,139,103,94.5,14,59.9,364,37.8,,,0.6,1.1,4,,,,345,7.9,48,24.8,1 -P-00007,Kevin,Martin,28,male,161.7,64.6,AB-,2025-04-16T07:34:02.746051,36.1,66,120,80,97.0,12,21.0,444,36.1,,,,1.4,,4,,,401,7.6,,24.7, -P-00008,Cheyenne,York,87,female,157.9,68.8,B-,2025-04-16T07:34:02.746051,36.9,79,125,90,98.8,14,21.0,423,36.5,,,,,,,30,30,397,8.4,28,27.6, -P-00009,Rebecca,Carpenter,49,female,166.8,73.3,AB+,2025-04-16T07:34:02.746051,36.6,50,140,83,94.9,12,21.0,415,36.3,,,,,,,,,403,7.1,,26.3,1 -P-00010,Carl,Bell,53,male,178.4,68.4,O-,2025-04-16T07:34:02.746051,37.2,84,135,89,97.1,16,21.0,520,,,,,1.5,,7,,,513,7.1,,21.5, -P-00011,Rachel,Butler,27,female,167.2,66.2,AB-,2025-04-16T07:34:02.746051,36.5,45,117,80,97.8,18,21.0,428,,,,,,,6,,,411,7.3,77,23.7, -P-00012,Megan,Soto,59,female,168.5,51.3,O+,2025-04-16T07:34:02.746051,36.5,80,138,86,97.5,20,21.0,489,,,,,,,,,,485,8.1,,18.1, -P-00013,Brandon,Horton,63,male,185.0,74.7,B+,2025-04-16T07:34:02.746051,37.4,79,130,81,96.5,20,40.4,387,39.6,,,0.4,0.9,8,,,,377,4.9,,21.8, -P-00014,Mandy,Perry,77,female,163.1,68.3,B-,2025-04-16T07:34:02.747129,37.0,105,132,83,94.9,18,21.0,430,,396,221,,1.5,,,,,402,7.8,,25.7,1 -P-00015,Melissa,Wallace,38,female,159.5,65.2,O+,2025-04-16T07:34:02.747129,37.3,60,113,75,97.0,12,21.0,383,41.1,,,,0.9,,,,,378,7.4,,25.6, -P-00016,Jeffery,Prince,45,male,177.0,65.1,A-,2025-04-16T07:34:02.747129,37.0,69,120,76,96.6,19,21.0,482,,,,,0.9,,8,,,481,6.7,,20.8, -P-00017,Dillon,Perez,24,male,169.6,89.1,O+,2025-04-16T07:34:02.747129,36.8,78,117,77,99.4,18,21.0,409,,473,82,,1.2,,5,29,27,383,6.2,79,31.0, -P-00018,Jessica,Peterson,85,female,168.7,74.5,B-,2025-04-16T07:34:02.747129,37.7,81,138,94,97.6,14,21.0,513,43.9,,,,1.1,,4,,,494,8.5,,26.2,1 -P-00019,Stephen,Wood,86,male,168.9,63.9,O+,2025-04-16T07:34:02.747129,36.9,81,148,91,96.2,13,52.7,365,42.3,,,0.53,1.4,4,8,27,23,331,5.6,,22.4, -P-00020,Jessica,Fields,66,female,159.1,65.8,A-,2025-04-16T07:34:02.747129,36.9,106,140,85,97.4,19,21.0,385,,358,156,,,,,,,385,7.5,,26.0, -P-00021,Kevin,Keller,24,male,174.1,53.5,A+,2025-04-16T07:34:02.747129,37.1,65,114,83,96.3,13,21.0,442,,,,,,,,,,423,6.3,,17.7, -P-00022,Susan,Tucker,42,other,177.0,81.3,A+,2025-04-16T07:34:02.747129,36.7,73,124,83,97.7,16,21.0,396,,,,,,,6,,,361,5.8,77,26.0, -P-00023,Justin,Miller,26,male,176.8,103.4,O+,2025-04-16T07:34:02.747129,37.0,80,123,71,97.3,12,21.0,382,42.1,349,318,,,,5,,,355,5.3,,33.1,1 -P-00024,Andrea,Webster,87,other,158.7,55.9,AB+,2025-04-16T07:34:02.747129,36.4,96,138,91,96.8,14,21.0,506,,248,208,,1.5,,,29,29,460,9.9,,22.2, -P-00025,Matthew,Griffin,58,male,168.1,76.1,AB+,2025-04-16T07:34:02.747129,37.1,84,147,101,96.6,15,21.0,362,40.9,,,,,,,35,30,355,5.6,,26.9, -P-00026,Andrew,Douglas,63,male,180.6,102.1,AB+,2025-04-16T07:34:02.747129,36.4,103,126,85,99.4,12,21.0,372,36.7,228,168,,1.1,,,31,22,346,4.9,,31.3, -P-00027,Anthony,Bruce,19,male,161.8,83.0,A-,2025-04-16T07:34:02.747129,36.2,40,116,81,98.4,17,48.1,397,36.5,287,273,0.48,0.8,7,8,,,388,6.8,,31.7, -P-00028,John,Gray,30,male,183.6,71.7,O+,2025-04-16T07:34:02.747129,36.1,71,113,78,99.0,20,21.0,467,40.5,,,,,,4,,,442,6.0,,21.3,1 -P-00029,Casey,Riley,86,male,179.9,90.6,B-,2025-04-16T07:34:02.747129,36.8,94,144,100,97.3,17,21.0,439,35.6,,,,1.4,,8,29,,434,5.9,,28.0, -P-00030,Jessica,Williams,69,female,171.3,63.3,AB+,2025-04-16T07:34:02.747129,37.0,99,143,91,98.3,14,21.0,369,35.7,,,,,,,,,356,5.9,,21.6, -P-00031,Madison,Bell,56,female,160.6,71.5,O+,2025-04-16T07:34:02.748156,36.3,65,137,95,96.2,16,53.4,468,44.9,,,0.53,,5,5,,,450,8.8,,27.7, -P-00032,Debra,Wilson,78,female,172.5,78.5,AB-,2025-04-16T07:34:02.748156,36.4,88,138,85,97.6,17,21.0,535,35.8,,,,1.2,,5,26,,499,8.4,,26.4, -P-00033,Andrew,Lewis,73,male,170.9,98.3,A+,2025-04-16T07:34:02.748156,38.0,90,142,87,99.3,15,21.0,456,44.2,,,,1.1,,,,,450,6.8,,33.7, -P-00034,Travis,Serrano,79,male,181.1,82.1,O-,2025-04-16T07:34:02.748156,37.1,91,141,85,98.0,14,21.0,537,38.1,,,,0.8,,,,,505,7.1,,25.0, -P-00035,Brian,Munoz,24,male,182.9,91.0,B+,2025-04-16T07:34:02.748156,36.7,53,118,70,97.7,14,28.7,376,43.0,,,0.29,1.4,5,5,,,339,4.8,56,27.2, -P-00036,Kenneth,Henry,37,male,169.8,73.9,B-,2025-04-16T07:34:02.748156,36.1,81,123,80,97.7,18,21.0,522,41.9,499,,,,,8,29,24,478,7.9,67,25.6, -P-00037,Andrea,Castaneda,27,female,154.7,74.5,B+,2025-04-16T07:34:02.748156,36.5,79,112,72,99.3,12,21.0,547,,462,138,,,,,,,510,11.5,70,31.1, -P-00038,Melissa,Riggs,50,female,166.2,64.8,AB-,2025-04-16T07:34:02.748156,37.0,68,137,85,99.2,17,21.0,364,,,,,,,,,,342,6.3,,23.5,1 -P-00039,Juan,Moore,88,other,177.3,73.5,AB+,2025-04-16T07:34:02.748156,36.9,80,131,96,93.2,20,21.0,485,,,,,,,,,,451,7.1,,23.4,1 -P-00040,Monique,Williamson,21,female,170.6,65.4,O-,2025-04-16T07:34:02.748156,36.7,67,121,80,99.3,18,21.0,357,,,,,,,5,,,349,5.8,,22.5, -P-00041,David,Stanley,46,other,173.5,70.9,O-,2025-04-16T07:34:02.748156,37.0,82,113,71,98.4,17,21.0,416,35.6,,,,,,,28,,405,6.4,,23.6, -P-00042,Ryan,Jenkins,19,male,176.1,89.8,A+,2025-04-16T07:34:02.748156,36.3,68,117,76,97.9,13,21.0,462,,,,,1.3,,,28,,459,6.5,,29.0, -P-00043,Kathleen,Jenkins,33,female,171.9,83.4,O-,2025-04-16T07:34:02.748156,36.4,67,125,76,96.4,15,21.0,435,35.7,,,,1.4,,5,,,404,6.9,,28.2, -P-00044,Sabrina,Ross,31,female,152.1,68.0,AB+,2025-04-16T07:34:02.748156,36.4,67,141,78,98.8,20,21.0,478,,,,,,,,26,25,444,10.6,,29.4,1 -P-00045,Michael,Castaneda,22,male,166.7,85.7,AB+,2025-04-16T07:34:02.748156,36.9,80,115,78,98.2,13,21.0,380,38.8,,,,,,,,,357,6.0,,30.8, -P-00046,Angela,Marsh,51,female,166.2,67.8,AB-,2025-04-16T07:34:02.749120,37.1,74,129,91,97.6,12,21.0,511,42.2,,,,1.1,,,,,460,8.8,,24.5, -P-00047,Latoya,Fox,76,other,164.9,55.7,A+,2025-04-16T07:34:02.749120,37.2,98,127,94,98.7,19,21.0,407,37.1,,,,1.2,,,32,32,399,7.2,,20.5,1 -P-00048,Patricia,Friedman,49,female,169.7,61.2,A+,2025-04-16T07:34:02.749120,36.8,40,121,71,99.0,13,21.0,369,35.8,,,,1.5,,,33,,334,6.0,,21.3, -P-00049,Adrian,Paul,22,male,170.9,92.7,O-,2025-04-16T07:34:02.749120,36.9,73,117,73,95.6,17,33.9,467,42.5,,,0.34,,6,,29,23,452,7.0,,31.7, -P-00050,Matthew,Morgan,67,male,187.4,90.1,O+,2025-04-16T07:34:02.749120,37.1,45,143,93,99.3,19,56.3,440,,,,0.56,,10,5,29,,409,5.4,,25.7,1 diff --git a/run.py b/run.py index 8429f4c9626d6e2124d2dbb804996fe58eeca774..4d4dcaaf801b07d855119bc4c342916e5b85bc45 100644 --- a/run.py +++ b/run.py @@ -9,13 +9,14 @@ import mysql.connector from flask_bcrypt import Bcrypt from flask import Flask from datetime import datetime -from flask_migrate import Migrate, init, migrate, upgrade +from flask_migrate import Migrate, init, migrate, upgrade, stamp import json # Import json # Import models and status needed for reset from app.models.user import User from app.models.dietitian import Dietitian, DietitianStatus -from app.models.patient import Patient, Encounter # Import Encounter +from app.models.patient import Patient +from app.models.encounter import Encounter from app.models.measurement import PhysiologicalMeasurement # Import Measurement def clear_cache(): @@ -90,34 +91,6 @@ def init_tables_and_admin(app, db): print(f"[Error] Init failed: {e}") return False -def run_migrations(app, db): - """ - Thực hiện migration để cập nhật cấu trúc cơ sở dữ liệu - """ - try: - print("Khởi tạo Migration...") - migrations_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'migrations') - migrate_instance = Migrate(app, db, directory=migrations_dir) - - with app.app_context(): - # Kiểm tra xem thư mục migrations đã tồn tại chưa - if not os.path.exists(migrations_dir): - print(f"Khởi tạo thư mục migrations tại {migrations_dir}...") - init(directory=migrations_dir) - - print("Tạo migration mới dựa trên model hiện tại...") - migrate(message="Update database schema to match models", directory=migrations_dir) - - print("Áp dụng migrations...") - upgrade(directory=migrations_dir) - - print("Migration hoàn tất!") - - return True - except Exception as e: - print(f"Lỗi khi thực hiện migration: {str(e)}") - return False - def perform_database_reset(app, db, bcrypt): """ Reset the database by deleting all data and recreating default accounts. @@ -271,6 +244,7 @@ def main(): parser.add_argument('--clean', action='store_true', help='Clean cache before running') parser.add_argument('--port', type=int, default=5000, help='Port to run the application') parser.add_argument('--migrate', action='store_true', help='Run database migrations') + parser.add_argument('--stamp', action='store_true', help='Stamp database to the latest migration version') args = parser.parse_args() from app import create_app, db @@ -290,10 +264,29 @@ def main(): return # Exit after reset attempt if args.migrate: - print("[DB] Starting database migration...") - run_migrations(app, db) + print("[DB] Applying database migrations...") + migrations_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'migrations') + migrate_instance = Migrate(app, db, directory=migrations_dir) + try: + with app.app_context(): + upgrade(directory=migrations_dir) + print("[DB] Migrations applied successfully!") + except Exception as e: + print(f"[Error] Failed to apply migrations: {e}") return # Exit after migration attempt + if args.stamp: + print("[DB] Stamping database to latest migration version...") + migrations_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'migrations') + migrate_instance = Migrate(app, db, directory=migrations_dir) + try: + with app.app_context(): + stamp(directory=migrations_dir, revision='head') + print("[DB] Database stamped successfully!") + except Exception as e: + print(f"[Error] Failed to stamp database: {e}") + return # Exit after stamp attempt + print(f"\n[App] Starting CCU HTM on port {args.port}...") app.run(host='0.0.0.0', port=args.port, debug=True) diff --git a/run_ccu.bat b/run_ccu.bat index ad98dfc7065bf71164b95a3695a70d6d3cba8217..a5a171a71da98a7d21e305c48a62ed8180632cd3 100644 --- a/run_ccu.bat +++ b/run_ccu.bat @@ -6,47 +6,71 @@ echo ------------------------- cls echo Select a function: echo 1. Run the application -echo 2. Apply database migrations -echo 3. Reset database (warning: this will delete all data) -echo 4. Exit +echo 2. Stamp database to latest migration (fix sync issues) +echo 3. Generate new migration (after changing models) +echo 4. Apply database migrations (upgrade) +echo 5. Reset database (warning: this will delete all data) +echo 6. Exit echo. -set /p choice=Your choice (1-4): - -if "%choice%"=="1" ( - echo Starting the application... - python run.py --clean - goto end -) -if "%choice%"=="2" ( - echo Applying database migrations... - python run.py --migrate - echo. - echo Press any key to return to the menu... - pause >nul - goto menu -) -if "%choice%"=="3" ( - echo WARNING: This will delete all existing data in the database. - set /p confirm="Are you sure you want to proceed? (Y/N) " - if /i "%confirm%"=="y" ( - echo Resetting database... - python run.py --reset - echo. - echo Press any key to return to the menu... - pause >nul - ) - goto menu -) -if "%choice%"=="4" ( - goto end -) else ( - echo Invalid choice! - echo. - echo Press any key to try again... - pause >nul - goto menu -) +set /p choice=Your choice (1-6): + +if "%choice%"=="1" goto run_app +if "%choice%"=="2" goto stamp_db +if "%choice%"=="3" goto generate_migration +if "%choice%"=="4" goto apply_migrations +if "%choice%"=="5" goto reset_db +if "%choice%"=="6" goto end + +echo Invalid choice! +echo. +echo Press any key to try again... +pause >nul +goto menu + +:run_app +echo Starting the application... +python "run.py" --clean +goto end + +:apply_migrations +echo Applying database migrations (upgrade)... +python "run.py" --migrate +echo. +echo Press any key to return to the menu... +pause >nul +goto menu + +:generate_migration +echo Generating new migration file... +set /p msg=Enter a short description for the migration: +flask db migrate -m "%msg%" +echo. +echo Migration file generated (or message shown if no changes detected). +echo You may need to run option 2 (Apply migrations) afterwards. +echo. +echo Press any key to return to the menu... +pause >nul +goto menu + +:stamp_db +echo Stamping database to latest migration version... +python "run.py" --stamp +echo. +echo Press any key to return to the menu... +pause >nul +goto menu + +:reset_db +echo WARNING: This will delete all existing data in the database. +set /p confirm="Are you sure you want to proceed? (Y/N) " +if /i not "%confirm%"=="y" goto menu +echo Resetting database... +python "run.py" --reset +echo. +echo Press any key to return to the menu... +pause >nul +goto menu :end echo Thank you for using the CCU HTM Management System!