From bac2a7bb2572725536cec82df39a10b90ece542c Mon Sep 17 00:00:00 2001 From: Kazem Alghasi Date: Mon, 10 Aug 2026 13:08:53 +0330 Subject: [PATCH] feat: Complete WordPress customer portal --- .../ifnex-bridge/assets/css/ifnex-orders.css | 159 ++++++ .../plugins/ifnex-bridge/ifnex-bridge.php | 31 +- .../ifnex-bridge/includes/shortcodes.php | 498 +++++++++++++++++- .../ifnex-bridge/includes/user-bridge.php | 174 ++++-- 4 files changed, 811 insertions(+), 51 deletions(-) diff --git a/03_WordPress/wp-content/plugins/ifnex-bridge/assets/css/ifnex-orders.css b/03_WordPress/wp-content/plugins/ifnex-bridge/assets/css/ifnex-orders.css index 52af5ff..60c6797 100644 --- a/03_WordPress/wp-content/plugins/ifnex-bridge/assets/css/ifnex-orders.css +++ b/03_WordPress/wp-content/plugins/ifnex-bridge/assets/css/ifnex-orders.css @@ -211,4 +211,163 @@ max-width: 500px; margin: 40px auto; text-align: center; background: #fff; padding: 40px; border-radius: var(--ifnex-radius); border: 1px solid var(--ifnex-border); +} + +/* ═══ Payment Page ═══ */ +.ifnex-payment-page { max-width: 700px; margin: 0 auto; padding: 20px 0; } +.ifnex-payment-card { + background: #fff; border-radius: var(--ifnex-radius); + border: 1px solid var(--ifnex-border); padding: 32px; +} +.ifnex-payment-card h2 { margin-top: 0; color: var(--ifnex-dark); } + +.ifnex-order-summary { + background: var(--ifnex-light); border-radius: 8px; + padding: 20px; margin-bottom: 24px; +} +.ifnex-summary-row { + display: flex; justify-content: space-between; padding: 8px 0; + border-bottom: 1px dashed var(--ifnex-border); +} +.ifnex-summary-row:last-child { border-bottom: none; } +.ifnex-summary-row.total { + font-size: 18px; font-weight: 700; color: var(--ifnex-primary-dark); + padding-top: 12px; margin-top: 8px; border-top: 2px solid var(--ifnex-primary); +} + +.ifnex-payment-methods { display: grid; gap: 16px; } +.ifnex-payment-option { + border: 2px solid var(--ifnex-border); border-radius: 8px; + padding: 20px; transition: all 0.2s; +} +.ifnex-payment-option:hover { border-color: var(--ifnex-primary); } +.ifnex-payment-option.disabled { opacity: 0.5; pointer-events: none; } +.ifnex-payment-option h3 { margin-top: 0; color: var(--ifnex-dark); } +.ifnex-payment-option p { color: var(--ifnex-gray); margin: 8px 0 16px; } + +.ifnex-warning { + background: #fef3c7; color: #92400e; padding: 10px; + border-radius: 6px; font-size: 13px; margin-bottom: 12px; +} + +.ifnex-payment-footer { + margin-top: 24px; padding-top: 20px; + border-top: 1px solid var(--ifnex-border); text-align: center; +} + +/* ═══ Order Detail Page ═══ */ +.ifnex-order-detail { max-width: 900px; margin: 0 auto; padding: 20px 0; } + +.ifnex-detail-header { + display: flex; justify-content: space-between; align-items: center; + margin-bottom: 20px; flex-wrap: wrap; gap: 12px; +} +.ifnex-detail-title { display: flex; align-items: center; gap: 12px; } +.ifnex-detail-title h2 { margin: 0; color: var(--ifnex-dark); } +.ifnex-awb-code { + background: var(--ifnex-dark); color: #fff; padding: 6px 12px; + border-radius: 6px; font-size: 14px; font-weight: 700; +} +.ifnex-badge-lg { padding: 8px 20px; font-size: 14px; } + +.ifnex-detail-actions { + display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 24px; + padding: 16px; background: var(--ifnex-light); border-radius: 8px; +} + +.ifnex-detail-section { + background: #fff; border: 1px solid var(--ifnex-border); + border-radius: var(--ifnex-radius); padding: 24px; margin-bottom: 20px; +} +.ifnex-detail-section h3 { + margin-top: 0; color: var(--ifnex-dark); padding-bottom: 12px; + border-bottom: 1px solid var(--ifnex-border); margin-bottom: 16px; +} + +.ifnex-route-detail { + display: flex; align-items: center; gap: 20px; padding: 20px; +} +.ifnex-route-point { + flex: 1; text-align: center; padding: 20px; + background: var(--ifnex-light); border-radius: 8px; +} +.ifnex-point-label { + font-size: 12px; color: var(--ifnex-gray); text-transform: uppercase; + letter-spacing: 1px; margin-bottom: 8px; +} +.ifnex-point-country { + font-size: 20px; font-weight: 700; color: var(--ifnex-dark); + margin-bottom: 4px; +} +.ifnex-point-iso { + font-size: 13px; color: var(--ifnex-gray); font-weight: 600; +} +.ifnex-route-line { + display: flex; align-items: center; justify-content: center; + flex: 0.5; +} +.ifnex-plane-icon { font-size: 32px; } + +.ifnex-info-grid { + display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; +} +.ifnex-info-item.full { grid-column: 1 / -1; } +.ifnex-info-label { + display: block; font-size: 12px; color: var(--ifnex-gray); + text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; +} +.ifnex-info-value { font-size: 15px; color: var(--ifnex-dark); font-weight: 500; } + +.ifnex-price-breakdown { padding: 8px 0; } +.ifnex-price-row { + display: flex; justify-content: space-between; padding: 10px 0; + border-bottom: 1px dashed var(--ifnex-border); font-size: 14px; +} +.ifnex-price-row:last-child { border-bottom: none; } +.ifnex-price-row.total { + font-size: 18px; font-weight: 800; color: var(--ifnex-primary-dark); + padding-top: 16px; margin-top: 8px; border-top: 2px solid var(--ifnex-primary); + border-bottom: none; +} + +.ifnex-text-success { color: var(--ifnex-success); font-weight: 600; } +.ifnex-text-warning { color: var(--ifnex-warning); font-weight: 600; } + +/* Timeline */ +.ifnex-timeline { position: relative; padding-right: 20px; } +.ifnex-timeline::before { + content: ''; position: absolute; right: 8px; top: 0; bottom: 0; + width: 2px; background: var(--ifnex-border); +} +.ifnex-timeline-item { + position: relative; padding-right: 32px; padding-bottom: 20px; +} +.ifnex-timeline-item:last-child { padding-bottom: 0; } +.ifnex-timeline-dot { + position: absolute; right: -20px; top: 4px; + width: 16px; height: 16px; border-radius: 50%; + background: var(--ifnex-primary); border: 3px solid #fff; + box-shadow: 0 0 0 2px var(--ifnex-border); +} +.ifnex-timeline-content { + background: var(--ifnex-light); padding: 12px 16px; + border-radius: 8px; border-right: 3px solid var(--ifnex-primary); +} +.ifnex-timeline-header { + display: flex; justify-content: space-between; flex-wrap: wrap; + gap: 8px; margin-bottom: 4px; +} +.ifnex-timeline-date { font-size: 12px; color: var(--ifnex-gray); } +.ifnex-timeline-location { font-size: 13px; color: var(--ifnex-gray); } +.ifnex-empty-timeline { color: var(--ifnex-gray); font-style: italic; } + +@media (max-width: 640px) { + .ifnex-route-detail { flex-direction: column; } + .ifnex-route-line { transform: rotate(90deg); } + .ifnex-info-grid { grid-template-columns: 1fr; } +} + +@media print { + .ifnex-detail-actions { display: none !important; } + .ifnex-order-detail { max-width: 100%; } } \ No newline at end of file diff --git a/03_WordPress/wp-content/plugins/ifnex-bridge/ifnex-bridge.php b/03_WordPress/wp-content/plugins/ifnex-bridge/ifnex-bridge.php index b6df2c9..2fb5f32 100644 --- a/03_WordPress/wp-content/plugins/ifnex-bridge/ifnex-bridge.php +++ b/03_WordPress/wp-content/plugins/ifnex-bridge/ifnex-bridge.php @@ -39,11 +39,13 @@ function ifnex_settings_page_html() { if (isset($_POST['ifnex_api_url']) && check_admin_referer('ifnex_settings_save')) { update_option('ifnex_api_url', sanitize_url($_POST['ifnex_api_url'])); update_option('ifnex_api_key', sanitize_text_field($_POST['ifnex_api_key'])); + update_option('ifnex_bridge_api_key', sanitize_text_field($_POST['ifnex_bridge_api_key'] ?? '')); echo '

