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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
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('✅ تأیید و ثبت سفارش');
|
||||
if (res.success) {
|
||||
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 {
|
||||
$('#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'])): ?>
|
||||
<!-- موجودی کیف پول -->
|
||||
<div class="ifnex-wallet-card">
|
||||
<h3>💰 کیف پول</h3>
|
||||
<div class="ifnex-wallet-balance">
|
||||
<span class="ifnex-balance-amount">
|
||||
<?php echo number_format($profile['wallet']['balance']); ?> ریال
|
||||
</span>
|
||||
<?php if ($profile['wallet']['is_frozen']): ?>
|
||||
<span class="ifnex-badge ifnex-badge-danger">مسدود</span>
|
||||
<?php else: ?>
|
||||
<span class="ifnex-badge ifnex-badge-success">فعال</span>
|
||||
<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 style="margin: 0 0 15px 0; color: #0369a1;">💰 وضعیت مالی</h3>
|
||||
|
||||
<!-- مانده حساب -->
|
||||
<div style="background: white; border-radius: 8px; padding: 15px; margin-bottom: 15px; border: 1px solid #e0f2fe;">
|
||||
<div style="font-size: 12px; color: #666; margin-bottom: 5px;">مانده حساب (موجودی منهای بدهیها)</div>
|
||||
<div style="font-size: 24px; font-weight: bold; color: <?php echo ($profile['account_balance'] ?? 0) >= 0 ? '#059669' : '#dc2626'; ?>;">
|
||||
<?php echo number_format($profile['account_balance'] ?? 0); ?> ریال
|
||||
</div>
|
||||
<?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; ?>
|
||||
</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>
|
||||
<?php endif; ?>
|
||||
|
||||
@ -256,7 +279,11 @@ function ifnex_user_profile_shortcode($atts) {
|
||||
<div class="ifnex-stat-label">کل سفارشات</div>
|
||||
</div>
|
||||
<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>
|
||||
<div class="ifnex-stat-card ifnex-stat-info">
|
||||
@ -405,7 +432,7 @@ function ifnex_orders_list_shortcode($atts) {
|
||||
</button>
|
||||
<?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'])); ?>"
|
||||
class="ifnex-btn ifnex-btn-sm ifnex-btn-success">
|
||||
💳 پرداخت
|
||||
@ -695,11 +722,18 @@ function ifnex_order_payment_shortcode($atts) {
|
||||
|
||||
$data = $order['data'];
|
||||
|
||||
// بررسی وضعیت
|
||||
if ($data['status']['value'] !== 'pending_payment') {
|
||||
return '<div class="ifnex-success-box">
|
||||
<h3>✅ این سفارش قبلاً پرداخت شده است</h3>
|
||||
<p>وضعیت: ' . esc_html($data['status']['label']) . '</p>
|
||||
// بررسی وضعیت (فقط سفارشات تأیید شده قابل پرداخت هستند)
|
||||
if ($data['status']['value'] !== 'approved') {
|
||||
$message = 'این سفارش هنوز تأیید نشده است و قابل پرداخت نیست.';
|
||||
if ($data['status']['value'] === 'pending_approval') {
|
||||
$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>
|
||||
</div>';
|
||||
}
|
||||
@ -938,7 +972,7 @@ function ifnex_order_detail_shortcode($atts) {
|
||||
← بازگشت به سفارشات
|
||||
</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)); ?>"
|
||||
class="ifnex-btn ifnex-btn-sm ifnex-btn-success">
|
||||
💳 پرداخت
|
||||
|
||||
@ -4,6 +4,8 @@ namespace App\Enums;
|
||||
|
||||
enum ShipmentStatus: string
|
||||
{
|
||||
case PendingApproval = 'pending_approval';
|
||||
case Approved = 'approved';
|
||||
case PendingPayment = 'pending_payment';
|
||||
case Cancelled = 'cancelled';
|
||||
case Processed = 'processed';
|
||||
@ -18,15 +20,17 @@ enum ShipmentStatus: string
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::PendingApproval => 'در انتظار تأیید',
|
||||
self::Approved => 'تأیید شده - در انتظار پرداخت',
|
||||
self::PendingPayment => 'در انتظار پرداخت',
|
||||
self::Cancelled => 'لغو شده',
|
||||
self::Processed => 'Processed',
|
||||
self::PickedUp => 'Picked Up',
|
||||
self::InTransit => 'In Transit',
|
||||
self::OutForDelivery => 'Out For Delivery',
|
||||
self::Failed => 'Failed',
|
||||
self::Delivered => 'Delivered',
|
||||
self::Returned => 'Returned',
|
||||
self::Processed => 'در حال پردازش',
|
||||
self::PickedUp => 'تحویل به حملکننده',
|
||||
self::InTransit => 'در حال حمل',
|
||||
self::OutForDelivery => 'آماده تحویل',
|
||||
self::Failed => 'تحویل ناموفق',
|
||||
self::Delivered => 'تحویل شده',
|
||||
self::Returned => 'برگشتی',
|
||||
self::Archived => 'آرشیو',
|
||||
};
|
||||
}
|
||||
@ -34,6 +38,8 @@ enum ShipmentStatus: string
|
||||
public function color(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::PendingApproval => 'warning',
|
||||
self::Approved => 'info',
|
||||
self::PendingPayment => 'warning',
|
||||
self::Cancelled => 'danger',
|
||||
self::Processed => 'gray',
|
||||
@ -49,11 +55,37 @@ enum ShipmentStatus: string
|
||||
|
||||
public function isPaid(): bool
|
||||
{
|
||||
return $this !== self::PendingPayment;
|
||||
return !in_array($this, [
|
||||
self::PendingApproval,
|
||||
self::PendingPayment,
|
||||
]);
|
||||
}
|
||||
|
||||
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'),
|
||||
])->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')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('sender_name')
|
||||
@ -406,6 +451,32 @@ class ShipmentResource extends Resource
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::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([
|
||||
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,
|
||||
'direction' => $validated['direction'],
|
||||
'type' => $validated['type'],
|
||||
'status' => ShipmentStatus::PendingPayment,
|
||||
'status' => ShipmentStatus::PendingApproval,
|
||||
|
||||
// وزن
|
||||
'weight' => $validated['weight'],
|
||||
@ -307,7 +307,7 @@ class CustomerOrderController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'سفارش شما با موفقیت ثبت شد. لطفاً برای تکمیل، پرداخت را انجام دهید.',
|
||||
'message' => 'سفارش شما با موفقیت ثبت شد. پس از تأیید کارمند، گزینه پرداخت برای شما فعال خواهد شد.',
|
||||
'data' => $this->formatShipment($shipment->load(['fromCountry', 'toCountry', 'items'])),
|
||||
'pricing' => $priceResult,
|
||||
], 201);
|
||||
@ -384,12 +384,25 @@ class CustomerOrderController extends Controller
|
||||
$user = Auth::user();
|
||||
$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 = [
|
||||
'total_orders' => Shipment::where('user_id', $user->id)->count(),
|
||||
'pending_orders' => Shipment::where('user_id', $user->id)
|
||||
->where('status', ShipmentStatus::PendingPayment)
|
||||
'pending_approval' => Shipment::where('user_id', $user->id)
|
||||
->where('status', ShipmentStatus::PendingApproval)
|
||||
->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)
|
||||
->whereIn('status', [
|
||||
ShipmentStatus::Processed,
|
||||
@ -402,7 +415,8 @@ class CustomerOrderController extends Controller
|
||||
->where('status', ShipmentStatus::Delivered)
|
||||
->count(),
|
||||
'total_spent' => Shipment::where('user_id', $user->id)
|
||||
->where('status', '!=', ShipmentStatus::PendingPayment)
|
||||
->where('status', '!=', ShipmentStatus::PendingApproval)
|
||||
->where('status', '!=', ShipmentStatus::Approved)
|
||||
->where('status', '!=', ShipmentStatus::Cancelled)
|
||||
->sum('total_fee'),
|
||||
];
|
||||
@ -420,6 +434,8 @@ class CustomerOrderController extends Controller
|
||||
'balance' => $user->wallet->balance,
|
||||
'is_frozen' => $user->wallet->is_frozen,
|
||||
] : null,
|
||||
'account_balance' => $account_balance,
|
||||
'pending_payments_total' => $pending_payments_total,
|
||||
'stats' => $stats,
|
||||
]);
|
||||
}
|
||||
@ -457,6 +473,8 @@ class CustomerOrderController extends Controller
|
||||
'color' => $shipment->status?->color(),
|
||||
'is_paid' => $shipment->status?->isPaid(),
|
||||
'can_cancel' => $shipment->status?->canBeCancelledByCustomer(),
|
||||
'can_pay' => $shipment->status?->canBePaid(),
|
||||
'is_approved' => $shipment->status?->isApproved(),
|
||||
],
|
||||
'from_country' => $shipment->fromCountry ? [
|
||||
'id' => $shipment->fromCountry->id,
|
||||
@ -565,11 +583,11 @@ class CustomerOrderController extends Controller
|
||||
], 403);
|
||||
}
|
||||
|
||||
// بررسی وضعیت
|
||||
if ($shipment->status !== ShipmentStatus::PendingPayment) {
|
||||
// بررسی وضعیت (فقط سفارشات تأیید شده قابل پرداخت هستند)
|
||||
if ($shipment->status !== ShipmentStatus::Approved) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'این سفارش در وضعیت قابل پرداخت نیست.',
|
||||
'message' => 'این سفارش هنوز تأیید نشده است و قابل پرداخت نیست.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
@ -611,11 +629,11 @@ class CustomerOrderController extends Controller
|
||||
], 403);
|
||||
}
|
||||
|
||||
// بررسی وضعیت
|
||||
if ($shipment->status !== ShipmentStatus::PendingPayment) {
|
||||
// بررسی وضعیت (فقط سفارشات تأیید شده قابل پرداخت هستند)
|
||||
if ($shipment->status !== ShipmentStatus::Approved) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'این سفارش در وضعیت قابل پرداخت نیست.',
|
||||
'message' => 'این سفارش هنوز تأیید نشده است و قابل پرداخت نیست.',
|
||||
], 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* تولید 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_rial',
|
||||
'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_name',
|
||||
'sender_company',
|
||||
@ -55,6 +65,8 @@ class Shipment extends Model
|
||||
// Customs
|
||||
'reason_for_export',
|
||||
'content_description',
|
||||
// User
|
||||
'user_id',
|
||||
];
|
||||
|
||||
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',
|
||||
'phone',
|
||||
'role', // فیلد قدیمی - بعداً حذف میکنیم
|
||||
'credit_limit', // سقف اعتبار
|
||||
'credit_used', // مبلغ استفاده شده از اعتبار
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
@ -30,6 +32,8 @@ class User extends Authenticatable
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'credit_limit' => 'decimal:2',
|
||||
'credit_used' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
|
||||
@ -62,6 +66,18 @@ class User extends Authenticatable
|
||||
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 ───────────────────────
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* تولید 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 (مطمئنترین روش)
|
||||
*/
|
||||
|
||||
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
|
||||
|
||||
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\DiscountCodeController;
|
||||
use App\Http\Controllers\Api\MockGatewayController;
|
||||
use App\Http\Controllers\Api\PaymentController;
|
||||
use App\Http\Controllers\Api\PricingController;
|
||||
use App\Http\Controllers\Api\StaffOrderController;
|
||||
use App\Http\Controllers\Api\TrackController;
|
||||
use App\Http\Controllers\Api\WalletController;
|
||||
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-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::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/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/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
|
||||
|
||||
> **آخرین بروزرسانی:** 2026-08-29
|
||||
> **آخرین بروزرسانی:** 2026-09-03
|
||||
|
||||
---
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
|
||||
| محیط | دامنه | نقش |
|
||||
|-------|-------|------|
|
||||
| Production | api.ifnex.vernahost.ir | API لاراول |
|
||||
| Production | ifnex.vernahost.ir | وبسایت وردپرس |
|
||||
| Production | system.ifnex.ir | API لاراول |
|
||||
| Production | ifnex.ir | وبسایت وردپرس |
|
||||
| Local | localhost:8000 | توسعه |
|
||||
|
||||
---
|
||||
@ -61,7 +61,7 @@ nano .env
|
||||
```env
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://api.ifnex.vernahost.ir
|
||||
APP_URL=https://system.ifnex.ir
|
||||
|
||||
DB_HOST=localhost
|
||||
DB_DATABASE=ifnex_db
|
||||
@ -71,14 +71,18 @@ DB_PASSWORD=STRONG_PASSWORD
|
||||
# مهم: Bridge API Key باید با وردپرس یکسان باشد
|
||||
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
|
||||
ZARINPAL_SANDBOX=false
|
||||
ZARINPAL_MERCHANT_ID=YOUR_REAL_MERCHANT_ID
|
||||
ZARINPAL_CALLBACK_URL=https://api.ifnex.vernahost.ir/api/v1/payment/callback
|
||||
ZARINPAL_FRONTEND_SUCCESS_URL=https://ifnex.vernahost.ir/wallet
|
||||
ZARINPAL_FRONTEND_FAILURE_URL=https://ifnex.vernahost.ir/wallet
|
||||
ZARINPAL_CALLBACK_URL=https://system.ifnex.ir/api/v1/payment/callback
|
||||
ZARINPAL_FRONTEND_SUCCESS_URL=https://ifnex.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 در سایدبار
|
||||
- **پشتیبانی از ۱۹۲ کشور** با پیششماره تلفن استاندارد ISO 3166-1
|
||||
- **اعتبارسنجی فرمها** شامل تشخیص خودکار زبان فارسی/انگلیسی و پیششماره خودکار
|
||||
- **سیستم تأیید سفارش** با فلوی `pending_approval → approved → paid → processed`
|
||||
- **فرم تعهدنامه** با قابلیت آپلود فایل توسط مدیر و مشاهده توسط مشتری
|
||||
- **فاکتور واردات** با طراحی مطابق Sheet ENG Invoice
|
||||
- **مانده حساب** با نمایش موجودی کیف پول منهای بدهیها
|
||||
- **وضعیت مالی مشتری** برای مدیر/کارمند با جزئیات بدهی به هر ارز
|
||||
- **سیستم اعتباردهی (Credit)** با قابلیت افزایش/کاهش توسط مدیر کل
|
||||
- **چکلیست کارمند** برای هر سفارش + تاریخچه مراحل
|
||||
- **ایمپورت گروهی وضعیت ترکینگ** با فایل CSV
|
||||
- **سیستم Audit Log** برای لاگ تغییرات مدیر
|
||||
- **اتصال سرویس اساماس Kavenegar** برای تأیید موبایل و ارسال پیامک
|
||||
|
||||
---
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user