Files
AutoBooking/autobooking-driver-dashboard.php
T

1341 lines
58 KiB
PHP

<?php
/**
* Plugin Name: Autobooking Driver Dashboard
* Description: Dashboard unificado del Conductor v2 — mapa, viaje, ganancias, métricas, historial, perfil y pánico.
* Version: 2.0.0
* Author: Autobooking
*/
if ( ! defined( 'ABSPATH' ) ) exit;
class ABDriverDashboard2 {
private static $instance = null;
const NAMESPACE = 'autobooking/v1';
const VERSION = '2.0.0';
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue' ] );
add_action( 'rest_api_init', [ $this, 'register_routes' ] );
add_shortcode( 'autobooking_driver_dashboard', [ $this, 'render' ] );
register_activation_hook( __FILE__, [ $this, 'activate' ] );
}
/* ─────────────────────────────────────────────────────────────
ACTIVATION — create new tables
───────────────────────────────────────────────────────────── */
public function activate() {
global $wpdb;
$charset = $wpdb->get_charset_collate();
$sql = [];
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}ab_incidents (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
driver_id BIGINT NOT NULL,
trip_id BIGINT NULL,
lat DECIMAL(10,7) NOT NULL DEFAULT 0,
lng DECIMAL(10,7) NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'active',
chunks_count INT NOT NULL DEFAULT 0,
incident_at DATETIME NOT NULL,
resolved_at DATETIME NULL,
notes TEXT NULL,
PRIMARY KEY (id),
KEY driver_id (driver_id)
) $charset;";
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}ab_zone_alerts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(160) NOT NULL DEFAULT '',
description TEXT NULL,
lat DECIMAL(10,7) NOT NULL DEFAULT 0,
lng DECIMAL(10,7) NOT NULL DEFAULT 0,
radius_km DECIMAL(5,2) NOT NULL DEFAULT 1.00,
bonus_amount DECIMAL(10,2) NOT NULL DEFAULT 0,
bonus_currency CHAR(3) NOT NULL DEFAULT 'USD',
countdown_seconds INT NOT NULL DEFAULT 180,
type VARCHAR(20) NOT NULL DEFAULT 'hotspot',
active TINYINT(1) NOT NULL DEFAULT 1,
expires_at DATETIME NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id)
) $charset;";
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}ab_zone_alert_responses (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
zone_id BIGINT NOT NULL,
driver_id BIGINT NOT NULL,
response VARCHAR(20) NOT NULL DEFAULT '',
responded_at DATETIME NOT NULL,
PRIMARY KEY (id),
KEY zone_driver (zone_id, driver_id)
) $charset;";
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}ab_scheduled_trips (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
trip_uuid VARCHAR(64) NOT NULL DEFAULT '',
driver_id BIGINT NOT NULL,
passenger_name VARCHAR(160) NOT NULL DEFAULT '',
pickup_time DATETIME NOT NULL,
pickup_address VARCHAR(255) NOT NULL DEFAULT '',
dropoff_address VARCHAR(255) NOT NULL DEFAULT '',
estimated_fare DECIMAL(10,2) NOT NULL DEFAULT 0,
currency CHAR(3) NOT NULL DEFAULT 'USD',
confirmed TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY driver_id (driver_id)
) $charset;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
foreach ( $sql as $s ) {
dbDelta( $s );
}
}
/* ─────────────────────────────────────────────────────────────
ENQUEUE
───────────────────────────────────────────────────────────── */
public function enqueue() {
global $post;
if ( ! is_a( $post, 'WP_Post' ) || ! has_shortcode( $post->post_content, 'autobooking_driver_dashboard' ) ) {
return;
}
$base = plugin_dir_url( __FILE__ );
$ver = self::VERSION;
// Google Maps
// Google Maps API key is stored in wp_options (ab_gmaps_key) and set via Admin → Config.
// NEVER hardcode the key here — read it from the database at runtime.
$gmaps_key = get_option( 'ab_gmaps_key', '' );
// CSS
wp_enqueue_style(
'abd-dashboard',
$base . 'assets/driver-dashboard.css',
[],
$ver
);
// JS
wp_enqueue_script(
'abd-dashboard',
$base . 'assets/driver-dashboard.js',
[],
$ver,
true
);
// Build driver data from user meta
$user = wp_get_current_user();
$uid = $user->ID;
$driver_name = get_user_meta( $uid, 'driver_name', true );
if ( ! $driver_name ) {
$driver_name = get_user_meta( $uid, 'full_name', true );
}
if ( ! $driver_name ) {
$fn = get_user_meta( $uid, 'first_name', true );
$ln = get_user_meta( $uid, 'last_name', true );
$driver_name = trim( "$fn $ln" );
}
if ( ! $driver_name ) {
$driver_name = $user->display_name;
}
$photo = get_user_meta( $uid, 'photo_url', true );
if ( ! $photo ) {
$photo = get_user_meta( $uid, 'driver_photo', true );
}
$vehicle_type = get_user_meta( $uid, 'vehicle_type', true );
$vehicle_plate = get_user_meta( $uid, 'vehicle_plate', true );
if ( ! $vehicle_plate ) {
$vehicle_plate = get_user_meta( $uid, 'license_plate', true );
}
$insurance_expiry = get_user_meta( $uid, 'insurance_expiry', true );
$license_expiry = get_user_meta( $uid, 'license_expiry', true );
$inspection_expiry = get_user_meta( $uid, 'inspection_expiry', true );
wp_localize_script( 'abd-dashboard', 'AB_DRIVER_CFG', [
'nonce' => wp_create_nonce( 'wp_rest' ),
'api_root' => esc_url_raw( get_rest_url() ),
'user_id' => $uid,
'gmaps_key' => $gmaps_key,
'driver' => [
'name' => $driver_name,
'photo' => $photo ?: get_avatar_url( $uid, [ 'size' => 112 ] ) ?: '',
'vehicle_type' => $vehicle_type ?: '',
'vehicle_plate' => $vehicle_plate ?: '',
'insurance_expiry' => $insurance_expiry ?: '',
'license_expiry' => $license_expiry ?: '',
'inspection_expiry' => $inspection_expiry ?: '',
'email' => $user->user_email,
'phone' => get_user_meta( $uid, 'phone', true ) ?: get_user_meta( $uid, 'billing_phone', true ) ?: '',
],
] );
}
/* ─────────────────────────────────────────────────────────────
REST ROUTES
───────────────────────────────────────────────────────────── */
public function register_routes() {
$ns = self::NAMESPACE;
$auth = [ 'permission_callback' => [ $this, 'is_driver' ] ];
// GET/POST driver status
register_rest_route( $ns, '/driver/status', [
[
'methods' => 'GET',
'callback' => [ $this, 'rest_get_status' ],
'permission_callback' => [ $this, 'is_driver' ],
],
[
'methods' => 'POST',
'callback' => [ $this, 'rest_post_status' ],
'permission_callback' => [ $this, 'is_driver' ],
],
] );
// GET current trip
register_rest_route( $ns, '/driver/current-trip', [
'methods' => 'GET',
'callback' => [ $this, 'rest_current_trip' ],
'permission_callback' => [ $this, 'is_driver' ],
] );
// GET earnings
register_rest_route( $ns, '/driver/earnings', [
'methods' => 'GET',
'callback' => [ $this, 'rest_earnings' ],
'permission_callback' => [ $this, 'is_driver' ],
] );
// GET metrics
register_rest_route( $ns, '/driver/metrics', [
'methods' => 'GET',
'callback' => [ $this, 'rest_metrics' ],
'permission_callback' => [ $this, 'is_driver' ],
] );
// GET history
register_rest_route( $ns, '/driver/history', [
'methods' => 'GET',
'callback' => [ $this, 'rest_history' ],
'permission_callback' => [ $this, 'is_driver' ],
] );
// GET scheduled
register_rest_route( $ns, '/driver/scheduled', [
'methods' => 'GET',
'callback' => [ $this, 'rest_scheduled' ],
'permission_callback' => [ $this, 'is_driver' ],
] );
// GET profile
register_rest_route( $ns, '/driver/profile', [
'methods' => 'GET',
'callback' => [ $this, 'rest_profile' ],
'permission_callback' => [ $this, 'is_driver' ],
] );
// GET zones
register_rest_route( $ns, '/driver/zones', [
'methods' => 'GET',
'callback' => [ $this, 'rest_zones' ],
'permission_callback' => '__return_true',
] );
// POST zone-response
register_rest_route( $ns, '/driver/zone-response', [
'methods' => 'POST',
'callback' => [ $this, 'rest_zone_response' ],
'permission_callback' => [ $this, 'is_driver' ],
] );
// POST incident/sos
register_rest_route( $ns, '/incident/sos', [
'methods' => 'POST',
'callback' => [ $this, 'rest_sos' ],
'permission_callback' => [ $this, 'is_driver' ],
] );
// POST telemetry/position
register_rest_route( $ns, '/telemetry/position', [
'methods' => 'POST',
'callback' => [ $this, 'rest_position' ],
'permission_callback' => [ $this, 'is_driver' ],
] );
// POST trip/start
register_rest_route( $ns, '/trip/start', [
'methods' => 'POST',
'callback' => [ $this, 'rest_trip_start' ],
'permission_callback' => [ $this, 'is_driver' ],
] );
// POST trip/finish
register_rest_route( $ns, '/trip/finish', [
'methods' => 'POST',
'callback' => [ $this, 'rest_trip_finish' ],
'permission_callback' => [ $this, 'is_driver' ],
] );
register_rest_route( $ns, '/driver/change-password', [
'methods' => 'POST',
'callback' => [ $this, 'rest_change_password' ],
'permission_callback' => [ $this, 'is_driver' ],
] );
register_rest_route( $ns, '/driver/update-profile', [
'methods' => 'POST',
'callback' => [ $this, 'rest_update_profile' ],
'permission_callback' => [ $this, 'is_driver' ],
] );
}
/* ─────────────────────────────────────────────────────────────
PERMISSION
───────────────────────────────────────────────────────────── */
public function is_driver() {
if ( ! is_user_logged_in() ) return new WP_Error( 'not_logged_in', 'Authentication required.', [ 'status' => 401 ] );
$user = wp_get_current_user();
if ( in_array( 'driver', (array) $user->roles, true ) ||
in_array( 'administrator', (array) $user->roles, true ) ) {
return true;
}
return new WP_Error( 'not_driver', 'Access denied.', [ 'status' => 403 ] );
}
/* ─────────────────────────────────────────────────────────────
REST HANDLERS
───────────────────────────────────────────────────────────── */
// GET /driver/status
public function rest_get_status( $req ) {
global $wpdb;
$uid = get_current_user_id();
$row = $wpdb->get_row( $wpdb->prepare(
"SELECT online, last_seen, current_trip_id FROM {$wpdb->prefix}ab_driver_status WHERE driver_id = %d",
$uid
) );
if ( ! $row ) {
return rest_ensure_response( [ 'online' => false, 'last_seen' => null, 'current_trip_id' => null ] );
}
return rest_ensure_response( [
'online' => (bool) $row->online,
'last_seen' => $row->last_seen,
'current_trip_id' => $row->current_trip_id,
] );
}
// POST /driver/status
public function rest_post_status( $req ) {
global $wpdb;
$uid = get_current_user_id();
$body = $req->get_json_params();
$online = ! empty( $body['online'] );
$lat = isset( $body['lat'] ) ? floatval( $body['lat'] ) : null;
$lng = isset( $body['lng'] ) ? floatval( $body['lng'] ) : null;
$now = current_time( 'mysql' );
$exists = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}ab_driver_status WHERE driver_id = %d", $uid
) );
$data = [
'online' => $online ? 1 : 0,
'last_seen' => $now,
];
if ( $lat !== null ) $data['last_lat'] = $lat;
if ( $lng !== null ) $data['last_lng'] = $lng;
if ( $exists ) {
$wpdb->update( "{$wpdb->prefix}ab_driver_status", $data, [ 'driver_id' => $uid ] );
} else {
$data['driver_id'] = $uid;
$wpdb->insert( "{$wpdb->prefix}ab_driver_status", $data );
}
// Manage sessions
if ( $online ) {
// Insert new session (go online)
$sess = [
'driver_id' => $uid,
'started_at' => $now,
'created_at' => $now,
'updated_at' => $now,
];
if ( $lat !== null ) { $sess['start_lat'] = $lat; $sess['start_lng'] = $lng; }
$wpdb->insert( "{$wpdb->prefix}ab_driver_sessions", $sess );
} else {
// Close latest open session
$sess_id = $wpdb->get_var( $wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}ab_driver_sessions WHERE driver_id = %d AND ended_at IS NULL ORDER BY started_at DESC LIMIT 1",
$uid
) );
if ( $sess_id ) {
$upd = [ 'ended_at' => $now, 'updated_at' => $now ];
if ( $lat !== null ) { $upd['end_lat'] = $lat; $upd['end_lng'] = $lng; }
// Calculate online_seconds
$start = $wpdb->get_var( $wpdb->prepare(
"SELECT started_at FROM {$wpdb->prefix}ab_driver_sessions WHERE id = %d", $sess_id
) );
if ( $start ) {
$upd['online_seconds'] = max( 0, strtotime( $now ) - strtotime( $start ) );
}
$wpdb->update( "{$wpdb->prefix}ab_driver_sessions", $upd, [ 'id' => $sess_id ] );
}
}
return rest_ensure_response( [ 'ok' => true, 'online' => $online ] );
}
// GET /driver/current-trip
public function rest_current_trip( $req ) {
global $wpdb;
$uid = get_current_user_id();
$statuses = "'assigned','en_route','waiting','in_progress'";
$trip = $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}ab_trips
WHERE driver_id = %d AND status IN ($statuses)
ORDER BY updated_at DESC LIMIT 1",
$uid
), ARRAY_A );
if ( ! $trip ) {
return rest_ensure_response( null );
}
// Unread chat count
$unread = (int) $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}autobooking_chat
WHERE ride_id = %d AND sender != 'driver'",
$trip['id']
) );
$trip['unread_count'] = $unread;
return rest_ensure_response( $trip );
}
// GET /driver/earnings
public function rest_earnings( $req ) {
global $wpdb;
$uid = get_current_user_id();
$period = $req->get_param( 'period' ) ?: 'day';
switch ( $period ) {
case 'week':
$where_date = "AND DATE(created_at) >= CURDATE() - INTERVAL 6 DAY";
break;
case 'month':
$where_date = "AND YEAR(created_at)=YEAR(NOW()) AND MONTH(created_at)=MONTH(NOW())";
break;
case 'year':
$where_date = "AND YEAR(created_at)=YEAR(NOW())";
break;
default: // day
$where_date = "AND DATE(created_at) = CURDATE()";
break;
}
$row = $wpdb->get_row( $wpdb->prepare(
"SELECT
COALESCE(SUM(fare_total_amount),0) AS gross,
COALESCE(SUM(platform_fee_amount),0) AS fee,
COALESCE(SUM(driver_payout_amount),0) AS net,
COUNT(*) AS trips_count,
COALESCE(SUM(fare_distance_m),0) AS distance_m
FROM {$wpdb->prefix}ab_trips
WHERE driver_id = %d AND status = 'finished' $where_date",
$uid
) );
// Last 7 days
$last7 = $wpdb->get_results( $wpdb->prepare(
"SELECT DATE(created_at) AS the_date, COALESCE(SUM(driver_payout_amount),0) AS net
FROM {$wpdb->prefix}ab_trips
WHERE driver_id = %d AND status = 'finished'
AND DATE(created_at) >= CURDATE() - INTERVAL 6 DAY
GROUP BY DATE(created_at)
ORDER BY DATE(created_at) ASC",
$uid
), ARRAY_A );
// Fill missing days
$days_map = [];
for ( $i = 6; $i >= 0; $i-- ) {
$d = date( 'Y-m-d', strtotime( "-$i days" ) );
$days_map[ $d ] = 0;
}
foreach ( $last7 as $r ) {
$days_map[ $r['the_date'] ] = (float) $r['net'];
}
$last7_arr = [];
foreach ( $days_map as $d => $v ) {
$last7_arr[] = [ 'date' => $d, 'net' => $v ];
}
return rest_ensure_response( [
'gross' => (float) $row->gross,
'fee' => (float) $row->fee,
'net' => (float) $row->net,
'trips_count' => (int) $row->trips_count,
'distance_km' => round( (float) $row->distance_m / 1000, 2 ),
'last_7_days' => $last7_arr,
] );
}
// GET /driver/metrics
public function rest_metrics( $req ) {
global $wpdb;
$uid = get_current_user_id();
// Last 30 days trips assigned to driver
$total_assigned = (int) $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}ab_trips
WHERE driver_id = %d AND status != 'available'
AND created_at >= NOW() - INTERVAL 30 DAY",
$uid
) );
$accepted = (int) $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}ab_trips
WHERE driver_id = %d AND status NOT IN ('assigned','canceled')
AND created_at >= NOW() - INTERVAL 30 DAY",
$uid
) );
$finished = (int) $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}ab_trips
WHERE driver_id = %d AND status = 'finished'
AND created_at >= NOW() - INTERVAL 30 DAY",
$uid
) );
$canceled = (int) $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}ab_trips
WHERE driver_id = %d AND status = 'canceled'
AND created_at >= NOW() - INTERVAL 30 DAY",
$uid
) );
$avg_rating = (float) $wpdb->get_var( $wpdb->prepare(
"SELECT AVG(driver_rating) FROM {$wpdb->prefix}ab_trips
WHERE driver_id = %d AND driver_rating > 0
AND created_at >= NOW() - INTERVAL 30 DAY",
$uid
) );
// Online hours this week from sessions
$online_secs = (int) $wpdb->get_var( $wpdb->prepare(
"SELECT COALESCE(SUM(online_seconds),0) FROM {$wpdb->prefix}ab_driver_sessions
WHERE driver_id = %d AND started_at >= CURDATE() - INTERVAL 6 DAY",
$uid
) );
// Zone responses
$zones_responded = (int) $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}ab_zone_alert_responses
WHERE driver_id = %d AND response = 'going'
AND responded_at >= NOW() - INTERVAL 30 DAY",
$uid
) );
$zones_ignored = (int) $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}ab_zone_alert_responses
WHERE driver_id = %d AND response IN ('not_going','ignored')
AND responded_at >= NOW() - INTERVAL 30 DAY",
$uid
) );
$acceptance_rate = $total_assigned > 0 ? round( ( $accepted / $total_assigned ) * 100, 1 ) : 0;
$total_started = $accepted;
$completion_rate = $total_started > 0 ? round( ( $finished / $total_started ) * 100, 1 ) : 0;
return rest_ensure_response( [
'acceptance_rate' => $acceptance_rate,
'completion_rate' => $completion_rate,
'avg_rating' => round( $avg_rating, 2 ),
'online_hours_week'=> round( $online_secs / 3600, 1 ),
'zones_responded' => $zones_responded,
'zones_ignored' => $zones_ignored,
] );
}
// GET /driver/history
public function rest_history( $req ) {
global $wpdb;
$uid = get_current_user_id();
$page = max( 1, intval( $req->get_param( 'page' ) ?: 1 ) );
$per_page = max( 1, min( 100, intval( $req->get_param( 'per_page' ) ?: 20 ) ) );
$offset = ( $page - 1 ) * $per_page;
$date_from = sanitize_text_field( $req->get_param( 'date_from' ) ?: '' );
$date_to = sanitize_text_field( $req->get_param( 'date_to' ) ?: '' );
try {
$where = $wpdb->prepare( "WHERE driver_id = %d", $uid );
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 );
$total = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->prefix}ab_trips $where"
);
if ( $wpdb->last_error ) throw new \Exception( $wpdb->last_error );
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT id, trip_uuid, created_at, passenger_name,
pickup_lat, pickup_lng, dropoff_lat, dropoff_lng,
fare_total_amount, platform_fee_amount, driver_payout_amount,
status, driver_rating, currency
FROM {$wpdb->prefix}ab_trips $where
ORDER BY created_at DESC
LIMIT %d OFFSET %d",
$per_page, $offset
),
ARRAY_A
);
if ( $wpdb->last_error ) throw new \Exception( $wpdb->last_error );
return rest_ensure_response( [
'ok' => true,
'rows' => $rows ?: [],
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'total_pages'=> $total > 0 ? (int) ceil( $total / $per_page ) : 0,
] );
} catch ( \Exception $e ) {
return rest_ensure_response( [
'ok' => true,
'rows' => [],
'total' => 0,
'page' => $page,
'per_page' => $per_page,
'total_pages'=> 0,
] );
}
}
// GET /driver/scheduled
public function rest_scheduled( $req ) {
global $wpdb;
$uid = get_current_user_id();
$rows = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}ab_trips
WHERE driver_id = %d AND status = 'assigned' AND pickup_time > NOW()
ORDER BY pickup_time ASC",
$uid
), ARRAY_A );
return rest_ensure_response( $rows ?: [] );
}
// GET /driver/profile
public function rest_profile( $req ) {
$uid = get_current_user_id();
$user = get_userdata( $uid );
$driver_name = get_user_meta( $uid, 'driver_name', true );
if ( ! $driver_name ) $driver_name = get_user_meta( $uid, 'full_name', true );
if ( ! $driver_name ) {
$fn = get_user_meta( $uid, 'first_name', true );
$ln = get_user_meta( $uid, 'last_name', true );
$driver_name = trim( "$fn $ln" );
}
if ( ! $driver_name ) $driver_name = $user->display_name;
$photo = get_user_meta( $uid, 'photo_url', true ) ?: get_user_meta( $uid, 'driver_photo', true );
$vehicle_plate = get_user_meta( $uid, 'vehicle_plate', true ) ?: get_user_meta( $uid, 'license_plate', true );
$insurance_expiry = get_user_meta( $uid, 'insurance_expiry', true );
$license_expiry = get_user_meta( $uid, 'license_expiry', true );
$inspection_expiry = get_user_meta( $uid, 'inspection_expiry', true );
$now = time();
return rest_ensure_response( [
'name' => $driver_name,
'email' => $user->user_email,
'phone' => get_user_meta( $uid, 'phone', true ) ?: get_user_meta( $uid, 'billing_phone', true ),
'vehicle_type' => get_user_meta( $uid, 'vehicle_type', true ),
'vehicle_plate' => $vehicle_plate,
'photo_url' => $photo,
'insurance_expiry' => $insurance_expiry,
'license_expiry' => $license_expiry,
'inspection_expiry' => $inspection_expiry,
'insurance_status' => $this->doc_status( $insurance_expiry ),
'license_status' => $this->doc_status( $license_expiry ),
'inspection_status' => $this->doc_status( $inspection_expiry ),
] );
}
private function doc_status( $expiry_date ) {
if ( ! $expiry_date ) return 'unknown';
$ts = strtotime( $expiry_date );
$diff = $ts - time();
if ( $diff < 0 ) return 'expired';
if ( $diff < 30 * 86400 ) return 'warning';
return 'ok';
}
// POST /driver/change-password
public function rest_change_password( $req ) {
$uid = get_current_user_id();
$current = $req->get_param('current_password');
$new_pw = $req->get_param('new_password');
$confirm = $req->get_param('confirm_password');
if ( ! $current || ! $new_pw || ! $confirm ) {
return new WP_Error( 'missing_fields', 'Todos los campos son requeridos.', [ 'status' => 400 ] );
}
if ( $new_pw !== $confirm ) {
return new WP_Error( 'password_mismatch', 'Las contraseñas no coinciden.', [ 'status' => 400 ] );
}
if ( strlen( $new_pw ) < 8 ) {
return new WP_Error( 'password_too_short', 'La contraseña debe tener al menos 8 caracteres.', [ 'status' => 400 ] );
}
$user = get_userdata( $uid );
if ( ! wp_check_password( $current, $user->user_pass, $uid ) ) {
return new WP_Error( 'wrong_password', 'Contraseña actual incorrecta.', [ 'status' => 400 ] );
}
wp_set_password( $new_pw, $uid );
return rest_ensure_response( [ 'ok' => true, 'msg' => 'Contraseña actualizada correctamente.' ] );
}
// POST /driver/update-profile
public function rest_update_profile( $req ) {
$uid = get_current_user_id();
$phone = sanitize_text_field( $req->get_param('phone') ?: '' );
$vehicle = sanitize_text_field( $req->get_param('vehicle_type') ?: '' );
$plate = sanitize_text_field( $req->get_param('vehicle_plate') ?: '' );
$photo = esc_url_raw( $req->get_param('photo_url') ?: '' );
if ( $phone ) update_user_meta( $uid, 'phone', $phone );
if ( $vehicle ) update_user_meta( $uid, 'vehicle_type', $vehicle );
if ( $plate ) {
update_user_meta( $uid, 'vehicle_plate', $plate );
update_user_meta( $uid, 'license_plate', $plate );
}
if ( $photo ) update_user_meta( $uid, 'photo_url', $photo );
return rest_ensure_response( [ 'ok' => true, 'msg' => 'Perfil actualizado correctamente.' ] );
}
// GET /driver/zones
public function rest_zones( $req ) {
global $wpdb;
$rows = $wpdb->get_results(
"SELECT id, title, description, lat, lng, radius_km, bonus_amount, bonus_currency,
countdown_seconds, type, expires_at
FROM {$wpdb->prefix}ab_zone_alerts
WHERE active = 1 AND (expires_at IS NULL OR expires_at > NOW())
ORDER BY created_at DESC",
ARRAY_A
);
return rest_ensure_response( $rows ?: [] );
}
// POST /driver/zone-response
public function rest_zone_response( $req ) {
global $wpdb;
$uid = get_current_user_id();
$body = $req->get_json_params();
$zone_id = intval( $body['zone_id'] ?? 0 );
$resp = sanitize_text_field( $body['response'] ?? '' );
if ( ! $zone_id || ! in_array( $resp, [ 'going', 'not_going' ], true ) ) {
return new WP_Error( 'bad_request', 'Invalid parameters.', [ 'status' => 400 ] );
}
$wpdb->insert( "{$wpdb->prefix}ab_zone_alert_responses", [
'zone_id' => $zone_id,
'driver_id' => $uid,
'response' => $resp,
'responded_at' => current_time( 'mysql' ),
] );
return rest_ensure_response( [ 'ok' => true ] );
}
// POST /incident/sos
public function rest_sos( $req ) {
global $wpdb;
$uid = get_current_user_id();
$body = $req->get_json_params();
$lat = floatval( $body['lat'] ?? 0 );
$lng = floatval( $body['lng'] ?? 0 );
$trip_uuid = sanitize_text_field( $body['trip_uuid'] ?? '' );
$chunk_index = isset( $body['chunk_index'] ) ? intval( $body['chunk_index'] ) : null;
$audio_data = $body['audio_data'] ?? null;
$incident_id = intval( $body['incident_id'] ?? 0 );
if ( ! $incident_id ) {
// First call — create incident
$trip_id = null;
if ( $trip_uuid ) {
$trip_id = $wpdb->get_var( $wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}ab_trips WHERE trip_uuid = %s LIMIT 1",
$trip_uuid
) );
}
$wpdb->insert( "{$wpdb->prefix}ab_incidents", [
'driver_id' => $uid,
'trip_id' => $trip_id ?: null,
'lat' => $lat,
'lng' => $lng,
'status' => 'active',
'chunks_count'=> 0,
'incident_at' => current_time( 'mysql' ),
] );
$incident_id = $wpdb->insert_id;
// Send admin email
$admin_email = get_option( 'admin_email' );
$maps_link = "https://maps.google.com/?q={$lat},{$lng}";
$user = get_userdata( $uid );
$subject = '🚨 SOS ALERT — Autobooking Driver';
$message = "Driver: {$user->display_name} (ID: {$uid})\n";
$message .= "GPS: {$lat}, {$lng}\n";
$message .= "Google Maps: {$maps_link}\n";
if ( $trip_uuid ) $message .= "Trip UUID: {$trip_uuid}\n";
$message .= "Time: " . current_time( 'mysql' ) . "\n";
wp_mail( $admin_email, $subject, $message );
}
// Save audio chunk if provided
if ( $audio_data && $incident_id ) {
$upload_dir = wp_upload_dir();
$dir = $upload_dir['basedir'] . '/ab-incidents/' . $incident_id;
if ( ! file_exists( $dir ) ) {
wp_mkdir_p( $dir );
}
$idx = intval( $chunk_index ) ?: 0;
$filename = $dir . '/chunk_' . $idx . '.webm';
$decoded = base64_decode( $audio_data );
if ( $decoded !== false ) {
file_put_contents( $filename, $decoded );
}
$wpdb->query( $wpdb->prepare(
"UPDATE {$wpdb->prefix}ab_incidents SET chunks_count = chunks_count + 1 WHERE id = %d",
$incident_id
) );
}
return rest_ensure_response( [ 'ok' => true, 'incident_id' => $incident_id ] );
}
// POST /telemetry/position
public function rest_position( $req ) {
global $wpdb;
$uid = get_current_user_id();
$body = $req->get_json_params();
$lat = floatval( $body['lat'] ?? 0 );
$lng = floatval( $body['lng'] ?? 0 );
$accuracy = intval( $body['accuracy'] ?? 0 );
$trip_id = intval( $body['trip_id'] ?? 0 );
// Update driver status
$exists = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}ab_driver_status WHERE driver_id = %d", $uid
) );
$now = current_time( 'mysql' );
$data = [
'last_lat' => $lat,
'last_lng' => $lng,
'last_accuracy' => $accuracy,
'last_seen' => $now,
];
if ( $exists ) {
$wpdb->update( "{$wpdb->prefix}ab_driver_status", $data, [ 'driver_id' => $uid ] );
} else {
$data['driver_id'] = $uid;
$wpdb->insert( "{$wpdb->prefix}ab_driver_status", $data );
}
// Insert trip position if trip_id given (table may not exist — catch quietly)
if ( $trip_id ) {
$wpdb->query( $wpdb->prepare(
"INSERT IGNORE INTO {$wpdb->prefix}ab_trip_positions (trip_id, driver_id, lat, lng, accuracy, recorded_at)
VALUES (%d, %d, %s, %s, %d, %s)",
$trip_id, $uid, $lat, $lng, $accuracy, $now
) );
}
return rest_ensure_response( [ 'ok' => true ] );
}
// POST /trip/start
public function rest_trip_start( $req ) {
global $wpdb;
$uid = get_current_user_id();
$body = $req->get_json_params();
$trip_uuid = sanitize_text_field( $body['trip_uuid'] ?? '' );
if ( ! $trip_uuid ) {
return new WP_Error( 'bad_request', 'trip_uuid required.', [ 'status' => 400 ] );
}
$affected = $wpdb->query( $wpdb->prepare(
"UPDATE {$wpdb->prefix}ab_trips SET status = 'in_progress', pickup_time = NOW(), updated_at = NOW()
WHERE trip_uuid = %s AND driver_id = %d AND status = 'waiting'",
$trip_uuid, $uid
) );
if ( ! $affected ) {
return new WP_Error( 'not_found', 'Trip not found or invalid state.', [ 'status' => 404 ] );
}
return rest_ensure_response( [ 'ok' => true ] );
}
// POST /trip/finish
public function rest_trip_finish( $req ) {
global $wpdb;
$uid = get_current_user_id();
$body = $req->get_json_params();
$trip_uuid = sanitize_text_field( $body['trip_uuid'] ?? '' );
$tolls = floatval( $body['tolls_amount'] ?? 0 );
$tips = floatval( $body['tips_amount'] ?? 0 );
$final_amount = isset( $body['final_amount'] ) ? floatval( $body['final_amount'] ) : null;
if ( ! $trip_uuid ) {
return new WP_Error( 'bad_request', 'trip_uuid required.', [ 'status' => 400 ] );
}
// Fetch trip to recalculate payout
$trip = $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}ab_trips WHERE trip_uuid = %s AND driver_id = %d",
$trip_uuid, $uid
) );
if ( ! $trip ) {
return new WP_Error( 'not_found', 'Trip not found.', [ 'status' => 404 ] );
}
$payout = $final_amount !== null
? $final_amount
: ( floatval( $trip->driver_payout_amount ) + $tolls + $tips );
$wpdb->query( $wpdb->prepare(
"UPDATE {$wpdb->prefix}ab_trips
SET status = 'finished', dropoff_time = NOW(), updated_at = NOW(),
tolls_amount = %f, driver_payout_amount = %f
WHERE trip_uuid = %s AND driver_id = %d",
$tolls, $payout, $trip_uuid, $uid
) );
// Clear current_trip_id in driver status
$wpdb->update(
"{$wpdb->prefix}ab_driver_status",
[ 'current_trip_id' => null ],
[ 'driver_id' => $uid ]
);
return rest_ensure_response( [ 'ok' => true ] );
}
/* ─────────────────────────────────────────────────────────────
SHORTCODE RENDER
───────────────────────────────────────────────────────────── */
public function render( $atts ) {
if ( ! is_user_logged_in() ) {
return '<div style="background:rgba(17,17,17,.45);border:1px solid rgba(255,255,255,.18);backdrop-filter:blur(8px);border-radius:16px;padding:32px;text-align:center;color:#fff;font-family:system-ui;max-width:420px;margin:40px auto;">'
. '<p style="font-size:18px;font-weight:700;margin-bottom:16px;">🔒 Acceso Restringido</p>'
. '<p style="color:rgba(255,255,255,.7);margin-bottom:20px;">Debes iniciar sesión para acceder al panel de conductor.</p>'
. '<a href="' . esc_url( wp_login_url( get_permalink() ) ) . '" style="display:inline-block;background:#FF6F00;color:#fff;padding:12px 24px;border-radius:12px;text-decoration:none;font-weight:800;text-transform:uppercase;">Iniciar sesión</a>'
. '</div>';
}
$user = wp_get_current_user();
$is_driver = in_array( 'driver', (array) $user->roles, true ) || in_array( 'administrator', (array) $user->roles, true );
if ( ! $is_driver ) {
return '<div style="background:rgba(17,17,17,.45);border:1px solid rgba(255,255,255,.18);backdrop-filter:blur(8px);border-radius:16px;padding:32px;text-align:center;color:#fff;font-family:system-ui;max-width:420px;margin:40px auto;">'
. '<p style="font-size:18px;font-weight:700;margin-bottom:16px;">🚫 Acceso Denegado</p>'
. '<p style="color:rgba(255,255,255,.7);">Esta página es exclusiva para conductores registrados.</p>'
. '</div>';
}
$uid = $user->ID;
$render_photo = get_user_meta( $uid, 'photo_url', true )
?: get_user_meta( $uid, 'driver_photo', true )
?: get_avatar_url( $uid, [ 'size' => 112 ] )
?: '';
ob_start();
?>
<div id="abd-app" class="abd-app">
<!-- Skeleton initial loader -->
<div id="abd-init-loader">
<div class="abd-init-logo">🚗</div>
<div class="abd-skeleton" style="width:160px;height:20px;margin:16px auto 8px;"></div>
<div class="abd-skeleton" style="width:100px;height:14px;margin:0 auto;"></div>
</div>
<!-- SOS Badge -->
<div id="abd-sos-badge" style="display:none">🚨 ALERTA ACTIVA — Admin notificado <button id="abd-sos-stop" style="margin-left:12px;background:rgba(0,0,0,.4);color:#fff;border:1px solid rgba(255,255,255,.4);border-radius:8px;padding:2px 10px;cursor:pointer;">Detener</button></div>
<!-- Header -->
<header class="abd-header">
<div class="abd-header-left">
<img id="abd-header-photo" src="<?php echo esc_url( $render_photo ); ?>" alt="" class="abd-header-avatar" <?php echo $render_photo ? '' : 'style="display:none"'; ?>>
<div class="abd-header-info">
<div id="abd-header-name" class="abd-header-name">Conductor</div>
<div id="abd-header-plate" class="abd-header-plate"></div>
</div>
</div>
<div class="abd-header-right">
<span id="abd-status-label" class="abd-status-label abd-status--offline">OFFLINE</span>
<label class="abd-toggle" title="Conectar / Desconectar">
<input type="checkbox" id="abd-online-toggle">
<span class="abd-toggle-track"><span class="abd-toggle-thumb"></span></span>
</label>
</div>
</header>
<!-- Tab navigation -->
<nav class="abd-tabs">
<button class="abd-tab abd-tab--active" data-tab="viaje">🚗 Viaje</button>
<button class="abd-tab" data-tab="ganancias">💰 Ganancias</button>
<button class="abd-tab" data-tab="metricas">📊 Métricas</button>
<button class="abd-tab" data-tab="historial">📋 Historial</button>
<button class="abd-tab" data-tab="perfil">👤 Perfil</button>
</nav>
<!-- ═══ VIAJE TAB ═══ -->
<section class="abd-panel abd-panel--active" id="abd-tab-viaje">
<!-- Zone Alert Card -->
<div id="abd-zone-card" class="abd-glass-card abd-zone-card" style="display:none">
<div class="abd-zone-header">
<span id="abd-zone-type-badge" class="abd-zone-badge">HOTSPOT</span>
<span id="abd-zone-countdown" class="abd-zone-countdown">3:00</span>
</div>
<div id="abd-zone-title" class="abd-zone-title"></div>
<div id="abd-zone-desc" class="abd-zone-desc"></div>
<div class="abd-zone-bonus" id="abd-zone-bonus" style="display:none">Bono: <b id="abd-zone-bonus-val"></b></div>
<div class="abd-zone-actions">
<button id="abd-zone-going" class="abd-btn abd-btn--brand">✅ Voy</button>
<button id="abd-zone-no" class="abd-btn abd-btn--ghost">❌ No puedo</button>
</div>
</div>
<!-- Map -->
<div id="abd-map-wrap">
<div id="abd-map"></div>
<div id="abd-route-banner" style="display:none">
<div id="abd-route-banner-text">Ruta sugerida disponible</div>
<div class="abd-route-banner-btns">
<button id="abd-route-accept" class="abd-btn abd-btn--brand">Aceptar</button>
<button id="abd-route-keep" class="abd-btn abd-btn--ghost">Mantener</button>
</div>
</div>
<div id="abd-theme-switcher">
<button class="abd-theme-btn active" data-theme="auto" title="Automático (oscuro de noche)">Auto</button>
<button class="abd-theme-btn" data-theme="dark" title="Siempre oscuro">Oscuro</button>
<button class="abd-theme-btn" data-theme="normal" title="Mapa normal">Normal</button>
</div>
<div id="abd-gps-badge">GPS: —</div>
</div>
<!-- Trip controls card -->
<div id="abd-trip-card" class="abd-glass-card">
<!-- No trip -->
<div id="abd-no-trip">
<div style="text-align:center;padding:12px 0;">
<img id="abd-driver-avatar-trip" src="" alt="" class="abd-driver-avatar-lg">
<div id="abd-driver-name-trip" class="abd-driver-name-lg"></div>
<div id="abd-driver-rating-trip" class="abd-driver-rating-lg">★ —</div>
<div id="abd-driver-vehicle-trip" class="abd-driver-vehicle-lg"></div>
<p style="color:rgba(255,255,255,.5);margin-top:16px;font-size:14px;">Conéctate para recibir viajes</p>
</div>
</div>
<!-- Active trip -->
<div id="abd-active-trip" style="display:none">
<div class="abd-passenger-info">
<img id="abd-pass-photo" src="" alt="" class="abd-pass-avatar">
<div>
<div id="abd-pass-name" class="abd-pass-name"></div>
<div id="abd-trip-status-badge" class="abd-trip-status-badge"></div>
</div>
</div>
<div class="abd-trip-info">
<div class="abd-trip-info-row"><span class="abd-info-label">Origen</span><b id="abd-origin"></b></div>
<div class="abd-trip-info-row"><span class="abd-info-label">Destino</span><b id="abd-dest"></b></div>
<div class="abd-trip-info-row"><span class="abd-info-label">Distancia</span><b id="abd-distance"></b></div>
<div class="abd-trip-info-row"><span class="abd-info-label">Tarifa estimada</span><b id="abd-fare"></b></div>
</div>
<div id="abd-trip-instructions" class="abd-instructions"></div>
<div id="abd-courtesy-timer" class="abd-courtesy-timer" style="display:none">⏱ Tiempo de cortesía: <b id="abd-courtesy-val">3:00</b></div>
<div id="abd-trip-actions" class="abd-actions"></div>
<button id="abd-sos-btn" class="abd-sos-btn" style="display:none">🆘 PÁNICO</button>
</div>
</div>
<!-- Chat -->
<div id="abd-chat" class="abd-glass-card" style="display:none">
<div class="abd-chat-header" id="abd-chat-toggle-header">
💬 Chat <span id="abd-chat-badge" class="ab-badge--warn" style="display:none">0</span>
</div>
<div id="abd-chat-body">
<div id="abd-chat-msgs"></div>
<div class="abd-chat-input-row">
<input id="abd-chat-input" placeholder="Escribe un mensaje…" maxlength="200" class="abd-chat-input">
<button id="abd-chat-send" class="abd-btn abd-btn--brand">➤</button>
</div>
</div>
</div>
</section>
<!-- ═══ GANANCIAS TAB ═══ -->
<section class="abd-panel" id="abd-tab-ganancias" style="display:none">
<div class="abd-period-tabs">
<button class="abd-period-tab abd-period-tab--active" data-period="day">HOY</button>
<button class="abd-period-tab" data-period="week">SEMANA</button>
<button class="abd-period-tab" data-period="month">MES</button>
<button class="abd-period-tab" data-period="year">AÑO</button>
</div>
<div class="abd-earnings-cards">
<div class="abd-glass-card abd-earn-card">
<div class="abd-earn-label">Bruto</div>
<div id="abd-earn-gross" class="abd-earn-value">$—</div>
</div>
<div class="abd-glass-card abd-earn-card">
<div class="abd-earn-label">Comisión (20%)</div>
<div id="abd-earn-fee" class="abd-earn-value abd-earn-fee">$—</div>
</div>
<div class="abd-glass-card abd-earn-card abd-earn-card--main">
<div class="abd-earn-label">NETO A RECIBIR</div>
<div id="abd-earn-net" class="abd-earn-value abd-earn-net">$—</div>
<div id="abd-earn-trips" class="abd-earn-sub"></div>
</div>
</div>
<div class="abd-glass-card abd-chart-card">
<div class="abd-card-title">Últimos 7 días</div>
<div id="abd-bar-chart" class="abd-bar-chart"></div>
</div>
<div class="abd-glass-card abd-tax-card">
<div class="abd-card-title">Resumen para impuestos (año en curso)</div>
<div class="abd-tax-row"><span>Total devengado</span><span id="abd-tax-gross">$—</span></div>
<div class="abd-tax-row"><span>Total comisiones</span><span id="abd-tax-fee">$—</span></div>
<div class="abd-tax-row abd-tax-row--total"><span>Neto anual</span><span id="abd-tax-net">$—</span></div>
<button id="abd-export-csv" class="abd-btn abd-btn--ghost" style="width:100%;margin-top:12px">📄 Exportar CSV para Taxes</button>
</div>
</section>
<!-- ═══ MÉTRICAS TAB ═══ -->
<section class="abd-panel" id="abd-tab-metricas" style="display:none">
<div class="abd-glass-card">
<div class="abd-card-title">Últimos 30 días</div>
<div class="abd-metric-row">
<span class="abd-metric-label">% Aceptación</span>
<div class="abd-progress-wrap"><div class="abd-progress-bar" id="bar-acceptance"></div></div>
<span id="val-acceptance" class="abd-metric-val">—</span>
</div>
<div class="abd-metric-row">
<span class="abd-metric-label">% Completación</span>
<div class="abd-progress-wrap"><div class="abd-progress-bar abd-bar-green" id="bar-completion"></div></div>
<span id="val-completion" class="abd-metric-val">—</span>
</div>
<div class="abd-metric-row">
<span class="abd-metric-label">Calificación promedio</span>
<div class="abd-stars" id="abd-stars"></div>
<span id="val-rating" class="abd-metric-val">—</span>
</div>
<div class="abd-metric-row">
<span class="abd-metric-label">Horas activas (semana)</span>
<div class="abd-progress-wrap"><div class="abd-progress-bar abd-bar-blue" id="bar-hours"></div></div>
<span id="val-hours" class="abd-metric-val">—</span>
</div>
<div class="abd-metric-row">
<span class="abd-metric-label">Alertas respondidas</span>
<div class="abd-progress-wrap"><div class="abd-progress-bar abd-bar-yellow" id="bar-zones"></div></div>
<span id="val-zones" class="abd-metric-val">—</span>
</div>
</div>
</section>
<!-- ═══ HISTORIAL TAB ═══ -->
<section class="abd-panel" id="abd-tab-historial" style="display:none">
<div class="abd-history-filters abd-glass-card">
<input type="date" id="abd-hist-from" class="abd-date-input">
<input type="date" id="abd-hist-to" class="abd-date-input">
<button id="abd-hist-filter" class="abd-btn abd-btn--ghost">Filtrar</button>
</div>
<div id="abd-history-table-wrap" class="abd-glass-card" style="overflow-x:auto">
<table class="abd-table">
<thead><tr>
<th>Fecha</th><th>Pasajero</th><th>Ruta</th>
<th>Bruto</th><th>Comisión</th><th>Neto</th><th>Estado</th>
</tr></thead>
<tbody id="abd-history-tbody"></tbody>
</table>
</div>
<div id="abd-history-pagination" class="abd-pagination"></div>
</section>
<!-- ═══ PERFIL TAB ═══ -->
<section class="abd-panel" id="abd-tab-perfil" style="display:none">
<div class="abd-glass-card abd-profile-card">
<img id="abd-prof-photo" src="<?php echo esc_url( $render_photo ); ?>" alt="" class="abd-prof-avatar" <?php echo $render_photo ? '' : 'style="display:none"'; ?>>
<div id="abd-prof-name" class="abd-prof-name"></div>
<div id="abd-prof-vehicle" class="abd-prof-vehicle"></div>
<div id="abd-prof-email" class="abd-prof-meta"></div>
<div id="abd-prof-phone" class="abd-prof-meta"></div>
<div class="abd-profile-actions">
<button id="abd-btn-edit-profile" class="abd-btn abd-btn--ghost">✏️ Editar Perfil</button>
<button id="abd-btn-change-password" class="abd-btn abd-btn--ghost">🔒 Cambiar Contraseña</button>
</div>
</div>
<div class="abd-glass-card">
<div class="abd-card-title">Documentos</div>
<div class="abd-doc-row" id="doc-insurance">
<span class="abd-doc-icon"></span>
<span class="abd-doc-label">Seguro del vehículo</span>
<span class="abd-doc-expiry"></span>
<span class="abd-doc-status-badge"></span>
</div>
<div class="abd-doc-row" id="doc-license">
<span class="abd-doc-icon"></span>
<span class="abd-doc-label">Licencia de conducir</span>
<span class="abd-doc-expiry"></span>
<span class="abd-doc-status-badge"></span>
</div>
<div class="abd-doc-row" id="doc-inspection">
<span class="abd-doc-icon"></span>
<span class="abd-doc-label">Revisión técnica</span>
<span class="abd-doc-expiry"></span>
<span class="abd-doc-status-badge"></span>
</div>
</div>
<div class="abd-glass-card">
<div class="abd-card-title">Viajes programados</div>
<div id="abd-scheduled-list"><p style="color:rgba(255,255,255,.5);font-size:14px;">Sin viajes programados próximamente.</p></div>
</div>
</section>
<!-- ═══ SOS MODAL ═══ -->
<div id="abd-sos-modal" class="abd-modal-overlay" style="display:none">
<div class="abd-modal">
<h3>🚨 BOTÓN DE PÁNICO</h3>
<p>Se notificará al administrador con tu ubicación GPS y se iniciará grabación de audio.</p>
<p id="abd-sos-rec-status" style="color:rgba(255,255,255,.6);font-size:13px;"></p>
<video id="abd-sos-video" autoplay muted playsinline style="display:none;width:100%;border-radius:12px;margin:8px 0"></video>
<div class="abd-modal-actions">
<button id="abd-sos-confirm" class="abd-btn abd-btn--danger">⚡ ACTIVAR ALERTA</button>
<button id="abd-sos-cancel" class="abd-btn abd-btn--ghost">Cancelar</button>
</div>
</div>
</div>
<!-- ═══ FINISH MODAL ═══ -->
<div id="abd-finish-modal" class="abd-modal-overlay" style="display:none">
<div class="abd-modal">
<h3>Finalizar Viaje</h3>
<div class="abd-modal-field">
<label>Peajes ($)</label>
<input type="number" id="abd-tolls-input" min="0" step="0.01" value="0" class="abd-modal-input">
</div>
<div class="abd-modal-field">
<label>Propina recibida ($)</label>
<input type="number" id="abd-tips-input" min="0" step="0.01" value="0" class="abd-modal-input">
</div>
<div class="abd-modal-actions">
<button id="abd-finish-confirm" class="abd-btn abd-btn--brand">✅ Confirmar y Finalizar</button>
<button id="abd-finish-cancel" class="abd-btn abd-btn--ghost">Volver</button>
</div>
</div>
</div>
<!-- ═══ CAMBIAR CONTRASEÑA MODAL ═══ -->
<div id="abd-pwd-modal" class="abd-modal-overlay" style="display:none">
<div class="abd-modal">
<h3>🔒 Cambiar Contraseña</h3>
<div class="abd-modal-field">
<label>Contraseña actual</label>
<input type="password" id="abd-pwd-current" class="abd-modal-input" autocomplete="current-password">
</div>
<div class="abd-modal-field">
<label>Nueva contraseña</label>
<input type="password" id="abd-pwd-new" class="abd-modal-input" autocomplete="new-password">
</div>
<div class="abd-modal-field">
<label>Confirmar nueva contraseña</label>
<input type="password" id="abd-pwd-confirm" class="abd-modal-input" autocomplete="new-password">
</div>
<p id="abd-pwd-status" class="abd-modal-status"></p>
<div class="abd-modal-actions">
<button id="abd-pwd-save" class="abd-btn abd-btn--brand">Guardar</button>
<button id="abd-pwd-cancel" class="abd-btn abd-btn--ghost">Cancelar</button>
</div>
</div>
</div>
<!-- ═══ EDITAR PERFIL MODAL ═══ -->
<div id="abd-editprofile-modal" class="abd-modal-overlay" style="display:none">
<div class="abd-modal">
<h3>✏️ Editar Perfil</h3>
<div class="abd-modal-field">
<label>Foto de perfil</label>
<div class="abd-photo-upload-row">
<img id="abd-ep-photo-preview" src="" alt="" class="abd-ep-photo-preview" style="display:none">
<button type="button" id="abd-ep-photo-btn" class="abd-btn abd-btn--ghost abd-btn--sm">📷 Seleccionar foto</button>
<input type="file" id="abd-ep-photo-file" accept="image/*" style="display:none">
</div>
<input type="url" id="abd-ep-photo-url" class="abd-modal-input" placeholder="O pega una URL de imagen" style="margin-top:6px;">
</div>
<div class="abd-modal-field">
<label>Teléfono</label>
<input type="tel" id="abd-ep-phone" class="abd-modal-input" autocomplete="tel">
</div>
<div class="abd-modal-field">
<label>Tipo de vehículo</label>
<input type="text" id="abd-ep-vehicle" class="abd-modal-input" placeholder="Ej: Sedan, SUV, Pickup…">
</div>
<div class="abd-modal-field">
<label>Placa del vehículo</label>
<input type="text" id="abd-ep-plate" class="abd-modal-input" placeholder="Ej: ABC-1234">
</div>
<p id="abd-ep-status" class="abd-modal-status"></p>
<div class="abd-modal-actions">
<button id="abd-ep-save" class="abd-btn abd-btn--brand">Guardar cambios</button>
<button id="abd-ep-cancel" class="abd-btn abd-btn--ghost">Cancelar</button>
</div>
</div>
</div>
</div>
<?php
return ob_get_clean();
}
}
ABDriverDashboard2::get_instance();