Compare commits

..

2 Commits

Author SHA1 Message Date
Kazem Alghasi
b25bfda597 feat(core): enhance order processing and bridge integration
Refactor the order and shipment lifecycle across WordPress and Laravel,
improving multi-package handling, payment automation, and frontend
reliability.

- Laravel:
  - Rename `package_number` to `package_no` and `description` to
    `content_description` in `ShipmentPackage` model and controller.
  - Update `PaymentController` to automatically transition approved
    shipments to `Processed` status upon successful payment.
  - Adjust `OrderPaymentService` to validate against `Approved` status
    instead of `PendingPayment`.
  - Expose `/countries` endpoint as a public route (unauthenticated).
  - Remove obsolete `read_excel.php` utility.

- WordPress (Bridge Plugin & Theme):
  - Implement AJAX handler for wallet-based order payments.
  - Update `ifnex-order-form.js` to support multi-package input names
    and auto-select Iran based on shipment direction.
  - Improve error handling and feedback in the order form and country
    loading logic.
  - Add automatic tracking submission when an `awb` parameter is
    present in the URL.
  - Update CSS with `!important` flags to ensure correct visibility
    of form steps and dashboard elements.
  - Implement cache-busting for plugin assets and prevent OPcache
    stale files via header controls.
  - Optimize theme logo loading with eager loading and explicit
    dimensions.
2026-09-09 12:03:58 +03:30
Kazem Alghasi
eda3110ac1 fix(ui): fix filter state handling in Filament resources and remove soft deletes
Handle array-based filter states in CustomerCreditResource and
ShipmentChecklistResource to ensure correct query execution.
Also remove SoftDeletes trait from ShipmentChecklist model.
2026-09-03 08:01:00 +03:30
18 changed files with 306 additions and 131 deletions

View File

