* @link https://descomplicar.pt * @since 1.0.0 */ namespace Care_API\Models; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class Prescription * * Model for handling patient prescriptions and medication management * * @since 1.0.0 */ class Prescription { /** * Database table name * * @var string */ private static $table_name = 'kc_prescription'; /** * Prescription ID * * @var int */ public $id; /** * Prescription data * * @var array */ private $data = array(); /** * Required fields for prescription creation * * @var array */ private static $required_fields = array( 'encounter_id', 'patient_id', 'name', 'frequency', 'duration' ); /** * Constructor * * @param int|array $prescription_id_or_data Prescription ID or data array * @since 1.0.0 */ public function __construct( $prescription_id_or_data = null ) { if ( is_numeric( $prescription_id_or_data ) ) { $this->id = (int) $prescription_id_or_data; $this->load_data(); } elseif ( is_array( $prescription_id_or_data ) ) { $this->data = $prescription_id_or_data; $this->id = isset( $this->data['id'] ) ? (int) $this->data['id'] : null; } } /** * Load prescription data from database * * @return bool True on success, false on failure * @since 1.0.0 */ private function load_data() { if ( ! $this->id ) { return false; } $prescription_data = self::get_prescription_full_data( $this->id ); if ( $prescription_data ) { $this->data = $prescription_data; return true; } return false; } /** * Create a new prescription * * @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 create( $prescription_data ) { // Validate required fields $validation = self::validate_prescription_data( $prescription_data ); if ( is_wp_error( $validation ) ) { return $validation; } global $wpdb; $table = $wpdb->prefix . self::$table_name; // Prepare data for insertion $insert_data = array( 'encounter_id' => (int) $prescription_data['encounter_id'], 'patient_id' => (int) $prescription_data['patient_id'], 'name' => sanitize_textarea_field( $prescription_data['name'] ), 'frequency' => sanitize_text_field( $prescription_data['frequency'] ), 'duration' => sanitize_text_field( $prescription_data['duration'] ), 'instruction' => isset( $prescription_data['instruction'] ) ? sanitize_textarea_field( $prescription_data['instruction'] ) : '', 'added_by' => get_current_user_id(), 'created_at' => current_time( 'mysql' ), 'is_from_template' => isset( $prescription_data['is_from_template'] ) ? (int) $prescription_data['is_from_template'] : 0 ); $result = $wpdb->insert( $table, $insert_data ); if ( $result === false ) { return new \WP_Error( 'prescription_creation_failed', 'Failed to create prescription: ' . $wpdb->last_error, array( 'status' => 500 ) ); } return $wpdb->insert_id; } /** * Update prescription data * * @param int $prescription_id Prescription ID * @param array $prescription_data Updated prescription data * @return bool|WP_Error True on success, WP_Error on failure * @since 1.0.0 */ public static function update( $prescription_id, $prescription_data ) { if ( ! self::exists( $prescription_id ) ) { return new \WP_Error( 'prescription_not_found', 'Prescription not found', array( 'status' => 404 ) ); } global $wpdb; $table = $wpdb->prefix . self::$table_name; // Prepare update data $update_data = array(); $allowed_fields = array( 'name', 'frequency', 'duration', 'instruction', 'is_from_template' ); foreach ( $allowed_fields as $field ) { if ( isset( $prescription_data[ $field ] ) ) { $value = $prescription_data[ $field ]; switch ( $field ) { case 'name': case 'instruction': $update_data[ $field ] = sanitize_textarea_field( $value ); break; case 'is_from_template': $update_data[ $field ] = (int) $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' => $prescription_id ), null, array( '%d' ) ); if ( $result === false ) { return new \WP_Error( 'prescription_update_failed', 'Failed to update prescription: ' . $wpdb->last_error, array( 'status' => 500 ) ); } return true; } /** * Delete a prescription * * @param int $prescription_id Prescription ID * @return bool|WP_Error True on success, WP_Error on failure * @since 1.0.0 */ public static function delete( $prescription_id ) { if ( ! self::exists( $prescription_id ) ) { return new \WP_Error( 'prescription_not_found', 'Prescription not found', array( 'status' => 404 ) ); } global $wpdb; $table = $wpdb->prefix . self::$table_name; $result = $wpdb->delete( $table, array( 'id' => $prescription_id ), array( '%d' ) ); if ( $result === false ) { return new \WP_Error( 'prescription_deletion_failed', 'Failed to delete prescription: ' . $wpdb->last_error, array( 'status' => 500 ) ); } return true; } /** * Get prescription by ID * * @param int $prescription_id Prescription ID * @return array|null Prescription data or null if not found * @since 1.0.0 */ public static function get_by_id( $prescription_id ) { return self::get_prescription_full_data( $prescription_id ); } /** * Get all prescriptions with optional filtering * * @param array $args Query arguments * @return array Array of prescription data * @since 1.0.0 */ public static function get_all( $args = array() ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $defaults = array( 'encounter_id' => null, 'patient_id' => null, 'search' => '', 'limit' => 50, 'offset' => 0, 'orderby' => 'created_at', 'order' => 'DESC' ); $args = wp_parse_args( $args, $defaults ); $where_clauses = array( '1=1' ); $where_values = array(); // Encounter filter if ( ! is_null( $args['encounter_id'] ) ) { $where_clauses[] = 'p.encounter_id = %d'; $where_values[] = $args['encounter_id']; } // Patient filter if ( ! is_null( $args['patient_id'] ) ) { $where_clauses[] = 'p.patient_id = %d'; $where_values[] = $args['patient_id']; } // Search filter if ( ! empty( $args['search'] ) ) { $where_clauses[] = '(p.name LIKE %s OR p.frequency LIKE %s OR p.instruction LIKE %s OR pt.first_name LIKE %s OR pt.last_name 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 ); // Whitelist for orderby $allowed_orderby = array( 'created_at', 'patient_name', 'name', 'frequency', 'duration' ); $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? 'p.' . $args['orderby'] : 'p.created_at'; // Whitelist for order $order = in_array( strtoupper( $args['order'] ), array( 'ASC', 'DESC' ), true ) ? strtoupper( $args['order'] ) : 'DESC'; // Build query $query = "SELECT p.*, CONCAT(pt.first_name, ' ', pt.last_name) as patient_name, pt.user_email as patient_email, CONCAT(ab.first_name, ' ', ab.last_name) as added_by_name, e.encounter_date FROM {$table} p LEFT JOIN {$wpdb->prefix}users pt ON p.patient_id = pt.ID LEFT JOIN {$wpdb->prefix}users ab ON p.added_by = ab.ID LEFT JOIN {$wpdb->prefix}kc_patient_encounters e ON p.encounter_id = e.id WHERE {$where_sql} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d"; $safe_query = $wpdb->prepare( $query, array_merge( $where_values, array( $args['limit'], $args['offset'] ) ) ); $prescriptions = $wpdb->get_results( $safe_query, ARRAY_A ); return array_map( array( self::class, 'format_prescription_data' ), $prescriptions ); } /** * Get prescription full data with related entities * * @param int $prescription_id Prescription ID * @return array|null Full prescription data or null if not found * @since 1.0.0 */ public static function get_prescription_full_data( $prescription_id ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $prescription = $wpdb->get_row( $wpdb->prepare( "SELECT p.*, CONCAT(pt.first_name, ' ', pt.last_name) as patient_name, pt.user_email as patient_email, CONCAT(ab.first_name, ' ', ab.last_name) as added_by_name, e.encounter_date, CONCAT(d.first_name, ' ', d.last_name) as doctor_name FROM {$table} p LEFT JOIN {$wpdb->prefix}users pt ON p.patient_id = pt.ID LEFT JOIN {$wpdb->prefix}users ab ON p.added_by = ab.ID LEFT JOIN {$wpdb->prefix}kc_patient_encounters e ON p.encounter_id = e.id LEFT JOIN {$wpdb->prefix}users d ON e.doctor_id = d.ID WHERE p.id = %d", $prescription_id ), ARRAY_A ); if ( ! $prescription ) { return null; } return self::format_prescription_data( $prescription ); } /** * Get prescriptions by encounter * * @param int $encounter_id Encounter ID * @return array Array of prescriptions * @since 1.0.0 */ public static function get_by_encounter( $encounter_id ) { return self::get_all( array( 'encounter_id' => $encounter_id ) ); } /** * Get prescriptions by patient * * @param int $patient_id Patient ID * @param array $args Additional query arguments * @return array Array of prescriptions * @since 1.0.0 */ public static function get_by_patient( $patient_id, $args = array() ) { $args['patient_id'] = $patient_id; return self::get_all( $args ); } /** * Search medications (for autocomplete) * * @param string $search_term Search term * @param int $limit Limit results * @return array Array of medication names * @since 1.0.0 */ public static function search_medications( $search_term, $limit = 20 ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; if ( empty( $search_term ) ) { return array(); } $medications = $wpdb->get_results( $wpdb->prepare( "SELECT DISTINCT name, COUNT(*) as usage_count FROM {$table} WHERE name LIKE %s GROUP BY name ORDER BY usage_count DESC, name ASC LIMIT %d", '%' . $wpdb->esc_like( $search_term ) . '%', $limit ), ARRAY_A ); return array_map( function( $med ) { return array( 'name' => $med['name'], 'usage_count' => (int) $med['usage_count'] ); }, $medications ); } /** * Get most prescribed medications * * @param array $args Query arguments * @return array Array of top medications * @since 1.0.0 */ public static function get_top_medications( $args = array() ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $defaults = array( 'limit' => 10, 'date_from' => null, 'date_to' => null, 'doctor_id' => null, 'patient_id' => null ); $args = wp_parse_args( $args, $defaults ); $where_clauses = array( '1=1' ); $where_values = array(); $join = ''; // Doctor filter (through encounter) if ( ! is_null( $args['doctor_id'] ) ) { $join = " LEFT JOIN {$wpdb->prefix}kc_patient_encounters e ON p.encounter_id = e.id"; $where_clauses[] = 'e.doctor_id = %d'; $where_values[] = $args['doctor_id']; } // Date range filters if ( ! is_null( $args['date_from'] ) ) { $where_clauses[] = 'p.created_at >= %s'; $where_values[] = $args['date_from'] . ' 00:00:00'; } if ( ! is_null( $args['date_to'] ) ) { $where_clauses[] = 'p.created_at <= %s'; $where_values[] = $args['date_to'] . ' 23:59:59'; } // Patient filter if ( ! is_null( $args['patient_id'] ) ) { $where_clauses[] = 'p.patient_id = %d'; $where_values[] = $args['patient_id']; } $where_sql = implode( ' AND ', $where_clauses ); $query = "SELECT p.name, COUNT(*) as prescription_count, COUNT(DISTINCT p.patient_id) as unique_patients, MAX(p.created_at) as last_prescribed FROM {$table} p {$join} WHERE {$where_sql} GROUP BY p.name ORDER BY prescription_count DESC LIMIT %d"; $where_values[] = $args['limit']; $medications = $wpdb->get_results( $wpdb->prepare( $query, $where_values ), ARRAY_A ); return array_map( function( $med ) { return array( 'name' => $med['name'], 'prescription_count' => (int) $med['prescription_count'], 'unique_patients' => (int) $med['unique_patients'], 'last_prescribed' => $med['last_prescribed'] ); }, $medications ); } /** * Get prescription templates * * @param array $args Query arguments * @return array Array of prescription templates * @since 1.0.0 */ public static function get_templates( $args = array() ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $defaults = array( 'limit' => 50, 'doctor_id' => null ); $args = wp_parse_args( $args, $defaults ); $where_clauses = array( 'is_from_template = 1' ); $where_values = array(); // Doctor filter if ( ! is_null( $args['doctor_id'] ) ) { $where_clauses[] = 'added_by = %d'; $where_values[] = $args['doctor_id']; } $where_sql = implode( ' AND ', $where_clauses ); $query = "SELECT name, frequency, duration, instruction, COUNT(*) as usage_count, MAX(created_at) as last_used FROM {$table} WHERE {$where_sql} GROUP BY name, frequency, duration, instruction ORDER BY usage_count DESC, last_used DESC LIMIT %d"; $where_values[] = $args['limit']; $templates = $wpdb->get_results( $wpdb->prepare( $query, $where_values ), ARRAY_A ); return array_map( function( $template ) { return array( 'name' => $template['name'], 'frequency' => $template['frequency'], 'duration' => $template['duration'], 'instruction' => $template['instruction'], 'usage_count' => (int) $template['usage_count'], 'last_used' => $template['last_used'] ); }, $templates ); } /** * Check if prescription exists * * @param int $prescription_id Prescription ID * @return bool True if exists, false otherwise * @since 1.0.0 */ public static function exists( $prescription_id ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE id = %d", $prescription_id ) ); return (int) $count > 0; } /** * Validate prescription data * * @param array $prescription_data Prescription data to validate * @return bool|WP_Error True if valid, WP_Error if invalid * @since 1.0.0 */ private static function validate_prescription_data( $prescription_data ) { $errors = array(); // Check required fields foreach ( self::$required_fields as $field ) { if ( empty( $prescription_data[ $field ] ) ) { $errors[] = "Field '{$field}' is required"; } } // Validate entities exist if ( ! empty( $prescription_data['encounter_id'] ) && ! Encounter::exists( $prescription_data['encounter_id'] ) ) { $errors[] = 'Encounter not found'; } if ( ! empty( $prescription_data['patient_id'] ) && ! Patient::exists( $prescription_data['patient_id'] ) ) { $errors[] = 'Patient not found'; } // Validate medication name if ( ! empty( $prescription_data['name'] ) ) { if ( strlen( $prescription_data['name'] ) > 500 ) { $errors[] = 'Medication name is too long (max 500 characters)'; } } // Validate frequency format if ( ! empty( $prescription_data['frequency'] ) ) { if ( strlen( $prescription_data['frequency'] ) > 199 ) { $errors[] = 'Frequency is too long (max 199 characters)'; } } // Validate duration format if ( ! empty( $prescription_data['duration'] ) ) { if ( strlen( $prescription_data['duration'] ) > 199 ) { $errors[] = 'Duration is too long (max 199 characters)'; } } if ( ! empty( $errors ) ) { return new \WP_Error( 'prescription_validation_failed', 'Prescription validation failed', array( 'status' => 400, 'errors' => $errors ) ); } return true; } /** * Format prescription data for API response * * @param array $prescription_data Raw prescription data * @return array Formatted prescription data * @since 1.0.0 */ private static function format_prescription_data( $prescription_data ) { if ( ! $prescription_data ) { return null; } $formatted = array( 'id' => (int) $prescription_data['id'], 'name' => $prescription_data['name'], 'frequency' => $prescription_data['frequency'], 'duration' => $prescription_data['duration'], 'instruction' => $prescription_data['instruction'], 'is_from_template' => (bool) $prescription_data['is_from_template'], 'created_at' => $prescription_data['created_at'], 'encounter' => array( 'id' => (int) $prescription_data['encounter_id'], 'encounter_date' => $prescription_data['encounter_date'] ?? null ), 'patient' => array( 'id' => (int) $prescription_data['patient_id'], 'name' => $prescription_data['patient_name'] ?? '', 'email' => $prescription_data['patient_email'] ?? '' ), 'added_by' => array( 'id' => (int) $prescription_data['added_by'], 'name' => $prescription_data['added_by_name'] ?? '' ), 'doctor_name' => $prescription_data['doctor_name'] ?? '' ); return $formatted; } /** * Get prescription statistics * * @param array $filters Optional filters * @return array Prescription 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['patient_id'] ) ) { $where_clauses[] = 'patient_id = %d'; $where_values[] = $filters['patient_id']; } if ( ! empty( $filters['doctor_id'] ) ) { $where_clauses[] = 'added_by = %d'; $where_values[] = $filters['doctor_id']; } $where_sql = implode( ' AND ', $where_clauses ); $stats = array( 'total_prescriptions' => 0, 'unique_medications' => 0, 'prescriptions_today' => 0, 'prescriptions_this_week' => 0, 'prescriptions_this_month' => 0, 'template_prescriptions' => 0, 'avg_prescriptions_per_encounter' => 0 ); // Total prescriptions $query = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}"; $stats['total_prescriptions'] = (int) $wpdb->get_var( $wpdb->prepare( $query, $where_values ) ); // Unique medications $query = "SELECT COUNT(DISTINCT name) FROM {$table} WHERE {$where_sql}"; $stats['unique_medications'] = (int) $wpdb->get_var( $wpdb->prepare( $query, $where_values ) ); // Prescriptions today $today_where_sql = $where_sql . ' AND DATE(created_at) = CURDATE()'; $query = "SELECT COUNT(*) FROM {$table} WHERE {$today_where_sql}"; $stats['prescriptions_today'] = (int) $wpdb->get_var( $wpdb->prepare( $query, $where_values ) ); // Prescriptions this week $week_where_sql = $where_sql . ' AND WEEK(created_at) = WEEK(CURDATE()) AND YEAR(created_at) = YEAR(CURDATE())'; $query = "SELECT COUNT(*) FROM {$table} WHERE {$week_where_sql}"; $stats['prescriptions_this_week'] = (int) $wpdb->get_var( $wpdb->prepare( $query, $where_values ) ); // Prescriptions this month $month_where_sql = $where_sql . ' AND MONTH(created_at) = MONTH(CURDATE()) AND YEAR(created_at) = YEAR(CURDATE())'; $query = "SELECT COUNT(*) FROM {$table} WHERE {$month_where_sql}"; $stats['prescriptions_this_month'] = (int) $wpdb->get_var( $wpdb->prepare( $query, $where_values ) ); // Template prescriptions $template_where_sql = $where_sql . ' AND is_from_template = 1'; $query = "SELECT COUNT(*) FROM {$table} WHERE {$template_where_sql}"; $stats['template_prescriptions'] = (int) $wpdb->get_var( $wpdb->prepare( $query, $where_values ) ); // Average prescriptions per encounter if ( $stats['total_prescriptions'] > 0 ) { $unique_encounters_query = "SELECT COUNT(DISTINCT encounter_id) FROM {$table} WHERE {$where_sql}"; $unique_encounters = (int) $wpdb->get_var( $wpdb->prepare( $unique_encounters_query, $where_values ) ); if ( $unique_encounters > 0 ) { $stats['avg_prescriptions_per_encounter'] = round( $stats['total_prescriptions'] / $unique_encounters, 2 ); } } return $stats; } }