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:
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