/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ * @link https://descomplicar.pt * @since 1.0.0 */ namespace Care_API\Models; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class Encounter * * Model for handling patient encounters (medical consultations) * * @since 1.0.0 */ class Encounter { /** * Database table name * * @var string */ private static $table_name = 'kc_patient_encounters'; /** * Encounter ID * * @var int */ public $id; /** * Encounter data * * @var array */ private $data = array(); /** * Required fields for encounter creation * * @var array */ private static $required_fields = array( 'encounter_date', 'clinic_id', 'doctor_id', 'patient_id' ); /** * Valid encounter statuses * * @var array */ private static $valid_statuses = array( 0 => 'draft', 1 => 'completed', 2 => 'cancelled' ); /** * Constructor * * @param int|array $encounter_id_or_data Encounter ID or data array * @since 1.0.0 */ public function __construct( $encounter_id_or_data = null ) { if ( is_numeric( $encounter_id_or_data ) ) { $this->id = (int) $encounter_id_or_data; $this->load_data(); } elseif ( is_array( $encounter_id_or_data ) ) { $this->data = $encounter_id_or_data; $this->id = isset( $this->data['id'] ) ? (int) $this->data['id'] : null; } } /** * Load encounter data from database * * @return bool True on success, false on failure * @since 1.0.0 */ private function load_data() { if ( ! $this->id ) { return false; } $encounter_data = self::get_encounter_full_data( $this->id ); if ( $encounter_data ) { $this->data = $encounter_data; return true; } return false; } /** * Create a new encounter * * @param array $encounter_data Encounter data * @return int|WP_Error Encounter ID on success, WP_Error on failure * @since 1.0.0 */ public static function create( $encounter_data ) { // Validate required fields $validation = self::validate_encounter_data( $encounter_data ); if ( is_wp_error( $validation ) ) { return $validation; } global $wpdb; $table = $wpdb->prefix . self::$table_name; // Prepare data for insertion $insert_data = array( 'encounter_date' => sanitize_text_field( $encounter_data['encounter_date'] ), 'clinic_id' => (int) $encounter_data['clinic_id'], 'doctor_id' => (int) $encounter_data['doctor_id'], 'patient_id' => (int) $encounter_data['patient_id'], 'appointment_id' => isset( $encounter_data['appointment_id'] ) ? (int) $encounter_data['appointment_id'] : null, 'description' => isset( $encounter_data['description'] ) ? sanitize_textarea_field( $encounter_data['description'] ) : '', 'status' => isset( $encounter_data['status'] ) ? (int) $encounter_data['status'] : 0, 'added_by' => get_current_user_id(), 'created_at' => current_time( 'mysql' ), 'template_id' => isset( $encounter_data['template_id'] ) ? (int) $encounter_data['template_id'] : null ); $result = $wpdb->insert( $table, $insert_data ); if ( $result === false ) { return new \WP_Error( 'encounter_creation_failed', 'Failed to create encounter: ' . $wpdb->last_error, array( 'status' => 500 ) ); } $encounter_id = $wpdb->insert_id; // Create medical history entries if provided if ( ! empty( $encounter_data['medical_history'] ) && is_array( $encounter_data['medical_history'] ) ) { self::create_medical_history_entries( $encounter_id, $encounter_data['medical_history'] ); } return $encounter_id; } /** * Update encounter data * * @param int $encounter_id Encounter ID * @param array $encounter_data Updated encounter data * @return bool|WP_Error True on success, WP_Error on failure * @since 1.0.0 */ public static function update( $encounter_id, $encounter_data ) { if ( ! self::exists( $encounter_id ) ) { return new \WP_Error( 'encounter_not_found', 'Encounter not found', array( 'status' => 404 ) ); } global $wpdb; $table = $wpdb->prefix . self::$table_name; // Prepare update data $update_data = array(); $allowed_fields = array( 'encounter_date', 'clinic_id', 'doctor_id', 'patient_id', 'appointment_id', 'description', 'status', 'template_id' ); foreach ( $allowed_fields as $field ) { if ( isset( $encounter_data[ $field ] ) ) { $value = $encounter_data[ $field ]; switch ( $field ) { case 'clinic_id': case 'doctor_id': case 'patient_id': case 'appointment_id': case 'status': case 'template_id': $update_data[ $field ] = (int) $value; break; case 'description': $update_data[ $field ] = sanitize_textarea_field( $value ); break; default: $update_data[ $field ] = sanitize_text_field( $value ); } } } if ( empty( $update_data ) ) { return new \WP_Error( 'no_data_to_update', 'No valid data provided for update', array( 'status' => 400 ) ); } $result = $wpdb->update( $table, $update_data, array( 'id' => $encounter_id ), null, array( '%d' ) ); if ( $result === false ) { return new \WP_Error( 'encounter_update_failed', 'Failed to update encounter: ' . $wpdb->last_error, array( 'status' => 500 ) ); } return true; } /** * Delete an encounter * * @param int $encounter_id Encounter ID * @return bool|WP_Error True on success, WP_Error on failure * @since 1.0.0 */ public static function delete( $encounter_id ) { if ( ! self::exists( $encounter_id ) ) { return new \WP_Error( 'encounter_not_found', 'Encounter not found', array( 'status' => 404 ) ); } // Check if encounter can be deleted (has prescriptions or bills) if ( self::has_dependencies( $encounter_id ) ) { return new \WP_Error( 'encounter_has_dependencies', 'Cannot delete encounter with associated prescriptions or bills', array( 'status' => 409 ) ); } global $wpdb; // Delete medical history entries first $wpdb->delete( "{$wpdb->prefix}kc_medical_history", array( 'encounter_id' => $encounter_id ), array( '%d' ) ); // Delete encounter $table = $wpdb->prefix . self::$table_name; $result = $wpdb->delete( $table, array( 'id' => $encounter_id ), array( '%d' ) ); if ( $result === false ) { return new \WP_Error( 'encounter_deletion_failed', 'Failed to delete encounter: ' . $wpdb->last_error, array( 'status' => 500 ) ); } return true; } /** * Get encounter by ID * * @param int $encounter_id Encounter ID * @return array|null Encounter data or null if not found * @since 1.0.0 */ public static function get_by_id( $encounter_id ) { return self::get_encounter_full_data( $encounter_id ); } /** * Get all encounters with optional filtering * * @param array $args Query arguments * @return array Array of encounter data * @since 1.0.0 */ public static function get_all( $args = array() ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $defaults = array( 'clinic_id' => null, 'doctor_id' => null, 'patient_id' => null, 'status' => null, 'date_from' => null, 'date_to' => null, 'search' => '', 'limit' => 50, 'offset' => 0, 'orderby' => 'encounter_date', 'order' => 'DESC' ); $args = wp_parse_args( $args, $defaults ); $where_clauses = array( '1=1' ); $where_values = array(); // Clinic filter if ( ! is_null( $args['clinic_id'] ) ) { $where_clauses[] = 'e.clinic_id = %d'; $where_values[] = $args['clinic_id']; } // Doctor filter if ( ! is_null( $args['doctor_id'] ) ) { $where_clauses[] = 'e.doctor_id = %d'; $where_values[] = $args['doctor_id']; } // Patient filter if ( ! is_null( $args['patient_id'] ) ) { $where_clauses[] = 'e.patient_id = %d'; $where_values[] = $args['patient_id']; } // Status filter if ( ! is_null( $args['status'] ) ) { $where_clauses[] = 'e.status = %d'; $where_values[] = $args['status']; } // Date range filters if ( ! is_null( $args['date_from'] ) ) { $where_clauses[] = 'e.encounter_date >= %s'; $where_values[] = $args['date_from']; } if ( ! is_null( $args['date_to'] ) ) { $where_clauses[] = 'e.encounter_date <= %s'; $where_values[] = $args['date_to']; } // Search filter if ( ! empty( $args['search'] ) ) { $where_clauses[] = '(p.first_name LIKE %s OR p.last_name LIKE %s OR d.first_name LIKE %s OR d.last_name LIKE %s OR e.description LIKE %s)'; $search_term = '%' . $wpdb->esc_like( $args['search'] ) . '%'; $where_values = array_merge( $where_values, array_fill( 0, 5, $search_term ) ); } $where_sql = implode( ' AND ', $where_clauses ); // Build query $query = "SELECT e.*, c.name as clinic_name, CONCAT(p.first_name, ' ', p.last_name) as patient_name, p.user_email as patient_email, CONCAT(d.first_name, ' ', d.last_name) as doctor_name, d.user_email as doctor_email, CONCAT(ab.first_name, ' ', ab.last_name) as added_by_name FROM {$table} e LEFT JOIN {$wpdb->prefix}kc_clinics c ON e.clinic_id = c.id LEFT JOIN {$wpdb->prefix}users p ON e.patient_id = p.ID LEFT JOIN {$wpdb->prefix}users d ON e.doctor_id = d.ID LEFT JOIN {$wpdb->prefix}users ab ON e.added_by = ab.ID WHERE {$where_sql}"; $query .= sprintf( ' ORDER BY e.%s %s', sanitize_sql_orderby( $args['orderby'] ), sanitize_sql_orderby( $args['order'] ) ); $query .= $wpdb->prepare( ' LIMIT %d OFFSET %d', $args['limit'], $args['offset'] ); if ( ! empty( $where_values ) ) { $query = $wpdb->prepare( $query, $where_values ); } $encounters = $wpdb->get_results( $query, ARRAY_A ); return array_map( array( self::class, 'format_encounter_data' ), $encounters ); } /** * Get encounter full data with related entities * * @param int $encounter_id Encounter ID * @return array|null Full encounter data or null if not found * @since 1.0.0 */ public static function get_encounter_full_data( $encounter_id ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $encounter = $wpdb->get_row( $wpdb->prepare( "SELECT e.*, c.name as clinic_name, c.address as clinic_address, CONCAT(p.first_name, ' ', p.last_name) as patient_name, p.user_email as patient_email, CONCAT(d.first_name, ' ', d.last_name) as doctor_name, d.user_email as doctor_email, CONCAT(ab.first_name, ' ', ab.last_name) as added_by_name FROM {$table} e LEFT JOIN {$wpdb->prefix}kc_clinics c ON e.clinic_id = c.id LEFT JOIN {$wpdb->prefix}users p ON e.patient_id = p.ID LEFT JOIN {$wpdb->prefix}users d ON e.doctor_id = d.ID LEFT JOIN {$wpdb->prefix}users ab ON e.added_by = ab.ID WHERE e.id = %d", $encounter_id ), ARRAY_A ); if ( ! $encounter ) { return null; } return self::format_encounter_data( $encounter ); } /** * Get encounter prescriptions * * @param int $encounter_id Encounter ID * @return array Array of prescriptions * @since 1.0.0 */ public static function get_prescriptions( $encounter_id ) { global $wpdb; $prescriptions = $wpdb->get_results( $wpdb->prepare( "SELECT p.*, CONCAT(ab.first_name, ' ', ab.last_name) as added_by_name FROM {$wpdb->prefix}kc_prescription p LEFT JOIN {$wpdb->prefix}users ab ON p.added_by = ab.ID WHERE p.encounter_id = %d ORDER BY p.created_at ASC", $encounter_id ), ARRAY_A ); return array_map( function( $prescription ) { return array( 'id' => (int) $prescription['id'], 'name' => $prescription['name'], 'frequency' => $prescription['frequency'], 'duration' => $prescription['duration'], 'instruction' => $prescription['instruction'], 'patient_id' => (int) $prescription['patient_id'], 'added_by' => (int) $prescription['added_by'], 'added_by_name' => $prescription['added_by_name'], 'created_at' => $prescription['created_at'], 'is_from_template' => (bool) $prescription['is_from_template'] ); }, $prescriptions ); } /** * Get encounter medical history * * @param int $encounter_id Encounter ID * @return array Array of medical history entries * @since 1.0.0 */ public static function get_medical_history( $encounter_id ) { global $wpdb; $history = $wpdb->get_results( $wpdb->prepare( "SELECT mh.*, CONCAT(ab.first_name, ' ', ab.last_name) as added_by_name FROM {$wpdb->prefix}kc_medical_history mh LEFT JOIN {$wpdb->prefix}users ab ON mh.added_by = ab.ID WHERE mh.encounter_id = %d ORDER BY mh.created_at ASC", $encounter_id ), ARRAY_A ); return array_map( function( $entry ) { return array( 'id' => (int) $entry['id'], 'type' => $entry['type'], 'title' => $entry['title'], 'patient_id' => (int) $entry['patient_id'], 'added_by' => (int) $entry['added_by'], 'added_by_name' => $entry['added_by_name'], 'created_at' => $entry['created_at'] ); }, $history ); } /** * Get encounter bills * * @param int $encounter_id Encounter ID * @return array Array of bills * @since 1.0.0 */ public static function get_bills( $encounter_id ) { global $wpdb; $bills = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}kc_bills WHERE encounter_id = %d ORDER BY created_at DESC", $encounter_id ), ARRAY_A ); return array_map( function( $bill ) { return array( 'id' => (int) $bill['id'], 'title' => $bill['title'], 'total_amount' => (float) $bill['total_amount'], 'discount' => (float) $bill['discount'], 'actual_amount' => (float) $bill['actual_amount'], 'status' => (int) $bill['status'], 'payment_status' => $bill['payment_status'], 'created_at' => $bill['created_at'], 'clinic_id' => (int) $bill['clinic_id'], 'appointment_id' => (int) $bill['appointment_id'] ); }, $bills ); } /** * Add prescription to encounter * * @param int $encounter_id Encounter ID * @param array $prescription_data Prescription data * @return int|WP_Error Prescription ID on success, WP_Error on failure * @since 1.0.0 */ public static function add_prescription( $encounter_id, $prescription_data ) { if ( ! self::exists( $encounter_id ) ) { return new \WP_Error( 'encounter_not_found', 'Encounter not found', array( 'status' => 404 ) ); } $encounter = self::get_by_id( $encounter_id ); return Prescription::create( array_merge( $prescription_data, array( 'encounter_id' => $encounter_id, 'patient_id' => $encounter['patient']['id'] ) ) ); } /** * Check if encounter exists * * @param int $encounter_id Encounter ID * @return bool True if exists, false otherwise * @since 1.0.0 */ public static function exists( $encounter_id ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE id = %d", $encounter_id ) ); return (int) $count > 0; } /** * Check if encounter has dependencies (prescriptions, bills, etc.) * * @param int $encounter_id Encounter ID * @return bool True if has dependencies, false otherwise * @since 1.0.0 */ private static function has_dependencies( $encounter_id ) { global $wpdb; // Check prescriptions $prescriptions_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kc_prescription WHERE encounter_id = %d", $encounter_id ) ); if ( (int) $prescriptions_count > 0 ) { return true; } // Check bills $bills_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kc_bills WHERE encounter_id = %d", $encounter_id ) ); return (int) $bills_count > 0; } /** * Validate encounter data * * @param array $encounter_data Encounter data to validate * @return bool|WP_Error True if valid, WP_Error if invalid * @since 1.0.0 */ private static function validate_encounter_data( $encounter_data ) { $errors = array(); // Check required fields foreach ( self::$required_fields as $field ) { if ( empty( $encounter_data[ $field ] ) ) { $errors[] = "Field '{$field}' is required"; } } // Validate date format if ( ! empty( $encounter_data['encounter_date'] ) ) { $date = \DateTime::createFromFormat( 'Y-m-d', $encounter_data['encounter_date'] ); if ( ! $date || $date->format( 'Y-m-d' ) !== $encounter_data['encounter_date'] ) { $errors[] = 'Invalid encounter date format. Use YYYY-MM-DD'; } } // Validate entities exist if ( ! empty( $encounter_data['clinic_id'] ) && ! Clinic::exists( $encounter_data['clinic_id'] ) ) { $errors[] = 'Clinic not found'; } if ( ! empty( $encounter_data['doctor_id'] ) && ! Doctor::exists( $encounter_data['doctor_id'] ) ) { $errors[] = 'Doctor not found'; } if ( ! empty( $encounter_data['patient_id'] ) && ! Patient::exists( $encounter_data['patient_id'] ) ) { $errors[] = 'Patient not found'; } // Validate appointment exists if provided if ( ! empty( $encounter_data['appointment_id'] ) && ! Appointment::exists( $encounter_data['appointment_id'] ) ) { $errors[] = 'Appointment not found'; } // Validate status if ( isset( $encounter_data['status'] ) && ! array_key_exists( $encounter_data['status'], self::$valid_statuses ) ) { $errors[] = 'Invalid status value'; } if ( ! empty( $errors ) ) { return new \WP_Error( 'encounter_validation_failed', 'Encounter validation failed', array( 'status' => 400, 'errors' => $errors ) ); } return true; } /** * Format encounter data for API response * * @param array $encounter_data Raw encounter data * @return array Formatted encounter data * @since 1.0.0 */ private static function format_encounter_data( $encounter_data ) { if ( ! $encounter_data ) { return null; } $formatted = array( 'id' => (int) $encounter_data['id'], 'encounter_date' => $encounter_data['encounter_date'], 'description' => $encounter_data['description'], 'status' => (int) $encounter_data['status'], 'status_text' => self::$valid_statuses[ $encounter_data['status'] ] ?? 'unknown', 'created_at' => $encounter_data['created_at'], 'clinic' => array( 'id' => (int) $encounter_data['clinic_id'], 'name' => $encounter_data['clinic_name'] ?? '', 'address' => $encounter_data['clinic_address'] ?? '' ), 'patient' => array( 'id' => (int) $encounter_data['patient_id'], 'name' => $encounter_data['patient_name'] ?? '', 'email' => $encounter_data['patient_email'] ?? '' ), 'doctor' => array( 'id' => (int) $encounter_data['doctor_id'], 'name' => $encounter_data['doctor_name'] ?? '', 'email' => $encounter_data['doctor_email'] ?? '' ), 'appointment_id' => isset( $encounter_data['appointment_id'] ) ? (int) $encounter_data['appointment_id'] : null, 'template_id' => isset( $encounter_data['template_id'] ) ? (int) $encounter_data['template_id'] : null, 'added_by' => array( 'id' => (int) $encounter_data['added_by'], 'name' => $encounter_data['added_by_name'] ?? '' ) ); return $formatted; } /** * Create medical history entries for encounter * * @param int $encounter_id Encounter ID * @param array $history_entries Array of history entries * @return bool True on success * @since 1.0.0 */ private static function create_medical_history_entries( $encounter_id, $history_entries ) { global $wpdb; $encounter = self::get_by_id( $encounter_id ); if ( ! $encounter ) { return false; } foreach ( $history_entries as $entry ) { $wpdb->insert( "{$wpdb->prefix}kc_medical_history", array( 'encounter_id' => $encounter_id, 'patient_id' => $encounter['patient']['id'], 'type' => sanitize_text_field( $entry['type'] ), 'title' => sanitize_text_field( $entry['title'] ), 'added_by' => get_current_user_id(), 'created_at' => current_time( 'mysql' ) ), array( '%d', '%d', '%s', '%s', '%d', '%s' ) ); } return true; } /** * Get encounter statistics * * @param array $filters Optional filters * @return array Encounter statistics * @since 1.0.0 */ public static function get_statistics( $filters = array() ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $where_clauses = array( '1=1' ); $where_values = array(); if ( ! empty( $filters['clinic_id'] ) ) { $where_clauses[] = 'clinic_id = %d'; $where_values[] = $filters['clinic_id']; } if ( ! empty( $filters['doctor_id'] ) ) { $where_clauses[] = 'doctor_id = %d'; $where_values[] = $filters['doctor_id']; } if ( ! empty( $filters['patient_id'] ) ) { $where_clauses[] = 'patient_id = %d'; $where_values[] = $filters['patient_id']; } $where_sql = implode( ' AND ', $where_clauses ); $stats = array( 'total_encounters' => 0, 'draft_encounters' => 0, 'completed_encounters' => 0, 'cancelled_encounters' => 0, 'encounters_today' => 0, 'encounters_this_week' => 0, 'encounters_this_month' => 0, 'avg_encounters_per_day' => 0 ); // Total encounters $query = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}"; if ( ! empty( $where_values ) ) { $query = $wpdb->prepare( $query, $where_values ); } $stats['total_encounters'] = (int) $wpdb->get_var( $query ); // Encounters by status foreach ( self::$valid_statuses as $status_id => $status_name ) { $status_where = $where_clauses; $status_where[] = 'status = %d'; $status_values = array_merge( $where_values, array( $status_id ) ); $query = $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $status_where ), $status_values ); $stats[ $status_name . '_encounters' ] = (int) $wpdb->get_var( $query ); } // Encounters today $today_where = array_merge( $where_clauses, array( 'encounter_date = CURDATE()' ) ); $query = "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $today_where ); if ( ! empty( $where_values ) ) { $query = $wpdb->prepare( $query, $where_values ); } $stats['encounters_today'] = (int) $wpdb->get_var( $query ); // Encounters this week $week_where = array_merge( $where_clauses, array( 'WEEK(encounter_date) = WEEK(CURDATE())', 'YEAR(encounter_date) = YEAR(CURDATE())' ) ); $query = "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $week_where ); if ( ! empty( $where_values ) ) { $query = $wpdb->prepare( $query, $where_values ); } $stats['encounters_this_week'] = (int) $wpdb->get_var( $query ); // Encounters this month $month_where = array_merge( $where_clauses, array( 'MONTH(encounter_date) = MONTH(CURDATE())', 'YEAR(encounter_date) = YEAR(CURDATE())' ) ); $query = "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $month_where ); if ( ! empty( $where_values ) ) { $query = $wpdb->prepare( $query, $where_values ); } $stats['encounters_this_month'] = (int) $wpdb->get_var( $query ); // Calculate average encounters per day (last 30 days) if ( $stats['total_encounters'] > 0 ) { $days_active = $wpdb->get_var( "SELECT DATEDIFF(MAX(encounter_date), MIN(encounter_date)) + 1 FROM {$table} WHERE encounter_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)" ); if ( $days_active > 0 ) { $stats['avg_encounters_per_day'] = round( $stats['encounters_this_month'] / min( $days_active, 30 ), 2 ); } } return $stats; } }