feat(core): implement order approval flow, credit system, and import invoicing
Introduce a comprehensive set of commercial features including a multi-step order approval workflow, customer credit management, and specialized import service invoicing. Key changes: - Implement `pending_approval` and `approved` shipment statuses to allow staff verification before customer payment. - Add a credit system to `User` model with `credit_limit` and `credit_used` to manage customer balances and debts. - Develop a new `importInvoice` PDF generation service following the "Sheet ENG Invoice" specification for import services. - Add Filament resources for managing Audit Logs, Commitment Forms, Customer Credits, and Shipment Checklists. - Implement staff-specific APIs for order approval/rejection and customer financial status monitoring. - Integrate Kavenegar SMS service for mobile verification and notifications. - Add bulk tracking import functionality via CSV/Excel. - Update WordPress bridge assets (CSS/JS) to support the new multi-step order form UI and updated redirection logic. - Update deployment configurations and documentation to reflect new production domains and feature sets.
This commit is contained in:
parent
b35e933475
commit
02c29db696
BIN
01_Documents/Sheet ENG invoice.pdf
Normal file
BIN
01_Documents/Sheet ENG invoice.pdf
Normal file
Binary file not shown.
BIN
01_Documents/Sheet ENG invoice.xlsx
Normal file
BIN
01_Documents/Sheet ENG invoice.xlsx
Normal file
Binary file not shown.
@ -448,3 +448,164 @@
|
|||||||
gap: 0.5rem !important;
|
gap: 0.5rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
ORDER FORM STEPS
|
||||||
|
============================================ */
|
||||||
|
.ifnex-form-step {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-form-step.active {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-steps {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
margin-bottom: 2rem !important;
|
||||||
|
padding: 1rem !important;
|
||||||
|
background: var(--ifnex-gray-50) !important;
|
||||||
|
border-radius: var(--ifnex-radius-lg) !important;
|
||||||
|
border: 1px solid var(--ifnex-gray-200) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-step {
|
||||||
|
flex: 1 !important;
|
||||||
|
text-align: center !important;
|
||||||
|
padding: 0.5rem !important;
|
||||||
|
position: relative !important;
|
||||||
|
color: var(--ifnex-gray-400) !important;
|
||||||
|
font-size: 0.75rem !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-step span {
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
width: 28px !important;
|
||||||
|
height: 28px !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
background: var(--ifnex-gray-200) !important;
|
||||||
|
color: var(--ifnex-gray-500) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
margin-bottom: 0.25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-step.active {
|
||||||
|
color: var(--ifnex-primary) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-step.active span {
|
||||||
|
background: var(--ifnex-primary) !important;
|
||||||
|
color: var(--ifnex-white) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-step.done {
|
||||||
|
color: var(--ifnex-success) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-step.done span {
|
||||||
|
background: var(--ifnex-success) !important;
|
||||||
|
color: var(--ifnex-white) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-form-nav {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
align-items: center !important;
|
||||||
|
margin-top: 1.5rem !important;
|
||||||
|
padding-top: 1rem !important;
|
||||||
|
border-top: 1px solid var(--ifnex-gray-200) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-form-card {
|
||||||
|
background: var(--ifnex-white) !important;
|
||||||
|
border: 1px solid var(--ifnex-gray-200) !important;
|
||||||
|
border-radius: var(--ifnex-radius-lg) !important;
|
||||||
|
padding: 1.5rem !important;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-form-card h3 {
|
||||||
|
font-size: 1rem !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: var(--ifnex-gray-900) !important;
|
||||||
|
margin-bottom: 1rem !important;
|
||||||
|
padding-bottom: 0.5rem !important;
|
||||||
|
border-bottom: 2px solid var(--ifnex-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-form-grid {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: repeat(2, 1fr) !important;
|
||||||
|
gap: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-form-grid .full {
|
||||||
|
grid-column: span 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-field {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-field label {
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
color: var(--ifnex-gray-700) !important;
|
||||||
|
margin-bottom: 0.375rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-field input,
|
||||||
|
.ifnex-field select,
|
||||||
|
.ifnex-field textarea {
|
||||||
|
padding: 0.625rem 0.875rem !important;
|
||||||
|
border: 1px solid var(--ifnex-gray-300) !important;
|
||||||
|
border-radius: var(--ifnex-radius-md) !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-family: var(--ifnex-font-family) !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-field input:focus,
|
||||||
|
.ifnex-field select:focus,
|
||||||
|
.ifnex-field textarea:focus {
|
||||||
|
outline: none !important;
|
||||||
|
border-color: var(--ifnex-primary) !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-error-box {
|
||||||
|
padding: 0.75rem 1rem !important;
|
||||||
|
background: var(--ifnex-danger-light) !important;
|
||||||
|
border: 1px solid var(--ifnex-danger) !important;
|
||||||
|
border-radius: var(--ifnex-radius-md) !important;
|
||||||
|
color: var(--ifnex-danger) !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
margin-bottom: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-success-box {
|
||||||
|
padding: 0.75rem 1rem !important;
|
||||||
|
background: var(--ifnex-success-light) !important;
|
||||||
|
border: 1px solid var(--ifnex-success) !important;
|
||||||
|
border-radius: var(--ifnex-radius-md) !important;
|
||||||
|
color: var(--ifnex-success) !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
margin-bottom: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ifnex-warning-box {
|
||||||
|
padding: 0.75rem 1rem !important;
|
||||||
|
background: var(--ifnex-warning-light) !important;
|
||||||
|
border: 1px solid var(--ifnex-warning) !important;
|
||||||
|
border-radius: var(--ifnex-radius-md) !important;
|
||||||
|
color: #92400e !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
margin-bottom: 1rem !important;
|
||||||
|
}
|
||||||
|
|||||||
@ -731,7 +731,9 @@ jQuery(document).ready(function($) {
|
|||||||
btn.prop('disabled', false).text('✅ تأیید و ثبت سفارش');
|
btn.prop('disabled', false).text('✅ تأیید و ثبت سفارش');
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
var orderId = res.data.data.id || res.data.id;
|
var orderId = res.data.data.id || res.data.id;
|
||||||
window.location.href = ifnex_ajax.orders_url + 'order-payment/?order_id=' + orderId;
|
// با فلوی جدید، کاربر به صفحه جزئیات سفارش هدایت میشود
|
||||||
|
// (چون پرداخت فقط بعد از تأیید کارمند امکانپذیر است)
|
||||||
|
window.location.href = ifnex_ajax.orders_url + 'order-detail/?order_id=' + orderId;
|
||||||
} else {
|
} else {
|
||||||
$('#ifnex-submit-error').text(res.data || 'خطا در ثبت سفارش').show();
|
$('#ifnex-submit-error').text(res.data || 'خطا در ثبت سفارش').show();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -231,18 +231,41 @@ function ifnex_user_profile_shortcode($atts) {
|
|||||||
|
|
||||||
<?php if ($atts['show_wallet'] === 'yes' && !empty($profile['wallet'])): ?>
|
<?php if ($atts['show_wallet'] === 'yes' && !empty($profile['wallet'])): ?>
|
||||||
<!-- موجودی کیف پول -->
|
<!-- موجودی کیف پول -->
|
||||||
<div class="ifnex-wallet-card">
|
<div class="ifnex-wallet-card" style="background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); border: 1px solid #bae6fd; border-radius: 12px; padding: 20px; margin-bottom: 20px;">
|
||||||
<h3>💰 کیف پول</h3>
|
<h3 style="margin: 0 0 15px 0; color: #0369a1;">💰 وضعیت مالی</h3>
|
||||||
<div class="ifnex-wallet-balance">
|
|
||||||
<span class="ifnex-balance-amount">
|
<!-- مانده حساب -->
|
||||||
<?php echo number_format($profile['wallet']['balance']); ?> ریال
|
<div style="background: white; border-radius: 8px; padding: 15px; margin-bottom: 15px; border: 1px solid #e0f2fe;">
|
||||||
</span>
|
<div style="font-size: 12px; color: #666; margin-bottom: 5px;">مانده حساب (موجودی منهای بدهیها)</div>
|
||||||
<?php if ($profile['wallet']['is_frozen']): ?>
|
<div style="font-size: 24px; font-weight: bold; color: <?php echo ($profile['account_balance'] ?? 0) >= 0 ? '#059669' : '#dc2626'; ?>;">
|
||||||
<span class="ifnex-badge ifnex-badge-danger">مسدود</span>
|
<?php echo number_format($profile['account_balance'] ?? 0); ?> ریال
|
||||||
<?php else: ?>
|
</div>
|
||||||
<span class="ifnex-badge ifnex-badge-success">فعال</span>
|
<?php if (($profile['pending_payments_total'] ?? 0) > 0): ?>
|
||||||
|
<div style="font-size: 11px; color: #dc2626; margin-top: 5px;">
|
||||||
|
⚠️ بدهی در انتظار پرداخت: <?php echo number_format($profile['pending_payments_total']); ?> ریال
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- جزئیات -->
|
||||||
|
<div style="display: flex; gap: 15px;">
|
||||||
|
<div style="flex: 1; background: white; border-radius: 8px; padding: 12px; border: 1px solid #e0f2fe;">
|
||||||
|
<div style="font-size: 11px; color: #666;">موجودی کیف پول</div>
|
||||||
|
<div style="font-size: 16px; font-weight: bold; color: #0369a1;">
|
||||||
|
<?php echo number_format($profile['wallet']['balance']); ?> ریال
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; background: white; border-radius: 8px; padding: 12px; border: 1px solid #e0f2fe;">
|
||||||
|
<div style="font-size: 11px; color: #666;">وضعیت</div>
|
||||||
|
<div style="font-size: 14px; font-weight: bold;">
|
||||||
|
<?php if ($profile['wallet']['is_frozen']): ?>
|
||||||
|
<span style="color: #dc2626;">🔴 مسدود</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="color: #059669;">🟢 فعال</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
@ -256,7 +279,11 @@ function ifnex_user_profile_shortcode($atts) {
|
|||||||
<div class="ifnex-stat-label">کل سفارشات</div>
|
<div class="ifnex-stat-label">کل سفارشات</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ifnex-stat-card ifnex-stat-warning">
|
<div class="ifnex-stat-card ifnex-stat-warning">
|
||||||
<div class="ifnex-stat-value"><?php echo number_format($profile['stats']['pending_orders']); ?></div>
|
<div class="ifnex-stat-value"><?php echo number_format($profile['stats']['pending_approval'] ?? 0); ?></div>
|
||||||
|
<div class="ifnex-stat-label">در انتظار تأیید</div>
|
||||||
|
</div>
|
||||||
|
<div class="ifnex-stat-card ifnex-stat-info">
|
||||||
|
<div class="ifnex-stat-value"><?php echo number_format($profile['stats']['pending_payment'] ?? 0); ?></div>
|
||||||
<div class="ifnex-stat-label">در انتظار پرداخت</div>
|
<div class="ifnex-stat-label">در انتظار پرداخت</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ifnex-stat-card ifnex-stat-info">
|
<div class="ifnex-stat-card ifnex-stat-info">
|
||||||
@ -405,7 +432,7 @@ function ifnex_orders_list_shortcode($atts) {
|
|||||||
</button>
|
</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (!$order['status']['is_paid']): ?>
|
<?php if ($order['status']['can_pay']): ?>
|
||||||
<a href="<?php echo esc_url(home_url('/order-payment/?order_id=' . $order['id'])); ?>"
|
<a href="<?php echo esc_url(home_url('/order-payment/?order_id=' . $order['id'])); ?>"
|
||||||
class="ifnex-btn ifnex-btn-sm ifnex-btn-success">
|
class="ifnex-btn ifnex-btn-sm ifnex-btn-success">
|
||||||
💳 پرداخت
|
💳 پرداخت
|
||||||
@ -695,11 +722,18 @@ function ifnex_order_payment_shortcode($atts) {
|
|||||||
|
|
||||||
$data = $order['data'];
|
$data = $order['data'];
|
||||||
|
|
||||||
// بررسی وضعیت
|
// بررسی وضعیت (فقط سفارشات تأیید شده قابل پرداخت هستند)
|
||||||
if ($data['status']['value'] !== 'pending_payment') {
|
if ($data['status']['value'] !== 'approved') {
|
||||||
return '<div class="ifnex-success-box">
|
$message = 'این سفارش هنوز تأیید نشده است و قابل پرداخت نیست.';
|
||||||
<h3>✅ این سفارش قبلاً پرداخت شده است</h3>
|
if ($data['status']['value'] === 'pending_approval') {
|
||||||
<p>وضعیت: ' . esc_html($data['status']['label']) . '</p>
|
$message = 'این سفارش در انتظار تأیید کارمند است. پس از تأیید، گزینه پرداخت فعال خواهد شد.';
|
||||||
|
} elseif ($data['status']['is_paid']) {
|
||||||
|
$message = 'این سفارش قبلاً پرداخت شده است.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<div class="ifnex-warning-box">
|
||||||
|
<h3>⏳ ' . $message . '</h3>
|
||||||
|
<p>وضعیت فعلی: ' . esc_html($data['status']['label']) . '</p>
|
||||||
<a href="' . esc_url(home_url('/my-orders/')) . '" class="ifnex-btn ifnex-btn-primary">بازگشت به سفارشات</a>
|
<a href="' . esc_url(home_url('/my-orders/')) . '" class="ifnex-btn ifnex-btn-primary">بازگشت به سفارشات</a>
|
||||||
</div>';
|
</div>';
|
||||||
}
|
}
|
||||||
@ -938,7 +972,7 @@ function ifnex_order_detail_shortcode($atts) {
|
|||||||
← بازگشت به سفارشات
|
← بازگشت به سفارشات
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<?php if (!$status['is_paid']): ?>
|
<?php if ($status['can_pay']): ?>
|
||||||
<a href="<?php echo esc_url(home_url('/order-payment/?order_id=' . $order_id)); ?>"
|
<a href="<?php echo esc_url(home_url('/order-payment/?order_id=' . $order_id)); ?>"
|
||||||
class="ifnex-btn ifnex-btn-sm ifnex-btn-success">
|
class="ifnex-btn ifnex-btn-sm ifnex-btn-success">
|
||||||
💳 پرداخت
|
💳 پرداخت
|
||||||
|
|||||||
@ -4,6 +4,8 @@ namespace App\Enums;
|
|||||||
|
|
||||||
enum ShipmentStatus: string
|
enum ShipmentStatus: string
|
||||||
{
|
{
|
||||||
|
case PendingApproval = 'pending_approval';
|
||||||
|
case Approved = 'approved';
|
||||||
case PendingPayment = 'pending_payment';
|
case PendingPayment = 'pending_payment';
|
||||||
case Cancelled = 'cancelled';
|
case Cancelled = 'cancelled';
|
||||||
case Processed = 'processed';
|
case Processed = 'processed';
|
||||||
@ -18,15 +20,17 @@ enum ShipmentStatus: string
|
|||||||
public function label(): string
|
public function label(): string
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
|
self::PendingApproval => 'در انتظار تأیید',
|
||||||
|
self::Approved => 'تأیید شده - در انتظار پرداخت',
|
||||||
self::PendingPayment => 'در انتظار پرداخت',
|
self::PendingPayment => 'در انتظار پرداخت',
|
||||||
self::Cancelled => 'لغو شده',
|
self::Cancelled => 'لغو شده',
|
||||||
self::Processed => 'Processed',
|
self::Processed => 'در حال پردازش',
|
||||||
self::PickedUp => 'Picked Up',
|
self::PickedUp => 'تحویل به حملکننده',
|
||||||
self::InTransit => 'In Transit',
|
self::InTransit => 'در حال حمل',
|
||||||
self::OutForDelivery => 'Out For Delivery',
|
self::OutForDelivery => 'آماده تحویل',
|
||||||
self::Failed => 'Failed',
|
self::Failed => 'تحویل ناموفق',
|
||||||
self::Delivered => 'Delivered',
|
self::Delivered => 'تحویل شده',
|
||||||
self::Returned => 'Returned',
|
self::Returned => 'برگشتی',
|
||||||
self::Archived => 'آرشیو',
|
self::Archived => 'آرشیو',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -34,6 +38,8 @@ enum ShipmentStatus: string
|
|||||||
public function color(): string
|
public function color(): string
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
|
self::PendingApproval => 'warning',
|
||||||
|
self::Approved => 'info',
|
||||||
self::PendingPayment => 'warning',
|
self::PendingPayment => 'warning',
|
||||||
self::Cancelled => 'danger',
|
self::Cancelled => 'danger',
|
||||||
self::Processed => 'gray',
|
self::Processed => 'gray',
|
||||||
@ -49,11 +55,37 @@ enum ShipmentStatus: string
|
|||||||
|
|
||||||
public function isPaid(): bool
|
public function isPaid(): bool
|
||||||
{
|
{
|
||||||
return $this !== self::PendingPayment;
|
return !in_array($this, [
|
||||||
|
self::PendingApproval,
|
||||||
|
self::PendingPayment,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function canBeCancelledByCustomer(): bool
|
public function canBeCancelledByCustomer(): bool
|
||||||
{
|
{
|
||||||
return $this === self::PendingPayment;
|
return in_array($this, [
|
||||||
|
self::PendingApproval,
|
||||||
|
self::PendingPayment,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public function isApproved(): bool
|
||||||
|
{
|
||||||
|
return $this === self::Approved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPendingApproval(): bool
|
||||||
|
{
|
||||||
|
return $this === self::PendingApproval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBeApprovedByStaff(): bool
|
||||||
|
{
|
||||||
|
return $this === self::PendingApproval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBePaid(): bool
|
||||||
|
{
|
||||||
|
return $this === self::Approved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
198
04_Laravel/app/Filament/Pages/BulkTrackingImport.php
Normal file
198
04_Laravel/app/Filament/Pages/BulkTrackingImport.php
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
|
use App\Models\Shipment;
|
||||||
|
use App\Enums\ShipmentStatus;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use Filament\Forms\Contracts\HasForms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class BulkTrackingImport extends Page implements HasForms
|
||||||
|
{
|
||||||
|
use InteractsWithForms;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'heroicon-o-arrow-up-tray';
|
||||||
|
protected static ?string $navigationGroup = 'مدیریت عملیات';
|
||||||
|
protected static ?string $navigationLabel = 'ایمپورت گروهی ترکینگ';
|
||||||
|
protected static ?string $title = 'ایمپورت گروهی وضعیت ترکینگ';
|
||||||
|
protected static ?int $navigationSort = 20;
|
||||||
|
protected static string $view = 'filament.pages.bulk-tracking-import';
|
||||||
|
|
||||||
|
public ?array $data = [];
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->form->fill([
|
||||||
|
'csv_file' => null,
|
||||||
|
'status' => 'in_transit',
|
||||||
|
'description' => '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Section::make('فایل CSV')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\FileUpload::make('csv_file')
|
||||||
|
->label('فایل CSV')
|
||||||
|
->acceptedFileTypes(['text/csv', 'text/plain'])
|
||||||
|
->maxSize(10240) // 10MB
|
||||||
|
->required()
|
||||||
|
->helperText('فرمت: awb_no,status,description (هر سطر یک سفارش)'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('تنظیمات')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Select::make('status')
|
||||||
|
->label('وضعیت جدید')
|
||||||
|
->options(collect(ShipmentStatus::cases())->mapWithKeys(fn ($status) => [$status->value => $status->label()])->toArray())
|
||||||
|
->default('in_transit')
|
||||||
|
->required(),
|
||||||
|
Forms\Components\TextInput::make('description')
|
||||||
|
->label('توضیحات پیشفرض')
|
||||||
|
->placeholder('مثال: بهروزرسانی گروهی وضعیت ترکینگ')
|
||||||
|
->default('ایمپورت گروهی وضعیت'),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('پیشنمایش')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Placeholder::make('preview')
|
||||||
|
->label('پیشنمایش فایل')
|
||||||
|
->content(fn () => $this->getPreviewContent()),
|
||||||
|
])
|
||||||
|
->collapsible(),
|
||||||
|
])
|
||||||
|
->statePath('data');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPreviewContent(): string
|
||||||
|
{
|
||||||
|
$file = $this->data['csv_file'] ?? null;
|
||||||
|
|
||||||
|
if (!$file) {
|
||||||
|
return 'فایلی انتخاب نشده است. لطفاً فایل CSV را آپلود کنید.';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$path = is_array($file) ? ($file['path'] ?? reset($file)) : $file;
|
||||||
|
$content = file_get_contents($path);
|
||||||
|
$lines = explode("\n", trim($content));
|
||||||
|
|
||||||
|
$preview = "<div style='font-family: monospace; font-size: 12px; background: #f5f5f5; padding: 10px; border-radius: 5px;'>";
|
||||||
|
$preview .= "<strong>خط اول (هدر):</strong> " . e($lines[0] ?? '') . "<br>";
|
||||||
|
$preview .= "<strong>تعداد خطوط:</strong> " . count($lines) . "<br>";
|
||||||
|
|
||||||
|
if (isset($lines[1])) {
|
||||||
|
$preview .= "<br><strong>نمونه (خط دوم):</strong> " . e($lines[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$preview .= "</div>";
|
||||||
|
return $preview;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return 'خطا در خواندن فایل: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function submit(): void
|
||||||
|
{
|
||||||
|
$data = $this->form->getState();
|
||||||
|
|
||||||
|
$file = $data['csv_file'];
|
||||||
|
$path = is_array($file) ? ($file['path'] ?? reset($file)) : $file;
|
||||||
|
$content = file_get_contents($path);
|
||||||
|
$lines = explode("\n", trim($content));
|
||||||
|
|
||||||
|
// حذف خط اول (هدر)
|
||||||
|
$dataLines = array_slice($lines, 1);
|
||||||
|
|
||||||
|
$successCount = 0;
|
||||||
|
$errorCount = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($dataLines as $lineNumber => $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if (empty($line)) continue;
|
||||||
|
|
||||||
|
$parts = str_getcsv($line);
|
||||||
|
|
||||||
|
if (count($parts) < 1) {
|
||||||
|
$errors[] = "خط " . ($lineNumber + 2) . ": فرمت نادرست";
|
||||||
|
$errorCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$awbNo = $parts[0];
|
||||||
|
$status = $parts[1] ?? $data['status'];
|
||||||
|
$description = $parts[2] ?? $data['description'];
|
||||||
|
|
||||||
|
// بررسی وجود سفارش
|
||||||
|
$shipment = Shipment::where('awb_no', $awbNo)->first();
|
||||||
|
|
||||||
|
if (!$shipment) {
|
||||||
|
$errors[] = "خط " . ($lineNumber + 2) . ": سفارش {$awbNo} یافت نشد";
|
||||||
|
$errorCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// بهروزرسانی وضعیت
|
||||||
|
$shipment->update([
|
||||||
|
'status' => $status,
|
||||||
|
'tracking_description' => $description,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ثبت تاریخچه
|
||||||
|
DB::table('tracking_histories')->insert([
|
||||||
|
'shipment_id' => $shipment->id,
|
||||||
|
'status' => $status,
|
||||||
|
'description' => $description,
|
||||||
|
'location' => 'سیستم ایمپورت گروهی',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$successCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('ایمپورت با موفقیت انجام شد')
|
||||||
|
->body("{$successCount} سفارش با موفقیت بهروزرسانی شد. {$errorCount} خطا رخ داد.")
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
if (!empty($errors)) {
|
||||||
|
Notification::make()
|
||||||
|
->title('خطاها')
|
||||||
|
->body(implode("\n", array_slice($errors, 0, 10)))
|
||||||
|
->warning()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->form->fill([
|
||||||
|
'csv_file' => null,
|
||||||
|
'status' => 'in_transit',
|
||||||
|
'description' => '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('خطا در ایمپورت')
|
||||||
|
->body($e->getMessage())
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
174
04_Laravel/app/Filament/Resources/AuditLogResource.php
Normal file
174
04_Laravel/app/Filament/Resources/AuditLogResource.php
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditLogResource\Pages;
|
||||||
|
use App\Models\AuditLog;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class AuditLogResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = AuditLog::class;
|
||||||
|
protected static ?string $navigationIcon = 'heroicon-o-clock';
|
||||||
|
protected static ?string $navigationGroup = 'گزارشها';
|
||||||
|
protected static ?string $navigationLabel = 'تاریخچه تغییرات';
|
||||||
|
protected static ?string $modelLabel = 'لاگ تغییرات';
|
||||||
|
protected static ?string $pluralModelLabel = 'تاریخچه تغییرات';
|
||||||
|
protected static ?int $navigationSort = 50;
|
||||||
|
protected static ?string $navigationBadgeColor = 'warning';
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form->schema([
|
||||||
|
Forms\Components\Section::make('اطلاعات لاگ')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Placeholder::make('event')
|
||||||
|
->label('نوع رویداد')
|
||||||
|
->content(fn ($record) => match($record->event) {
|
||||||
|
'created' => '🟢 ایجاد',
|
||||||
|
'updated' => '🟡 بهروزرسانی',
|
||||||
|
'deleted' => '🔴 حذف',
|
||||||
|
default => $record->event,
|
||||||
|
}),
|
||||||
|
Forms\Components\Placeholder::make('user')
|
||||||
|
->label('کاربر')
|
||||||
|
->content(fn ($record) => $record->user?->name ?? 'سیستم'),
|
||||||
|
Forms\Components\Placeholder::make('auditable')
|
||||||
|
->label('رکورد')
|
||||||
|
->content(fn ($record) => $record->auditable_type . ' #' . $record->auditable_id),
|
||||||
|
Forms\Components\Placeholder::make('created_at')
|
||||||
|
->label('تاریخ و زمان')
|
||||||
|
->content(fn ($record) => $record->created_at->format('Y-m-d H:i:s')),
|
||||||
|
])->columns(2),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('تغییرات')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Placeholder::make('old_values')
|
||||||
|
->label('مقادیر قبلی')
|
||||||
|
->content(fn ($record) => $record->old_values ? json_encode($record->old_values, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : '-'),
|
||||||
|
Forms\Components\Placeholder::make('new_values')
|
||||||
|
->label('مقادیر جدید')
|
||||||
|
->content(fn ($record) => $record->new_values ? json_encode($record->new_values, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) : '-'),
|
||||||
|
])->columns(2),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('اطلاعات فنی')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Placeholder::make('description')
|
||||||
|
->label('توضیحات')
|
||||||
|
->content(fn ($record) => $record->description ?? '-'),
|
||||||
|
Forms\Components\Placeholder::make('ip_address')
|
||||||
|
->label('آدرس IP')
|
||||||
|
->content(fn ($record) => $record->ip_address ?? '-'),
|
||||||
|
Forms\Components\Placeholder::make('user_agent')
|
||||||
|
->label('مرورگر')
|
||||||
|
->content(fn ($record) => $record->user_agent ?? '-'),
|
||||||
|
])->columns(2),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('id')
|
||||||
|
->label('شناسه')
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('user.name')
|
||||||
|
->label('کاربر')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('event')
|
||||||
|
->label('رویداد')
|
||||||
|
->formatStateUsing(fn ($state) => match($state) {
|
||||||
|
'created' => '🟢 ایجاد',
|
||||||
|
'updated' => '🟡 بهروزرسانی',
|
||||||
|
'deleted' => '🔴 حذف',
|
||||||
|
default => $state,
|
||||||
|
})
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('auditable_type')
|
||||||
|
->label('نوع مدل')
|
||||||
|
->formatStateUsing(fn ($state) => class_basename($state))
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('auditable_id')
|
||||||
|
->label('شناسه رکورد')
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('description')
|
||||||
|
->label('توضیحات')
|
||||||
|
->limit(50)
|
||||||
|
->wrap(),
|
||||||
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
|
->label('تاریخ')
|
||||||
|
->dateTime()
|
||||||
|
->sortable(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
Tables\Filters\SelectFilter::make('event')
|
||||||
|
->label('نوع رویداد')
|
||||||
|
->options([
|
||||||
|
'created' => 'ایجاد',
|
||||||
|
'updated' => 'بهروزرسانی',
|
||||||
|
'deleted' => 'حذف',
|
||||||
|
]),
|
||||||
|
Tables\Filters\SelectFilter::make('user_id')
|
||||||
|
->label('کاربر')
|
||||||
|
->relationship('user', 'name')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Filters\SelectFilter::make('auditable_type')
|
||||||
|
->label('نوع مدل')
|
||||||
|
->options([
|
||||||
|
'App\Models\Shipment' => 'سفارش',
|
||||||
|
'App\Models\User' => 'کاربر',
|
||||||
|
'App\Models\Wallet' => 'کیف پول',
|
||||||
|
'App\Models\WalletTransaction' => 'تراکنش کیف پول',
|
||||||
|
'App\Models\ShipmentChecklist' => 'چکلیست',
|
||||||
|
]),
|
||||||
|
Tables\Filters\Filter::make('created_at')
|
||||||
|
->label('تاریخ ایجاد')
|
||||||
|
->form([
|
||||||
|
Forms\Components\DatePicker::make('created_from')
|
||||||
|
->label('از تاریخ'),
|
||||||
|
Forms\Components\DatePicker::make('created_until')
|
||||||
|
->label('تا تاریخ'),
|
||||||
|
])
|
||||||
|
->query(function ($query, array $data): Builder {
|
||||||
|
return $query
|
||||||
|
->when($data['created_from'], fn ($query, $date) => $query->where('created_at', '>=', $date))
|
||||||
|
->when($data['created_until'], fn ($query, $date) => $query->where('created_at', '<=', $date));
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->defaultSort('created_at', 'desc');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListAuditLogs::route('/'),
|
||||||
|
'edit' => Pages\EditAuditLog::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::where('created_at', '>=', now()->subDays(7))->count();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\AuditLogResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditLogResource;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditAuditLog extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = AuditLogResource::class;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\AuditLogResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\AuditLogResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListAuditLogs extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = AuditLogResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
157
04_Laravel/app/Filament/Resources/CommitmentFormResource.php
Normal file
157
04_Laravel/app/Filament/Resources/CommitmentFormResource.php
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\CommitmentFormResource\Pages;
|
||||||
|
use App\Models\CommitmentForm;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Forms\Components\FileUpload;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class CommitmentFormResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = CommitmentForm::class;
|
||||||
|
|
||||||
|
protected static ?string $navigationIcon = 'heroicon-o-document-text';
|
||||||
|
|
||||||
|
protected static ?string $navigationLabel = 'فایلهای تعهدنامه';
|
||||||
|
|
||||||
|
protected static ?string $navigationGroup = 'تنظیمات';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 5;
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Section::make('اطلاعات تعهدنامه')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('title')
|
||||||
|
->required()
|
||||||
|
->maxLength(255)
|
||||||
|
->label('عنوان'),
|
||||||
|
Forms\Components\Textarea::make('description')
|
||||||
|
->nullable()
|
||||||
|
->rows(3)
|
||||||
|
->label('توضیحات'),
|
||||||
|
Forms\Components\Select::make('direction')
|
||||||
|
->options([
|
||||||
|
'export' => 'صادرات',
|
||||||
|
'import' => 'واردات',
|
||||||
|
'both' => 'هر دو',
|
||||||
|
])
|
||||||
|
->default('both')
|
||||||
|
->required()
|
||||||
|
->label('جهت ارسال'),
|
||||||
|
Forms\Components\Toggle::make('is_active')
|
||||||
|
->default(true)
|
||||||
|
->label('فعال'),
|
||||||
|
Forms\Components\TextInput::make('sort_order')
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->label('ترتیب نمایش'),
|
||||||
|
])->columns(2),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('فایل')
|
||||||
|
->schema([
|
||||||
|
FileUpload::make('file_path')
|
||||||
|
->label('فایل تعهدنامه')
|
||||||
|
->disk('public')
|
||||||
|
->directory('commitment-forms')
|
||||||
|
->acceptedFileTypes(['application/pdf', 'image/png', 'image/jpeg'])
|
||||||
|
->maxSize(10240) // 10MB
|
||||||
|
->required()
|
||||||
|
->columnSpanFull()
|
||||||
|
->extraAttributes(['class' => 'w-full'])
|
||||||
|
->getUploadedFileNameForStorageUsing(function ($file): string {
|
||||||
|
$extension = $file->getClientOriginalExtension();
|
||||||
|
$filename = time() . '_' . uniqid() . '.' . $extension;
|
||||||
|
return $filename;
|
||||||
|
}),
|
||||||
|
Forms\Components\Placeholder::make('file_info')
|
||||||
|
->label('اطلاعات فایل')
|
||||||
|
->content(function ($record) {
|
||||||
|
if (!$record) return 'فایلی آپلود نشده';
|
||||||
|
|
||||||
|
$type = strtoupper(pathinfo($record->file_path, PATHINFO_EXTENSION));
|
||||||
|
$size = $record->formatted_size;
|
||||||
|
|
||||||
|
return "فرمت: {$type} | حجم: {$size}";
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('title')
|
||||||
|
->searchable()
|
||||||
|
->label('عنوان'),
|
||||||
|
Tables\Columns\TextColumn::make('direction')
|
||||||
|
->badge()
|
||||||
|
->color(fn (string $state): string => match ($state) {
|
||||||
|
'export' => 'success',
|
||||||
|
'import' => 'info',
|
||||||
|
'both' => 'warning',
|
||||||
|
})
|
||||||
|
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||||
|
'export' => 'صادرات',
|
||||||
|
'import' => 'واردات',
|
||||||
|
'both' => 'هر دو',
|
||||||
|
})
|
||||||
|
->label('جهت'),
|
||||||
|
Tables\Columns\IconColumn::make('is_active')
|
||||||
|
->boolean()
|
||||||
|
->label('فعال'),
|
||||||
|
Tables\Columns\TextColumn::make('sort_order')
|
||||||
|
->sortable()
|
||||||
|
->label('ترتیب'),
|
||||||
|
Tables\Columns\TextColumn::make('uploader.name')
|
||||||
|
->label('آپلود کننده'),
|
||||||
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
|
->dateTime()
|
||||||
|
->sortable()
|
||||||
|
->label('تاریخ ایجاد'),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
Tables\Filters\SelectFilter::make('direction')
|
||||||
|
->options([
|
||||||
|
'export' => 'صادرات',
|
||||||
|
'import' => 'واردات',
|
||||||
|
'both' => 'هر دو',
|
||||||
|
]),
|
||||||
|
Tables\Filters\TernaryFilter::make('is_active')
|
||||||
|
->label('فعال'),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
Tables\Actions\DeleteAction::make(),
|
||||||
|
Tables\Actions\Action::make('download')
|
||||||
|
->label('دانلود')
|
||||||
|
->icon('heroicon-o-arrow-down-tray')
|
||||||
|
->color('info')
|
||||||
|
->url(fn ($record) => $record->file_url)
|
||||||
|
->openUrlInNewTab(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListCommitmentForms::route('/'),
|
||||||
|
'create' => Pages\CreateCommitmentForm::route('/create'),
|
||||||
|
'edit' => Pages\EditCommitmentForm::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\CommitmentFormResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\CommitmentFormResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateCommitmentForm extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = CommitmentFormResource::class;
|
||||||
|
|
||||||
|
protected function mutateFormDataBeforeCreate(array $data): array
|
||||||
|
{
|
||||||
|
$data['uploaded_by'] = auth()->id();
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\CommitmentFormResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\CommitmentFormResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditCommitmentForm extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = CommitmentFormResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\CommitmentFormResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\CommitmentFormResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListCommitmentForms extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = CommitmentFormResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
242
04_Laravel/app/Filament/Resources/CustomerCreditResource.php
Normal file
242
04_Laravel/app/Filament/Resources/CustomerCreditResource.php
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\CustomerCreditResource\Pages;
|
||||||
|
use App\Models\User;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class CustomerCreditResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = User::class;
|
||||||
|
protected static ?string $navigationIcon = 'heroicon-o-banknotes';
|
||||||
|
protected static ?string $navigationGroup = 'مدیریت مالی';
|
||||||
|
protected static ?string $navigationLabel = 'اعتبار مشتریان';
|
||||||
|
protected static ?string $modelLabel = 'اعتبار مشتری';
|
||||||
|
protected static ?string $pluralModelLabel = 'اعتبار مشتریان';
|
||||||
|
protected static ?int $navigationSort = 10;
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form->schema([
|
||||||
|
Forms\Components\Section::make('اطلاعات مشتری')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('name')
|
||||||
|
->label('نام')
|
||||||
|
->disabled(),
|
||||||
|
Forms\Components\TextInput::make('email')
|
||||||
|
->label('ایمیل')
|
||||||
|
->disabled(),
|
||||||
|
Forms\Components\TextInput::make('phone')
|
||||||
|
->label('موبایل')
|
||||||
|
->disabled(),
|
||||||
|
])->columns(3),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('اطلاعات اعتبار')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('credit_limit')
|
||||||
|
->label('سقف اعتبار (ریال)')
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->minValue(0)
|
||||||
|
->required(),
|
||||||
|
Forms\Components\TextInput::make('credit_used')
|
||||||
|
->label('مبلغ استفاده شده (ریال)')
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->disabled(),
|
||||||
|
Forms\Components\TextInput::make('available_credit')
|
||||||
|
->label('اعتبار باقیمانده (ریال)')
|
||||||
|
->numeric()
|
||||||
|
->disabled()
|
||||||
|
->dehydrated(false),
|
||||||
|
])->columns(3),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('تاریخچه تراکنشها')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Placeholder::make('transactions_history')
|
||||||
|
->label('تراکنشهای اخیر')
|
||||||
|
->content(function ($record) {
|
||||||
|
if (!$record) return '-';
|
||||||
|
|
||||||
|
$transactions = DB::table('wallet_transactions')
|
||||||
|
->where('user_id', $record->id)
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->take(5)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($transactions->isEmpty()) return 'تراکنشی ثبت نشده';
|
||||||
|
|
||||||
|
$html = '<div style="font-size: 12px; line-height: 1.8;">';
|
||||||
|
foreach ($transactions as $tx) {
|
||||||
|
$type = match($tx->type) {
|
||||||
|
'credit_add' => '➕ افزایش اعتبار',
|
||||||
|
'credit_use' => '➖ استفاده از اعتبار',
|
||||||
|
default => $tx->type,
|
||||||
|
};
|
||||||
|
$html .= '<div>' . $type . ': ' . number_format($tx->amount) . ' ریال - ' . $tx->description . '</div>';
|
||||||
|
}
|
||||||
|
$html .= '</div>';
|
||||||
|
return $html;
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('id')
|
||||||
|
->label('شناسه')
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('name')
|
||||||
|
->label('نام')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('email')
|
||||||
|
->label('ایمیل')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('phone')
|
||||||
|
->label('موبایل')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('credit_limit')
|
||||||
|
->label('سقف اعتبار')
|
||||||
|
->numeric()
|
||||||
|
->formatStateUsing(fn ($state) => number_format($state) . ' ریال')
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('credit_used')
|
||||||
|
->label('استفاده شده')
|
||||||
|
->numeric()
|
||||||
|
->formatStateUsing(fn ($state) => number_format($state) . ' ریال')
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('available_credit')
|
||||||
|
->label('باقیمانده')
|
||||||
|
->numeric()
|
||||||
|
->formatStateUsing(fn ($state) => number_format($state) . ' ریال')
|
||||||
|
->color(fn ($state) => $state > 0 ? 'success' : 'danger')
|
||||||
|
->sortable(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
Tables\Filters\SelectFilter::make('has_credit')
|
||||||
|
->label('وضعیت اعتبار')
|
||||||
|
->options([
|
||||||
|
'has_limit' => 'دارای سقف اعتبار',
|
||||||
|
'no_limit' => 'بدون سقف اعتبار',
|
||||||
|
'has_used' => 'استفاده شده',
|
||||||
|
'has_available' => 'باقیمانده',
|
||||||
|
])
|
||||||
|
->query(function ($query, $filter) {
|
||||||
|
match ($filter->getState()) {
|
||||||
|
'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'),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
Tables\Actions\Action::make('add_credit')
|
||||||
|
->label('افزایش اعتبار')
|
||||||
|
->icon('heroicon-o-plus-circle')
|
||||||
|
->color('success')
|
||||||
|
->form([
|
||||||
|
Forms\Components\TextInput::make('amount')
|
||||||
|
->label('مبلغ (ریال)')
|
||||||
|
->numeric()
|
||||||
|
->required()
|
||||||
|
->minValue(1),
|
||||||
|
Forms\Components\TextInput::make('description')
|
||||||
|
->label('توضیحات')
|
||||||
|
->required(),
|
||||||
|
])
|
||||||
|
->action(function ($record, array $data): void {
|
||||||
|
DB::beginTransaction();
|
||||||
|
try {
|
||||||
|
$record->credit_limit += $data['amount'];
|
||||||
|
$record->save();
|
||||||
|
|
||||||
|
DB::table('wallet_transactions')->insert([
|
||||||
|
'user_id' => $record->id,
|
||||||
|
'type' => 'credit_add',
|
||||||
|
'amount' => $data['amount'],
|
||||||
|
'description' => $data['description'],
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Tables\Actions\Action::make('reduce_credit')
|
||||||
|
->label('کاهش اعتبار')
|
||||||
|
->icon('heroicon-o-minus-circle')
|
||||||
|
->color('danger')
|
||||||
|
->form([
|
||||||
|
Forms\Components\TextInput::make('amount')
|
||||||
|
->label('مبلغ (ریال)')
|
||||||
|
->numeric()
|
||||||
|
->required()
|
||||||
|
->minValue(1),
|
||||||
|
Forms\Components\TextInput::make('description')
|
||||||
|
->label('توضیحات')
|
||||||
|
->required(),
|
||||||
|
])
|
||||||
|
->action(function ($record, array $data): void {
|
||||||
|
DB::beginTransaction();
|
||||||
|
try {
|
||||||
|
$record->credit_limit -= $data['amount'];
|
||||||
|
$record->save();
|
||||||
|
|
||||||
|
DB::table('wallet_transactions')->insert([
|
||||||
|
'user_id' => $record->id,
|
||||||
|
'type' => 'credit_reduce',
|
||||||
|
'amount' => $data['amount'],
|
||||||
|
'description' => $data['description'],
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListCustomerCredits::route('/'),
|
||||||
|
'edit' => Pages\EditCustomerCredit::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::where('credit_limit', '>', 0)->count();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\CustomerCreditResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\CustomerCreditResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditCustomerCredit extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = CustomerCreditResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\CustomerCreditResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\CustomerCreditResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListCustomerCredits extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = CustomerCreditResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
196
04_Laravel/app/Filament/Resources/ShipmentChecklistResource.php
Normal file
196
04_Laravel/app/Filament/Resources/ShipmentChecklistResource.php
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ShipmentChecklistResource\Pages;
|
||||||
|
use App\Models\ShipmentChecklist;
|
||||||
|
use App\Enums\ShipmentStatus;
|
||||||
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Form;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class ShipmentChecklistResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = ShipmentChecklist::class;
|
||||||
|
protected static ?string $navigationIcon = 'heroicon-o-clipboard-document-check';
|
||||||
|
protected static ?string $navigationGroup = 'مدیریت عملیات';
|
||||||
|
protected static ?string $navigationLabel = 'چکلیست سفارشات';
|
||||||
|
protected static ?string $modelLabel = 'چکلیست سفارش';
|
||||||
|
protected static ?string $pluralModelLabel = 'چکلیست سفارشات';
|
||||||
|
protected static ?int $navigationSort = 15;
|
||||||
|
|
||||||
|
public static function form(Form $form): Form
|
||||||
|
{
|
||||||
|
return $form->schema([
|
||||||
|
Forms\Components\Section::make('اطلاعات سفارش')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Select::make('shipment_id')
|
||||||
|
->label('سفارش')
|
||||||
|
->relationship('shipment', 'awb_no')
|
||||||
|
->searchable()
|
||||||
|
->required()
|
||||||
|
->getOptionLabelFromRecordUsing(fn ($record) => "{$record->awb_no} - {$record->status->label()}"),
|
||||||
|
Forms\Components\Select::make('user_id')
|
||||||
|
->label('کارمند')
|
||||||
|
->relationship('user', 'name')
|
||||||
|
->searchable()
|
||||||
|
->required(),
|
||||||
|
])->columns(2),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('اطلاعات چکلیست')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\Select::make('status')
|
||||||
|
->label('وضعیت فعلی')
|
||||||
|
->options(collect(ShipmentStatus::cases())->mapWithKeys(fn ($status) => [$status->value => $status->label()])->toArray())
|
||||||
|
->required(),
|
||||||
|
Forms\Components\Toggle::make('is_completed')
|
||||||
|
->label('تکمیل شده')
|
||||||
|
->default(false),
|
||||||
|
Forms\Components\Textarea::make('notes')
|
||||||
|
->label('یادداشتها')
|
||||||
|
->rows(3),
|
||||||
|
Forms\Components\DateTimePicker::make('completed_at')
|
||||||
|
->label('زمان تکمیل')
|
||||||
|
->disabled(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('id')
|
||||||
|
->label('شناسه')
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('shipment.awb_no')
|
||||||
|
->label('شماره سفارش')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('user.name')
|
||||||
|
->label('کارمند')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('status')
|
||||||
|
->label('وضعیت')
|
||||||
|
->formatStateUsing(fn ($state) => match($state) {
|
||||||
|
'pending_approval' => 'در انتظار تأیید',
|
||||||
|
'approved' => 'تأیید شده',
|
||||||
|
'pending_payment' => 'در انتظار پرداخت',
|
||||||
|
'paid' => 'پرداخت شده',
|
||||||
|
'processed' => 'در حال پردازش',
|
||||||
|
'picked_up' => 'جمعآوری شده',
|
||||||
|
'in_transit' => 'در حال ارسال',
|
||||||
|
'out_for_delivery' => 'آماده تحویل',
|
||||||
|
'delivered' => 'تحویل شده',
|
||||||
|
'cancelled' => 'لغو شده',
|
||||||
|
default => $state,
|
||||||
|
})
|
||||||
|
->badge()
|
||||||
|
->color(fn ($state) => match($state) {
|
||||||
|
'pending_approval' => 'warning',
|
||||||
|
'approved' => 'info',
|
||||||
|
'pending_payment' => 'warning',
|
||||||
|
'paid' => 'success',
|
||||||
|
'processed' => 'info',
|
||||||
|
'picked_up' => 'info',
|
||||||
|
'in_transit' => 'primary',
|
||||||
|
'out_for_delivery' => 'success',
|
||||||
|
'delivered' => 'success',
|
||||||
|
'cancelled' => 'danger',
|
||||||
|
default => 'gray',
|
||||||
|
}),
|
||||||
|
Tables\Columns\IconColumn::make('is_completed')
|
||||||
|
->label('تکمیل')
|
||||||
|
->boolean(),
|
||||||
|
Tables\Columns\TextColumn::make('notes')
|
||||||
|
->label('یادداشت')
|
||||||
|
->limit(50)
|
||||||
|
->wrap(),
|
||||||
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
|
->label('تاریخ ایجاد')
|
||||||
|
->dateTime()
|
||||||
|
->sortable()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
Tables\Columns\TextColumn::make('completed_at')
|
||||||
|
->label('زمان تکمیل')
|
||||||
|
->dateTime()
|
||||||
|
->sortable(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
Tables\Filters\SelectFilter::make('is_completed')
|
||||||
|
->label('وضعیت تکمیل')
|
||||||
|
->options([
|
||||||
|
'completed' => 'تکمیل شده',
|
||||||
|
'pending' => 'در انتظار',
|
||||||
|
])
|
||||||
|
->query(function ($query, $filter) {
|
||||||
|
match ($filter->getState()) {
|
||||||
|
'completed' => $query->where('is_completed', true),
|
||||||
|
'pending' => $query->where('is_completed', false),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
Tables\Filters\SelectFilter::make('status')
|
||||||
|
->label('وضعیت سفارش')
|
||||||
|
->options(collect(ShipmentStatus::cases())->mapWithKeys(fn ($status) => [$status->value => $status->label()])->toArray()),
|
||||||
|
Tables\Filters\SelectFilter::make('user_id')
|
||||||
|
->label('کارمند')
|
||||||
|
->relationship('user', 'name')
|
||||||
|
->searchable(),
|
||||||
|
])
|
||||||
|
->actions([
|
||||||
|
Tables\Actions\EditAction::make(),
|
||||||
|
Tables\Actions\Action::make('complete')
|
||||||
|
->label('تکمیل')
|
||||||
|
->icon('heroicon-o-check-circle')
|
||||||
|
->color('success')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->modalHeading('تأیید تکمیل')
|
||||||
|
->modalDescription('آیا از تکمیل این چکلیست اطمینان دارید؟')
|
||||||
|
->action(fn ($record) => $record->complete())
|
||||||
|
->visible(fn ($record) => !$record->is_completed),
|
||||||
|
Tables\Actions\Action::make('add_note')
|
||||||
|
->label('افزودن یادداشت')
|
||||||
|
->icon('heroicon-o-chat-bubble-bottom-center-text')
|
||||||
|
->color('info')
|
||||||
|
->form([
|
||||||
|
Forms\Components\Textarea::make('note')
|
||||||
|
->label('یادداشت جدید')
|
||||||
|
->required(),
|
||||||
|
])
|
||||||
|
->action(function ($record, array $data): void {
|
||||||
|
$existingNotes = $record->notes ? $record->notes . "\n\n" : '';
|
||||||
|
$record->update([
|
||||||
|
'notes' => $existingNotes . "[" . now()->format('Y-m-d H:i') . "] " . $data['note'],
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
Tables\Actions\BulkActionGroup::make([
|
||||||
|
Tables\Actions\DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListShipmentChecklists::route('/'),
|
||||||
|
'create' => Pages\CreateShipmentChecklist::route('/create'),
|
||||||
|
'edit' => Pages\EditShipmentChecklist::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getNavigationBadge(): ?string
|
||||||
|
{
|
||||||
|
return static::getModel()::where('is_completed', false)->count();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\ShipmentChecklistResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ShipmentChecklistResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateShipmentChecklist extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = ShipmentChecklistResource::class;
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\ShipmentChecklistResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ShipmentChecklistResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditShipmentChecklist extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = ShipmentChecklistResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\ShipmentChecklistResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\ShipmentChecklistResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListShipmentChecklists extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = ShipmentChecklistResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -212,6 +212,51 @@ class ShipmentResource extends Resource
|
|||||||
->label('Cash on Delivery'),
|
->label('Cash on Delivery'),
|
||||||
])->columns(4),
|
])->columns(4),
|
||||||
|
|
||||||
|
Forms\Components\Section::make('Import Invoice Fields (Service Sales Invoice)')
|
||||||
|
->schema([
|
||||||
|
Forms\Components\TextInput::make('brand_fee')
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->prefix('ریال')
|
||||||
|
->label('Brand Fee'),
|
||||||
|
Forms\Components\TextInput::make('report_fee')
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->prefix('ریال')
|
||||||
|
->label('Report Fee'),
|
||||||
|
Forms\Components\TextInput::make('customs_clearance_cost')
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->prefix('ریال')
|
||||||
|
->label('Customs Clearance Cost'),
|
||||||
|
Forms\Components\TextInput::make('order_registration_fee')
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->prefix('ریال')
|
||||||
|
->label('Order Registration Fee'),
|
||||||
|
Forms\Components\TextInput::make('other_clearance_charges')
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->prefix('ریال')
|
||||||
|
->label('Other Clearance Charges'),
|
||||||
|
Forms\Components\TextInput::make('exchange_rate')
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->label('Exchange Rate'),
|
||||||
|
Forms\Components\TextInput::make('goods_nature')
|
||||||
|
->nullable()
|
||||||
|
->label('Goods Nature'),
|
||||||
|
Forms\Components\Select::make('invoice_currency')
|
||||||
|
->options([
|
||||||
|
'USD' => 'USD',
|
||||||
|
'EUR' => 'EUR',
|
||||||
|
'AED' => 'AED',
|
||||||
|
'IRR' => 'IRR',
|
||||||
|
])
|
||||||
|
->default('USD')
|
||||||
|
->label('Invoice Currency'),
|
||||||
|
])->columns(4),
|
||||||
|
|
||||||
Forms\Components\Section::make('Sender Info')
|
Forms\Components\Section::make('Sender Info')
|
||||||
->schema([
|
->schema([
|
||||||
Forms\Components\TextInput::make('sender_name')
|
Forms\Components\TextInput::make('sender_name')
|
||||||
@ -406,6 +451,32 @@ class ShipmentResource extends Resource
|
|||||||
->actions([
|
->actions([
|
||||||
Tables\Actions\ViewAction::make(),
|
Tables\Actions\ViewAction::make(),
|
||||||
Tables\Actions\EditAction::make(),
|
Tables\Actions\EditAction::make(),
|
||||||
|
Action::make('approve')
|
||||||
|
->label('تأیید')
|
||||||
|
->icon('heroicon-o-check-circle')
|
||||||
|
->color('success')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->modalHeading('تأیید سفارش')
|
||||||
|
->modalDescription('آیا از تأیید این سفارش اطمینان دارید؟ پس از تأیید، مشتری میتواند پرداخت را انجام دهد.')
|
||||||
|
->modalSubmitActionLabel('بله، تأیید کن')
|
||||||
|
->visible(fn ($record) => $record->status === \App\Enums\ShipmentStatus::PendingApproval)
|
||||||
|
->action(function ($record) {
|
||||||
|
$oldStatus = $record->status;
|
||||||
|
$record->update(['status' => \App\Enums\ShipmentStatus::Approved]);
|
||||||
|
|
||||||
|
\App\Models\ShipmentStatusHistory::create([
|
||||||
|
'shipment_id' => $record->id,
|
||||||
|
'from_status' => $oldStatus->value,
|
||||||
|
'to_status' => \App\Enums\ShipmentStatus::Approved->value,
|
||||||
|
'reason' => 'تأیید توسط مدیر از پنل Filament',
|
||||||
|
'changed_by' => auth()->id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
\Filament\Notifications\Notification::make()
|
||||||
|
->title('سفارش با موفقیت تأیید شد')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
->headerActions([
|
->headerActions([
|
||||||
Action::make('exportCsv')
|
Action::make('exportCsv')
|
||||||
|
|||||||
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\CommitmentForm;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class CommitmentFormController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* دریافت لیست فایلهای تعهدنامه فعال
|
||||||
|
* GET /api/v1/commitment-forms
|
||||||
|
*/
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$forms = CommitmentForm::query()
|
||||||
|
->where('is_active', true)
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get()
|
||||||
|
->map(fn ($form) => [
|
||||||
|
'id' => $form->id,
|
||||||
|
'title' => $form->title,
|
||||||
|
'description' => $form->description,
|
||||||
|
'file_url' => $form->file_url,
|
||||||
|
'file_type' => strtoupper(pathinfo($form->file_path, PATHINFO_EXTENSION)),
|
||||||
|
'direction' => $form->direction,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $forms,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* دریافت تعهدنامهها بر اساس جهت ارسال
|
||||||
|
* GET /api/v1/commitment-forms/{direction}
|
||||||
|
*/
|
||||||
|
public function byDirection(string $direction): JsonResponse
|
||||||
|
{
|
||||||
|
$forms = CommitmentForm::query()
|
||||||
|
->where('is_active', true)
|
||||||
|
->where(function ($query) use ($direction) {
|
||||||
|
$query->where('direction', 'both')
|
||||||
|
->orWhere('direction', $direction);
|
||||||
|
})
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get()
|
||||||
|
->map(fn ($form) => [
|
||||||
|
'id' => $form->id,
|
||||||
|
'title' => $form->title,
|
||||||
|
'description' => $form->description,
|
||||||
|
'file_url' => $form->file_url,
|
||||||
|
'file_type' => strtoupper(pathinfo($form->file_path, PATHINFO_EXTENSION)),
|
||||||
|
'direction' => $form->direction,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $forms,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -212,7 +212,7 @@ class CustomerOrderController extends Controller
|
|||||||
'awb_no' => $awbNo,
|
'awb_no' => $awbNo,
|
||||||
'direction' => $validated['direction'],
|
'direction' => $validated['direction'],
|
||||||
'type' => $validated['type'],
|
'type' => $validated['type'],
|
||||||
'status' => ShipmentStatus::PendingPayment,
|
'status' => ShipmentStatus::PendingApproval,
|
||||||
|
|
||||||
// وزن
|
// وزن
|
||||||
'weight' => $validated['weight'],
|
'weight' => $validated['weight'],
|
||||||
@ -307,7 +307,7 @@ class CustomerOrderController extends Controller
|
|||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => 'سفارش شما با موفقیت ثبت شد. لطفاً برای تکمیل، پرداخت را انجام دهید.',
|
'message' => 'سفارش شما با موفقیت ثبت شد. پس از تأیید کارمند، گزینه پرداخت برای شما فعال خواهد شد.',
|
||||||
'data' => $this->formatShipment($shipment->load(['fromCountry', 'toCountry', 'items'])),
|
'data' => $this->formatShipment($shipment->load(['fromCountry', 'toCountry', 'items'])),
|
||||||
'pricing' => $priceResult,
|
'pricing' => $priceResult,
|
||||||
], 201);
|
], 201);
|
||||||
@ -384,12 +384,25 @@ class CustomerOrderController extends Controller
|
|||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$user->load(['wallet']);
|
$user->load(['wallet']);
|
||||||
|
|
||||||
|
// محاسبه مجموع سفارشات در انتظار پرداخت (بدهی کاربر)
|
||||||
|
$pending_payments_total = Shipment::where('user_id', $user->id)
|
||||||
|
->where('status', ShipmentStatus::Approved)
|
||||||
|
->sum('total_fee');
|
||||||
|
|
||||||
|
// مانده حساب = موجودی کیف پول - بدهیها
|
||||||
|
$wallet_balance = $user->wallet ? $user->wallet->balance : 0;
|
||||||
|
$account_balance = $wallet_balance - $pending_payments_total;
|
||||||
|
|
||||||
// آمار سفارشات
|
// آمار سفارشات
|
||||||
$stats = [
|
$stats = [
|
||||||
'total_orders' => Shipment::where('user_id', $user->id)->count(),
|
'total_orders' => Shipment::where('user_id', $user->id)->count(),
|
||||||
'pending_orders' => Shipment::where('user_id', $user->id)
|
'pending_approval' => Shipment::where('user_id', $user->id)
|
||||||
->where('status', ShipmentStatus::PendingPayment)
|
->where('status', ShipmentStatus::PendingApproval)
|
||||||
->count(),
|
->count(),
|
||||||
|
'pending_payment' => Shipment::where('user_id', $user->id)
|
||||||
|
->where('status', ShipmentStatus::Approved)
|
||||||
|
->count(),
|
||||||
|
'pending_payment_total' => $pending_payments_total,
|
||||||
'active_orders' => Shipment::where('user_id', $user->id)
|
'active_orders' => Shipment::where('user_id', $user->id)
|
||||||
->whereIn('status', [
|
->whereIn('status', [
|
||||||
ShipmentStatus::Processed,
|
ShipmentStatus::Processed,
|
||||||
@ -402,7 +415,8 @@ class CustomerOrderController extends Controller
|
|||||||
->where('status', ShipmentStatus::Delivered)
|
->where('status', ShipmentStatus::Delivered)
|
||||||
->count(),
|
->count(),
|
||||||
'total_spent' => Shipment::where('user_id', $user->id)
|
'total_spent' => Shipment::where('user_id', $user->id)
|
||||||
->where('status', '!=', ShipmentStatus::PendingPayment)
|
->where('status', '!=', ShipmentStatus::PendingApproval)
|
||||||
|
->where('status', '!=', ShipmentStatus::Approved)
|
||||||
->where('status', '!=', ShipmentStatus::Cancelled)
|
->where('status', '!=', ShipmentStatus::Cancelled)
|
||||||
->sum('total_fee'),
|
->sum('total_fee'),
|
||||||
];
|
];
|
||||||
@ -420,6 +434,8 @@ class CustomerOrderController extends Controller
|
|||||||
'balance' => $user->wallet->balance,
|
'balance' => $user->wallet->balance,
|
||||||
'is_frozen' => $user->wallet->is_frozen,
|
'is_frozen' => $user->wallet->is_frozen,
|
||||||
] : null,
|
] : null,
|
||||||
|
'account_balance' => $account_balance,
|
||||||
|
'pending_payments_total' => $pending_payments_total,
|
||||||
'stats' => $stats,
|
'stats' => $stats,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -457,6 +473,8 @@ class CustomerOrderController extends Controller
|
|||||||
'color' => $shipment->status?->color(),
|
'color' => $shipment->status?->color(),
|
||||||
'is_paid' => $shipment->status?->isPaid(),
|
'is_paid' => $shipment->status?->isPaid(),
|
||||||
'can_cancel' => $shipment->status?->canBeCancelledByCustomer(),
|
'can_cancel' => $shipment->status?->canBeCancelledByCustomer(),
|
||||||
|
'can_pay' => $shipment->status?->canBePaid(),
|
||||||
|
'is_approved' => $shipment->status?->isApproved(),
|
||||||
],
|
],
|
||||||
'from_country' => $shipment->fromCountry ? [
|
'from_country' => $shipment->fromCountry ? [
|
||||||
'id' => $shipment->fromCountry->id,
|
'id' => $shipment->fromCountry->id,
|
||||||
@ -565,11 +583,11 @@ class CustomerOrderController extends Controller
|
|||||||
], 403);
|
], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
// بررسی وضعیت
|
// بررسی وضعیت (فقط سفارشات تأیید شده قابل پرداخت هستند)
|
||||||
if ($shipment->status !== ShipmentStatus::PendingPayment) {
|
if ($shipment->status !== ShipmentStatus::Approved) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'این سفارش در وضعیت قابل پرداخت نیست.',
|
'message' => 'این سفارش هنوز تأیید نشده است و قابل پرداخت نیست.',
|
||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -611,11 +629,11 @@ class CustomerOrderController extends Controller
|
|||||||
], 403);
|
], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
// بررسی وضعیت
|
// بررسی وضعیت (فقط سفارشات تأیید شده قابل پرداخت هستند)
|
||||||
if ($shipment->status !== ShipmentStatus::PendingPayment) {
|
if ($shipment->status !== ShipmentStatus::Approved) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'این سفارش در وضعیت قابل پرداخت نیست.',
|
'message' => 'این سفارش هنوز تأیید نشده است و قابل پرداخت نیست.',
|
||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Shipment;
|
||||||
|
use App\Models\WalletTransaction;
|
||||||
|
use App\Enums\ShipmentStatus;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class CustomerFinancialController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* جستجوی مشتری بر اساس نام یا ایمیل
|
||||||
|
* GET /api/v1/staff/customers/search?q=search_term
|
||||||
|
*/
|
||||||
|
public function search(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'q' => ['required', 'string', 'min:2'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$query = User::query()
|
||||||
|
->where(function ($q) use ($validated) {
|
||||||
|
$q->where('name', 'LIKE', "%{$validated['q']}%")
|
||||||
|
->orWhere('email', 'LIKE', "%{$validated['q']}%")
|
||||||
|
->orWhere('phone', 'LIKE', "%{$validated['q']}%");
|
||||||
|
})
|
||||||
|
->with('wallet');
|
||||||
|
|
||||||
|
$customers = $query->take(20)->get()->map(function ($customer) {
|
||||||
|
return [
|
||||||
|
'id' => $customer->id,
|
||||||
|
'name' => $customer->name,
|
||||||
|
'email' => $customer->email,
|
||||||
|
'phone' => $customer->phone,
|
||||||
|
'wallet_balance' => $customer->wallet ? $customer->wallet->balance : 0,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $customers,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* دریافت وضعیت مالی کامل مشتری
|
||||||
|
* GET /api/v1/staff/customers/{customer}/financial-status
|
||||||
|
*/
|
||||||
|
public function financialStatus(User $customer): JsonResponse
|
||||||
|
{
|
||||||
|
// دریافت سفارشات در انتظار پرداخت (تأیید شده)
|
||||||
|
$pending_orders = Shipment::where('user_id', $customer->id)
|
||||||
|
->where('status', ShipmentStatus::Approved)
|
||||||
|
->with(['fromCountry', 'toCountry'])
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// محاسبه بدهی به هر ارز
|
||||||
|
$debts_by_currency = $pending_orders->groupBy(function ($order) {
|
||||||
|
return $order->fromCountry?->iso_code ?? 'unknown';
|
||||||
|
})->map(function ($orders, $currency) {
|
||||||
|
return [
|
||||||
|
'currency' => $currency,
|
||||||
|
'total' => $orders->sum('total_fee'),
|
||||||
|
'count' => $orders->count(),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// دریافت تراکنشهای اخیر
|
||||||
|
$recent_transactions = WalletTransaction::where('user_id', $customer->id)
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->take(10)
|
||||||
|
->get()
|
||||||
|
->map(fn ($tx) => [
|
||||||
|
'id' => $tx->id,
|
||||||
|
'type' => $tx->type,
|
||||||
|
'amount' => $tx->amount,
|
||||||
|
'description' => $tx->description,
|
||||||
|
'created_at' => $tx->created_at->format('Y-m-d H:i'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// دریافت سفارشات اخیر
|
||||||
|
$recent_orders = Shipment::where('user_id', $customer->id)
|
||||||
|
->with(['fromCountry', 'toCountry'])
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->take(10)
|
||||||
|
->get()
|
||||||
|
->map(fn ($order) => [
|
||||||
|
'id' => $order->id,
|
||||||
|
'awb_no' => $order->awb_no,
|
||||||
|
'status' => [
|
||||||
|
'value' => $order->status?->value,
|
||||||
|
'label' => $order->status?->label(),
|
||||||
|
],
|
||||||
|
'direction' => $order->direction?->value,
|
||||||
|
'total_fee' => $order->total_fee,
|
||||||
|
'from_country' => $order->fromCountry?->name,
|
||||||
|
'to_country' => $order->toCountry?->name,
|
||||||
|
'created_at' => $order->created_at->format('Y-m-d H:i'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// محاسبه کل بدهی
|
||||||
|
$total_debt = $pending_orders->sum('total_fee');
|
||||||
|
|
||||||
|
// موجودی کیف پول
|
||||||
|
$wallet_balance = $customer->wallet ? $customer->wallet->balance : 0;
|
||||||
|
|
||||||
|
// مانده حساب
|
||||||
|
$account_balance = $wallet_balance - $total_debt;
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'customer' => [
|
||||||
|
'id' => $customer->id,
|
||||||
|
'name' => $customer->name,
|
||||||
|
'email' => $customer->email,
|
||||||
|
'phone' => $customer->phone,
|
||||||
|
],
|
||||||
|
'financial' => [
|
||||||
|
'wallet_balance' => $wallet_balance,
|
||||||
|
'total_debt' => $total_debt,
|
||||||
|
'account_balance' => $account_balance,
|
||||||
|
'debts_by_currency' => array_values($debts_by_currency->toArray()),
|
||||||
|
'pending_orders_count' => $pending_orders->count(),
|
||||||
|
],
|
||||||
|
'recent_transactions' => $recent_transactions,
|
||||||
|
'recent_orders' => $recent_orders,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\KavenegarSmsService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class MobileVerificationController extends Controller
|
||||||
|
{
|
||||||
|
private KavenegarSmsService $smsService;
|
||||||
|
|
||||||
|
public function __construct(KavenegarSmsService $smsService)
|
||||||
|
{
|
||||||
|
$this->smsService = $smsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ارسال کد تأیید به شماره موبایل
|
||||||
|
* POST /api/v1/verify/send-code
|
||||||
|
*/
|
||||||
|
public function sendCode(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'phone' => ['required', 'string', 'regex:/^09[0-9]{9}$/'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->smsService->sendVerificationCode($validated['phone']);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'کد تأیید با موفقیت ارسال شد',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $result['message'],
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* بررسی کد تأیید
|
||||||
|
* POST /api/v1/verify/check-code
|
||||||
|
*/
|
||||||
|
public function checkCode(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'phone' => ['required', 'string', 'regex:/^09[0-9]{9}$/'],
|
||||||
|
'code' => ['required', 'string', 'size:5'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$isValid = $this->smsService->verifyCode($validated['phone'], $validated['code']);
|
||||||
|
|
||||||
|
if ($isValid) {
|
||||||
|
// بهروزرسانی شماره موبایل کاربر (اختیاری)
|
||||||
|
$user = Auth::user();
|
||||||
|
if ($user && $user->phone !== $validated['phone']) {
|
||||||
|
$user->update(['phone' => $validated['phone']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'کد تأیید صحیح است',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'کد تأیید نادرست است یا منقضی شده است',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ارسال پیامک به شماره موبایل (برای استفاده داخلی)
|
||||||
|
* POST /api/v1/verify/send-sms
|
||||||
|
*/
|
||||||
|
public function sendSms(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'phone' => ['required', 'string', 'regex:/^09[0-9]{9}$/'],
|
||||||
|
'message' => ['required', 'string', 'max:500'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->smsService->send($validated['phone'], $validated['message']);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'پیامک با موفقیت ارسال شد',
|
||||||
|
'cost' => $result['cost'] ?? 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => $result['message'],
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
201
04_Laravel/app/Http/Controllers/Api/StaffOrderController.php
Normal file
201
04_Laravel/app/Http/Controllers/Api/StaffOrderController.php
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Shipment;
|
||||||
|
use App\Models\ShipmentStatusHistory;
|
||||||
|
use App\Enums\ShipmentStatus;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class StaffOrderController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* لیست سفارشات در انتظار تأیید
|
||||||
|
* GET /api/v1/staff/orders/pending-approval
|
||||||
|
*/
|
||||||
|
public function pendingApproval(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$shipments = Shipment::query()
|
||||||
|
->where('status', ShipmentStatus::PendingApproval)
|
||||||
|
->with(['fromCountry', 'toCountry', 'user'])
|
||||||
|
->orderBy('created_at', 'asc')
|
||||||
|
->paginate($validated['per_page'] ?? 15);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'data' => $shipments->map(function ($shipment) {
|
||||||
|
return $this->formatShipment($shipment);
|
||||||
|
}),
|
||||||
|
'pagination' => [
|
||||||
|
'current_page' => $shipments->currentPage(),
|
||||||
|
'last_page' => $shipments->lastPage(),
|
||||||
|
'per_page' => $shipments->perPage(),
|
||||||
|
'total' => $shipments->total(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* تأیید سفارش توسط کارمند
|
||||||
|
* POST /api/v1/staff/orders/{shipment}/approve
|
||||||
|
*/
|
||||||
|
public function approve(Shipment $shipment, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
// بررسی وضعیت
|
||||||
|
if ($shipment->status !== ShipmentStatus::PendingApproval) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'این سفارش در وضعیت قابل تأیید نیست.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'notes' => ['nullable', 'string', 'max:1000'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($shipment, $user, $validated) {
|
||||||
|
// تغییر وضعیت به Approved
|
||||||
|
$oldStatus = $shipment->status;
|
||||||
|
$shipment->update([
|
||||||
|
'status' => ShipmentStatus::Approved,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ثبت در تاریخچه تغییرات
|
||||||
|
ShipmentStatusHistory::create([
|
||||||
|
'shipment_id' => $shipment->id,
|
||||||
|
'old_status' => $oldStatus->value,
|
||||||
|
'new_status' => ShipmentStatus::Approved->value,
|
||||||
|
'changed_by' => $user->id,
|
||||||
|
'notes' => $validated['notes'] ?? 'تأیید توسط کارمند',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'سفارش با موفقیت تأیید شد. مشتری میتواند پرداخت را انجام دهد.',
|
||||||
|
'data' => $this->formatShipment($shipment->fresh()->load(['fromCountry', 'toCountry'])),
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'خطا در تأیید سفارش: ' . $e->getMessage(),
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* رد سفارش توسط کارمند
|
||||||
|
* POST /api/v1/staff/orders/{shipment}/reject
|
||||||
|
*/
|
||||||
|
public function reject(Shipment $shipment, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
// بررسی وضعیت
|
||||||
|
if ($shipment->status !== ShipmentStatus::PendingApproval) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'این سفارش در وضعیت قابل رد نیست.',
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'reason' => ['required', 'string', 'max:1000'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::transaction(function () use ($shipment, $user, $validated) {
|
||||||
|
// تغییر وضعیت به Cancelled
|
||||||
|
$oldStatus = $shipment->status;
|
||||||
|
$shipment->update([
|
||||||
|
'status' => ShipmentStatus::Cancelled,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ثبت در تاریخچه تغییرات
|
||||||
|
ShipmentStatusHistory::create([
|
||||||
|
'shipment_id' => $shipment->id,
|
||||||
|
'old_status' => $oldStatus->value,
|
||||||
|
'new_status' => ShipmentStatus::Cancelled->value,
|
||||||
|
'changed_by' => $user->id,
|
||||||
|
'notes' => 'رد شده: ' . $validated['reason'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'سفارش رد شد.',
|
||||||
|
'data' => $this->formatShipment($shipment->fresh()->load(['fromCountry', 'toCountry'])),
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json([
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'خطا در رد سفارش: ' . $e->getMessage(),
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* فرمت کردن Shipment برای خروجی API
|
||||||
|
*/
|
||||||
|
private function formatShipment(Shipment $shipment): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $shipment->id,
|
||||||
|
'awb_no' => $shipment->awb_no,
|
||||||
|
'direction' => $shipment->direction?->value,
|
||||||
|
'type' => $shipment->type?->value,
|
||||||
|
'status' => [
|
||||||
|
'value' => $shipment->status?->value,
|
||||||
|
'label' => $shipment->status?->label(),
|
||||||
|
'color' => $shipment->status?->color(),
|
||||||
|
],
|
||||||
|
'from_country' => $shipment->fromCountry ? [
|
||||||
|
'id' => $shipment->fromCountry->id,
|
||||||
|
'name' => $shipment->fromCountry->name,
|
||||||
|
'iso_code' => $shipment->fromCountry->iso_code,
|
||||||
|
] : null,
|
||||||
|
'to_country' => $shipment->toCountry ? [
|
||||||
|
'id' => $shipment->toCountry->id,
|
||||||
|
'name' => $shipment->toCountry->name,
|
||||||
|
'iso_code' => $shipment->toCountry->iso_code,
|
||||||
|
] : null,
|
||||||
|
'user' => $shipment->user ? [
|
||||||
|
'id' => $shipment->user->id,
|
||||||
|
'name' => $shipment->user->name,
|
||||||
|
'email' => $shipment->user->email,
|
||||||
|
] : null,
|
||||||
|
'weight' => $shipment->weight,
|
||||||
|
'chargeable_weight' => $shipment->chargeable_weight,
|
||||||
|
'total_fee' => $shipment->total_fee,
|
||||||
|
'created_at' => $shipment->created_at->toIso8601String(),
|
||||||
|
'created_at_jalali' => $this->toJalali($shipment->created_at),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* تبدیل تاریخ به شمسی
|
||||||
|
*/
|
||||||
|
private function toJalali($date): string
|
||||||
|
{
|
||||||
|
if (!$date) return '—';
|
||||||
|
|
||||||
|
try {
|
||||||
|
return \Morilog\Jalali\Jalalian::fromCarbon($date)->format('Y/m/d H:i');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $date->format('Y-m-d H:i');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -128,4 +128,56 @@ class ShipmentPdfController extends Controller
|
|||||||
return response('PDF generation failed: ' . $e->getMessage(), 500);
|
return response('PDF generation failed: ' . $e->getMessage(), 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* تولید PDF فاکتور فروش خدمات (واردات) — مطابق Sheet ENG Invoice
|
||||||
|
*/
|
||||||
|
public function importInvoice(Shipment $shipment, Request $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// دریافت پارامترها از درخواست
|
||||||
|
$options = $request->only([
|
||||||
|
'invoice_no',
|
||||||
|
'invoice_date',
|
||||||
|
'customs_office',
|
||||||
|
'exchange_rate',
|
||||||
|
'international_freight',
|
||||||
|
'pick_up_fee',
|
||||||
|
'brand_fee',
|
||||||
|
'report_fee',
|
||||||
|
'other_charges',
|
||||||
|
'customs_clearance',
|
||||||
|
'domestic_transport',
|
||||||
|
'warehousing_fee',
|
||||||
|
'order_registration_fee',
|
||||||
|
'other_clearance_charges',
|
||||||
|
'terms_and_conditions',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// تبدیل exchange_rate به عدد
|
||||||
|
if (isset($options['exchange_rate'])) {
|
||||||
|
$options['exchange_rate'] = (float) $options['exchange_rate'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = $this->pdf->importInvoice($shipment, $options);
|
||||||
|
|
||||||
|
Log::info('Import Invoice PDF generated successfully', [
|
||||||
|
'awb' => $shipment->awb_no,
|
||||||
|
'options' => $options,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response($content, 200, [
|
||||||
|
'Content-Type' => 'application/pdf',
|
||||||
|
'Content-Disposition' => 'attachment; filename="SERVICE-INVOICE-' . $shipment->awb_no . '.pdf"',
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Import Invoice PDF generation failed', [
|
||||||
|
'shipment_id' => $shipment->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response('PDF generation failed: ' . $e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
04_Laravel/app/Models/AuditLog.php
Normal file
65
04_Laravel/app/Models/AuditLog.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class AuditLog extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'audit_logs';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'event',
|
||||||
|
'auditable_type',
|
||||||
|
'auditable_id',
|
||||||
|
'old_values',
|
||||||
|
'new_values',
|
||||||
|
'description',
|
||||||
|
'ip_address',
|
||||||
|
'user_agent',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'old_values' => 'array',
|
||||||
|
'new_values' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── Relationships ───────────────────────────────
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function auditable()
|
||||||
|
{
|
||||||
|
return $this->morphTo();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helper Methods ──────────────────────────────
|
||||||
|
|
||||||
|
public static function log(
|
||||||
|
string $event,
|
||||||
|
Model $model,
|
||||||
|
array $oldValues = [],
|
||||||
|
array $newValues = [],
|
||||||
|
?string $description = null
|
||||||
|
): self {
|
||||||
|
return static::create([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'event' => $event,
|
||||||
|
'auditable_type' => get_class($model),
|
||||||
|
'auditable_id' => $model->getKey(),
|
||||||
|
'old_values' => $oldValues,
|
||||||
|
'new_values' => $newValues,
|
||||||
|
'description' => $description,
|
||||||
|
'ip_address' => request()->ip(),
|
||||||
|
'user_agent' => request()->userAgent(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
04_Laravel/app/Models/CommitmentForm.php
Normal file
61
04_Laravel/app/Models/CommitmentForm.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class CommitmentForm extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'title',
|
||||||
|
'description',
|
||||||
|
'file_path',
|
||||||
|
'file_type',
|
||||||
|
'file_size',
|
||||||
|
'direction',
|
||||||
|
'is_active',
|
||||||
|
'sort_order',
|
||||||
|
'uploaded_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'sort_order' => 'integer',
|
||||||
|
'file_size' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* کاربری که فایل را آپلود کرده
|
||||||
|
*/
|
||||||
|
public function uploader(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'uploaded_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* آدرس کامل فایل
|
||||||
|
*/
|
||||||
|
public function getFileUrlAttribute(): string
|
||||||
|
{
|
||||||
|
return asset('storage/' . $this->file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* فرمت حجم فایل
|
||||||
|
*/
|
||||||
|
public function getFormattedSizeAttribute(): string
|
||||||
|
{
|
||||||
|
$bytes = $this->file_size;
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
|
||||||
|
for ($i = 0; $bytes >= 1024 && $i < count($units) - 1; $i++) {
|
||||||
|
$bytes /= 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round($bytes, 2) . ' ' . $units[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -34,6 +34,16 @@ class Shipment extends Model
|
|||||||
'net_dirham',
|
'net_dirham',
|
||||||
'net_rial',
|
'net_rial',
|
||||||
'invoice_total_usd',
|
'invoice_total_usd',
|
||||||
|
'cod_amount',
|
||||||
|
// Import Invoice Fields
|
||||||
|
'brand_fee',
|
||||||
|
'report_fee',
|
||||||
|
'customs_clearance_cost',
|
||||||
|
'order_registration_fee',
|
||||||
|
'other_clearance_charges',
|
||||||
|
'exchange_rate',
|
||||||
|
'goods_nature',
|
||||||
|
'invoice_currency',
|
||||||
// Sender
|
// Sender
|
||||||
'sender_name',
|
'sender_name',
|
||||||
'sender_company',
|
'sender_company',
|
||||||
@ -55,6 +65,8 @@ class Shipment extends Model
|
|||||||
// Customs
|
// Customs
|
||||||
'reason_for_export',
|
'reason_for_export',
|
||||||
'content_description',
|
'content_description',
|
||||||
|
// User
|
||||||
|
'user_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
|||||||
48
04_Laravel/app/Models/ShipmentChecklist.php
Normal file
48
04_Laravel/app/Models/ShipmentChecklist.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'shipment_id',
|
||||||
|
'user_id',
|
||||||
|
'status',
|
||||||
|
'is_completed',
|
||||||
|
'notes',
|
||||||
|
'completed_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_completed' => 'boolean',
|
||||||
|
'completed_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── Relationships ───────────────────────────────
|
||||||
|
|
||||||
|
public function shipment()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Shipment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helper Methods ──────────────────────────────
|
||||||
|
|
||||||
|
public function complete(): void
|
||||||
|
{
|
||||||
|
$this->update([
|
||||||
|
'is_completed' => true,
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,6 +18,8 @@ class User extends Authenticatable
|
|||||||
'password',
|
'password',
|
||||||
'phone',
|
'phone',
|
||||||
'role', // فیلد قدیمی - بعداً حذف میکنیم
|
'role', // فیلد قدیمی - بعداً حذف میکنیم
|
||||||
|
'credit_limit', // سقف اعتبار
|
||||||
|
'credit_used', // مبلغ استفاده شده از اعتبار
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
@ -30,6 +32,8 @@ class User extends Authenticatable
|
|||||||
return [
|
return [
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
|
'credit_limit' => 'decimal:2',
|
||||||
|
'credit_used' => 'decimal:2',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,6 +66,18 @@ class User extends Authenticatable
|
|||||||
return $this->hasRole('customer');
|
return $this->hasRole('customer');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Credit Methods ──────────────────────────────
|
||||||
|
|
||||||
|
public function getAvailableCreditAttribute(): float
|
||||||
|
{
|
||||||
|
return $this->credit_limit - $this->credit_used;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasAvailableCredit(float $amount): bool
|
||||||
|
{
|
||||||
|
return $this->available_credit >= $amount;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Filament Panel Access ───────────────────────
|
// ─── Filament Panel Access ───────────────────────
|
||||||
|
|
||||||
public function canAccessPanel(\Filament\Panel $panel): bool
|
public function canAccessPanel(\Filament\Panel $panel): bool
|
||||||
|
|||||||
169
04_Laravel/app/Services/KavenegarSmsService.php
Normal file
169
04_Laravel/app/Services/KavenegarSmsService.php
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class KavenegarSmsService
|
||||||
|
{
|
||||||
|
private string $apiKey;
|
||||||
|
private string $sender;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->apiKey = config('services.kavenegar.api_key', '');
|
||||||
|
$this->sender = config('services.kavenegar.sender', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ارسال کد تأیید به شماره موبایل
|
||||||
|
*/
|
||||||
|
public function sendVerificationCode(string $phone): array
|
||||||
|
{
|
||||||
|
$code = Str::random(5, '0123456789');
|
||||||
|
$cacheKey = "sms_verify_{$phone}";
|
||||||
|
|
||||||
|
// ذخیره کد در cache به مدت ۱۰ دقیقه
|
||||||
|
Cache::put($cacheKey, $code, now()->addMinutes(10));
|
||||||
|
|
||||||
|
$message = "کد تأیید IFNEX: {$code}";
|
||||||
|
|
||||||
|
return $this->send($phone, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* بررسی کد تأیید
|
||||||
|
*/
|
||||||
|
public function verifyCode(string $phone, string $code): bool
|
||||||
|
{
|
||||||
|
$cacheKey = "sms_verify_{$phone}";
|
||||||
|
$storedCode = Cache::get($cacheKey);
|
||||||
|
|
||||||
|
if ($storedCode && $storedCode === $code) {
|
||||||
|
Cache::forget($cacheKey);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ارسال پیامک
|
||||||
|
*/
|
||||||
|
public function send(string $receptor, string $message): array
|
||||||
|
{
|
||||||
|
if (empty($this->apiKey)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'API Key Kavenegar تنظیم نشده است',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Http::timeout(10)
|
||||||
|
->post("https://api.kavenegar.com/v1/{$this->apiKey}/sms/send.json", [
|
||||||
|
'receptor' => $receptor,
|
||||||
|
'sender' => $this->sender,
|
||||||
|
'message' => $message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
|
||||||
|
if ($response->successful() && isset($data['return']['status']) && $data['return']['status'] == 200) {
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'پیامک با موفقیت ارسال شد',
|
||||||
|
'cost' => $data['entries'][0]['cost'] ?? 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $data['return']['message'] ?? 'خطا در ارسال پیامک',
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'خطا در اتصال به سرور Kavenegar: ' . $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ارسال پیامک گروهی
|
||||||
|
*/
|
||||||
|
public function sendBulk(array $receptors, string $message): array
|
||||||
|
{
|
||||||
|
if (empty($this->apiKey)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'API Key Kavenegar تنظیم نشده است',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Http::timeout(30)
|
||||||
|
->post("https://api.kavenegar.com/v1/{$this->apiKey}/sms/send.json", [
|
||||||
|
'receptor' => $receptors,
|
||||||
|
'sender' => $this->sender,
|
||||||
|
'message' => $message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
|
||||||
|
if ($response->successful() && isset($data['return']['status']) && $data['return']['status'] == 200) {
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'پیامک با موفقیت ارسال شد',
|
||||||
|
'cost' => collect($data['entries'])->sum('cost'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $data['return']['message'] ?? 'خطا در ارسال پیامک',
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'خطا در اتصال به سرور Kavenegar: ' . $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* بررسی وضعیت پیامک ارسال شده
|
||||||
|
*/
|
||||||
|
public function checkStatus(string $messageId): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$response = Http::timeout(10)
|
||||||
|
->get("https://api.kavenegar.com/v1/{$this->apiKey}/select.json", [
|
||||||
|
'messageid' => $messageId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$data = $response->json();
|
||||||
|
|
||||||
|
if ($response->successful() && isset($data['return']['status']) && $data['return']['status'] == 200) {
|
||||||
|
$entry = $data['entries'][0] ?? null;
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'status' => $entry['status'] ?? 'unknown',
|
||||||
|
'status_text' => $entry['statustext'] ?? 'نامشخص',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => $data['return']['message'] ?? 'خطا در بررسی وضعیت',
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'خطا در اتصال به سرور Kavenegar: ' . $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -87,6 +87,194 @@ class PdfService
|
|||||||
return $this->generatePdf($html, 'A5', 'landscape');
|
return $this->generatePdf($html, 'A5', 'landscape');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* تولید PDF فاکتور فروش خدمات (واردات) — مطابق Sheet ENG Invoice
|
||||||
|
*/
|
||||||
|
public function importInvoice(Shipment $shipment, array $options = []): string
|
||||||
|
{
|
||||||
|
$shipment->loadMissing(['fromCountry', 'toCountry', 'items']);
|
||||||
|
|
||||||
|
// آدرس گیرنده
|
||||||
|
$receiver = [
|
||||||
|
'name' => $shipment->receiver_name,
|
||||||
|
'company' => $shipment->receiver_company,
|
||||||
|
'phone' => $shipment->receiver_phone,
|
||||||
|
'email' => $shipment->receiver_email,
|
||||||
|
'address' => $shipment->receiver_address,
|
||||||
|
'city' => $shipment->receiver_city,
|
||||||
|
'country' => $shipment->toCountry?->name ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
// شماره فاکتور (از شماره AWB مشتق میشود)
|
||||||
|
$invoice_no = $options['invoice_no'] ?? 'PEX' . date('ymd') . substr($shipment->awb_no, -4);
|
||||||
|
|
||||||
|
// تاریخ فاکتور
|
||||||
|
$invoice_date = $options['invoice_date'] ?? now()->format('l, F j, Y');
|
||||||
|
|
||||||
|
// دفتر گمرکی
|
||||||
|
$customs_office = $options['customs_office'] ?? 'Imam Khomeini Airport';
|
||||||
|
|
||||||
|
// نرخ ارز
|
||||||
|
$exchange_rate = $options['exchange_rate'] ?? 500000; // پیشفرض
|
||||||
|
|
||||||
|
// اقلام خدماتی (۱۰ ردیف ثابت مطابق Sheet ENG Invoice)
|
||||||
|
$service_items = $this->buildServiceItems($shipment, $options);
|
||||||
|
|
||||||
|
// جمعبندی
|
||||||
|
$freight_items = array_slice($service_items, 0, 5); // آیتمهای حمل
|
||||||
|
$clearance_items = array_slice($service_items, 5); // آیتمهای ترخیص
|
||||||
|
|
||||||
|
$freight_subtotal_currency = array_sum(array_column($freight_items, 'total_currency'));
|
||||||
|
$freight_subtotal_rial = array_sum(array_column($freight_items, 'total_rial'));
|
||||||
|
$clearance_subtotal_currency = array_sum(array_column($clearance_items, 'total_currency'));
|
||||||
|
$clearance_subtotal_rial = array_sum(array_column($clearance_items, 'total_rial'));
|
||||||
|
$total_currency = $freight_subtotal_currency + $clearance_subtotal_currency;
|
||||||
|
$total_rial = $freight_subtotal_rial + $clearance_subtotal_rial;
|
||||||
|
|
||||||
|
// شرایط و ضوابط
|
||||||
|
$terms_and_conditions = $options['terms_and_conditions'] ??
|
||||||
|
'This proforma invoice is issued based on the shipment weight declared by the customer. Final weight and dimensions will be determined after re-weighing at the destination customs.';
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'shipment' => $shipment,
|
||||||
|
'receiver' => $receiver,
|
||||||
|
'invoice_no' => $invoice_no,
|
||||||
|
'invoice_date' => $invoice_date,
|
||||||
|
'customs_office' => $customs_office,
|
||||||
|
'exchange_rate' => $exchange_rate,
|
||||||
|
'service_items' => $service_items,
|
||||||
|
'freight_subtotal_currency' => $freight_subtotal_currency,
|
||||||
|
'freight_subtotal_rial' => $freight_subtotal_rial,
|
||||||
|
'clearance_subtotal_currency' => $clearance_subtotal_currency,
|
||||||
|
'clearance_subtotal_rial' => $clearance_subtotal_rial,
|
||||||
|
'total_currency' => $total_currency,
|
||||||
|
'total_rial' => $total_rial,
|
||||||
|
'terms_and_conditions' => $terms_and_conditions,
|
||||||
|
];
|
||||||
|
|
||||||
|
$html = view('pdfs.import_invoice', $data)->render();
|
||||||
|
|
||||||
|
return $this->generatePdf($html, 'A4', 'portrait');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ساخت اقلام خدماتی فاکتور واردات
|
||||||
|
*/
|
||||||
|
private function buildServiceItems(Shipment $shipment, array $options): array
|
||||||
|
{
|
||||||
|
// فیلدهای پیشفرض از گزینهها یا مقادیر پیشفرض
|
||||||
|
$fields = [
|
||||||
|
'international_freight' => $options['international_freight'] ?? ($shipment->shipping_price ?? 0),
|
||||||
|
'pick_up_fee' => $options['pick_up_fee'] ?? ($shipment->domestic_pickup ?? 0),
|
||||||
|
'brand_fee' => $options['brand_fee'] ?? 0,
|
||||||
|
'report_fee' => $options['report_fee'] ?? 0,
|
||||||
|
'other_charges' => $options['other_charges'] ?? ($shipment->extra_service ?? 0),
|
||||||
|
'customs_clearance' => $options['customs_clearance'] ?? 0,
|
||||||
|
'domestic_transport' => $options['domestic_transport'] ?? ($shipment->domestic_delivery ?? 0),
|
||||||
|
'warehousing_fee' => $options['warehousing_fee'] ?? ($shipment->warehousing_cost ?? 0),
|
||||||
|
'order_registration_fee' => $options['order_registration_fee'] ?? 0,
|
||||||
|
'other_clearance_charges' => $options['other_clearance_charges'] ?? 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$exchange_rate = $options['exchange_rate'] ?? 500000;
|
||||||
|
$weight = $shipment->chargeable_weight ?? $shipment->weight;
|
||||||
|
|
||||||
|
return [
|
||||||
|
// آیتمهای حمل (Freight)
|
||||||
|
[
|
||||||
|
'description' => 'International Freight',
|
||||||
|
'unit' => 'kg',
|
||||||
|
'quantity' => $weight,
|
||||||
|
'unit_price' => $fields['international_freight'] > 0 ? $fields['international_freight'] / $weight : 0,
|
||||||
|
'total_currency' => $fields['international_freight'],
|
||||||
|
'total_rial' => $fields['international_freight'] * $exchange_rate,
|
||||||
|
'notes' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'description' => 'Pick Up Fee',
|
||||||
|
'unit' => '',
|
||||||
|
'quantity' => 0,
|
||||||
|
'unit_price' => 0,
|
||||||
|
'total_currency' => $fields['pick_up_fee'],
|
||||||
|
'total_rial' => $fields['pick_up_fee'] * $exchange_rate,
|
||||||
|
'notes' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'description' => 'Brand Fee',
|
||||||
|
'unit' => '',
|
||||||
|
'quantity' => 0,
|
||||||
|
'unit_price' => 0,
|
||||||
|
'total_currency' => $fields['brand_fee'],
|
||||||
|
'total_rial' => $fields['brand_fee'] * $exchange_rate,
|
||||||
|
'notes' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'description' => 'Report Fee',
|
||||||
|
'unit' => '',
|
||||||
|
'quantity' => 0,
|
||||||
|
'unit_price' => 0,
|
||||||
|
'total_currency' => $fields['report_fee'],
|
||||||
|
'total_rial' => $fields['report_fee'] * $exchange_rate,
|
||||||
|
'notes' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'description' => 'Other Charges',
|
||||||
|
'unit' => '',
|
||||||
|
'quantity' => 0,
|
||||||
|
'unit_price' => 0,
|
||||||
|
'total_currency' => $fields['other_charges'],
|
||||||
|
'total_rial' => $fields['other_charges'] * $exchange_rate,
|
||||||
|
'notes' => '',
|
||||||
|
],
|
||||||
|
// آیتمهای ترخیص (Clearance)
|
||||||
|
[
|
||||||
|
'description' => 'Customs Clearance',
|
||||||
|
'unit' => 'service',
|
||||||
|
'quantity' => $weight,
|
||||||
|
'unit_price' => $fields['customs_clearance'] > 0 ? $fields['customs_clearance'] / $weight : 0,
|
||||||
|
'total_currency' => $fields['customs_clearance'],
|
||||||
|
'total_rial' => $fields['customs_clearance'] * $exchange_rate,
|
||||||
|
'notes' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'description' => 'Domestic Transport',
|
||||||
|
'unit' => '',
|
||||||
|
'quantity' => 0,
|
||||||
|
'unit_price' => 0,
|
||||||
|
'total_currency' => $fields['domestic_transport'],
|
||||||
|
'total_rial' => $fields['domestic_transport'] * $exchange_rate,
|
||||||
|
'notes' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'description' => 'Warehousing Fee',
|
||||||
|
'unit' => '',
|
||||||
|
'quantity' => 0,
|
||||||
|
'unit_price' => 0,
|
||||||
|
'total_currency' => $fields['warehousing_fee'],
|
||||||
|
'total_rial' => $fields['warehousing_fee'] * $exchange_rate,
|
||||||
|
'notes' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'description' => 'Order Registration Fee',
|
||||||
|
'unit' => '',
|
||||||
|
'quantity' => 0,
|
||||||
|
'unit_price' => 0,
|
||||||
|
'total_currency' => $fields['order_registration_fee'],
|
||||||
|
'total_rial' => $fields['order_registration_fee'] * $exchange_rate,
|
||||||
|
'notes' => '',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'description' => 'Other Clearance Charges',
|
||||||
|
'unit' => '',
|
||||||
|
'quantity' => 0,
|
||||||
|
'unit_price' => 0,
|
||||||
|
'total_currency' => $fields['other_clearance_charges'],
|
||||||
|
'total_rial' => $fields['other_clearance_charges'] * $exchange_rate,
|
||||||
|
'notes' => '',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* تولید بارکد از AWB number — base64 embed (مطمئنترین روش)
|
* تولید بارکد از AWB number — base64 embed (مطمئنترین روش)
|
||||||
*/
|
*/
|
||||||
|
|||||||
37
04_Laravel/app/Traits/Auditable.php
Normal file
37
04_Laravel/app/Traits/Auditable.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
use App\Models\AuditLog;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
trait Auditable
|
||||||
|
{
|
||||||
|
protected static function bootAuditable(): void
|
||||||
|
{
|
||||||
|
static::created(function (Model $model) {
|
||||||
|
AuditLog::log('created', $model, [], $model->getAttributes(), "ایجاد رکورد جدید");
|
||||||
|
});
|
||||||
|
|
||||||
|
static::updated(function (Model $model) {
|
||||||
|
$changes = $model->getChanges();
|
||||||
|
$original = $model->getOriginal();
|
||||||
|
|
||||||
|
// حذف فیلدهای زمانی از تغییرات
|
||||||
|
unset($changes['updated_at'], $original['updated_at']);
|
||||||
|
|
||||||
|
if (!empty($changes)) {
|
||||||
|
AuditLog::log('updated', $model, $original, $changes, "بهروزرسانی رکورد");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
static::deleted(function (Model $model) {
|
||||||
|
AuditLog::log('deleted', $model, $model->getAttributes(), [], "حذف رکورد");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAuditableChanges(): array
|
||||||
|
{
|
||||||
|
return $this->getChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,4 +35,9 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'kavenegar' => [
|
||||||
|
'api_key' => env('KAVENEGAR_API_KEY', ''),
|
||||||
|
'sender' => env('KAVENEGAR_SENDER', '10008566'),
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// اضافه کردن pending_approval و approved به enum
|
||||||
|
DB::statement("
|
||||||
|
ALTER TABLE `shipments`
|
||||||
|
MODIFY COLUMN `status` ENUM(
|
||||||
|
'pending_approval',
|
||||||
|
'approved',
|
||||||
|
'pending_payment',
|
||||||
|
'cancelled',
|
||||||
|
'processed',
|
||||||
|
'picked_up',
|
||||||
|
'in_transit',
|
||||||
|
'out_for_delivery',
|
||||||
|
'delivered',
|
||||||
|
'failed',
|
||||||
|
'returned',
|
||||||
|
'archived'
|
||||||
|
) DEFAULT 'pending_approval'
|
||||||
|
");
|
||||||
|
|
||||||
|
// تبدیل سفارشات pending_payment موجود به approved (برای سازگاری)
|
||||||
|
// سفارشات قدیمی که قبلاً pending_payment بودند و هنوز پرداخت نشدهاند
|
||||||
|
// را به approved تغییر میدهیم تا مشتری بتواند پرداخت کند
|
||||||
|
DB::table('shipments')
|
||||||
|
->where('status', 'pending_payment')
|
||||||
|
->update(['status' => 'approved']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// تبدیل approved به pending_payment قبل از حذف
|
||||||
|
DB::table('shipments')
|
||||||
|
->whereIn('status', ['pending_approval', 'approved'])
|
||||||
|
->update(['status' => 'pending_payment']);
|
||||||
|
|
||||||
|
DB::statement("
|
||||||
|
ALTER TABLE `shipments`
|
||||||
|
MODIFY COLUMN `status` ENUM(
|
||||||
|
'pending_payment',
|
||||||
|
'cancelled',
|
||||||
|
'processed',
|
||||||
|
'picked_up',
|
||||||
|
'in_transit',
|
||||||
|
'out_for_delivery',
|
||||||
|
'delivered',
|
||||||
|
'failed',
|
||||||
|
'returned',
|
||||||
|
'archived'
|
||||||
|
) DEFAULT 'pending_payment'
|
||||||
|
");
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('commitment_forms', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('title');
|
||||||
|
$table->text('description')->nullable();
|
||||||
|
$table->string('file_path');
|
||||||
|
$table->string('file_type'); // pdf, png, jpg
|
||||||
|
$table->unsignedBigInteger('file_size');
|
||||||
|
$table->enum('direction', ['export', 'import', 'both'])->default('both');
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->integer('sort_order')->default(0);
|
||||||
|
$table->unsignedBigInteger('uploaded_by')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
|
||||||
|
$table->foreign('uploaded_by')->references('id')->on('users')->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('commitment_forms');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->decimal('credit_limit', 15, 2, true)->default(0)->after('phone');
|
||||||
|
$table->decimal('credit_used', 15, 2, true)->default(0)->after('credit_limit');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['credit_limit', 'credit_used']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('shipment_checklists', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('shipment_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade'); // کارمند
|
||||||
|
$table->string('status'); // وضعیت فعلی سفارش
|
||||||
|
$table->boolean('is_completed')->default(false); // آیا چکلیست تکمیل شده
|
||||||
|
$table->text('notes')->nullable(); // یادداشتهای کارمند
|
||||||
|
$table->timestamp('completed_at')->nullable(); // زمان تکمیل
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('shipment_checklists');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('audit_logs', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null'); // کاربری که تغییر داده
|
||||||
|
$table->string('event'); // نوع رویداد (create, update, delete, etc.)
|
||||||
|
$table->string('auditable_type'); // مدل مورد نظر
|
||||||
|
$table->unsignedBigInteger('auditable_id'); // شناسه رکورد
|
||||||
|
$table->json('old_values'); // مقادیر قبلی
|
||||||
|
$table->json('new_values'); // مقادیر جدید
|
||||||
|
$table->text('description')->nullable(); // توضیحات
|
||||||
|
$table->string('ip_address', 45)->nullable(); // آدرس IP
|
||||||
|
$table->string('user_agent')->nullable(); // مرورگر کاربر
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['auditable_type', 'auditable_id']);
|
||||||
|
$table->index('event');
|
||||||
|
$table->index('created_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('audit_logs');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('shipments', function (Blueprint $table) {
|
||||||
|
$table->decimal('brand_fee', 15, 2, true)->default(0)->after('cod_amount');
|
||||||
|
$table->decimal('report_fee', 15, 2, true)->default(0)->after('brand_fee');
|
||||||
|
$table->decimal('customs_clearance_cost', 15, 2, true)->default(0)->after('report_fee');
|
||||||
|
$table->decimal('order_registration_fee', 15, 2, true)->default(0)->after('customs_clearance_cost');
|
||||||
|
$table->decimal('other_clearance_charges', 15, 2, true)->default(0)->after('order_registration_fee');
|
||||||
|
$table->decimal('exchange_rate', 10, 4, true)->default(0)->after('other_clearance_charges');
|
||||||
|
$table->string('goods_nature')->nullable()->after('exchange_rate');
|
||||||
|
$table->string('invoice_currency', 10)->default('USD')->after('goods_nature');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('shipments', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'brand_fee',
|
||||||
|
'report_fee',
|
||||||
|
'customs_clearance_cost',
|
||||||
|
'order_registration_fee',
|
||||||
|
'other_clearance_charges',
|
||||||
|
'exchange_rate',
|
||||||
|
'goods_nature',
|
||||||
|
'invoice_currency',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
26
04_Laravel/read_excel.php
Normal file
26
04_Laravel/read_excel.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
<x-filament-panels::page>
|
||||||
|
<form wire:submit="submit">
|
||||||
|
{{ $this->form }}
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<x-filament::button type="submit" color="primary">
|
||||||
|
ایمپورت وضعیت ترکینگ
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<h3 class="text-lg font-semibold mb-4">راهنما</h3>
|
||||||
|
<div class="bg-gray-50 p-4 rounded-lg text-sm">
|
||||||
|
<p class="mb-2"><strong>فرمت فایل CSV:</strong></p>
|
||||||
|
<ul class="list-disc list-inside space-y-1 mb-4">
|
||||||
|
<li>خط اول: هدر (.awb_no,status,description)</li>
|
||||||
|
<li>هر سطر بعدی: یک سفارش</li>
|
||||||
|
<li><strong>awb_no:</strong> شماره سفارش (الزامی)</li>
|
||||||
|
<li><strong>status:</strong> وضعیت جدید (اختیاری - اگر خالی باشد از وضعیت پیشفرض استفاده میشود)</li>
|
||||||
|
<li><strong>description:</strong> توضیحات (اختیاری)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="mb-2"><strong>وضعیتهای مجاز:</strong></p>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||||
|
<span class="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-xs">pending_approval</span>
|
||||||
|
<span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs">approved</span>
|
||||||
|
<span class="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-xs">pending_payment</span>
|
||||||
|
<span class="bg-green-100 text-green-800 px-2 py-1 rounded text-xs">paid</span>
|
||||||
|
<span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs">processed</span>
|
||||||
|
<span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs">picked_up</span>
|
||||||
|
<span class="bg-purple-100 text-purple-800 px-2 py-1 rounded text-xs">in_transit</span>
|
||||||
|
<span class="bg-green-100 text-green-800 px-2 py-1 rounded text-xs">out_for_delivery</span>
|
||||||
|
<span class="bg-green-100 text-green-800 px-2 py-1 rounded text-xs">delivered</span>
|
||||||
|
<span class="bg-red-100 text-red-800 px-2 py-1 rounded text-xs">cancelled</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-4"><strong>نمونه فایل:</strong></p>
|
||||||
|
<pre class="bg-white p-2 rounded mt-1 text-xs">.awb_no,status,description
|
||||||
|
123456789,in_transit,ارسال شد از تهران
|
||||||
|
987654321,delivered,تحویل داده شد</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament-panels::page>
|
||||||
353
04_Laravel/resources/views/pdfs/import_invoice.blade.php
Normal file
353
04_Laravel/resources/views/pdfs/import_invoice.blade.php
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: DejaVu Sans, Arial, sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #000;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.page {
|
||||||
|
width: 210mm;
|
||||||
|
min-height: 297mm;
|
||||||
|
padding: 15mm;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── HEADER ─── */
|
||||||
|
.header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.invoice-to {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.invoice-to h2 {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.invoice-to p {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.invoice-to .name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── INVOICE INFO ─── */
|
||||||
|
.invoice-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.invoice-info-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.invoice-info-table td {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.invoice-info-table td:first-child {
|
||||||
|
font-weight: bold;
|
||||||
|
background: #f5f5f5;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── SHIPMENT DETAILS ─── */
|
||||||
|
.shipment-details {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.shipment-details h3 {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.details-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.detail-item {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
.detail-item .label {
|
||||||
|
font-size: 9px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.detail-item .value {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── ITEMS TABLE ─── */
|
||||||
|
.items-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.items-section h3 {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 6px 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
td.text-center { text-align: center; }
|
||||||
|
td.text-right { text-align: right; }
|
||||||
|
.total-row {
|
||||||
|
background: #f9f9f9;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.subtotal-row {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── TOTALS ─── */
|
||||||
|
.totals-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.totals-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 11px;
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
.totals-table td {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.totals-table td:first-child {
|
||||||
|
font-weight: bold;
|
||||||
|
background: #f5f5f5;
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
.totals-table .grand-total {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── TERMS & CONDITIONS ─── */
|
||||||
|
.terms-section {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.terms-section h4 {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.terms-section p {
|
||||||
|
font-size: 9px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── SIGNATURE ─── */
|
||||||
|
.signature-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.signature-box {
|
||||||
|
width: 200px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.signature-box .label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.signature-line {
|
||||||
|
border-top: 1px solid #000;
|
||||||
|
padding-top: 5px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── FOOTER ─── */
|
||||||
|
.footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 2px solid #f37021;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 9px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<!-- INVOICE TO -->
|
||||||
|
<div class="invoice-to">
|
||||||
|
<h2>INVOICE TO</h2>
|
||||||
|
<p class="name">{{ $receiver['name'] }}</p>
|
||||||
|
<p>{{ $receiver['company'] ?: '' }}</p>
|
||||||
|
<p>{{ $receiver['address'] }}</p>
|
||||||
|
<p>{{ $receiver['city'] }}, {{ $receiver['country'] }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- INVOICE INFO -->
|
||||||
|
<div class="invoice-info">
|
||||||
|
<table class="invoice-info-table">
|
||||||
|
<tr>
|
||||||
|
<td>Invoice No.</td>
|
||||||
|
<td>{{ $invoice_no }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Date</td>
|
||||||
|
<td>{{ $invoice_date }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Origin</td>
|
||||||
|
<td>{{ $shipment->fromCountry?->name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Transport</td>
|
||||||
|
<td>Air Freight</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Customs</td>
|
||||||
|
<td>{{ $customs_office ?: 'Imam Khomeini Airport' }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SHIPMENT DETAILS -->
|
||||||
|
<div class="shipment-details">
|
||||||
|
<h3>SHIPMENT DETAILS</h3>
|
||||||
|
<div class="details-grid">
|
||||||
|
<div class="detail-item">
|
||||||
|
<div class="label">Actual Weight:</div>
|
||||||
|
<div class="value">{{ number_format($shipment->weight, 2) }} KG</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<div class="label">Volume:</div>
|
||||||
|
<div class="value">{{ $shipment->dimensions ?: '—' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<div class="label">Volumetric Weight:</div>
|
||||||
|
<div class="value">{{ number_format($shipment->volumetric_weight ?? 0, 2) }} KG</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<div class="label">Chargeable Weight:</div>
|
||||||
|
<div class="value">{{ number_format($shipment->chargeable_weight ?? $shipment->weight, 2) }} KG</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<div class="label">Goods Nature:</div>
|
||||||
|
<div class="value">{{ $shipment->content_description ?: 'General Goods' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<div class="label">Exchange Rate:</div>
|
||||||
|
<div class="value">{{ number_format($exchange_rate) }} IRR</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ITEMS TABLE -->
|
||||||
|
<div class="items-section">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 40px;">No.</th>
|
||||||
|
<th>Item Description</th>
|
||||||
|
<th style="width: 70px;">Unit</th>
|
||||||
|
<th style="width: 60px;">Qty</th>
|
||||||
|
<th style="width: 100px;">Unit Price</th>
|
||||||
|
<th style="width: 120px;">Total (Currency)</th>
|
||||||
|
<th style="width: 120px;">Total (Rial)</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($service_items as $index => $item)
|
||||||
|
<tr>
|
||||||
|
<td class="text-center">{{ $index + 1 }}</td>
|
||||||
|
<td>{{ $item['description'] }}</td>
|
||||||
|
<td class="text-center">{{ $item['unit'] ?: 'service' }}</td>
|
||||||
|
<td class="text-center">{{ number_format($item['quantity'], 2) }}</td>
|
||||||
|
<td class="text-right">{{ $item['unit_price'] > 0 ? number_format($item['unit_price'], 2) : '0.00' }}</td>
|
||||||
|
<td class="text-right">{{ $item['total_currency'] > 0 ? number_format($item['total_currency'], 2) : '0.00' }}</td>
|
||||||
|
<td class="text-right">{{ $item['total_rial'] > 0 ? number_format($item['total_rial']) : '0' }}</td>
|
||||||
|
<td>{{ $item['notes'] ?: '' }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
<!-- Freight Subtotal -->
|
||||||
|
<tr class="subtotal-row">
|
||||||
|
<td colspan="5" class="text-right">Freight Subtotal</td>
|
||||||
|
<td class="text-right">{{ number_format($freight_subtotal_currency, 2) }}</td>
|
||||||
|
<td class="text-right">{{ number_format($freight_subtotal_rial) }}</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Clearance Subtotal -->
|
||||||
|
<tr class="subtotal-row">
|
||||||
|
<td colspan="5" class="text-right">Clearance Subtotal</td>
|
||||||
|
<td class="text-right">{{ number_format($clearance_subtotal_currency, 2) }}</td>
|
||||||
|
<td class="text-right">{{ number_format($clearance_subtotal_rial) }}</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Currency Rate -->
|
||||||
|
<tr class="subtotal-row">
|
||||||
|
<td colspan="5" class="text-right">Currency Rate:</td>
|
||||||
|
<td class="text-right" colspan="2">{{ number_format($exchange_rate) }}</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- TOTAL -->
|
||||||
|
<tr class="total-row">
|
||||||
|
<td colspan="5" class="text-right">TOTAL</td>
|
||||||
|
<td class="text-right">{{ number_format($total_currency, 2) }}</td>
|
||||||
|
<td class="text-right">{{ number_format($total_rial) }}</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TERMS & CONDITIONS -->
|
||||||
|
<div class="terms-section">
|
||||||
|
<h4>TERMS & CONDITIONS</h4>
|
||||||
|
<p>{{ $terms_and_conditions }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SIGNATURE -->
|
||||||
|
<div class="signature-section">
|
||||||
|
<div class="signature-box">
|
||||||
|
<div class="label">Authorised Signature</div>
|
||||||
|
<div class="signature-line">IFNEX Logistics</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FOOTER -->
|
||||||
|
<div class="footer">
|
||||||
|
IFNEX Logistics — We Deliver Value | Invoice: {{ $invoice_no }} | Generated: {{ now()->format('Y-m-d H:i') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,10 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\CustomerFinancialController;
|
||||||
|
use App\Http\Controllers\Api\CommitmentFormController;
|
||||||
|
use App\Http\Controllers\Api\MobileVerificationController;
|
||||||
use App\Http\Controllers\Api\Customer\CustomerOrderController;
|
use App\Http\Controllers\Api\Customer\CustomerOrderController;
|
||||||
use App\Http\Controllers\Api\DiscountCodeController;
|
use App\Http\Controllers\Api\DiscountCodeController;
|
||||||
use App\Http\Controllers\Api\MockGatewayController;
|
use App\Http\Controllers\Api\MockGatewayController;
|
||||||
use App\Http\Controllers\Api\PaymentController;
|
use App\Http\Controllers\Api\PaymentController;
|
||||||
use App\Http\Controllers\Api\PricingController;
|
use App\Http\Controllers\Api\PricingController;
|
||||||
|
use App\Http\Controllers\Api\StaffOrderController;
|
||||||
use App\Http\Controllers\Api\TrackController;
|
use App\Http\Controllers\Api\TrackController;
|
||||||
use App\Http\Controllers\Api\WalletController;
|
use App\Http\Controllers\Api\WalletController;
|
||||||
use App\Http\Middleware\ApiKeyMiddleware;
|
use App\Http\Middleware\ApiKeyMiddleware;
|
||||||
@ -75,6 +79,20 @@ Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
|
|||||||
Route::post('/orders/{shipment}/pay-wallet', [CustomerOrderController::class, 'payFromWallet']);
|
Route::post('/orders/{shipment}/pay-wallet', [CustomerOrderController::class, 'payFromWallet']);
|
||||||
Route::post('/orders/{shipment}/pay-gateway', [CustomerOrderController::class, 'payViaGateway']);
|
Route::post('/orders/{shipment}/pay-gateway', [CustomerOrderController::class, 'payViaGateway']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Staff Orders API (تأیید سفارشات) ───
|
||||||
|
Route::prefix('staff')->group(function () {
|
||||||
|
// لیست سفارشات در انتظار تأیید
|
||||||
|
Route::get('/orders/pending-approval', [StaffOrderController::class, 'pendingApproval']);
|
||||||
|
|
||||||
|
// تأیید یا رد سفارش
|
||||||
|
Route::post('/orders/{shipment}/approve', [StaffOrderController::class, 'approve']);
|
||||||
|
Route::post('/orders/{shipment}/reject', [StaffOrderController::class, 'reject']);
|
||||||
|
|
||||||
|
// ─── Customer Financial API (وضعیت مالی مشتری) ───
|
||||||
|
Route::get('/customers/search', [CustomerFinancialController::class, 'search']);
|
||||||
|
Route::get('/customers/{customer}/financial-status', [CustomerFinancialController::class, 'financialStatus']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
@ -95,4 +113,15 @@ Route::any('/v1/payment/callback', [PaymentController::class, 'callback'])
|
|||||||
// ══════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════
|
||||||
Route::post('/v1/calculate', [PricingController::class, 'calculate']);
|
Route::post('/v1/calculate', [PricingController::class, 'calculate']);
|
||||||
Route::get('/v1/discount-codes', [DiscountCodeController::class, 'index']);
|
Route::get('/v1/discount-codes', [DiscountCodeController::class, 'index']);
|
||||||
Route::post('/v1/discount-codes/validate', [DiscountCodeController::class, 'validate']);
|
Route::post('/v1/discount-codes/validate', [DiscountCodeController::class, 'validate']);
|
||||||
|
|
||||||
|
// API فایلهای تعهدنامه (عمومی)
|
||||||
|
Route::get('/v1/commitment-forms', [CommitmentFormController::class, 'index']);
|
||||||
|
Route::get('/v1/commitment-forms/{direction}', [CommitmentFormController::class, 'byDirection']);
|
||||||
|
|
||||||
|
// API تأیید موبایل (عمومی)
|
||||||
|
Route::post('/v1/verify/send-code', [MobileVerificationController::class, 'sendCode']);
|
||||||
|
Route::post('/v1/verify/check-code', [MobileVerificationController::class, 'checkCode']);
|
||||||
|
|
||||||
|
// API ارسال پیامک (نیاز به auth دارد)
|
||||||
|
Route::middleware(['auth:sanctum'])->post('/v1/verify/send-sms', [MobileVerificationController::class, 'sendSms']);
|
||||||
@ -19,6 +19,7 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::get('/shipments/{shipment}/pdf/awb', [ShipmentPdfController::class, 'awb'])->name('shipments.pdf.awb');
|
Route::get('/shipments/{shipment}/pdf/awb', [ShipmentPdfController::class, 'awb'])->name('shipments.pdf.awb');
|
||||||
Route::get('/shipments/{shipment}/pdf/invoice', [ShipmentPdfController::class, 'invoice'])->name('shipments.pdf.invoice');
|
Route::get('/shipments/{shipment}/pdf/invoice', [ShipmentPdfController::class, 'invoice'])->name('shipments.pdf.invoice');
|
||||||
Route::get('/shipments/{shipment}/pdf/label', [ShipmentPdfController::class, 'label'])->name('shipments.pdf.label');
|
Route::get('/shipments/{shipment}/pdf/label', [ShipmentPdfController::class, 'label'])->name('shipments.pdf.label');
|
||||||
|
Route::get('/shipments/{shipment}/pdf/import-invoice', [ShipmentPdfController::class, 'importInvoice'])->name('shipments.pdf.import-invoice');
|
||||||
});
|
});
|
||||||
|
|
||||||
// صفحات نتیجه پرداخت
|
// صفحات نتیجه پرداخت
|
||||||
|
|||||||
259
AGENT.md
Normal file
259
AGENT.md
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
# IFNEX Logistics Management System — Agent Guide
|
||||||
|
|
||||||
|
> **هدف:** راهنمای جامع برای دستیار هوش مصنوعی (OpenCode + DeepSeek Flash) جهت درک سریع پروژه، معماری، قراردادها و نکات کلیدی.
|
||||||
|
>
|
||||||
|
> **نسخه:** 1.0
|
||||||
|
> **تاریخ:** 2026-09-02
|
||||||
|
> **مدل پیشنهادی:** DeepSeek Flash (تنظیم شده با API)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 خلاصه پروژه
|
||||||
|
|
||||||
|
سیستم مدیریت لجستیک بینالمللی با معماری Headless (Laravel 11 بهعنوان بکاند، WordPress بهعنوان فرانتاند، Filament 3.3 بهعنوان پنل مدیریت). جایگزین فرآیندهای دستی مبتنی بر اکسل شده و از Multi-Package، فاکتور گمرکی (Invoice)، کیف پول دیجیتال، درگاه پرداخت Zarinpal، تولید PDF با بارکد، و سیستم رهگیری پیشرفته پشتیبانی میکند.
|
||||||
|
|
||||||
|
**وضعیت فعلی:** فازهای ۰، ۱، ۲، ۳ و ۳.۵ (۹۰٪) تکمیل شدهاند. تنها مورد باقیمانده از فاز ۳.۵، پشتیبانی کامل چندزبانه (i18n) است که به زمان دیپلوی موکول شده است.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧰 تکنولوژیها و نسخهها
|
||||||
|
|
||||||
|
| لایه | تکنولوژی | نسخه / توضیح |
|
||||||
|
|------|-----------|--------------|
|
||||||
|
| **Backend** | Laravel | 11.x |
|
||||||
|
| **PHP** | PHP | ^8.2 (۸.۳ نیز پشتیبانی میشود) |
|
||||||
|
| **Admin Panel** | Filament | 3.3.x |
|
||||||
|
| **Frontend** | WordPress | 7.0.3 + قالب سفارشی IFNEX |
|
||||||
|
| **Authentication** | Laravel Sanctum + Bridge Auth | بدون رمز عبور (API Key مشترک) |
|
||||||
|
| **Payment** | Zarinpal + Mock Gateway (تست) | کلید sandbox برای تست لوکال |
|
||||||
|
| **PDF & Barcode** | Dompdf + picqer/php-barcode-generator | تولید AWB، Invoice، Label |
|
||||||
|
| **Excel** | maatwebsite/excel | Import/Export نرخها و دادههای تاریخی |
|
||||||
|
| **Date/Time** | morilog/jalali + Carbon | تاریخ شمسی در نمایش، میلادی در DB |
|
||||||
|
| **Database** | MySQL | 8.0+ |
|
||||||
|
| **Queue** | Redis (ترجیح) / Database | برای پردازشهای سنگین |
|
||||||
|
| **Server** | HestiaCP + Nginx + PHP-FPM | تولید (api.ifnex.vernahost.ir) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 ساختار دایرکتوری (کلیدی)
|
||||||
|
|
||||||
|
IFNEX-Logistics/
|
||||||
|
├── 01_Documents/ # مستندات فنی (تحلیل اکسل، نقشه راه، چکلیست، ...)
|
||||||
|
│ ├── EXCEL_ANALYSIS.md # تحلیل کامل فایلهای اکسل (۳۹۵۰ رکورد)
|
||||||
|
│ ├── IFNEX_Phase0_Checklist.md # چکلیست کامل فازها
|
||||||
|
│ ├── IFNEX_Roadmap.md # نقشه راه ۵ فازی
|
||||||
|
│ ├── IFNEX_File_Map.md # نقشه ۱۰۰+ فایل پروژه
|
||||||
|
│ ├── IFNEX_I18N_Strategy.md # استراتژی چندزبانه
|
||||||
|
│ ├── IFNEX_Commercial_Model.md # مدل تجاری و پلنهای فروش
|
||||||
|
│ └── DESIGN_SYSTEM.md # رنگها، تایپوگرافی، فاصلهگذاری
|
||||||
|
│
|
||||||
|
├── 03_WordPress/ # فرانتاند وردپرس
|
||||||
|
│ ├── wp-content/themes/ifnex/ # قالب سفارشی
|
||||||
|
│ └── wp-content/plugins/ifnex-bridge/ # پلاگین ارتباط با لاراول
|
||||||
|
│
|
||||||
|
├── 04_Laravel/ # بکاند لاراول (هسته اصلی)
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── Enums/ # ShipmentStatus, ShipmentDirection, ShipmentType, PaymentGateway, TransactionStatus
|
||||||
|
│ │ ├── Filament/
|
||||||
|
│ │ │ ├── Resources/ # ۱۰+ Resource (Shipment, Country, ShippingRate, Currency, ...)
|
||||||
|
│ │ │ ├── Pages/ # Settings, PriceTest, ImportRates, FinancialReport
|
||||||
|
│ │ │ └── Widgets/ # داشبورد
|
||||||
|
│ │ ├── Http/
|
||||||
|
│ │ │ ├── Controllers/Api/ # Track, Pricing, Auth, Bridge, Customer, Wallet, Payment, MockGateway
|
||||||
|
│ │ │ └── Middleware/ # ApiKeyMiddleware
|
||||||
|
│ │ ├── Models/ # ۱۷ مدل (Shipment, Package, Invoice, Wallet, ...)
|
||||||
|
│ │ ├── Notifications/ # ShipmentUpdatedNotification
|
||||||
|
│ │ ├── Services/ # PriceCalculator, Pdf, Tracking, OrderPayment, Zarinpal, MockZarinpal
|
||||||
|
│ │ ├── Imports/ # OldShipmentsImport, ShippingRatesImport
|
||||||
|
│ │ └── Exports/ # ShippingRatesTemplateExport
|
||||||
|
│ ├── database/
|
||||||
|
│ │ ├── migrations/ # ۲۸+ migration
|
||||||
|
│ │ └── seeders/ # Countries, SystemSettings, DatabaseSeeder
|
||||||
|
│ ├── resources/views/
|
||||||
|
│ │ ├── pdfs/ # awb, invoice, label (با بارکد)
|
||||||
|
│ │ └── filament/ # صفحات سفارشی
|
||||||
|
│ └── routes/
|
||||||
|
│ ├── api.php # ۳۰+ endpoint
|
||||||
|
│ └── web.php # روتهای عمومی (دانلود template، Mock Gateway)
|
||||||
|
│
|
||||||
|
├── DEPLOYMENT.md # راهنمای استقرار در سرور
|
||||||
|
└── README.md # معرفی کلی پروژه
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 مفاهیم کلیدی بیزینس
|
||||||
|
|
||||||
|
### ۱. مرسوله (Shipment)
|
||||||
|
- **انواع (Type):** `DOC_NORMAL`، `DOC_ECONOMY`، `PARCEL`
|
||||||
|
- **جهت (Direction):** `export` (صادرات) و `import` (واردات)
|
||||||
|
- **وضعیت (Status):** `pending` → `confirmed` → `picked_up` → `in_transit` → `out_for_delivery` → `delivered` / `failed` / `returned` / `cancelled`
|
||||||
|
- **ویژگیها:** دارای چند بسته (`packages`)، اقلام گمرکی (`items` - فقط برای PARCEL)، تاریخچه تغییرات وضعیت (`statusHistories`)، شرکتهای حمل (`carrierMappings`)، رویدادهای رهگیری (`trackingEvents`).
|
||||||
|
|
||||||
|
### ۲. بسته (Package)
|
||||||
|
- هر مرسوله میتواند چندین بسته داشته باشد.
|
||||||
|
- وزن حجمی = `(Length × Width × Height) / 5000` (استاندارد IATA).
|
||||||
|
- وزن قابل پرداخت = `max(وزن واقعی, وزن حجمی)`.
|
||||||
|
|
||||||
|
### ۳. فاکتور گمرکی (Invoice)
|
||||||
|
- فقط برای مرسولههای نوع `PARCEL` صادر میشود.
|
||||||
|
- حداکثر ۹ قلم کالا (شرح، HS Code، تعداد، قیمت واحد، مجموع به USD).
|
||||||
|
- مجموع کل فاکتور در `shipments.invoice_total_usd` ذخیره میشود.
|
||||||
|
|
||||||
|
### ۴. کیف پول (Wallet)
|
||||||
|
- هر کاربر یک کیف پول دارد.
|
||||||
|
- تراکنشها (`WalletTransaction`) با نوع `deposit`، `payment`، `refund`، `adjustment`.
|
||||||
|
- پرداخت از کیف پول یا درگاه Zarinpal (با Mock برای تست).
|
||||||
|
|
||||||
|
### ۵. احراز هویت Bridge
|
||||||
|
- کاربران وردپرس بدون نیاز به رمز عبور، از طریق `POST /api/v1/bridge/login` با ارسال `bridge_api_key` و `wp_user_id` وارد لاراول میشوند.
|
||||||
|
- پاسخ شامل `token` Sanctum (معتبر ۳۰ روز) و اطلاعات کاربر است.
|
||||||
|
- کلید `IFNEX_BRIDGE_API_KEY` باید در هر دو طرف (`.env` لاراول و تنظیمات پلاگین وردپرس) یکسان باشد.
|
||||||
|
|
||||||
|
### ۶. رهگیری (Tracking)
|
||||||
|
- کاربر با شماره AWB جستجو میکند.
|
||||||
|
- سیستم رویدادهای رهگیری را از جدول `shipment_tracking_events` با `source` (manual, api, system) نمایش میدهد.
|
||||||
|
- در فاز ۳.۵، صفحه رهگیری با تایملاین زیبا و badge رنگی بازطراحی شده است.
|
||||||
|
|
||||||
|
### ۷. قیمتگذاری (Pricing)
|
||||||
|
- بر اساس ۴ زون مجزا (Export/Import × Parcel/Doc) و ۳ نوع سرویس.
|
||||||
|
- فرمول: قیمت پایه (AED) × ضریب سود × نرخ تبدیل به ریال + هزینههای جانبی (Packing, Domestic Pickup, ...) + VAT (۹٪).
|
||||||
|
- محمولههای بالای ۳۰ کیلوگرم مشمول نرخ ویژه (Spot Rate) هستند و محاسبه آنلاین ندارند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ دستورات روزمره (برای ایجنت)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# نصب وابستگیها
|
||||||
|
composer install
|
||||||
|
|
||||||
|
# تنظیم محیط
|
||||||
|
cp .env.example .env
|
||||||
|
php artisan key:generate
|
||||||
|
|
||||||
|
# دیتابیس (ابتدا باید ایجاد شود)
|
||||||
|
php artisan migrate --force
|
||||||
|
php artisan db:seed --force
|
||||||
|
|
||||||
|
# اجرای سرور توسعه
|
||||||
|
php artisan serve
|
||||||
|
|
||||||
|
# اجرای Queue (برای پردازشهای سنگین)
|
||||||
|
php artisan queue:work
|
||||||
|
|
||||||
|
# تستها
|
||||||
|
php artisan test
|
||||||
|
|
||||||
|
# پاکسازی کش
|
||||||
|
php artisan optimize:clear
|
||||||
|
php artisan config:cache
|
||||||
|
php artisan route:cache
|
||||||
|
php artisan view:cache
|
||||||
|
php artisan filament:clear-cached-components
|
||||||
|
|
||||||
|
# ایمپورت/اکسپورت نرخها
|
||||||
|
php artisan ifnex:import-rates
|
||||||
|
php artisan ifnex:export-rates-template
|
||||||
|
|
||||||
|
# بروزرسانی نرخ ارز (کرون)
|
||||||
|
php artisan ifnex:update-exchange-rates
|
||||||
|
|
||||||
|
# تولید API Token برای ادمین
|
||||||
|
php artisan ifnex:generate-api-token
|
||||||
|
|
||||||
|
|
||||||
|
🧪 متغیرهای محیطی کلیدی (.env)
|
||||||
|
APP_ENV=local
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=http://localhost:8000
|
||||||
|
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=ifnex_db
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
|
||||||
|
# IFNEX اختصاصی
|
||||||
|
IFNEX_API_KEY=ifnex-local-dev-key
|
||||||
|
IFNEX_BRIDGE_API_KEY=ifnex-bridge-secret-key-2026-vernasoft # باید با وردپرس یکی باشد
|
||||||
|
IFNEX_TRACKING_RATE_LIMIT=60
|
||||||
|
|
||||||
|
# CORS (فقط دامنههای مجاز وردپرس)
|
||||||
|
CORS_ALLOWED_ORIGINS=http://localhost,http://127.0.0.1
|
||||||
|
|
||||||
|
# Zarinpal (برای تست از Mock استفاده کن)
|
||||||
|
ZARINPAL_MERCHANT_ID=fake-merchant-id-for-testing # اگر fake باشد، MockGateway فعال میشود
|
||||||
|
ZARINPAL_SANDBOX=true
|
||||||
|
ZARINPAL_CALLBACK_URL=http://localhost:8000/api/v1/payment/callback
|
||||||
|
ZARINPAL_FRONTEND_SUCCESS_URL=http://localhost/IFNEX-Logistics/03_WordPress/wallet
|
||||||
|
ZARINPAL_FRONTEND_FAILURE_URL=http://localhost/IFNEX-Logistics/03_WordPress/wallet
|
||||||
|
|
||||||
|
# Wallet
|
||||||
|
WALLET_MIN_DEPOSIT=10000
|
||||||
|
WALLET_MAX_DEPOSIT=500000000
|
||||||
|
WALLET_AUTO_CREATE=true
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ خط قرمزها (ممنوعیتهای مطلق)
|
||||||
|
|
||||||
|
❌ هرگز ✅ همیشه
|
||||||
|
برگرداندن کشورها به ۲ زون ۴ زون مجزا (Export/Import × Parcel/Doc)
|
||||||
|
استفاده از ۲ نوع سرویس ۳ نوع (DOC_NORMAL, DOC_ECONOMY, PARCEL)
|
||||||
|
ذخیره تاریخ شمسی در DB ذخیره timestamp میلادی + تبدیل در نمایش
|
||||||
|
CORS * در Production CORS محدود به دامنه وردپرس
|
||||||
|
کامیت .env در Git در .gitignore باشد
|
||||||
|
APP_DEBUG=true در Production APP_DEBUG=false
|
||||||
|
PDF فارسی (AWB/Invoice/Label) همیشه انگلیسی (برای حمل بینالمللی)
|
||||||
|
کپی از DHL طراحی منحصر به فرد IFNEX
|
||||||
|
استفاده از wire:click برای دانلود استفاده از <a href> با روت مستقیم
|
||||||
|
getFormActions() در Custom Pages استفاده از wire:click در Blade
|
||||||
|
هدایت AJAX به redirect() استفاده از payment_url در response JSON
|
||||||
|
متدهای نوتیفیکیشن بیرون از کلاس داخل کلاس IFNEX_User_Bridge
|
||||||
|
📌 نکات ویژه برای ایجنت (DeepSeek Flash)
|
||||||
|
هنگام تولید کد جدید، حتماً از Enumها به جای رشتههای سختکد شده استفاده کن.
|
||||||
|
|
||||||
|
برای هر مدل جدید، migration، مدل، و در صورت نیاز کنترلر/ریسورس Filament بساز.
|
||||||
|
|
||||||
|
خطاهای رایج:
|
||||||
|
|
||||||
|
عدم وجود فیلد deleted_at در کوئریها (از SoftDeletes استفاده کن).
|
||||||
|
|
||||||
|
فراموشی fillable یا casts در مدلها.
|
||||||
|
|
||||||
|
فراموشی $with برای بارگذاری روابط در Resourceها.
|
||||||
|
|
||||||
|
استفاده از redirect() در کنترلرهای API (باید JSON برگردانند).
|
||||||
|
|
||||||
|
برای تغییر وضعیت مرسوله، حتماً تایملاین را بهروز کن (مدل ShipmentStatusHistory).
|
||||||
|
|
||||||
|
در فرم سفارش مشتری، مرحله Invoice فقط برای نوع PARCEL نمایش داده شود.
|
||||||
|
|
||||||
|
تولید PDF با بارکد بهصورت base64 embed انجام شود.
|
||||||
|
|
||||||
|
Bridge Auth نیازی به رمز عبور ندارد؛ فقط از IFNEX_BRIDGE_API_KEY استفاده میکند.
|
||||||
|
|
||||||
|
تست لوکال پرداخت: از ZARINPAL_MERCHANT_ID=fake-merchant-id-for-testing برای فعالسازی Mock Gateway استفاده کن.
|
||||||
|
|
||||||
|
مدیریت تاریخ: همیشه از Carbon استفاده کن و تاریخ را بهصورت Y-m-d H:i:s در DB ذخیره کن.
|
||||||
|
|
||||||
|
برای کوئریهای سنگین، از chunk() یا cursor() استفاده کن تا حافظه مصرف نشود.
|
||||||
|
|
||||||
|
|
||||||
|
📚 مستندات مرجع (برای مطالعه بیشتر)
|
||||||
|
فایل محتوا
|
||||||
|
01_Documents/EXCEL_ANALYSIS.md تحلیل کامل فایلهای اکسل (۳۹۵۰ رکورد، فرمولها، زونها)
|
||||||
|
01_Documents/IFNEX_Phase0_Checklist.md چکلیست کامل فازها (۰ تا ۳.۵)
|
||||||
|
01_Documents/IFNEX_Roadmap.md نقشه راه ۵ فازی
|
||||||
|
01_Documents/IFNEX_File_Map.md نقشه ۱۰۰+ فایل پروژه
|
||||||
|
01_Documents/IFNEX_I18N_Strategy.md استراتژی چندزبانه (برای زمان دیپلوی)
|
||||||
|
01_Documents/IFNEX_Commercial_Model.md مدل تجاری و پلنهای فروش
|
||||||
|
01_Documents/DESIGN_SYSTEM.md رنگها، تایپوگرافی، فاصلهگذاری
|
||||||
|
DEPLOYMENT.md راهنمای کامل استقرار روی سرور
|
||||||
|
04_Laravel/README.md راهنمای بکاند
|
||||||
|
تاریخ: 2026-09-02
|
||||||
|
نسخه: 1.0
|
||||||
|
تهیهکننده: VernaSoft Group — Kazem Alghasi
|
||||||
|
مدل هدف: DeepSeek Flash (API)
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# راهنمای استقرار IFNEX
|
# راهنمای استقرار IFNEX
|
||||||
|
|
||||||
> **آخرین بروزرسانی:** 2026-08-29
|
> **آخرین بروزرسانی:** 2026-09-03
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
| محیط | دامنه | نقش |
|
| محیط | دامنه | نقش |
|
||||||
|-------|-------|------|
|
|-------|-------|------|
|
||||||
| Production | api.ifnex.vernahost.ir | API لاراول |
|
| Production | system.ifnex.ir | API لاراول |
|
||||||
| Production | ifnex.vernahost.ir | وبسایت وردپرس |
|
| Production | ifnex.ir | وبسایت وردپرس |
|
||||||
| Local | localhost:8000 | توسعه |
|
| Local | localhost:8000 | توسعه |
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -61,7 +61,7 @@ nano .env
|
|||||||
```env
|
```env
|
||||||
APP_ENV=production
|
APP_ENV=production
|
||||||
APP_DEBUG=false
|
APP_DEBUG=false
|
||||||
APP_URL=https://api.ifnex.vernahost.ir
|
APP_URL=https://system.ifnex.ir
|
||||||
|
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
DB_DATABASE=ifnex_db
|
DB_DATABASE=ifnex_db
|
||||||
@ -71,14 +71,18 @@ DB_PASSWORD=STRONG_PASSWORD
|
|||||||
# مهم: Bridge API Key باید با وردپرس یکسان باشد
|
# مهم: Bridge API Key باید با وردپرس یکسان باشد
|
||||||
IFNEX_BRIDGE_API_KEY=ifnex-bridge-secret-key-2026-vernasoft
|
IFNEX_BRIDGE_API_KEY=ifnex-bridge-secret-key-2026-vernasoft
|
||||||
|
|
||||||
CORS_ALLOWED_ORIGINS=https://ifnex.vernahost.ir
|
CORS_ALLOWED_ORIGINS=https://ifnex.ir
|
||||||
|
|
||||||
# Payment Gateway
|
# Payment Gateway
|
||||||
ZARINPAL_SANDBOX=false
|
ZARINPAL_SANDBOX=false
|
||||||
ZARINPAL_MERCHANT_ID=YOUR_REAL_MERCHANT_ID
|
ZARINPAL_MERCHANT_ID=YOUR_REAL_MERCHANT_ID
|
||||||
ZARINPAL_CALLBACK_URL=https://api.ifnex.vernahost.ir/api/v1/payment/callback
|
ZARINPAL_CALLBACK_URL=https://system.ifnex.ir/api/v1/payment/callback
|
||||||
ZARINPAL_FRONTEND_SUCCESS_URL=https://ifnex.vernahost.ir/wallet
|
ZARINPAL_FRONTEND_SUCCESS_URL=https://ifnex.ir/wallet
|
||||||
ZARINPAL_FRONTEND_FAILURE_URL=https://ifnex.vernahost.ir/wallet
|
ZARINPAL_FRONTEND_FAILURE_URL=https://ifnex.ir/wallet
|
||||||
|
|
||||||
|
# Kavenegar SMS Service
|
||||||
|
KAVENEGAR_API_KEY=YOUR_KAVENEGAR_API_KEY
|
||||||
|
KAVENEGAR_SENDER=10008566
|
||||||
```
|
```
|
||||||
|
|
||||||
### ۱.۴ دیتابیس
|
### ۱.۴ دیتابیس
|
||||||
|
|||||||
10
README.md
10
README.md
@ -37,6 +37,16 @@
|
|||||||
- **اعلانهای دیتابیس** برای ادمین و مشتری با badge unread در سایدبار
|
- **اعلانهای دیتابیس** برای ادمین و مشتری با badge unread در سایدبار
|
||||||
- **پشتیبانی از ۱۹۲ کشور** با پیششماره تلفن استاندارد ISO 3166-1
|
- **پشتیبانی از ۱۹۲ کشور** با پیششماره تلفن استاندارد ISO 3166-1
|
||||||
- **اعتبارسنجی فرمها** شامل تشخیص خودکار زبان فارسی/انگلیسی و پیششماره خودکار
|
- **اعتبارسنجی فرمها** شامل تشخیص خودکار زبان فارسی/انگلیسی و پیششماره خودکار
|
||||||
|
- **سیستم تأیید سفارش** با فلوی `pending_approval → approved → paid → processed`
|
||||||
|
- **فرم تعهدنامه** با قابلیت آپلود فایل توسط مدیر و مشاهده توسط مشتری
|
||||||
|
- **فاکتور واردات** با طراحی مطابق Sheet ENG Invoice
|
||||||
|
- **مانده حساب** با نمایش موجودی کیف پول منهای بدهیها
|
||||||
|
- **وضعیت مالی مشتری** برای مدیر/کارمند با جزئیات بدهی به هر ارز
|
||||||
|
- **سیستم اعتباردهی (Credit)** با قابلیت افزایش/کاهش توسط مدیر کل
|
||||||
|
- **چکلیست کارمند** برای هر سفارش + تاریخچه مراحل
|
||||||
|
- **ایمپورت گروهی وضعیت ترکینگ** با فایل CSV
|
||||||
|
- **سیستم Audit Log** برای لاگ تغییرات مدیر
|
||||||
|
- **اتصال سرویس اساماس Kavenegar** برای تأیید موبایل و ارسال پیامک
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user