تنظیمات ذخیره شد.

'; } $api_url = get_option('ifnex_api_url', 'http://localhost:8000/api/v1'); $api_key = get_option('ifnex_api_key', ''); + $bridge_api_key = get_option('ifnex_bridge_api_key', ''); ?>

تنظیمات IFNEX Logistics Bridge

@@ -52,15 +54,36 @@ function ifnex_settings_page_html() { - + - - + + + + + +
+ +

آدرس پایه API لاراول

+
+ +

برای APIهای عمومی مثل Tracking

+
+ +

مهم: باید با مقدار IFNEX_BRIDGE_API_KEY در فایل .env لاراول یکسان باشد.

+
+ +
+

راهنمای راه‌اندازی

+
    +
  1. در فایل .env لاراول، خط IFNEX_BRIDGE_API_KEY=your-secret-key را تنظیم کنید
  2. +
  3. همان مقدار را در فیلد "Bridge API Key" بالا وارد کنید
  4. +
  5. کاربران وردپرس باید در لاراول با همان ایمیل موجود باشند (از php artisan ifnex:sync-wp-users استفاده کنید)
  6. +
- 👁️ جزئیات @@ -406,8 +406,8 @@ function ifnex_orders_list_shortcode($atts) { - + 💳 پرداخت @@ -604,4 +604,496 @@ function ifnex_order_form_shortcode($atts) {
برای پرداخت، باید وارد شوید.

'; + } + + $atts = shortcode_atts(array('order_id' => ''), $atts); + + // دریافت order_id از URL parameter + $order_id = $atts['order_id'] ?: ($_GET['order_id'] ?? 0); + + if (!$order_id) { + return '

شناسه سفارش مشخص نشده است.

'; + } + + $bridge = new IFNEX_User_Bridge(); + $user_id = get_current_user_id(); + + $order = $bridge->get_customer_order($user_id, $order_id); + + if (is_wp_error($order)) { + return '

' . esc_html($order->get_error_message()) . '

'; + } + + $data = $order['data']; + + // بررسی وضعیت + if ($data['status']['value'] !== 'pending_payment') { + return '
+

✅ این سفارش قبلاً پرداخت شده است

+

وضعیت: ' . esc_html($data['status']['label']) . '

+ بازگشت به سفارشات +
'; + } + + $balance = $bridge->get_wallet_balance($user_id); + $wallet_balance = is_wp_error($balance) ? 0 : ($balance['balance'] ?? 0); + $can_pay_wallet = $wallet_balance >= $data['total_fee']; + + ob_start(); + ?> +
+
+

💳 پرداخت سفارش

+ +
+
+ شماره پیگیری: + +
+
+ مسیر: + +
+
+ مبلغ قابل پرداخت: + ریال +
+
+ +
+ +
+

💰 پرداخت از کیف پول

+

موجودی فعلی: ریال

+ + + +

⚠️ موجودی کافی نیست. لطفاً ابتدا کیف پول خود را شارژ کنید.

+ شارژ کیف پول + +
+ + +
+

🏦 پرداخت از درگاه بانکی

+

انتقال به درگاه پرداخت امن (زرین‌پال)

+ +
+
+ + + + +
+
+ + + برای مشاهده جزئیات، باید وارد شوید.

'; + } + + $atts = shortcode_atts(array('order_id' => ''), $atts); + $order_id = $atts['order_id'] ?: ($_GET['order_id'] ?? 0); + + // اگر از URL pretty استفاده شده (order-detail/14/) + if (!$order_id && isset($_SERVER['REQUEST_URI'])) { + if (preg_match('/order-detail\/(\d+)/', $_SERVER['REQUEST_URI'], $matches)) { + $order_id = $matches[1]; + } + } + + if (!$order_id) { + return '

شناسه سفارش مشخص نشده است.

'; + } + + $bridge = new IFNEX_User_Bridge(); + $user_id = get_current_user_id(); + + $order = $bridge->get_customer_order($user_id, $order_id); + + if (is_wp_error($order)) { + return '

' . esc_html($order->get_error_message()) . '

'; + } + + $data = $order['data']; + $status = $data['status']; + + ob_start(); + ?> +
+ +
+
+

📦 جزئیات سفارش

+ +
+ + + +
+ + +
+ + ← بازگشت به سفارشات + + + + + 💳 پرداخت + + + + + + + + +
+ + +
+

🗺️ مسیر ارسال

+
+
+
از
+
+ +
+
+ +
+
+
+
✈️
+
+
+
به
+
+ +
+
+ +
+
+
+
+ + +
+

📦 اطلاعات مرسوله

+
+
+ نوع سرویس + +
+
+ جهت ارسال + + + +
+
+ وزن واقعی + kg +
+
+ وزن قابل محاسبه + kg +
+
+ تاریخ ثبت + +
+
+
+ + +
+

💰 اطلاعات مالی

+
+
+ مبلغ خالص (درهم): + AED +
+
+ مبلغ خالص (ریال): + ریال +
+
+ مبلغ کل: + ریال +
+
+ وضعیت پرداخت: + + + +
+
+
+ + + +
+

👤 اطلاعات فرستنده

+
+
+ نام + +
+
+ تلفن + +
+
+ آدرس + +
+
+
+ + + + +
+

📮 اطلاعات گیرنده

+
+
+ نام + +
+
+ تلفن + +
+
+ آدرس + +
+
+
+ + + + +
+

📍 تاریخچه رهگیری

+
+ +
+
+
+
+ + + + + - + + +
+ +
+ 📍 +
+ +
+
+ +
+
+ +
+

📍 تاریخچه رهگیری

+

هنوز رویدادی ثبت نشده است.

+
+ +
+ + + time()) { return $token; } - $user = get_userdata($user_id); - if (!$user) { + // دریافت اطلاعات کاربر وردپرس + $wp_user = get_userdata($user_id); + if (!$wp_user) { return false; } - $token = $this->request_token($user->user_email, $this->generate_temp_password($user_id)); - + // درخواست توکن از Bridge endpoint (جدید) + $token = $this->request_bridge_token( + $user_id, + $wp_user->user_email, + $wp_user->display_name + ); + if ($token) { update_user_meta($user_id, 'ifnex_laravel_token', $token); update_user_meta($user_id, 'ifnex_laravel_token_expiry', date('Y-m-d H:i:s', strtotime('+30 days'))); @@ -43,13 +50,18 @@ class IFNEX_User_Bridge { return $token; } - /** - * درخواست توکن از لاراول + /** + * درخواست توکن از Bridge endpoint جدید + * این روش نیازی به رمز عبور ندارد و از API Key مشترک استفاده می‌کند */ - private function request_token($email, $password) { - //$url = $this->api_url . '/sanctum/token'; - - $url = $this->api_url . '/auth/login'; + private function request_bridge_token($wp_user_id, $email, $name) { + $url = $this->api_url . '/bridge/login'; + $bridge_key = get_option('ifnex_bridge_api_key', ''); + + if (empty($bridge_key)) { + error_log('IFNEX: Bridge API Key is not configured in WordPress settings'); + return false; + } $args = array( 'headers' => array( @@ -57,8 +69,10 @@ class IFNEX_User_Bridge { 'Accept' => 'application/json', ), 'body' => json_encode(array( - 'email' => $email, - 'password' => $password, + 'bridge_api_key' => $bridge_key, + 'wp_user_id' => $wp_user_id, + 'wp_user_email' => $email, + 'wp_user_name' => $name, 'token_name' => 'wordpress-bridge-' . get_current_blog_id(), )), 'timeout' => 30, @@ -67,6 +81,7 @@ class IFNEX_User_Bridge { $response = wp_remote_post($url, $args); if (is_wp_error($response)) { + error_log('IFNEX Bridge Error: ' . $response->get_error_message()); return false; } @@ -77,35 +92,23 @@ class IFNEX_User_Bridge { return $data['token']; } + error_log('IFNEX Bridge Token Error: ' . ($data['message'] ?? 'Unknown error')); return false; } - /** - * تولید رمز عبور موقت برای کاربر وردپرس - */ - private function generate_temp_password($user_id) { - $temp_pass = get_user_meta($user_id, 'ifnex_temp_password', true); - - if (!$temp_pass) { - $temp_pass = wp_generate_password(32, true, true); - update_user_meta($user_id, 'ifnex_temp_password', $temp_pass); - } - - return $temp_pass; - } - - /** - * ارسال درخواست احراز هویت به API لاراول + /** + * ارسال درخواست احراز هویت‌شده به API لاراول */ public function authenticated_request($user_id, $endpoint, $method = 'GET', $data = array()) { $token = $this->get_user_token($user_id); - + if (!$token) { return new WP_Error('no_token', 'توکن احراز هویت یافت نشد.'); } $url = $this->api_url . $endpoint; - + $method = strtoupper($method); + $args = array( 'headers' => array( 'Authorization' => 'Bearer ' . $token, @@ -113,16 +116,16 @@ class IFNEX_User_Bridge { 'Content-Type' => 'application/json', ), 'timeout' => 30, + 'method' => $method, // ✅ همیشه متد درخواستی استفاده شود ); - if ($method === 'POST' && !empty($data)) { - $args['body'] = json_encode($data); - $args['method'] = 'POST'; - } else { - $args['method'] = 'GET'; - if (!empty($data)) { - $url = add_query_arg($data, $url); - } + // ✅ اصلاح: بر اساس متد، data را اضافه کن + if (in_array($method, ['POST', 'PUT', 'PATCH'])) { + // حتی اگر data خالی باشد، body را خالی بفرست + $args['body'] = json_encode($data ?: new \stdClass()); + } elseif ($method === 'GET' && !empty($data)) { + // برای GET، data را به URL اضافه کن + $url = add_query_arg($data, $url); } $response = wp_remote_request($url, $args); @@ -132,13 +135,22 @@ class IFNEX_User_Bridge { } $body = wp_remote_retrieve_body($response); + $code = wp_remote_retrieve_response_code($response); $data = json_decode($body, true); - if ($response['response']['code'] === 401) { + // لاگ برای دیباگ + error_log("IFNEX API [{$method}] {$endpoint} => Status: {$code}"); + + if ($code === 401) { $this->invalidate_token(get_current_user_id()); return new WP_Error('token_expired', 'توکن منقضی شده است. لطفاً دوباره وارد شوید.'); } + // اگر پاسخ JSON نیست + if (!is_array($data)) { + return new WP_Error('invalid_response', 'پاسخ نامعتبر از سرور: ' . substr($body, 0, 200)); + } + return $data; } @@ -427,10 +439,12 @@ function ifnex_enqueue_order_assets() { global $post; if (!is_a($post, 'WP_Post')) return; - $has_order_shortcodes = - has_shortcode($post->post_content, 'ifnex_orders_list') || - has_shortcode($post->post_content, 'ifnex_order_form') || - has_shortcode($post->post_content, 'ifnex_user_profile'); + $has_order_shortcodes = + has_shortcode($post->post_content, 'ifnex_orders_list') || + has_shortcode($post->post_content, 'ifnex_order_form') || + has_shortcode($post->post_content, 'ifnex_user_profile') || + has_shortcode($post->post_content, 'ifnex_order_payment') || + has_shortcode($post->post_content, 'ifnex_order_detail'); if ($has_order_shortcodes) { // ✅ اصلاح: dirname(__FILE__) یک سطح بالاتر می‌رود @@ -461,4 +475,76 @@ function ifnex_enqueue_order_assets() { 'orders_url' => home_url('/my-orders/'), )); } +} + +// ══════════════════════════════════════════════════════════════ +// AJAX Handlers برای پرداخت سفارش +// ══════════════════════════════════════════════════════════════ + +add_action('wp_ajax_ifnex_pay_order_wallet', 'ifnex_pay_order_wallet_ajax'); +function ifnex_pay_order_wallet_ajax() { + check_ajax_referer('ifnex_ajax_nonce', 'nonce'); + if (!is_user_logged_in()) wp_send_json_error('باید وارد شوید.'); + + $order_id = intval($_POST['order_id'] ?? 0); + if (!$order_id) wp_send_json_error('شناسه سفارش نامعتبر است.'); + + $bridge = new IFNEX_User_Bridge(); + $result = $bridge->authenticated_request( + get_current_user_id(), + "/customer/orders/{$order_id}/pay-wallet", + 'POST' + ); + + if (is_wp_error($result)) wp_send_json_error($result->get_error_message()); + if (isset($result['success']) && !$result['success']) { + wp_send_json_error($result['message'] ?? 'خطا در پرداخت'); + } + + wp_send_json_success($result); +} + +add_action('wp_ajax_ifnex_pay_order_gateway', 'ifnex_pay_order_gateway_ajax'); +function ifnex_pay_order_gateway_ajax() { + check_ajax_referer('ifnex_ajax_nonce', 'nonce'); + if (!is_user_logged_in()) wp_send_json_error('باید وارد شوید.'); + + $order_id = intval($_POST['order_id'] ?? 0); + $callback_url = sanitize_url($_POST['callback_url'] ?? ''); + + if (!$order_id) wp_send_json_error('شناسه سفارش نامعتبر است.'); + if (!$callback_url) wp_send_json_error('آدرس بازگشت نامعتبر است.'); + + $bridge = new IFNEX_User_Bridge(); + $result = $bridge->authenticated_request( + get_current_user_id(), + "/customer/orders/{$order_id}/pay-gateway", + 'POST', + ['frontend_callback' => $callback_url] + ); + + if (is_wp_error($result)) wp_send_json_error($result->get_error_message()); + if (isset($result['success']) && !$result['success']) { + wp_send_json_error($result['message'] ?? 'خطا در اتصال به درگاه'); + } + + wp_send_json_success($result); +} + +// Enqueue برای صفحه پرداخت +add_action('wp_enqueue_scripts', 'ifnex_enqueue_payment_assets'); +function ifnex_enqueue_payment_assets() { + global $post; + if (!is_a($post, 'WP_Post')) return; + + if (has_shortcode($post->post_content, 'ifnex_order_payment')) { + $plugin_url = plugin_dir_url(dirname(__FILE__)); + wp_enqueue_style('ifnex-orders-css', $plugin_url . 'assets/css/ifnex-orders.css', array(), '1.3.0'); + wp_enqueue_script('jquery'); + wp_localize_script('jquery', 'ifnex_ajax', array( + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('ifnex_ajax_nonce'), + 'orders_url' => home_url('/my-orders/'), + )); + } } \ No newline at end of file