@ -153,8 +153,8 @@
}
.ifnex-step.active { color: var(--ifnex-primary-dark); font-weight: 700; }
.ifnex-form-step { display: none; }
.ifnex-form-step.active { display: block; animation: ifnexFadeIn 0.3s; }
.ifnex-form-step { display: none !important; }
.ifnex-form-step.active { display: block !important; }
@keyframes ifnexFadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; } }
.ifnex-form-card {
@ -374,14 +374,14 @@
/* ═══ Modern Customer Dashboard (ifnx) ═══ */
.ifnx-dashboard {
display: flex; gap: 24px; max-width: 1240px; margin: 0 auto; padding: 24px 0;
display: flex !important; gap: 24px; max-width: 1240px; margin: 0 auto; padding: 24px 0;
font-family: inherit;
}
/* ── Sidebar ─ */
.ifnx-sidebar {
width: 260px; flex-shrink: 0; align-self: flex-start;
background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
width: 260px !important; flex-shrink: 0 !important; align-self: flex-start !important;
background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%) !important;
border-radius: 20px; overflow: hidden;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.25);
}
@ -395,7 +395,7 @@
.ifnx-user-name { color: #fff; font-weight: 700; font-size: 16px; }
.ifnx-user-email { color: rgba(255,255,255,0.55); font-size: 12px; margin-top: 4px; direction: ltr; }
.ifnx-menu { padding: 12px 0; display: flex; flex-direction: column; }
.ifnx-menu { padding: 12px 0; display: flex !important; flex-direction: column !important; }
.ifnx-menu-item {
display: flex; align-items: center; gap: 12px;
padding: 13px 22px; color: rgba(255,255,255,0.72);

View File

@ -10,20 +10,38 @@ jQuery(document).ready(function($) {
// ─── Load Countries ───
function loadCountries() {
if (!ifnex_ajax || !ifnex_ajax.ajax_url) {
console.error('IFNEX: ifnex_ajax is not defined');
showFormError('خطا در تنظیمات پلاگین. لطفاً صفحه را دوباره بارگذاری کنید.');
return;
}
$.ajax({
url: ifnex_ajax.ajax_url,
method: 'POST',
data: { action: 'ifnex_get_countries', nonce: ifnex_ajax.nonce },
success: function(res) {
if (res.success) {
if (res.success && res.data && res.data.length > 0) {
countries = res.data;
populateCountrySelects();
// Auto-select Iran based on default direction
var iran = countries.find(function(c) { return c.iso_code === 'IR'; });
if (iran) {
var dir = $('#ifnex-direction').val();
if (dir === 'export') {
$('#ifnex-from-country').val(iran.id).trigger('change');
} else if (dir === 'import') {
$('#ifnex-to-country').val(iran.id).trigger('change');
}
}
} else {
console.error('IFNEX: Failed to load countries', res);
showFormError('خطا در دریافت لیست کشورها. لطفاً صفحه را دوباره بارگذاری کنید.');
}
},
error: function(xhr) {
console.error('IFNEX: AJAX error loading countries', xhr.status);
showFormError('خطا در ارتباط با سرور. لطفاً اینترنت خود را بررسی کنید.');
}
});
}
@ -166,26 +184,26 @@ jQuery(document).ready(function($) {
// وزن واقعی
html += '<div class="ifnex-field">';
html += '<label>وزن واقعی (kg) *</label>';
html += '<input type="number" class="ifnex-pkg-weight" step="0.1" min="0.1" data-pkg-id="' + pkg.id + '" placeholder="مثلاً 2.5" value="' + (pkg.weight || '') + '" required>';
html += '<label for="ifnex-pkg-weight-' + pkg.id + '">وزن واقعی (kg) *</label>';
html += '<input type="number" id="ifnex-pkg-weight-' + pkg.id + '" class="ifnex-pkg-weight" name="packages[' + pkg.number + '][weight]" step="0.1" min="0.1" data-pkg-id="' + pkg.id + '" placeholder="مثلاً 2.5" value="' + (pkg.weight || '') + '" required>';
html += '</div>';
// ابعاد
html += '<div class="ifnex-field">';
html += '<label>ابعاد (طول×عرض×ارتفاع cm) *</label>';
html += '<input type="text" class="ifnex-pkg-dimensions" data-pkg-id="' + pkg.id + '" placeholder="مثلاً 5*6*9" value="' + (pkg.dimensions || '') + '" style="direction: ltr;" required>';
html += '<label for="ifnex-pkg-dimensions-' + pkg.id + '">ابعاد (طول×عرض×ارتفاع cm) *</label>';
html += '<input type="text" id="ifnex-pkg-dimensions-' + pkg.id + '" class="ifnex-pkg-dimensions" name="packages[' + pkg.number + '][dimensions]" data-pkg-id="' + pkg.id + '" placeholder="مثلاً 5*6*9" value="' + (pkg.dimensions || '') + '" style="direction: ltr;" required>';
html += '</div>';
// وزن حجمی (محاسبه خودکار - فقط نمایش)
html += '<div class="ifnex-field">';
html += '<label>وزن حجمی (kg)</label>';
html += '<input type="text" class="ifnex-pkg-volumetric-display" data-pkg-id="' + pkg.id + '" value="0" readonly style="background: #f0f9ff; color: #0369a1; font-weight: bold;">';
html += '<label for="ifnex-pkg-volumetric-' + pkg.id + '">وزن حجمی (kg)</label>';
html += '<input type="text" id="ifnex-pkg-volumetric-' + pkg.id + '" class="ifnex-pkg-volumetric-display" name="packages[' + pkg.number + '][volumetric_weight]" data-pkg-id="' + pkg.id + '" value="0" readonly style="background: #f0f9ff; color: #0369a1; font-weight: bold;">';
html += '</div>';
// توضیحات
html += '<div class="ifnex-field full">';
html += '<label>توضیحات بسته (اختیاری)</label>';
html += '<input type="text" class="ifnex-pkg-description" data-pkg-id="' + pkg.id + '" placeholder="مثلاً کتاب، لباس، اسباب‌بازی..." value="' + (pkg.description || '') + '">';
html += '<label for="ifnex-pkg-desc-' + pkg.id + '">توضیحات بسته (اختیاری)</label>';
html += '<input type="text" id="ifnex-pkg-desc-' + pkg.id + '" class="ifnex-pkg-description" name="packages[' + pkg.number + '][description]" data-pkg-id="' + pkg.id + '" placeholder="مثلاً کتاب، لباس، اسباب‌بازی..." value="' + (pkg.description || '') + '">';
html += '</div>';
html += '</div>';
@ -327,32 +345,32 @@ jQuery(document).ready(function($) {
// توضیحات
html += '<div class="ifnex-field full">';
html += '<label>شرح کالا *</label>';
html += '<input type="text" class="ifnex-item-description" data-item-id="' + item.id + '" placeholder="مثلاً Electronics PCB Board" value="' + (item.description || '') + '" required style="direction: ltr;">';
html += '<label for="ifnex-item-desc-' + item.id + '">شرح کالا *</label>';
html += '<input type="text" id="ifnex-item-desc-' + item.id + '" class="ifnex-item-description" name="items[' + item.number + '][description]" data-item-id="' + item.id + '" placeholder="مثلاً Electronics PCB Board" value="' + (item.description || '') + '" required style="direction: ltr;">';
html += '</div>';
// HS Code
html += '<div class="ifnex-field">';
html += '<label>کد گمرکی (HS Code) *</label>';
html += '<input type="text" class="ifnex-item-hs-code" data-item-id="' + item.id + '" placeholder="مثلاً 8542390001" value="' + (item.hsCode || '') + '" required style="direction: ltr;">';
html += '<label for="ifnex-item-hs-' + item.id + '">کد گمرکی (HS Code) *</label>';
html += '<input type="text" id="ifnex-item-hs-' + item.id + '" class="ifnex-item-hs-code" name="items[' + item.number + '][hs_code]" data-item-id="' + item.id + '" placeholder="مثلاً 8542390001" value="' + (item.hsCode || '') + '" required style="direction: ltr;">';
html += '</div>';
// تعداد
html += '<div class="ifnex-field">';
html += '<label>تعداد *</label>';
html += '<input type="number" class="ifnex-item-quantity" data-item-id="' + item.id + '" min="1" step="1" placeholder="مثلاً 100" value="' + (item.quantity || '') + '" required>';
html += '<label for="ifnex-item-qty-' + item.id + '">تعداد *</label>';
html += '<input type="number" id="ifnex-item-qty-' + item.id + '" class="ifnex-item-quantity" name="items[' + item.number + '][quantity]" min="1" step="1" data-item-id="' + item.id + '" placeholder="مثلاً 100" value="' + (item.quantity || '') + '" required>';
html += '</div>';
// قیمت واحد
html += '<div class="ifnex-field">';
html += '<label>قیمت واحد (USD) *</label>';
html += '<input type="number" class="ifnex-item-unit-price" data-item-id="' + item.id + '" min="0" step="0.01" placeholder="مثلاً 1.10" value="' + (item.unitPrice || '') + '" required style="direction: ltr;">';
html += '<label for="ifnex-item-price-' + item.id + '">قیمت واحد (USD) *</label>';
html += '<input type="number" id="ifnex-item-price-' + item.id + '" class="ifnex-item-unit-price" name="items[' + item.number + '][unit_price]" min="0" step="0.01" data-item-id="' + item.id + '" placeholder="مثلاً 1.10" value="' + (item.unitPrice || '') + '" required style="direction: ltr;">';
html += '</div>';
// مجموع (محاسبه خودکار)
html += '<div class="ifnex-field">';
html += '<label>مجموع (USD)</label>';
html += '<input type="text" class="ifnex-item-total-display" data-item-id="' + item.id + '" value="0.00" readonly style="background: #f0f9ff; color: #0369a1; font-weight: bold;">';
html += '<input type="text" id="ifnex-item-total-' + item.id + '" class="ifnex-item-total-display" name="items[' + item.number + '][total]" data-item-id="' + item.id + '" value="0.00" readonly style="background: #f0f9ff; color: #0369a1; font-weight: bold;">';
html += '</div>';
html += '</div>';
@ -448,15 +466,15 @@ jQuery(document).ready(function($) {
function updateInvoiceStepVisibility() {
var type = $('#ifnex-type').val();
if (type === 'PARCEL') {
$('.ifnex-step-invoice').show();
$('.ifnex-step-invoice-content').show();
$('.ifnex-step-invoice').css('display', '');
$('.ifnex-step-invoice-content').each(function() { this.style.setProperty('display', '', 'important'); });
// اگه اقلام خالی هست، یه قلم اولیه اضافه کن
if (invoiceItems.length === 0) {
addInvoiceItem();
}
} else {
$('.ifnex-step-invoice').hide();
$('.ifnex-step-invoice-content').hide();
$('.ifnex-step-invoice').css('display', 'none');
$('.ifnex-step-invoice-content').each(function() { this.style.setProperty('display', 'none', 'important'); });
// اگه کاربر توی Step 3 بود و نوع رو تغییر داد
if (currentStep === 3) {
goToStep(2);
@ -498,16 +516,24 @@ jQuery(document).ready(function($) {
}
// ─── Step Navigation ───
// مراحل فرم خواهر هستند و فقط با کلاس active نمایش داده می‌شوند (CSS داخل ifnex-orders.css)
function goToStep(step) {
currentStep = step;
$('.ifnex-form-step').removeClass('active');
$('.ifnex-form-step[data-step="' + step + '"]').addClass('active');
$('.ifnex-step').each(function() {
var s = parseInt($(this).data('step'));
$(this).toggleClass('active', s === step);
$(this).toggleClass('done', s < step);
var $steps = $('#ifnex-order-form .ifnex-form-step');
$steps.removeClass('active');
$steps.filter('[data-step="' + step + '"]').addClass('active');
// به‌روزرسانی نشانگرهای پیشرفت بالای فرم
$('.ifnex-steps .ifnex-step').each(function() {
var s = parseInt($(this).attr('data-step'), 10);
$(this).removeClass('active done');
if (s === step) $(this).addClass('active');
else if (s < step) $(this).addClass('done');
});
$('.ifnex-order-form-container')[0].scrollIntoView({ behavior: 'smooth' });
try {
document.querySelector('.ifnex-order-form-container').scrollIntoView({ behavior: 'smooth' });
} catch(e) {}
}
$('.ifnex-next-step').on('click', function() {
@ -574,12 +600,15 @@ jQuery(document).ready(function($) {
function validateStep2() {
var ok = true;
var persianRegex = /[\u0600-\u06FF]/;
['#ifnex-sender-name', '#ifnex-sender-phone', '#ifnex-sender-city', '#ifnex-sender-address',
'#ifnex-receiver-name', '#ifnex-receiver-phone', '#ifnex-receiver-city', '#ifnex-receiver-address'].forEach(function(sel) {
if (!$(sel).val()) { $(sel).css('border-color', '#ef4444'); ok = false; }
var val = $(sel).val();
if (!val) { $(sel).css('border-color', '#ef4444'); ok = false; }
else if (persianRegex.test(val)) { $(sel).css('border-color', '#ef4444'); ok = false; }
else { $(sel).css('border-color', ''); }
});
if (!ok) showFormError('لطفاً اطلاعات فرستنده و گیرنده را کامل کنید.');
if (!ok) showFormError('لطفاً اطلاعات را به انگلیسی وارد کنید. آدرس و مشخصات باید به انگلیسی باشد.');
return ok;
}
@ -599,7 +628,7 @@ jQuery(document).ready(function($) {
if (res.success) {
priceData = res.data;
renderPriceSummary(res.data);
goToStep(4);
goToStep(5);
} else {
showCalcError(res.data || 'خطا در محاسبه قیمت');
}
@ -733,7 +762,7 @@ jQuery(document).ready(function($) {
var orderId = res.data.data.id || res.data.id;
// با فلوی جدید، کاربر به صفحه جزئیات سفارش هدایت می‌شود
// (چون پرداخت فقط بعد از تأیید کارمند امکان‌پذیر است)
window.location.href = ifnex_ajax.orders_url + 'order-detail/?order_id=' + orderId;
window.location.href = ifnex_ajax.orders_url + '?tab=orders';
} else {
$('#ifnex-submit-error').text(res.data || 'خطا در ثبت سفارش').show();
}
@ -745,13 +774,26 @@ jQuery(document).ready(function($) {
});
});
// ─── Auto-select Iran based on direction ───
$('#ifnex-direction').on('change', function() {
var direction = $(this).val();
if (!countries.length) return;
var iran = countries.find(function(c) { return c.iso_code === 'IR'; });
if (!iran) return;
if (direction === 'export') {
$('#ifnex-from-country').val(iran.id).trigger('change');
} else if (direction === 'import') {
$('#ifnex-to-country').val(iran.id).trigger('change');
}
});
// ─── Init ───
if ($('#ifnex-order-form').length) {
loadCountries();
// افزودن بسته اول به‌صورت پیش‌فرض
// افزودن بسته اول به‌صورت پیش‌فرض (قبل از loadCountries تا اگه خطا داد، بسته باشه)
addPackage();
// بررسی نوع محموله برای نمایش/مخفی‌کردن Step Invoice
// این تابع خودش اگه PARCEL باشه و اقلام خالی باشه، یه قلم اضافه می‌کنه
updateInvoiceStepVisibility();
// لود کشورها (اگه خطا داد اشکالی نداره)
try { loadCountries(); } catch(e) { console.error('IFNEX: loadCountries error', e); }
}
});

View File

@ -1,4 +1,7 @@
<?php
if (function_exists('opcache_reset')) { @opcache_reset(); }
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
/**
* Plugin Name: IFNEX Logistics Bridge
* Description: اتصال فرانت‌اند سایت ایف‌نکس به API لاراول
@ -16,7 +19,7 @@ require_once plugin_dir_path( __FILE__ ) . 'includes/user-bridge.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/shortcodes.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/tracking-form.php';
// لود کردن استایل‌های پلاگین
// لود کردن استایل‌های و اسکریپت‌های پلاگین
add_action('wp_enqueue_scripts', 'ifnex_enqueue_scripts');
function ifnex_enqueue_scripts() {
global $post;
@ -24,8 +27,15 @@ function ifnex_enqueue_scripts() {
(has_shortcode($post->post_content, 'ifnex_tracking_form') ||
has_shortcode($post->post_content, 'ifnex_wallet_balance') ||
has_shortcode($post->post_content, 'ifnex_transactions') ||
has_shortcode($post->post_content, 'ifnex_tracking_status'))) {
wp_enqueue_style('ifnex-bridge-css', plugin_dir_url(__FILE__) . 'assets/css/ifnex-bridge.css', array(), '1.0.0');
has_shortcode($post->post_content, 'ifnex_tracking_status') ||
has_shortcode($post->post_content, 'ifnex_order_form') ||
has_shortcode($post->post_content, 'ifnex_user_profile') ||
has_shortcode($post->post_content, 'ifnex_orders_list') ||
has_shortcode($post->post_content, 'ifnex_order_detail') ||
has_shortcode($post->post_content, 'ifnex_order_payment') ||
has_shortcode($post->post_content, 'ifnex_customer_dashboard'))) {
wp_enqueue_style('ifnex-bridge-css', plugin_dir_url(__FILE__) . 'assets/css/ifnex-bridge.css', array('ifnex-main'), '2.0.0-' . time());
wp_enqueue_script('ifnex-order-form-js', plugin_dir_url(__FILE__) . 'assets/js/ifnex-order-form.js', array('jquery'), '2.0.0-' . time(), true);
}
}

View File

@ -1,4 +1,7 @@
<?php
if (function_exists('opcache_reset')) { @opcache_reset(); }
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
/**
* IFNEX API Client
* اتصال به API لاراول برای استعلام ترکینگ

View File

@ -1,4 +1,7 @@
<?php
if (function_exists('opcache_reset')) { @opcache_reset(); }
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
/**
* IFNEX Shortcodes
* شورت‌کدهای پلاگین IFNEX Bridge
@ -18,13 +21,16 @@ function ifnex_tracking_form_shortcode($atts) {
'button_text' => 'رهگیری',
), $atts);
// خواندن پارامتر awb از URL
$awb_from_url = isset($_GET['awb']) ? sanitize_text_field($_GET['awb']) : '';
ob_start();
?>
<div class="ifnex-tracking-container">
<h2><?php echo esc_html($atts['title']); ?></h2>
<form id="ifnex-tracking-form" class="ifnex-tracking-form">
<div class="ifnex-form-group">
<input type="text" id="ifnex-awb-input" placeholder="<?php echo esc_attr($atts['placeholder']); ?>" required>
<input type="text" id="ifnex-awb-input" placeholder="<?php echo esc_attr($atts['placeholder']); ?>" value="<?php echo esc_attr($awb_from_url); ?>" required>
<button type="submit" id="ifnex-track-btn"><?php echo esc_html($atts['button_text']); ?></button>
</div>
</form>
@ -537,28 +543,29 @@ function ifnex_order_form_shortcode($atts) {
<h3>📦 مشخصات مرسوله</h3>
<div class="ifnex-form-grid">
<div class="ifnex-field">
<label>جهت ارسال *</label>
<select id="ifnex-direction">
<label for="ifnex-direction">جهت ارسال *</label>
<select id="ifnex-direction" name="ifnex_direction">
<option value="export">صادرات (از ایران به خارج)</option>
<option value="import">واردات (از خارج به ایران)</option>
</select>
</div>
<div class="ifnex-field">
<label>نوع سرویس *</label>
<select id="ifnex-type">
<label for="ifnex-type">نوع سرویس *</label>
<select id="ifnex-type" name="ifnex_type">
<option value="PARCEL">بسته (Parcel)</option>
<option value="DOC_NORMAL">مدارک عادی</option>
<option value="DOC_ECONOMY">مدارک اقتصادی</option>
</select>
</div>
<div class="ifnex-field">
<label>کشور مبدأ *</label>
<select id="ifnex-from-country"></select>
<label for="ifnex-from-country">کشور مبدأ *</label>
<select id="ifnex-from-country" name="ifnex_from_country"></select>
</div>
<div class="ifnex-field">
<label>کشور مقصد *</label>
<select id="ifnex-to-country"></select>
<label for="ifnex-to-country">کشور مقصد *</label>
<select id="ifnex-to-country" name="ifnex_to_country"></select>
</div>
</div>
<!-- ─── بخش بسته‌ها (Multi-Package) ─── -->
<h3 style="margin-top: 24px;">📦 بسته‌های مرسوله</h3>
<p style="font-size: 12px; color: #666; margin-bottom: 12px;">
@ -602,17 +609,17 @@ function ifnex_order_form_shortcode($atts) {
<div class="ifnex-form-card">
<h3>👤 اطلاعات فرستنده</h3>
<div class="ifnex-form-grid">
<div class="ifnex-field"><label>نام و نام خانوادگی *</label><input type="text" id="ifnex-sender-name"></div>
<div class="ifnex-field"><label>شماره تماس *</label><input type="tel" id="ifnex-sender-phone"></div>
<div class="ifnex-field"><label>شهر</label><input type="text" id="ifnex-sender-city"></div>
<div class="ifnex-field full"><label>آدرس کامل *</label><textarea id="ifnex-sender-address" rows="2"></textarea></div>
<div class="ifnex-field"><label for="ifnex-sender-name">نام و نام خانوادگی *</label><input type="text" id="ifnex-sender-name" name="ifnex_sender_name"></div>
<div class="ifnex-field"><label for="ifnex-sender-phone">شماره تماس *</label><input type="tel" id="ifnex-sender-phone" name="ifnex_sender_phone"></div>
<div class="ifnex-field"><label for="ifnex-sender-city">شهر</label><input type="text" id="ifnex-sender-city" name="ifnex_sender_city"></div>
<div class="ifnex-field full"><label for="ifnex-sender-address">آدرس کامل *</label><textarea id="ifnex-sender-address" name="ifnex_sender_address" rows="2"></textarea></div>
</div>
<h3 style="margin-top:24px;">📮 اطلاعات گیرنده</h3>
<div class="ifnex-form-grid">
<div class="ifnex-field"><label>نام و نام خانوادگی *</label><input type="text" id="ifnex-receiver-name"></div>
<div class="ifnex-field"><label>شماره تماس *</label><input type="tel" id="ifnex-receiver-phone"></div>
<div class="ifnex-field"><label>شهر</label><input type="text" id="ifnex-receiver-city"></div>
<div class="ifnex-field full"><label>آدرس کامل *</label><textarea id="ifnex-receiver-address" rows="2"></textarea></div>
<div class="ifnex-field"><label for="ifnex-receiver-name">نام و نام خانوادگی *</label><input type="text" id="ifnex-receiver-name" name="ifnex_receiver_name"></div>
<div class="ifnex-field"><label for="ifnex-receiver-phone">شماره تماس *</label><input type="tel" id="ifnex-receiver-phone" name="ifnex_receiver_phone"></div>
<div class="ifnex-field"><label for="ifnex-receiver-city">شهر</label><input type="text" id="ifnex-receiver-city" name="ifnex_receiver_city"></div>
<div class="ifnex-field full"><label for="ifnex-receiver-address">آدرس کامل *</label><textarea id="ifnex-receiver-address" name="ifnex_receiver_address" rows="2"></textarea></div>
</div>
<div class="ifnex-form-nav">
<button class="ifnex-btn ifnex-prev-step"> قبلی</button>
@ -661,7 +668,7 @@ function ifnex_order_form_shortcode($atts) {
<div class="ifnex-form-card">
<h3>🏷️ کد تخفیف (اختیاری)</h3>
<div class="ifnex-form-grid">
<div class="ifnex-field full"><label>کد تخفیف</label><input type="text" id="ifnex-discount-code" placeholder="مثلاً WELCOME10"></div>
<div class="ifnex-field full"><label for="ifnex-discount-code">کد تخفیف</label><input type="text" id="ifnex-discount-code" name="ifnex_discount_code" placeholder="مثلاً WELCOME10"></div>
</div>
<div id="ifnex-calc-error" class="ifnex-error-box" style="display:none;"></div>
<div id="ifnex-calc-loading" style="display:none; text-align:center; padding:20px;">
@ -742,12 +749,37 @@ function ifnex_order_payment_shortcode($atts) {
$wallet_balance = is_wp_error($balance) ? 0 : ($balance['balance'] ?? 0);
$can_pay_wallet = $wallet_balance >= $data['total_fee'];
// بررسی نتیجه پرداخت از درگاه (callback)
$payment_status = sanitize_text_field($_GET['status'] ?? '');
$payment_message = sanitize_text_field($_GET['message'] ?? '');
$payment_ref_id = sanitize_text_field($_GET['ref_id'] ?? '');
ob_start();
?>
<div class="ifnex-payment-page">
<div class="ifnex-payment-card">
<h2>💳 پرداخت سفارش</h2>
<?php if ($payment_status === 'success'): ?>
<div class="ifnex-success-box">
<h3> پرداخت با موفقیت انجام شد!</h3>
<p>کد پیگیری تراکنش: <strong><?php echo esc_html($payment_ref_id); ?></strong></p>
<p>مبلغ: <strong><?php echo number_format($data['total_fee']); ?> ریال</strong></p>
<p>سفارش شما در حال پردازش است.</p>
<a href="<?php echo esc_url(home_url('/my-orders/')); ?>" class="ifnex-btn ifnex-btn-primary">مشاهده سفارشات</a>
</div>
<?php elseif ($payment_status === 'failed'): ?>
<div class="ifnex-error-box">
<h3> پرداخت ناموفق بود</h3>
<p><?php echo esc_html($payment_message ?: 'پرداخت با خطا مواجه شد. لطفاً دوباره تلاش کنید.'); ?></p>
</div>
<?php elseif ($payment_status === 'error'): ?>
<div class="ifnex-error-box">
<h3>⚠️ خطا در پرداخت</h3>
<p><?php echo esc_html($payment_message ?: 'خطای سیستمی رخ داده است.'); ?></p>
</div>
<?php else: ?>
<div class="ifnex-order-summary">
<div class="ifnex-summary-row">
<span>شماره پیگیری:</span>
@ -797,7 +829,7 @@ function ifnex_order_payment_shortcode($atts) {
</div>
</div>
</div>
<?php endif; ?>
<script>
jQuery(document).ready(function($) {
// پرداخت از کیف پول

View File

@ -1,4 +1,7 @@
<?php
if (function_exists('opcache_reset')) { @opcache_reset(); }
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
/**
* IFNEX Tracking Form Handler
* JavaScript و منطق فرم رهگیری
@ -67,6 +70,12 @@ function ifnex_tracking_form_scripts() {
};
xhr.send('action=ifnex_track_shipment&awb=' + encodeURIComponent(awb));
});
// ارسال خودکار اگر AWB از URL اومده باشد
var input = document.getElementById('ifnex-awb-input');
if (input && input.value.trim()) {
form.dispatchEvent(new Event('submit'));
}
});
</script>

View File

@ -1,4 +1,7 @@
<?php
if (function_exists('opcache_reset')) { @opcache_reset(); }
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
/**
* IFNEX User Bridge
* مدیریت ارتباط کاربران وردپرس با لاراول
@ -254,13 +257,24 @@ class IFNEX_User_Bridge {
* دریافت لیست کشورها
*/
public function get_countries($user_id) {
$result = $this->authenticated_request($user_id, '/customer/countries');
if (is_wp_error($result)) {
return $result;
$api_url = rtrim(get_option('ifnex_api_url', 'http://localhost:8000/api/v1'), '/');
$response = wp_remote_get($api_url . '/countries', array(
'headers' => array('Accept' => 'application/json'),
'timeout' => 15,
));
if (is_wp_error($response)) {
return $response;
}
return $result['data'] ?? [];
$body = json_decode(wp_remote_retrieve_body($response), true);
$code = wp_remote_retrieve_response_code($response);
if ($code !== 200 || empty($body['data'])) {
return new WP_Error('countries_error', 'خطا در دریافت لیست کشورها');
}
return $body['data'];
}
/**
@ -462,14 +476,14 @@ function ifnex_enqueue_order_assets() {
global $post;
if (!is_a($post, 'WP_Post')) return;
$plugin_url = plugin_dir_url(dirname(__FILE__));
$plugin_url = trailingslashit(plugins_url('', dirname(__FILE__)));
// لود CSS
// لود CSS — وابسته به CSS قالب
wp_enqueue_style(
'ifnex-orders-css',
$plugin_url . 'assets/css/ifnex-orders.css',
array(),
'1.7.0'
'ifnex-orders-css',
$plugin_url . 'assets/css/ifnex-orders.css',
array('ifnex-main'),
'2.0.0-' . time()
);
// لود jQuery & JS
@ -478,7 +492,7 @@ function ifnex_enqueue_order_assets() {
'ifnex-order-form-js',
$plugin_url . 'assets/js/ifnex-order-form.js',
array('jquery'),
'1.7.0',
'2.0.0-' . time(),
true
);
@ -486,7 +500,7 @@ function ifnex_enqueue_order_assets() {
wp_localize_script('ifnex-order-form-js', 'ifnex_ajax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('ifnex_ajax_nonce'),
'orders_url' => home_url('/my-orders/'),
'orders_url' => home_url('/my-account/?tab=orders'),
));
}
@ -532,6 +546,41 @@ function ifnex_wallet_charge_ajax() {
wp_send_json_success($result);
}
// ══════════════════════════════════════════════════════════════
// AJAX Handler برای پرداخت سفارش از کیف پول
// ══════════════════════════════════════════════════════════════
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');
@ -566,14 +615,19 @@ function ifnex_enqueue_payment_assets() {
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.4.0');
$plugin_url = trailingslashit(plugins_url('', dirname(__FILE__)));
wp_enqueue_style(
'ifnex-orders-css',
$plugin_url . 'assets/css/ifnex-orders.css',
array('ifnex-main'),
'2.0.0-' . time()
);
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/'),
));
wp_add_inline_script('jquery', 'var ifnex_ajax = ' . wp_json_encode(array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('ifnex_ajax_nonce'),
'orders_url' => home_url('/my-account/?tab=orders'),
)) . ';', 'before');
}
}
@ -585,24 +639,24 @@ function ifnex_enqueue_payment_assets() {
add_action('wp_enqueue_scripts', 'ifnex_enqueue_global_assets', 20);
function ifnex_enqueue_global_assets() {
$plugin_url = plugin_dir_url(dirname(__FILE__));
$plugin_url = trailingslashit(plugins_url('', dirname(__FILE__)));
// لود CSS اصلی (نسخه را بالا بردیم تا cache مرورگر بشکند)
// لود CSS اصلی — وابسته به CSS قالب تا بعد از آن لود شود
wp_enqueue_style(
'ifnex-orders-css',
$plugin_url . 'assets/css/ifnex-orders.css',
array(),
'1.6.0'
array('ifnex-main'),
'2.0.0-' . time()
);
// لود jQuery
wp_enqueue_script('jquery');
// متغیرهای AJAX برای همه صفحات
wp_localize_script('jquery', 'ifnex_ajax', array(
wp_add_inline_script('jquery', 'var ifnex_ajax = ' . wp_json_encode(array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('ifnex_ajax_nonce'),
'orders_url' => home_url('/my-account/?tab=orders'),
'home_url' => home_url('/'),
));
)) . ';', 'before');
}

View File

@ -97,4 +97,32 @@
});
});
// ─── Hero Tracking Form ───
const trackingForm = document.querySelector('.tracking-form');
if (trackingForm) {
const trackingInput = trackingForm.querySelector('.tracking-input');
const trackingBtn = trackingForm.querySelector('.tracking-btn');
function submitTracking() {
var awb = trackingInput.value.trim();
if (!awb) {
trackingInput.focus();
return;
}
var baseUrl = (typeof ifnexConfig !== 'undefined' && ifnexConfig.homeUrl) ? ifnexConfig.homeUrl : '/';
window.location.href = baseUrl.replace(/\/$/, '') + '/tracking/?awb=' + encodeURIComponent(awb);
}
if (trackingBtn) {
trackingBtn.addEventListener('click', submitTracking);
}
trackingInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
submitTracking();
}
});
}
})();

View File

@ -176,6 +176,9 @@ function ifnex_get_logo_html($context = 'header') {
$custom_logo = wp_get_attachment_image($logo_id, 'full', false, [
'class' => 'logo-image',
'alt' => get_bloginfo('name'),
'loading' => 'eager',
'width' => 200,
'height' => 50,
]);
if ($custom_logo) {

View File

@ -134,11 +134,16 @@ class CustomerCreditResource extends Resource
'has_available' => 'باقیمانده',
])
->query(function ($query, $filter) {
match ($filter->getState()) {
$state = $filter->getState();
if (is_array($state)) {
$state = $state['value'] ?? null;
}
match ($state) {
'has_limit' => $query->where('credit_limit', '>', 0),
'no_limit' => $query->where('credit_limit', 0),
'has_used' => $query->where('credit_used', '>', 0),
'has_available' => $query->whereRaw('credit_limit - credit_used > 0'),
default => null,
};
}),
])

View File

@ -128,9 +128,11 @@ class ShipmentChecklistResource extends Resource
'pending' => 'در انتظار',
])
->query(function ($query, $filter) {
match ($filter->getState()) {
$state = is_array($filter->getState()) ? ($filter->getState()['value'] ?? null) : $filter->getState();
match ($state) {
'completed' => $query->where('is_completed', true),
'pending' => $query->where('is_completed', false),
default => null,
};
}),
Tables\Filters\SelectFilter::make('status')

View File

@ -266,24 +266,24 @@ class CustomerOrderController extends Controller
ShipmentPackage::create([
'shipment_id' => $shipment->id,
'package_number' => $index + 1,
'package_no' => $index + 1,
'weight' => $pkg['weight'],
'volumetric_weight' => $volWeight,
'chargeable_weight' => $chargeable,
'dimensions' => $pkg['dimensions'] ?? null,
'description' => $pkg['description'] ?? null,
'content_description' => $pkg['description'] ?? null,
]);
}
} else {
// حالت قدیمی: یه بسته با وزن کلی سفارش
ShipmentPackage::create([
'shipment_id' => $shipment->id,
'package_number' => 1,
'package_no' => 1,
'weight' => $validated['weight'],
'volumetric_weight' => $validated['volumetric_weight'] ?? 0,
'chargeable_weight' => $validated['chargeable_weight'],
'dimensions' => $validated['dimensions'] ?? null,
'description' => null,
'content_description' => null,
]);
}

View File

@ -213,7 +213,14 @@ class PaymentController extends Controller
gatewayReferenceId: $verification['ref_id'] ?? $authority
);
// ─── NEW: اگر تراکنش مربوط به سفارش است، status سفارش را تغییر بده ─
// اگر تراکنش مربوط به سفارش است، status سفارش را تغییر بده
$metadata = $transaction->metadata ?? [];
if (!empty($metadata['is_order_payment']) && !empty($metadata['shipment_id'])) {
$shipment = \App\Models\Shipment::find($metadata['shipment_id']);
if ($shipment && $shipment->status === \App\Enums\ShipmentStatus::Approved) {
$shipment->update(['status' => \App\Enums\ShipmentStatus::Processed]);
}
}
Log::info('Payment completed successfully', [
'transaction_id' => $transaction->id,

View File

@ -4,11 +4,10 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class ShipmentChecklist extends Model
{
use HasFactory, SoftDeletes;
use HasFactory;
protected $fillable = [
'shipment_id',

View File

@ -9,12 +9,12 @@ class ShipmentPackage extends Model
{
protected $fillable = [
'shipment_id',
'package_number',
'package_no',
'weight',
'volumetric_weight',
'chargeable_weight',
'dimensions',
'description',
'content_description',
];
protected $casts = [

View File

@ -163,7 +163,7 @@ class OrderPaymentService
return DB::transaction(function () use ($transaction) {
$shipment = Shipment::find($transaction->transactionable_id);
if (!$shipment || $shipment->status !== ShipmentStatus::PendingPayment) {
if (!$shipment || $shipment->status !== ShipmentStatus::Approved) {
return false;
}

View File

@ -1,26 +0,0 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$reader = IOFactory::createReader('Xlsx');
$spreadsheet = $reader->load(__DIR__ . '/../01_Documents/Sheet ENG invoice.xlsx');
$sheet = $spreadsheet->getActiveSheet();
echo 'Sheet: ' . $sheet->getTitle() . PHP_EOL;
echo 'Rows: ' . $sheet->getHighestRow() . PHP_EOL;
// Use toArray for cleaner output
$data = $sheet->toArray(null, true, true, true);
echo PHP_EOL . '=== CONTENT ===' . PHP_EOL;
foreach ($data as $rowIndex => $row) {
$line = '';
foreach ($row as $colIndex => $value) {
if ($value !== null && trim((string)$value) !== '') {
$line .= $colIndex . '=' . trim((string)$value) . ' | ';
}
}
if ($line) echo 'Row' . $rowIndex . ': ' . $line . PHP_EOL;
}

View File

@ -29,6 +29,13 @@ Route::middleware([ApiKeyMiddleware::class])->prefix('v1')->group(function () {
Route::get('/track/{awb_no}', [TrackController::class, 'show']);
});
// ══════════════════════════════════════════════════════════════
// API عمومی (بدون نیاز به احراز هویت)
// ══════════════════════════════════════════════════════════════
Route::prefix('v1')->group(function () {
Route::get('/countries', [CustomerOrderController::class, 'countries']);
});
// ══════════════════════════════════════════════════════════════
// Auth API (عمومی - برای دریافت توکن)
// ══════════════════════════════════════════════════════════════
@ -60,8 +67,8 @@ Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
// پروفایل و آمار
Route::get('/profile', [CustomerOrderController::class, 'profile']);
// لیست کشورها
Route::get('/countries', [CustomerOrderController::class, 'countries']);
// لیست کشورها (عمومی - بدون نیاز به احراز هویت)
// این endpoint در گروه auth:sanctum نیست
// سفارشات
Route::get('/orders', [CustomerOrderController::class, 'index']);