diff --git a/03_WordPress/wp-content/plugins/ifnex-bridge/assets/css/ifnex-orders.css b/03_WordPress/wp-content/plugins/ifnex-bridge/assets/css/ifnex-orders.css
index 81b86fd..a344573 100644
--- a/03_WordPress/wp-content/plugins/ifnex-bridge/assets/css/ifnex-orders.css
+++ b/03_WordPress/wp-content/plugins/ifnex-bridge/assets/css/ifnex-orders.css
@@ -760,4 +760,120 @@
@media (max-width: 640px) {
.ifnx-notif-item { padding: 12px 14px; }
+}
+
+/* ═══════════════════════════════════════════
+ Order Detail - Documents & Commitment Forms
+ ═══════════════════════════════════════════ */
+
+.ifnex-docs-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+ gap: 12px;
+ margin-top: 12px;
+}
+
+.ifnex-doc-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ padding: 16px 12px;
+ background: #fff;
+ border: 1px solid var(--ifnex-border);
+ border-radius: var(--ifnex-radius);
+ text-decoration: none !important;
+ transition: all 0.2s;
+}
+
+.ifnex-doc-btn:hover {
+ border-color: var(--ifnex-primary);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
+ transform: translateY(-2px);
+}
+
+.ifnex-doc-icon {
+ font-size: 28px;
+ line-height: 1;
+}
+
+.ifnex-doc-label {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--ifnex-dark);
+}
+
+.ifnex-forms-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ margin-top: 12px;
+}
+
+.ifnex-form-item {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 14px;
+ background: #fff;
+ border: 1px solid var(--ifnex-border);
+ border-radius: var(--ifnex-radius);
+}
+
+.ifnex-form-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.ifnex-form-info strong {
+ display: block;
+ font-size: 14px;
+ color: var(--ifnex-dark);
+ margin-bottom: 4px;
+}
+
+.ifnex-form-info p {
+ margin: 0;
+ font-size: 12px;
+ color: var(--ifnex-gray);
+}
+
+.ifnex-form-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.ifnex-form-actions .ifnex-badge {
+ margin-right: 4px;
+}
+
+.ifnex-empty-state {
+ color: var(--ifnex-gray);
+ font-size: 14px;
+ margin: 8px 0;
+}
+
+.ifnex-loading {
+ font-size: 12px;
+ color: var(--ifnex-primary);
+ font-weight: 600;
+}
+
+.ifnex-text-warning {
+ color: var(--ifnex-warning);
+ font-size: 12px;
+ margin-top: 4px;
+}
+
+@media (max-width: 640px) {
+ .ifnex-form-item {
+ flex-direction: column;
+ }
+
+ .ifnex-form-actions {
+ width: 100%;
+ }
}
\ No newline at end of file
diff --git a/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php b/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php
index a4ad058..b77ef0c 100644
--- a/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php
+++ b/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php
@@ -972,6 +972,13 @@ function ifnex_order_detail_shortcode($atts) {
return '
شناسه سفارش مشخص نشده است.
';
}
+ // Handle PDF download
+ $download_type = sanitize_text_field($_GET['download'] ?? '');
+ if ($download_type && in_array($download_type, ['awb', 'invoice', 'label', 'import-invoice'])) {
+ ifnex_handle_pdf_download($order_id, $download_type);
+ return '';
+ }
+
$bridge = new IFNEX_User_Bridge();
$user_id = get_current_user_id();
@@ -1147,6 +1154,41 @@ function ifnex_order_detail_shortcode($atts) {
+
+
@@ -1217,6 +1259,43 @@ function ifnex_order_detail_shortcode($atts) {
});
});
});
+
+ function uploadCommitmentForm(orderId, formId, input) {
+ var file = input.files[0];
+ if (!file) return;
+
+ var formData = new FormData();
+ formData.append('action', 'ifnex_upload_commitment_form');
+ formData.append('order_id', orderId);
+ formData.append('form_id', formId);
+ formData.append('file', file);
+ formData.append('nonce', ifnex_ajax.nonce);
+
+ var btn = $(input).closest('.ifnex-form-actions');
+ var originalHtml = btn.html();
+ btn.html('
در حال آپلود...');
+
+ $.ajax({
+ url: ifnex_ajax.ajax_url,
+ method: 'POST',
+ data: formData,
+ processData: false,
+ contentType: false,
+ success: function(res) {
+ if (res.success) {
+ alert('فایل با موفقیت آپلود شد.');
+ location.reload();
+ } else {
+ alert('خطا: ' + (res.data || 'نامشخص'));
+ btn.html(originalHtml);
+ }
+ },
+ error: function() {
+ alert('خطا در ارتباط با سرور.');
+ btn.html(originalHtml);
+ }
+ });
+ }
get_customer_order($user_id, $order_id);
+ if (is_wp_error($order)) wp_die('خطا: ' . $order->get_error_message());
+
+ $api_url = get_option('ifnex_api_url', 'http://localhost:8000/api/v1');
+ $download_url = rtrim($api_url, '/') . "/customer/orders/{$order_id}/pdf/{$download_type}";
+
+ $token = get_user_meta($user_id, 'ifnex_api_token', true);
+ if (!$token) wp_die('توکن احراز هویت یافت نشد.');
+
+ $response = wp_remote_get($download_url, array(
+ 'headers' => array(
+ 'Authorization' => 'Bearer ' . $token,
+ 'Accept' => 'application/pdf',
+ ),
+ 'timeout' => 30,
+ ));
+
+ if (is_wp_error($response)) wp_die('خطا در ارتباط با سرور.');
+
+ $status_code = wp_remote_retrieve_response_code($response);
+ if ($status_code !== 200) wp_die('خطا در دانلود فایل.');
+
+ $file_content = wp_remote_retrieve_body($response);
+ $filename = strtoupper($download_type) . '-' . $order['data']['awb_no'] . '.pdf';
+
+ nocache_headers();
+ header('Content-Type: application/pdf');
+ header('Content-Disposition: attachment; filename="' . $filename . '"');
+ header('Content-Length: ' . strlen($file_content));
+ echo $file_content;
+ exit;
+}
+
function ifnex_icon($name, $size = 20) {
$icons = [
'dashboard' => '
',
diff --git a/03_WordPress/wp-content/plugins/ifnex-bridge/includes/user-bridge.php b/03_WordPress/wp-content/plugins/ifnex-bridge/includes/user-bridge.php
index 57c94dd..be7c0a6 100644
--- a/03_WordPress/wp-content/plugins/ifnex-bridge/includes/user-bridge.php
+++ b/03_WordPress/wp-content/plugins/ifnex-bridge/includes/user-bridge.php
@@ -659,4 +659,183 @@ function ifnex_enqueue_global_assets() {
'orders_url' => home_url('/my-account/?tab=orders'),
'home_url' => home_url('/'),
)) . ';', 'before');
+}
+
+// ══════════════════════════════════════════════════════════════
+// AJAX Handler برای دانلود مدارک PDF سفارش
+// ══════════════════════════════════════════════════════════════
+
+add_action('wp_ajax_ifnex_download_order_pdf', 'ifnex_download_order_pdf_ajax');
+
+function ifnex_download_order_pdf_ajax() {
+ check_ajax_referer('ifnex_ajax_nonce', 'nonce');
+ if (!is_user_logged_in()) {
+ wp_send_json_error('باید وارد شوید.');
+ }
+
+ $order_id = intval($_POST['order_id'] ?? 0);
+ $download_type = sanitize_text_field($_POST['download_type'] ?? '');
+
+ if (!$order_id) wp_send_json_error('شناسه سفارش نامعتبر است.');
+ if (!in_array($download_type, ['awb', 'invoice', 'label', 'import-invoice'])) wp_send_json_error('نوع فایل نامعتبر است.');
+
+ $bridge = new IFNEX_User_Bridge();
+ $user_id = get_current_user_id();
+
+ // بررسی مالکیت سفارش
+ $order = $bridge->get_customer_order($user_id, $order_id);
+ if (is_wp_error($order)) wp_send_json_error($order->get_error_message());
+
+ // ساخت URL دانلود از API لاراول
+ $api_url = get_option('ifnex_api_url', 'http://localhost:8000/api/v1');
+ $download_url = rtrim($api_url, '/') . "/customer/orders/{$order_id}/pdf/{$download_type}";
+
+ // ارسال درخواست به لاراول با توکن احراز هویت
+ $token = get_user_meta($user_id, 'ifnex_api_token', true);
+ if (!$token) wp_send_json_error('توکن احراز هویت یافت نشد.');
+
+ $response = wp_remote_get($download_url, array(
+ 'headers' => array(
+ 'Authorization' => 'Bearer ' . $token,
+ 'Accept' => 'application/pdf',
+ ),
+ 'timeout' => 30,
+ ));
+
+ if (is_wp_error($response)) wp_send_json_error('خطا در ارتباط با سرور.');
+
+ $status_code = wp_remote_retrieve_response_code($response);
+ if ($status_code !== 200) wp_send_json_error('خطا در دانلود فایل.');
+
+ $file_content = wp_remote_retrieve_body($response);
+ $filename = strtoupper($download_type) . '-' . $order['data']['awb_no'] . '.pdf';
+
+ // ارسال فایل به مرورگر
+ nocache_headers();
+ header('Content-Type: application/pdf');
+ header('Content-Disposition: attachment; filename="' . $filename . '"');
+ header('Content-Length: ' . strlen($file_content));
+ echo $file_content;
+ exit;
+}
+
+// ══════════════════════════════════════════════════════════════
+// AJAX Handler برای دریافت لیست تعهدنامههای سفارش
+// ══════════════════════════════════════════════════════════════
+
+add_action('wp_ajax_ifnex_get_commitment_forms', 'ifnex_get_commitment_forms_ajax');
+
+function ifnex_get_commitment_forms_ajax() {
+ check_ajax_referer('ifnex_ajax_nonce', 'nonce');
+ if (!is_user_logged_in()) {
+ wp_send_json_error('باید وارد شوید.');
+ }
+
+ $order_id = intval($_POST['order_id'] ?? 0);
+ if (!$order_id) wp_send_json_error('شناسه سفارش نامعتبر است.');
+
+ $bridge = new IFNEX_User_Bridge();
+ $user_id = get_current_user_id();
+
+ $order = $bridge->get_customer_order($user_id, $order_id);
+ if (is_wp_error($order)) wp_send_json_error($order->get_error_message());
+
+ $direction = strtolower($order['data']['direction'] ?? 'export');
+ $api_url = get_option('ifnex_api_url', 'http://localhost:8000/api/v1');
+ $url = rtrim($api_url, '/') . "/customer/orders/{$order_id}/commitment-forms";
+
+ $token = get_user_meta($user_id, 'ifnex_api_token', true);
+ if (!$token) wp_send_json_error('توکن احراز هویت یافت نشد.');
+
+ $response = wp_remote_get($url, array(
+ 'headers' => array(
+ 'Authorization' => 'Bearer ' . $token,
+ ),
+ 'timeout' => 30,
+ ));
+
+ if (is_wp_error($response)) wp_send_json_error('خطا در ارتباط با سرور.');
+
+ $body = json_decode(wp_remote_retrieve_body($response), true);
+ if (!$body || !isset($body['success'])) wp_send_json_error('خطا در دریافت اطلاعات.');
+
+ wp_send_json_success($body['data']);
+}
+
+// ══════════════════════════════════════════════════════════════
+// AJAX Handler برای آپلود فرم تعهدنامه امضاشده
+// ══════════════════════════════════════════════════════════════
+
+add_action('wp_ajax_ifnex_upload_commitment_form', 'ifnex_upload_commitment_form_ajax');
+
+function ifnex_upload_commitment_form_ajax() {
+ check_ajax_referer('ifnex_ajax_nonce', 'nonce');
+ if (!is_user_logged_in()) {
+ wp_send_json_error('باید وارد شوید.');
+ }
+
+ $order_id = intval($_POST['order_id'] ?? 0);
+ $form_id = intval($_POST['form_id'] ?? 0);
+
+ if (!$order_id) wp_send_json_error('شناسه سفارش نامعتبر است.');
+ if (!$form_id) wp_send_json_error('شناسه فرم نامعتبر است.');
+ if (!isset($_FILES['file'])) wp_send_json_error('فایل ارسال نشده است.');
+
+ $bridge = new IFNEX_User_Bridge();
+ $user_id = get_current_user_id();
+
+ $order = $bridge->get_customer_order($user_id, $order_id);
+ if (is_wp_error($order)) wp_send_json_error($order->get_error_message());
+
+ $file = $_FILES['file'];
+ $allowed_types = array('application/pdf', 'image/jpeg', 'image/jpg', 'image/png');
+ $max_size = 5 * 1024 * 1024; // 5MB
+
+ if (!in_array($file['type'], $allowed_types)) wp_send_json_error('فرمت فایل مجاز نیست. فقط PDF, JPG, PNG.');
+ if ($file['size'] > $max_size) wp_send_json_error('حجم فایل نباید بیشتر از 5MB باشد.');
+
+ // آپلود فایل به وردپرس
+ require_once(ABSPATH . 'wp-admin/includes/file.php');
+ require_once(ABSPATH . 'wp-admin/includes/media.php');
+ require_once(ABSPATH . 'wp-admin/includes/image.php');
+
+ $upload = wp_handle_upload($file, array('test_form' => false));
+ if (isset($upload['error'])) wp_send_json_error('خطا در آپلود: ' . $upload['error']);
+
+ $file_path = str_replace(wp_get_upload_dir()['basedir'] . '/', '', $upload['file']);
+ $file_url = $upload['url'];
+
+ // ارسال به API لاراول
+ $api_url = get_option('ifnex_api_url', 'http://localhost:8000/api/v1');
+ $url = rtrim($api_url, '/') . "/customer/orders/{$order_id}/commitment-forms/{$form_id}/upload";
+
+ $token = get_user_meta($user_id, 'ifnex_api_token', true);
+ if (!$token) wp_send_json_error('توکن احراز هویت یافت نشد.');
+
+ $multipart = new CURLFile($upload['file'], $file['type'], $file['name']);
+ $post_fields = array(
+ 'file' => $multipart,
+ 'notes' => sanitize_textarea_field($_POST['notes'] ?? ''),
+ );
+
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $url);
+ curl_setopt($ch, CURLOPT_POST, 1);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
+ curl_setopt($ch, CURLOPT_HTTPHEADER, array(
+ 'Authorization: Bearer ' . $token,
+ ));
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+
+ $response = curl_exec($ch);
+ $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($http_code !== 200 && $http_code !== 201) wp_send_json_error('خطا در ارسال فایل به سرور.');
+
+ $result = json_decode($response, true);
+ if (!$result || !isset($result['success'])) wp_send_json_error('خطا در پردازش پاسخ سرور.');
+
+ wp_send_json_success($result['data']);
}
\ No newline at end of file
diff --git a/04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php b/04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php
new file mode 100644
index 0000000..eb2f79a
--- /dev/null
+++ b/04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php
@@ -0,0 +1,55 @@
+startOfDay();
+
+ $totalRevenue = WalletTransaction::where('type', 'order_payment')
+ ->where('status', 'completed')
+ ->where('amount', '>', 0)
+ ->sum('amount');
+
+ $todayRevenue = WalletTransaction::where('type', 'order_payment')
+ ->where('status', 'completed')
+ ->where('amount', '>', 0)
+ ->where('created_at', '>=', $today)
+ ->sum('amount');
+
+ $pendingPayments = Shipment::where('status', 'approved')->count();
+ $pendingAmount = Shipment::where('status', 'approved')->sum('total_fee');
+
+ $totalWalletBalance = Wallet::sum('balance');
+
+ return [
+ Stat::make('درآمد کل (تکمیل شده)', number_format($totalRevenue) . ' ریال')
+ ->icon('heroicon-o-currency-dollar')
+ ->color('success'),
+
+ Stat::make('درآمد امروز', number_format($todayRevenue) . ' ریال')
+ ->icon('heroicon-o-chart-bar')
+ ->color('info'),
+
+ Stat::make('پرداختهای در انتظار', $pendingPayments . ' سفارش')
+ ->description(number_format($pendingAmount) . ' ریال')
+ ->icon('heroicon-o-clock')
+ ->color('warning'),
+
+ Stat::make('مجموع موجودی کیفپولها', number_format($totalWalletBalance) . ' ریال')
+ ->icon('heroicon-o-wallet')
+ ->color('gray'),
+ ];
+ }
+}
diff --git a/04_Laravel/app/Http/Controllers/Api/CommitmentFormController.php b/04_Laravel/app/Http/Controllers/Api/CommitmentFormController.php
index aa33d99..ccf1e46 100644
--- a/04_Laravel/app/Http/Controllers/Api/CommitmentFormController.php
+++ b/04_Laravel/app/Http/Controllers/Api/CommitmentFormController.php
@@ -4,7 +4,11 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\CommitmentForm;
+use App\Models\Shipment;
+use App\Models\ShipmentCommitmentForm;
use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Storage;
class CommitmentFormController extends Controller
{
@@ -63,4 +67,114 @@ class CommitmentFormController extends Controller
'data' => $forms,
]);
}
+
+ /**
+ * دریافت لیست تعهدنامههای مربوط به سفارش خاص
+ * GET /api/v1/customer/orders/{shipment}/commitment-forms
+ */
+ public function shipmentForms(Shipment $shipment): JsonResponse
+ {
+ $user = auth()->user();
+
+ // بررسی مالکیت
+ if ($shipment->user_id !== $user->id) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'شما به این سفارش دسترسی ندارید.',
+ ], 403);
+ }
+
+ $forms = CommitmentForm::query()
+ ->where('is_active', true)
+ ->where(function ($query) use ($shipment) {
+ $query->where('direction', 'both')
+ ->orWhere('direction', $shipment->direction);
+ })
+ ->orderBy('sort_order')
+ ->orderBy('created_at', 'desc')
+ ->get()
+ ->map(function ($form) use ($shipment) {
+ $upload = ShipmentCommitmentForm::where('shipment_id', $shipment->id)
+ ->where('commitment_form_id', $form->id)
+ ->first();
+
+ return [
+ '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,
+ 'upload_status' => $upload ? $upload->status : 'pending',
+ 'uploaded_file_url' => $upload && $upload->uploaded_file_path ? asset('storage/' . $upload->uploaded_file_path) : null,
+ 'uploaded_at' => $upload ? $upload->created_at : null,
+ 'notes' => $upload ? $upload->notes : null,
+ ];
+ });
+
+ return response()->json([
+ 'success' => true,
+ 'data' => $forms,
+ ]);
+ }
+
+ /**
+ * آپلود فرم تعهدنامه امضاشده
+ * POST /api/v1/customer/orders/{shipment}/commitment-forms/{form}/upload
+ */
+ public function uploadSigned(Request $request, Shipment $shipment, CommitmentForm $form): JsonResponse
+ {
+ $user = auth()->user();
+
+ // بررسی مالکیت
+ if ($shipment->user_id !== $user->id) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'شما به این سفارش دسترسی ندارید.',
+ ], 403);
+ }
+
+ $request->validate([
+ 'file' => 'required|file|mimes:pdf,jpg,jpeg,png|max:5120', // حداکثر 5MB
+ 'notes' => 'nullable|string|max:1000',
+ ]);
+
+ try {
+ $file = $request->file('file');
+ $path = $file->store("commitment-forms/{$shipment->id}", 'public');
+
+ $upload = ShipmentCommitmentForm::updateOrCreate(
+ [
+ 'shipment_id' => $shipment->id,
+ 'commitment_form_id' => $form->id,
+ ],
+ [
+ 'uploaded_file_path' => $path,
+ 'uploaded_file_type' => $file->getClientOriginalExtension(),
+ 'uploaded_file_size' => $file->getSize(),
+ 'status' => 'uploaded',
+ 'notes' => $request->input('notes'),
+ 'uploaded_by' => $user->id,
+ ]
+ );
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'فایل با موفقیت آپلود شد.',
+ 'data' => [
+ 'id' => $upload->id,
+ 'file_url' => asset('storage/' . $path),
+ 'file_type' => $upload->uploaded_file_type,
+ 'status' => $upload->status,
+ 'uploaded_at' => $upload->created_at,
+ ],
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'خطا در آپلود فایل: ' . $e->getMessage(),
+ ], 500);
+ }
+ }
}
+
diff --git a/04_Laravel/app/Models/ShipmentCommitmentForm.php b/04_Laravel/app/Models/ShipmentCommitmentForm.php
new file mode 100644
index 0000000..58e40bf
--- /dev/null
+++ b/04_Laravel/app/Models/ShipmentCommitmentForm.php
@@ -0,0 +1,47 @@
+ 'integer',
+ 'verified_at' => 'datetime',
+ ];
+
+ public function shipment(): BelongsTo
+ {
+ return $this->belongsTo(Shipment::class);
+ }
+
+ public function form(): BelongsTo
+ {
+ return $this->belongsTo(CommitmentForm::class, 'commitment_form_id');
+ }
+
+ public function uploader(): BelongsTo
+ {
+ return $this->belongsTo(User::class, 'uploaded_by');
+ }
+
+ public function verifier(): BelongsTo
+ {
+ return $this->belongsTo(User::class, 'verified_by');
+ }
+}
diff --git a/04_Laravel/app/Models/User.php b/04_Laravel/app/Models/User.php
index 93ca6fd..f5b25a2 100644
--- a/04_Laravel/app/Models/User.php
+++ b/04_Laravel/app/Models/User.php
@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
+use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Spatie\Permission\Traits\HasRoles;
@@ -44,6 +45,11 @@ class User extends Authenticatable
return $this->hasOne(Wallet::class);
}
+ public function notifications()
+ {
+ return $this->morphMany(DatabaseNotification::class, 'notifiable')->orderByDesc('created_at');
+ }
+
// ─── Helper Methods (Role Checks) ────────────────
public function isSuperAdmin(): bool
@@ -66,6 +72,16 @@ class User extends Authenticatable
return $this->hasRole('customer');
}
+ public function markNotificationRead(string $notificationId): bool
+ {
+ $notification = $this->notifications()->where('id', $notificationId)->first();
+ if ($notification) {
+ $notification->markAsRead();
+ return true;
+ }
+ return false;
+ }
+
// ─── Credit Methods ──────────────────────────────
public function getAvailableCreditAttribute(): float
diff --git a/04_Laravel/database/migrations/2026_09_09_000001_create_shipment_commitment_forms_table.php b/04_Laravel/database/migrations/2026_09_09_000001_create_shipment_commitment_forms_table.php
new file mode 100644
index 0000000..9c0e8fd
--- /dev/null
+++ b/04_Laravel/database/migrations/2026_09_09_000001_create_shipment_commitment_forms_table.php
@@ -0,0 +1,36 @@
+id();
+ $table->foreignId('shipment_id')->constrained()->cascadeOnDelete();
+ $table->foreignId('commitment_form_id')->constrained()->cascadeOnDelete();
+ $table->string('uploaded_file_path')->nullable();
+ $table->string('uploaded_file_type')->nullable();
+ $table->unsignedBigInteger('uploaded_file_size')->nullable();
+ $table->enum('status', ['pending', 'uploaded', 'verified', 'rejected'])->default('pending');
+ $table->text('notes')->nullable();
+ $table->unsignedBigInteger('uploaded_by')->nullable();
+ $table->unsignedBigInteger('verified_by')->nullable();
+ $table->timestamp('verified_at')->nullable();
+ $table->timestamps();
+
+ $table->foreign('uploaded_by')->references('id')->on('users')->nullOnDelete();
+ $table->foreign('verified_by')->references('id')->on('users')->nullOnDelete();
+
+ $table->unique(['shipment_id', 'commitment_form_id']);
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('shipment_commitment_forms');
+ }
+};
diff --git a/04_Laravel/routes/api.php b/04_Laravel/routes/api.php
index 1c8d28f..1c0e1b4 100644
--- a/04_Laravel/routes/api.php
+++ b/04_Laravel/routes/api.php
@@ -76,11 +76,19 @@ Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
Route::get('/orders/{shipment}', [CustomerOrderController::class, 'show']);
Route::post('/orders/{shipment}/cancel', [CustomerOrderController::class, 'cancel']);
+ // دانلود PDFهای سفارش
+ Route::get('/orders/{shipment}/pdf/awb', [ShipmentPdfController::class, 'awb']);
+ Route::get('/orders/{shipment}/pdf/invoice', [ShipmentPdfController::class, 'invoice']);
+ Route::get('/orders/{shipment}/pdf/label', [ShipmentPdfController::class, 'label']);
+ Route::get('/orders/{shipment}/pdf/import-invoice', [ShipmentPdfController::class, 'importInvoice']);
+ // تعهدنامهها
+ Route::get('/orders/{shipment}/commitment-forms', [CommitmentFormController::class, 'shipmentForms']);
+ Route::post('/orders/{shipment}/commitment-forms/{form}/upload', [CommitmentFormController::class, 'uploadSigned']);
+
// نوتیفیکیشنها
Route::get('/notifications', [CustomerOrderController::class, 'notifications']);
Route::post('/notifications/{notification}/read', [CustomerOrderController::class, 'markNotificationRead']);
-
// پرداخت سفارش
Route::post('/orders/{shipment}/pay-wallet', [CustomerOrderController::class, 'payFromWallet']);