* @link https://descomplicar.pt * @since 1.0.0 */ namespace Care_API\Models; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class Patient * * Model for handling patient data, medical history and clinic associations * * @since 1.0.0 */ class Patient { /** * Patient user ID (wp_users table) * * @var int */ public $user_id; /** * Patient data * * @var array */ private $data = array(); /** * Required fields for patient registration * * @var array */ private static $required_fields = array( 'first_name', 'last_name', 'user_email', 'birth_date', 'gender' ); /** * Constructor * * @param int|array $user_id_or_data Patient user ID or data array * @since 1.0.0 */ public function __construct( $user_id_or_data = null ) { if ( is_numeric( $user_id_or_data ) ) { $this->user_id = (int) $user_id_or_data; $this->load_data(); } elseif ( is_array( $user_id_or_data ) ) { $this->data = $user_id_or_data; $this->user_id = isset( $this->data['user_id'] ) ? (int) $this->data['user_id'] : null; } } /** * Load patient data from database * * @return bool True on success, false on failure * @since 1.0.0 */ private function load_data() { if ( ! $this->user_id ) { return false; } $patient_data = self::get_patient_full_data( $this->user_id ); if ( $patient_data ) { $this->data = $patient_data; return true; } return false; } /** * Create a new patient * * @param array $patient_data Patient data * @return int|WP_Error Patient user ID on success, WP_Error on failure * @since 1.0.0 */ public static function create( $patient_data ) { // Validate required fields $validation = self::validate_patient_data( $patient_data ); if ( is_wp_error( $validation ) ) { return $validation; } // Create WordPress user $user_data = array( 'user_login' => self::generate_unique_username( $patient_data ), 'user_email' => sanitize_email( $patient_data['user_email'] ), 'first_name' => sanitize_text_field( $patient_data['first_name'] ), 'last_name' => sanitize_text_field( $patient_data['last_name'] ), 'role' => 'kivicare_patient', 'user_pass' => isset( $patient_data['user_pass'] ) ? $patient_data['user_pass'] : wp_generate_password() ); $user_id = wp_insert_user( $user_data ); if ( is_wp_error( $user_id ) ) { return new \WP_Error( 'patient_creation_failed', 'Failed to create patient user: ' . $user_id->get_error_message(), array( 'status' => 500 ) ); } // Add patient meta data $meta_fields = array( 'birth_date' => sanitize_text_field( $patient_data['birth_date'] ), 'gender' => sanitize_text_field( $patient_data['gender'] ), 'mobile_number' => isset( $patient_data['mobile_number'] ) ? sanitize_text_field( $patient_data['mobile_number'] ) : '', 'address' => isset( $patient_data['address'] ) ? sanitize_textarea_field( $patient_data['address'] ) : '', 'city' => isset( $patient_data['city'] ) ? sanitize_text_field( $patient_data['city'] ) : '', 'state' => isset( $patient_data['state'] ) ? sanitize_text_field( $patient_data['state'] ) : '', 'country' => isset( $patient_data['country'] ) ? sanitize_text_field( $patient_data['country'] ) : '', 'postal_code' => isset( $patient_data['postal_code'] ) ? sanitize_text_field( $patient_data['postal_code'] ) : '', 'blood_group' => isset( $patient_data['blood_group'] ) ? sanitize_text_field( $patient_data['blood_group'] ) : '', 'emergency_contact' => isset( $patient_data['emergency_contact'] ) ? sanitize_text_field( $patient_data['emergency_contact'] ) : '', 'medical_notes' => isset( $patient_data['medical_notes'] ) ? sanitize_textarea_field( $patient_data['medical_notes'] ) : '', 'patient_registration_date' => current_time( 'mysql' ) ); foreach ( $meta_fields as $meta_key => $meta_value ) { update_user_meta( $user_id, $meta_key, $meta_value ); } // Create clinic mapping if clinic_id provided if ( ! empty( $patient_data['clinic_id'] ) ) { self::assign_to_clinic( $user_id, $patient_data['clinic_id'] ); } return $user_id; } /** * Update patient data * * @param int $user_id Patient user ID * @param array $patient_data Updated patient data * @return bool|WP_Error True on success, WP_Error on failure * @since 1.0.0 */ public static function update( $user_id, $patient_data ) { if ( ! self::exists( $user_id ) ) { return new \WP_Error( 'patient_not_found', 'Patient not found', array( 'status' => 404 ) ); } // Update user data $user_update_data = array( 'ID' => $user_id ); $user_fields = array( 'first_name', 'last_name', 'user_email' ); foreach ( $user_fields as $field ) { if ( isset( $patient_data[ $field ] ) ) { $user_update_data[ $field ] = sanitize_text_field( $patient_data[ $field ] ); } } if ( count( $user_update_data ) > 1 ) { $result = wp_update_user( $user_update_data ); if ( is_wp_error( $result ) ) { return new \WP_Error( 'patient_update_failed', 'Failed to update patient: ' . $result->get_error_message(), array( 'status' => 500 ) ); } } // Update meta fields $meta_fields = array( 'birth_date', 'gender', 'mobile_number', 'address', 'city', 'state', 'country', 'postal_code', 'blood_group', 'emergency_contact', 'medical_notes' ); foreach ( $meta_fields as $meta_key ) { if ( isset( $patient_data[ $meta_key ] ) ) { $value = $patient_data[ $meta_key ]; if ( in_array( $meta_key, array( 'address', 'medical_notes' ) ) ) { $value = sanitize_textarea_field( $value ); } else { $value = sanitize_text_field( $value ); } update_user_meta( $user_id, $meta_key, $value ); } } return true; } /** * Delete a patient (soft delete - deactivate user) * * @param int $user_id Patient user ID * @return bool|WP_Error True on success, WP_Error on failure * @since 1.0.0 */ public static function delete( $user_id ) { if ( ! self::exists( $user_id ) ) { return new \WP_Error( 'patient_not_found', 'Patient not found', array( 'status' => 404 ) ); } // Check for dependencies if ( self::has_dependencies( $user_id ) ) { return new \WP_Error( 'patient_has_dependencies', 'Cannot delete patient with existing appointments or medical records', array( 'status' => 409 ) ); } // Soft delete - set user status to inactive update_user_meta( $user_id, 'patient_status', 'inactive' ); return true; } /** * Get patient by ID * * @param int $user_id Patient user ID * @return array|null Patient data or null if not found * @since 1.0.0 */ public static function get_by_id( $user_id ) { if ( ! self::exists( $user_id ) ) { return null; } return self::get_patient_full_data( $user_id ); } /** * Get all patients with optional filtering * * @param array $args Query arguments * @return array Array of patient data * @since 1.0.0 */ public static function get_all( $args = array() ) { $defaults = array( 'clinic_id' => null, 'status' => 'active', 'search' => '', 'limit' => 50, 'offset' => 0, 'orderby' => 'display_name', 'order' => 'ASC' ); $args = wp_parse_args( $args, $defaults ); $user_query_args = array( 'role' => 'kivicare_patient', 'number' => $args['limit'], 'offset' => $args['offset'], 'orderby' => $args['orderby'], 'order' => $args['order'], 'fields' => 'ID' ); // Add search if ( ! empty( $args['search'] ) ) { $user_query_args['search'] = '*' . $args['search'] . '*'; $user_query_args['search_columns'] = array( 'user_login', 'user_email', 'display_name' ); } // Add status filter via meta query if ( ! empty( $args['status'] ) ) { $user_query_args['meta_query'] = array( 'relation' => 'OR', array( 'key' => 'patient_status', 'value' => $args['status'], 'compare' => '=' ), array( 'key' => 'patient_status', 'compare' => 'NOT EXISTS' ) ); if ( $args['status'] === 'active' ) { $user_query_args['meta_query'][1]['value'] = 'active'; } } $user_query = new \WP_User_Query( $user_query_args ); $user_ids = $user_query->get_results(); $patients = array(); foreach ( $user_ids as $user_id ) { $patient_data = self::get_patient_full_data( $user_id ); // Filter by clinic if specified if ( ! is_null( $args['clinic_id'] ) && (int) $patient_data['clinic_id'] !== (int) $args['clinic_id'] ) { continue; } if ( $patient_data ) { $patients[] = $patient_data; } } return $patients; } /** * Get patient full data with clinic information * * @param int $user_id Patient user ID * @return array|null Full patient data or null if not found * @since 1.0.0 */ public static function get_patient_full_data( $user_id ) { global $wpdb; $user = get_user_by( 'id', $user_id ); if ( ! $user || ! in_array( 'kivicare_patient', $user->roles ) ) { return null; } // Get clinic mapping $clinic_mapping = $wpdb->get_row( $wpdb->prepare( "SELECT pcm.clinic_id, c.name as clinic_name FROM {$wpdb->prefix}kc_patient_clinic_mappings pcm LEFT JOIN {$wpdb->prefix}kc_clinics c ON pcm.clinic_id = c.id WHERE pcm.patient_id = %d LIMIT 1", $user_id ), ARRAY_A ); // Get patient meta data $patient_data = array( 'user_id' => $user_id, 'username' => $user->user_login, 'email' => $user->user_email, 'first_name' => $user->first_name, 'last_name' => $user->last_name, 'display_name' => $user->display_name, 'birth_date' => get_user_meta( $user_id, 'birth_date', true ), 'gender' => get_user_meta( $user_id, 'gender', true ), 'mobile_number' => get_user_meta( $user_id, 'mobile_number', true ), 'address' => get_user_meta( $user_id, 'address', true ), 'city' => get_user_meta( $user_id, 'city', true ), 'state' => get_user_meta( $user_id, 'state', true ), 'country' => get_user_meta( $user_id, 'country', true ), 'postal_code' => get_user_meta( $user_id, 'postal_code', true ), 'blood_group' => get_user_meta( $user_id, 'blood_group', true ), 'emergency_contact' => get_user_meta( $user_id, 'emergency_contact', true ), 'medical_notes' => get_user_meta( $user_id, 'medical_notes', true ), 'status' => get_user_meta( $user_id, 'patient_status', true ) ?: 'active', 'registration_date' => get_user_meta( $user_id, 'patient_registration_date', true ), 'clinic_id' => $clinic_mapping ? (int) $clinic_mapping['clinic_id'] : null, 'clinic_name' => $clinic_mapping ? $clinic_mapping['clinic_name'] : null, 'age' => self::calculate_age( get_user_meta( $user_id, 'birth_date', true ) ), 'total_appointments' => self::get_appointment_count( $user_id ), 'last_visit' => self::get_last_visit_date( $user_id ) ); return $patient_data; } /** * Get patient medical history * * @param int $user_id Patient user ID * @param array $args Query arguments * @return array Patient medical history * @since 1.0.0 */ public static function get_medical_history( $user_id, $args = array() ) { global $wpdb; $defaults = array( 'type' => null, 'limit' => 50, 'offset' => 0, 'orderby' => 'created_at', 'order' => 'DESC' ); $args = wp_parse_args( $args, $defaults ); $where_clauses = array( 'patient_id = %d' ); $where_values = array( $user_id ); if ( ! is_null( $args['type'] ) ) { $where_clauses[] = 'type = %s'; $where_values[] = $args['type']; } $where_sql = implode( ' AND ', $where_clauses ); // Whitelist for orderby $allowed_orderby = array( 'created_at', 'type', 'title' ); $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'created_at'; // Whitelist for order $order = in_array( strtoupper( $args['order'] ), array( 'ASC', 'DESC' ), true ) ? strtoupper( $args['order'] ) : 'DESC'; $query = $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}kc_medical_history WHERE {$where_sql} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d", array_merge( $where_values, array( $args['limit'], $args['offset'] ) ) ); $history = $wpdb->get_results( $query, ARRAY_A ); return array_map( function( $item ) { return array( 'id' => (int) $item['id'], 'encounter_id' => (int) $item['encounter_id'], 'type' => $item['type'], 'title' => $item['title'], 'date' => $item['created_at'], 'added_by' => (int) $item['added_by'] ); }, $history ); } /** * Get patient encounters * * @param int $user_id Patient user ID * @param array $args Query arguments * @return array Patient encounters * @since 1.0.0 */ public static function get_encounters( $user_id, $args = array() ) { global $wpdb; $defaults = array( 'limit' => 50, 'offset' => 0, 'orderby' => 'encounter_date', 'order' => 'DESC', 'status' => null ); $args = wp_parse_args( $args, $defaults ); $where_clauses = array( 'e.patient_id = %d' ); $where_values = array( $user_id ); if ( ! is_null( $args['status'] ) ) { $where_clauses[] = 'e.status = %d'; $where_values[] = $args['status']; } $where_sql = implode( ' AND ', $where_clauses ); // Whitelist for orderby $allowed_orderby = array( 'encounter_date', 'clinic_name', 'doctor_name', 'status' ); $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? 'e.' . $args['orderby'] : 'e.encounter_date'; // Whitelist for order $order = in_array( strtoupper( $args['order'] ), array( 'ASC', 'DESC' ), true ) ? strtoupper( $args['order'] ) : 'DESC'; $query = $wpdb->prepare( "SELECT e.*, c.name as clinic_name, CONCAT(u.first_name, ' ', u.last_name) as doctor_name FROM {$wpdb->prefix}kc_patient_encounters e LEFT JOIN {$wpdb->prefix}kc_clinics c ON e.clinic_id = c.id LEFT JOIN {$wpdb->prefix}users u ON e.doctor_id = u.ID WHERE {$where_sql} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d", array_merge( $where_values, array( $args['limit'], $args['offset'] ) ) ); $encounters = $wpdb->get_results( $query, ARRAY_A ); return array_map( function( $encounter ) { return array( 'id' => (int) $encounter['id'], 'encounter_date' => $encounter['encounter_date'], 'description' => $encounter['description'], 'status' => (int) $encounter['status'], 'clinic' => array( 'id' => (int) $encounter['clinic_id'], 'name' => $encounter['clinic_name'] ), 'doctor' => array( 'id' => (int) $encounter['doctor_id'], 'name' => $encounter['doctor_name'] ), 'appointment_id' => (int) $encounter['appointment_id'], 'template_id' => (int) $encounter['template_id'], 'created_at' => $encounter['created_at'] ); }, $encounters ); } /** * Assign patient to clinic * * @param int $user_id Patient user ID * @param int $clinic_id Clinic ID * @return bool|WP_Error True on success, WP_Error on failure * @since 1.0.0 */ public static function assign_to_clinic( $user_id, $clinic_id ) { global $wpdb; // Check if clinic exists if ( ! Clinic::exists( $clinic_id ) ) { return new \WP_Error( 'clinic_not_found', 'Clinic not found', array( 'status' => 404 ) ); } // Check if mapping already exists $existing_mapping = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kc_patient_clinic_mappings WHERE patient_id = %d AND clinic_id = %d", $user_id, $clinic_id ) ); if ( (int) $existing_mapping > 0 ) { return true; // Already assigned } // Remove existing mappings (patient can only be assigned to one clinic at a time) $wpdb->delete( "{$wpdb->prefix}kc_patient_clinic_mappings", array( 'patient_id' => $user_id ), array( '%d' ) ); // Create new mapping $result = $wpdb->insert( "{$wpdb->prefix}kc_patient_clinic_mappings", array( 'patient_id' => $user_id, 'clinic_id' => $clinic_id, 'created_at' => current_time( 'mysql' ) ), array( '%d', '%d', '%s' ) ); if ( $result === false ) { return new \WP_Error( 'clinic_assignment_failed', 'Failed to assign patient to clinic: ' . $wpdb->last_error, array( 'status' => 500 ) ); } return true; } /** * Check if patient exists * * @param int $user_id Patient user ID * @return bool True if exists, false otherwise * @since 1.0.0 */ public static function exists( $user_id ) { $user = get_user_by( 'id', $user_id ); return $user && in_array( 'kivicare_patient', $user->roles ); } /** * Check if patient has dependencies (appointments, encounters, etc.) * * @param int $user_id Patient user ID * @return bool True if has dependencies, false otherwise * @since 1.0.0 */ private static function has_dependencies( $user_id ) { global $wpdb; // Check appointments $appointments_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments WHERE patient_id = %d", $user_id ) ); if ( (int) $appointments_count > 0 ) { return true; } // Check encounters $encounters_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kc_patient_encounters WHERE patient_id = %d", $user_id ) ); return (int) $encounters_count > 0; } /** * Validate patient data * * @param array $patient_data Patient data to validate * @return bool|WP_Error True if valid, WP_Error if invalid * @since 1.0.0 */ private static function validate_patient_data( $patient_data ) { $errors = array(); // Check required fields foreach ( self::$required_fields as $field ) { if ( empty( $patient_data[ $field ] ) ) { $errors[] = "Field '{$field}' is required"; } } // Validate email format if ( ! empty( $patient_data['user_email'] ) && ! is_email( $patient_data['user_email'] ) ) { $errors[] = 'Invalid email format'; } // Check for duplicate email if ( ! empty( $patient_data['user_email'] ) ) { $existing_user = get_user_by( 'email', $patient_data['user_email'] ); if ( $existing_user ) { $errors[] = 'A user with this email already exists'; } } // Validate birth date format if ( ! empty( $patient_data['birth_date'] ) ) { $birth_date = \DateTime::createFromFormat( 'Y-m-d', $patient_data['birth_date'] ); if ( ! $birth_date || $birth_date->format( 'Y-m-d' ) !== $patient_data['birth_date'] ) { $errors[] = 'Invalid birth date format. Use YYYY-MM-DD'; } } // Validate gender if ( ! empty( $patient_data['gender'] ) && ! in_array( $patient_data['gender'], array( 'M', 'F', 'O' ) ) ) { $errors[] = 'Invalid gender. Use M, F, or O'; } if ( ! empty( $errors ) ) { return new \WP_Error( 'patient_validation_failed', 'Patient validation failed', array( 'status' => 400, 'errors' => $errors ) ); } return true; } /** * Generate unique username for patient * * @param array $patient_data Patient data * @return string Unique username * @since 1.0.0 */ private static function generate_unique_username( $patient_data ) { $base_username = strtolower( $patient_data['first_name'] . '.' . $patient_data['last_name'] ); $base_username = sanitize_user( $base_username ); $username = $base_username; $counter = 1; while ( username_exists( $username ) ) { $username = $base_username . $counter; $counter++; } return $username; } /** * Calculate patient age from birth date * * @param string $birth_date Birth date in Y-m-d format * @return int|null Age in years or null if invalid * @since 1.0.0 */ private static function calculate_age( $birth_date ) { if ( empty( $birth_date ) ) { return null; } $birth = \DateTime::createFromFormat( 'Y-m-d', $birth_date ); if ( ! $birth ) { return null; } $now = new \DateTime(); $age = $now->diff( $birth ); return $age->y; } /** * Get patient appointment count * * @param int $user_id Patient user ID * @return int Total appointments * @since 1.0.0 */ private static function get_appointment_count( $user_id ) { global $wpdb; return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments WHERE patient_id = %d", $user_id ) ); } /** * Get patient's last visit date * * @param int $user_id Patient user ID * @return string|null Last visit date or null * @since 1.0.0 */ private static function get_last_visit_date( $user_id ) { global $wpdb; return $wpdb->get_var( $wpdb->prepare( "SELECT MAX(encounter_date) FROM {$wpdb->prefix}kc_patient_encounters WHERE patient_id = %d", $user_id ) ); } /** * Get patient statistics * * @param int $user_id Patient user ID * @return array Patient statistics * @since 1.0.0 */ public static function get_statistics( $user_id ) { global $wpdb; $stats = array( 'total_appointments' => self::get_appointment_count( $user_id ), 'completed_encounters' => 0, 'pending_appointments' => 0, 'total_prescriptions' => 0, 'last_visit' => self::get_last_visit_date( $user_id ), 'next_appointment' => null ); // Completed encounters $stats['completed_encounters'] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kc_patient_encounters WHERE patient_id = %d AND status = 1", $user_id ) ); // Pending appointments $stats['pending_appointments'] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments WHERE patient_id = %d AND status = 1 AND appointment_start_date >= CURDATE()", $user_id ) ); // Total prescriptions $stats['total_prescriptions'] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kc_prescription WHERE patient_id = %d", $user_id ) ); // Next appointment $stats['next_appointment'] = $wpdb->get_var( $wpdb->prepare( "SELECT MIN(appointment_start_date) FROM {$wpdb->prefix}kc_appointments WHERE patient_id = %d AND status = 1 AND appointment_start_date >= CURDATE()", $user_id ) ); return $stats; } }