- Phase 3.5.1: Multi-Package support in order form

- Phase 3.5.2: Invoice items form (PARCEL only)
- ShipmentPackage model and migration
- Updated CustomerOrderController to accept packages[] and items[]
- Auto-calculate volumetric weight from dimensions
- 5-step order form with conditional Invoice step

Phase 3.5 — core complete"
git push origin main
This commit is contained in:
Kazem Alghasi 2026-08-29 07:33:28 +03:30
parent 1c8fb2c9d9
commit 1e4a8532fa
2 changed files with 306 additions and 8 deletions

View File

@ -5,6 +5,8 @@ jQuery(document).ready(function($) {
var countries = [];
var priceData = null;
var packages = []; // آرایه بسته‌ها
var invoiceItems = []; // آرایه اقلام گمرکی
var MAX_ITEMS = 9; // حداکثر تعداد اقلام
// ─── Load Countries ───
function loadCountries() {
@ -276,6 +278,225 @@ jQuery(document).ready(function($) {
removePackage(pkgId);
});
// ══════════════════════════════════════════════════════════════
// ─── Invoice Items Management (برای PARCEL) ───
// ══════════════════════════════════════════════════════════════
// افزودن یه قلم جدید
function addInvoiceItem() {
if (invoiceItems.length >= MAX_ITEMS) {
alert('حداکثر ' + MAX_ITEMS + ' قلم می‌توانید اضافه کنید.');
return;
}
var itemNumber = invoiceItems.length + 1;
var item = {
id: 'item_' + Date.now() + '_' + itemNumber,
number: itemNumber,
description: '',
hsCode: '',
quantity: '',
unitPrice: ''
};
invoiceItems.push(item);
renderInvoiceItem(item);
updateItemsSummary();
}
// حذف یه قلم
function removeInvoiceItem(itemId) {
if (invoiceItems.length <= 1) {
alert('حداقل باید یک قلم وجود داشته باشد.');
return;
}
invoiceItems = invoiceItems.filter(function(i) { return i.id !== itemId; });
invoiceItems.forEach(function(item, i) {
item.number = i + 1;
});
renderAllInvoiceItems();
updateItemsSummary();
}
// رندر یه قلم
function renderInvoiceItem(item) {
var html = '<div class="ifnex-item-card" data-item-id="' + item.id + '" style="border: 1px solid #ddd; padding: 16px; margin-bottom: 12px; border-radius: 8px; background: #fafafa; position: relative;">';
html += '<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">';
html += '<strong style="color: #f37021; font-size: 14px;">📋 قلم #' + item.number + '</strong>';
html += '<button type="button" class="ifnex-btn ifnex-btn-sm ifnex-btn-danger ifnex-remove-item" data-item-id="' + item.id + '" title="حذف قلم" style="padding: 4px 8px;">❌ حذف</button>';
html += '</div>';
html += '<div class="ifnex-form-grid">';
// توضیحات
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 += '</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 += '</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 += '</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 += '</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 += '</div>';
html += '</div>';
html += '</div>';
$('#ifnex-items-container').append(html);
}
// رندر همه اقلام
function renderAllInvoiceItems() {
$('#ifnex-items-container').empty();
invoiceItems.forEach(function(item) {
renderInvoiceItem(item);
});
}
// محاسبه مجموع یه قلم
function calculateItemTotal(itemId) {
var item = invoiceItems.find(function(i) { return i.id === itemId; });
if (!item) return 0;
var qty = parseFloat(item.quantity) || 0;
var price = parseFloat(item.unitPrice) || 0;
return qty * price;
}
// بروزرسانی خلاصه اقلام
function updateItemsSummary() {
var total = 0;
invoiceItems.forEach(function(item) {
total += calculateItemTotal(item.id);
});
$('#ifnex-item-count').text(invoiceItems.length);
$('#ifnex-invoice-total').text(total.toFixed(2));
if (invoiceItems.length > 0) {
$('#ifnex-items-summary').show();
} else {
$('#ifnex-items-summary').hide();
}
}
// Event: افزودن قلم
$(document).on('click', '#ifnex-add-item', function() {
addInvoiceItem();
});
// Event: تغییر توضیحات
$(document).on('input', '.ifnex-item-description', function() {
var itemId = $(this).data('item-id');
var item = invoiceItems.find(function(i) { return i.id === itemId; });
if (item) item.description = $(this).val();
});
// Event: تغییر HS Code
$(document).on('input', '.ifnex-item-hs-code', function() {
var itemId = $(this).data('item-id');
var item = invoiceItems.find(function(i) { return i.id === itemId; });
if (item) item.hsCode = $(this).val();
});
// Event: تغییر تعداد (محاسبه مجموع)
$(document).on('input', '.ifnex-item-quantity', function() {
var itemId = $(this).data('item-id');
var item = invoiceItems.find(function(i) { return i.id === itemId; });
if (item) {
item.quantity = $(this).val();
var total = calculateItemTotal(itemId);
$('.ifnex-item-total-display[data-item-id="' + itemId + '"]').val(total.toFixed(2));
updateItemsSummary();
}
});
// Event: تغییر قیمت واحد (محاسبه مجموع)
$(document).on('input', '.ifnex-item-unit-price', function() {
var itemId = $(this).data('item-id');
var item = invoiceItems.find(function(i) { return i.id === itemId; });
if (item) {
item.unitPrice = $(this).val();
var total = calculateItemTotal(itemId);
$('.ifnex-item-total-display[data-item-id="' + itemId + '"]').val(total.toFixed(2));
updateItemsSummary();
}
});
// Event: حذف قلم
$(document).on('click', '.ifnex-remove-item', function() {
var itemId = $(this).data('item-id');
removeInvoiceItem(itemId);
});
// ─── بررسی نوع محموله و نمایش/مخفی‌کردن Step Invoice ───
function updateInvoiceStepVisibility() {
var type = $('#ifnex-type').val();
if (type === 'PARCEL') {
$('.ifnex-step-invoice').show();
$('.ifnex-step-invoice-content').show();
// اگه اقلام خالی هست، یه قلم اولیه اضافه کن
if (invoiceItems.length === 0) {
addInvoiceItem();
}
} else {
$('.ifnex-step-invoice').hide();
$('.ifnex-step-invoice-content').hide();
// اگه کاربر توی Step 3 بود و نوع رو تغییر داد
if (currentStep === 3) {
goToStep(2);
}
}
}
$('#ifnex-type').on('change', function() {
updateInvoiceStepVisibility();
});
// اعتبارسنجی اقلام گمرکی
function validateStep3() {
if ($('#ifnex-type').val() !== 'PARCEL') {
return true; // اگه PARCEL نیست، این مرحله نیست
}
if (invoiceItems.length === 0) {
showFormError('حداقل باید یک قلم اضافه کنید.');
return false;
}
var allValid = true;
invoiceItems.forEach(function(item) {
if (!item.description || !item.hsCode || !item.quantity || !item.unitPrice) {
allValid = false;
if (!item.description) $('.ifnex-item-description[data-item-id="' + item.id + '"]').css('border-color', '#ef4444');
if (!item.hsCode) $('.ifnex-item-hs-code[data-item-id="' + item.id + '"]').css('border-color', '#ef4444');
if (!item.quantity) $('.ifnex-item-quantity[data-item-id="' + item.id + '"]').css('border-color', '#ef4444');
if (!item.unitPrice) $('.ifnex-item-unit-price[data-item-id="' + item.id + '"]').css('border-color', '#ef4444');
}
});
if (!allValid) {
showFormError('تمامی فیلدهای اقلام گمرکی را تکمیل کنید.');
return false;
}
return true;
}
// ─── Step Navigation ───
function goToStep(step) {
currentStep = step;
@ -293,12 +514,24 @@ jQuery(document).ready(function($) {
var next = currentStep + 1;
if (currentStep === 1 && !validateStep1()) return;
if (currentStep === 2 && !validateStep2()) return;
if (currentStep === 3) { calculatePrice(); return; }
if (currentStep === 3 && !validateStep3()) return;
if (currentStep === 4) { calculatePrice(); return; }
// اگه PARCEL نیست، از Step 2 مستقیم به Step 4 بپر
if (currentStep === 2 && $('#ifnex-type').val() !== 'PARCEL') {
goToStep(4);
return;
}
goToStep(next);
});
$('.ifnex-prev-step').on('click', function() {
goToStep(currentStep - 1);
var prev = currentStep - 1;
// اگه PARCEL نیست، از Step 4 مستقیم به Step 2 برگرد
if (currentStep === 4 && $('#ifnex-type').val() !== 'PARCEL') {
goToStep(2);
return;
}
goToStep(prev);
});
function validateStep1() {
@ -402,6 +635,19 @@ jQuery(document).ready(function($) {
};
});
// ساخت آرایه اقلام گمرکی برای ارسال به API (فقط اگه PARCEL هست)
var itemsArray = [];
if ($('#ifnex-type').val() === 'PARCEL') {
itemsArray = invoiceItems.map(function(item) {
return {
description: item.description || '',
hs_code: item.hsCode || '',
quantity: parseInt(item.quantity) || 0,
unit_price: parseFloat(item.unitPrice) || 0
};
});
}
return {
direction: $('#ifnex-direction').val(),
type: $('#ifnex-type').val(),
@ -414,6 +660,7 @@ jQuery(document).ready(function($) {
volumetric_weight: totalVolumetric,
chargeable_weight: chargeableWeight,
packages: packagesArray,
items: itemsArray,
discount_code: $('#ifnex-discount-code').val() || '',
sender_name: $('#ifnex-sender-name').val(),
sender_phone: $('#ifnex-sender-phone').val(),
@ -432,6 +679,18 @@ jQuery(document).ready(function($) {
html += '<div class="ifnex-price-row"><span>مجموع وزن واقعی:</span><strong>' + numberFormat(data.weight || 0) + ' kg</strong></div>';
html += '<div class="ifnex-price-row"><span>مجموع وزن حجمی:</span><strong>' + numberFormat(data.volumetric_weight || 0) + ' kg</strong></div>';
html += '<div class="ifnex-price-row"><span>وزن قابل محاسبه:</span><strong>' + numberFormat(data.chargeable_weight || 0) + ' kg</strong></div>';
// نمایش اقلام گمرکی (اگه PARCEL هست)
if ($('#ifnex-type').val() === 'PARCEL' && invoiceItems.length > 0) {
html += '<hr style="margin: 10px 0;">';
html += '<div class="ifnex-price-row"><span>📋 تعداد اقلام:</span><strong>' + invoiceItems.length + '</strong></div>';
var invoiceTotal = 0;
invoiceItems.forEach(function(item) {
invoiceTotal += calculateItemTotal(item.id);
});
html += '<div class="ifnex-price-row"><span>💰 مجموع فاکتور:</span><strong>$' + numberFormat(invoiceTotal) + ' USD</strong></div>';
}
html += '<hr style="margin: 10px 0;">';
html += '<div class="ifnex-price-row"><span>قیمت پایه:</span><strong>' + (data.base_price || '0') + ' درهم</strong></div>';
html += '<div class="ifnex-price-row"><span>ناخالص:</span><strong>' + (data.net_dirham || '0') + ' درهم</strong></div>';
@ -489,5 +748,8 @@ jQuery(document).ready(function($) {
loadCountries();
// افزودن بسته اول به‌صورت پیش‌فرض
addPackage();
// بررسی نوع محموله برای نمایش/مخفی‌کردن Step Invoice
// این تابع خودش اگه PARCEL باشه و اقلام خالی باشه، یه قلم اضافه می‌کنه
updateInvoiceStepVisibility();
}
});

View File

@ -497,8 +497,9 @@ function ifnex_order_form_shortcode($atts) {
<div class="ifnex-steps">
<div class="ifnex-step active" data-step="1"><span>۱</span><br>مسیر و بسته</div>
<div class="ifnex-step" data-step="2"><span>۲</span><br>اطلاعات تماس</div>
<div class="ifnex-step" data-step="3"><span>۳</span><br>بررسی قیمت</div>
<div class="ifnex-step" data-step="4"><span>۴</span><br>تأیید نهایی</div>
<div class="ifnex-step ifnex-step-invoice" data-step="3" style="display:none;"><span>۳</span><br>اقلام گمرکی</div>
<div class="ifnex-step ifnex-step-discount" data-step="4"><span>۴</span><br>بررسی قیمت</div>
<div class="ifnex-step" data-step="5"><span>۵</span><br>تأیید نهایی</div>
</div>
<div id="ifnex-form-error" class="ifnex-error-box" style="display:none;"></div>
@ -593,8 +594,43 @@ function ifnex_order_form_shortcode($atts) {
</div>
</div>
<!-- Step 3: Review & Discount -->
<div class="ifnex-form-step" data-step="3">
<!-- Step 3: Invoice Items (فقط برای PARCEL) -->
<div class="ifnex-form-step ifnex-step-invoice-content" data-step="3" style="display:none;">
<div class="ifnex-form-card">
<h3>📋 اقلام گمرکی (Invoice)</h3>
<p style="font-size: 12px; color: #666; margin-bottom: 12px;">
اقلام موجود در بسته‌های خود را وارد کنید. این اطلاعات برای فاکتور صادراتی الزامی است. حداکثر ۹ ردیف.
</p>
<div id="ifnex-items-container">
<!-- آیتم‌ها اینجا اضافه می‌شن -->
</div>
<button type="button" id="ifnex-add-item" class="ifnex-btn ifnex-btn-info" style="margin-top: 10px;">
افزودن قلم دیگر
</button>
<!-- خلاصه فاکتور -->
<div class="ifnex-items-summary" id="ifnex-items-summary" style="display:none; margin-top: 16px; padding: 12px; background: #fef3c7; border: 1px solid #fde68a; border-radius: 6px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>📦 تعداد اقلام:</strong> <span id="ifnex-item-count">0</span>
</div>
<div>
<strong>💰 مجموع فاکتور (USD):</strong> <span id="ifnex-invoice-total" style="color: #059669; font-size: 16px;">0.00</span>
</div>
</div>
</div>
<div class="ifnex-form-nav">
<button class="ifnex-btn ifnex-prev-step"> قبلی</button>
<button class="ifnex-btn ifnex-btn-primary ifnex-next-step">ادامه </button>
</div>
</div>
</div>
<!-- Step 4: Review & Discount -->
<div class="ifnex-form-step" data-step="4">
<div class="ifnex-form-card">
<h3>🏷️ کد تخفیف (اختیاری)</h3>
<div class="ifnex-form-grid">
@ -611,8 +647,8 @@ function ifnex_order_form_shortcode($atts) {
</div>
</div>
<!-- Step 4: Confirm -->
<div class="ifnex-form-step" data-step="4">
<!-- Step 5: Confirm -->
<div class="ifnex-form-step" data-step="5">
<div class="ifnex-form-card">
<h3> تأیید نهایی سفارش</h3>
<div class="ifnex-price-summary" id="ifnex-price-summary"></div>