chore: add spec-kit and standardize signatures
- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
339
src/includes/class-api-init.php
Normal file
339
src/includes/class-api-init.php
Normal file
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* KiviCare API Initialization
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main API initialization class.
|
||||
*
|
||||
* @class KiviCare_API_Init
|
||||
*/
|
||||
class KiviCare_API_Init {
|
||||
|
||||
/**
|
||||
* The single instance of the class.
|
||||
*
|
||||
* @var KiviCare_API_Init
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected static $_instance = null;
|
||||
|
||||
/**
|
||||
* REST API namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const API_NAMESPACE = 'kivicare/v1';
|
||||
|
||||
/**
|
||||
* Main KiviCare_API_Init Instance.
|
||||
*
|
||||
* Ensures only one instance of KiviCare_API_Init is loaded or can be loaded.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @static
|
||||
* @return KiviCare_API_Init - Main instance.
|
||||
*/
|
||||
public static function instance() {
|
||||
if ( is_null( self::$_instance ) ) {
|
||||
self::$_instance = new self();
|
||||
}
|
||||
return self::$_instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* KiviCare_API_Init Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->init_hooks();
|
||||
$this->includes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into actions and filters.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function init_hooks() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
|
||||
add_action( 'init', array( $this, 'check_dependencies' ) );
|
||||
add_filter( 'rest_pre_serve_request', array( $this, 'rest_pre_serve_request' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Include required core files.
|
||||
*/
|
||||
public function includes() {
|
||||
// Base classes will be included here as they are created
|
||||
// include_once KIVICARE_API_ABSPATH . 'services/class-jwt-auth.php';
|
||||
// include_once KIVICARE_API_ABSPATH . 'endpoints/class-auth-endpoints.php';
|
||||
// etc.
|
||||
}
|
||||
|
||||
/**
|
||||
* Check plugin dependencies.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function check_dependencies() {
|
||||
// Check if KiviCare plugin is active
|
||||
if ( ! $this->is_kivicare_active() ) {
|
||||
add_action( 'admin_notices', array( $this, 'kivicare_dependency_notice' ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check required database tables
|
||||
if ( ! $this->check_kivicare_tables() ) {
|
||||
add_action( 'admin_notices', array( $this, 'database_tables_notice' ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if KiviCare plugin is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_kivicare_active() {
|
||||
return is_plugin_active( 'kivicare-clinic-&-patient-management-system/kivicare-clinic-&-patient-management-system.php' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if required KiviCare database tables exist.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function check_kivicare_tables() {
|
||||
global $wpdb;
|
||||
|
||||
$required_tables = array(
|
||||
'kc_clinics',
|
||||
'kc_appointments',
|
||||
'kc_patient_encounters',
|
||||
'kc_prescription',
|
||||
'kc_bills',
|
||||
'kc_services',
|
||||
'kc_doctor_clinic_mappings',
|
||||
'kc_patient_clinic_mappings',
|
||||
);
|
||||
|
||||
foreach ( $required_tables as $table ) {
|
||||
$table_name = $wpdb->prefix . $table;
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$table_exists = $wpdb->get_var( $wpdb->prepare( "SHOW TABLES LIKE %s", $table_name ) );
|
||||
|
||||
if ( $table_name !== $table_exists ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display admin notice for KiviCare dependency.
|
||||
*/
|
||||
public function kivicare_dependency_notice() {
|
||||
?>
|
||||
<div class="notice notice-error">
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'KiviCare API Error:', 'kivicare-api' ); ?></strong>
|
||||
<?php esc_html_e( 'KiviCare Plugin is required for KiviCare API to work properly.', 'kivicare-api' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Display admin notice for missing database tables.
|
||||
*/
|
||||
public function database_tables_notice() {
|
||||
?>
|
||||
<div class="notice notice-error">
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'KiviCare API Error:', 'kivicare-api' ); ?></strong>
|
||||
<?php esc_html_e( 'Required KiviCare database tables are missing. Please ensure KiviCare plugin is properly activated.', 'kivicare-api' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API routes.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function register_rest_routes() {
|
||||
// Only register routes if dependencies are met
|
||||
if ( ! $this->check_dependencies() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow plugins to hook into REST API registration.
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
do_action( 'kivicare_api_register_rest_routes' );
|
||||
|
||||
// Register a test endpoint to verify API is working
|
||||
register_rest_route(
|
||||
self::API_NAMESPACE,
|
||||
'/status',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_api_status' ),
|
||||
'permission_callback' => array( $this, 'check_api_permissions' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API status endpoint.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_api_status( $request ) {
|
||||
global $wpdb;
|
||||
|
||||
// Get basic KiviCare database stats
|
||||
$clinic_count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}kc_clinics WHERE status = 1" );
|
||||
$patient_count = $wpdb->get_var(
|
||||
"SELECT COUNT(DISTINCT u.ID) FROM {$wpdb->users} u
|
||||
INNER JOIN {$wpdb->usermeta} um ON u.ID = um.user_id
|
||||
WHERE um.meta_key = '{$wpdb->prefix}capabilities'
|
||||
AND um.meta_value LIKE '%patient%'"
|
||||
);
|
||||
|
||||
$response_data = array(
|
||||
'status' => 'active',
|
||||
'version' => KIVICARE_API_VERSION,
|
||||
'namespace' => self::API_NAMESPACE,
|
||||
'timestamp' => current_time( 'mysql' ),
|
||||
'wordpress_version' => get_bloginfo( 'version' ),
|
||||
'php_version' => phpversion(),
|
||||
'kivicare_active' => $this->is_kivicare_active(),
|
||||
'statistics' => array(
|
||||
'active_clinics' => (int) $clinic_count,
|
||||
'total_patients' => (int) $patient_count,
|
||||
),
|
||||
'endpoints' => $this->get_available_endpoints(),
|
||||
);
|
||||
|
||||
return rest_ensure_response( $response_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available API endpoints.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_available_endpoints() {
|
||||
return array(
|
||||
'authentication' => array(
|
||||
'POST /auth/login',
|
||||
'POST /auth/refresh',
|
||||
'POST /auth/logout',
|
||||
),
|
||||
'clinics' => array(
|
||||
'GET /clinics',
|
||||
'POST /clinics',
|
||||
'GET /clinics/{id}',
|
||||
'PUT /clinics/{id}',
|
||||
'DELETE /clinics/{id}',
|
||||
),
|
||||
'patients' => array(
|
||||
'GET /patients',
|
||||
'POST /patients',
|
||||
'GET /patients/{id}',
|
||||
'PUT /patients/{id}',
|
||||
'GET /patients/{id}/encounters',
|
||||
),
|
||||
'appointments' => array(
|
||||
'GET /appointments',
|
||||
'POST /appointments',
|
||||
'GET /appointments/{id}',
|
||||
'PUT /appointments/{id}',
|
||||
'DELETE /appointments/{id}',
|
||||
),
|
||||
'encounters' => array(
|
||||
'GET /encounters',
|
||||
'POST /encounters',
|
||||
'GET /encounters/{id}',
|
||||
'PUT /encounters/{id}',
|
||||
'POST /encounters/{id}/prescriptions',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check API permissions.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function check_api_permissions( $request ) {
|
||||
// For status endpoint, allow if user can manage options or has API access
|
||||
if ( current_user_can( 'manage_options' ) || current_user_can( 'manage_kivicare_api' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Allow unauthenticated access to status endpoint for basic health checks
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify REST API response headers.
|
||||
*
|
||||
* @param bool $served Whether the request has already been served.
|
||||
* @param WP_HTTP_Response $result Result to send to the client.
|
||||
* @param WP_REST_Request $request Request used to generate the response.
|
||||
* @param WP_REST_Server $server Server instance.
|
||||
* @return bool
|
||||
*/
|
||||
public function rest_pre_serve_request( $served, $result, $request, $server ) {
|
||||
// Only modify responses for our API namespace
|
||||
$route = $request->get_route();
|
||||
if ( strpos( $route, '/' . self::API_NAMESPACE . '/' ) !== 0 ) {
|
||||
return $served;
|
||||
}
|
||||
|
||||
// Add custom headers
|
||||
$result->header( 'X-KiviCare-API-Version', KIVICARE_API_VERSION );
|
||||
$result->header( 'X-Powered-By', 'KiviCare API by Descomplicar®' );
|
||||
|
||||
// Add CORS headers for development
|
||||
if ( defined( 'KIVICARE_API_DEBUG' ) && KIVICARE_API_DEBUG ) {
|
||||
$result->header( 'Access-Control-Allow-Origin', '*' );
|
||||
$result->header( 'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS' );
|
||||
$result->header( 'Access-Control-Allow-Headers', 'Authorization, Content-Type, X-WP-Nonce' );
|
||||
}
|
||||
|
||||
return $served;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API namespace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_namespace() {
|
||||
return self::API_NAMESPACE;
|
||||
}
|
||||
}
|
||||
1045
src/includes/models/class-appointment.php
Normal file
1045
src/includes/models/class-appointment.php
Normal file
File diff suppressed because it is too large
Load Diff
843
src/includes/models/class-bill.php
Normal file
843
src/includes/models/class-bill.php
Normal file
@@ -0,0 +1,843 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Bill Model
|
||||
*
|
||||
* Handles billing operations and payment management
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Models
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_API\Models;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Bill
|
||||
*
|
||||
* Model for handling billing, invoices and payment management
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Bill {
|
||||
|
||||
/**
|
||||
* Database table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $table_name = 'kc_bills';
|
||||
|
||||
/**
|
||||
* Bill ID
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Bill data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $data = array();
|
||||
|
||||
/**
|
||||
* Required fields for bill creation
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $required_fields = array(
|
||||
'title',
|
||||
'total_amount',
|
||||
'clinic_id'
|
||||
);
|
||||
|
||||
/**
|
||||
* Valid payment statuses
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $valid_payment_statuses = array(
|
||||
'pending' => 'Pending',
|
||||
'paid' => 'Paid',
|
||||
'partial' => 'Partially Paid',
|
||||
'overdue' => 'Overdue',
|
||||
'cancelled' => 'Cancelled'
|
||||
);
|
||||
|
||||
/**
|
||||
* Valid bill statuses
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $valid_statuses = array(
|
||||
1 => 'active',
|
||||
0 => 'inactive'
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param int|array $bill_id_or_data Bill ID or data array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct( $bill_id_or_data = null ) {
|
||||
if ( is_numeric( $bill_id_or_data ) ) {
|
||||
$this->id = (int) $bill_id_or_data;
|
||||
$this->load_data();
|
||||
} elseif ( is_array( $bill_id_or_data ) ) {
|
||||
$this->data = $bill_id_or_data;
|
||||
$this->id = isset( $this->data['id'] ) ? (int) $this->data['id'] : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load bill data from database
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function load_data() {
|
||||
if ( ! $this->id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$bill_data = self::get_bill_full_data( $this->id );
|
||||
|
||||
if ( $bill_data ) {
|
||||
$this->data = $bill_data;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new bill
|
||||
*
|
||||
* @param array $bill_data Bill data
|
||||
* @return int|WP_Error Bill ID on success, WP_Error on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function create( $bill_data ) {
|
||||
// Validate required fields
|
||||
$validation = self::validate_bill_data( $bill_data );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
// Calculate actual amount (total - discount)
|
||||
$total_amount = (float) $bill_data['total_amount'];
|
||||
$discount = isset( $bill_data['discount'] ) ? (float) $bill_data['discount'] : 0;
|
||||
$actual_amount = $total_amount - $discount;
|
||||
|
||||
// Prepare data for insertion
|
||||
$insert_data = array(
|
||||
'encounter_id' => isset( $bill_data['encounter_id'] ) ? (int) $bill_data['encounter_id'] : null,
|
||||
'appointment_id' => isset( $bill_data['appointment_id'] ) ? (int) $bill_data['appointment_id'] : null,
|
||||
'title' => sanitize_text_field( $bill_data['title'] ),
|
||||
'total_amount' => number_format( $total_amount, 2, '.', '' ),
|
||||
'discount' => number_format( $discount, 2, '.', '' ),
|
||||
'actual_amount' => number_format( $actual_amount, 2, '.', '' ),
|
||||
'status' => isset( $bill_data['status'] ) ? (int) $bill_data['status'] : 1,
|
||||
'payment_status' => isset( $bill_data['payment_status'] ) ? sanitize_text_field( $bill_data['payment_status'] ) : 'pending',
|
||||
'clinic_id' => (int) $bill_data['clinic_id'],
|
||||
'created_at' => current_time( 'mysql' )
|
||||
);
|
||||
|
||||
$result = $wpdb->insert( $table, $insert_data );
|
||||
|
||||
if ( $result === false ) {
|
||||
return new \WP_Error(
|
||||
'bill_creation_failed',
|
||||
'Failed to create bill: ' . $wpdb->last_error,
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update bill data
|
||||
*
|
||||
* @param int $bill_id Bill ID
|
||||
* @param array $bill_data Updated bill data
|
||||
* @return bool|WP_Error True on success, WP_Error on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function update( $bill_id, $bill_data ) {
|
||||
if ( ! self::exists( $bill_id ) ) {
|
||||
return new \WP_Error(
|
||||
'bill_not_found',
|
||||
'Bill not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
// Prepare update data
|
||||
$update_data = array();
|
||||
$allowed_fields = array(
|
||||
'title', 'total_amount', 'discount', 'status', 'payment_status'
|
||||
);
|
||||
|
||||
foreach ( $allowed_fields as $field ) {
|
||||
if ( isset( $bill_data[ $field ] ) ) {
|
||||
$value = $bill_data[ $field ];
|
||||
|
||||
switch ( $field ) {
|
||||
case 'total_amount':
|
||||
case 'discount':
|
||||
$update_data[ $field ] = number_format( (float) $value, 2, '.', '' );
|
||||
break;
|
||||
case 'status':
|
||||
$update_data[ $field ] = (int) $value;
|
||||
break;
|
||||
case 'payment_status':
|
||||
if ( in_array( $value, array_keys( self::$valid_payment_statuses ) ) ) {
|
||||
$update_data[ $field ] = sanitize_text_field( $value );
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$update_data[ $field ] = sanitize_text_field( $value );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate actual amount if total or discount changed
|
||||
if ( isset( $update_data['total_amount'] ) || isset( $update_data['discount'] ) ) {
|
||||
$current_bill = self::get_by_id( $bill_id );
|
||||
|
||||
$new_total = isset( $update_data['total_amount'] ) ?
|
||||
(float) $update_data['total_amount'] :
|
||||
(float) $current_bill['total_amount'];
|
||||
|
||||
$new_discount = isset( $update_data['discount'] ) ?
|
||||
(float) $update_data['discount'] :
|
||||
(float) $current_bill['discount'];
|
||||
|
||||
$update_data['actual_amount'] = number_format( $new_total - $new_discount, 2, '.', '' );
|
||||
}
|
||||
|
||||
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' => $bill_id ),
|
||||
null,
|
||||
array( '%d' )
|
||||
);
|
||||
|
||||
if ( $result === false ) {
|
||||
return new \WP_Error(
|
||||
'bill_update_failed',
|
||||
'Failed to update bill: ' . $wpdb->last_error,
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a bill
|
||||
*
|
||||
* @param int $bill_id Bill ID
|
||||
* @return bool|WP_Error True on success, WP_Error on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function delete( $bill_id ) {
|
||||
if ( ! self::exists( $bill_id ) ) {
|
||||
return new \WP_Error(
|
||||
'bill_not_found',
|
||||
'Bill not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check if bill is paid - might want to prevent deletion
|
||||
$bill = self::get_by_id( $bill_id );
|
||||
if ( $bill['payment_status'] === 'paid' ) {
|
||||
return new \WP_Error(
|
||||
'cannot_delete_paid_bill',
|
||||
'Cannot delete a paid bill',
|
||||
array( 'status' => 409 )
|
||||
);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$result = $wpdb->delete(
|
||||
$table,
|
||||
array( 'id' => $bill_id ),
|
||||
array( '%d' )
|
||||
);
|
||||
|
||||
if ( $result === false ) {
|
||||
return new \WP_Error(
|
||||
'bill_deletion_failed',
|
||||
'Failed to delete bill: ' . $wpdb->last_error,
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bill by ID
|
||||
*
|
||||
* @param int $bill_id Bill ID
|
||||
* @return array|null Bill data or null if not found
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_by_id( $bill_id ) {
|
||||
return self::get_bill_full_data( $bill_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all bills with optional filtering
|
||||
*
|
||||
* @param array $args Query arguments
|
||||
* @return array Array of bill 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,
|
||||
'encounter_id' => null,
|
||||
'appointment_id' => null,
|
||||
'status' => null,
|
||||
'payment_status' => null,
|
||||
'date_from' => null,
|
||||
'date_to' => 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();
|
||||
|
||||
// Clinic filter
|
||||
if ( ! is_null( $args['clinic_id'] ) ) {
|
||||
$where_clauses[] = 'b.clinic_id = %d';
|
||||
$where_values[] = $args['clinic_id'];
|
||||
}
|
||||
|
||||
// Encounter filter
|
||||
if ( ! is_null( $args['encounter_id'] ) ) {
|
||||
$where_clauses[] = 'b.encounter_id = %d';
|
||||
$where_values[] = $args['encounter_id'];
|
||||
}
|
||||
|
||||
// Appointment filter
|
||||
if ( ! is_null( $args['appointment_id'] ) ) {
|
||||
$where_clauses[] = 'b.appointment_id = %d';
|
||||
$where_values[] = $args['appointment_id'];
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if ( ! is_null( $args['status'] ) ) {
|
||||
$where_clauses[] = 'b.status = %d';
|
||||
$where_values[] = $args['status'];
|
||||
}
|
||||
|
||||
// Payment status filter
|
||||
if ( ! is_null( $args['payment_status'] ) ) {
|
||||
$where_clauses[] = 'b.payment_status = %s';
|
||||
$where_values[] = $args['payment_status'];
|
||||
}
|
||||
|
||||
// Date range filters
|
||||
if ( ! is_null( $args['date_from'] ) ) {
|
||||
$where_clauses[] = 'DATE(b.created_at) >= %s';
|
||||
$where_values[] = $args['date_from'];
|
||||
}
|
||||
|
||||
if ( ! is_null( $args['date_to'] ) ) {
|
||||
$where_clauses[] = 'DATE(b.created_at) <= %s';
|
||||
$where_values[] = $args['date_to'];
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if ( ! empty( $args['search'] ) ) {
|
||||
$where_clauses[] = '(b.title LIKE %s OR p.first_name LIKE %s OR p.last_name LIKE %s OR c.name LIKE %s)';
|
||||
$search_term = '%' . $wpdb->esc_like( $args['search'] ) . '%';
|
||||
$where_values = array_merge( $where_values, array_fill( 0, 4, $search_term ) );
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
// Build query
|
||||
$query = "SELECT b.*,
|
||||
c.name as clinic_name,
|
||||
CONCAT(p.first_name, ' ', p.last_name) as patient_name,
|
||||
p.user_email as patient_email,
|
||||
e.encounter_date,
|
||||
a.appointment_start_date
|
||||
FROM {$table} b
|
||||
LEFT JOIN {$wpdb->prefix}kc_clinics c ON b.clinic_id = c.id
|
||||
LEFT JOIN {$wpdb->prefix}kc_patient_encounters e ON b.encounter_id = e.id
|
||||
LEFT JOIN {$wpdb->prefix}users p ON e.patient_id = p.ID
|
||||
LEFT JOIN {$wpdb->prefix}kc_appointments a ON b.appointment_id = a.id
|
||||
WHERE {$where_sql}";
|
||||
|
||||
$query .= sprintf( ' ORDER BY b.%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 );
|
||||
}
|
||||
|
||||
$bills = $wpdb->get_results( $query, ARRAY_A );
|
||||
|
||||
return array_map( array( self::class, 'format_bill_data' ), $bills );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bill full data with related entities
|
||||
*
|
||||
* @param int $bill_id Bill ID
|
||||
* @return array|null Full bill data or null if not found
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_bill_full_data( $bill_id ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$bill = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT b.*,
|
||||
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,
|
||||
e.encounter_date,
|
||||
CONCAT(d.first_name, ' ', d.last_name) as doctor_name,
|
||||
a.appointment_start_date, a.appointment_start_time
|
||||
FROM {$table} b
|
||||
LEFT JOIN {$wpdb->prefix}kc_clinics c ON b.clinic_id = c.id
|
||||
LEFT JOIN {$wpdb->prefix}kc_patient_encounters e ON b.encounter_id = e.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}kc_appointments a ON b.appointment_id = a.id
|
||||
WHERE b.id = %d",
|
||||
$bill_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( ! $bill ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::format_bill_data( $bill );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process payment for bill
|
||||
*
|
||||
* @param int $bill_id Bill ID
|
||||
* @param array $payment_data Payment information
|
||||
* @return bool|WP_Error True on success, WP_Error on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function process_payment( $bill_id, $payment_data ) {
|
||||
if ( ! self::exists( $bill_id ) ) {
|
||||
return new \WP_Error(
|
||||
'bill_not_found',
|
||||
'Bill not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
$bill = self::get_by_id( $bill_id );
|
||||
|
||||
// Validate payment amount
|
||||
$payment_amount = (float) $payment_data['amount'];
|
||||
$bill_amount = (float) $bill['actual_amount'];
|
||||
|
||||
if ( $payment_amount <= 0 ) {
|
||||
return new \WP_Error(
|
||||
'invalid_payment_amount',
|
||||
'Payment amount must be greater than zero',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
if ( $payment_amount > $bill_amount ) {
|
||||
return new \WP_Error(
|
||||
'payment_exceeds_bill',
|
||||
'Payment amount exceeds bill amount',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
// Determine new payment status
|
||||
$new_status = 'paid';
|
||||
if ( $payment_amount < $bill_amount ) {
|
||||
$new_status = 'partial';
|
||||
}
|
||||
|
||||
// Update bill payment status
|
||||
$result = self::update( $bill_id, array(
|
||||
'payment_status' => $new_status
|
||||
) );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Log payment (could be extended to create payment records)
|
||||
do_action( 'kivicare_payment_processed', $bill_id, $payment_data );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get overdue bills
|
||||
*
|
||||
* @param array $args Query arguments
|
||||
* @return array Array of overdue bills
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_overdue_bills( $args = array() ) {
|
||||
$defaults = array(
|
||||
'days_overdue' => 30,
|
||||
'clinic_id' => null,
|
||||
'limit' => 50,
|
||||
'offset' => 0
|
||||
);
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$where_clauses = array(
|
||||
'payment_status IN ("pending", "partial")',
|
||||
'status = 1',
|
||||
'DATEDIFF(CURDATE(), created_at) > %d'
|
||||
);
|
||||
$where_values = array( $args['days_overdue'] );
|
||||
|
||||
if ( ! is_null( $args['clinic_id'] ) ) {
|
||||
$where_clauses[] = 'clinic_id = %d';
|
||||
$where_values[] = $args['clinic_id'];
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
$bills = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT *, DATEDIFF(CURDATE(), created_at) as days_overdue
|
||||
FROM {$table}
|
||||
WHERE {$where_sql}
|
||||
ORDER BY created_at ASC
|
||||
LIMIT %d OFFSET %d",
|
||||
array_merge( $where_values, array( $args['limit'], $args['offset'] ) )
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
// Update overdue bills status
|
||||
foreach ( $bills as &$bill ) {
|
||||
if ( $bill['payment_status'] !== 'overdue' ) {
|
||||
self::update( $bill['id'], array( 'payment_status' => 'overdue' ) );
|
||||
$bill['payment_status'] = 'overdue';
|
||||
}
|
||||
}
|
||||
|
||||
return array_map( array( self::class, 'format_bill_data' ), $bills );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if bill exists
|
||||
*
|
||||
* @param int $bill_id Bill ID
|
||||
* @return bool True if exists, false otherwise
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function exists( $bill_id ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE id = %d",
|
||||
$bill_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate bill data
|
||||
*
|
||||
* @param array $bill_data Bill data to validate
|
||||
* @return bool|WP_Error True if valid, WP_Error if invalid
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function validate_bill_data( $bill_data ) {
|
||||
$errors = array();
|
||||
|
||||
// Check required fields
|
||||
foreach ( self::$required_fields as $field ) {
|
||||
if ( empty( $bill_data[ $field ] ) ) {
|
||||
$errors[] = "Field '{$field}' is required";
|
||||
}
|
||||
}
|
||||
|
||||
// Validate amounts
|
||||
if ( isset( $bill_data['total_amount'] ) ) {
|
||||
if ( ! is_numeric( $bill_data['total_amount'] ) || (float) $bill_data['total_amount'] <= 0 ) {
|
||||
$errors[] = 'Total amount must be a positive number';
|
||||
}
|
||||
}
|
||||
|
||||
if ( isset( $bill_data['discount'] ) ) {
|
||||
if ( ! is_numeric( $bill_data['discount'] ) || (float) $bill_data['discount'] < 0 ) {
|
||||
$errors[] = 'Discount must be a non-negative number';
|
||||
}
|
||||
|
||||
// Check if discount doesn't exceed total
|
||||
if ( isset( $bill_data['total_amount'] ) &&
|
||||
(float) $bill_data['discount'] > (float) $bill_data['total_amount'] ) {
|
||||
$errors[] = 'Discount cannot exceed total amount';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate payment status
|
||||
if ( isset( $bill_data['payment_status'] ) &&
|
||||
! array_key_exists( $bill_data['payment_status'], self::$valid_payment_statuses ) ) {
|
||||
$errors[] = 'Invalid payment status';
|
||||
}
|
||||
|
||||
// Validate entities exist
|
||||
if ( ! empty( $bill_data['clinic_id'] ) &&
|
||||
! Clinic::exists( $bill_data['clinic_id'] ) ) {
|
||||
$errors[] = 'Clinic not found';
|
||||
}
|
||||
|
||||
if ( ! empty( $bill_data['encounter_id'] ) &&
|
||||
! Encounter::exists( $bill_data['encounter_id'] ) ) {
|
||||
$errors[] = 'Encounter not found';
|
||||
}
|
||||
|
||||
if ( ! empty( $bill_data['appointment_id'] ) &&
|
||||
! Appointment::exists( $bill_data['appointment_id'] ) ) {
|
||||
$errors[] = 'Appointment not found';
|
||||
}
|
||||
|
||||
// Validate status
|
||||
if ( isset( $bill_data['status'] ) &&
|
||||
! array_key_exists( $bill_data['status'], self::$valid_statuses ) ) {
|
||||
$errors[] = 'Invalid status value';
|
||||
}
|
||||
|
||||
if ( ! empty( $errors ) ) {
|
||||
return new \WP_Error(
|
||||
'bill_validation_failed',
|
||||
'Bill validation failed',
|
||||
array(
|
||||
'status' => 400,
|
||||
'errors' => $errors
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bill data for API response
|
||||
*
|
||||
* @param array $bill_data Raw bill data
|
||||
* @return array Formatted bill data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function format_bill_data( $bill_data ) {
|
||||
if ( ! $bill_data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$formatted = array(
|
||||
'id' => (int) $bill_data['id'],
|
||||
'title' => $bill_data['title'],
|
||||
'total_amount' => (float) $bill_data['total_amount'],
|
||||
'discount' => (float) $bill_data['discount'],
|
||||
'actual_amount' => (float) $bill_data['actual_amount'],
|
||||
'status' => (int) $bill_data['status'],
|
||||
'status_text' => self::$valid_statuses[ $bill_data['status'] ] ?? 'unknown',
|
||||
'payment_status' => $bill_data['payment_status'],
|
||||
'payment_status_text' => self::$valid_payment_statuses[ $bill_data['payment_status'] ] ?? 'Unknown',
|
||||
'created_at' => $bill_data['created_at'],
|
||||
'clinic' => array(
|
||||
'id' => (int) $bill_data['clinic_id'],
|
||||
'name' => $bill_data['clinic_name'] ?? '',
|
||||
'address' => $bill_data['clinic_address'] ?? ''
|
||||
),
|
||||
'encounter_id' => isset( $bill_data['encounter_id'] ) ? (int) $bill_data['encounter_id'] : null,
|
||||
'appointment_id' => isset( $bill_data['appointment_id'] ) ? (int) $bill_data['appointment_id'] : null,
|
||||
'patient' => array(
|
||||
'name' => $bill_data['patient_name'] ?? '',
|
||||
'email' => $bill_data['patient_email'] ?? ''
|
||||
),
|
||||
'doctor_name' => $bill_data['doctor_name'] ?? '',
|
||||
'encounter_date' => $bill_data['encounter_date'] ?? null,
|
||||
'appointment_date' => $bill_data['appointment_start_date'] ?? null,
|
||||
'appointment_time' => $bill_data['appointment_start_time'] ?? null,
|
||||
'days_overdue' => isset( $bill_data['days_overdue'] ) ? (int) $bill_data['days_overdue'] : null
|
||||
);
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bill statistics
|
||||
*
|
||||
* @param array $filters Optional filters
|
||||
* @return array Bill statistics
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_statistics( $filters = array() ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$where_clauses = array( 'status = 1' );
|
||||
$where_values = array();
|
||||
|
||||
if ( ! empty( $filters['clinic_id'] ) ) {
|
||||
$where_clauses[] = 'clinic_id = %d';
|
||||
$where_values[] = $filters['clinic_id'];
|
||||
}
|
||||
|
||||
if ( ! empty( $filters['date_from'] ) ) {
|
||||
$where_clauses[] = 'DATE(created_at) >= %s';
|
||||
$where_values[] = $filters['date_from'];
|
||||
}
|
||||
|
||||
if ( ! empty( $filters['date_to'] ) ) {
|
||||
$where_clauses[] = 'DATE(created_at) <= %s';
|
||||
$where_values[] = $filters['date_to'];
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
$stats = array(
|
||||
'total_bills' => 0,
|
||||
'total_revenue' => 0,
|
||||
'pending_amount' => 0,
|
||||
'paid_amount' => 0,
|
||||
'overdue_amount' => 0,
|
||||
'bills_today' => 0,
|
||||
'bills_this_month' => 0,
|
||||
'average_bill_amount' => 0,
|
||||
'payment_status_breakdown' => array()
|
||||
);
|
||||
|
||||
// Total bills
|
||||
$query = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$stats['total_bills'] = (int) $wpdb->get_var( $query );
|
||||
|
||||
// Total revenue (actual amount)
|
||||
$query = "SELECT SUM(CAST(actual_amount AS DECIMAL(10,2))) FROM {$table} WHERE {$where_sql}";
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$stats['total_revenue'] = (float) $wpdb->get_var( $query ) ?: 0;
|
||||
|
||||
// Revenue by payment status
|
||||
foreach ( array_keys( self::$valid_payment_statuses ) as $status ) {
|
||||
$status_where = $where_clauses;
|
||||
$status_where[] = 'payment_status = %s';
|
||||
$status_values = array_merge( $where_values, array( $status ) );
|
||||
|
||||
$amount_query = $wpdb->prepare(
|
||||
"SELECT SUM(CAST(actual_amount AS DECIMAL(10,2))) FROM {$table} WHERE " . implode( ' AND ', $status_where ),
|
||||
$status_values
|
||||
);
|
||||
|
||||
$count_query = $wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $status_where ),
|
||||
$status_values
|
||||
);
|
||||
|
||||
$amount = (float) $wpdb->get_var( $amount_query ) ?: 0;
|
||||
$count = (int) $wpdb->get_var( $count_query );
|
||||
|
||||
$stats[ $status . '_amount' ] = $amount;
|
||||
$stats['payment_status_breakdown'][ $status ] = array(
|
||||
'count' => $count,
|
||||
'amount' => $amount
|
||||
);
|
||||
}
|
||||
|
||||
// Bills today
|
||||
$today_where = array_merge( $where_clauses, array( 'DATE(created_at) = CURDATE()' ) );
|
||||
$query = "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $today_where );
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$stats['bills_today'] = (int) $wpdb->get_var( $query );
|
||||
|
||||
// Bills this month
|
||||
$month_where = array_merge( $where_clauses, array(
|
||||
'MONTH(created_at) = MONTH(CURDATE())',
|
||||
'YEAR(created_at) = YEAR(CURDATE())'
|
||||
) );
|
||||
$query = "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $month_where );
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$stats['bills_this_month'] = (int) $wpdb->get_var( $query );
|
||||
|
||||
// Average bill amount
|
||||
if ( $stats['total_bills'] > 0 ) {
|
||||
$stats['average_bill_amount'] = round( $stats['total_revenue'] / $stats['total_bills'], 2 );
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
657
src/includes/models/class-clinic.php
Normal file
657
src/includes/models/class-clinic.php
Normal file
@@ -0,0 +1,657 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Clinic Model
|
||||
*
|
||||
* Handles clinic entity operations and business logic
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Models
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_API\Models;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Clinic
|
||||
*
|
||||
* Model for handling clinic data and operations
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Clinic {
|
||||
|
||||
/**
|
||||
* Database table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $table_name = 'kc_clinics';
|
||||
|
||||
/**
|
||||
* Clinic ID
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Clinic properties
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $data = array();
|
||||
|
||||
/**
|
||||
* Required fields for clinic creation
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $required_fields = array(
|
||||
'name',
|
||||
'email',
|
||||
'telephone_no',
|
||||
'address',
|
||||
'city',
|
||||
'state',
|
||||
'country',
|
||||
'postal_code'
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param int|array $clinic_id_or_data Clinic ID or data array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct( $clinic_id_or_data = null ) {
|
||||
if ( is_numeric( $clinic_id_or_data ) ) {
|
||||
$this->id = (int) $clinic_id_or_data;
|
||||
$this->load_data();
|
||||
} elseif ( is_array( $clinic_id_or_data ) ) {
|
||||
$this->data = $clinic_id_or_data;
|
||||
$this->id = isset( $this->data['id'] ) ? (int) $this->data['id'] : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load clinic data from database
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function load_data() {
|
||||
if ( ! $this->id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$clinic_data = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE id = %d",
|
||||
$this->id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( $clinic_data ) {
|
||||
$this->data = $clinic_data;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new clinic
|
||||
*
|
||||
* @param array $clinic_data Clinic data
|
||||
* @return int|WP_Error Clinic ID on success, WP_Error on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function create( $clinic_data ) {
|
||||
// Validate required fields
|
||||
$validation = self::validate_clinic_data( $clinic_data );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
// Prepare data for insertion
|
||||
$insert_data = array(
|
||||
'name' => sanitize_text_field( $clinic_data['name'] ),
|
||||
'email' => sanitize_email( $clinic_data['email'] ),
|
||||
'telephone_no' => sanitize_text_field( $clinic_data['telephone_no'] ),
|
||||
'specialties' => isset( $clinic_data['specialties'] ) ? wp_json_encode( $clinic_data['specialties'] ) : '',
|
||||
'address' => sanitize_textarea_field( $clinic_data['address'] ),
|
||||
'city' => sanitize_text_field( $clinic_data['city'] ),
|
||||
'state' => sanitize_text_field( $clinic_data['state'] ),
|
||||
'country' => sanitize_text_field( $clinic_data['country'] ),
|
||||
'postal_code' => sanitize_text_field( $clinic_data['postal_code'] ),
|
||||
'status' => isset( $clinic_data['status'] ) ? (int) $clinic_data['status'] : 1,
|
||||
'clinic_admin_id' => isset( $clinic_data['clinic_admin_id'] ) ? (int) $clinic_data['clinic_admin_id'] : null,
|
||||
'clinic_logo' => isset( $clinic_data['clinic_logo'] ) ? (int) $clinic_data['clinic_logo'] : null,
|
||||
'profile_image' => isset( $clinic_data['profile_image'] ) ? (int) $clinic_data['profile_image'] : null,
|
||||
'extra' => isset( $clinic_data['extra'] ) ? wp_json_encode( $clinic_data['extra'] ) : '',
|
||||
'created_at' => current_time( 'mysql' )
|
||||
);
|
||||
|
||||
$insert_data = array_map( array( self::class, 'prepare_for_db' ), $insert_data );
|
||||
|
||||
$result = $wpdb->insert( $table, $insert_data );
|
||||
|
||||
if ( $result === false ) {
|
||||
return new \WP_Error(
|
||||
'clinic_creation_failed',
|
||||
'Failed to create clinic: ' . $wpdb->last_error,
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update clinic data
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @param array $clinic_data Updated clinic data
|
||||
* @return bool|WP_Error True on success, WP_Error on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function update( $clinic_id, $clinic_data ) {
|
||||
if ( ! self::exists( $clinic_id ) ) {
|
||||
return new \WP_Error(
|
||||
'clinic_not_found',
|
||||
'Clinic not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
// Prepare update data
|
||||
$update_data = array();
|
||||
$allowed_fields = array(
|
||||
'name', 'email', 'telephone_no', 'specialties', 'address',
|
||||
'city', 'state', 'country', 'postal_code', 'status',
|
||||
'clinic_admin_id', 'clinic_logo', 'profile_image', 'extra'
|
||||
);
|
||||
|
||||
foreach ( $allowed_fields as $field ) {
|
||||
if ( isset( $clinic_data[ $field ] ) ) {
|
||||
$value = $clinic_data[ $field ];
|
||||
|
||||
switch ( $field ) {
|
||||
case 'email':
|
||||
$update_data[ $field ] = sanitize_email( $value );
|
||||
break;
|
||||
case 'specialties':
|
||||
case 'extra':
|
||||
$update_data[ $field ] = is_array( $value ) ? wp_json_encode( $value ) : $value;
|
||||
break;
|
||||
case 'status':
|
||||
case 'clinic_admin_id':
|
||||
case 'clinic_logo':
|
||||
case 'profile_image':
|
||||
$update_data[ $field ] = (int) $value;
|
||||
break;
|
||||
case 'address':
|
||||
$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 )
|
||||
);
|
||||
}
|
||||
|
||||
$update_data = array_map( array( self::class, 'prepare_for_db' ), $update_data );
|
||||
|
||||
$result = $wpdb->update(
|
||||
$table,
|
||||
$update_data,
|
||||
array( 'id' => $clinic_id ),
|
||||
null,
|
||||
array( '%d' )
|
||||
);
|
||||
|
||||
if ( $result === false ) {
|
||||
return new \WP_Error(
|
||||
'clinic_update_failed',
|
||||
'Failed to update clinic: ' . $wpdb->last_error,
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a clinic
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return bool|WP_Error True on success, WP_Error on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function delete( $clinic_id ) {
|
||||
if ( ! self::exists( $clinic_id ) ) {
|
||||
return new \WP_Error(
|
||||
'clinic_not_found',
|
||||
'Clinic not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check for dependencies
|
||||
if ( self::has_dependencies( $clinic_id ) ) {
|
||||
return new \WP_Error(
|
||||
'clinic_has_dependencies',
|
||||
'Cannot delete clinic with associated appointments or patients',
|
||||
array( 'status' => 409 )
|
||||
);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$result = $wpdb->delete(
|
||||
$table,
|
||||
array( 'id' => $clinic_id ),
|
||||
array( '%d' )
|
||||
);
|
||||
|
||||
if ( $result === false ) {
|
||||
return new \WP_Error(
|
||||
'clinic_deletion_failed',
|
||||
'Failed to delete clinic: ' . $wpdb->last_error,
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinic by ID
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return array|null Clinic data or null if not found
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_by_id( $clinic_id ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$clinic = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE id = %d",
|
||||
$clinic_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( $clinic ) {
|
||||
return self::format_clinic_data( $clinic );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all clinics with optional filtering
|
||||
*
|
||||
* @param array $args Query arguments
|
||||
* @return array Array of clinic data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_all( $args = array() ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$defaults = array(
|
||||
'status' => null,
|
||||
'search' => '',
|
||||
'limit' => 50,
|
||||
'offset' => 0,
|
||||
'orderby' => 'name',
|
||||
'order' => 'ASC'
|
||||
);
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
$where_clauses = array( '1=1' );
|
||||
$where_values = array();
|
||||
|
||||
// Status filter
|
||||
if ( ! is_null( $args['status'] ) ) {
|
||||
$where_clauses[] = 'status = %d';
|
||||
$where_values[] = $args['status'];
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if ( ! empty( $args['search'] ) ) {
|
||||
$where_clauses[] = '(name LIKE %s OR email LIKE %s OR city LIKE %s)';
|
||||
$search_term = '%' . $wpdb->esc_like( $args['search'] ) . '%';
|
||||
$where_values[] = $search_term;
|
||||
$where_values[] = $search_term;
|
||||
$where_values[] = $search_term;
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
// Build query
|
||||
$query = "SELECT * FROM {$table} WHERE {$where_sql}";
|
||||
$query .= sprintf( ' ORDER BY %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 );
|
||||
}
|
||||
|
||||
$clinics = $wpdb->get_results( $query, ARRAY_A );
|
||||
|
||||
return array_map( array( self::class, 'format_clinic_data' ), $clinics );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total count of clinics
|
||||
*
|
||||
* @param array $args Filter arguments
|
||||
* @return int Total count
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_count( $args = array() ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$where_clauses = array( '1=1' );
|
||||
$where_values = array();
|
||||
|
||||
if ( ! is_null( $args['status'] ?? null ) ) {
|
||||
$where_clauses[] = 'status = %d';
|
||||
$where_values[] = $args['status'];
|
||||
}
|
||||
|
||||
if ( ! empty( $args['search'] ?? '' ) ) {
|
||||
$where_clauses[] = '(name LIKE %s OR email LIKE %s OR city LIKE %s)';
|
||||
$search_term = '%' . $wpdb->esc_like( $args['search'] ) . '%';
|
||||
$where_values[] = $search_term;
|
||||
$where_values[] = $search_term;
|
||||
$where_values[] = $search_term;
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
$query = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
|
||||
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
|
||||
return (int) $wpdb->get_var( $query );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if clinic exists
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return bool True if exists, false otherwise
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function exists( $clinic_id ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE id = %d",
|
||||
$clinic_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if clinic has dependencies (appointments, patients, etc.)
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return bool True if has dependencies, false otherwise
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function has_dependencies( $clinic_id ) {
|
||||
global $wpdb;
|
||||
|
||||
// Check appointments
|
||||
$appointments_count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments WHERE clinic_id = %d",
|
||||
$clinic_id
|
||||
)
|
||||
);
|
||||
|
||||
if ( (int) $appointments_count > 0 ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check patient mappings
|
||||
$patients_count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_patient_clinic_mappings WHERE clinic_id = %d",
|
||||
$clinic_id
|
||||
)
|
||||
);
|
||||
|
||||
if ( (int) $patients_count > 0 ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check doctor mappings
|
||||
$doctors_count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_doctor_clinic_mappings WHERE clinic_id = %d",
|
||||
$clinic_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $doctors_count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate clinic data
|
||||
*
|
||||
* @param array $clinic_data Clinic data to validate
|
||||
* @return bool|WP_Error True if valid, WP_Error if invalid
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function validate_clinic_data( $clinic_data ) {
|
||||
$errors = array();
|
||||
|
||||
// Check required fields
|
||||
foreach ( self::$required_fields as $field ) {
|
||||
if ( empty( $clinic_data[ $field ] ) ) {
|
||||
$errors[] = "Field '{$field}' is required";
|
||||
}
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
if ( ! empty( $clinic_data['email'] ) && ! is_email( $clinic_data['email'] ) ) {
|
||||
$errors[] = 'Invalid email format';
|
||||
}
|
||||
|
||||
// Check for duplicate email
|
||||
if ( ! empty( $clinic_data['email'] ) ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$existing_clinic = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE email = %s",
|
||||
$clinic_data['email']
|
||||
)
|
||||
);
|
||||
|
||||
if ( $existing_clinic ) {
|
||||
$errors[] = 'A clinic with this email already exists';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $errors ) ) {
|
||||
return new \WP_Error(
|
||||
'clinic_validation_failed',
|
||||
'Clinic validation failed',
|
||||
array(
|
||||
'status' => 400,
|
||||
'errors' => $errors
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format clinic data for API response
|
||||
*
|
||||
* @param array $clinic_data Raw clinic data
|
||||
* @return array Formatted clinic data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function format_clinic_data( $clinic_data ) {
|
||||
if ( ! $clinic_data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse JSON fields
|
||||
if ( ! empty( $clinic_data['specialties'] ) ) {
|
||||
$clinic_data['specialties'] = json_decode( $clinic_data['specialties'], true ) ?: array();
|
||||
}
|
||||
|
||||
if ( ! empty( $clinic_data['extra'] ) ) {
|
||||
$clinic_data['extra'] = json_decode( $clinic_data['extra'], true ) ?: array();
|
||||
}
|
||||
|
||||
// Cast numeric fields
|
||||
$numeric_fields = array( 'id', 'status', 'clinic_admin_id', 'clinic_logo', 'profile_image' );
|
||||
foreach ( $numeric_fields as $field ) {
|
||||
if ( isset( $clinic_data[ $field ] ) ) {
|
||||
$clinic_data[ $field ] = (int) $clinic_data[ $field ];
|
||||
}
|
||||
}
|
||||
|
||||
return $clinic_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare data for database insertion/update
|
||||
*
|
||||
* @param mixed $value Data value
|
||||
* @return mixed Prepared value
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function prepare_for_db( $value ) {
|
||||
if ( is_null( $value ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( is_array( $value ) ) {
|
||||
return wp_json_encode( $value );
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinic statistics
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return array Clinic statistics
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_statistics( $clinic_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$stats = array(
|
||||
'total_appointments' => 0,
|
||||
'total_patients' => 0,
|
||||
'total_doctors' => 0,
|
||||
'revenue_this_month' => 0,
|
||||
'appointments_today' => 0
|
||||
);
|
||||
|
||||
// Total appointments
|
||||
$stats['total_appointments'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments WHERE clinic_id = %d",
|
||||
$clinic_id
|
||||
)
|
||||
);
|
||||
|
||||
// Total patients
|
||||
$stats['total_patients'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_patient_clinic_mappings WHERE clinic_id = %d",
|
||||
$clinic_id
|
||||
)
|
||||
);
|
||||
|
||||
// Total doctors
|
||||
$stats['total_doctors'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_doctor_clinic_mappings WHERE clinic_id = %d",
|
||||
$clinic_id
|
||||
)
|
||||
);
|
||||
|
||||
// Revenue this month
|
||||
$stats['revenue_this_month'] = (float) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT SUM(CAST(actual_amount AS DECIMAL(10,2)))
|
||||
FROM {$wpdb->prefix}kc_bills
|
||||
WHERE clinic_id = %d
|
||||
AND MONTH(created_at) = MONTH(CURRENT_DATE())
|
||||
AND YEAR(created_at) = YEAR(CURRENT_DATE())
|
||||
AND payment_status = 'paid'",
|
||||
$clinic_id
|
||||
)
|
||||
) ?: 0;
|
||||
|
||||
// Appointments today
|
||||
$stats['appointments_today'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments
|
||||
WHERE clinic_id = %d
|
||||
AND appointment_start_date = CURDATE()",
|
||||
$clinic_id
|
||||
)
|
||||
);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
980
src/includes/models/class-doctor.php
Normal file
980
src/includes/models/class-doctor.php
Normal file
@@ -0,0 +1,980 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Doctor Model
|
||||
*
|
||||
* Handles doctor entity operations, schedules and clinic associations
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Models
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_API\Models;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Doctor
|
||||
*
|
||||
* Model for handling doctor data, schedules and clinic management
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Doctor {
|
||||
|
||||
/**
|
||||
* Doctor user ID (wp_users table)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $user_id;
|
||||
|
||||
/**
|
||||
* Doctor data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $data = array();
|
||||
|
||||
/**
|
||||
* Required fields for doctor registration
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $required_fields = array(
|
||||
'first_name',
|
||||
'last_name',
|
||||
'user_email',
|
||||
'specialization',
|
||||
'qualification'
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param int|array $user_id_or_data Doctor 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 doctor 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;
|
||||
}
|
||||
|
||||
$doctor_data = self::get_doctor_full_data( $this->user_id );
|
||||
|
||||
if ( $doctor_data ) {
|
||||
$this->data = $doctor_data;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new doctor
|
||||
*
|
||||
* @param array $doctor_data Doctor data
|
||||
* @return int|WP_Error Doctor user ID on success, WP_Error on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function create( $doctor_data ) {
|
||||
// Validate required fields
|
||||
$validation = self::validate_doctor_data( $doctor_data );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// Create WordPress user
|
||||
$user_data = array(
|
||||
'user_login' => self::generate_unique_username( $doctor_data ),
|
||||
'user_email' => sanitize_email( $doctor_data['user_email'] ),
|
||||
'first_name' => sanitize_text_field( $doctor_data['first_name'] ),
|
||||
'last_name' => sanitize_text_field( $doctor_data['last_name'] ),
|
||||
'role' => 'kivicare_doctor',
|
||||
'user_pass' => isset( $doctor_data['user_pass'] ) ? $doctor_data['user_pass'] : wp_generate_password()
|
||||
);
|
||||
|
||||
$user_id = wp_insert_user( $user_data );
|
||||
|
||||
if ( is_wp_error( $user_id ) ) {
|
||||
return new \WP_Error(
|
||||
'doctor_creation_failed',
|
||||
'Failed to create doctor user: ' . $user_id->get_error_message(),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
// Add doctor meta data
|
||||
$meta_fields = array(
|
||||
'specialization' => sanitize_text_field( $doctor_data['specialization'] ),
|
||||
'qualification' => sanitize_text_field( $doctor_data['qualification'] ),
|
||||
'experience_years' => isset( $doctor_data['experience_years'] ) ? (int) $doctor_data['experience_years'] : 0,
|
||||
'mobile_number' => isset( $doctor_data['mobile_number'] ) ? sanitize_text_field( $doctor_data['mobile_number'] ) : '',
|
||||
'address' => isset( $doctor_data['address'] ) ? sanitize_textarea_field( $doctor_data['address'] ) : '',
|
||||
'city' => isset( $doctor_data['city'] ) ? sanitize_text_field( $doctor_data['city'] ) : '',
|
||||
'state' => isset( $doctor_data['state'] ) ? sanitize_text_field( $doctor_data['state'] ) : '',
|
||||
'country' => isset( $doctor_data['country'] ) ? sanitize_text_field( $doctor_data['country'] ) : '',
|
||||
'postal_code' => isset( $doctor_data['postal_code'] ) ? sanitize_text_field( $doctor_data['postal_code'] ) : '',
|
||||
'license_number' => isset( $doctor_data['license_number'] ) ? sanitize_text_field( $doctor_data['license_number'] ) : '',
|
||||
'consultation_fee' => isset( $doctor_data['consultation_fee'] ) ? (float) $doctor_data['consultation_fee'] : 0,
|
||||
'biography' => isset( $doctor_data['biography'] ) ? sanitize_textarea_field( $doctor_data['biography'] ) : '',
|
||||
'languages' => isset( $doctor_data['languages'] ) ? $doctor_data['languages'] : array(),
|
||||
'working_hours' => isset( $doctor_data['working_hours'] ) ? $doctor_data['working_hours'] : array(),
|
||||
'doctor_registration_date' => current_time( 'mysql' ),
|
||||
'doctor_status' => 'active'
|
||||
);
|
||||
|
||||
foreach ( $meta_fields as $meta_key => $meta_value ) {
|
||||
if ( is_array( $meta_value ) ) {
|
||||
$meta_value = wp_json_encode( $meta_value );
|
||||
}
|
||||
update_user_meta( $user_id, $meta_key, $meta_value );
|
||||
}
|
||||
|
||||
// Create clinic mapping if clinic_id provided
|
||||
if ( ! empty( $doctor_data['clinic_id'] ) ) {
|
||||
self::assign_to_clinic( $user_id, $doctor_data['clinic_id'] );
|
||||
}
|
||||
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update doctor data
|
||||
*
|
||||
* @param int $user_id Doctor user ID
|
||||
* @param array $doctor_data Updated doctor data
|
||||
* @return bool|WP_Error True on success, WP_Error on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function update( $user_id, $doctor_data ) {
|
||||
if ( ! self::exists( $user_id ) ) {
|
||||
return new \WP_Error(
|
||||
'doctor_not_found',
|
||||
'Doctor 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( $doctor_data[ $field ] ) ) {
|
||||
$user_update_data[ $field ] = sanitize_text_field( $doctor_data[ $field ] );
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $user_update_data ) > 1 ) {
|
||||
$result = wp_update_user( $user_update_data );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return new \WP_Error(
|
||||
'doctor_update_failed',
|
||||
'Failed to update doctor: ' . $result->get_error_message(),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update meta fields
|
||||
$meta_fields = array(
|
||||
'specialization', 'qualification', 'experience_years', 'mobile_number',
|
||||
'address', 'city', 'state', 'country', 'postal_code', 'license_number',
|
||||
'consultation_fee', 'biography', 'languages', 'working_hours', 'doctor_status'
|
||||
);
|
||||
|
||||
foreach ( $meta_fields as $meta_key ) {
|
||||
if ( isset( $doctor_data[ $meta_key ] ) ) {
|
||||
$value = $doctor_data[ $meta_key ];
|
||||
|
||||
if ( in_array( $meta_key, array( 'address', 'biography' ) ) ) {
|
||||
$value = sanitize_textarea_field( $value );
|
||||
} elseif ( $meta_key === 'experience_years' ) {
|
||||
$value = (int) $value;
|
||||
} elseif ( $meta_key === 'consultation_fee' ) {
|
||||
$value = (float) $value;
|
||||
} elseif ( in_array( $meta_key, array( 'languages', 'working_hours' ) ) ) {
|
||||
$value = is_array( $value ) ? wp_json_encode( $value ) : $value;
|
||||
} else {
|
||||
$value = sanitize_text_field( $value );
|
||||
}
|
||||
|
||||
update_user_meta( $user_id, $meta_key, $value );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a doctor (soft delete - deactivate user)
|
||||
*
|
||||
* @param int $user_id Doctor 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(
|
||||
'doctor_not_found',
|
||||
'Doctor not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check for dependencies
|
||||
if ( self::has_dependencies( $user_id ) ) {
|
||||
return new \WP_Error(
|
||||
'doctor_has_dependencies',
|
||||
'Cannot delete doctor with existing appointments or patient encounters',
|
||||
array( 'status' => 409 )
|
||||
);
|
||||
}
|
||||
|
||||
// Soft delete - set user status to inactive
|
||||
update_user_meta( $user_id, 'doctor_status', 'inactive' );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get doctor by ID
|
||||
*
|
||||
* @param int $user_id Doctor user ID
|
||||
* @return array|null Doctor 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_doctor_full_data( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all doctors with optional filtering
|
||||
*
|
||||
* @param array $args Query arguments
|
||||
* @return array Array of doctor data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_all( $args = array() ) {
|
||||
$defaults = array(
|
||||
'clinic_id' => null,
|
||||
'specialization' => 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_doctor',
|
||||
'number' => $args['limit'],
|
||||
'offset' => $args['offset'],
|
||||
'orderby' => $args['orderby'],
|
||||
'order' => $args['order'],
|
||||
'fields' => 'ID',
|
||||
'meta_query' => array()
|
||||
);
|
||||
|
||||
// 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
|
||||
if ( ! empty( $args['status'] ) ) {
|
||||
$user_query_args['meta_query'][] = array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => 'doctor_status',
|
||||
'value' => $args['status'],
|
||||
'compare' => '='
|
||||
),
|
||||
array(
|
||||
'key' => 'doctor_status',
|
||||
'compare' => 'NOT EXISTS'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Add specialization filter
|
||||
if ( ! empty( $args['specialization'] ) ) {
|
||||
$user_query_args['meta_query'][] = array(
|
||||
'key' => 'specialization',
|
||||
'value' => $args['specialization'],
|
||||
'compare' => 'LIKE'
|
||||
);
|
||||
}
|
||||
|
||||
$user_query = new \WP_User_Query( $user_query_args );
|
||||
$user_ids = $user_query->get_results();
|
||||
|
||||
$doctors = array();
|
||||
foreach ( $user_ids as $user_id ) {
|
||||
$doctor_data = self::get_doctor_full_data( $user_id );
|
||||
|
||||
// Filter by clinic if specified
|
||||
if ( ! is_null( $args['clinic_id'] ) &&
|
||||
! in_array( (int) $args['clinic_id'], $doctor_data['clinic_ids'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $doctor_data ) {
|
||||
$doctors[] = $doctor_data;
|
||||
}
|
||||
}
|
||||
|
||||
return $doctors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get doctor full data with clinic information
|
||||
*
|
||||
* @param int $user_id Doctor user ID
|
||||
* @return array|null Full doctor data or null if not found
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_doctor_full_data( $user_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
if ( ! $user || ! in_array( 'kivicare_doctor', $user->roles ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get clinic mappings
|
||||
$clinic_mappings = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT dcm.clinic_id, c.name as clinic_name
|
||||
FROM {$wpdb->prefix}kc_doctor_clinic_mappings dcm
|
||||
LEFT JOIN {$wpdb->prefix}kc_clinics c ON dcm.clinic_id = c.id
|
||||
WHERE dcm.doctor_id = %d",
|
||||
$user_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
$clinic_ids = array();
|
||||
$clinic_names = array();
|
||||
foreach ( $clinic_mappings as $mapping ) {
|
||||
$clinic_ids[] = (int) $mapping['clinic_id'];
|
||||
$clinic_names[] = $mapping['clinic_name'];
|
||||
}
|
||||
|
||||
// Get doctor meta data
|
||||
$languages = get_user_meta( $user_id, 'languages', true );
|
||||
$working_hours = get_user_meta( $user_id, 'working_hours', true );
|
||||
|
||||
if ( is_string( $languages ) ) {
|
||||
$languages = json_decode( $languages, true ) ?: array();
|
||||
}
|
||||
|
||||
if ( is_string( $working_hours ) ) {
|
||||
$working_hours = json_decode( $working_hours, true ) ?: array();
|
||||
}
|
||||
|
||||
$doctor_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,
|
||||
'specialization' => get_user_meta( $user_id, 'specialization', true ),
|
||||
'qualification' => get_user_meta( $user_id, 'qualification', true ),
|
||||
'experience_years' => (int) get_user_meta( $user_id, 'experience_years', 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 ),
|
||||
'license_number' => get_user_meta( $user_id, 'license_number', true ),
|
||||
'consultation_fee' => (float) get_user_meta( $user_id, 'consultation_fee', true ),
|
||||
'biography' => get_user_meta( $user_id, 'biography', true ),
|
||||
'languages' => $languages,
|
||||
'working_hours' => $working_hours,
|
||||
'status' => get_user_meta( $user_id, 'doctor_status', true ) ?: 'active',
|
||||
'registration_date' => get_user_meta( $user_id, 'doctor_registration_date', true ),
|
||||
'clinic_ids' => $clinic_ids,
|
||||
'clinic_names' => $clinic_names,
|
||||
'total_appointments' => self::get_appointment_count( $user_id ),
|
||||
'total_patients' => self::get_patient_count( $user_id ),
|
||||
'rating' => self::get_doctor_rating( $user_id )
|
||||
);
|
||||
|
||||
return $doctor_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get doctor schedule
|
||||
*
|
||||
* @param int $user_id Doctor user ID
|
||||
* @param array $args Query arguments
|
||||
* @return array Doctor schedule
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_schedule( $user_id, $args = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$defaults = array(
|
||||
'date_from' => date( 'Y-m-d' ),
|
||||
'date_to' => date( 'Y-m-d', strtotime( '+30 days' ) ),
|
||||
'clinic_id' => null
|
||||
);
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
// Get appointments in date range
|
||||
$where_clauses = array(
|
||||
'doctor_id = %d',
|
||||
'appointment_start_date BETWEEN %s AND %s'
|
||||
);
|
||||
$where_values = array( $user_id, $args['date_from'], $args['date_to'] );
|
||||
|
||||
if ( ! is_null( $args['clinic_id'] ) ) {
|
||||
$where_clauses[] = 'clinic_id = %d';
|
||||
$where_values[] = $args['clinic_id'];
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
$appointments = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT a.*,
|
||||
CONCAT(p.first_name, ' ', p.last_name) as patient_name,
|
||||
c.name as clinic_name
|
||||
FROM {$wpdb->prefix}kc_appointments a
|
||||
LEFT JOIN {$wpdb->prefix}users p ON a.patient_id = p.ID
|
||||
LEFT JOIN {$wpdb->prefix}kc_clinics c ON a.clinic_id = c.id
|
||||
WHERE {$where_sql}
|
||||
ORDER BY appointment_start_date, appointment_start_time",
|
||||
$where_values
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
// Get working hours
|
||||
$working_hours = get_user_meta( $user_id, 'working_hours', true );
|
||||
if ( is_string( $working_hours ) ) {
|
||||
$working_hours = json_decode( $working_hours, true ) ?: array();
|
||||
}
|
||||
|
||||
$schedule = array(
|
||||
'working_hours' => $working_hours,
|
||||
'appointments' => array()
|
||||
);
|
||||
|
||||
foreach ( $appointments as $appointment ) {
|
||||
$schedule['appointments'][] = array(
|
||||
'id' => (int) $appointment['id'],
|
||||
'date' => $appointment['appointment_start_date'],
|
||||
'start_time' => $appointment['appointment_start_time'],
|
||||
'end_time' => $appointment['appointment_end_time'],
|
||||
'patient' => array(
|
||||
'id' => (int) $appointment['patient_id'],
|
||||
'name' => $appointment['patient_name']
|
||||
),
|
||||
'clinic' => array(
|
||||
'id' => (int) $appointment['clinic_id'],
|
||||
'name' => $appointment['clinic_name']
|
||||
),
|
||||
'visit_type' => $appointment['visit_type'],
|
||||
'status' => (int) $appointment['status'],
|
||||
'description' => $appointment['description']
|
||||
);
|
||||
}
|
||||
|
||||
return $schedule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update doctor schedule/working hours
|
||||
*
|
||||
* @param int $user_id Doctor user ID
|
||||
* @param array $working_hours Working hours data
|
||||
* @return bool|WP_Error True on success, WP_Error on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function update_schedule( $user_id, $working_hours ) {
|
||||
if ( ! self::exists( $user_id ) ) {
|
||||
return new \WP_Error(
|
||||
'doctor_not_found',
|
||||
'Doctor not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Validate working hours format
|
||||
$validation = self::validate_working_hours( $working_hours );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
update_user_meta( $user_id, 'working_hours', wp_json_encode( $working_hours ) );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get doctor appointments
|
||||
*
|
||||
* @param int $user_id Doctor user ID
|
||||
* @param array $args Query arguments
|
||||
* @return array Doctor appointments
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_appointments( $user_id, $args = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$defaults = array(
|
||||
'status' => null,
|
||||
'date_from' => null,
|
||||
'date_to' => null,
|
||||
'clinic_id' => null,
|
||||
'limit' => 50,
|
||||
'offset' => 0,
|
||||
'orderby' => 'appointment_start_date',
|
||||
'order' => 'ASC'
|
||||
);
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
$where_clauses = array( 'doctor_id = %d' );
|
||||
$where_values = array( $user_id );
|
||||
|
||||
if ( ! is_null( $args['status'] ) ) {
|
||||
$where_clauses[] = 'status = %d';
|
||||
$where_values[] = $args['status'];
|
||||
}
|
||||
|
||||
if ( ! is_null( $args['date_from'] ) ) {
|
||||
$where_clauses[] = 'appointment_start_date >= %s';
|
||||
$where_values[] = $args['date_from'];
|
||||
}
|
||||
|
||||
if ( ! is_null( $args['date_to'] ) ) {
|
||||
$where_clauses[] = 'appointment_start_date <= %s';
|
||||
$where_values[] = $args['date_to'];
|
||||
}
|
||||
|
||||
if ( ! is_null( $args['clinic_id'] ) ) {
|
||||
$where_clauses[] = 'clinic_id = %d';
|
||||
$where_values[] = $args['clinic_id'];
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT a.*,
|
||||
CONCAT(p.first_name, ' ', p.last_name) as patient_name,
|
||||
c.name as clinic_name
|
||||
FROM {$wpdb->prefix}kc_appointments a
|
||||
LEFT JOIN {$wpdb->prefix}users p ON a.patient_id = p.ID
|
||||
LEFT JOIN {$wpdb->prefix}kc_clinics c ON a.clinic_id = c.id
|
||||
WHERE {$where_sql}
|
||||
ORDER BY {$args['orderby']} {$args['order']}
|
||||
LIMIT %d OFFSET %d",
|
||||
array_merge( $where_values, array( $args['limit'], $args['offset'] ) )
|
||||
);
|
||||
|
||||
$appointments = $wpdb->get_results( $query, ARRAY_A );
|
||||
|
||||
return array_map( function( $appointment ) {
|
||||
return array(
|
||||
'id' => (int) $appointment['id'],
|
||||
'start_date' => $appointment['appointment_start_date'],
|
||||
'start_time' => $appointment['appointment_start_time'],
|
||||
'end_date' => $appointment['appointment_end_date'],
|
||||
'end_time' => $appointment['appointment_end_time'],
|
||||
'visit_type' => $appointment['visit_type'],
|
||||
'status' => (int) $appointment['status'],
|
||||
'patient' => array(
|
||||
'id' => (int) $appointment['patient_id'],
|
||||
'name' => $appointment['patient_name']
|
||||
),
|
||||
'clinic' => array(
|
||||
'id' => (int) $appointment['clinic_id'],
|
||||
'name' => $appointment['clinic_name']
|
||||
),
|
||||
'description' => $appointment['description'],
|
||||
'appointment_report' => $appointment['appointment_report'],
|
||||
'created_at' => $appointment['created_at']
|
||||
);
|
||||
}, $appointments );
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign doctor to clinic
|
||||
*
|
||||
* @param int $user_id Doctor 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_doctor_clinic_mappings
|
||||
WHERE doctor_id = %d AND clinic_id = %d",
|
||||
$user_id, $clinic_id
|
||||
)
|
||||
);
|
||||
|
||||
if ( (int) $existing_mapping > 0 ) {
|
||||
return true; // Already assigned
|
||||
}
|
||||
|
||||
// Create new mapping
|
||||
$result = $wpdb->insert(
|
||||
"{$wpdb->prefix}kc_doctor_clinic_mappings",
|
||||
array(
|
||||
'doctor_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 doctor to clinic: ' . $wpdb->last_error,
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if doctor exists
|
||||
*
|
||||
* @param int $user_id Doctor 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_doctor', $user->roles );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if doctor has dependencies (appointments, encounters, etc.)
|
||||
*
|
||||
* @param int $user_id Doctor 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 doctor_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 doctor_id = %d",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $encounters_count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate doctor data
|
||||
*
|
||||
* @param array $doctor_data Doctor data to validate
|
||||
* @return bool|WP_Error True if valid, WP_Error if invalid
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function validate_doctor_data( $doctor_data ) {
|
||||
$errors = array();
|
||||
|
||||
// Check required fields
|
||||
foreach ( self::$required_fields as $field ) {
|
||||
if ( empty( $doctor_data[ $field ] ) ) {
|
||||
$errors[] = "Field '{$field}' is required";
|
||||
}
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
if ( ! empty( $doctor_data['user_email'] ) && ! is_email( $doctor_data['user_email'] ) ) {
|
||||
$errors[] = 'Invalid email format';
|
||||
}
|
||||
|
||||
// Check for duplicate email
|
||||
if ( ! empty( $doctor_data['user_email'] ) ) {
|
||||
$existing_user = get_user_by( 'email', $doctor_data['user_email'] );
|
||||
if ( $existing_user ) {
|
||||
$errors[] = 'A user with this email already exists';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate consultation fee
|
||||
if ( isset( $doctor_data['consultation_fee'] ) &&
|
||||
! is_numeric( $doctor_data['consultation_fee'] ) ) {
|
||||
$errors[] = 'Invalid consultation fee. Must be a number';
|
||||
}
|
||||
|
||||
// Validate experience years
|
||||
if ( isset( $doctor_data['experience_years'] ) &&
|
||||
( ! is_numeric( $doctor_data['experience_years'] ) ||
|
||||
(int) $doctor_data['experience_years'] < 0 ) ) {
|
||||
$errors[] = 'Invalid experience years. Must be a positive number';
|
||||
}
|
||||
|
||||
if ( ! empty( $errors ) ) {
|
||||
return new \WP_Error(
|
||||
'doctor_validation_failed',
|
||||
'Doctor validation failed',
|
||||
array(
|
||||
'status' => 400,
|
||||
'errors' => $errors
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate working hours format
|
||||
*
|
||||
* @param array $working_hours Working hours data
|
||||
* @return bool|WP_Error True if valid, WP_Error if invalid
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function validate_working_hours( $working_hours ) {
|
||||
if ( ! is_array( $working_hours ) ) {
|
||||
return new \WP_Error(
|
||||
'invalid_working_hours',
|
||||
'Working hours must be an array',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
$valid_days = array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' );
|
||||
|
||||
foreach ( $working_hours as $day => $hours ) {
|
||||
if ( ! in_array( strtolower( $day ), $valid_days ) ) {
|
||||
return new \WP_Error(
|
||||
'invalid_day',
|
||||
"Invalid day: {$day}",
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $hours['start_time'] ) && isset( $hours['end_time'] ) ) {
|
||||
// Validate time format (HH:MM)
|
||||
if ( ! preg_match( '/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $hours['start_time'] ) ||
|
||||
! preg_match( '/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $hours['end_time'] ) ) {
|
||||
return new \WP_Error(
|
||||
'invalid_time_format',
|
||||
'Time must be in HH:MM format',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique username for doctor
|
||||
*
|
||||
* @param array $doctor_data Doctor data
|
||||
* @return string Unique username
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function generate_unique_username( $doctor_data ) {
|
||||
$base_username = 'dr.' . strtolower(
|
||||
$doctor_data['first_name'] . '.' . $doctor_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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get doctor appointment count
|
||||
*
|
||||
* @param int $user_id Doctor 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 doctor_id = %d",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get doctor patient count
|
||||
*
|
||||
* @param int $user_id Doctor user ID
|
||||
* @return int Unique patients treated
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_patient_count( $user_id ) {
|
||||
global $wpdb;
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT patient_id) FROM {$wpdb->prefix}kc_appointments WHERE doctor_id = %d",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get doctor rating (placeholder for future implementation)
|
||||
*
|
||||
* @param int $user_id Doctor user ID
|
||||
* @return float Doctor rating
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_doctor_rating( $user_id ) {
|
||||
// Placeholder for future rating system
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get doctor statistics
|
||||
*
|
||||
* @param int $user_id Doctor user ID
|
||||
* @return array Doctor statistics
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_statistics( $user_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$stats = array(
|
||||
'total_appointments' => self::get_appointment_count( $user_id ),
|
||||
'total_patients' => self::get_patient_count( $user_id ),
|
||||
'appointments_today' => 0,
|
||||
'appointments_this_week' => 0,
|
||||
'appointments_this_month' => 0,
|
||||
'completed_encounters' => 0,
|
||||
'revenue_this_month' => 0
|
||||
);
|
||||
|
||||
// Appointments today
|
||||
$stats['appointments_today'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments
|
||||
WHERE doctor_id = %d AND appointment_start_date = CURDATE()",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// Appointments this week
|
||||
$stats['appointments_this_week'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments
|
||||
WHERE doctor_id = %d AND WEEK(appointment_start_date) = WEEK(CURDATE())
|
||||
AND YEAR(appointment_start_date) = YEAR(CURDATE())",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// Appointments this month
|
||||
$stats['appointments_this_month'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments
|
||||
WHERE doctor_id = %d AND MONTH(appointment_start_date) = MONTH(CURDATE())
|
||||
AND YEAR(appointment_start_date) = YEAR(CURDATE())",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// Completed encounters
|
||||
$stats['completed_encounters'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_patient_encounters
|
||||
WHERE doctor_id = %d AND status = 1",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// Revenue this month (if bills have doctor association)
|
||||
$consultation_fee = (float) get_user_meta( $user_id, 'consultation_fee', true );
|
||||
if ( $consultation_fee > 0 ) {
|
||||
$stats['revenue_this_month'] = $stats['appointments_this_month'] * $consultation_fee;
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
894
src/includes/models/class-encounter.php
Normal file
894
src/includes/models/class-encounter.php
Normal file
@@ -0,0 +1,894 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Encounter Model
|
||||
*
|
||||
* Handles medical encounter operations and patient consultation data
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Models
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_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;
|
||||
}
|
||||
}
|
||||
825
src/includes/models/class-patient.php
Normal file
825
src/includes/models/class-patient.php
Normal file
@@ -0,0 +1,825 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Patient Model
|
||||
*
|
||||
* Handles patient entity operations and medical data management
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Models
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_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 );
|
||||
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kc_medical_history
|
||||
WHERE {$where_sql}
|
||||
ORDER BY {$args['orderby']} {$args['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( 'patient_id = %d' );
|
||||
$where_values = array( $user_id );
|
||||
|
||||
if ( ! is_null( $args['status'] ) ) {
|
||||
$where_clauses[] = 'status = %d';
|
||||
$where_values[] = $args['status'];
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
$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 {$args['orderby']} {$args['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;
|
||||
}
|
||||
}
|
||||
804
src/includes/models/class-prescription.php
Normal file
804
src/includes/models/class-prescription.php
Normal file
@@ -0,0 +1,804 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Prescription Model
|
||||
*
|
||||
* Handles prescription operations and medication management
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Models
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_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 );
|
||||
|
||||
// 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}";
|
||||
|
||||
$query .= sprintf( ' ORDER BY p.%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 );
|
||||
}
|
||||
|
||||
$prescriptions = $wpdb->get_results( $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();
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
// Doctor filter (through encounter)
|
||||
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[] = '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";
|
||||
|
||||
if ( ! is_null( $args['doctor_id'] ) ) {
|
||||
$query .= " LEFT JOIN {$wpdb->prefix}kc_patient_encounters e ON p.encounter_id = e.id";
|
||||
}
|
||||
|
||||
$query .= " 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(
|
||||
empty( $where_values ) ?
|
||||
$wpdb->prepare( $query, $args['limit'] ) :
|
||||
$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}";
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$stats['total_prescriptions'] = (int) $wpdb->get_var( $query );
|
||||
|
||||
// Unique medications
|
||||
$query = "SELECT COUNT(DISTINCT name) FROM {$table} WHERE {$where_sql}";
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$stats['unique_medications'] = (int) $wpdb->get_var( $query );
|
||||
|
||||
// Prescriptions today
|
||||
$today_where = array_merge( $where_clauses, array( 'DATE(created_at) = CURDATE()' ) );
|
||||
$query = "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $today_where );
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$stats['prescriptions_today'] = (int) $wpdb->get_var( $query );
|
||||
|
||||
// Prescriptions this week
|
||||
$week_where = array_merge( $where_clauses, array(
|
||||
'WEEK(created_at) = WEEK(CURDATE())',
|
||||
'YEAR(created_at) = YEAR(CURDATE())'
|
||||
) );
|
||||
$query = "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $week_where );
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$stats['prescriptions_this_week'] = (int) $wpdb->get_var( $query );
|
||||
|
||||
// Prescriptions this month
|
||||
$month_where = array_merge( $where_clauses, array(
|
||||
'MONTH(created_at) = MONTH(CURDATE())',
|
||||
'YEAR(created_at) = YEAR(CURDATE())'
|
||||
) );
|
||||
$query = "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $month_where );
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$stats['prescriptions_this_month'] = (int) $wpdb->get_var( $query );
|
||||
|
||||
// Template prescriptions
|
||||
$template_where = array_merge( $where_clauses, array( 'is_from_template = 1' ) );
|
||||
$query = "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $template_where );
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$stats['template_prescriptions'] = (int) $wpdb->get_var( $query );
|
||||
|
||||
// Average prescriptions per encounter
|
||||
if ( $stats['total_prescriptions'] > 0 ) {
|
||||
$unique_encounters = $wpdb->get_var(
|
||||
"SELECT COUNT(DISTINCT encounter_id) FROM {$table} WHERE {$where_sql}"
|
||||
);
|
||||
if ( $unique_encounters > 0 ) {
|
||||
$stats['avg_prescriptions_per_encounter'] = round( $stats['total_prescriptions'] / $unique_encounters, 2 );
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
814
src/includes/models/class-service.php
Normal file
814
src/includes/models/class-service.php
Normal file
@@ -0,0 +1,814 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Service Model
|
||||
*
|
||||
* Handles medical service operations and pricing management
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Models
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_API\Models;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Service
|
||||
*
|
||||
* Model for handling medical services, procedures and pricing
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Service {
|
||||
|
||||
/**
|
||||
* Database table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $table_name = 'kc_services';
|
||||
|
||||
/**
|
||||
* Service ID
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Service data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $data = array();
|
||||
|
||||
/**
|
||||
* Required fields for service creation
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $required_fields = array(
|
||||
'name',
|
||||
'type',
|
||||
'price'
|
||||
);
|
||||
|
||||
/**
|
||||
* Valid service types
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $valid_types = array(
|
||||
'consultation' => 'Consultation',
|
||||
'procedure' => 'Medical Procedure',
|
||||
'diagnostic' => 'Diagnostic Test',
|
||||
'therapy' => 'Therapy Session',
|
||||
'surgery' => 'Surgery',
|
||||
'vaccination' => 'Vaccination',
|
||||
'checkup' => 'Health Checkup',
|
||||
'other' => 'Other'
|
||||
);
|
||||
|
||||
/**
|
||||
* Valid service statuses
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $valid_statuses = array(
|
||||
1 => 'active',
|
||||
0 => 'inactive'
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param int|array $service_id_or_data Service ID or data array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct( $service_id_or_data = null ) {
|
||||
if ( is_numeric( $service_id_or_data ) ) {
|
||||
$this->id = (int) $service_id_or_data;
|
||||
$this->load_data();
|
||||
} elseif ( is_array( $service_id_or_data ) ) {
|
||||
$this->data = $service_id_or_data;
|
||||
$this->id = isset( $this->data['id'] ) ? (int) $this->data['id'] : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load service data from database
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function load_data() {
|
||||
if ( ! $this->id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$service_data = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE id = %d",
|
||||
$this->id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( $service_data ) {
|
||||
$this->data = self::format_service_data( $service_data );
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new service
|
||||
*
|
||||
* @param array $service_data Service data
|
||||
* @return int|WP_Error Service ID on success, WP_Error on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function create( $service_data ) {
|
||||
// Validate required fields
|
||||
$validation = self::validate_service_data( $service_data );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
// Prepare data for insertion
|
||||
$insert_data = array(
|
||||
'name' => sanitize_text_field( $service_data['name'] ),
|
||||
'type' => sanitize_text_field( $service_data['type'] ),
|
||||
'price' => number_format( (float) $service_data['price'], 2, '.', '' ),
|
||||
'description' => isset( $service_data['description'] ) ? sanitize_textarea_field( $service_data['description'] ) : '',
|
||||
'duration' => isset( $service_data['duration'] ) ? (int) $service_data['duration'] : null,
|
||||
'status' => isset( $service_data['status'] ) ? (int) $service_data['status'] : 1,
|
||||
'created_at' => current_time( 'mysql' )
|
||||
);
|
||||
|
||||
$result = $wpdb->insert( $table, $insert_data );
|
||||
|
||||
if ( $result === false ) {
|
||||
return new \WP_Error(
|
||||
'service_creation_failed',
|
||||
'Failed to create service: ' . $wpdb->last_error,
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update service data
|
||||
*
|
||||
* @param int $service_id Service ID
|
||||
* @param array $service_data Updated service data
|
||||
* @return bool|WP_Error True on success, WP_Error on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function update( $service_id, $service_data ) {
|
||||
if ( ! self::exists( $service_id ) ) {
|
||||
return new \WP_Error(
|
||||
'service_not_found',
|
||||
'Service not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
// Prepare update data
|
||||
$update_data = array();
|
||||
$allowed_fields = array(
|
||||
'name', 'type', 'price', 'description', 'duration', 'status'
|
||||
);
|
||||
|
||||
foreach ( $allowed_fields as $field ) {
|
||||
if ( isset( $service_data[ $field ] ) ) {
|
||||
$value = $service_data[ $field ];
|
||||
|
||||
switch ( $field ) {
|
||||
case 'price':
|
||||
$update_data[ $field ] = number_format( (float) $value, 2, '.', '' );
|
||||
break;
|
||||
case 'duration':
|
||||
case 'status':
|
||||
$update_data[ $field ] = (int) $value;
|
||||
break;
|
||||
case 'description':
|
||||
$update_data[ $field ] = sanitize_textarea_field( $value );
|
||||
break;
|
||||
case 'type':
|
||||
if ( array_key_exists( $value, self::$valid_types ) ) {
|
||||
$update_data[ $field ] = sanitize_text_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' => $service_id ),
|
||||
null,
|
||||
array( '%d' )
|
||||
);
|
||||
|
||||
if ( $result === false ) {
|
||||
return new \WP_Error(
|
||||
'service_update_failed',
|
||||
'Failed to update service: ' . $wpdb->last_error,
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a service
|
||||
*
|
||||
* @param int $service_id Service ID
|
||||
* @return bool|WP_Error True on success, WP_Error on failure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function delete( $service_id ) {
|
||||
if ( ! self::exists( $service_id ) ) {
|
||||
return new \WP_Error(
|
||||
'service_not_found',
|
||||
'Service not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check if service is being used in appointments
|
||||
if ( self::has_dependencies( $service_id ) ) {
|
||||
return new \WP_Error(
|
||||
'service_has_dependencies',
|
||||
'Cannot delete service that is associated with appointments',
|
||||
array( 'status' => 409 )
|
||||
);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$result = $wpdb->delete(
|
||||
$table,
|
||||
array( 'id' => $service_id ),
|
||||
array( '%d' )
|
||||
);
|
||||
|
||||
if ( $result === false ) {
|
||||
return new \WP_Error(
|
||||
'service_deletion_failed',
|
||||
'Failed to delete service: ' . $wpdb->last_error,
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service by ID
|
||||
*
|
||||
* @param int $service_id Service ID
|
||||
* @return array|null Service data or null if not found
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_by_id( $service_id ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$service = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE id = %d",
|
||||
$service_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( $service ) {
|
||||
return self::format_service_data( $service );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all services with optional filtering
|
||||
*
|
||||
* @param array $args Query arguments
|
||||
* @return array Array of service data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_all( $args = array() ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$defaults = array(
|
||||
'type' => null,
|
||||
'status' => 1, // Only active by default
|
||||
'search' => '',
|
||||
'limit' => 50,
|
||||
'offset' => 0,
|
||||
'orderby' => 'name',
|
||||
'order' => 'ASC'
|
||||
);
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
$where_clauses = array( '1=1' );
|
||||
$where_values = array();
|
||||
|
||||
// Type filter
|
||||
if ( ! is_null( $args['type'] ) ) {
|
||||
$where_clauses[] = 'type = %s';
|
||||
$where_values[] = $args['type'];
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if ( ! is_null( $args['status'] ) ) {
|
||||
$where_clauses[] = 'status = %d';
|
||||
$where_values[] = $args['status'];
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if ( ! empty( $args['search'] ) ) {
|
||||
$where_clauses[] = '(name LIKE %s OR description LIKE %s)';
|
||||
$search_term = '%' . $wpdb->esc_like( $args['search'] ) . '%';
|
||||
$where_values[] = $search_term;
|
||||
$where_values[] = $search_term;
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
// Build query
|
||||
$query = "SELECT * FROM {$table} WHERE {$where_sql}";
|
||||
$query .= sprintf( ' ORDER BY %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 );
|
||||
}
|
||||
|
||||
$services = $wpdb->get_results( $query, ARRAY_A );
|
||||
|
||||
return array_map( array( self::class, 'format_service_data' ), $services );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get services by type
|
||||
*
|
||||
* @param string $type Service type
|
||||
* @param array $args Additional query arguments
|
||||
* @return array Array of services
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_by_type( $type, $args = array() ) {
|
||||
$args['type'] = $type;
|
||||
return self::get_all( $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get most popular services
|
||||
*
|
||||
* @param array $args Query arguments
|
||||
* @return array Array of popular services with usage stats
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_popular_services( $args = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$defaults = array(
|
||||
'limit' => 10,
|
||||
'date_from' => null,
|
||||
'date_to' => null,
|
||||
'clinic_id' => null
|
||||
);
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
$where_clauses = array( '1=1' );
|
||||
$where_values = array();
|
||||
|
||||
// Date range filters
|
||||
if ( ! is_null( $args['date_from'] ) ) {
|
||||
$where_clauses[] = 'a.created_at >= %s';
|
||||
$where_values[] = $args['date_from'] . ' 00:00:00';
|
||||
}
|
||||
|
||||
if ( ! is_null( $args['date_to'] ) ) {
|
||||
$where_clauses[] = 'a.created_at <= %s';
|
||||
$where_values[] = $args['date_to'] . ' 23:59:59';
|
||||
}
|
||||
|
||||
// Clinic filter
|
||||
if ( ! is_null( $args['clinic_id'] ) ) {
|
||||
$where_clauses[] = 'a.clinic_id = %d';
|
||||
$where_values[] = $args['clinic_id'];
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
$query = "SELECT s.*,
|
||||
COUNT(asm.service_id) as usage_count,
|
||||
MAX(a.created_at) as last_used
|
||||
FROM {$wpdb->prefix}kc_services s
|
||||
LEFT JOIN {$wpdb->prefix}kc_appointment_service_mapping asm ON s.id = asm.service_id
|
||||
LEFT JOIN {$wpdb->prefix}kc_appointments a ON asm.appointment_id = a.id
|
||||
WHERE s.status = 1 AND {$where_sql}
|
||||
GROUP BY s.id
|
||||
ORDER BY usage_count DESC, s.name ASC
|
||||
LIMIT %d";
|
||||
|
||||
$where_values[] = $args['limit'];
|
||||
|
||||
$services = $wpdb->get_results(
|
||||
$wpdb->prepare( $query, $where_values ),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map( function( $service ) {
|
||||
$formatted = self::format_service_data( $service );
|
||||
$formatted['usage_count'] = (int) $service['usage_count'];
|
||||
$formatted['last_used'] = $service['last_used'];
|
||||
return $formatted;
|
||||
}, $services );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service pricing tiers (if implemented)
|
||||
*
|
||||
* @param int $service_id Service ID
|
||||
* @return array Service pricing information
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_pricing( $service_id ) {
|
||||
$service = self::get_by_id( $service_id );
|
||||
|
||||
if ( ! $service ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Basic pricing - could be extended for complex pricing models
|
||||
return array(
|
||||
'service_id' => $service['id'],
|
||||
'base_price' => $service['price'],
|
||||
'currency' => 'EUR', // Could be configurable
|
||||
'pricing_type' => 'fixed', // fixed, variable, tiered, etc.
|
||||
'includes_consultation' => $service['type'] === 'consultation'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search services (for autocomplete)
|
||||
*
|
||||
* @param string $search_term Search term
|
||||
* @param int $limit Limit results
|
||||
* @return array Array of service names
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function search_services( $search_term, $limit = 20 ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
if ( empty( $search_term ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$services = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT id, name, type, price
|
||||
FROM {$table}
|
||||
WHERE status = 1 AND (name LIKE %s OR description LIKE %s)
|
||||
ORDER BY name ASC
|
||||
LIMIT %d",
|
||||
'%' . $wpdb->esc_like( $search_term ) . '%',
|
||||
'%' . $wpdb->esc_like( $search_term ) . '%',
|
||||
$limit
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map( function( $service ) {
|
||||
return array(
|
||||
'id' => (int) $service['id'],
|
||||
'name' => $service['name'],
|
||||
'type' => $service['type'],
|
||||
'type_text' => self::$valid_types[ $service['type'] ] ?? 'Unknown',
|
||||
'price' => (float) $service['price']
|
||||
);
|
||||
}, $services );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if service exists
|
||||
*
|
||||
* @param int $service_id Service ID
|
||||
* @return bool True if exists, false otherwise
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function exists( $service_id ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE id = %d",
|
||||
$service_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if service has dependencies (appointments)
|
||||
*
|
||||
* @param int $service_id Service ID
|
||||
* @return bool True if has dependencies, false otherwise
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function has_dependencies( $service_id ) {
|
||||
global $wpdb;
|
||||
|
||||
// Check appointment service mappings
|
||||
$mappings_count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointment_service_mapping WHERE service_id = %d",
|
||||
$service_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $mappings_count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate service data
|
||||
*
|
||||
* @param array $service_data Service data to validate
|
||||
* @return bool|WP_Error True if valid, WP_Error if invalid
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function validate_service_data( $service_data ) {
|
||||
$errors = array();
|
||||
|
||||
// Check required fields
|
||||
foreach ( self::$required_fields as $field ) {
|
||||
if ( empty( $service_data[ $field ] ) ) {
|
||||
$errors[] = "Field '{$field}' is required";
|
||||
}
|
||||
}
|
||||
|
||||
// Validate price
|
||||
if ( isset( $service_data['price'] ) ) {
|
||||
if ( ! is_numeric( $service_data['price'] ) || (float) $service_data['price'] < 0 ) {
|
||||
$errors[] = 'Price must be a non-negative number';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate type
|
||||
if ( isset( $service_data['type'] ) &&
|
||||
! array_key_exists( $service_data['type'], self::$valid_types ) ) {
|
||||
$errors[] = 'Invalid service type';
|
||||
}
|
||||
|
||||
// Validate duration
|
||||
if ( isset( $service_data['duration'] ) ) {
|
||||
if ( ! is_numeric( $service_data['duration'] ) || (int) $service_data['duration'] <= 0 ) {
|
||||
$errors[] = 'Duration must be a positive number (in minutes)';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate status
|
||||
if ( isset( $service_data['status'] ) &&
|
||||
! array_key_exists( $service_data['status'], self::$valid_statuses ) ) {
|
||||
$errors[] = 'Invalid status value';
|
||||
}
|
||||
|
||||
// Check for duplicate service name
|
||||
if ( ! empty( $service_data['name'] ) ) {
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . self::$table_name;
|
||||
|
||||
$existing_service = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE name = %s",
|
||||
$service_data['name']
|
||||
)
|
||||
);
|
||||
|
||||
if ( $existing_service ) {
|
||||
$errors[] = 'A service with this name already exists';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $errors ) ) {
|
||||
return new \WP_Error(
|
||||
'service_validation_failed',
|
||||
'Service validation failed',
|
||||
array(
|
||||
'status' => 400,
|
||||
'errors' => $errors
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format service data for API response
|
||||
*
|
||||
* @param array $service_data Raw service data
|
||||
* @return array Formatted service data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function format_service_data( $service_data ) {
|
||||
if ( ! $service_data ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$formatted = array(
|
||||
'id' => (int) $service_data['id'],
|
||||
'name' => $service_data['name'],
|
||||
'type' => $service_data['type'],
|
||||
'type_text' => self::$valid_types[ $service_data['type'] ] ?? 'Unknown',
|
||||
'price' => (float) $service_data['price'],
|
||||
'description' => $service_data['description'] ?? '',
|
||||
'duration' => isset( $service_data['duration'] ) ? (int) $service_data['duration'] : null,
|
||||
'status' => (int) $service_data['status'],
|
||||
'status_text' => self::$valid_statuses[ $service_data['status'] ] ?? 'unknown',
|
||||
'created_at' => $service_data['created_at']
|
||||
);
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service statistics
|
||||
*
|
||||
* @param array $filters Optional filters
|
||||
* @return array Service 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['type'] ) ) {
|
||||
$where_clauses[] = 'type = %s';
|
||||
$where_values[] = $filters['type'];
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
$stats = array(
|
||||
'total_services' => 0,
|
||||
'active_services' => 0,
|
||||
'inactive_services' => 0,
|
||||
'services_by_type' => array(),
|
||||
'average_price' => 0,
|
||||
'price_range' => array(
|
||||
'min' => 0,
|
||||
'max' => 0
|
||||
),
|
||||
'most_expensive_service' => null,
|
||||
'least_expensive_service' => null
|
||||
);
|
||||
|
||||
// Total services
|
||||
$query = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$stats['total_services'] = (int) $wpdb->get_var( $query );
|
||||
|
||||
// Services by status
|
||||
foreach ( array_keys( self::$valid_statuses ) as $status_id ) {
|
||||
$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
|
||||
);
|
||||
|
||||
$count = (int) $wpdb->get_var( $query );
|
||||
$stats[ self::$valid_statuses[ $status_id ] . '_services' ] = $count;
|
||||
}
|
||||
|
||||
// Services by type
|
||||
foreach ( array_keys( self::$valid_types ) as $type ) {
|
||||
$type_where = $where_clauses;
|
||||
$type_where[] = 'type = %s';
|
||||
$type_where[] = 'status = 1'; // Only active services
|
||||
$type_values = array_merge( $where_values, array( $type ) );
|
||||
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $type_where ),
|
||||
$type_values
|
||||
);
|
||||
|
||||
$count = (int) $wpdb->get_var( $query );
|
||||
if ( $count > 0 ) {
|
||||
$stats['services_by_type'][ $type ] = array(
|
||||
'count' => $count,
|
||||
'name' => self::$valid_types[ $type ]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Price statistics (active services only)
|
||||
$price_where = $where_clauses;
|
||||
$price_where[] = 'status = 1';
|
||||
$price_where[] = 'price > 0';
|
||||
|
||||
// Average price
|
||||
$query = "SELECT AVG(CAST(price AS DECIMAL(10,2))) FROM {$table} WHERE " . implode( ' AND ', $price_where );
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$stats['average_price'] = round( (float) $wpdb->get_var( $query ) ?: 0, 2 );
|
||||
|
||||
// Price range
|
||||
$query = "SELECT MIN(CAST(price AS DECIMAL(10,2))), MAX(CAST(price AS DECIMAL(10,2))) FROM {$table} WHERE " . implode( ' AND ', $price_where );
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$price_range = $wpdb->get_row( $query, ARRAY_N );
|
||||
if ( $price_range ) {
|
||||
$stats['price_range']['min'] = (float) $price_range[0] ?: 0;
|
||||
$stats['price_range']['max'] = (float) $price_range[1] ?: 0;
|
||||
}
|
||||
|
||||
// Most and least expensive services
|
||||
if ( $stats['total_services'] > 0 ) {
|
||||
$query = "SELECT name, price FROM {$table} WHERE " . implode( ' AND ', $price_where ) . " ORDER BY CAST(price AS DECIMAL(10,2)) DESC LIMIT 1";
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$most_expensive = $wpdb->get_row( $query, ARRAY_A );
|
||||
if ( $most_expensive ) {
|
||||
$stats['most_expensive_service'] = array(
|
||||
'name' => $most_expensive['name'],
|
||||
'price' => (float) $most_expensive['price']
|
||||
);
|
||||
}
|
||||
|
||||
$query = "SELECT name, price FROM {$table} WHERE " . implode( ' AND ', $price_where ) . " ORDER BY CAST(price AS DECIMAL(10,2)) ASC LIMIT 1";
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$query = $wpdb->prepare( $query, $where_values );
|
||||
}
|
||||
$least_expensive = $wpdb->get_row( $query, ARRAY_A );
|
||||
if ( $least_expensive ) {
|
||||
$stats['least_expensive_service'] = array(
|
||||
'name' => $least_expensive['name'],
|
||||
'price' => (float) $least_expensive['price']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
848
src/includes/services/class-auth-service.php
Normal file
848
src/includes/services/class-auth-service.php
Normal file
@@ -0,0 +1,848 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Authentication Service
|
||||
*
|
||||
* Handles JWT authentication, user validation and security
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Services
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_API\Services;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Auth_Service
|
||||
*
|
||||
* JWT Authentication service for KiviCare API
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Auth_Service {
|
||||
|
||||
/**
|
||||
* JWT Secret key
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $jwt_secret;
|
||||
|
||||
/**
|
||||
* Token expiration time (24 hours)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $token_expiration = 86400;
|
||||
|
||||
/**
|
||||
* Refresh token expiration (7 days)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $refresh_token_expiration = 604800;
|
||||
|
||||
/**
|
||||
* Valid KiviCare roles
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $valid_roles = array(
|
||||
'administrator',
|
||||
'kivicare_doctor',
|
||||
'kivicare_patient',
|
||||
'kivicare_receptionist'
|
||||
);
|
||||
|
||||
/**
|
||||
* Initialize the authentication service
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function init() {
|
||||
self::$jwt_secret = self::get_jwt_secret();
|
||||
|
||||
// Hook into WordPress authentication
|
||||
add_filter( 'determine_current_user', array( self::class, 'determine_current_user' ), 20 );
|
||||
add_filter( 'rest_authentication_errors', array( self::class, 'rest_authentication_errors' ) );
|
||||
|
||||
// Add custom headers
|
||||
add_action( 'rest_api_init', array( self::class, 'add_cors_headers' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate user with username/email and password
|
||||
*
|
||||
* @param string $username Username or email
|
||||
* @param string $password Password
|
||||
* @return array|WP_Error Authentication response or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function authenticate( $username, $password ) {
|
||||
// Input validation
|
||||
if ( empty( $username ) || empty( $password ) ) {
|
||||
return new \WP_Error(
|
||||
'missing_credentials',
|
||||
'Username and password are required',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
// Attempt to get user by username or email
|
||||
$user = self::get_user_by_login( $username );
|
||||
|
||||
if ( ! $user ) {
|
||||
return new \WP_Error(
|
||||
'invalid_username',
|
||||
'Invalid username or email address',
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
|
||||
// Log failed login attempt
|
||||
self::log_failed_login( $user->ID, $username );
|
||||
|
||||
return new \WP_Error(
|
||||
'invalid_password',
|
||||
'Invalid password',
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user has valid KiviCare role
|
||||
if ( ! self::has_valid_role( $user ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'User does not have permission to access KiviCare API',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user account is active
|
||||
$user_status = get_user_meta( $user->ID, 'kivicare_user_status', true );
|
||||
if ( $user_status === 'inactive' || $user_status === 'suspended' ) {
|
||||
return new \WP_Error(
|
||||
'account_inactive',
|
||||
'User account is inactive or suspended',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
$access_token = self::generate_jwt_token( $user );
|
||||
$refresh_token = self::generate_refresh_token( $user );
|
||||
|
||||
// Update user login metadata
|
||||
self::update_login_metadata( $user->ID );
|
||||
|
||||
// Return authentication response
|
||||
return array(
|
||||
'success' => true,
|
||||
'data' => array(
|
||||
'user' => self::format_user_data( $user ),
|
||||
'access_token' => $access_token,
|
||||
'refresh_token' => $refresh_token,
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in' => self::$token_expiration
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh access token using refresh token
|
||||
*
|
||||
* @param string $refresh_token Refresh token
|
||||
* @return array|WP_Error New tokens or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function refresh_token( $refresh_token ) {
|
||||
if ( empty( $refresh_token ) ) {
|
||||
return new \WP_Error(
|
||||
'missing_refresh_token',
|
||||
'Refresh token is required',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
// Validate refresh token
|
||||
$user_id = self::validate_refresh_token( $refresh_token );
|
||||
|
||||
if ( is_wp_error( $user_id ) ) {
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
if ( ! $user ) {
|
||||
return new \WP_Error(
|
||||
'user_not_found',
|
||||
'User not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user still has valid permissions
|
||||
if ( ! self::has_valid_role( $user ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'User no longer has permission to access KiviCare API',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
$new_access_token = self::generate_jwt_token( $user );
|
||||
$new_refresh_token = self::generate_refresh_token( $user );
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'data' => array(
|
||||
'access_token' => $new_access_token,
|
||||
'refresh_token' => $new_refresh_token,
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in' => self::$token_expiration
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate JWT token and return user
|
||||
*
|
||||
* @param string $token JWT token
|
||||
* @return WP_User|WP_Error User object or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function validate_token( $token ) {
|
||||
if ( empty( $token ) ) {
|
||||
return new \WP_Error(
|
||||
'missing_token',
|
||||
'Authentication token is required',
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Remove 'Bearer ' prefix if present
|
||||
$token = str_replace( 'Bearer ', '', $token );
|
||||
|
||||
// Decode JWT token
|
||||
$payload = self::decode_jwt_token( $token );
|
||||
|
||||
if ( is_wp_error( $payload ) ) {
|
||||
return $payload;
|
||||
}
|
||||
|
||||
// Get user
|
||||
$user = get_user_by( 'id', $payload->user_id );
|
||||
|
||||
if ( ! $user ) {
|
||||
return new \WP_Error(
|
||||
'user_not_found',
|
||||
'User not found',
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
// Validate user still has proper role
|
||||
if ( ! self::has_valid_role( $user ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'User no longer has permission to access KiviCare API',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
return $user;
|
||||
|
||||
} catch ( Exception $e ) {
|
||||
return new \WP_Error(
|
||||
'token_validation_failed',
|
||||
'Token validation failed: ' . $e->getMessage(),
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user by invalidating tokens
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @return bool Success status
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function logout( $user_id ) {
|
||||
// Invalidate all refresh tokens for this user
|
||||
delete_user_meta( $user_id, 'kivicare_refresh_token' );
|
||||
delete_user_meta( $user_id, 'kivicare_refresh_token_expires' );
|
||||
|
||||
// Update logout timestamp
|
||||
update_user_meta( $user_id, 'kivicare_last_logout', current_time( 'mysql' ) );
|
||||
|
||||
// Log logout action
|
||||
self::log_user_action( $user_id, 'logout' );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current authenticated user
|
||||
*
|
||||
* @return WP_User|null Current user or null
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_current_user() {
|
||||
$user_id = get_current_user_id();
|
||||
return $user_id ? get_user_by( 'id', $user_id ) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current user has specific capability
|
||||
*
|
||||
* @param string $capability Capability to check
|
||||
* @param array $args Additional arguments
|
||||
* @return bool True if user has capability
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function current_user_can( $capability, $args = array() ) {
|
||||
$user = self::get_current_user();
|
||||
|
||||
if ( ! $user ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check WordPress capability
|
||||
if ( ! empty( $args ) ) {
|
||||
return user_can( $user, $capability, ...$args );
|
||||
}
|
||||
|
||||
return user_can( $user, $capability );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JWT token for user
|
||||
*
|
||||
* @param WP_User $user User object
|
||||
* @return string JWT token
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function generate_jwt_token( $user ) {
|
||||
$issued_at = time();
|
||||
$expiration = $issued_at + self::$token_expiration;
|
||||
|
||||
$payload = array(
|
||||
'iss' => get_site_url(),
|
||||
'aud' => 'kivicare-api',
|
||||
'iat' => $issued_at,
|
||||
'exp' => $expiration,
|
||||
'user_id' => $user->ID,
|
||||
'username' => $user->user_login,
|
||||
'email' => $user->user_email,
|
||||
'roles' => $user->roles,
|
||||
'jti' => wp_generate_uuid4()
|
||||
);
|
||||
|
||||
return self::encode_jwt_token( $payload );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate refresh token for user
|
||||
*
|
||||
* @param WP_User $user User object
|
||||
* @return string Refresh token
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function generate_refresh_token( $user ) {
|
||||
$refresh_token = wp_generate_uuid4();
|
||||
$expires_at = time() + self::$refresh_token_expiration;
|
||||
|
||||
// Store refresh token in user meta
|
||||
update_user_meta( $user->ID, 'kivicare_refresh_token', wp_hash( $refresh_token ) );
|
||||
update_user_meta( $user->ID, 'kivicare_refresh_token_expires', $expires_at );
|
||||
|
||||
return $refresh_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate refresh token
|
||||
*
|
||||
* @param string $refresh_token Refresh token
|
||||
* @return int|WP_Error User ID or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function validate_refresh_token( $refresh_token ) {
|
||||
// Find user with this refresh token
|
||||
$users = get_users( array(
|
||||
'meta_key' => 'kivicare_refresh_token',
|
||||
'meta_value' => wp_hash( $refresh_token ),
|
||||
'number' => 1
|
||||
) );
|
||||
|
||||
if ( empty( $users ) ) {
|
||||
return new \WP_Error(
|
||||
'invalid_refresh_token',
|
||||
'Invalid refresh token',
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
$user = $users[0];
|
||||
$expires_at = get_user_meta( $user->ID, 'kivicare_refresh_token_expires', true );
|
||||
|
||||
// Check if token is expired
|
||||
if ( $expires_at && time() > $expires_at ) {
|
||||
// Clean up expired token
|
||||
delete_user_meta( $user->ID, 'kivicare_refresh_token' );
|
||||
delete_user_meta( $user->ID, 'kivicare_refresh_token_expires' );
|
||||
|
||||
return new \WP_Error(
|
||||
'refresh_token_expired',
|
||||
'Refresh token has expired',
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
return $user->ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode JWT token
|
||||
*
|
||||
* @param array $payload Token payload
|
||||
* @return string Encoded token
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function encode_jwt_token( $payload ) {
|
||||
$header = array(
|
||||
'typ' => 'JWT',
|
||||
'alg' => 'HS256'
|
||||
);
|
||||
|
||||
$header_encoded = self::base64url_encode( wp_json_encode( $header ) );
|
||||
$payload_encoded = self::base64url_encode( wp_json_encode( $payload ) );
|
||||
|
||||
$signature = hash_hmac(
|
||||
'sha256',
|
||||
$header_encoded . '.' . $payload_encoded,
|
||||
self::$jwt_secret,
|
||||
true
|
||||
);
|
||||
|
||||
$signature_encoded = self::base64url_encode( $signature );
|
||||
|
||||
return $header_encoded . '.' . $payload_encoded . '.' . $signature_encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode JWT token
|
||||
*
|
||||
* @param string $token JWT token
|
||||
* @return object|WP_Error Token payload or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function decode_jwt_token( $token ) {
|
||||
$parts = explode( '.', $token );
|
||||
|
||||
if ( count( $parts ) !== 3 ) {
|
||||
return new \WP_Error(
|
||||
'invalid_token_format',
|
||||
'Invalid token format',
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
list( $header_encoded, $payload_encoded, $signature_encoded ) = $parts;
|
||||
|
||||
// Verify signature
|
||||
$signature = self::base64url_decode( $signature_encoded );
|
||||
$expected_signature = hash_hmac(
|
||||
'sha256',
|
||||
$header_encoded . '.' . $payload_encoded,
|
||||
self::$jwt_secret,
|
||||
true
|
||||
);
|
||||
|
||||
if ( ! hash_equals( $signature, $expected_signature ) ) {
|
||||
return new \WP_Error(
|
||||
'invalid_token_signature',
|
||||
'Invalid token signature',
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
// Decode payload
|
||||
$payload = json_decode( self::base64url_decode( $payload_encoded ) );
|
||||
|
||||
if ( ! $payload ) {
|
||||
return new \WP_Error(
|
||||
'invalid_token_payload',
|
||||
'Invalid token payload',
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if ( isset( $payload->exp ) && time() > $payload->exp ) {
|
||||
return new \WP_Error(
|
||||
'token_expired',
|
||||
'Token has expired',
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64URL encode
|
||||
*
|
||||
* @param string $data Data to encode
|
||||
* @return string Encoded data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function base64url_encode( $data ) {
|
||||
return rtrim( strtr( base64_encode( $data ), '+/', '-_' ), '=' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64URL decode
|
||||
*
|
||||
* @param string $data Data to decode
|
||||
* @return string Decoded data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function base64url_decode( $data ) {
|
||||
return base64_decode( str_pad( strtr( $data, '-_', '+/' ), strlen( $data ) % 4, '=', STR_PAD_RIGHT ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create JWT secret
|
||||
*
|
||||
* @return string JWT secret
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_jwt_secret() {
|
||||
// Try to get from wp-config constant first
|
||||
if ( defined( 'KIVICARE_JWT_SECRET' ) && ! empty( KIVICARE_JWT_SECRET ) ) {
|
||||
return KIVICARE_JWT_SECRET;
|
||||
}
|
||||
|
||||
// Get from options
|
||||
$secret = get_option( 'kivicare_jwt_secret' );
|
||||
|
||||
if ( empty( $secret ) ) {
|
||||
// Generate new secret
|
||||
$secret = wp_generate_password( 64, true, true );
|
||||
update_option( 'kivicare_jwt_secret', $secret );
|
||||
}
|
||||
|
||||
return $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user by username or email
|
||||
*
|
||||
* @param string $login Username or email
|
||||
* @return WP_User|false User object or false
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_user_by_login( $login ) {
|
||||
// Try by username first
|
||||
$user = get_user_by( 'login', $login );
|
||||
|
||||
// If not found, try by email
|
||||
if ( ! $user && is_email( $login ) ) {
|
||||
$user = get_user_by( 'email', $login );
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has valid KiviCare role
|
||||
*
|
||||
* @param WP_User $user User object
|
||||
* @return bool True if has valid role
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function has_valid_role( $user ) {
|
||||
$user_roles = $user->roles;
|
||||
return ! empty( array_intersect( $user_roles, self::$valid_roles ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format user data for API response
|
||||
*
|
||||
* @param WP_User $user User object
|
||||
* @return array Formatted user data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function format_user_data( $user ) {
|
||||
return array(
|
||||
'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,
|
||||
'roles' => $user->roles,
|
||||
'primary_role' => self::get_primary_role( $user ),
|
||||
'avatar_url' => get_avatar_url( $user->ID ),
|
||||
'registered_date' => $user->user_registered
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get primary KiviCare role for user
|
||||
*
|
||||
* @param WP_User $user User object
|
||||
* @return string Primary role
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_primary_role( $user ) {
|
||||
$kivicare_roles = array_intersect( $user->roles, self::$valid_roles );
|
||||
|
||||
// Priority order for KiviCare roles
|
||||
$role_priority = array(
|
||||
'administrator',
|
||||
'kivicare_doctor',
|
||||
'kivicare_receptionist',
|
||||
'kivicare_patient'
|
||||
);
|
||||
|
||||
foreach ( $role_priority as $role ) {
|
||||
if ( in_array( $role, $kivicare_roles ) ) {
|
||||
return $role;
|
||||
}
|
||||
}
|
||||
|
||||
return 'kivicare_patient'; // Default fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user login metadata
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function update_login_metadata( $user_id ) {
|
||||
update_user_meta( $user_id, 'kivicare_last_login', current_time( 'mysql' ) );
|
||||
update_user_meta( $user_id, 'kivicare_login_count', (int) get_user_meta( $user_id, 'kivicare_login_count', true ) + 1 );
|
||||
update_user_meta( $user_id, 'kivicare_last_ip', self::get_client_ip() );
|
||||
|
||||
// Log successful login
|
||||
self::log_user_action( $user_id, 'login' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log failed login attempt
|
||||
*
|
||||
* @param int $user_id User ID (if found)
|
||||
* @param string $username Attempted username
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function log_failed_login( $user_id, $username ) {
|
||||
$log_data = array(
|
||||
'user_id' => $user_id,
|
||||
'username' => $username,
|
||||
'ip_address' => self::get_client_ip(),
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||
'timestamp' => current_time( 'mysql' )
|
||||
);
|
||||
|
||||
// Could be extended to store in custom table or send alerts
|
||||
do_action( 'kivicare_failed_login', $log_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log user action
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @param string $action Action performed
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function log_user_action( $user_id, $action ) {
|
||||
$log_data = array(
|
||||
'user_id' => $user_id,
|
||||
'action' => $action,
|
||||
'ip_address' => self::get_client_ip(),
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||
'timestamp' => current_time( 'mysql' )
|
||||
);
|
||||
|
||||
// Could be extended for audit logging
|
||||
do_action( 'kivicare_user_action', $log_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client IP address
|
||||
*
|
||||
* @return string IP address
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_client_ip() {
|
||||
$ip_keys = array(
|
||||
'HTTP_CF_CONNECTING_IP',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_FORWARDED',
|
||||
'HTTP_X_CLUSTER_CLIENT_IP',
|
||||
'HTTP_FORWARDED_FOR',
|
||||
'HTTP_FORWARDED',
|
||||
'REMOTE_ADDR'
|
||||
);
|
||||
|
||||
foreach ( $ip_keys as $key ) {
|
||||
if ( ! empty( $_SERVER[ $key ] ) ) {
|
||||
$ip = $_SERVER[ $key ];
|
||||
if ( strpos( $ip, ',' ) !== false ) {
|
||||
$ip = explode( ',', $ip )[0];
|
||||
}
|
||||
$ip = trim( $ip );
|
||||
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* WordPress hook: Determine current user from JWT token
|
||||
*
|
||||
* @param int $user_id Current user ID
|
||||
* @return int User ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function determine_current_user( $user_id ) {
|
||||
// Skip if user is already determined
|
||||
if ( $user_id ) {
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
// Only for REST API requests
|
||||
if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
// Get authorization header
|
||||
$auth_header = self::get_authorization_header();
|
||||
|
||||
if ( empty( $auth_header ) ) {
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
// Validate token
|
||||
$user = self::validate_token( $auth_header );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
return $user->ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* WordPress hook: REST authentication errors
|
||||
*
|
||||
* @param WP_Error|null|bool $result Previous result
|
||||
* @return WP_Error|null|bool Authentication result
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function rest_authentication_errors( $result ) {
|
||||
// Skip if already processed
|
||||
if ( ! empty( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Check if this is a KiviCare API request
|
||||
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
if ( strpos( $request_uri, '/wp-json/kivicare/v1' ) === false ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Allow authentication endpoints without token
|
||||
$public_endpoints = array(
|
||||
'/wp-json/kivicare/v1/auth/login',
|
||||
'/wp-json/kivicare/v1/auth/refresh'
|
||||
);
|
||||
|
||||
foreach ( $public_endpoints as $endpoint ) {
|
||||
if ( strpos( $request_uri, $endpoint ) !== false ) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Require authentication for all other KiviCare endpoints
|
||||
if ( ! get_current_user_id() ) {
|
||||
return new \WP_Error(
|
||||
'rest_not_logged_in',
|
||||
'You are not currently logged in.',
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorization header
|
||||
*
|
||||
* @return string|null Authorization header
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_authorization_header() {
|
||||
$headers = array(
|
||||
'HTTP_AUTHORIZATION',
|
||||
'REDIRECT_HTTP_AUTHORIZATION'
|
||||
);
|
||||
|
||||
foreach ( $headers as $header ) {
|
||||
if ( ! empty( $_SERVER[ $header ] ) ) {
|
||||
return trim( $_SERVER[ $header ] );
|
||||
}
|
||||
}
|
||||
|
||||
// Check if using PHP-CGI
|
||||
if ( function_exists( 'apache_request_headers' ) ) {
|
||||
$apache_headers = apache_request_headers();
|
||||
if ( isset( $apache_headers['Authorization'] ) ) {
|
||||
return trim( $apache_headers['Authorization'] );
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add CORS headers for API requests
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function add_cors_headers() {
|
||||
// Allow specific origins (should be configured)
|
||||
$allowed_origins = apply_filters( 'kivicare_api_allowed_origins', array() );
|
||||
|
||||
if ( ! empty( $allowed_origins ) ) {
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
if ( in_array( $origin, $allowed_origins ) ) {
|
||||
header( 'Access-Control-Allow-Origin: ' . $origin );
|
||||
}
|
||||
}
|
||||
|
||||
header( 'Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS' );
|
||||
header( 'Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Nonce' );
|
||||
header( 'Access-Control-Allow-Credentials: true' );
|
||||
}
|
||||
}
|
||||
838
src/includes/services/class-permission-service.php
Normal file
838
src/includes/services/class-permission-service.php
Normal file
@@ -0,0 +1,838 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Permission Service
|
||||
*
|
||||
* Handles role-based access control and permission management
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Services
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_API\Services;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Permission_Service
|
||||
*
|
||||
* Role-based permission system for KiviCare API
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Permission_Service {
|
||||
|
||||
/**
|
||||
* Permission matrix defining what each role can do
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $permission_matrix = array();
|
||||
|
||||
/**
|
||||
* Resource-specific permissions
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $resource_permissions = array();
|
||||
|
||||
/**
|
||||
* Initialize the permission service
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function init() {
|
||||
self::define_permission_matrix();
|
||||
self::define_resource_permissions();
|
||||
|
||||
// Hook into WordPress capability system
|
||||
add_filter( 'user_has_cap', array( self::class, 'user_has_cap' ), 10, 4 );
|
||||
|
||||
// Add custom capabilities on plugin activation
|
||||
add_action( 'init', array( self::class, 'add_kivicare_capabilities' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has permission for specific action
|
||||
*
|
||||
* @param int|WP_User $user User ID or user object
|
||||
* @param string $permission Permission to check
|
||||
* @param array $context Additional context (resource_id, clinic_id, etc.)
|
||||
* @return bool True if user has permission
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function has_permission( $user, $permission, $context = array() ) {
|
||||
if ( is_numeric( $user ) ) {
|
||||
$user = get_user_by( 'id', $user );
|
||||
}
|
||||
|
||||
if ( ! $user instanceof \WP_User ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Super admin has all permissions
|
||||
if ( is_super_admin( $user->ID ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get user's primary KiviCare role
|
||||
$primary_role = self::get_primary_kivicare_role( $user );
|
||||
|
||||
if ( ! $primary_role ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check basic permission matrix
|
||||
if ( ! self::role_has_permission( $primary_role, $permission ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply contextual restrictions
|
||||
return self::check_contextual_permissions( $user, $permission, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current user has permission
|
||||
*
|
||||
* @param string $permission Permission to check
|
||||
* @param array $context Additional context
|
||||
* @return bool True if current user has permission
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function current_user_can( $permission, $context = array() ) {
|
||||
$user = wp_get_current_user();
|
||||
return $user && self::has_permission( $user, $permission, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all permissions for a user
|
||||
*
|
||||
* @param int|WP_User $user User ID or user object
|
||||
* @return array Array of permissions
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_user_permissions( $user ) {
|
||||
if ( is_numeric( $user ) ) {
|
||||
$user = get_user_by( 'id', $user );
|
||||
}
|
||||
|
||||
if ( ! $user instanceof \WP_User ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$primary_role = self::get_primary_kivicare_role( $user );
|
||||
|
||||
if ( ! $primary_role || ! isset( self::$permission_matrix[ $primary_role ] ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return self::$permission_matrix[ $primary_role ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check clinic access permission
|
||||
*
|
||||
* @param int|WP_User $user User ID or user object
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return bool True if user has access to clinic
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function can_access_clinic( $user, $clinic_id ) {
|
||||
if ( is_numeric( $user ) ) {
|
||||
$user = get_user_by( 'id', $user );
|
||||
}
|
||||
|
||||
if ( ! $user instanceof \WP_User ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Administrator has access to all clinics
|
||||
if ( in_array( 'administrator', $user->roles ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$primary_role = self::get_primary_kivicare_role( $user );
|
||||
|
||||
switch ( $primary_role ) {
|
||||
case 'kivicare_doctor':
|
||||
return self::doctor_has_clinic_access( $user->ID, $clinic_id );
|
||||
|
||||
case 'kivicare_patient':
|
||||
return self::patient_has_clinic_access( $user->ID, $clinic_id );
|
||||
|
||||
case 'kivicare_receptionist':
|
||||
return self::receptionist_has_clinic_access( $user->ID, $clinic_id );
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check patient access permission
|
||||
*
|
||||
* @param int|WP_User $user User ID or user object
|
||||
* @param int $patient_id Patient ID
|
||||
* @return bool True if user can access patient data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function can_access_patient( $user, $patient_id ) {
|
||||
if ( is_numeric( $user ) ) {
|
||||
$user = get_user_by( 'id', $user );
|
||||
}
|
||||
|
||||
if ( ! $user instanceof \WP_User ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Administrator has access to all patients
|
||||
if ( in_array( 'administrator', $user->roles ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$primary_role = self::get_primary_kivicare_role( $user );
|
||||
|
||||
switch ( $primary_role ) {
|
||||
case 'kivicare_doctor':
|
||||
return self::doctor_can_access_patient( $user->ID, $patient_id );
|
||||
|
||||
case 'kivicare_patient':
|
||||
// Patients can only access their own data
|
||||
return $user->ID === $patient_id;
|
||||
|
||||
case 'kivicare_receptionist':
|
||||
return self::receptionist_can_access_patient( $user->ID, $patient_id );
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check appointment access permission
|
||||
*
|
||||
* @param int|WP_User $user User ID or user object
|
||||
* @param int $appointment_id Appointment ID
|
||||
* @return bool True if user can access appointment
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function can_access_appointment( $user, $appointment_id ) {
|
||||
if ( is_numeric( $user ) ) {
|
||||
$user = get_user_by( 'id', $user );
|
||||
}
|
||||
|
||||
if ( ! $user instanceof \WP_User ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get appointment data
|
||||
global $wpdb;
|
||||
$appointment = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT doctor_id, patient_id, clinic_id FROM {$wpdb->prefix}kc_appointments WHERE id = %d",
|
||||
$appointment_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( ! $appointment ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Administrator has access to all appointments
|
||||
if ( in_array( 'administrator', $user->roles ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$primary_role = self::get_primary_kivicare_role( $user );
|
||||
|
||||
switch ( $primary_role ) {
|
||||
case 'kivicare_doctor':
|
||||
// Doctors can access their own appointments or appointments in their clinics
|
||||
return $user->ID === (int) $appointment['doctor_id'] ||
|
||||
self::doctor_has_clinic_access( $user->ID, $appointment['clinic_id'] );
|
||||
|
||||
case 'kivicare_patient':
|
||||
// Patients can only access their own appointments
|
||||
return $user->ID === (int) $appointment['patient_id'];
|
||||
|
||||
case 'kivicare_receptionist':
|
||||
// Receptionists can access appointments in their clinics
|
||||
return self::receptionist_has_clinic_access( $user->ID, $appointment['clinic_id'] );
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get filtered clinic IDs for user
|
||||
*
|
||||
* @param int|WP_User $user User ID or user object
|
||||
* @return array Array of clinic IDs user can access
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_accessible_clinic_ids( $user ) {
|
||||
if ( is_numeric( $user ) ) {
|
||||
$user = get_user_by( 'id', $user );
|
||||
}
|
||||
|
||||
if ( ! $user instanceof \WP_User ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Administrator has access to all clinics
|
||||
if ( in_array( 'administrator', $user->roles ) ) {
|
||||
global $wpdb;
|
||||
$clinic_ids = $wpdb->get_col( "SELECT id FROM {$wpdb->prefix}kc_clinics WHERE status = 1" );
|
||||
return array_map( 'intval', $clinic_ids );
|
||||
}
|
||||
|
||||
$primary_role = self::get_primary_kivicare_role( $user );
|
||||
|
||||
switch ( $primary_role ) {
|
||||
case 'kivicare_doctor':
|
||||
return self::get_doctor_clinic_ids( $user->ID );
|
||||
|
||||
case 'kivicare_patient':
|
||||
return self::get_patient_clinic_ids( $user->ID );
|
||||
|
||||
case 'kivicare_receptionist':
|
||||
return self::get_receptionist_clinic_ids( $user->ID );
|
||||
|
||||
default:
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define permission matrix for each role
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function define_permission_matrix() {
|
||||
self::$permission_matrix = array(
|
||||
'administrator' => array(
|
||||
// Full system access
|
||||
'manage_clinics',
|
||||
'manage_users',
|
||||
'manage_doctors',
|
||||
'manage_patients',
|
||||
'manage_appointments',
|
||||
'manage_encounters',
|
||||
'manage_prescriptions',
|
||||
'manage_bills',
|
||||
'manage_services',
|
||||
'view_all_data',
|
||||
'manage_settings',
|
||||
'view_reports',
|
||||
'export_data'
|
||||
),
|
||||
|
||||
'kivicare_doctor' => array(
|
||||
// Patient management within assigned clinics
|
||||
'view_patients',
|
||||
'create_patients',
|
||||
'edit_assigned_patients',
|
||||
|
||||
// Appointment management
|
||||
'view_own_appointments',
|
||||
'edit_own_appointments',
|
||||
'create_appointments',
|
||||
|
||||
// Medical records
|
||||
'view_patient_encounters',
|
||||
'create_encounters',
|
||||
'edit_own_encounters',
|
||||
'view_prescriptions',
|
||||
'create_prescriptions',
|
||||
'edit_own_prescriptions',
|
||||
|
||||
// Billing (limited)
|
||||
'view_bills',
|
||||
'create_bills',
|
||||
|
||||
// Services
|
||||
'view_services',
|
||||
|
||||
// Profile management
|
||||
'edit_own_profile',
|
||||
'view_own_schedule'
|
||||
),
|
||||
|
||||
'kivicare_patient' => array(
|
||||
// Own data access only
|
||||
'view_own_profile',
|
||||
'edit_own_profile',
|
||||
'view_own_appointments',
|
||||
'create_own_appointments', // If enabled
|
||||
'view_own_encounters',
|
||||
'view_own_prescriptions',
|
||||
'view_own_bills',
|
||||
'view_services'
|
||||
),
|
||||
|
||||
'kivicare_receptionist' => array(
|
||||
// Patient management for assigned clinics
|
||||
'view_patients',
|
||||
'create_patients',
|
||||
'edit_patients',
|
||||
|
||||
// Appointment management
|
||||
'view_appointments',
|
||||
'create_appointments',
|
||||
'edit_appointments',
|
||||
'cancel_appointments',
|
||||
|
||||
// Basic billing
|
||||
'view_bills',
|
||||
'create_bills',
|
||||
'process_payments',
|
||||
|
||||
// Services
|
||||
'view_services',
|
||||
|
||||
// Limited reporting
|
||||
'view_basic_reports'
|
||||
)
|
||||
);
|
||||
|
||||
// Apply filters to allow customization
|
||||
self::$permission_matrix = apply_filters( 'kivicare_permission_matrix', self::$permission_matrix );
|
||||
}
|
||||
|
||||
/**
|
||||
* Define resource-specific permissions
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function define_resource_permissions() {
|
||||
self::$resource_permissions = array(
|
||||
'clinics' => array(
|
||||
'view' => array( 'administrator', 'kivicare_doctor', 'kivicare_receptionist' ),
|
||||
'create' => array( 'administrator' ),
|
||||
'edit' => array( 'administrator' ),
|
||||
'delete' => array( 'administrator' )
|
||||
),
|
||||
|
||||
'patients' => array(
|
||||
'view' => array( 'administrator', 'kivicare_doctor', 'kivicare_receptionist' ),
|
||||
'create' => array( 'administrator', 'kivicare_doctor', 'kivicare_receptionist' ),
|
||||
'edit' => array( 'administrator', 'kivicare_doctor', 'kivicare_receptionist' ),
|
||||
'delete' => array( 'administrator' )
|
||||
),
|
||||
|
||||
'appointments' => array(
|
||||
'view' => array( 'administrator', 'kivicare_doctor', 'kivicare_patient', 'kivicare_receptionist' ),
|
||||
'create' => array( 'administrator', 'kivicare_doctor', 'kivicare_receptionist' ),
|
||||
'edit' => array( 'administrator', 'kivicare_doctor', 'kivicare_receptionist' ),
|
||||
'delete' => array( 'administrator', 'kivicare_doctor', 'kivicare_receptionist' )
|
||||
),
|
||||
|
||||
'encounters' => array(
|
||||
'view' => array( 'administrator', 'kivicare_doctor' ),
|
||||
'create' => array( 'administrator', 'kivicare_doctor' ),
|
||||
'edit' => array( 'administrator', 'kivicare_doctor' ),
|
||||
'delete' => array( 'administrator' )
|
||||
),
|
||||
|
||||
'prescriptions' => array(
|
||||
'view' => array( 'administrator', 'kivicare_doctor', 'kivicare_patient' ),
|
||||
'create' => array( 'administrator', 'kivicare_doctor' ),
|
||||
'edit' => array( 'administrator', 'kivicare_doctor' ),
|
||||
'delete' => array( 'administrator', 'kivicare_doctor' )
|
||||
),
|
||||
|
||||
'bills' => array(
|
||||
'view' => array( 'administrator', 'kivicare_doctor', 'kivicare_patient', 'kivicare_receptionist' ),
|
||||
'create' => array( 'administrator', 'kivicare_doctor', 'kivicare_receptionist' ),
|
||||
'edit' => array( 'administrator', 'kivicare_receptionist' ),
|
||||
'delete' => array( 'administrator' )
|
||||
)
|
||||
);
|
||||
|
||||
self::$resource_permissions = apply_filters( 'kivicare_resource_permissions', self::$resource_permissions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if role has basic permission
|
||||
*
|
||||
* @param string $role User role
|
||||
* @param string $permission Permission to check
|
||||
* @return bool True if role has permission
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function role_has_permission( $role, $permission ) {
|
||||
if ( ! isset( self::$permission_matrix[ $role ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array( $permission, self::$permission_matrix[ $role ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check contextual permissions based on resource ownership and clinic access
|
||||
*
|
||||
* @param WP_User $user User object
|
||||
* @param string $permission Permission to check
|
||||
* @param array $context Context data
|
||||
* @return bool True if user has contextual permission
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function check_contextual_permissions( $user, $permission, $context ) {
|
||||
// Extract context information
|
||||
$resource_id = $context['resource_id'] ?? null;
|
||||
$clinic_id = $context['clinic_id'] ?? null;
|
||||
$patient_id = $context['patient_id'] ?? null;
|
||||
$doctor_id = $context['doctor_id'] ?? null;
|
||||
$resource_type = $context['resource_type'] ?? '';
|
||||
|
||||
$primary_role = self::get_primary_kivicare_role( $user );
|
||||
|
||||
// Apply role-specific contextual rules
|
||||
switch ( $primary_role ) {
|
||||
case 'kivicare_doctor':
|
||||
return self::check_doctor_context( $user->ID, $permission, $context );
|
||||
|
||||
case 'kivicare_patient':
|
||||
return self::check_patient_context( $user->ID, $permission, $context );
|
||||
|
||||
case 'kivicare_receptionist':
|
||||
return self::check_receptionist_context( $user->ID, $permission, $context );
|
||||
|
||||
default:
|
||||
return true; // Administrator or unknown role
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check doctor-specific contextual permissions
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @param string $permission Permission to check
|
||||
* @param array $context Context data
|
||||
* @return bool True if doctor has contextual permission
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function check_doctor_context( $doctor_id, $permission, $context ) {
|
||||
$clinic_id = $context['clinic_id'] ?? null;
|
||||
$patient_id = $context['patient_id'] ?? null;
|
||||
$resource_type = $context['resource_type'] ?? '';
|
||||
|
||||
// Check clinic access
|
||||
if ( $clinic_id && ! self::doctor_has_clinic_access( $doctor_id, $clinic_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check patient access
|
||||
if ( $patient_id && ! self::doctor_can_access_patient( $doctor_id, $patient_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resource-specific rules
|
||||
if ( $resource_type === 'appointment' ) {
|
||||
$appointment_id = $context['resource_id'] ?? null;
|
||||
if ( $appointment_id ) {
|
||||
return self::can_access_appointment( $doctor_id, $appointment_id );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check patient-specific contextual permissions
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @param string $permission Permission to check
|
||||
* @param array $context Context data
|
||||
* @return bool True if patient has contextual permission
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function check_patient_context( $patient_id, $permission, $context ) {
|
||||
// Patients can only access their own data
|
||||
$resource_patient_id = $context['patient_id'] ?? null;
|
||||
|
||||
if ( $resource_patient_id && $resource_patient_id !== $patient_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check receptionist-specific contextual permissions
|
||||
*
|
||||
* @param int $receptionist_id Receptionist ID
|
||||
* @param string $permission Permission to check
|
||||
* @param array $context Context data
|
||||
* @return bool True if receptionist has contextual permission
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function check_receptionist_context( $receptionist_id, $permission, $context ) {
|
||||
$clinic_id = $context['clinic_id'] ?? null;
|
||||
|
||||
// Check clinic access
|
||||
if ( $clinic_id && ! self::receptionist_has_clinic_access( $receptionist_id, $clinic_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get primary KiviCare role for user
|
||||
*
|
||||
* @param WP_User $user User object
|
||||
* @return string|null Primary KiviCare role
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_primary_kivicare_role( $user ) {
|
||||
$kivicare_roles = array( 'administrator', 'kivicare_doctor', 'kivicare_patient', 'kivicare_receptionist' );
|
||||
$user_roles = array_intersect( $user->roles, $kivicare_roles );
|
||||
|
||||
if ( empty( $user_roles ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Priority order
|
||||
$priority_order = array( 'administrator', 'kivicare_doctor', 'kivicare_receptionist', 'kivicare_patient' );
|
||||
|
||||
foreach ( $priority_order as $role ) {
|
||||
if ( in_array( $role, $user_roles ) ) {
|
||||
return $role;
|
||||
}
|
||||
}
|
||||
|
||||
return reset( $user_roles );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if doctor has access to specific clinic
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return bool True if doctor has access
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function doctor_has_clinic_access( $doctor_id, $clinic_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_doctor_clinic_mappings WHERE doctor_id = %d AND clinic_id = %d",
|
||||
$doctor_id, $clinic_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if patient has access to specific clinic
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return bool True if patient has access
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function patient_has_clinic_access( $patient_id, $clinic_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_patient_clinic_mappings WHERE patient_id = %d AND clinic_id = %d",
|
||||
$patient_id, $clinic_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if receptionist has access to specific clinic
|
||||
*
|
||||
* @param int $receptionist_id Receptionist ID
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return bool True if receptionist has access
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function receptionist_has_clinic_access( $receptionist_id, $clinic_id ) {
|
||||
// For now, assuming receptionists are assigned via user meta
|
||||
// This could be extended with a dedicated mapping table
|
||||
$assigned_clinics = get_user_meta( $receptionist_id, 'kivicare_assigned_clinics', true );
|
||||
|
||||
if ( ! is_array( $assigned_clinics ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array( $clinic_id, $assigned_clinics );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if doctor can access specific patient
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @param int $patient_id Patient ID
|
||||
* @return bool True if doctor can access patient
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function doctor_can_access_patient( $doctor_id, $patient_id ) {
|
||||
global $wpdb;
|
||||
|
||||
// Check if doctor has appointments with this patient
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments WHERE doctor_id = %d AND patient_id = %d",
|
||||
$doctor_id, $patient_id
|
||||
)
|
||||
);
|
||||
|
||||
if ( (int) $count > 0 ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if patient is in doctor's clinics
|
||||
$doctor_clinics = self::get_doctor_clinic_ids( $doctor_id );
|
||||
$patient_clinics = self::get_patient_clinic_ids( $patient_id );
|
||||
|
||||
return ! empty( array_intersect( $doctor_clinics, $patient_clinics ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if receptionist can access specific patient
|
||||
*
|
||||
* @param int $receptionist_id Receptionist ID
|
||||
* @param int $patient_id Patient ID
|
||||
* @return bool True if receptionist can access patient
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function receptionist_can_access_patient( $receptionist_id, $patient_id ) {
|
||||
$receptionist_clinics = self::get_receptionist_clinic_ids( $receptionist_id );
|
||||
$patient_clinics = self::get_patient_clinic_ids( $patient_id );
|
||||
|
||||
return ! empty( array_intersect( $receptionist_clinics, $patient_clinics ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinic IDs for doctor
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return array Array of clinic IDs
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_doctor_clinic_ids( $doctor_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$clinic_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT clinic_id FROM {$wpdb->prefix}kc_doctor_clinic_mappings WHERE doctor_id = %d",
|
||||
$doctor_id
|
||||
)
|
||||
);
|
||||
|
||||
return array_map( 'intval', $clinic_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinic IDs for patient
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @return array Array of clinic IDs
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_patient_clinic_ids( $patient_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$clinic_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT clinic_id FROM {$wpdb->prefix}kc_patient_clinic_mappings WHERE patient_id = %d",
|
||||
$patient_id
|
||||
)
|
||||
);
|
||||
|
||||
return array_map( 'intval', $clinic_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinic IDs for receptionist
|
||||
*
|
||||
* @param int $receptionist_id Receptionist ID
|
||||
* @return array Array of clinic IDs
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_receptionist_clinic_ids( $receptionist_id ) {
|
||||
$assigned_clinics = get_user_meta( $receptionist_id, 'kivicare_assigned_clinics', true );
|
||||
|
||||
if ( ! is_array( $assigned_clinics ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array_map( 'intval', $assigned_clinics );
|
||||
}
|
||||
|
||||
/**
|
||||
* WordPress hook: Modify user capabilities
|
||||
*
|
||||
* @param array $allcaps All capabilities
|
||||
* @param array $caps Requested capabilities
|
||||
* @param array $args Arguments
|
||||
* @param WP_User $user User object
|
||||
* @return array Modified capabilities
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function user_has_cap( $allcaps, $caps, $args, $user ) {
|
||||
// Only modify for KiviCare capabilities
|
||||
foreach ( $caps as $cap ) {
|
||||
if ( strpos( $cap, 'kivicare_' ) === 0 ) {
|
||||
$allcaps[ $cap ] = self::has_permission( $user, $cap, $args );
|
||||
}
|
||||
}
|
||||
|
||||
return $allcaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add KiviCare-specific capabilities to WordPress
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function add_kivicare_capabilities() {
|
||||
// Get all unique capabilities from permission matrix
|
||||
$all_capabilities = array();
|
||||
|
||||
foreach ( self::$permission_matrix as $role_caps ) {
|
||||
$all_capabilities = array_merge( $all_capabilities, $role_caps );
|
||||
}
|
||||
|
||||
$all_capabilities = array_unique( $all_capabilities );
|
||||
|
||||
// Add capabilities to administrator role
|
||||
$admin_role = get_role( 'administrator' );
|
||||
if ( $admin_role ) {
|
||||
foreach ( $all_capabilities as $cap ) {
|
||||
$admin_role->add_cap( $cap );
|
||||
}
|
||||
}
|
||||
|
||||
// Add role-specific capabilities
|
||||
foreach ( self::$permission_matrix as $role_name => $capabilities ) {
|
||||
$role = get_role( $role_name );
|
||||
if ( $role ) {
|
||||
foreach ( $capabilities as $cap ) {
|
||||
$role->add_cap( $cap );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
905
src/includes/services/class-session-service.php
Normal file
905
src/includes/services/class-session-service.php
Normal file
@@ -0,0 +1,905 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Session Service
|
||||
*
|
||||
* Handles user session management, security and monitoring
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Services
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_API\Services;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Session_Service
|
||||
*
|
||||
* Session management and security monitoring for KiviCare API
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Session_Service {
|
||||
|
||||
/**
|
||||
* Maximum concurrent sessions per user
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $max_concurrent_sessions = 3;
|
||||
|
||||
/**
|
||||
* Session timeout (in seconds) - 30 minutes
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $session_timeout = 1800;
|
||||
|
||||
/**
|
||||
* Maximum failed login attempts
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $max_failed_attempts = 5;
|
||||
|
||||
/**
|
||||
* Lockout duration (in seconds) - 15 minutes
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private static $lockout_duration = 900;
|
||||
|
||||
/**
|
||||
* Initialize the session service
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function init() {
|
||||
// Hook into authentication events
|
||||
add_action( 'kivicare_user_authenticated', array( self::class, 'on_user_authenticated' ), 10, 2 );
|
||||
add_action( 'kivicare_user_logout', array( self::class, 'on_user_logout' ), 10, 1 );
|
||||
add_action( 'kivicare_failed_login', array( self::class, 'on_failed_login' ), 10, 1 );
|
||||
|
||||
// Cleanup expired sessions
|
||||
add_action( 'kivicare_cleanup_sessions', array( self::class, 'cleanup_expired_sessions' ) );
|
||||
|
||||
// Schedule cleanup if not already scheduled
|
||||
if ( ! wp_next_scheduled( 'kivicare_cleanup_sessions' ) ) {
|
||||
wp_schedule_event( time(), 'hourly', 'kivicare_cleanup_sessions' );
|
||||
}
|
||||
|
||||
// Monitor session activity
|
||||
add_action( 'init', array( self::class, 'monitor_session_activity' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new session for user
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @param string $ip_address IP address
|
||||
* @param string $user_agent User agent
|
||||
* @return string Session ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function create_session( $user_id, $ip_address, $user_agent ) {
|
||||
// Generate unique session ID
|
||||
$session_id = wp_generate_uuid4();
|
||||
|
||||
// Check concurrent session limit
|
||||
self::enforce_concurrent_session_limit( $user_id );
|
||||
|
||||
// Create session data
|
||||
$session_data = array(
|
||||
'session_id' => $session_id,
|
||||
'user_id' => $user_id,
|
||||
'ip_address' => $ip_address,
|
||||
'user_agent' => $user_agent,
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
'last_activity' => current_time( 'mysql' ),
|
||||
'expires_at' => date( 'Y-m-d H:i:s', time() + self::$session_timeout ),
|
||||
'is_active' => 1
|
||||
);
|
||||
|
||||
// Store session in database
|
||||
self::store_session( $session_data );
|
||||
|
||||
// Update user session metadata
|
||||
self::update_user_session_meta( $user_id, $session_id, $ip_address );
|
||||
|
||||
return $session_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate session
|
||||
*
|
||||
* @param string $session_id Session ID
|
||||
* @param int $user_id User ID
|
||||
* @return bool|array Session data or false if invalid
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function validate_session( $session_id, $user_id ) {
|
||||
$session = self::get_session( $session_id );
|
||||
|
||||
if ( ! $session ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if session belongs to user
|
||||
if ( (int) $session['user_id'] !== $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if session is active
|
||||
if ( ! $session['is_active'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if session is expired
|
||||
if ( strtotime( $session['expires_at'] ) < time() ) {
|
||||
self::expire_session( $session_id );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for session timeout based on last activity
|
||||
$last_activity = strtotime( $session['last_activity'] );
|
||||
if ( ( time() - $last_activity ) > self::$session_timeout ) {
|
||||
self::expire_session( $session_id );
|
||||
return false;
|
||||
}
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session activity
|
||||
*
|
||||
* @param string $session_id Session ID
|
||||
* @return bool Success status
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function update_session_activity( $session_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$result = $wpdb->update(
|
||||
$wpdb->prefix . 'kivicare_sessions',
|
||||
array(
|
||||
'last_activity' => current_time( 'mysql' ),
|
||||
'expires_at' => date( 'Y-m-d H:i:s', time() + self::$session_timeout )
|
||||
),
|
||||
array( 'session_id' => $session_id ),
|
||||
array( '%s', '%s' ),
|
||||
array( '%s' )
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expire session
|
||||
*
|
||||
* @param string $session_id Session ID
|
||||
* @return bool Success status
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function expire_session( $session_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$result = $wpdb->update(
|
||||
$wpdb->prefix . 'kivicare_sessions',
|
||||
array(
|
||||
'is_active' => 0,
|
||||
'ended_at' => current_time( 'mysql' )
|
||||
),
|
||||
array( 'session_id' => $session_id ),
|
||||
array( '%d', '%s' ),
|
||||
array( '%s' )
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expire all sessions for user
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @return bool Success status
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function expire_user_sessions( $user_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$result = $wpdb->update(
|
||||
$wpdb->prefix . 'kivicare_sessions',
|
||||
array(
|
||||
'is_active' => 0,
|
||||
'ended_at' => current_time( 'mysql' )
|
||||
),
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'is_active' => 1
|
||||
),
|
||||
array( '%d', '%s' ),
|
||||
array( '%d', '%d' )
|
||||
);
|
||||
|
||||
// Clear user session metadata
|
||||
delete_user_meta( $user_id, 'kivicare_current_session' );
|
||||
delete_user_meta( $user_id, 'kivicare_session_ip' );
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active sessions for user
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @return array Array of active sessions
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_user_sessions( $user_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$sessions = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d AND is_active = 1 AND expires_at > NOW()
|
||||
ORDER BY last_activity DESC",
|
||||
$user_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map( array( self::class, 'format_session_data' ), $sessions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user account is locked
|
||||
*
|
||||
* @param int|string $user_identifier User ID or username
|
||||
* @return bool True if account is locked
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function is_account_locked( $user_identifier ) {
|
||||
if ( is_numeric( $user_identifier ) ) {
|
||||
$user = get_user_by( 'id', $user_identifier );
|
||||
} else {
|
||||
$user = get_user_by( 'login', $user_identifier );
|
||||
if ( ! $user && is_email( $user_identifier ) ) {
|
||||
$user = get_user_by( 'email', $user_identifier );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $user ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lockout_time = get_user_meta( $user->ID, 'kivicare_lockout_time', true );
|
||||
|
||||
if ( ! $lockout_time ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if lockout has expired
|
||||
if ( time() > $lockout_time ) {
|
||||
delete_user_meta( $user->ID, 'kivicare_lockout_time' );
|
||||
delete_user_meta( $user->ID, 'kivicare_failed_attempts' );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining lockout time
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @return int Remaining lockout time in seconds
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_lockout_remaining_time( $user_id ) {
|
||||
$lockout_time = get_user_meta( $user_id, 'kivicare_lockout_time', true );
|
||||
|
||||
if ( ! $lockout_time ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$remaining = $lockout_time - time();
|
||||
return max( 0, $remaining );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session statistics for user
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @return array Session statistics
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_user_session_stats( $user_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$stats = array(
|
||||
'active_sessions' => 0,
|
||||
'total_sessions_today' => 0,
|
||||
'total_sessions_this_month' => 0,
|
||||
'last_login' => null,
|
||||
'last_ip' => null,
|
||||
'failed_attempts_today' => 0,
|
||||
'is_locked' => false,
|
||||
'lockout_remaining' => 0
|
||||
);
|
||||
|
||||
// Active sessions
|
||||
$stats['active_sessions'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d AND is_active = 1 AND expires_at > NOW()",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// Sessions today
|
||||
$stats['total_sessions_today'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d AND DATE(created_at) = CURDATE()",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// Sessions this month
|
||||
$stats['total_sessions_this_month'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d AND MONTH(created_at) = MONTH(CURDATE()) AND YEAR(created_at) = YEAR(CURDATE())",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// Last login info
|
||||
$last_session = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT created_at, ip_address FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d ORDER BY created_at DESC LIMIT 1",
|
||||
$user_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( $last_session ) {
|
||||
$stats['last_login'] = $last_session['created_at'];
|
||||
$stats['last_ip'] = $last_session['ip_address'];
|
||||
}
|
||||
|
||||
// Failed attempts today
|
||||
$stats['failed_attempts_today'] = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_failed_logins
|
||||
WHERE user_id = %d AND DATE(attempted_at) = CURDATE()",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// Lockout status
|
||||
$stats['is_locked'] = self::is_account_locked( $user_id );
|
||||
$stats['lockout_remaining'] = self::get_lockout_remaining_time( $user_id );
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor session activity for security
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function monitor_session_activity() {
|
||||
// Only monitor for authenticated API requests
|
||||
if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
if ( ! $user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current session from auth header or meta
|
||||
$session_id = self::get_current_session_id( $user_id );
|
||||
|
||||
if ( $session_id ) {
|
||||
// Update session activity
|
||||
self::update_session_activity( $session_id );
|
||||
|
||||
// Check for suspicious activity
|
||||
self::detect_suspicious_activity( $user_id, $session_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current session ID for user
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @return string|null Session ID or null
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_current_session_id( $user_id ) {
|
||||
// Try to get from JWT token first (would need to be added to JWT payload)
|
||||
// For now, get from user meta (set during authentication)
|
||||
return get_user_meta( $user_id, 'kivicare_current_session', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect suspicious session activity
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @param string $session_id Session ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function detect_suspicious_activity( $user_id, $session_id ) {
|
||||
$session = self::get_session( $session_id );
|
||||
if ( ! $session ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$current_ip = self::get_client_ip();
|
||||
$session_ip = $session['ip_address'];
|
||||
|
||||
// Check for IP address change
|
||||
if ( $current_ip !== $session_ip ) {
|
||||
self::log_security_event( $user_id, 'ip_change', array(
|
||||
'session_id' => $session_id,
|
||||
'original_ip' => $session_ip,
|
||||
'new_ip' => $current_ip
|
||||
) );
|
||||
|
||||
// Optionally expire session or require re-authentication
|
||||
if ( apply_filters( 'kivicare_expire_on_ip_change', false ) ) {
|
||||
self::expire_session( $session_id );
|
||||
}
|
||||
}
|
||||
|
||||
// Check for unusual activity patterns
|
||||
self::check_activity_patterns( $user_id, $session_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for unusual activity patterns
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @param string $session_id Session ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function check_activity_patterns( $user_id, $session_id ) {
|
||||
// This could be extended to check:
|
||||
// - Rapid API calls (possible bot activity)
|
||||
// - Access to unusual resources
|
||||
// - Concurrent sessions from different locations
|
||||
// - Time-based anomalies (access at unusual hours)
|
||||
|
||||
do_action( 'kivicare_check_activity_patterns', $user_id, $session_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user authentication event
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @param array $context Authentication context
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function on_user_authenticated( $user_id, $context ) {
|
||||
$ip_address = $context['ip_address'] ?? self::get_client_ip();
|
||||
$user_agent = $context['user_agent'] ?? ( $_SERVER['HTTP_USER_AGENT'] ?? '' );
|
||||
|
||||
// Create new session
|
||||
$session_id = self::create_session( $user_id, $ip_address, $user_agent );
|
||||
|
||||
// Log successful authentication
|
||||
self::log_security_event( $user_id, 'login', array(
|
||||
'session_id' => $session_id,
|
||||
'ip_address' => $ip_address,
|
||||
'user_agent' => $user_agent
|
||||
) );
|
||||
|
||||
// Clear failed login attempts
|
||||
delete_user_meta( $user_id, 'kivicare_failed_attempts' );
|
||||
delete_user_meta( $user_id, 'kivicare_lockout_time' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user logout event
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function on_user_logout( $user_id ) {
|
||||
$session_id = get_user_meta( $user_id, 'kivicare_current_session', true );
|
||||
|
||||
if ( $session_id ) {
|
||||
self::expire_session( $session_id );
|
||||
}
|
||||
|
||||
// Log logout
|
||||
self::log_security_event( $user_id, 'logout', array(
|
||||
'session_id' => $session_id
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle failed login event
|
||||
*
|
||||
* @param array $context Failed login context
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function on_failed_login( $context ) {
|
||||
$user_id = $context['user_id'] ?? null;
|
||||
$username = $context['username'] ?? '';
|
||||
$ip_address = $context['ip_address'] ?? self::get_client_ip();
|
||||
|
||||
// Log failed attempt
|
||||
self::log_failed_login_attempt( $user_id, $username, $ip_address );
|
||||
|
||||
if ( $user_id ) {
|
||||
// Increment failed attempts counter
|
||||
$failed_attempts = (int) get_user_meta( $user_id, 'kivicare_failed_attempts', true ) + 1;
|
||||
update_user_meta( $user_id, 'kivicare_failed_attempts', $failed_attempts );
|
||||
|
||||
// Check if account should be locked
|
||||
if ( $failed_attempts >= self::$max_failed_attempts ) {
|
||||
self::lock_account( $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
// Log security event
|
||||
self::log_security_event( $user_id, 'failed_login', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock user account
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function lock_account( $user_id ) {
|
||||
$lockout_time = time() + self::$lockout_duration;
|
||||
update_user_meta( $user_id, 'kivicare_lockout_time', $lockout_time );
|
||||
|
||||
// Expire all active sessions
|
||||
self::expire_user_sessions( $user_id );
|
||||
|
||||
// Log security event
|
||||
self::log_security_event( $user_id, 'account_locked', array(
|
||||
'lockout_duration' => self::$lockout_duration,
|
||||
'lockout_until' => date( 'Y-m-d H:i:s', $lockout_time )
|
||||
) );
|
||||
|
||||
// Send notification (could be extended)
|
||||
do_action( 'kivicare_account_locked', $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce concurrent session limit
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function enforce_concurrent_session_limit( $user_id ) {
|
||||
global $wpdb;
|
||||
|
||||
// Get active sessions count
|
||||
$active_sessions = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d AND is_active = 1 AND expires_at > NOW()",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
// If at limit, expire oldest session
|
||||
if ( $active_sessions >= self::$max_concurrent_sessions ) {
|
||||
$oldest_session = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT session_id FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE user_id = %d AND is_active = 1 AND expires_at > NOW()
|
||||
ORDER BY last_activity ASC LIMIT 1",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
if ( $oldest_session ) {
|
||||
self::expire_session( $oldest_session );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store session in database
|
||||
*
|
||||
* @param array $session_data Session data
|
||||
* @return bool Success status
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function store_session( $session_data ) {
|
||||
global $wpdb;
|
||||
|
||||
// Create table if it doesn't exist
|
||||
self::create_sessions_table();
|
||||
|
||||
$result = $wpdb->insert(
|
||||
$wpdb->prefix . 'kivicare_sessions',
|
||||
$session_data,
|
||||
array( '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%d' )
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session from database
|
||||
*
|
||||
* @param string $session_id Session ID
|
||||
* @return array|null Session data or null
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_session( $session_id ) {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kivicare_sessions WHERE session_id = %s",
|
||||
$session_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user session metadata
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @param string $session_id Session ID
|
||||
* @param string $ip_address IP address
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function update_user_session_meta( $user_id, $session_id, $ip_address ) {
|
||||
update_user_meta( $user_id, 'kivicare_current_session', $session_id );
|
||||
update_user_meta( $user_id, 'kivicare_session_ip', $ip_address );
|
||||
update_user_meta( $user_id, 'kivicare_last_activity', current_time( 'mysql' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log failed login attempt
|
||||
*
|
||||
* @param int|null $user_id User ID (if found)
|
||||
* @param string $username Username attempted
|
||||
* @param string $ip_address IP address
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function log_failed_login_attempt( $user_id, $username, $ip_address ) {
|
||||
global $wpdb;
|
||||
|
||||
// Create table if it doesn't exist
|
||||
self::create_failed_logins_table();
|
||||
|
||||
$wpdb->insert(
|
||||
$wpdb->prefix . 'kivicare_failed_logins',
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'username' => $username,
|
||||
'ip_address' => $ip_address,
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||
'attempted_at' => current_time( 'mysql' )
|
||||
),
|
||||
array( '%d', '%s', '%s', '%s', '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log security event
|
||||
*
|
||||
* @param int $user_id User ID
|
||||
* @param string $event Event type
|
||||
* @param array $data Event data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function log_security_event( $user_id, $event, $data = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
// Create table if it doesn't exist
|
||||
self::create_security_log_table();
|
||||
|
||||
$wpdb->insert(
|
||||
$wpdb->prefix . 'kivicare_security_log',
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'event_type' => $event,
|
||||
'event_data' => wp_json_encode( $data ),
|
||||
'ip_address' => self::get_client_ip(),
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||
'created_at' => current_time( 'mysql' )
|
||||
),
|
||||
array( '%d', '%s', '%s', '%s', '%s', '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format session data for API response
|
||||
*
|
||||
* @param array $session_data Raw session data
|
||||
* @return array Formatted session data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function format_session_data( $session_data ) {
|
||||
return array(
|
||||
'session_id' => $session_data['session_id'],
|
||||
'ip_address' => $session_data['ip_address'],
|
||||
'user_agent' => $session_data['user_agent'],
|
||||
'created_at' => $session_data['created_at'],
|
||||
'last_activity' => $session_data['last_activity'],
|
||||
'expires_at' => $session_data['expires_at'],
|
||||
'is_current' => get_user_meta( $session_data['user_id'], 'kivicare_current_session', true ) === $session_data['session_id']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup expired sessions
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function cleanup_expired_sessions() {
|
||||
global $wpdb;
|
||||
|
||||
// Delete expired sessions
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->prefix}kivicare_sessions
|
||||
WHERE expires_at < NOW() OR (is_active = 0 AND ended_at < DATE_SUB(NOW(), INTERVAL 30 DAY))"
|
||||
);
|
||||
|
||||
// Clean up old failed login attempts
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->prefix}kivicare_failed_logins
|
||||
WHERE attempted_at < DATE_SUB(NOW(), INTERVAL 7 DAY)"
|
||||
);
|
||||
|
||||
// Clean up old security log entries (keep 90 days)
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->prefix}kivicare_security_log
|
||||
WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client IP address
|
||||
*
|
||||
* @return string IP address
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_client_ip() {
|
||||
$ip_keys = array(
|
||||
'HTTP_CF_CONNECTING_IP',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_FORWARDED',
|
||||
'HTTP_X_CLUSTER_CLIENT_IP',
|
||||
'HTTP_FORWARDED_FOR',
|
||||
'HTTP_FORWARDED',
|
||||
'REMOTE_ADDR'
|
||||
);
|
||||
|
||||
foreach ( $ip_keys as $key ) {
|
||||
if ( ! empty( $_SERVER[ $key ] ) ) {
|
||||
$ip = $_SERVER[ $key ];
|
||||
if ( strpos( $ip, ',' ) !== false ) {
|
||||
$ip = explode( ',', $ip )[0];
|
||||
}
|
||||
$ip = trim( $ip );
|
||||
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create sessions table
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function create_sessions_table() {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'kivicare_sessions';
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
session_id varchar(36) NOT NULL,
|
||||
user_id bigint(20) unsigned NOT NULL,
|
||||
ip_address varchar(45) NOT NULL,
|
||||
user_agent text NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
last_activity datetime NOT NULL,
|
||||
expires_at datetime NOT NULL,
|
||||
ended_at datetime NULL,
|
||||
is_active tinyint(1) NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY session_id (session_id),
|
||||
KEY user_id (user_id),
|
||||
KEY expires_at (expires_at),
|
||||
KEY is_active (is_active)
|
||||
) {$charset_collate};";
|
||||
|
||||
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
|
||||
dbDelta( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create failed logins table
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function create_failed_logins_table() {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'kivicare_failed_logins';
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NULL,
|
||||
username varchar(60) NOT NULL,
|
||||
ip_address varchar(45) NOT NULL,
|
||||
user_agent text NOT NULL,
|
||||
attempted_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY user_id (user_id),
|
||||
KEY ip_address (ip_address),
|
||||
KEY attempted_at (attempted_at)
|
||||
) {$charset_collate};";
|
||||
|
||||
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
|
||||
dbDelta( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create security log table
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function create_security_log_table() {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'kivicare_security_log';
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NULL,
|
||||
event_type varchar(50) NOT NULL,
|
||||
event_data longtext NULL,
|
||||
ip_address varchar(45) NOT NULL,
|
||||
user_agent text NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY user_id (user_id),
|
||||
KEY event_type (event_type),
|
||||
KEY created_at (created_at)
|
||||
) {$charset_collate};";
|
||||
|
||||
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
|
||||
dbDelta( $sql );
|
||||
}
|
||||
}
|
||||
966
src/includes/services/database/class-appointment-service.php
Normal file
966
src/includes/services/database/class-appointment-service.php
Normal file
@@ -0,0 +1,966 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Appointment Database Service
|
||||
*
|
||||
* Handles advanced appointment data operations and business logic
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Services\Database
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_API\Services\Database;
|
||||
|
||||
use KiviCare_API\Models\Appointment;
|
||||
use KiviCare_API\Services\Permission_Service;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Appointment_Service
|
||||
*
|
||||
* Advanced database service for appointment management with business logic
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Appointment_Service {
|
||||
|
||||
/**
|
||||
* Appointment status constants
|
||||
*/
|
||||
const STATUS_BOOKED = 1;
|
||||
const STATUS_COMPLETED = 2;
|
||||
const STATUS_CANCELLED = 3;
|
||||
const STATUS_NO_SHOW = 4;
|
||||
const STATUS_RESCHEDULED = 5;
|
||||
|
||||
/**
|
||||
* Initialize the service
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function init() {
|
||||
// Hook into WordPress actions
|
||||
add_action( 'kivicare_appointment_created', array( self::class, 'on_appointment_created' ), 10, 2 );
|
||||
add_action( 'kivicare_appointment_updated', array( self::class, 'on_appointment_updated' ), 10, 2 );
|
||||
add_action( 'kivicare_appointment_cancelled', array( self::class, 'on_appointment_cancelled' ), 10, 1 );
|
||||
add_action( 'kivicare_appointment_completed', array( self::class, 'on_appointment_completed' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create appointment with advanced business logic
|
||||
*
|
||||
* @param array $appointment_data Appointment data
|
||||
* @param int $user_id Creating user ID
|
||||
* @return array|WP_Error Appointment data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function create_appointment( $appointment_data, $user_id = null ) {
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_manage_appointments( get_current_user_id(), $appointment_data['clinic_id'] ?? 0 ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'You do not have permission to create appointments',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Enhanced validation
|
||||
$validation = self::validate_appointment_business_rules( $appointment_data );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// Check doctor availability
|
||||
$availability_check = self::check_doctor_availability( $appointment_data );
|
||||
if ( is_wp_error( $availability_check ) ) {
|
||||
return $availability_check;
|
||||
}
|
||||
|
||||
// Add metadata
|
||||
$appointment_data['created_by'] = $user_id ?: get_current_user_id();
|
||||
$appointment_data['created_at'] = current_time( 'mysql' );
|
||||
$appointment_data['status'] = self::STATUS_BOOKED;
|
||||
|
||||
// Generate appointment number if not provided
|
||||
if ( empty( $appointment_data['appointment_number'] ) ) {
|
||||
$appointment_data['appointment_number'] = self::generate_appointment_number();
|
||||
}
|
||||
|
||||
// Calculate end time if not provided
|
||||
if ( empty( $appointment_data['appointment_end_time'] ) ) {
|
||||
$appointment_data['appointment_end_time'] = self::calculate_end_time(
|
||||
$appointment_data['appointment_start_time'],
|
||||
$appointment_data['duration'] ?? 30
|
||||
);
|
||||
}
|
||||
|
||||
// Create appointment
|
||||
$appointment_id = Appointment::create( $appointment_data );
|
||||
|
||||
if ( is_wp_error( $appointment_id ) ) {
|
||||
return $appointment_id;
|
||||
}
|
||||
|
||||
// Post-creation tasks
|
||||
self::setup_appointment_defaults( $appointment_id, $appointment_data );
|
||||
|
||||
// Send notifications
|
||||
self::send_appointment_notifications( $appointment_id, 'created' );
|
||||
|
||||
// Trigger action
|
||||
do_action( 'kivicare_appointment_created', $appointment_id, $appointment_data );
|
||||
|
||||
// Return full appointment data
|
||||
return self::get_appointment_with_metadata( $appointment_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update appointment with business logic
|
||||
*
|
||||
* @param int $appointment_id Appointment ID
|
||||
* @param array $appointment_data Updated data
|
||||
* @return array|WP_Error Updated appointment data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function update_appointment( $appointment_id, $appointment_data ) {
|
||||
// Get current appointment data
|
||||
$current_appointment = Appointment::get_by_id( $appointment_id );
|
||||
if ( ! $current_appointment ) {
|
||||
return new \WP_Error(
|
||||
'appointment_not_found',
|
||||
'Appointment not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_manage_appointments( get_current_user_id(), $current_appointment['clinic_id'] ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'You do not have permission to update this appointment',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Enhanced validation
|
||||
$validation = self::validate_appointment_business_rules( $appointment_data, $appointment_id );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// Check if this is a rescheduling
|
||||
$is_rescheduling = self::is_appointment_rescheduling( $current_appointment, $appointment_data );
|
||||
if ( $is_rescheduling ) {
|
||||
$availability_check = self::check_doctor_availability( $appointment_data, $appointment_id );
|
||||
if ( is_wp_error( $availability_check ) ) {
|
||||
return $availability_check;
|
||||
}
|
||||
}
|
||||
|
||||
// Add update metadata
|
||||
$appointment_data['updated_by'] = get_current_user_id();
|
||||
$appointment_data['updated_at'] = current_time( 'mysql' );
|
||||
|
||||
// Update appointment
|
||||
$result = Appointment::update( $appointment_id, $appointment_data );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Handle status changes
|
||||
self::handle_status_changes( $appointment_id, $current_appointment, $appointment_data );
|
||||
|
||||
// Send notifications if needed
|
||||
if ( $is_rescheduling ) {
|
||||
self::send_appointment_notifications( $appointment_id, 'rescheduled' );
|
||||
}
|
||||
|
||||
// Trigger action
|
||||
do_action( 'kivicare_appointment_updated', $appointment_id, $appointment_data );
|
||||
|
||||
// Return updated appointment data
|
||||
return self::get_appointment_with_metadata( $appointment_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel appointment
|
||||
*
|
||||
* @param int $appointment_id Appointment ID
|
||||
* @param string $reason Cancellation reason
|
||||
* @return array|WP_Error Updated appointment data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function cancel_appointment( $appointment_id, $reason = '' ) {
|
||||
$appointment = Appointment::get_by_id( $appointment_id );
|
||||
if ( ! $appointment ) {
|
||||
return new \WP_Error(
|
||||
'appointment_not_found',
|
||||
'Appointment not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_manage_appointments( get_current_user_id(), $appointment['clinic_id'] ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'You do not have permission to cancel this appointment',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check if appointment can be cancelled
|
||||
if ( $appointment['status'] == self::STATUS_COMPLETED ) {
|
||||
return new \WP_Error(
|
||||
'cannot_cancel_completed',
|
||||
'Cannot cancel a completed appointment',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
if ( $appointment['status'] == self::STATUS_CANCELLED ) {
|
||||
return new \WP_Error(
|
||||
'already_cancelled',
|
||||
'Appointment is already cancelled',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
// Update appointment status
|
||||
$update_data = array(
|
||||
'status' => self::STATUS_CANCELLED,
|
||||
'cancellation_reason' => $reason,
|
||||
'cancelled_by' => get_current_user_id(),
|
||||
'cancelled_at' => current_time( 'mysql' ),
|
||||
'updated_at' => current_time( 'mysql' )
|
||||
);
|
||||
|
||||
$result = Appointment::update( $appointment_id, $update_data );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Send cancellation notifications
|
||||
self::send_appointment_notifications( $appointment_id, 'cancelled' );
|
||||
|
||||
// Trigger action
|
||||
do_action( 'kivicare_appointment_cancelled', $appointment_id );
|
||||
|
||||
return self::get_appointment_with_metadata( $appointment_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete appointment
|
||||
*
|
||||
* @param int $appointment_id Appointment ID
|
||||
* @param array $completion_data Completion data
|
||||
* @return array|WP_Error Updated appointment data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function complete_appointment( $appointment_id, $completion_data = array() ) {
|
||||
$appointment = Appointment::get_by_id( $appointment_id );
|
||||
if ( ! $appointment ) {
|
||||
return new \WP_Error(
|
||||
'appointment_not_found',
|
||||
'Appointment not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_manage_appointments( get_current_user_id(), $appointment['clinic_id'] ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'You do not have permission to complete this appointment',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check if appointment can be completed
|
||||
if ( $appointment['status'] == self::STATUS_CANCELLED ) {
|
||||
return new \WP_Error(
|
||||
'cannot_complete_cancelled',
|
||||
'Cannot complete a cancelled appointment',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
if ( $appointment['status'] == self::STATUS_COMPLETED ) {
|
||||
return new \WP_Error(
|
||||
'already_completed',
|
||||
'Appointment is already completed',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
// Update appointment status
|
||||
$update_data = array_merge( $completion_data, array(
|
||||
'status' => self::STATUS_COMPLETED,
|
||||
'completed_by' => get_current_user_id(),
|
||||
'completed_at' => current_time( 'mysql' ),
|
||||
'updated_at' => current_time( 'mysql' )
|
||||
));
|
||||
|
||||
$result = Appointment::update( $appointment_id, $update_data );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Auto-generate bill if configured
|
||||
if ( get_option( 'kivicare_auto_generate_bills', true ) ) {
|
||||
self::auto_generate_bill( $appointment_id );
|
||||
}
|
||||
|
||||
// Send completion notifications
|
||||
self::send_appointment_notifications( $appointment_id, 'completed' );
|
||||
|
||||
// Trigger action
|
||||
do_action( 'kivicare_appointment_completed', $appointment_id );
|
||||
|
||||
return self::get_appointment_with_metadata( $appointment_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appointment with enhanced metadata
|
||||
*
|
||||
* @param int $appointment_id Appointment ID
|
||||
* @return array|WP_Error Appointment data with metadata or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_appointment_with_metadata( $appointment_id ) {
|
||||
$appointment = Appointment::get_by_id( $appointment_id );
|
||||
|
||||
if ( ! $appointment ) {
|
||||
return new \WP_Error(
|
||||
'appointment_not_found',
|
||||
'Appointment not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_view_appointment( get_current_user_id(), $appointment_id ) ) {
|
||||
return new \WP_Error(
|
||||
'access_denied',
|
||||
'You do not have access to this appointment',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Add enhanced metadata
|
||||
$appointment['patient'] = self::get_appointment_patient( $appointment['patient_id'] );
|
||||
$appointment['doctor'] = self::get_appointment_doctor( $appointment['doctor_id'] );
|
||||
$appointment['clinic'] = self::get_appointment_clinic( $appointment['clinic_id'] );
|
||||
$appointment['service'] = self::get_appointment_service( $appointment['service_id'] ?? null );
|
||||
$appointment['encounters'] = self::get_appointment_encounters( $appointment_id );
|
||||
$appointment['bills'] = self::get_appointment_bills( $appointment_id );
|
||||
$appointment['status_label'] = self::get_status_label( $appointment['status'] );
|
||||
|
||||
return $appointment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search appointments with advanced criteria
|
||||
*
|
||||
* @param array $filters Search filters
|
||||
* @return array Search results
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function search_appointments( $filters = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$accessible_clinic_ids = Permission_Service::get_accessible_clinic_ids( $user_id );
|
||||
|
||||
if ( empty( $accessible_clinic_ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Build search query
|
||||
$where_clauses = array( "a.clinic_id IN (" . implode( ',', $accessible_clinic_ids ) . ")" );
|
||||
$where_values = array();
|
||||
|
||||
// Date range filter
|
||||
if ( ! empty( $filters['start_date'] ) ) {
|
||||
$where_clauses[] = "DATE(a.appointment_start_date) >= %s";
|
||||
$where_values[] = $filters['start_date'];
|
||||
}
|
||||
|
||||
if ( ! empty( $filters['end_date'] ) ) {
|
||||
$where_clauses[] = "DATE(a.appointment_start_date) <= %s";
|
||||
$where_values[] = $filters['end_date'];
|
||||
}
|
||||
|
||||
// Doctor filter
|
||||
if ( ! empty( $filters['doctor_id'] ) ) {
|
||||
$where_clauses[] = "a.doctor_id = %d";
|
||||
$where_values[] = $filters['doctor_id'];
|
||||
}
|
||||
|
||||
// Patient filter
|
||||
if ( ! empty( $filters['patient_id'] ) ) {
|
||||
$where_clauses[] = "a.patient_id = %d";
|
||||
$where_values[] = $filters['patient_id'];
|
||||
}
|
||||
|
||||
// Clinic filter
|
||||
if ( ! empty( $filters['clinic_id'] ) && in_array( $filters['clinic_id'], $accessible_clinic_ids ) ) {
|
||||
$where_clauses[] = "a.clinic_id = %d";
|
||||
$where_values[] = $filters['clinic_id'];
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if ( ! empty( $filters['status'] ) ) {
|
||||
if ( is_array( $filters['status'] ) ) {
|
||||
$status_placeholders = implode( ',', array_fill( 0, count( $filters['status'] ), '%d' ) );
|
||||
$where_clauses[] = "a.status IN ({$status_placeholders})";
|
||||
$where_values = array_merge( $where_values, $filters['status'] );
|
||||
} else {
|
||||
$where_clauses[] = "a.status = %d";
|
||||
$where_values[] = $filters['status'];
|
||||
}
|
||||
}
|
||||
|
||||
// Search term
|
||||
if ( ! empty( $filters['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 a.appointment_number LIKE %s)";
|
||||
$search_term = '%' . $wpdb->esc_like( $filters['search'] ) . '%';
|
||||
$where_values = array_merge( $where_values, array_fill( 0, 5, $search_term ) );
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
// Pagination
|
||||
$limit = isset( $filters['limit'] ) ? (int) $filters['limit'] : 20;
|
||||
$offset = isset( $filters['offset'] ) ? (int) $filters['offset'] : 0;
|
||||
|
||||
$query = "SELECT a.*,
|
||||
p.first_name as patient_first_name, p.last_name as patient_last_name,
|
||||
d.first_name as doctor_first_name, d.last_name as doctor_last_name,
|
||||
c.name as clinic_name
|
||||
FROM {$wpdb->prefix}kc_appointments a
|
||||
LEFT JOIN {$wpdb->prefix}kc_patients p ON a.patient_id = p.id
|
||||
LEFT JOIN {$wpdb->prefix}kc_doctors d ON a.doctor_id = d.id
|
||||
LEFT JOIN {$wpdb->prefix}kc_clinics c ON a.clinic_id = c.id
|
||||
WHERE {$where_sql}
|
||||
ORDER BY a.appointment_start_date DESC, a.appointment_start_time DESC
|
||||
LIMIT {$limit} OFFSET {$offset}";
|
||||
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$results = $wpdb->get_results( $wpdb->prepare( $query, $where_values ), ARRAY_A );
|
||||
} else {
|
||||
$results = $wpdb->get_results( $query, ARRAY_A );
|
||||
}
|
||||
|
||||
// Get total count for pagination
|
||||
$count_query = "SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments a
|
||||
LEFT JOIN {$wpdb->prefix}kc_patients p ON a.patient_id = p.id
|
||||
LEFT JOIN {$wpdb->prefix}kc_doctors d ON a.doctor_id = d.id
|
||||
WHERE {$where_sql}";
|
||||
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$total = (int) $wpdb->get_var( $wpdb->prepare( $count_query, $where_values ) );
|
||||
} else {
|
||||
$total = (int) $wpdb->get_var( $count_query );
|
||||
}
|
||||
|
||||
return array(
|
||||
'appointments' => array_map( function( $appointment ) {
|
||||
$appointment['id'] = (int) $appointment['id'];
|
||||
$appointment['status_label'] = self::get_status_label( $appointment['status'] );
|
||||
return $appointment;
|
||||
}, $results ),
|
||||
'total' => $total,
|
||||
'has_more' => ( $offset + $limit ) < $total
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get doctor availability for date range
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @param string $start_date Start date
|
||||
* @param string $end_date End date
|
||||
* @return array Available slots
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_doctor_availability( $doctor_id, $start_date, $end_date ) {
|
||||
global $wpdb;
|
||||
|
||||
// Get doctor schedule
|
||||
$doctor_schedule = get_option( "kivicare_doctor_{$doctor_id}_schedule", array() );
|
||||
|
||||
if ( empty( $doctor_schedule ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Get existing appointments in date range
|
||||
$existing_appointments = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT appointment_start_date, appointment_start_time, appointment_end_time, duration
|
||||
FROM {$wpdb->prefix}kc_appointments
|
||||
WHERE doctor_id = %d
|
||||
AND appointment_start_date BETWEEN %s AND %s
|
||||
AND status NOT IN (%d, %d)",
|
||||
$doctor_id, $start_date, $end_date,
|
||||
self::STATUS_CANCELLED, self::STATUS_NO_SHOW
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
// Calculate available slots
|
||||
$available_slots = array();
|
||||
$current_date = new \DateTime( $start_date );
|
||||
$end_date_obj = new \DateTime( $end_date );
|
||||
|
||||
while ( $current_date <= $end_date_obj ) {
|
||||
$day_name = strtolower( $current_date->format( 'l' ) );
|
||||
|
||||
if ( isset( $doctor_schedule[$day_name] ) && ! isset( $doctor_schedule[$day_name]['closed'] ) ) {
|
||||
$day_slots = self::calculate_day_slots(
|
||||
$current_date->format( 'Y-m-d' ),
|
||||
$doctor_schedule[$day_name],
|
||||
$existing_appointments
|
||||
);
|
||||
|
||||
if ( ! empty( $day_slots ) ) {
|
||||
$available_slots[$current_date->format( 'Y-m-d' )] = $day_slots;
|
||||
}
|
||||
}
|
||||
|
||||
$current_date->add( new \DateInterval( 'P1D' ) );
|
||||
}
|
||||
|
||||
return $available_slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique appointment number
|
||||
*
|
||||
* @return string Appointment number
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function generate_appointment_number() {
|
||||
global $wpdb;
|
||||
|
||||
$date_prefix = current_time( 'Ymd' );
|
||||
|
||||
// Get the highest existing appointment number for today
|
||||
$max_number = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT MAX(CAST(SUBSTRING(appointment_number, 9) AS UNSIGNED))
|
||||
FROM {$wpdb->prefix}kc_appointments
|
||||
WHERE appointment_number LIKE %s",
|
||||
$date_prefix . '%'
|
||||
)
|
||||
);
|
||||
|
||||
$next_number = ( $max_number ? $max_number + 1 : 1 );
|
||||
|
||||
return $date_prefix . str_pad( $next_number, 4, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate appointment end time
|
||||
*
|
||||
* @param string $start_time Start time
|
||||
* @param int $duration Duration in minutes
|
||||
* @return string End time
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function calculate_end_time( $start_time, $duration ) {
|
||||
$start_datetime = new \DateTime( $start_time );
|
||||
$start_datetime->add( new \DateInterval( "PT{$duration}M" ) );
|
||||
return $start_datetime->format( 'H:i:s' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate appointment business rules
|
||||
*
|
||||
* @param array $appointment_data Appointment data
|
||||
* @param int $appointment_id Appointment ID (for updates)
|
||||
* @return bool|WP_Error True if valid, WP_Error otherwise
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function validate_appointment_business_rules( $appointment_data, $appointment_id = null ) {
|
||||
$errors = array();
|
||||
|
||||
// Validate required fields
|
||||
$required_fields = array( 'patient_id', 'doctor_id', 'clinic_id', 'appointment_start_date', 'appointment_start_time' );
|
||||
|
||||
foreach ( $required_fields as $field ) {
|
||||
if ( empty( $appointment_data[$field] ) ) {
|
||||
$errors[] = "Field {$field} is required";
|
||||
}
|
||||
}
|
||||
|
||||
// Validate appointment date is not in the past
|
||||
if ( ! empty( $appointment_data['appointment_start_date'] ) ) {
|
||||
$appointment_date = new \DateTime( $appointment_data['appointment_start_date'] );
|
||||
$today = new \DateTime();
|
||||
$today->setTime( 0, 0, 0 );
|
||||
|
||||
if ( $appointment_date < $today ) {
|
||||
$errors[] = 'Appointment date cannot be in the past';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate patient exists
|
||||
if ( ! empty( $appointment_data['patient_id'] ) ) {
|
||||
global $wpdb;
|
||||
$patient_exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$wpdb->prefix}kc_patients WHERE id = %d",
|
||||
$appointment_data['patient_id']
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $patient_exists ) {
|
||||
$errors[] = 'Invalid patient ID';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate doctor exists
|
||||
if ( ! empty( $appointment_data['doctor_id'] ) ) {
|
||||
global $wpdb;
|
||||
$doctor_exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$wpdb->prefix}kc_doctors WHERE id = %d",
|
||||
$appointment_data['doctor_id']
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $doctor_exists ) {
|
||||
$errors[] = 'Invalid doctor ID';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate clinic exists
|
||||
if ( ! empty( $appointment_data['clinic_id'] ) ) {
|
||||
global $wpdb;
|
||||
$clinic_exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$wpdb->prefix}kc_clinics WHERE id = %d",
|
||||
$appointment_data['clinic_id']
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $clinic_exists ) {
|
||||
$errors[] = 'Invalid clinic ID';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $errors ) ) {
|
||||
return new \WP_Error(
|
||||
'appointment_business_validation_failed',
|
||||
'Appointment business validation failed',
|
||||
array(
|
||||
'status' => 400,
|
||||
'errors' => $errors
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check doctor availability for appointment slot
|
||||
*
|
||||
* @param array $appointment_data Appointment data
|
||||
* @param int $exclude_id Appointment ID to exclude (for updates)
|
||||
* @return bool|WP_Error True if available, WP_Error otherwise
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function check_doctor_availability( $appointment_data, $exclude_id = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$doctor_id = $appointment_data['doctor_id'];
|
||||
$start_date = $appointment_data['appointment_start_date'];
|
||||
$start_time = $appointment_data['appointment_start_time'];
|
||||
$duration = $appointment_data['duration'] ?? 30;
|
||||
$end_time = self::calculate_end_time( $start_time, $duration );
|
||||
|
||||
// Check for conflicting appointments
|
||||
$conflict_query = "SELECT id FROM {$wpdb->prefix}kc_appointments
|
||||
WHERE doctor_id = %d
|
||||
AND appointment_start_date = %s
|
||||
AND status NOT IN (%d, %d)
|
||||
AND (
|
||||
(appointment_start_time <= %s AND appointment_end_time > %s) OR
|
||||
(appointment_start_time < %s AND appointment_end_time >= %s) OR
|
||||
(appointment_start_time >= %s AND appointment_end_time <= %s)
|
||||
)";
|
||||
|
||||
$conflict_params = array(
|
||||
$doctor_id, $start_date,
|
||||
self::STATUS_CANCELLED, self::STATUS_NO_SHOW,
|
||||
$start_time, $start_time,
|
||||
$end_time, $end_time,
|
||||
$start_time, $end_time
|
||||
);
|
||||
|
||||
if ( $exclude_id ) {
|
||||
$conflict_query .= " AND id != %d";
|
||||
$conflict_params[] = $exclude_id;
|
||||
}
|
||||
|
||||
$conflict = $wpdb->get_var( $wpdb->prepare( $conflict_query, $conflict_params ) );
|
||||
|
||||
if ( $conflict ) {
|
||||
return new \WP_Error(
|
||||
'doctor_not_available',
|
||||
'Doctor is not available at the selected time slot',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check doctor working hours
|
||||
$doctor_schedule = get_option( "kivicare_doctor_{$doctor_id}_schedule", array() );
|
||||
$day_name = strtolower( date( 'l', strtotime( $start_date ) ) );
|
||||
|
||||
if ( empty( $doctor_schedule[$day_name] ) || isset( $doctor_schedule[$day_name]['closed'] ) ) {
|
||||
return new \WP_Error(
|
||||
'doctor_not_working',
|
||||
'Doctor is not working on this day',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
$working_hours = $doctor_schedule[$day_name];
|
||||
if ( $start_time < $working_hours['start_time'] || $end_time > $working_hours['end_time'] ) {
|
||||
return new \WP_Error(
|
||||
'outside_working_hours',
|
||||
'Appointment time is outside doctor working hours',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check break time if exists
|
||||
if ( isset( $working_hours['break_start'] ) && isset( $working_hours['break_end'] ) ) {
|
||||
$break_start = $working_hours['break_start'];
|
||||
$break_end = $working_hours['break_end'];
|
||||
|
||||
if ( ( $start_time >= $break_start && $start_time < $break_end ) ||
|
||||
( $end_time > $break_start && $end_time <= $break_end ) ||
|
||||
( $start_time <= $break_start && $end_time >= $break_end ) ) {
|
||||
return new \WP_Error(
|
||||
'during_break_time',
|
||||
'Appointment time conflicts with doctor break time',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional helper methods for appointment management
|
||||
*/
|
||||
|
||||
private static function is_appointment_rescheduling( $current_appointment, $new_data ) {
|
||||
return ( isset( $new_data['appointment_start_date'] ) && $new_data['appointment_start_date'] != $current_appointment['appointment_start_date'] ) ||
|
||||
( isset( $new_data['appointment_start_time'] ) && $new_data['appointment_start_time'] != $current_appointment['appointment_start_time'] ) ||
|
||||
( isset( $new_data['doctor_id'] ) && $new_data['doctor_id'] != $current_appointment['doctor_id'] );
|
||||
}
|
||||
|
||||
private static function handle_status_changes( $appointment_id, $current_appointment, $new_data ) {
|
||||
if ( isset( $new_data['status'] ) && $new_data['status'] != $current_appointment['status'] ) {
|
||||
$status_change = array(
|
||||
'appointment_id' => $appointment_id,
|
||||
'from_status' => $current_appointment['status'],
|
||||
'to_status' => $new_data['status'],
|
||||
'changed_by' => get_current_user_id(),
|
||||
'changed_at' => current_time( 'mysql' )
|
||||
);
|
||||
|
||||
do_action( 'kivicare_appointment_status_changed', $status_change );
|
||||
}
|
||||
}
|
||||
|
||||
private static function setup_appointment_defaults( $appointment_id, $appointment_data ) {
|
||||
// Setup any default values or related data
|
||||
update_option( "kivicare_appointment_{$appointment_id}_created", current_time( 'mysql' ) );
|
||||
}
|
||||
|
||||
private static function send_appointment_notifications( $appointment_id, $type ) {
|
||||
// Send notifications to patient, doctor, etc.
|
||||
do_action( "kivicare_send_appointment_{$type}_notification", $appointment_id );
|
||||
}
|
||||
|
||||
private static function auto_generate_bill( $appointment_id ) {
|
||||
// Auto-generate bill for completed appointment
|
||||
do_action( 'kivicare_auto_generate_bill', $appointment_id );
|
||||
}
|
||||
|
||||
private static function get_appointment_patient( $patient_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT id, first_name, last_name, user_email, contact_no FROM {$wpdb->prefix}kc_patients WHERE id = %d",
|
||||
$patient_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_appointment_doctor( $doctor_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT id, first_name, last_name, user_email, mobile_number, specialties FROM {$wpdb->prefix}kc_doctors WHERE id = %d",
|
||||
$doctor_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_appointment_clinic( $clinic_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT id, name, address, city, telephone_no FROM {$wpdb->prefix}kc_clinics WHERE id = %d",
|
||||
$clinic_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_appointment_service( $service_id ) {
|
||||
if ( ! $service_id ) return null;
|
||||
|
||||
global $wpdb;
|
||||
return $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT id, name, price, duration FROM {$wpdb->prefix}kc_services WHERE id = %d",
|
||||
$service_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_appointment_encounters( $appointment_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kc_encounters WHERE appointment_id = %d ORDER BY encounter_date DESC",
|
||||
$appointment_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_appointment_bills( $appointment_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kc_bills WHERE appointment_id = %d ORDER BY created_at DESC",
|
||||
$appointment_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_status_label( $status ) {
|
||||
$labels = array(
|
||||
self::STATUS_BOOKED => 'Booked',
|
||||
self::STATUS_COMPLETED => 'Completed',
|
||||
self::STATUS_CANCELLED => 'Cancelled',
|
||||
self::STATUS_NO_SHOW => 'No Show',
|
||||
self::STATUS_RESCHEDULED => 'Rescheduled'
|
||||
);
|
||||
|
||||
return $labels[$status] ?? 'Unknown';
|
||||
}
|
||||
|
||||
private static function calculate_day_slots( $date, $schedule, $existing_appointments ) {
|
||||
$slots = array();
|
||||
$slot_duration = 30; // minutes
|
||||
|
||||
$start_time = new \DateTime( $date . ' ' . $schedule['start_time'] );
|
||||
$end_time = new \DateTime( $date . ' ' . $schedule['end_time'] );
|
||||
|
||||
// Handle break time
|
||||
$break_start = isset( $schedule['break_start'] ) ? new \DateTime( $date . ' ' . $schedule['break_start'] ) : null;
|
||||
$break_end = isset( $schedule['break_end'] ) ? new \DateTime( $date . ' ' . $schedule['break_end'] ) : null;
|
||||
|
||||
$current_time = clone $start_time;
|
||||
|
||||
while ( $current_time < $end_time ) {
|
||||
$slot_end = clone $current_time;
|
||||
$slot_end->add( new \DateInterval( "PT{$slot_duration}M" ) );
|
||||
|
||||
// Skip if in break time
|
||||
if ( $break_start && $break_end &&
|
||||
( $current_time >= $break_start && $current_time < $break_end ) ) {
|
||||
$current_time = clone $break_end;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if slot is available
|
||||
$is_available = true;
|
||||
foreach ( $existing_appointments as $appointment ) {
|
||||
if ( $appointment['appointment_start_date'] == $date ) {
|
||||
$app_start = new \DateTime( $date . ' ' . $appointment['appointment_start_time'] );
|
||||
$app_end = new \DateTime( $date . ' ' . $appointment['appointment_end_time'] );
|
||||
|
||||
if ( ( $current_time >= $app_start && $current_time < $app_end ) ||
|
||||
( $slot_end > $app_start && $slot_end <= $app_end ) ||
|
||||
( $current_time <= $app_start && $slot_end >= $app_end ) ) {
|
||||
$is_available = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $is_available ) {
|
||||
$slots[] = array(
|
||||
'start_time' => $current_time->format( 'H:i:s' ),
|
||||
'end_time' => $slot_end->format( 'H:i:s' ),
|
||||
'duration' => $slot_duration
|
||||
);
|
||||
}
|
||||
|
||||
$current_time->add( new \DateInterval( "PT{$slot_duration}M" ) );
|
||||
}
|
||||
|
||||
return $slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handlers
|
||||
*/
|
||||
public static function on_appointment_created( $appointment_id, $appointment_data ) {
|
||||
error_log( "KiviCare: New appointment created - ID: {$appointment_id}, Patient: " . ( $appointment_data['patient_id'] ?? 'Unknown' ) );
|
||||
}
|
||||
|
||||
public static function on_appointment_updated( $appointment_id, $appointment_data ) {
|
||||
error_log( "KiviCare: Appointment updated - ID: {$appointment_id}" );
|
||||
wp_cache_delete( "appointment_{$appointment_id}", 'kivicare' );
|
||||
}
|
||||
|
||||
public static function on_appointment_cancelled( $appointment_id ) {
|
||||
error_log( "KiviCare: Appointment cancelled - ID: {$appointment_id}" );
|
||||
wp_cache_delete( "appointment_{$appointment_id}", 'kivicare' );
|
||||
}
|
||||
|
||||
public static function on_appointment_completed( $appointment_id ) {
|
||||
error_log( "KiviCare: Appointment completed - ID: {$appointment_id}" );
|
||||
wp_cache_delete( "appointment_{$appointment_id}", 'kivicare' );
|
||||
}
|
||||
}
|
||||
1049
src/includes/services/database/class-bill-service.php
Normal file
1049
src/includes/services/database/class-bill-service.php
Normal file
File diff suppressed because it is too large
Load Diff
810
src/includes/services/database/class-clinic-service.php
Normal file
810
src/includes/services/database/class-clinic-service.php
Normal file
@@ -0,0 +1,810 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Clinic Database Service
|
||||
*
|
||||
* Handles advanced clinic data operations and business logic
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Services\Database
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_API\Services\Database;
|
||||
|
||||
use KiviCare_API\Models\Clinic;
|
||||
use KiviCare_API\Services\Permission_Service;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Clinic_Service
|
||||
*
|
||||
* Advanced database service for clinic management with business logic
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Clinic_Service {
|
||||
|
||||
/**
|
||||
* Initialize the service
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function init() {
|
||||
// Hook into WordPress actions
|
||||
add_action( 'kivicare_clinic_created', array( self::class, 'on_clinic_created' ), 10, 2 );
|
||||
add_action( 'kivicare_clinic_updated', array( self::class, 'on_clinic_updated' ), 10, 2 );
|
||||
add_action( 'kivicare_clinic_deleted', array( self::class, 'on_clinic_deleted' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create clinic with advanced business logic
|
||||
*
|
||||
* @param array $clinic_data Clinic data
|
||||
* @param int $user_id Creating user ID
|
||||
* @return array|WP_Error Clinic data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function create_clinic( $clinic_data, $user_id = null ) {
|
||||
// Permission check
|
||||
if ( ! Permission_Service::current_user_can( 'manage_clinics' ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'You do not have permission to create clinics',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Enhanced validation
|
||||
$validation = self::validate_clinic_business_rules( $clinic_data );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// Add metadata
|
||||
$clinic_data['created_by'] = $user_id ?: get_current_user_id();
|
||||
$clinic_data['created_at'] = current_time( 'mysql' );
|
||||
|
||||
// Create clinic
|
||||
$clinic_id = Clinic::create( $clinic_data );
|
||||
|
||||
if ( is_wp_error( $clinic_id ) ) {
|
||||
return $clinic_id;
|
||||
}
|
||||
|
||||
// Post-creation tasks
|
||||
self::setup_clinic_defaults( $clinic_id, $clinic_data );
|
||||
|
||||
// Trigger action
|
||||
do_action( 'kivicare_clinic_created', $clinic_id, $clinic_data );
|
||||
|
||||
// Return full clinic data
|
||||
return self::get_clinic_with_metadata( $clinic_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update clinic with business logic
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @param array $clinic_data Updated data
|
||||
* @return array|WP_Error Updated clinic data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function update_clinic( $clinic_id, $clinic_data ) {
|
||||
// Permission check
|
||||
if ( ! Permission_Service::current_user_can( 'manage_clinics' ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'You do not have permission to update clinics',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check if clinic exists
|
||||
if ( ! Clinic::exists( $clinic_id ) ) {
|
||||
return new \WP_Error(
|
||||
'clinic_not_found',
|
||||
'Clinic not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Get current data for comparison
|
||||
$current_data = Clinic::get_by_id( $clinic_id );
|
||||
|
||||
// Enhanced validation
|
||||
$validation = self::validate_clinic_business_rules( $clinic_data, $clinic_id );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// Add update metadata
|
||||
$clinic_data['updated_by'] = get_current_user_id();
|
||||
$clinic_data['updated_at'] = current_time( 'mysql' );
|
||||
|
||||
// Update clinic
|
||||
$result = Clinic::update( $clinic_id, $clinic_data );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Handle specialty changes
|
||||
self::handle_specialty_changes( $clinic_id, $current_data, $clinic_data );
|
||||
|
||||
// Trigger action
|
||||
do_action( 'kivicare_clinic_updated', $clinic_id, $clinic_data );
|
||||
|
||||
// Return updated clinic data
|
||||
return self::get_clinic_with_metadata( $clinic_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinic with enhanced metadata
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return array|WP_Error Clinic data with metadata or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_clinic_with_metadata( $clinic_id ) {
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_access_clinic( get_current_user_id(), $clinic_id ) ) {
|
||||
return new \WP_Error(
|
||||
'access_denied',
|
||||
'You do not have access to this clinic',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
$clinic = Clinic::get_by_id( $clinic_id );
|
||||
|
||||
if ( ! $clinic ) {
|
||||
return new \WP_Error(
|
||||
'clinic_not_found',
|
||||
'Clinic not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Add enhanced metadata
|
||||
$clinic['statistics'] = self::get_clinic_statistics( $clinic_id );
|
||||
$clinic['doctors'] = self::get_clinic_doctors( $clinic_id );
|
||||
$clinic['services'] = self::get_clinic_services( $clinic_id );
|
||||
$clinic['working_hours'] = self::get_clinic_working_hours( $clinic_id );
|
||||
$clinic['contact_info'] = self::get_clinic_contact_info( $clinic_id );
|
||||
|
||||
return $clinic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinics accessible to user with filters
|
||||
*
|
||||
* @param array $args Query arguments
|
||||
* @return array Clinics with metadata
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_accessible_clinics( $args = array() ) {
|
||||
$user_id = get_current_user_id();
|
||||
$accessible_clinic_ids = Permission_Service::get_accessible_clinic_ids( $user_id );
|
||||
|
||||
if ( empty( $accessible_clinic_ids ) ) {
|
||||
return array(
|
||||
'clinics' => array(),
|
||||
'total' => 0,
|
||||
'has_more' => false
|
||||
);
|
||||
}
|
||||
|
||||
// Add clinic filter to args
|
||||
$args['clinic_ids'] = $accessible_clinic_ids;
|
||||
|
||||
$defaults = array(
|
||||
'limit' => 20,
|
||||
'offset' => 0,
|
||||
'status' => 1,
|
||||
'include_statistics' => false,
|
||||
'include_doctors' => false,
|
||||
'include_services' => false
|
||||
);
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
// Get clinics with custom query for better performance
|
||||
$clinics = self::query_clinics_with_filters( $args );
|
||||
|
||||
// Add metadata if requested
|
||||
if ( $args['include_statistics'] || $args['include_doctors'] || $args['include_services'] ) {
|
||||
foreach ( $clinics as &$clinic ) {
|
||||
if ( $args['include_statistics'] ) {
|
||||
$clinic['statistics'] = self::get_clinic_statistics( $clinic['id'] );
|
||||
}
|
||||
if ( $args['include_doctors'] ) {
|
||||
$clinic['doctors'] = self::get_clinic_doctors( $clinic['id'] );
|
||||
}
|
||||
if ( $args['include_services'] ) {
|
||||
$clinic['services'] = self::get_clinic_services( $clinic['id'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get total count for pagination
|
||||
$total = self::count_clinics_with_filters( $args );
|
||||
|
||||
return array(
|
||||
'clinics' => $clinics,
|
||||
'total' => $total,
|
||||
'has_more' => ( $args['offset'] + $args['limit'] ) < $total
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search clinics with advanced criteria
|
||||
*
|
||||
* @param string $search_term Search term
|
||||
* @param array $filters Additional filters
|
||||
* @return array Search results
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function search_clinics( $search_term, $filters = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$accessible_clinic_ids = Permission_Service::get_accessible_clinic_ids( $user_id );
|
||||
|
||||
if ( empty( $accessible_clinic_ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Build search query
|
||||
$where_clauses = array( "c.id IN (" . implode( ',', $accessible_clinic_ids ) . ")" );
|
||||
$where_values = array();
|
||||
|
||||
// Search term
|
||||
if ( ! empty( $search_term ) ) {
|
||||
$where_clauses[] = "(c.name LIKE %s OR c.address LIKE %s OR c.city LIKE %s OR c.specialties LIKE %s)";
|
||||
$search_term = '%' . $wpdb->esc_like( $search_term ) . '%';
|
||||
$where_values = array_merge( $where_values, array_fill( 0, 4, $search_term ) );
|
||||
}
|
||||
|
||||
// Location filter
|
||||
if ( ! empty( $filters['city'] ) ) {
|
||||
$where_clauses[] = "c.city = %s";
|
||||
$where_values[] = $filters['city'];
|
||||
}
|
||||
|
||||
if ( ! empty( $filters['state'] ) ) {
|
||||
$where_clauses[] = "c.state = %s";
|
||||
$where_values[] = $filters['state'];
|
||||
}
|
||||
|
||||
// Specialty filter
|
||||
if ( ! empty( $filters['specialty'] ) ) {
|
||||
$where_clauses[] = "c.specialties LIKE %s";
|
||||
$where_values[] = '%' . $wpdb->esc_like( $filters['specialty'] ) . '%';
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
$query = "SELECT c.*,
|
||||
COUNT(DISTINCT dcm.doctor_id) as doctor_count,
|
||||
COUNT(DISTINCT pcm.patient_id) as patient_count
|
||||
FROM {$wpdb->prefix}kc_clinics c
|
||||
LEFT JOIN {$wpdb->prefix}kc_doctor_clinic_mappings dcm ON c.id = dcm.clinic_id
|
||||
LEFT JOIN {$wpdb->prefix}kc_patient_clinic_mappings pcm ON c.id = pcm.clinic_id
|
||||
WHERE {$where_sql}
|
||||
GROUP BY c.id
|
||||
ORDER BY c.name ASC
|
||||
LIMIT 20";
|
||||
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$results = $wpdb->get_results( $wpdb->prepare( $query, $where_values ), ARRAY_A );
|
||||
} else {
|
||||
$results = $wpdb->get_results( $query, ARRAY_A );
|
||||
}
|
||||
|
||||
return array_map( function( $clinic ) {
|
||||
$clinic['id'] = (int) $clinic['id'];
|
||||
$clinic['doctor_count'] = (int) $clinic['doctor_count'];
|
||||
$clinic['patient_count'] = (int) $clinic['patient_count'];
|
||||
return $clinic;
|
||||
}, $results );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinic dashboard data
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return array|WP_Error Dashboard data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_clinic_dashboard( $clinic_id ) {
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_access_clinic( get_current_user_id(), $clinic_id ) ) {
|
||||
return new \WP_Error(
|
||||
'access_denied',
|
||||
'You do not have access to this clinic dashboard',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
$dashboard = array();
|
||||
|
||||
// Basic clinic info
|
||||
$dashboard['clinic'] = Clinic::get_by_id( $clinic_id );
|
||||
|
||||
if ( ! $dashboard['clinic'] ) {
|
||||
return new \WP_Error(
|
||||
'clinic_not_found',
|
||||
'Clinic not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Statistics
|
||||
$dashboard['statistics'] = self::get_comprehensive_statistics( $clinic_id );
|
||||
|
||||
// Recent activity
|
||||
$dashboard['recent_appointments'] = self::get_recent_appointments( $clinic_id, 10 );
|
||||
$dashboard['recent_patients'] = self::get_recent_patients( $clinic_id, 10 );
|
||||
|
||||
// Performance metrics
|
||||
$dashboard['performance'] = self::get_performance_metrics( $clinic_id );
|
||||
|
||||
// Alerts and notifications
|
||||
$dashboard['alerts'] = self::get_clinic_alerts( $clinic_id );
|
||||
|
||||
return $dashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinic performance metrics
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return array Performance metrics
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_performance_metrics( $clinic_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$metrics = array();
|
||||
|
||||
// Appointment completion rate (last 30 days)
|
||||
$completion_data = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT
|
||||
COUNT(*) as total_appointments,
|
||||
SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as completed_appointments,
|
||||
SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) as cancelled_appointments,
|
||||
SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) as no_show_appointments
|
||||
FROM {$wpdb->prefix}kc_appointments
|
||||
WHERE clinic_id = %d
|
||||
AND appointment_start_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)",
|
||||
$clinic_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( $completion_data && $completion_data['total_appointments'] > 0 ) {
|
||||
$total = (int) $completion_data['total_appointments'];
|
||||
$metrics['completion_rate'] = round( ( (int) $completion_data['completed_appointments'] / $total ) * 100, 1 );
|
||||
$metrics['cancellation_rate'] = round( ( (int) $completion_data['cancelled_appointments'] / $total ) * 100, 1 );
|
||||
$metrics['no_show_rate'] = round( ( (int) $completion_data['no_show_appointments'] / $total ) * 100, 1 );
|
||||
} else {
|
||||
$metrics['completion_rate'] = 0;
|
||||
$metrics['cancellation_rate'] = 0;
|
||||
$metrics['no_show_rate'] = 0;
|
||||
}
|
||||
|
||||
// Average patient wait time (would require additional tracking)
|
||||
$metrics['avg_wait_time'] = 0; // Placeholder
|
||||
|
||||
// Revenue trend (last 3 months)
|
||||
$metrics['revenue_trend'] = self::get_revenue_trend( $clinic_id, 3 );
|
||||
|
||||
// Utilization rate (appointments vs. available slots)
|
||||
$metrics['utilization_rate'] = self::calculate_utilization_rate( $clinic_id );
|
||||
|
||||
return $metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate clinic business rules
|
||||
*
|
||||
* @param array $clinic_data Clinic data
|
||||
* @param int $clinic_id Clinic ID (for updates)
|
||||
* @return bool|WP_Error True if valid, WP_Error otherwise
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function validate_clinic_business_rules( $clinic_data, $clinic_id = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$errors = array();
|
||||
|
||||
// Check for duplicate clinic name in same city
|
||||
if ( ! empty( $clinic_data['name'] ) && ! empty( $clinic_data['city'] ) ) {
|
||||
$existing_query = "SELECT id FROM {$wpdb->prefix}kc_clinics WHERE name = %s AND city = %s";
|
||||
$query_params = array( $clinic_data['name'], $clinic_data['city'] );
|
||||
|
||||
if ( $clinic_id ) {
|
||||
$existing_query .= " AND id != %d";
|
||||
$query_params[] = $clinic_id;
|
||||
}
|
||||
|
||||
$existing_clinic = $wpdb->get_var( $wpdb->prepare( $existing_query, $query_params ) );
|
||||
|
||||
if ( $existing_clinic ) {
|
||||
$errors[] = 'A clinic with this name already exists in the same city';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate contact information format
|
||||
if ( ! empty( $clinic_data['telephone_no'] ) ) {
|
||||
if ( ! preg_match( '/^[+]?[0-9\s\-\(\)]{7,20}$/', $clinic_data['telephone_no'] ) ) {
|
||||
$errors[] = 'Invalid telephone number format';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate specialties if provided
|
||||
if ( ! empty( $clinic_data['specialties'] ) ) {
|
||||
$specialties = is_array( $clinic_data['specialties'] ) ?
|
||||
$clinic_data['specialties'] :
|
||||
json_decode( $clinic_data['specialties'], true );
|
||||
|
||||
if ( is_array( $specialties ) ) {
|
||||
$valid_specialties = self::get_valid_specialties();
|
||||
foreach ( $specialties as $specialty ) {
|
||||
if ( ! in_array( $specialty, $valid_specialties ) ) {
|
||||
$errors[] = "Invalid specialty: {$specialty}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate clinic admin if provided
|
||||
if ( ! empty( $clinic_data['clinic_admin_id'] ) ) {
|
||||
$admin_user = get_user_by( 'id', $clinic_data['clinic_admin_id'] );
|
||||
if ( ! $admin_user || ! in_array( 'kivicare_doctor', $admin_user->roles ) ) {
|
||||
$errors[] = 'Clinic admin must be a valid doctor user';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $errors ) ) {
|
||||
return new \WP_Error(
|
||||
'clinic_business_validation_failed',
|
||||
'Clinic business validation failed',
|
||||
array(
|
||||
'status' => 400,
|
||||
'errors' => $errors
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup clinic defaults after creation
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @param array $clinic_data Clinic data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function setup_clinic_defaults( $clinic_id, $clinic_data ) {
|
||||
// Create default services
|
||||
self::create_default_services( $clinic_id );
|
||||
|
||||
// Setup default working hours
|
||||
self::setup_default_working_hours( $clinic_id );
|
||||
|
||||
// Create default appointment slots
|
||||
self::setup_default_appointment_slots( $clinic_id );
|
||||
|
||||
// Initialize clinic settings
|
||||
self::initialize_clinic_settings( $clinic_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default services for new clinic
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function create_default_services( $clinic_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$default_services = array(
|
||||
array( 'name' => 'General Consultation', 'type' => 'consultation', 'price' => 50.00, 'duration' => 30 ),
|
||||
array( 'name' => 'Follow-up Visit', 'type' => 'consultation', 'price' => 30.00, 'duration' => 15 ),
|
||||
array( 'name' => 'Health Checkup', 'type' => 'checkup', 'price' => 80.00, 'duration' => 45 )
|
||||
);
|
||||
|
||||
foreach ( $default_services as $service ) {
|
||||
$wpdb->insert(
|
||||
$wpdb->prefix . 'kc_services',
|
||||
array_merge( $service, array(
|
||||
'clinic_id' => $clinic_id,
|
||||
'status' => 1,
|
||||
'created_at' => current_time( 'mysql' )
|
||||
) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup default working hours
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function setup_default_working_hours( $clinic_id ) {
|
||||
$default_hours = array(
|
||||
'monday' => array( 'start_time' => '09:00', 'end_time' => '17:00' ),
|
||||
'tuesday' => array( 'start_time' => '09:00', 'end_time' => '17:00' ),
|
||||
'wednesday' => array( 'start_time' => '09:00', 'end_time' => '17:00' ),
|
||||
'thursday' => array( 'start_time' => '09:00', 'end_time' => '17:00' ),
|
||||
'friday' => array( 'start_time' => '09:00', 'end_time' => '17:00' ),
|
||||
'saturday' => array( 'start_time' => '09:00', 'end_time' => '13:00' ),
|
||||
'sunday' => array( 'closed' => true )
|
||||
);
|
||||
|
||||
update_option( "kivicare_clinic_{$clinic_id}_working_hours", $default_hours );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinic statistics
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return array Statistics
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_clinic_statistics( $clinic_id ) {
|
||||
return Clinic::get_statistics( $clinic_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinic doctors
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return array Doctors
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_clinic_doctors( $clinic_id ) {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT u.ID, u.display_name, u.user_email,
|
||||
COUNT(a.id) as total_appointments
|
||||
FROM {$wpdb->prefix}kc_doctor_clinic_mappings dcm
|
||||
JOIN {$wpdb->prefix}users u ON dcm.doctor_id = u.ID
|
||||
LEFT JOIN {$wpdb->prefix}kc_appointments a ON u.ID = a.doctor_id AND a.clinic_id = %d
|
||||
WHERE dcm.clinic_id = %d
|
||||
GROUP BY u.ID
|
||||
ORDER BY u.display_name",
|
||||
$clinic_id, $clinic_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinic services
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return array Services
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_clinic_services( $clinic_id ) {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kc_services WHERE clinic_id = %d AND status = 1 ORDER BY name",
|
||||
$clinic_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinic working hours
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return array Working hours
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_clinic_working_hours( $clinic_id ) {
|
||||
return get_option( "kivicare_clinic_{$clinic_id}_working_hours", array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clinic contact information
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return array Contact info
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_clinic_contact_info( $clinic_id ) {
|
||||
$clinic = Clinic::get_by_id( $clinic_id );
|
||||
|
||||
if ( ! $clinic ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array(
|
||||
'email' => $clinic['email'],
|
||||
'telephone' => $clinic['telephone_no'],
|
||||
'address' => $clinic['address'],
|
||||
'city' => $clinic['city'],
|
||||
'state' => $clinic['state'],
|
||||
'country' => $clinic['country'],
|
||||
'postal_code' => $clinic['postal_code']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get valid medical specialties
|
||||
*
|
||||
* @return array Valid specialties
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_valid_specialties() {
|
||||
return array(
|
||||
'general_medicine', 'cardiology', 'dermatology', 'endocrinology',
|
||||
'gastroenterology', 'gynecology', 'neurology', 'oncology',
|
||||
'ophthalmology', 'orthopedics', 'otolaryngology', 'pediatrics',
|
||||
'psychiatry', 'pulmonology', 'radiology', 'urology'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query clinics with custom filters
|
||||
*
|
||||
* @param array $args Query arguments
|
||||
* @return array Clinics
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function query_clinics_with_filters( $args ) {
|
||||
global $wpdb;
|
||||
|
||||
$where_clauses = array( '1=1' );
|
||||
$where_values = array();
|
||||
|
||||
if ( ! empty( $args['clinic_ids'] ) ) {
|
||||
$placeholders = implode( ',', array_fill( 0, count( $args['clinic_ids'] ), '%d' ) );
|
||||
$where_clauses[] = "id IN ({$placeholders})";
|
||||
$where_values = array_merge( $where_values, $args['clinic_ids'] );
|
||||
}
|
||||
|
||||
if ( isset( $args['status'] ) ) {
|
||||
$where_clauses[] = 'status = %d';
|
||||
$where_values[] = $args['status'];
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
$query = "SELECT * FROM {$wpdb->prefix}kc_clinics WHERE {$where_sql} ORDER BY name ASC LIMIT %d OFFSET %d";
|
||||
$where_values[] = $args['limit'];
|
||||
$where_values[] = $args['offset'];
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare( $query, $where_values ), ARRAY_A );
|
||||
}
|
||||
|
||||
/**
|
||||
* Count clinics with filters
|
||||
*
|
||||
* @param array $args Query arguments
|
||||
* @return int Count
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function count_clinics_with_filters( $args ) {
|
||||
global $wpdb;
|
||||
|
||||
$where_clauses = array( '1=1' );
|
||||
$where_values = array();
|
||||
|
||||
if ( ! empty( $args['clinic_ids'] ) ) {
|
||||
$placeholders = implode( ',', array_fill( 0, count( $args['clinic_ids'] ), '%d' ) );
|
||||
$where_clauses[] = "id IN ({$placeholders})";
|
||||
$where_values = array_merge( $where_values, $args['clinic_ids'] );
|
||||
}
|
||||
|
||||
if ( isset( $args['status'] ) ) {
|
||||
$where_clauses[] = 'status = %d';
|
||||
$where_values[] = $args['status'];
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
$query = "SELECT COUNT(*) FROM {$wpdb->prefix}kc_clinics WHERE {$where_sql}";
|
||||
|
||||
if ( ! empty( $where_values ) ) {
|
||||
return (int) $wpdb->get_var( $wpdb->prepare( $query, $where_values ) );
|
||||
}
|
||||
|
||||
return (int) $wpdb->get_var( $query );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle clinic specialty changes
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @param array $current_data Current clinic data
|
||||
* @param array $new_data New clinic data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function handle_specialty_changes( $clinic_id, $current_data, $new_data ) {
|
||||
if ( ! isset( $new_data['specialties'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$old_specialties = is_array( $current_data['specialties'] ) ?
|
||||
$current_data['specialties'] :
|
||||
json_decode( $current_data['specialties'] ?? '[]', true );
|
||||
|
||||
$new_specialties = is_array( $new_data['specialties'] ) ?
|
||||
$new_data['specialties'] :
|
||||
json_decode( $new_data['specialties'], true );
|
||||
|
||||
// Trigger action if specialties changed
|
||||
if ( $old_specialties !== $new_specialties ) {
|
||||
do_action( 'kivicare_clinic_specialties_changed', $clinic_id, $old_specialties, $new_specialties );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler: Clinic created
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @param array $clinic_data Clinic data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function on_clinic_created( $clinic_id, $clinic_data ) {
|
||||
// Log the creation
|
||||
error_log( "KiviCare: New clinic created - ID: {$clinic_id}, Name: " . ( $clinic_data['name'] ?? 'Unknown' ) );
|
||||
|
||||
// Could trigger notifications, integrations, etc.
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler: Clinic updated
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @param array $clinic_data Updated clinic data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function on_clinic_updated( $clinic_id, $clinic_data ) {
|
||||
// Log the update
|
||||
error_log( "KiviCare: Clinic updated - ID: {$clinic_id}" );
|
||||
|
||||
// Clear related caches
|
||||
wp_cache_delete( "clinic_{$clinic_id}", 'kivicare' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler: Clinic deleted
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function on_clinic_deleted( $clinic_id ) {
|
||||
// Clean up related data
|
||||
delete_option( "kivicare_clinic_{$clinic_id}_working_hours" );
|
||||
delete_option( "kivicare_clinic_{$clinic_id}_settings" );
|
||||
|
||||
// Clear caches
|
||||
wp_cache_delete( "clinic_{$clinic_id}", 'kivicare' );
|
||||
|
||||
// Log the deletion
|
||||
error_log( "KiviCare: Clinic deleted - ID: {$clinic_id}" );
|
||||
}
|
||||
}
|
||||
919
src/includes/services/database/class-doctor-service.php
Normal file
919
src/includes/services/database/class-doctor-service.php
Normal file
@@ -0,0 +1,919 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Doctor Database Service
|
||||
*
|
||||
* Handles advanced doctor data operations and business logic
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Services\Database
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_API\Services\Database;
|
||||
|
||||
use KiviCare_API\Models\Doctor;
|
||||
use KiviCare_API\Services\Permission_Service;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Doctor_Service
|
||||
*
|
||||
* Advanced database service for doctor management with business logic
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Doctor_Service {
|
||||
|
||||
/**
|
||||
* Initialize the service
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function init() {
|
||||
// Hook into WordPress actions
|
||||
add_action( 'kivicare_doctor_created', array( self::class, 'on_doctor_created' ), 10, 2 );
|
||||
add_action( 'kivicare_doctor_updated', array( self::class, 'on_doctor_updated' ), 10, 2 );
|
||||
add_action( 'kivicare_doctor_deleted', array( self::class, 'on_doctor_deleted' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create doctor with advanced business logic
|
||||
*
|
||||
* @param array $doctor_data Doctor data
|
||||
* @param int $clinic_id Primary clinic ID
|
||||
* @param int $user_id Creating user ID
|
||||
* @return array|WP_Error Doctor data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function create_doctor( $doctor_data, $clinic_id, $user_id = null ) {
|
||||
// Permission check
|
||||
if ( ! Permission_Service::current_user_can( 'manage_doctors' ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'You do not have permission to create doctors',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Enhanced validation
|
||||
$validation = self::validate_doctor_business_rules( $doctor_data, $clinic_id );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// Create WordPress user first if email provided
|
||||
$wordpress_user_id = null;
|
||||
if ( ! empty( $doctor_data['user_email'] ) ) {
|
||||
$wordpress_user_id = self::create_doctor_wordpress_user( $doctor_data );
|
||||
if ( is_wp_error( $wordpress_user_id ) ) {
|
||||
return $wordpress_user_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Add metadata
|
||||
$doctor_data['clinic_id'] = $clinic_id;
|
||||
$doctor_data['user_id'] = $wordpress_user_id;
|
||||
$doctor_data['created_by'] = $user_id ?: get_current_user_id();
|
||||
$doctor_data['created_at'] = current_time( 'mysql' );
|
||||
|
||||
// Generate doctor ID if not provided
|
||||
if ( empty( $doctor_data['doctor_id'] ) ) {
|
||||
$doctor_data['doctor_id'] = self::generate_doctor_id( $clinic_id );
|
||||
}
|
||||
|
||||
// Create doctor
|
||||
$doctor_id = Doctor::create( $doctor_data );
|
||||
|
||||
if ( is_wp_error( $doctor_id ) ) {
|
||||
// Clean up WordPress user if created
|
||||
if ( $wordpress_user_id ) {
|
||||
wp_delete_user( $wordpress_user_id );
|
||||
}
|
||||
return $doctor_id;
|
||||
}
|
||||
|
||||
// Post-creation tasks
|
||||
self::setup_doctor_defaults( $doctor_id, $doctor_data );
|
||||
|
||||
// Create clinic association
|
||||
self::associate_doctor_with_clinic( $doctor_id, $clinic_id );
|
||||
|
||||
// Trigger action
|
||||
do_action( 'kivicare_doctor_created', $doctor_id, $doctor_data );
|
||||
|
||||
// Return full doctor data
|
||||
return self::get_doctor_with_metadata( $doctor_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update doctor with business logic
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @param array $doctor_data Updated data
|
||||
* @return array|WP_Error Updated doctor data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function update_doctor( $doctor_id, $doctor_data ) {
|
||||
// Get current doctor data
|
||||
$current_doctor = Doctor::get_by_id( $doctor_id );
|
||||
if ( ! $current_doctor ) {
|
||||
return new \WP_Error(
|
||||
'doctor_not_found',
|
||||
'Doctor not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_manage_doctor( get_current_user_id(), $doctor_id ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'You do not have permission to update this doctor',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Enhanced validation
|
||||
$validation = self::validate_doctor_business_rules( $doctor_data, $current_doctor['clinic_id'], $doctor_id );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// Handle WordPress user updates
|
||||
if ( ! empty( $doctor_data['user_email'] ) && $current_doctor['user_id'] ) {
|
||||
$wp_user_update = wp_update_user( array(
|
||||
'ID' => $current_doctor['user_id'],
|
||||
'user_email' => $doctor_data['user_email'],
|
||||
'display_name' => ( $doctor_data['first_name'] ?? '' ) . ' ' . ( $doctor_data['last_name'] ?? '' )
|
||||
) );
|
||||
|
||||
if ( is_wp_error( $wp_user_update ) ) {
|
||||
return new \WP_Error(
|
||||
'wordpress_user_update_failed',
|
||||
'Failed to update WordPress user: ' . $wp_user_update->get_error_message(),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add update metadata
|
||||
$doctor_data['updated_by'] = get_current_user_id();
|
||||
$doctor_data['updated_at'] = current_time( 'mysql' );
|
||||
|
||||
// Update doctor
|
||||
$result = Doctor::update( $doctor_id, $doctor_data );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Handle specialty changes
|
||||
self::handle_specialty_changes( $doctor_id, $current_doctor, $doctor_data );
|
||||
|
||||
// Handle clinic associations
|
||||
if ( isset( $doctor_data['additional_clinics'] ) ) {
|
||||
self::update_clinic_associations( $doctor_id, $doctor_data['additional_clinics'] );
|
||||
}
|
||||
|
||||
// Trigger action
|
||||
do_action( 'kivicare_doctor_updated', $doctor_id, $doctor_data );
|
||||
|
||||
// Return updated doctor data
|
||||
return self::get_doctor_with_metadata( $doctor_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get doctor with enhanced metadata
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return array|WP_Error Doctor data with metadata or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_doctor_with_metadata( $doctor_id ) {
|
||||
$doctor = Doctor::get_by_id( $doctor_id );
|
||||
|
||||
if ( ! $doctor ) {
|
||||
return new \WP_Error(
|
||||
'doctor_not_found',
|
||||
'Doctor not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_view_doctor( get_current_user_id(), $doctor_id ) ) {
|
||||
return new \WP_Error(
|
||||
'access_denied',
|
||||
'You do not have access to this doctor',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Add enhanced metadata
|
||||
$doctor['clinics'] = self::get_doctor_clinics( $doctor_id );
|
||||
$doctor['specialties'] = self::get_doctor_specialties( $doctor_id );
|
||||
$doctor['schedule'] = self::get_doctor_schedule( $doctor_id );
|
||||
$doctor['statistics'] = self::get_doctor_statistics( $doctor_id );
|
||||
$doctor['recent_appointments'] = self::get_recent_appointments( $doctor_id, 5 );
|
||||
$doctor['qualifications'] = self::get_doctor_qualifications( $doctor_id );
|
||||
$doctor['availability'] = self::get_doctor_availability( $doctor_id );
|
||||
|
||||
return $doctor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search doctors with advanced criteria
|
||||
*
|
||||
* @param string $search_term Search term
|
||||
* @param array $filters Additional filters
|
||||
* @return array Search results
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function search_doctors( $search_term, $filters = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$accessible_clinic_ids = Permission_Service::get_accessible_clinic_ids( $user_id );
|
||||
|
||||
if ( empty( $accessible_clinic_ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Build search query
|
||||
$where_clauses = array( "d.clinic_id IN (" . implode( ',', $accessible_clinic_ids ) . ")" );
|
||||
$where_values = array();
|
||||
|
||||
// Search term
|
||||
if ( ! empty( $search_term ) ) {
|
||||
$where_clauses[] = "(d.first_name LIKE %s OR d.last_name LIKE %s OR d.doctor_id LIKE %s OR d.mobile_number LIKE %s OR d.user_email LIKE %s OR d.specialties LIKE %s)";
|
||||
$search_term = '%' . $wpdb->esc_like( $search_term ) . '%';
|
||||
$where_values = array_merge( $where_values, array_fill( 0, 6, $search_term ) );
|
||||
}
|
||||
|
||||
// Specialty filter
|
||||
if ( ! empty( $filters['specialty'] ) ) {
|
||||
$where_clauses[] = "d.specialties LIKE %s";
|
||||
$where_values[] = '%' . $wpdb->esc_like( $filters['specialty'] ) . '%';
|
||||
}
|
||||
|
||||
// Clinic filter
|
||||
if ( ! empty( $filters['clinic_id'] ) && in_array( $filters['clinic_id'], $accessible_clinic_ids ) ) {
|
||||
$where_clauses[] = "d.clinic_id = %d";
|
||||
$where_values[] = $filters['clinic_id'];
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if ( isset( $filters['status'] ) ) {
|
||||
$where_clauses[] = "d.status = %d";
|
||||
$where_values[] = $filters['status'];
|
||||
} else {
|
||||
$where_clauses[] = "d.status = 1"; // Active by default
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
$query = "SELECT d.*,
|
||||
c.name as clinic_name,
|
||||
COUNT(DISTINCT a.id) as appointment_count,
|
||||
AVG(CASE WHEN a.status = 2 THEN 1 ELSE 0 END) as completion_rate
|
||||
FROM {$wpdb->prefix}kc_doctors d
|
||||
LEFT JOIN {$wpdb->prefix}kc_clinics c ON d.clinic_id = c.id
|
||||
LEFT JOIN {$wpdb->prefix}kc_appointments a ON d.id = a.doctor_id
|
||||
WHERE {$where_sql}
|
||||
GROUP BY d.id
|
||||
ORDER BY d.first_name, d.last_name
|
||||
LIMIT 50";
|
||||
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$results = $wpdb->get_results( $wpdb->prepare( $query, $where_values ), ARRAY_A );
|
||||
} else {
|
||||
$results = $wpdb->get_results( $query, ARRAY_A );
|
||||
}
|
||||
|
||||
return array_map( function( $doctor ) {
|
||||
$doctor['id'] = (int) $doctor['id'];
|
||||
$doctor['appointment_count'] = (int) $doctor['appointment_count'];
|
||||
$doctor['completion_rate'] = round( (float) $doctor['completion_rate'] * 100, 1 );
|
||||
return $doctor;
|
||||
}, $results );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get doctor dashboard data
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return array|WP_Error Dashboard data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_doctor_dashboard( $doctor_id ) {
|
||||
$doctor = Doctor::get_by_id( $doctor_id );
|
||||
|
||||
if ( ! $doctor ) {
|
||||
return new \WP_Error(
|
||||
'doctor_not_found',
|
||||
'Doctor not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_view_doctor( get_current_user_id(), $doctor_id ) ) {
|
||||
return new \WP_Error(
|
||||
'access_denied',
|
||||
'You do not have access to this doctor dashboard',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
$dashboard = array();
|
||||
|
||||
// Basic doctor info
|
||||
$dashboard['doctor'] = $doctor;
|
||||
|
||||
// Today's schedule
|
||||
$dashboard['todays_schedule'] = self::get_doctor_daily_schedule( $doctor_id, current_time( 'Y-m-d' ) );
|
||||
|
||||
// Statistics
|
||||
$dashboard['statistics'] = self::get_comprehensive_statistics( $doctor_id );
|
||||
|
||||
// Recent patients
|
||||
$dashboard['recent_patients'] = self::get_recent_patients( $doctor_id, 10 );
|
||||
|
||||
// Upcoming appointments
|
||||
$dashboard['upcoming_appointments'] = self::get_upcoming_appointments( $doctor_id );
|
||||
|
||||
// Performance metrics
|
||||
$dashboard['performance'] = self::get_performance_metrics( $doctor_id );
|
||||
|
||||
// Revenue data
|
||||
$dashboard['revenue'] = self::get_revenue_data( $doctor_id );
|
||||
|
||||
return $dashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique doctor ID
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return string Doctor ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function generate_doctor_id( $clinic_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$prefix = 'D' . str_pad( $clinic_id, 3, '0', STR_PAD_LEFT );
|
||||
|
||||
// Get the highest existing doctor ID for this clinic
|
||||
$max_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT MAX(CAST(SUBSTRING(doctor_id, 5) AS UNSIGNED))
|
||||
FROM {$wpdb->prefix}kc_doctors
|
||||
WHERE clinic_id = %d AND doctor_id LIKE %s",
|
||||
$clinic_id,
|
||||
$prefix . '%'
|
||||
)
|
||||
);
|
||||
|
||||
$next_number = ( $max_id ? $max_id + 1 : 1 );
|
||||
|
||||
return $prefix . str_pad( $next_number, 4, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create WordPress user for doctor
|
||||
*
|
||||
* @param array $doctor_data Doctor data
|
||||
* @return int|WP_Error WordPress user ID or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function create_doctor_wordpress_user( $doctor_data ) {
|
||||
$username = self::generate_username( $doctor_data );
|
||||
$password = wp_generate_password( 12, false );
|
||||
|
||||
$user_data = array(
|
||||
'user_login' => $username,
|
||||
'user_email' => $doctor_data['user_email'],
|
||||
'user_pass' => $password,
|
||||
'first_name' => $doctor_data['first_name'] ?? '',
|
||||
'last_name' => $doctor_data['last_name'] ?? '',
|
||||
'display_name' => ( $doctor_data['first_name'] ?? '' ) . ' ' . ( $doctor_data['last_name'] ?? '' ),
|
||||
'role' => 'kivicare_doctor'
|
||||
);
|
||||
|
||||
$user_id = wp_insert_user( $user_data );
|
||||
|
||||
if ( is_wp_error( $user_id ) ) {
|
||||
return new \WP_Error(
|
||||
'wordpress_user_creation_failed',
|
||||
'Failed to create WordPress user: ' . $user_id->get_error_message(),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
// Send welcome email with credentials
|
||||
self::send_doctor_welcome_email( $user_id, $username, $password );
|
||||
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique username
|
||||
*
|
||||
* @param array $doctor_data Doctor data
|
||||
* @return string Username
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function generate_username( $doctor_data ) {
|
||||
$first_name = sanitize_user( $doctor_data['first_name'] ?? '' );
|
||||
$last_name = sanitize_user( $doctor_data['last_name'] ?? '' );
|
||||
|
||||
$base_username = strtolower( $first_name . '.' . $last_name );
|
||||
$username = $base_username;
|
||||
$counter = 1;
|
||||
|
||||
while ( username_exists( $username ) ) {
|
||||
$username = $base_username . $counter;
|
||||
$counter++;
|
||||
}
|
||||
|
||||
return $username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate doctor business rules
|
||||
*
|
||||
* @param array $doctor_data Doctor data
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @param int $doctor_id Doctor ID (for updates)
|
||||
* @return bool|WP_Error True if valid, WP_Error otherwise
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function validate_doctor_business_rules( $doctor_data, $clinic_id, $doctor_id = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$errors = array();
|
||||
|
||||
// Check for duplicate doctor ID in clinic
|
||||
if ( ! empty( $doctor_data['doctor_id'] ) ) {
|
||||
$existing_query = "SELECT id FROM {$wpdb->prefix}kc_doctors WHERE doctor_id = %s";
|
||||
$query_params = array( $doctor_data['doctor_id'] );
|
||||
|
||||
if ( $doctor_id ) {
|
||||
$existing_query .= " AND id != %d";
|
||||
$query_params[] = $doctor_id;
|
||||
}
|
||||
|
||||
$existing_doctor = $wpdb->get_var( $wpdb->prepare( $existing_query, $query_params ) );
|
||||
|
||||
if ( $existing_doctor ) {
|
||||
$errors[] = 'A doctor with this ID already exists';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate email format and uniqueness
|
||||
if ( ! empty( $doctor_data['user_email'] ) ) {
|
||||
if ( ! is_email( $doctor_data['user_email'] ) ) {
|
||||
$errors[] = 'Invalid email format';
|
||||
} else {
|
||||
$existing_email = email_exists( $doctor_data['user_email'] );
|
||||
if ( $existing_email && ( ! $doctor_id || $existing_email != $doctor_id ) ) {
|
||||
$errors[] = 'Email already exists';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate mobile number format
|
||||
if ( ! empty( $doctor_data['mobile_number'] ) ) {
|
||||
if ( ! preg_match( '/^[+]?[0-9\s\-\(\)]{7,20}$/', $doctor_data['mobile_number'] ) ) {
|
||||
$errors[] = 'Invalid mobile number format';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate specialties
|
||||
if ( ! empty( $doctor_data['specialties'] ) ) {
|
||||
$specialties = is_array( $doctor_data['specialties'] ) ?
|
||||
$doctor_data['specialties'] :
|
||||
json_decode( $doctor_data['specialties'], true );
|
||||
|
||||
if ( is_array( $specialties ) ) {
|
||||
$valid_specialties = self::get_valid_specialties();
|
||||
foreach ( $specialties as $specialty ) {
|
||||
if ( ! in_array( $specialty, $valid_specialties ) ) {
|
||||
$errors[] = "Invalid specialty: {$specialty}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate license number format (if provided)
|
||||
if ( ! empty( $doctor_data['license_number'] ) ) {
|
||||
if ( ! preg_match( '/^[A-Z0-9\-]{5,20}$/', $doctor_data['license_number'] ) ) {
|
||||
$errors[] = 'Invalid license number format';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $errors ) ) {
|
||||
return new \WP_Error(
|
||||
'doctor_business_validation_failed',
|
||||
'Doctor business validation failed',
|
||||
array(
|
||||
'status' => 400,
|
||||
'errors' => $errors
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup doctor defaults after creation
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @param array $doctor_data Doctor data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function setup_doctor_defaults( $doctor_id, $doctor_data ) {
|
||||
// Setup default schedule
|
||||
self::setup_default_schedule( $doctor_id );
|
||||
|
||||
// Initialize preferences
|
||||
self::setup_default_preferences( $doctor_id );
|
||||
|
||||
// Create service mappings if specialties provided
|
||||
if ( ! empty( $doctor_data['specialties'] ) ) {
|
||||
self::create_default_services( $doctor_id, $doctor_data['specialties'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate doctor with clinic
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function associate_doctor_with_clinic( $doctor_id, $clinic_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->insert(
|
||||
$wpdb->prefix . 'kc_doctor_clinic_mappings',
|
||||
array(
|
||||
'doctor_id' => $doctor_id,
|
||||
'clinic_id' => $clinic_id,
|
||||
'created_at' => current_time( 'mysql' )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get valid medical specialties
|
||||
*
|
||||
* @return array Valid specialties
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_valid_specialties() {
|
||||
return array(
|
||||
'general_medicine', 'cardiology', 'dermatology', 'endocrinology',
|
||||
'gastroenterology', 'gynecology', 'neurology', 'oncology',
|
||||
'ophthalmology', 'orthopedics', 'otolaryngology', 'pediatrics',
|
||||
'psychiatry', 'pulmonology', 'radiology', 'urology', 'surgery',
|
||||
'anesthesiology', 'pathology', 'emergency_medicine', 'family_medicine'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get doctor statistics
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return array Statistics
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_doctor_statistics( $doctor_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$stats = array();
|
||||
|
||||
// Total patients
|
||||
$stats['total_patients'] = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT patient_id) FROM {$wpdb->prefix}kc_appointments WHERE doctor_id = %d",
|
||||
$doctor_id
|
||||
)
|
||||
);
|
||||
|
||||
// Total appointments
|
||||
$stats['total_appointments'] = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments WHERE doctor_id = %d",
|
||||
$doctor_id
|
||||
)
|
||||
);
|
||||
|
||||
// This month appointments
|
||||
$stats['this_month_appointments'] = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments
|
||||
WHERE doctor_id = %d AND MONTH(appointment_start_date) = MONTH(CURDATE())
|
||||
AND YEAR(appointment_start_date) = YEAR(CURDATE())",
|
||||
$doctor_id
|
||||
)
|
||||
);
|
||||
|
||||
// Revenue (if bills are linked to appointments)
|
||||
$stats['total_revenue'] = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COALESCE(SUM(b.total_amount), 0)
|
||||
FROM {$wpdb->prefix}kc_bills b
|
||||
JOIN {$wpdb->prefix}kc_appointments a ON b.appointment_id = a.id
|
||||
WHERE a.doctor_id = %d AND b.status = 'paid'",
|
||||
$doctor_id
|
||||
)
|
||||
);
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
// Additional helper methods would be implemented here...
|
||||
private static function get_doctor_clinics( $doctor_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT c.id, c.name, c.address, c.city
|
||||
FROM {$wpdb->prefix}kc_doctor_clinic_mappings dcm
|
||||
JOIN {$wpdb->prefix}kc_clinics c ON dcm.clinic_id = c.id
|
||||
WHERE dcm.doctor_id = %d",
|
||||
$doctor_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_doctor_specialties( $doctor_id ) {
|
||||
$doctor = Doctor::get_by_id( $doctor_id );
|
||||
if ( $doctor && ! empty( $doctor['specialties'] ) ) {
|
||||
return is_array( $doctor['specialties'] ) ?
|
||||
$doctor['specialties'] :
|
||||
json_decode( $doctor['specialties'], true );
|
||||
}
|
||||
return array();
|
||||
}
|
||||
|
||||
private static function get_doctor_schedule( $doctor_id ) {
|
||||
return get_option( "kivicare_doctor_{$doctor_id}_schedule", array() );
|
||||
}
|
||||
|
||||
private static function get_recent_appointments( $doctor_id, $limit ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT a.*, p.first_name, p.last_name, c.name as clinic_name
|
||||
FROM {$wpdb->prefix}kc_appointments a
|
||||
LEFT JOIN {$wpdb->prefix}kc_patients p ON a.patient_id = p.id
|
||||
LEFT JOIN {$wpdb->prefix}kc_clinics c ON a.clinic_id = c.id
|
||||
WHERE a.doctor_id = %d
|
||||
ORDER BY a.appointment_start_date DESC
|
||||
LIMIT %d",
|
||||
$doctor_id, $limit
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_doctor_qualifications( $doctor_id ) {
|
||||
return get_option( "kivicare_doctor_{$doctor_id}_qualifications", array() );
|
||||
}
|
||||
|
||||
private static function get_doctor_availability( $doctor_id ) {
|
||||
// This would calculate current availability based on schedule and appointments
|
||||
return array(
|
||||
'today' => self::get_today_availability( $doctor_id ),
|
||||
'this_week' => self::get_week_availability( $doctor_id ),
|
||||
'next_available' => self::get_next_available_slot( $doctor_id )
|
||||
);
|
||||
}
|
||||
|
||||
// Event handlers and additional methods...
|
||||
public static function on_doctor_created( $doctor_id, $doctor_data ) {
|
||||
error_log( "KiviCare: New doctor created - ID: {$doctor_id}, Name: " . ( $doctor_data['first_name'] ?? 'Unknown' ) );
|
||||
}
|
||||
|
||||
public static function on_doctor_updated( $doctor_id, $doctor_data ) {
|
||||
error_log( "KiviCare: Doctor updated - ID: {$doctor_id}" );
|
||||
wp_cache_delete( "doctor_{$doctor_id}", 'kivicare' );
|
||||
}
|
||||
|
||||
public static function on_doctor_deleted( $doctor_id ) {
|
||||
// Clean up related data
|
||||
delete_option( "kivicare_doctor_{$doctor_id}_schedule" );
|
||||
delete_option( "kivicare_doctor_{$doctor_id}_preferences" );
|
||||
delete_option( "kivicare_doctor_{$doctor_id}_qualifications" );
|
||||
|
||||
wp_cache_delete( "doctor_{$doctor_id}", 'kivicare' );
|
||||
error_log( "KiviCare: Doctor deleted - ID: {$doctor_id}" );
|
||||
}
|
||||
|
||||
// Placeholder methods for additional functionality
|
||||
private static function setup_default_schedule( $doctor_id ) {
|
||||
$default_schedule = array(
|
||||
'monday' => array( 'start_time' => '09:00', 'end_time' => '17:00', 'break_start' => '12:00', 'break_end' => '13:00' ),
|
||||
'tuesday' => array( 'start_time' => '09:00', 'end_time' => '17:00', 'break_start' => '12:00', 'break_end' => '13:00' ),
|
||||
'wednesday' => array( 'start_time' => '09:00', 'end_time' => '17:00', 'break_start' => '12:00', 'break_end' => '13:00' ),
|
||||
'thursday' => array( 'start_time' => '09:00', 'end_time' => '17:00', 'break_start' => '12:00', 'break_end' => '13:00' ),
|
||||
'friday' => array( 'start_time' => '09:00', 'end_time' => '17:00', 'break_start' => '12:00', 'break_end' => '13:00' ),
|
||||
'saturday' => array( 'start_time' => '09:00', 'end_time' => '13:00' ),
|
||||
'sunday' => array( 'closed' => true )
|
||||
);
|
||||
|
||||
update_option( "kivicare_doctor_{$doctor_id}_schedule", $default_schedule );
|
||||
}
|
||||
|
||||
private static function setup_default_preferences( $doctor_id ) {
|
||||
$default_preferences = array(
|
||||
'appointment_duration' => 30,
|
||||
'buffer_time' => 5,
|
||||
'max_appointments_per_day' => 20,
|
||||
'email_notifications' => true,
|
||||
'sms_notifications' => false,
|
||||
'auto_confirm_appointments' => false
|
||||
);
|
||||
|
||||
update_option( "kivicare_doctor_{$doctor_id}_preferences", $default_preferences );
|
||||
}
|
||||
|
||||
private static function create_default_services( $doctor_id, $specialties ) {
|
||||
// This would create default services based on doctor specialties
|
||||
// Implementation would depend on the services structure
|
||||
}
|
||||
|
||||
private static function handle_specialty_changes( $doctor_id, $current_data, $new_data ) {
|
||||
// Handle when doctor specialties change
|
||||
if ( isset( $new_data['specialties'] ) ) {
|
||||
$old_specialties = isset( $current_data['specialties'] ) ?
|
||||
( is_array( $current_data['specialties'] ) ? $current_data['specialties'] : json_decode( $current_data['specialties'], true ) ) : array();
|
||||
|
||||
$new_specialties = is_array( $new_data['specialties'] ) ?
|
||||
$new_data['specialties'] :
|
||||
json_decode( $new_data['specialties'], true );
|
||||
|
||||
if ( $old_specialties !== $new_specialties ) {
|
||||
do_action( 'kivicare_doctor_specialties_changed', $doctor_id, $old_specialties, $new_specialties );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function update_clinic_associations( $doctor_id, $clinic_ids ) {
|
||||
global $wpdb;
|
||||
|
||||
// Remove existing associations
|
||||
$wpdb->delete(
|
||||
$wpdb->prefix . 'kc_doctor_clinic_mappings',
|
||||
array( 'doctor_id' => $doctor_id )
|
||||
);
|
||||
|
||||
// Add new associations
|
||||
foreach ( $clinic_ids as $clinic_id ) {
|
||||
$wpdb->insert(
|
||||
$wpdb->prefix . 'kc_doctor_clinic_mappings',
|
||||
array(
|
||||
'doctor_id' => $doctor_id,
|
||||
'clinic_id' => $clinic_id,
|
||||
'created_at' => current_time( 'mysql' )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static function send_doctor_welcome_email( $user_id, $username, $password ) {
|
||||
// Implementation for sending welcome email with credentials
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
if ( $user ) {
|
||||
wp_new_user_notification( $user_id, null, 'both' );
|
||||
}
|
||||
}
|
||||
|
||||
// Additional placeholder methods for dashboard functionality
|
||||
private static function get_doctor_daily_schedule( $doctor_id, $date ) {
|
||||
// Get appointments for specific date
|
||||
global $wpdb;
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kc_appointments
|
||||
WHERE doctor_id = %d AND DATE(appointment_start_date) = %s
|
||||
ORDER BY appointment_start_time",
|
||||
$doctor_id, $date
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_comprehensive_statistics( $doctor_id ) {
|
||||
return self::get_doctor_statistics( $doctor_id );
|
||||
}
|
||||
|
||||
private static function get_recent_patients( $doctor_id, $limit ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT DISTINCT p.*, MAX(a.appointment_start_date) as last_visit
|
||||
FROM {$wpdb->prefix}kc_patients p
|
||||
JOIN {$wpdb->prefix}kc_appointments a ON p.id = a.patient_id
|
||||
WHERE a.doctor_id = %d
|
||||
GROUP BY p.id
|
||||
ORDER BY last_visit DESC
|
||||
LIMIT %d",
|
||||
$doctor_id, $limit
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_upcoming_appointments( $doctor_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT a.*, p.first_name, p.last_name
|
||||
FROM {$wpdb->prefix}kc_appointments a
|
||||
JOIN {$wpdb->prefix}kc_patients p ON a.patient_id = p.id
|
||||
WHERE a.doctor_id = %d AND a.appointment_start_date >= CURDATE()
|
||||
ORDER BY a.appointment_start_date, a.appointment_start_time
|
||||
LIMIT 10",
|
||||
$doctor_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_performance_metrics( $doctor_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$metrics = array();
|
||||
|
||||
// Completion rate
|
||||
$completion_data = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END) as completed,
|
||||
SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) as cancelled
|
||||
FROM {$wpdb->prefix}kc_appointments
|
||||
WHERE doctor_id = %d AND appointment_start_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)",
|
||||
$doctor_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
if ( $completion_data && $completion_data['total'] > 0 ) {
|
||||
$metrics['completion_rate'] = round( ( $completion_data['completed'] / $completion_data['total'] ) * 100, 1 );
|
||||
$metrics['cancellation_rate'] = round( ( $completion_data['cancelled'] / $completion_data['total'] ) * 100, 1 );
|
||||
} else {
|
||||
$metrics['completion_rate'] = 0;
|
||||
$metrics['cancellation_rate'] = 0;
|
||||
}
|
||||
|
||||
return $metrics;
|
||||
}
|
||||
|
||||
private static function get_revenue_data( $doctor_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT DATE_FORMAT(b.created_at, '%%Y-%%m') as month, SUM(b.total_amount) as revenue
|
||||
FROM {$wpdb->prefix}kc_bills b
|
||||
JOIN {$wpdb->prefix}kc_appointments a ON b.appointment_id = a.id
|
||||
WHERE a.doctor_id = %d AND b.status = 'paid'
|
||||
GROUP BY DATE_FORMAT(b.created_at, '%%Y-%%m')
|
||||
ORDER BY month DESC
|
||||
LIMIT 12",
|
||||
$doctor_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_today_availability( $doctor_id ) {
|
||||
// Calculate available slots for today
|
||||
return array();
|
||||
}
|
||||
|
||||
private static function get_week_availability( $doctor_id ) {
|
||||
// Calculate available slots for this week
|
||||
return array();
|
||||
}
|
||||
|
||||
private static function get_next_available_slot( $doctor_id ) {
|
||||
// Find next available appointment slot
|
||||
return null;
|
||||
}
|
||||
}
|
||||
891
src/includes/services/database/class-encounter-service.php
Normal file
891
src/includes/services/database/class-encounter-service.php
Normal file
@@ -0,0 +1,891 @@
|
||||
<?php
|
||||
/**
|
||||
* Encounter Database Service
|
||||
*
|
||||
* Handles advanced encounter data operations and business logic
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Services\Database
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_API\Services\Database;
|
||||
|
||||
use KiviCare_API\Models\Encounter;
|
||||
use KiviCare_API\Services\Permission_Service;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Encounter_Service
|
||||
*
|
||||
* Advanced database service for encounter management with business logic
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Encounter_Service {
|
||||
|
||||
/**
|
||||
* Initialize the service
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function init() {
|
||||
// Hook into WordPress actions
|
||||
add_action( 'kivicare_encounter_created', array( self::class, 'on_encounter_created' ), 10, 2 );
|
||||
add_action( 'kivicare_encounter_updated', array( self::class, 'on_encounter_updated' ), 10, 2 );
|
||||
add_action( 'kivicare_encounter_deleted', array( self::class, 'on_encounter_deleted' ), 10, 1 );
|
||||
add_action( 'kivicare_encounter_finalized', array( self::class, 'on_encounter_finalized' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create encounter with advanced business logic
|
||||
*
|
||||
* @param array $encounter_data Encounter data
|
||||
* @param int $user_id Creating user ID
|
||||
* @return array|WP_Error Encounter data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function create_encounter( $encounter_data, $user_id = null ) {
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_manage_encounters( get_current_user_id(), $encounter_data['clinic_id'] ?? 0 ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'You do not have permission to create encounters',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Enhanced validation
|
||||
$validation = self::validate_encounter_business_rules( $encounter_data );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// Add metadata
|
||||
$encounter_data['created_by'] = $user_id ?: get_current_user_id();
|
||||
$encounter_data['created_at'] = current_time( 'mysql' );
|
||||
$encounter_data['status'] = 'draft'; // Default status
|
||||
|
||||
// Generate encounter number if not provided
|
||||
if ( empty( $encounter_data['encounter_number'] ) ) {
|
||||
$encounter_data['encounter_number'] = self::generate_encounter_number( $encounter_data['clinic_id'] );
|
||||
}
|
||||
|
||||
// Set encounter date if not provided
|
||||
if ( empty( $encounter_data['encounter_date'] ) ) {
|
||||
$encounter_data['encounter_date'] = current_time( 'mysql' );
|
||||
}
|
||||
|
||||
// Create encounter
|
||||
$encounter_id = Encounter::create( $encounter_data );
|
||||
|
||||
if ( is_wp_error( $encounter_id ) ) {
|
||||
return $encounter_id;
|
||||
}
|
||||
|
||||
// Post-creation tasks
|
||||
self::setup_encounter_defaults( $encounter_id, $encounter_data );
|
||||
|
||||
// Auto-link to appointment if provided
|
||||
if ( ! empty( $encounter_data['appointment_id'] ) ) {
|
||||
self::link_encounter_to_appointment( $encounter_id, $encounter_data['appointment_id'] );
|
||||
}
|
||||
|
||||
// Trigger action
|
||||
do_action( 'kivicare_encounter_created', $encounter_id, $encounter_data );
|
||||
|
||||
// Return full encounter data
|
||||
return self::get_encounter_with_metadata( $encounter_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update encounter with business logic
|
||||
*
|
||||
* @param int $encounter_id Encounter ID
|
||||
* @param array $encounter_data Updated data
|
||||
* @return array|WP_Error Updated encounter data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function update_encounter( $encounter_id, $encounter_data ) {
|
||||
// Get current encounter data
|
||||
$current_encounter = Encounter::get_by_id( $encounter_id );
|
||||
if ( ! $current_encounter ) {
|
||||
return new \WP_Error(
|
||||
'encounter_not_found',
|
||||
'Encounter not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_manage_encounters( get_current_user_id(), $current_encounter['clinic_id'] ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'You do not have permission to update this encounter',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check if encounter is finalized
|
||||
if ( $current_encounter['status'] === 'finalized' ) {
|
||||
return new \WP_Error(
|
||||
'encounter_finalized',
|
||||
'Cannot update a finalized encounter',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
// Enhanced validation
|
||||
$validation = self::validate_encounter_business_rules( $encounter_data, $encounter_id );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// Add update metadata
|
||||
$encounter_data['updated_by'] = get_current_user_id();
|
||||
$encounter_data['updated_at'] = current_time( 'mysql' );
|
||||
|
||||
// Update encounter
|
||||
$result = Encounter::update( $encounter_id, $encounter_data );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Handle status changes
|
||||
self::handle_status_changes( $encounter_id, $current_encounter, $encounter_data );
|
||||
|
||||
// Handle SOAP notes updates
|
||||
if ( isset( $encounter_data['soap_notes'] ) ) {
|
||||
self::update_soap_notes( $encounter_id, $encounter_data['soap_notes'] );
|
||||
}
|
||||
|
||||
// Handle vital signs updates
|
||||
if ( isset( $encounter_data['vital_signs'] ) ) {
|
||||
self::update_vital_signs( $encounter_id, $encounter_data['vital_signs'] );
|
||||
}
|
||||
|
||||
// Trigger action
|
||||
do_action( 'kivicare_encounter_updated', $encounter_id, $encounter_data );
|
||||
|
||||
// Return updated encounter data
|
||||
return self::get_encounter_with_metadata( $encounter_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize encounter
|
||||
*
|
||||
* @param int $encounter_id Encounter ID
|
||||
* @param array $final_data Final data
|
||||
* @return array|WP_Error Updated encounter data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function finalize_encounter( $encounter_id, $final_data = array() ) {
|
||||
$encounter = Encounter::get_by_id( $encounter_id );
|
||||
if ( ! $encounter ) {
|
||||
return new \WP_Error(
|
||||
'encounter_not_found',
|
||||
'Encounter not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_manage_encounters( get_current_user_id(), $encounter['clinic_id'] ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'You do not have permission to finalize this encounter',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Check if already finalized
|
||||
if ( $encounter['status'] === 'finalized' ) {
|
||||
return new \WP_Error(
|
||||
'already_finalized',
|
||||
'Encounter is already finalized',
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
// Validate required data for finalization
|
||||
$validation = self::validate_finalization_requirements( $encounter_id );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// Update encounter status
|
||||
$update_data = array_merge( $final_data, array(
|
||||
'status' => 'finalized',
|
||||
'finalized_by' => get_current_user_id(),
|
||||
'finalized_at' => current_time( 'mysql' ),
|
||||
'updated_at' => current_time( 'mysql' )
|
||||
));
|
||||
|
||||
$result = Encounter::update( $encounter_id, $update_data );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Post-finalization tasks
|
||||
self::handle_finalization_tasks( $encounter_id );
|
||||
|
||||
// Trigger action
|
||||
do_action( 'kivicare_encounter_finalized', $encounter_id );
|
||||
|
||||
return self::get_encounter_with_metadata( $encounter_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encounter with enhanced metadata
|
||||
*
|
||||
* @param int $encounter_id Encounter ID
|
||||
* @return array|WP_Error Encounter data with metadata or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_encounter_with_metadata( $encounter_id ) {
|
||||
$encounter = Encounter::get_by_id( $encounter_id );
|
||||
|
||||
if ( ! $encounter ) {
|
||||
return new \WP_Error(
|
||||
'encounter_not_found',
|
||||
'Encounter not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_view_encounter( get_current_user_id(), $encounter_id ) ) {
|
||||
return new \WP_Error(
|
||||
'access_denied',
|
||||
'You do not have access to this encounter',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Add enhanced metadata
|
||||
$encounter['patient'] = self::get_encounter_patient( $encounter['patient_id'] );
|
||||
$encounter['doctor'] = self::get_encounter_doctor( $encounter['doctor_id'] );
|
||||
$encounter['clinic'] = self::get_encounter_clinic( $encounter['clinic_id'] );
|
||||
$encounter['appointment'] = self::get_encounter_appointment( $encounter['appointment_id'] ?? null );
|
||||
$encounter['soap_notes'] = self::get_soap_notes( $encounter_id );
|
||||
$encounter['vital_signs'] = self::get_vital_signs( $encounter_id );
|
||||
$encounter['diagnoses'] = self::get_encounter_diagnoses( $encounter_id );
|
||||
$encounter['prescriptions'] = self::get_encounter_prescriptions( $encounter_id );
|
||||
$encounter['attachments'] = self::get_encounter_attachments( $encounter_id );
|
||||
$encounter['bills'] = self::get_encounter_bills( $encounter_id );
|
||||
|
||||
return $encounter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search encounters with advanced criteria
|
||||
*
|
||||
* @param array $filters Search filters
|
||||
* @return array Search results
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function search_encounters( $filters = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$accessible_clinic_ids = Permission_Service::get_accessible_clinic_ids( $user_id );
|
||||
|
||||
if ( empty( $accessible_clinic_ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Build search query
|
||||
$where_clauses = array( "e.clinic_id IN (" . implode( ',', $accessible_clinic_ids ) . ")" );
|
||||
$where_values = array();
|
||||
|
||||
// Date range filter
|
||||
if ( ! empty( $filters['start_date'] ) ) {
|
||||
$where_clauses[] = "DATE(e.encounter_date) >= %s";
|
||||
$where_values[] = $filters['start_date'];
|
||||
}
|
||||
|
||||
if ( ! empty( $filters['end_date'] ) ) {
|
||||
$where_clauses[] = "DATE(e.encounter_date) <= %s";
|
||||
$where_values[] = $filters['end_date'];
|
||||
}
|
||||
|
||||
// Doctor filter
|
||||
if ( ! empty( $filters['doctor_id'] ) ) {
|
||||
$where_clauses[] = "e.doctor_id = %d";
|
||||
$where_values[] = $filters['doctor_id'];
|
||||
}
|
||||
|
||||
// Patient filter
|
||||
if ( ! empty( $filters['patient_id'] ) ) {
|
||||
$where_clauses[] = "e.patient_id = %d";
|
||||
$where_values[] = $filters['patient_id'];
|
||||
}
|
||||
|
||||
// Clinic filter
|
||||
if ( ! empty( $filters['clinic_id'] ) && in_array( $filters['clinic_id'], $accessible_clinic_ids ) ) {
|
||||
$where_clauses[] = "e.clinic_id = %d";
|
||||
$where_values[] = $filters['clinic_id'];
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if ( ! empty( $filters['status'] ) ) {
|
||||
if ( is_array( $filters['status'] ) ) {
|
||||
$status_placeholders = implode( ',', array_fill( 0, count( $filters['status'] ), '%s' ) );
|
||||
$where_clauses[] = "e.status IN ({$status_placeholders})";
|
||||
$where_values = array_merge( $where_values, $filters['status'] );
|
||||
} else {
|
||||
$where_clauses[] = "e.status = %s";
|
||||
$where_values[] = $filters['status'];
|
||||
}
|
||||
}
|
||||
|
||||
// Search term
|
||||
if ( ! empty( $filters['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.encounter_number LIKE %s OR e.chief_complaint LIKE %s)";
|
||||
$search_term = '%' . $wpdb->esc_like( $filters['search'] ) . '%';
|
||||
$where_values = array_merge( $where_values, array_fill( 0, 6, $search_term ) );
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
// Pagination
|
||||
$limit = isset( $filters['limit'] ) ? (int) $filters['limit'] : 20;
|
||||
$offset = isset( $filters['offset'] ) ? (int) $filters['offset'] : 0;
|
||||
|
||||
$query = "SELECT e.*,
|
||||
p.first_name as patient_first_name, p.last_name as patient_last_name,
|
||||
d.first_name as doctor_first_name, d.last_name as doctor_last_name,
|
||||
c.name as clinic_name
|
||||
FROM {$wpdb->prefix}kc_encounters e
|
||||
LEFT JOIN {$wpdb->prefix}kc_patients p ON e.patient_id = p.id
|
||||
LEFT JOIN {$wpdb->prefix}kc_doctors d ON e.doctor_id = d.id
|
||||
LEFT JOIN {$wpdb->prefix}kc_clinics c ON e.clinic_id = c.id
|
||||
WHERE {$where_sql}
|
||||
ORDER BY e.encounter_date DESC
|
||||
LIMIT {$limit} OFFSET {$offset}";
|
||||
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$results = $wpdb->get_results( $wpdb->prepare( $query, $where_values ), ARRAY_A );
|
||||
} else {
|
||||
$results = $wpdb->get_results( $query, ARRAY_A );
|
||||
}
|
||||
|
||||
// Get total count for pagination
|
||||
$count_query = "SELECT COUNT(*) FROM {$wpdb->prefix}kc_encounters e
|
||||
LEFT JOIN {$wpdb->prefix}kc_patients p ON e.patient_id = p.id
|
||||
LEFT JOIN {$wpdb->prefix}kc_doctors d ON e.doctor_id = d.id
|
||||
WHERE {$where_sql}";
|
||||
|
||||
if ( ! empty( $where_values ) ) {
|
||||
$total = (int) $wpdb->get_var( $wpdb->prepare( $count_query, $where_values ) );
|
||||
} else {
|
||||
$total = (int) $wpdb->get_var( $count_query );
|
||||
}
|
||||
|
||||
return array(
|
||||
'encounters' => array_map( function( $encounter ) {
|
||||
$encounter['id'] = (int) $encounter['id'];
|
||||
return $encounter;
|
||||
}, $results ),
|
||||
'total' => $total,
|
||||
'has_more' => ( $offset + $limit ) < $total
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get patient encounter history
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @param int $limit Limit
|
||||
* @return array Encounter history
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_patient_encounter_history( $patient_id, $limit = 10 ) {
|
||||
global $wpdb;
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$accessible_clinic_ids = Permission_Service::get_accessible_clinic_ids( $user_id );
|
||||
|
||||
if ( empty( $accessible_clinic_ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$query = "SELECT e.*,
|
||||
d.first_name as doctor_first_name, d.last_name as doctor_last_name,
|
||||
c.name as clinic_name
|
||||
FROM {$wpdb->prefix}kc_encounters e
|
||||
LEFT JOIN {$wpdb->prefix}kc_doctors d ON e.doctor_id = d.id
|
||||
LEFT JOIN {$wpdb->prefix}kc_clinics c ON e.clinic_id = c.id
|
||||
WHERE e.patient_id = %d
|
||||
AND e.clinic_id IN (" . implode( ',', $accessible_clinic_ids ) . ")
|
||||
ORDER BY e.encounter_date DESC
|
||||
LIMIT %d";
|
||||
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare( $query, $patient_id, $limit ),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map( function( $encounter ) {
|
||||
$encounter['id'] = (int) $encounter['id'];
|
||||
$encounter['soap_notes'] = self::get_soap_notes( $encounter['id'] );
|
||||
$encounter['diagnoses'] = self::get_encounter_diagnoses( $encounter['id'] );
|
||||
return $encounter;
|
||||
}, $results );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique encounter number
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return string Encounter number
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function generate_encounter_number( $clinic_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$prefix = 'E' . str_pad( $clinic_id, 3, '0', STR_PAD_LEFT ) . date( 'ym' );
|
||||
|
||||
// Get the highest existing encounter number for this clinic and month
|
||||
$max_number = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT MAX(CAST(SUBSTRING(encounter_number, 8) AS UNSIGNED))
|
||||
FROM {$wpdb->prefix}kc_encounters
|
||||
WHERE encounter_number LIKE %s",
|
||||
$prefix . '%'
|
||||
)
|
||||
);
|
||||
|
||||
$next_number = ( $max_number ? $max_number + 1 : 1 );
|
||||
|
||||
return $prefix . str_pad( $next_number, 4, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate encounter business rules
|
||||
*
|
||||
* @param array $encounter_data Encounter data
|
||||
* @param int $encounter_id Encounter ID (for updates)
|
||||
* @return bool|WP_Error True if valid, WP_Error otherwise
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function validate_encounter_business_rules( $encounter_data, $encounter_id = null ) {
|
||||
$errors = array();
|
||||
|
||||
// Validate required fields
|
||||
$required_fields = array( 'patient_id', 'doctor_id', 'clinic_id' );
|
||||
|
||||
foreach ( $required_fields as $field ) {
|
||||
if ( empty( $encounter_data[$field] ) ) {
|
||||
$errors[] = "Field {$field} is required";
|
||||
}
|
||||
}
|
||||
|
||||
// Validate patient exists
|
||||
if ( ! empty( $encounter_data['patient_id'] ) ) {
|
||||
global $wpdb;
|
||||
$patient_exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$wpdb->prefix}kc_patients WHERE id = %d",
|
||||
$encounter_data['patient_id']
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $patient_exists ) {
|
||||
$errors[] = 'Invalid patient ID';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate doctor exists
|
||||
if ( ! empty( $encounter_data['doctor_id'] ) ) {
|
||||
global $wpdb;
|
||||
$doctor_exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$wpdb->prefix}kc_doctors WHERE id = %d",
|
||||
$encounter_data['doctor_id']
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $doctor_exists ) {
|
||||
$errors[] = 'Invalid doctor ID';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate clinic exists
|
||||
if ( ! empty( $encounter_data['clinic_id'] ) ) {
|
||||
global $wpdb;
|
||||
$clinic_exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$wpdb->prefix}kc_clinics WHERE id = %d",
|
||||
$encounter_data['clinic_id']
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $clinic_exists ) {
|
||||
$errors[] = 'Invalid clinic ID';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate appointment if provided
|
||||
if ( ! empty( $encounter_data['appointment_id'] ) ) {
|
||||
global $wpdb;
|
||||
$appointment_exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$wpdb->prefix}kc_appointments WHERE id = %d",
|
||||
$encounter_data['appointment_id']
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $appointment_exists ) {
|
||||
$errors[] = 'Invalid appointment ID';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $errors ) ) {
|
||||
return new \WP_Error(
|
||||
'encounter_business_validation_failed',
|
||||
'Encounter business validation failed',
|
||||
array(
|
||||
'status' => 400,
|
||||
'errors' => $errors
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate finalization requirements
|
||||
*
|
||||
* @param int $encounter_id Encounter ID
|
||||
* @return bool|WP_Error True if valid, WP_Error otherwise
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function validate_finalization_requirements( $encounter_id ) {
|
||||
$encounter = Encounter::get_by_id( $encounter_id );
|
||||
$errors = array();
|
||||
|
||||
// Check if chief complaint is provided
|
||||
if ( empty( $encounter['chief_complaint'] ) ) {
|
||||
$errors[] = 'Chief complaint is required for finalization';
|
||||
}
|
||||
|
||||
// Check if at least one SOAP note section is filled
|
||||
$soap_notes = self::get_soap_notes( $encounter_id );
|
||||
if ( empty( $soap_notes['subjective'] ) && empty( $soap_notes['objective'] ) &&
|
||||
empty( $soap_notes['assessment'] ) && empty( $soap_notes['plan'] ) ) {
|
||||
$errors[] = 'At least one SOAP note section must be completed for finalization';
|
||||
}
|
||||
|
||||
if ( ! empty( $errors ) ) {
|
||||
return new \WP_Error(
|
||||
'encounter_finalization_validation_failed',
|
||||
'Encounter finalization validation failed',
|
||||
array(
|
||||
'status' => 400,
|
||||
'errors' => $errors
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup encounter defaults after creation
|
||||
*
|
||||
* @param int $encounter_id Encounter ID
|
||||
* @param array $encounter_data Encounter data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function setup_encounter_defaults( $encounter_id, $encounter_data ) {
|
||||
// Initialize SOAP notes structure
|
||||
self::initialize_soap_notes( $encounter_id );
|
||||
|
||||
// Initialize vital signs structure
|
||||
self::initialize_vital_signs( $encounter_id );
|
||||
|
||||
// Setup encounter preferences
|
||||
self::setup_encounter_preferences( $encounter_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Link encounter to appointment
|
||||
*
|
||||
* @param int $encounter_id Encounter ID
|
||||
* @param int $appointment_id Appointment ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function link_encounter_to_appointment( $encounter_id, $appointment_id ) {
|
||||
global $wpdb;
|
||||
|
||||
// Update appointment with encounter reference
|
||||
$wpdb->update(
|
||||
$wpdb->prefix . 'kc_appointments',
|
||||
array( 'encounter_id' => $encounter_id ),
|
||||
array( 'id' => $appointment_id ),
|
||||
array( '%d' ),
|
||||
array( '%d' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle status changes
|
||||
*
|
||||
* @param int $encounter_id Encounter ID
|
||||
* @param array $current_encounter Current encounter data
|
||||
* @param array $new_data New data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function handle_status_changes( $encounter_id, $current_encounter, $new_data ) {
|
||||
if ( isset( $new_data['status'] ) && $new_data['status'] != $current_encounter['status'] ) {
|
||||
$status_change = array(
|
||||
'encounter_id' => $encounter_id,
|
||||
'from_status' => $current_encounter['status'],
|
||||
'to_status' => $new_data['status'],
|
||||
'changed_by' => get_current_user_id(),
|
||||
'changed_at' => current_time( 'mysql' )
|
||||
);
|
||||
|
||||
do_action( 'kivicare_encounter_status_changed', $status_change );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle finalization tasks
|
||||
*
|
||||
* @param int $encounter_id Encounter ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function handle_finalization_tasks( $encounter_id ) {
|
||||
// Generate encounter summary
|
||||
self::generate_encounter_summary( $encounter_id );
|
||||
|
||||
// Auto-create follow-up reminders if needed
|
||||
self::create_follow_up_reminders( $encounter_id );
|
||||
|
||||
// Update patient medical history
|
||||
self::update_patient_medical_history( $encounter_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods for encounter data management
|
||||
*/
|
||||
|
||||
private static function initialize_soap_notes( $encounter_id ) {
|
||||
$default_soap = array(
|
||||
'subjective' => '',
|
||||
'objective' => '',
|
||||
'assessment' => '',
|
||||
'plan' => ''
|
||||
);
|
||||
|
||||
update_option( "kivicare_encounter_{$encounter_id}_soap_notes", $default_soap );
|
||||
}
|
||||
|
||||
private static function initialize_vital_signs( $encounter_id ) {
|
||||
$default_vitals = array(
|
||||
'temperature' => '',
|
||||
'blood_pressure_systolic' => '',
|
||||
'blood_pressure_diastolic' => '',
|
||||
'heart_rate' => '',
|
||||
'respiratory_rate' => '',
|
||||
'oxygen_saturation' => '',
|
||||
'weight' => '',
|
||||
'height' => '',
|
||||
'bmi' => ''
|
||||
);
|
||||
|
||||
update_option( "kivicare_encounter_{$encounter_id}_vital_signs", $default_vitals );
|
||||
}
|
||||
|
||||
private static function setup_encounter_preferences( $encounter_id ) {
|
||||
$default_preferences = array(
|
||||
'auto_save' => true,
|
||||
'show_patient_history' => true,
|
||||
'template_type' => 'standard'
|
||||
);
|
||||
|
||||
update_option( "kivicare_encounter_{$encounter_id}_preferences", $default_preferences );
|
||||
}
|
||||
|
||||
private static function update_soap_notes( $encounter_id, $soap_notes ) {
|
||||
update_option( "kivicare_encounter_{$encounter_id}_soap_notes", $soap_notes );
|
||||
}
|
||||
|
||||
private static function update_vital_signs( $encounter_id, $vital_signs ) {
|
||||
// Calculate BMI if height and weight are provided
|
||||
if ( ! empty( $vital_signs['height'] ) && ! empty( $vital_signs['weight'] ) ) {
|
||||
$height_m = $vital_signs['height'] / 100; // Convert cm to meters
|
||||
$vital_signs['bmi'] = round( $vital_signs['weight'] / ( $height_m * $height_m ), 2 );
|
||||
}
|
||||
|
||||
update_option( "kivicare_encounter_{$encounter_id}_vital_signs", $vital_signs );
|
||||
}
|
||||
|
||||
private static function get_soap_notes( $encounter_id ) {
|
||||
return get_option( "kivicare_encounter_{$encounter_id}_soap_notes", array() );
|
||||
}
|
||||
|
||||
private static function get_vital_signs( $encounter_id ) {
|
||||
return get_option( "kivicare_encounter_{$encounter_id}_vital_signs", array() );
|
||||
}
|
||||
|
||||
private static function get_encounter_patient( $patient_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT id, first_name, last_name, user_email, contact_no, dob, gender FROM {$wpdb->prefix}kc_patients WHERE id = %d",
|
||||
$patient_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_encounter_doctor( $doctor_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT id, first_name, last_name, user_email, mobile_number, specialties FROM {$wpdb->prefix}kc_doctors WHERE id = %d",
|
||||
$doctor_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_encounter_clinic( $clinic_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT id, name, address, city, telephone_no FROM {$wpdb->prefix}kc_clinics WHERE id = %d",
|
||||
$clinic_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_encounter_appointment( $appointment_id ) {
|
||||
if ( ! $appointment_id ) return null;
|
||||
|
||||
global $wpdb;
|
||||
return $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT id, appointment_number, appointment_start_date, appointment_start_time, status FROM {$wpdb->prefix}kc_appointments WHERE id = %d",
|
||||
$appointment_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_encounter_diagnoses( $encounter_id ) {
|
||||
return get_option( "kivicare_encounter_{$encounter_id}_diagnoses", array() );
|
||||
}
|
||||
|
||||
private static function get_encounter_prescriptions( $encounter_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kc_prescriptions WHERE encounter_id = %d ORDER BY created_at DESC",
|
||||
$encounter_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_encounter_attachments( $encounter_id ) {
|
||||
return get_option( "kivicare_encounter_{$encounter_id}_attachments", array() );
|
||||
}
|
||||
|
||||
private static function get_encounter_bills( $encounter_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kc_bills WHERE encounter_id = %d ORDER BY created_at DESC",
|
||||
$encounter_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function generate_encounter_summary( $encounter_id ) {
|
||||
$encounter = Encounter::get_by_id( $encounter_id );
|
||||
$soap_notes = self::get_soap_notes( $encounter_id );
|
||||
|
||||
$summary = array(
|
||||
'encounter_id' => $encounter_id,
|
||||
'chief_complaint' => $encounter['chief_complaint'],
|
||||
'key_findings' => $soap_notes['objective'] ?? '',
|
||||
'diagnosis' => $soap_notes['assessment'] ?? '',
|
||||
'treatment_plan' => $soap_notes['plan'] ?? '',
|
||||
'generated_at' => current_time( 'mysql' )
|
||||
);
|
||||
|
||||
update_option( "kivicare_encounter_{$encounter_id}_summary", $summary );
|
||||
}
|
||||
|
||||
private static function create_follow_up_reminders( $encounter_id ) {
|
||||
// This would create follow-up reminders based on the treatment plan
|
||||
// Implementation depends on reminder system
|
||||
}
|
||||
|
||||
private static function update_patient_medical_history( $encounter_id ) {
|
||||
$encounter = Encounter::get_by_id( $encounter_id );
|
||||
$patient_id = $encounter['patient_id'];
|
||||
|
||||
$medical_history = get_option( "kivicare_patient_{$patient_id}_medical_history", array() );
|
||||
|
||||
// Add this encounter to patient history
|
||||
if ( ! isset( $medical_history['encounters'] ) ) {
|
||||
$medical_history['encounters'] = array();
|
||||
}
|
||||
|
||||
$medical_history['encounters'][] = array(
|
||||
'encounter_id' => $encounter_id,
|
||||
'date' => $encounter['encounter_date'],
|
||||
'chief_complaint' => $encounter['chief_complaint'],
|
||||
'doctor_id' => $encounter['doctor_id'],
|
||||
'clinic_id' => $encounter['clinic_id']
|
||||
);
|
||||
|
||||
update_option( "kivicare_patient_{$patient_id}_medical_history", $medical_history );
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handlers
|
||||
*/
|
||||
public static function on_encounter_created( $encounter_id, $encounter_data ) {
|
||||
error_log( "KiviCare: New encounter created - ID: {$encounter_id}, Patient: " . ( $encounter_data['patient_id'] ?? 'Unknown' ) );
|
||||
}
|
||||
|
||||
public static function on_encounter_updated( $encounter_id, $encounter_data ) {
|
||||
error_log( "KiviCare: Encounter updated - ID: {$encounter_id}" );
|
||||
wp_cache_delete( "encounter_{$encounter_id}", 'kivicare' );
|
||||
}
|
||||
|
||||
public static function on_encounter_deleted( $encounter_id ) {
|
||||
// Clean up related data
|
||||
delete_option( "kivicare_encounter_{$encounter_id}_soap_notes" );
|
||||
delete_option( "kivicare_encounter_{$encounter_id}_vital_signs" );
|
||||
delete_option( "kivicare_encounter_{$encounter_id}_preferences" );
|
||||
delete_option( "kivicare_encounter_{$encounter_id}_diagnoses" );
|
||||
delete_option( "kivicare_encounter_{$encounter_id}_attachments" );
|
||||
delete_option( "kivicare_encounter_{$encounter_id}_summary" );
|
||||
|
||||
wp_cache_delete( "encounter_{$encounter_id}", 'kivicare' );
|
||||
error_log( "KiviCare: Encounter deleted - ID: {$encounter_id}" );
|
||||
}
|
||||
|
||||
public static function on_encounter_finalized( $encounter_id ) {
|
||||
error_log( "KiviCare: Encounter finalized - ID: {$encounter_id}" );
|
||||
wp_cache_delete( "encounter_{$encounter_id}", 'kivicare' );
|
||||
}
|
||||
}
|
||||
743
src/includes/services/database/class-patient-service.php
Normal file
743
src/includes/services/database/class-patient-service.php
Normal file
@@ -0,0 +1,743 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Patient Database Service
|
||||
*
|
||||
* Handles advanced patient data operations and business logic
|
||||
*
|
||||
* @package KiviCare_API
|
||||
* @subpackage Services\Database
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar® <dev@descomplicar.pt>
|
||||
* @link https://descomplicar.pt
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace KiviCare_API\Services\Database;
|
||||
|
||||
use KiviCare_API\Models\Patient;
|
||||
use KiviCare_API\Services\Permission_Service;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Patient_Service
|
||||
*
|
||||
* Advanced database service for patient management with business logic
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Patient_Service {
|
||||
|
||||
/**
|
||||
* Initialize the service
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function init() {
|
||||
// Hook into WordPress actions
|
||||
add_action( 'kivicare_patient_created', array( self::class, 'on_patient_created' ), 10, 2 );
|
||||
add_action( 'kivicare_patient_updated', array( self::class, 'on_patient_updated' ), 10, 2 );
|
||||
add_action( 'kivicare_patient_deleted', array( self::class, 'on_patient_deleted' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create patient with advanced business logic
|
||||
*
|
||||
* @param array $patient_data Patient data
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @param int $user_id Creating user ID
|
||||
* @return array|WP_Error Patient data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function create_patient( $patient_data, $clinic_id, $user_id = null ) {
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_access_clinic( get_current_user_id(), $clinic_id ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'You do not have permission to create patients in this clinic',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Enhanced validation
|
||||
$validation = self::validate_patient_business_rules( $patient_data, $clinic_id );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// Add metadata
|
||||
$patient_data['clinic_id'] = $clinic_id;
|
||||
$patient_data['created_by'] = $user_id ?: get_current_user_id();
|
||||
$patient_data['created_at'] = current_time( 'mysql' );
|
||||
|
||||
// Generate patient ID if not provided
|
||||
if ( empty( $patient_data['patient_id'] ) ) {
|
||||
$patient_data['patient_id'] = self::generate_patient_id( $clinic_id );
|
||||
}
|
||||
|
||||
// Create patient
|
||||
$patient_id = Patient::create( $patient_data );
|
||||
|
||||
if ( is_wp_error( $patient_id ) ) {
|
||||
return $patient_id;
|
||||
}
|
||||
|
||||
// Post-creation tasks
|
||||
self::setup_patient_defaults( $patient_id, $patient_data );
|
||||
|
||||
// Trigger action
|
||||
do_action( 'kivicare_patient_created', $patient_id, $patient_data );
|
||||
|
||||
// Return full patient data
|
||||
return self::get_patient_with_metadata( $patient_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update patient with business logic
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @param array $patient_data Updated data
|
||||
* @return array|WP_Error Updated patient data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function update_patient( $patient_id, $patient_data ) {
|
||||
// Get current patient data
|
||||
$current_patient = Patient::get_by_id( $patient_id );
|
||||
if ( ! $current_patient ) {
|
||||
return new \WP_Error(
|
||||
'patient_not_found',
|
||||
'Patient not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_access_clinic( get_current_user_id(), $current_patient['clinic_id'] ) ) {
|
||||
return new \WP_Error(
|
||||
'insufficient_permissions',
|
||||
'You do not have permission to update this patient',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Enhanced validation
|
||||
$validation = self::validate_patient_business_rules( $patient_data, $current_patient['clinic_id'], $patient_id );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
// Add update metadata
|
||||
$patient_data['updated_by'] = get_current_user_id();
|
||||
$patient_data['updated_at'] = current_time( 'mysql' );
|
||||
|
||||
// Update patient
|
||||
$result = Patient::update( $patient_id, $patient_data );
|
||||
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Handle emergency contact changes
|
||||
self::handle_emergency_contact_changes( $patient_id, $current_patient, $patient_data );
|
||||
|
||||
// Trigger action
|
||||
do_action( 'kivicare_patient_updated', $patient_id, $patient_data );
|
||||
|
||||
// Return updated patient data
|
||||
return self::get_patient_with_metadata( $patient_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get patient with enhanced metadata
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @return array|WP_Error Patient data with metadata or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_patient_with_metadata( $patient_id ) {
|
||||
$patient = Patient::get_by_id( $patient_id );
|
||||
|
||||
if ( ! $patient ) {
|
||||
return new \WP_Error(
|
||||
'patient_not_found',
|
||||
'Patient not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_access_clinic( get_current_user_id(), $patient['clinic_id'] ) ) {
|
||||
return new \WP_Error(
|
||||
'access_denied',
|
||||
'You do not have access to this patient',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Add enhanced metadata
|
||||
$patient['appointments'] = self::get_patient_appointments( $patient_id );
|
||||
$patient['encounters'] = self::get_patient_encounters( $patient_id );
|
||||
$patient['prescriptions'] = self::get_patient_prescriptions( $patient_id );
|
||||
$patient['bills'] = self::get_patient_bills( $patient_id );
|
||||
$patient['medical_history'] = self::get_patient_medical_history( $patient_id );
|
||||
$patient['emergency_contacts'] = self::get_patient_emergency_contacts( $patient_id );
|
||||
|
||||
return $patient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search patients with advanced criteria
|
||||
*
|
||||
* @param string $search_term Search term
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @param array $filters Additional filters
|
||||
* @return array Search results
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function search_patients( $search_term, $clinic_id, $filters = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_access_clinic( get_current_user_id(), $clinic_id ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Build search query
|
||||
$where_clauses = array( "p.clinic_id = %d" );
|
||||
$where_values = array( $clinic_id );
|
||||
|
||||
// Search term
|
||||
if ( ! empty( $search_term ) ) {
|
||||
$where_clauses[] = "(p.first_name LIKE %s OR p.last_name LIKE %s OR p.patient_id LIKE %s OR p.contact_no LIKE %s OR p.user_email LIKE %s)";
|
||||
$search_term = '%' . $wpdb->esc_like( $search_term ) . '%';
|
||||
$where_values = array_merge( $where_values, array_fill( 0, 5, $search_term ) );
|
||||
}
|
||||
|
||||
// Age filter
|
||||
if ( ! empty( $filters['age_min'] ) || ! empty( $filters['age_max'] ) ) {
|
||||
if ( ! empty( $filters['age_min'] ) ) {
|
||||
$where_clauses[] = "TIMESTAMPDIFF(YEAR, p.dob, CURDATE()) >= %d";
|
||||
$where_values[] = (int) $filters['age_min'];
|
||||
}
|
||||
if ( ! empty( $filters['age_max'] ) ) {
|
||||
$where_clauses[] = "TIMESTAMPDIFF(YEAR, p.dob, CURDATE()) <= %d";
|
||||
$where_values[] = (int) $filters['age_max'];
|
||||
}
|
||||
}
|
||||
|
||||
// Gender filter
|
||||
if ( ! empty( $filters['gender'] ) ) {
|
||||
$where_clauses[] = "p.gender = %s";
|
||||
$where_values[] = $filters['gender'];
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if ( isset( $filters['status'] ) ) {
|
||||
$where_clauses[] = "p.status = %d";
|
||||
$where_values[] = $filters['status'];
|
||||
} else {
|
||||
$where_clauses[] = "p.status = 1"; // Active by default
|
||||
}
|
||||
|
||||
$where_sql = implode( ' AND ', $where_clauses );
|
||||
|
||||
$query = "SELECT p.*,
|
||||
COUNT(DISTINCT a.id) as appointment_count,
|
||||
MAX(a.appointment_start_date) as last_visit
|
||||
FROM {$wpdb->prefix}kc_patients p
|
||||
LEFT JOIN {$wpdb->prefix}kc_appointments a ON p.id = a.patient_id
|
||||
WHERE {$where_sql}
|
||||
GROUP BY p.id
|
||||
ORDER BY p.first_name, p.last_name
|
||||
LIMIT 50";
|
||||
|
||||
$results = $wpdb->get_results( $wpdb->prepare( $query, $where_values ), ARRAY_A );
|
||||
|
||||
return array_map( function( $patient ) {
|
||||
$patient['id'] = (int) $patient['id'];
|
||||
$patient['appointment_count'] = (int) $patient['appointment_count'];
|
||||
$patient['age'] = $patient['dob'] ? self::calculate_age( $patient['dob'] ) : null;
|
||||
return $patient;
|
||||
}, $results );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get patient dashboard data
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @return array|WP_Error Dashboard data or error
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public static function get_patient_dashboard( $patient_id ) {
|
||||
$patient = Patient::get_by_id( $patient_id );
|
||||
|
||||
if ( ! $patient ) {
|
||||
return new \WP_Error(
|
||||
'patient_not_found',
|
||||
'Patient not found',
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Permission check
|
||||
if ( ! Permission_Service::can_access_clinic( get_current_user_id(), $patient['clinic_id'] ) ) {
|
||||
return new \WP_Error(
|
||||
'access_denied',
|
||||
'You do not have access to this patient dashboard',
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
$dashboard = array();
|
||||
|
||||
// Basic patient info
|
||||
$dashboard['patient'] = $patient;
|
||||
$dashboard['patient']['age'] = self::calculate_age( $patient['dob'] );
|
||||
|
||||
// Recent activity
|
||||
$dashboard['recent_appointments'] = self::get_recent_appointments( $patient_id, 5 );
|
||||
$dashboard['recent_encounters'] = self::get_recent_encounters( $patient_id, 5 );
|
||||
$dashboard['active_prescriptions'] = self::get_active_prescriptions( $patient_id );
|
||||
|
||||
// Medical summary
|
||||
$dashboard['medical_summary'] = self::get_medical_summary( $patient_id );
|
||||
|
||||
// Upcoming appointments
|
||||
$dashboard['upcoming_appointments'] = self::get_upcoming_appointments( $patient_id );
|
||||
|
||||
// Outstanding bills
|
||||
$dashboard['outstanding_bills'] = self::get_outstanding_bills( $patient_id );
|
||||
|
||||
return $dashboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique patient ID
|
||||
*
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @return string Patient ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function generate_patient_id( $clinic_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$prefix = 'P' . str_pad( $clinic_id, 3, '0', STR_PAD_LEFT );
|
||||
|
||||
// Get the highest existing patient ID for this clinic
|
||||
$max_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT MAX(CAST(SUBSTRING(patient_id, 5) AS UNSIGNED))
|
||||
FROM {$wpdb->prefix}kc_patients
|
||||
WHERE clinic_id = %d AND patient_id LIKE %s",
|
||||
$clinic_id,
|
||||
$prefix . '%'
|
||||
)
|
||||
);
|
||||
|
||||
$next_number = ( $max_id ? $max_id + 1 : 1 );
|
||||
|
||||
return $prefix . str_pad( $next_number, 6, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate patient business rules
|
||||
*
|
||||
* @param array $patient_data Patient data
|
||||
* @param int $clinic_id Clinic ID
|
||||
* @param int $patient_id Patient ID (for updates)
|
||||
* @return bool|WP_Error True if valid, WP_Error otherwise
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function validate_patient_business_rules( $patient_data, $clinic_id, $patient_id = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$errors = array();
|
||||
|
||||
// Check for duplicate patient ID in clinic
|
||||
if ( ! empty( $patient_data['patient_id'] ) ) {
|
||||
$existing_query = "SELECT id FROM {$wpdb->prefix}kc_patients WHERE patient_id = %s AND clinic_id = %d";
|
||||
$query_params = array( $patient_data['patient_id'], $clinic_id );
|
||||
|
||||
if ( $patient_id ) {
|
||||
$existing_query .= " AND id != %d";
|
||||
$query_params[] = $patient_id;
|
||||
}
|
||||
|
||||
$existing_patient = $wpdb->get_var( $wpdb->prepare( $existing_query, $query_params ) );
|
||||
|
||||
if ( $existing_patient ) {
|
||||
$errors[] = 'A patient with this ID already exists in the clinic';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate contact information format
|
||||
if ( ! empty( $patient_data['contact_no'] ) ) {
|
||||
if ( ! preg_match( '/^[+]?[0-9\s\-\(\)]{7,20}$/', $patient_data['contact_no'] ) ) {
|
||||
$errors[] = 'Invalid contact number format';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
if ( ! empty( $patient_data['user_email'] ) ) {
|
||||
if ( ! is_email( $patient_data['user_email'] ) ) {
|
||||
$errors[] = 'Invalid email format';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate date of birth
|
||||
if ( ! empty( $patient_data['dob'] ) ) {
|
||||
$dob = strtotime( $patient_data['dob'] );
|
||||
if ( ! $dob || $dob > time() ) {
|
||||
$errors[] = 'Invalid date of birth';
|
||||
}
|
||||
|
||||
// Check for reasonable age limits
|
||||
$age = self::calculate_age( $patient_data['dob'] );
|
||||
if ( $age > 150 ) {
|
||||
$errors[] = 'Date of birth indicates unrealistic age';
|
||||
}
|
||||
}
|
||||
|
||||
// Validate gender
|
||||
if ( ! empty( $patient_data['gender'] ) ) {
|
||||
$valid_genders = array( 'male', 'female', 'other' );
|
||||
if ( ! in_array( strtolower( $patient_data['gender'] ), $valid_genders ) ) {
|
||||
$errors[] = 'Invalid gender value';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $errors ) ) {
|
||||
return new \WP_Error(
|
||||
'patient_business_validation_failed',
|
||||
'Patient business validation failed',
|
||||
array(
|
||||
'status' => 400,
|
||||
'errors' => $errors
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup patient defaults after creation
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @param array $patient_data Patient data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function setup_patient_defaults( $patient_id, $patient_data ) {
|
||||
// Initialize medical history
|
||||
self::initialize_medical_history( $patient_id );
|
||||
|
||||
// Setup default preferences
|
||||
self::setup_default_preferences( $patient_id );
|
||||
|
||||
// Create patient folder structure (if needed)
|
||||
self::create_patient_folder_structure( $patient_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate age from date of birth
|
||||
*
|
||||
* @param string $dob Date of birth
|
||||
* @return int Age in years
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function calculate_age( $dob ) {
|
||||
if ( empty( $dob ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$birth_date = new \DateTime( $dob );
|
||||
$today = new \DateTime();
|
||||
|
||||
return $birth_date->diff( $today )->y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get patient appointments
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @param int $limit Limit
|
||||
* @return array Appointments
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_patient_appointments( $patient_id, $limit = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$query = "SELECT a.*, d.display_name as doctor_name, c.name as clinic_name
|
||||
FROM {$wpdb->prefix}kc_appointments a
|
||||
LEFT JOIN {$wpdb->prefix}users d ON a.doctor_id = d.ID
|
||||
LEFT JOIN {$wpdb->prefix}kc_clinics c ON a.clinic_id = c.id
|
||||
WHERE a.patient_id = %d
|
||||
ORDER BY a.appointment_start_date DESC";
|
||||
|
||||
if ( $limit ) {
|
||||
$query .= " LIMIT {$limit}";
|
||||
}
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare( $query, $patient_id ), ARRAY_A );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get patient encounters
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @param int $limit Limit
|
||||
* @return array Encounters
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_patient_encounters( $patient_id, $limit = null ) {
|
||||
global $wpdb;
|
||||
|
||||
$query = "SELECT e.*, d.display_name as doctor_name
|
||||
FROM {$wpdb->prefix}kc_encounters e
|
||||
LEFT JOIN {$wpdb->prefix}users d ON e.doctor_id = d.ID
|
||||
WHERE e.patient_id = %d
|
||||
ORDER BY e.encounter_date DESC";
|
||||
|
||||
if ( $limit ) {
|
||||
$query .= " LIMIT {$limit}";
|
||||
}
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare( $query, $patient_id ), ARRAY_A );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get patient prescriptions
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @return array Prescriptions
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_patient_prescriptions( $patient_id ) {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT p.*, d.display_name as doctor_name
|
||||
FROM {$wpdb->prefix}kc_prescriptions p
|
||||
LEFT JOIN {$wpdb->prefix}users d ON p.doctor_id = d.ID
|
||||
WHERE p.patient_id = %d
|
||||
ORDER BY p.created_at DESC",
|
||||
$patient_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get patient bills
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @return array Bills
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_patient_bills( $patient_id ) {
|
||||
global $wpdb;
|
||||
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kc_bills
|
||||
WHERE patient_id = %d
|
||||
ORDER BY created_at DESC",
|
||||
$patient_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize medical history for patient
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function initialize_medical_history( $patient_id ) {
|
||||
$default_history = array(
|
||||
'allergies' => array(),
|
||||
'medications' => array(),
|
||||
'conditions' => array(),
|
||||
'surgeries' => array(),
|
||||
'family_history' => array()
|
||||
);
|
||||
|
||||
update_option( "kivicare_patient_{$patient_id}_medical_history", $default_history );
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup default preferences
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function setup_default_preferences( $patient_id ) {
|
||||
$default_preferences = array(
|
||||
'appointment_reminders' => true,
|
||||
'email_notifications' => true,
|
||||
'sms_notifications' => false,
|
||||
'preferred_language' => 'en',
|
||||
'preferred_communication' => 'email'
|
||||
);
|
||||
|
||||
update_option( "kivicare_patient_{$patient_id}_preferences", $default_preferences );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get patient medical history
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @return array Medical history
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_patient_medical_history( $patient_id ) {
|
||||
return get_option( "kivicare_patient_{$patient_id}_medical_history", array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get patient emergency contacts
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @return array Emergency contacts
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function get_patient_emergency_contacts( $patient_id ) {
|
||||
return get_option( "kivicare_patient_{$patient_id}_emergency_contacts", array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle emergency contact changes
|
||||
*
|
||||
* @param int $patient_id Patient ID
|
||||
* @param array $current_data Current patient data
|
||||
* @param array $new_data New patient data
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private static function handle_emergency_contact_changes( $patient_id, $current_data, $new_data ) {
|
||||
// This would handle emergency contact updates
|
||||
// Implementation depends on how emergency contacts are stored
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handlers
|
||||
*/
|
||||
public static function on_patient_created( $patient_id, $patient_data ) {
|
||||
error_log( "KiviCare: New patient created - ID: {$patient_id}, Name: " . ( $patient_data['first_name'] ?? 'Unknown' ) );
|
||||
}
|
||||
|
||||
public static function on_patient_updated( $patient_id, $patient_data ) {
|
||||
error_log( "KiviCare: Patient updated - ID: {$patient_id}" );
|
||||
wp_cache_delete( "patient_{$patient_id}", 'kivicare' );
|
||||
}
|
||||
|
||||
public static function on_patient_deleted( $patient_id ) {
|
||||
// Clean up related data
|
||||
delete_option( "kivicare_patient_{$patient_id}_medical_history" );
|
||||
delete_option( "kivicare_patient_{$patient_id}_preferences" );
|
||||
delete_option( "kivicare_patient_{$patient_id}_emergency_contacts" );
|
||||
|
||||
wp_cache_delete( "patient_{$patient_id}", 'kivicare' );
|
||||
error_log( "KiviCare: Patient deleted - ID: {$patient_id}" );
|
||||
}
|
||||
|
||||
// Additional helper methods would be implemented here...
|
||||
private static function get_recent_appointments( $patient_id, $limit ) {
|
||||
return self::get_patient_appointments( $patient_id, $limit );
|
||||
}
|
||||
|
||||
private static function get_recent_encounters( $patient_id, $limit ) {
|
||||
return self::get_patient_encounters( $patient_id, $limit );
|
||||
}
|
||||
|
||||
private static function get_active_prescriptions( $patient_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kc_prescriptions
|
||||
WHERE patient_id = %d AND status = 'active'
|
||||
ORDER BY created_at DESC",
|
||||
$patient_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_medical_summary( $patient_id ) {
|
||||
return array(
|
||||
'medical_history' => self::get_patient_medical_history( $patient_id ),
|
||||
'last_visit' => self::get_last_visit_date( $patient_id ),
|
||||
'chronic_conditions' => self::get_chronic_conditions( $patient_id ),
|
||||
'active_medications' => self::get_active_medications( $patient_id )
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_upcoming_appointments( $patient_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT a.*, d.display_name as doctor_name
|
||||
FROM {$wpdb->prefix}kc_appointments a
|
||||
LEFT JOIN {$wpdb->prefix}users d ON a.doctor_id = d.ID
|
||||
WHERE a.patient_id = %d AND a.appointment_start_date > NOW()
|
||||
ORDER BY a.appointment_start_date ASC
|
||||
LIMIT 5",
|
||||
$patient_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_outstanding_bills( $patient_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}kc_bills
|
||||
WHERE patient_id = %d AND status = 'pending'
|
||||
ORDER BY created_at DESC",
|
||||
$patient_id
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_last_visit_date( $patient_id ) {
|
||||
global $wpdb;
|
||||
return $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT MAX(appointment_start_date) FROM {$wpdb->prefix}kc_appointments
|
||||
WHERE patient_id = %d AND status = 2",
|
||||
$patient_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static function get_chronic_conditions( $patient_id ) {
|
||||
$medical_history = self::get_patient_medical_history( $patient_id );
|
||||
return isset( $medical_history['conditions'] ) ?
|
||||
array_filter( $medical_history['conditions'], function( $condition ) {
|
||||
return isset( $condition['chronic'] ) && $condition['chronic'];
|
||||
} ) : array();
|
||||
}
|
||||
|
||||
private static function get_active_medications( $patient_id ) {
|
||||
$medical_history = self::get_patient_medical_history( $patient_id );
|
||||
return isset( $medical_history['medications'] ) ?
|
||||
array_filter( $medical_history['medications'], function( $medication ) {
|
||||
return isset( $medication['active'] ) && $medication['active'];
|
||||
} ) : array();
|
||||
}
|
||||
|
||||
private static function create_patient_folder_structure( $patient_id ) {
|
||||
// Implementation for creating patient document folders if needed
|
||||
// This would depend on the file management system
|
||||
}
|
||||
}
|
||||
1031
src/includes/services/database/class-prescription-service.php
Normal file
1031
src/includes/services/database/class-prescription-service.php
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user