plugin_url = plugin_dir_url( __FILE__ ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue' ] ); add_action( 'rest_api_init', [ $this, 'register_routes' ] ); add_shortcode( 'autobooking_admin', [ $this, 'render' ] ); register_activation_hook( __FILE__, [ $this, 'activate' ] ); } /* ============================================================ ACTIVACION ============================================================ */ public function activate() { global $wpdb; $charset = $wpdb->get_charset_collate(); require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta( "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}ab_admin_audit ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, admin_user_id BIGINT UNSIGNED NOT NULL, action VARCHAR(60) NOT NULL DEFAULT '', target_type VARCHAR(40) NOT NULL DEFAULT '', target_id BIGINT UNSIGNED NOT NULL DEFAULT 0, meta LONGTEXT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY admin_user_id (admin_user_id), KEY target (target_type, target_id), KEY created_at (created_at) ) $charset;" ); dbDelta( "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}ab_fare_config ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, country_code CHAR(2) NOT NULL DEFAULT '', currency CHAR(3) NOT NULL DEFAULT 'USD', base_fare DECIMAL(10,2) NOT NULL DEFAULT 3.00, per_km DECIMAL(10,4) NOT NULL DEFAULT 1.80, per_minute DECIMAL(10,4) NOT NULL DEFAULT 0.30, platform_fee_pct DECIMAL(5,4) NOT NULL DEFAULT 0.2000, minimum_fare DECIMAL(10,2) NOT NULL DEFAULT 5.00, active TINYINT(1) NOT NULL DEFAULT 1, updated_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY country_code (country_code) ) $charset;" ); // Indice en wp_ab_trip_positions (tabla de alto volumen) si no existe $idx = $wpdb->get_var( "SHOW INDEX FROM {$wpdb->prefix}ab_trip_positions WHERE Key_name = 'trip_ts'" ); if ( ! $idx ) { $wpdb->query( "ALTER TABLE {$wpdb->prefix}ab_trip_positions ADD INDEX trip_ts (trip_id, ts)" ); } } /* ============================================================ PERMISSION ============================================================ */ private function is_admin() { return is_user_logged_in() && current_user_can( 'manage_autobooking' ); } private function perm() { if ( ! $this->is_admin() ) { return new WP_Error( 'forbidden', 'Acceso denegado.', [ 'status' => 403 ] ); } return true; } private function audit( $action, $target_type, $target_id, $meta = [] ) { global $wpdb; $wpdb->insert( "{$wpdb->prefix}ab_admin_audit", [ 'admin_user_id' => get_current_user_id(), 'action' => sanitize_text_field( $action ), 'target_type' => sanitize_text_field( $target_type ), 'target_id' => absint( $target_id ), 'meta' => $meta ? wp_json_encode( $meta ) : null, 'created_at' => current_time( 'mysql' ), ] ); } /* ============================================================ ENQUEUE ============================================================ */ public function enqueue() { global $post; if ( ! is_a( $post, 'WP_Post' ) || ! has_shortcode( $post->post_content, 'autobooking_admin' ) ) return; if ( ! $this->is_admin() ) return; wp_enqueue_style( 'abad-style', $this->plugin_url . 'assets/admin-dashboard.css', [], self::VERSION ); wp_enqueue_script( 'google-maps-abad', 'https://maps.googleapis.com/maps/api/js?key=' . esc_attr( get_option( 'ab_gmaps_key', '' ) ) . '&libraries=places,drawing', [], null, true ); wp_enqueue_script( 'abad-script', $this->plugin_url . 'assets/admin-dashboard.js', [], self::VERSION, true ); $user = wp_get_current_user(); wp_localize_script( 'abad-script', 'AB_ADMIN_CFG', [ 'nonce' => wp_create_nonce( 'wp_rest' ), 'api_root' => esc_url_raw( get_rest_url() ), 'gmaps_key' => esc_attr( get_option( 'ab_gmaps_key', '' ) ), 'user_id' => $user->ID, 'user_name' => esc_html( $user->display_name ), ] ); } /* ============================================================ REST ROUTES ============================================================ */ public function register_routes() { $ns = self::NS; $perm = [ $this, 'perm' ]; register_rest_route( $ns, '/admin/overview', [ 'methods' => 'GET', 'callback' => [ $this, 'rest_overview' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/drivers', [ 'methods' => 'GET', 'callback' => [ $this, 'rest_drivers' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/drivers/pending', [ 'methods' => 'GET', 'callback' => [ $this, 'rest_drivers_pending' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/drivers/approve', [ 'methods' => 'POST', 'callback' => [ $this, 'rest_driver_approve' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/drivers/reject', [ 'methods' => 'POST', 'callback' => [ $this, 'rest_driver_reject' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/drivers/suspend', [ 'methods' => 'POST', 'callback' => [ $this, 'rest_driver_suspend' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/companies', [ 'methods' => 'GET', 'callback' => [ $this, 'rest_companies' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/companies/activate', [ 'methods' => 'POST', 'callback' => [ $this, 'rest_company_activate' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/companies/deactivate',[ 'methods' => 'POST', 'callback' => [ $this, 'rest_company_deactivate' ],'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/companies/(?P\d+)/invoices', [ 'methods' => 'GET', 'callback' => [ $this, 'rest_company_invoices' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/trips', [ 'methods' => 'GET', 'callback' => [ $this, 'rest_trips' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/trips/(?P\d+)', [ 'methods' => 'GET', 'callback' => [ $this, 'rest_trip_detail' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/trips/export-csv', [ 'methods' => 'GET', 'callback' => [ $this, 'rest_trips_csv' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/incidents', [ 'methods' => 'GET', 'callback' => [ $this, 'rest_incidents' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/incidents/(?P\d+)/resolve', [ 'methods' => 'POST', 'callback' => [ $this, 'rest_incident_resolve' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/zones', [ 'methods' => 'GET', 'callback' => [ $this, 'rest_zones' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/zones/save', [ 'methods' => 'POST', 'callback' => [ $this, 'rest_zone_save' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/zones/(?P\d+)/deactivate', [ 'methods' => 'POST', 'callback' => [ $this, 'rest_zone_deactivate' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/settings', [ 'methods' => 'GET', 'callback' => [ $this, 'rest_settings_get' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/settings/save', [ 'methods' => 'POST', 'callback' => [ $this, 'rest_settings_save' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/fare-config', [ 'methods' => 'GET', 'callback' => [ $this, 'rest_fare_get' ], 'permission_callback' => $perm ] ); register_rest_route( $ns, '/admin/fare-config/save', [ 'methods' => 'POST', 'callback' => [ $this, 'rest_fare_save' ], 'permission_callback' => $perm ] ); } /* ============================================================ OVERVIEW ============================================================ */ public function rest_overview( $req ) { global $wpdb; $today_start = date( 'Y-m-d 00:00:00' ); $today_end = date( 'Y-m-d 23:59:59' ); $week_start = date( 'Y-m-d 00:00:00', strtotime( '-6 days' ) ); $trips_today = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}ab_trips WHERE created_at BETWEEN %s AND %s", $today_start, $today_end ) ); $trips_week = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}ab_trips WHERE created_at >= %s", $week_start ) ); $active_trips = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}ab_trips WHERE status IN ('assigned','en_route','waiting','in_progress')" ); $drivers_online = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}ab_driver_status WHERE online = 1" ); $revenue_today = (float) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(fare_total_amount),0) FROM {$wpdb->prefix}ab_trips WHERE status='finished' AND created_at BETWEEN %s AND %s", $today_start, $today_end ) ); $sos_open = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}ab_incidents WHERE status='active'" ); $pending_drivers= (int) ( new WP_User_Query( [ 'role' => 'driver_pending', 'count_total' => true, 'number' => 0 ] ) )->get_total(); $chart = $wpdb->get_results( "SELECT DATE(created_at) AS the_date, COUNT(*) AS total, COALESCE(SUM(fare_total_amount),0) AS revenue FROM {$wpdb->prefix}ab_trips WHERE created_at >= NOW() - INTERVAL 30 DAY GROUP BY DATE(created_at) ORDER BY the_date ASC", ARRAY_A ); return rest_ensure_response( compact( 'trips_today','trips_week','active_trips','drivers_online','revenue_today','sos_open','pending_drivers' ) + [ 'chart' => $chart ?: [] ] ); } /* ============================================================ DRIVERS ============================================================ */ public function rest_drivers( $req ) { $search = sanitize_text_field( $req->get_param( 'search' ) ?: '' ); $filter = sanitize_text_field( $req->get_param( 'filter' ) ?: '' ); $page = max( 1, (int) ( $req->get_param( 'page' ) ?: 1 ) ); $per = 20; $args = [ 'role' => 'driver', 'number' => $per, 'offset' => ( $page - 1 ) * $per, 'orderby' => 'display_name', 'order' => 'ASC' ]; if ( $search ) { $args['search'] = '*' . $search . '*'; $args['search_columns'] = [ 'display_name', 'user_email' ]; } $query = new WP_User_Query( $args ); $total = $query->get_total(); $drivers = []; global $wpdb; foreach ( $query->get_results() as $user ) { $uid = $user->ID; $status = $wpdb->get_row( $wpdb->prepare( "SELECT online, last_seen, last_lat, last_lng FROM {$wpdb->prefix}ab_driver_status WHERE driver_id = %d", $uid ) ); if ( $filter === 'online' && ( ! $status || ! $status->online ) ) continue; if ( $filter === 'offline' && $status && $status->online ) continue; $rating = (float) $wpdb->get_var( $wpdb->prepare( "SELECT AVG(driver_rating) FROM {$wpdb->prefix}ab_trips WHERE driver_id = %d AND driver_rating > 0", $uid ) ); $trips_total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}ab_trips WHERE driver_id = %d AND status='finished'", $uid ) ); $drivers[] = [ 'id' => $uid, 'name' => $user->display_name, 'email' => $user->user_email, 'phone' => get_user_meta( $uid, 'phone', true ), 'vehicle_type' => get_user_meta( $uid, 'vehicle_type', true ), 'vehicle_plate' => get_user_meta( $uid, 'vehicle_plate', true ) ?: get_user_meta( $uid, 'license_plate', true ), 'photo_url' => get_user_meta( $uid, 'photo_url', true ), 'online' => $status ? (bool) $status->online : false, 'last_seen' => $status ? $status->last_seen : null, 'avg_rating' => round( $rating, 2 ), 'trips_total' => $trips_total, 'registered' => $user->user_registered, ]; } return rest_ensure_response( compact( 'drivers', 'total', 'page' ) + [ 'per_page' => $per ] ); } public function rest_drivers_pending( $req ) { $query = new WP_User_Query( [ 'role' => 'driver_pending', 'number' => 100, 'orderby' => 'registered', 'order' => 'ASC' ] ); $list = []; foreach ( $query->get_results() as $user ) { $uid = $user->ID; $list[] = [ 'id' => $uid, 'name' => $user->display_name, 'email' => $user->user_email, 'phone' => get_user_meta( $uid, 'phone', true ), 'vehicle_type' => get_user_meta( $uid, 'vehicle_type', true ), 'vehicle_plate' => get_user_meta( $uid, 'vehicle_plate', true ) ?: get_user_meta( $uid, 'license_plate', true ), 'license_expiry' => get_user_meta( $uid, 'license_expiry', true ), 'insurance_expiry' => get_user_meta( $uid, 'insurance_expiry', true ), 'photo_url' => get_user_meta( $uid, 'photo_url', true ), 'registered' => $user->user_registered, // TODO: documentos (wp_ab_driver_documents) pendiente de crear - ver CHANGES.md ]; } return rest_ensure_response( [ 'drivers' => $list, 'total' => count( $list ) ] ); } public function rest_driver_approve( $req ) { $uid = absint( $req->get_param( 'user_id' ) ); $user = $uid ? get_userdata( $uid ) : null; if ( ! $user ) return new WP_Error( 'not_found', 'Usuario no encontrado.', [ 'status' => 404 ] ); if ( ! in_array( 'driver_pending', (array) $user->roles, true ) ) { return new WP_Error( 'bad_state', 'El usuario no es driver_pending.', [ 'status' => 400 ] ); } $user->remove_role( 'driver_pending' ); $user->add_role( 'driver' ); $this->audit( 'driver_approve', 'user', $uid ); return rest_ensure_response( [ 'ok' => true, 'msg' => 'Conductor aprobado.' ] ); } public function rest_driver_reject( $req ) { $uid = absint( $req->get_param( 'user_id' ) ); $reason = sanitize_textarea_field( $req->get_param( 'reason' ) ?: '' ); $user = $uid ? get_userdata( $uid ) : null; if ( ! $user ) return new WP_Error( 'not_found', 'Usuario no encontrado.', [ 'status' => 404 ] ); $user->set_role( 'subscriber' ); if ( $reason ) update_user_meta( $uid, 'ab_rejection_reason', $reason ); $this->audit( 'driver_reject', 'user', $uid, [ 'reason' => $reason ] ); return rest_ensure_response( [ 'ok' => true, 'msg' => 'Conductor rechazado.' ] ); } public function rest_driver_suspend( $req ) { global $wpdb; $uid = absint( $req->get_param( 'user_id' ) ); $reason = sanitize_textarea_field( $req->get_param( 'reason' ) ?: '' ); $user = $uid ? get_userdata( $uid ) : null; if ( ! $user ) return new WP_Error( 'not_found', 'Usuario no encontrado.', [ 'status' => 404 ] ); $user->set_role( 'subscriber' ); update_user_meta( $uid, 'ab_suspended', 1 ); if ( $reason ) update_user_meta( $uid, 'ab_suspension_reason', $reason ); $wpdb->update( "{$wpdb->prefix}ab_driver_status", [ 'online' => 0 ], [ 'driver_id' => $uid ] ); $this->audit( 'driver_suspend', 'user', $uid, [ 'reason' => $reason ] ); return rest_ensure_response( [ 'ok' => true, 'msg' => 'Conductor suspendido.' ] ); } /* ============================================================ COMPANIES ============================================================ */ public function rest_companies( $req ) { global $wpdb; $search = sanitize_text_field( $req->get_param( 'search' ) ?: '' ); $page = max( 1, (int) ( $req->get_param( 'page' ) ?: 1 ) ); $per = 20; $where = '1=1'; if ( $search ) $where .= $wpdb->prepare( ' AND (name LIKE %s OR email LIKE %s)', '%' . $wpdb->esc_like( $search ) . '%', '%' . $wpdb->esc_like( $search ) . '%' ); $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}ab_companies WHERE $where" ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}ab_companies WHERE $where ORDER BY created_at DESC LIMIT %d OFFSET %d", $per, ( $page - 1 ) * $per ), ARRAY_A ); foreach ( $rows as &$row ) { $cid = (int) $row['id']; $row['trips_count'] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}ab_trips WHERE company_id = %d", $cid ) ); $wp_uid = $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key='ab_company_id' AND meta_value=%s LIMIT 1", $cid ) ); $row['wp_user_id'] = $wp_uid ? (int) $wp_uid : null; } unset( $row ); return rest_ensure_response( [ 'companies' => $rows ?: [], 'total' => $total, 'page' => $page, 'per_page' => $per ] ); } public function rest_company_activate( $req ) { global $wpdb; $cid = absint( $req->get_param( 'company_id' ) ); if ( ! $cid ) return new WP_Error( 'bad_request', 'company_id requerido.', [ 'status' => 400 ] ); $wpdb->update( "{$wpdb->prefix}ab_companies", [ 'active' => 1 ], [ 'id' => $cid ] ); $user_ids = $wpdb->get_col( $wpdb->prepare( "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key='ab_company_id' AND meta_value=%s", $cid ) ); foreach ( $user_ids as $uid ) { $u = get_userdata( (int) $uid ); if ( $u && ! in_array( 'corporate_admin', (array) $u->roles, true ) ) $u->add_role( 'corporate_admin' ); } $this->audit( 'company_activate', 'company', $cid ); return rest_ensure_response( [ 'ok' => true, 'msg' => 'Empresa activada.' ] ); } public function rest_company_deactivate( $req ) { global $wpdb; $cid = absint( $req->get_param( 'company_id' ) ); if ( ! $cid ) return new WP_Error( 'bad_request', 'company_id requerido.', [ 'status' => 400 ] ); $wpdb->update( "{$wpdb->prefix}ab_companies", [ 'active' => 0 ], [ 'id' => $cid ] ); $user_ids = $wpdb->get_col( $wpdb->prepare( "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key='ab_company_id' AND meta_value=%s", $cid ) ); foreach ( $user_ids as $uid ) { $u = get_userdata( (int) $uid ); if ( $u ) $u->remove_role( 'corporate_admin' ); } $this->audit( 'company_deactivate', 'company', $cid ); return rest_ensure_response( [ 'ok' => true, 'msg' => 'Empresa desactivada.' ] ); } public function rest_company_invoices( $req ) { global $wpdb; $cid = absint( $req['id'] ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}ab_invoices WHERE company_id = %d ORDER BY created_at DESC LIMIT 24", $cid ), ARRAY_A ); return rest_ensure_response( [ 'invoices' => $rows ?: [] ] ); } /* ============================================================ TRIPS ============================================================ */ public function rest_trips( $req ) { global $wpdb; $page = max( 1, (int) ( $req->get_param( 'page' ) ?: 1 ) ); $per = 25; $status = sanitize_text_field( $req->get_param( 'status' ) ?: '' ); $driver_id = absint( $req->get_param( 'driver_id' ) ?: 0 ); $date_from = sanitize_text_field( $req->get_param( 'date_from' ) ?: '' ); $date_to = sanitize_text_field( $req->get_param( 'date_to' ) ?: '' ); $search = sanitize_text_field( $req->get_param( 'search' ) ?: '' ); $where = '1=1'; if ( $status ) $where .= $wpdb->prepare( ' AND status = %s', $status ); if ( $driver_id ) $where .= $wpdb->prepare( ' AND driver_id = %d', $driver_id ); if ( $date_from ) $where .= $wpdb->prepare( ' AND DATE(created_at) >= %s', $date_from ); if ( $date_to ) $where .= $wpdb->prepare( ' AND DATE(created_at) <= %s', $date_to ); if ( $search ) $where .= $wpdb->prepare( ' AND (passenger_name LIKE %s OR driver_name LIKE %s OR trip_uuid LIKE %s)', '%' . $wpdb->esc_like( $search ) . '%', '%' . $wpdb->esc_like( $search ) . '%', '%' . $wpdb->esc_like( $search ) . '%' ); $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}ab_trips WHERE $where" ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, trip_uuid, status, driver_id, driver_name, passenger_name, fare_total_amount, platform_fee_amount, driver_payout_amount, currency, driver_rating, created_at, updated_at FROM {$wpdb->prefix}ab_trips WHERE $where ORDER BY created_at DESC LIMIT %d OFFSET %d", $per, ( $page - 1 ) * $per ), ARRAY_A ); return rest_ensure_response( [ 'trips' => $rows ?: [], 'total' => $total, 'page' => $page, 'per_page' => $per ] ); } public function rest_trip_detail( $req ) { global $wpdb; $id = absint( $req['id'] ); $trip = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}ab_trips WHERE id = %d", $id ), ARRAY_A ); if ( ! $trip ) return new WP_Error( 'not_found', 'Viaje no encontrado.', [ 'status' => 404 ] ); // Posiciones acotadas a 500 para rendimiento (tabla de alto volumen) $positions = $wpdb->get_results( $wpdb->prepare( "SELECT lat, lng, ts FROM {$wpdb->prefix}ab_trip_positions WHERE trip_id = %d ORDER BY ts ASC LIMIT 500", $id ), ARRAY_A ); $chat = $wpdb->get_results( $wpdb->prepare( "SELECT sender, message, created_at FROM {$wpdb->prefix}autobooking_chat WHERE ride_id = %d ORDER BY created_at ASC LIMIT 200", $id ), ARRAY_A ); return rest_ensure_response( [ 'trip' => $trip, 'positions' => $positions ?: [], 'chat' => $chat ?: [] ] ); } public function rest_trips_csv( $req ) { global $wpdb; $date_from = sanitize_text_field( $req->get_param( 'date_from' ) ?: '' ); $date_to = sanitize_text_field( $req->get_param( 'date_to' ) ?: '' ); $status = sanitize_text_field( $req->get_param( 'status' ) ?: '' ); $where = '1=1'; if ( $status ) $where .= $wpdb->prepare( ' AND status = %s', $status ); if ( $date_from ) $where .= $wpdb->prepare( ' AND DATE(created_at) >= %s', $date_from ); if ( $date_to ) $where .= $wpdb->prepare( ' AND DATE(created_at) <= %s', $date_to ); $rows = $wpdb->get_results( "SELECT id, trip_uuid, created_at, driver_name, passenger_name, fare_total_amount, platform_fee_amount, driver_payout_amount, currency, status, driver_rating FROM {$wpdb->prefix}ab_trips WHERE $where ORDER BY created_at DESC LIMIT 10000", ARRAY_A ); $csv = '"ID","UUID","Fecha","Conductor","Pasajero","Total","Comision","Neto","Moneda","Estado","Rating"' . "\n"; foreach ( $rows as $r ) { $csv .= implode( ',', array_map( fn($c) => '"' . str_replace( '"', '""', (string) $c ) . '"', array_values( $r ) ) ) . "\n"; } return rest_ensure_response( [ 'csv' => $csv, 'filename' => 'viajes-' . date( 'Y-m-d' ) . '.csv' ] ); } /* ============================================================ INCIDENTS ============================================================ */ public function rest_incidents( $req ) { global $wpdb; $filter = sanitize_text_field( $req->get_param( 'filter' ) ?: 'active' ); $page = max( 1, (int) ( $req->get_param( 'page' ) ?: 1 ) ); $per = 20; $where = '1=1'; if ( $filter && $filter !== 'all' ) $where .= $wpdb->prepare( ' AND i.status = %s', $filter ); $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}ab_incidents i WHERE $where" ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT i.*, u.display_name AS driver_name FROM {$wpdb->prefix}ab_incidents i LEFT JOIN {$wpdb->users} u ON u.ID = i.driver_id WHERE $where ORDER BY (i.status = 'active') DESC, i.incident_at DESC LIMIT %d OFFSET %d", $per, ( $page - 1 ) * $per ), ARRAY_A ); $upload_dir = wp_upload_dir(); foreach ( $rows as &$row ) { $dir = $upload_dir['basedir'] . '/ab-incidents/' . $row['id']; $row['has_audio'] = is_dir( $dir ) && ! empty( glob( $dir . '/*.webm' ) ); } unset( $row ); return rest_ensure_response( [ 'incidents' => $rows ?: [], 'total' => $total, 'page' => $page ] ); } public function rest_incident_resolve( $req ) { global $wpdb; $id = absint( $req['id'] ); $note = sanitize_textarea_field( $req->get_param( 'note' ) ?: '' ); $status = in_array( $req->get_param( 'status' ), [ 'resolved', 'attended' ], true ) ? $req->get_param( 'status' ) : 'resolved'; $inc = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}ab_incidents WHERE id = %d", $id ) ); if ( ! $inc ) return new WP_Error( 'not_found', 'Incidente no encontrado.', [ 'status' => 404 ] ); $wpdb->update( "{$wpdb->prefix}ab_incidents", [ 'status' => $status, 'resolved_at' => current_time( 'mysql' ), 'notes' => $note ], [ 'id' => $id ] ); $this->audit( 'incident_' . $status, 'incident', $id, [ 'note' => $note ] ); return rest_ensure_response( [ 'ok' => true ] ); } /* ============================================================ ZONES ============================================================ */ public function rest_zones( $req ) { global $wpdb; return rest_ensure_response( $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}ab_zone_alerts ORDER BY active DESC, created_at DESC", ARRAY_A ) ?: [] ); } public function rest_zone_save( $req ) { global $wpdb; $body = $req->get_json_params(); $id = absint( $body['id'] ?? 0 ); $title = sanitize_text_field( $body['title'] ?? '' ); $lat = (float) ( $body['lat'] ?? 0 ); $lng = (float) ( $body['lng'] ?? 0 ); $type = in_array( $body['type'] ?? '', [ 'hotspot', 'bonus', 'warning' ], true ) ? $body['type'] : 'hotspot'; if ( ! $title || ! $lat || ! $lng ) return new WP_Error( 'missing', 'Titulo, lat y lng son requeridos.', [ 'status' => 400 ] ); $data = [ 'title' => $title, 'description' => sanitize_textarea_field( $body['description'] ?? '' ), 'lat' => $lat, 'lng' => $lng, 'radius_km' => max( 0.1, (float) ( $body['radius_km'] ?? 2 ) ), 'bonus_amount' => max( 0, (float) ( $body['bonus_amount'] ?? 0 ) ), 'bonus_currency' => sanitize_text_field( $body['bonus_currency'] ?? 'USD' ), 'countdown_seconds' => max( 60, (int) ( $body['countdown_seconds'] ?? 180 ) ), 'type' => $type, 'active' => 1, 'expires_at' => sanitize_text_field( $body['expires_at'] ?? '' ) ?: null, ]; if ( $id ) { $wpdb->update( "{$wpdb->prefix}ab_zone_alerts", $data, [ 'id' => $id ] ); $this->audit( 'zone_update', 'zone', $id ); } else { $data['created_at'] = current_time( 'mysql' ); $wpdb->insert( "{$wpdb->prefix}ab_zone_alerts", $data ); $id = $wpdb->insert_id; $this->audit( 'zone_create', 'zone', $id ); } return rest_ensure_response( [ 'ok' => true, 'id' => $id ] ); } public function rest_zone_deactivate( $req ) { global $wpdb; $id = absint( $req['id'] ); $wpdb->update( "{$wpdb->prefix}ab_zone_alerts", [ 'active' => 0 ], [ 'id' => $id ] ); $this->audit( 'zone_deactivate', 'zone', $id ); return rest_ensure_response( [ 'ok' => true ] ); } /* ============================================================ SETTINGS & FARE CONFIG ============================================================ */ public function rest_settings_get( $req ) { return rest_ensure_response( [ 'platform_name' => get_option( 'ab_platform_name', 'AutoBooking' ), 'platform_logo' => get_option( 'ab_platform_logo', '' ), 'sos_email' => get_option( 'ab_sos_email', get_option( 'admin_email' ) ), 'gmaps_key' => get_option( 'ab_gmaps_key', '' ), 'platform_fee_pct' => (float) get_option( 'ab_platform_fee_pct', 0.20 ), ] ); } public function rest_settings_save( $req ) { $body = $req->get_json_params(); if ( isset( $body['platform_name'] ) ) update_option( 'ab_platform_name', sanitize_text_field( $body['platform_name'] ) ); if ( isset( $body['platform_logo'] ) ) update_option( 'ab_platform_logo', esc_url_raw( $body['platform_logo'] ) ); if ( isset( $body['sos_email'] ) ) update_option( 'ab_sos_email', sanitize_email( $body['sos_email'] ) ); if ( isset( $body['gmaps_key'] ) ) update_option( 'ab_gmaps_key', sanitize_text_field( $body['gmaps_key'] ) ); if ( isset( $body['platform_fee_pct'] ) ) update_option( 'ab_platform_fee_pct', max( 0, min( 1, (float) $body['platform_fee_pct'] ) ) ); $this->audit( 'settings_save', 'settings', 0 ); return rest_ensure_response( [ 'ok' => true ] ); } public function rest_fare_get( $req ) { global $wpdb; return rest_ensure_response( $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}ab_fare_config ORDER BY country_code ASC", ARRAY_A ) ?: [] ); } public function rest_fare_save( $req ) { global $wpdb; $body = $req->get_json_params(); $country_code = strtoupper( sanitize_text_field( $body['country_code'] ?? '' ) ); if ( strlen( $country_code ) !== 2 ) return new WP_Error( 'bad_request', 'country_code debe ser 2 letras ISO 3166.', [ 'status' => 400 ] ); $data = [ 'country_code' => $country_code, 'currency' => strtoupper( sanitize_text_field( $body['currency'] ?? 'USD' ) ), 'base_fare' => max( 0, (float) ( $body['base_fare'] ?? 3 ) ), 'per_km' => max( 0, (float) ( $body['per_km'] ?? 1.80 ) ), 'per_minute' => max( 0, (float) ( $body['per_minute'] ?? 0.30 ) ), 'platform_fee_pct' => max( 0, min( 1, (float) ( $body['platform_fee_pct'] ?? 0.20 ) ) ), 'minimum_fare' => max( 0, (float) ( $body['minimum_fare'] ?? 5 ) ), 'active' => 1, 'updated_at' => current_time( 'mysql' ), ]; $existing = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}ab_fare_config WHERE country_code = %s", $country_code ) ); if ( $existing ) $wpdb->update( "{$wpdb->prefix}ab_fare_config", $data, [ 'country_code' => $country_code ] ); else $wpdb->insert( "{$wpdb->prefix}ab_fare_config", $data ); $this->audit( 'fare_save', 'fare_config', 0, [ 'country' => $country_code ] ); return rest_ensure_response( [ 'ok' => true ] ); } /* ============================================================ SHORTCODE RENDER ============================================================ */ public function render( $atts ) { if ( ! is_user_logged_in() ) { wp_redirect( wp_login_url( get_permalink() ) ); exit; } if ( ! current_user_can( 'manage_autobooking' ) ) { return '

Acceso Restringido

Esta seccion es exclusiva para administradores de la plataforma.

Cerrar sesion
'; } $user = wp_get_current_user(); $logo = get_option( 'ab_platform_logo', '' ); $plat_name = esc_html( get_option( 'ab_platform_name', 'AutoBooking' ) ); ob_start(); ?>
Admin Panel
display_name ); ?>
Viajes hoy
Esta semana
Activos ahora
Conductores online
Activos ahora
Conductores online
Ingresos hoy
SOS activos
Pendientes
Viajes — Ultimos 30 dias