feat(api): implement shipment commitment forms and document downloads

Introduce a complete workflow for managing shipment-related documents
and mandatory commitment forms between the Laravel backend and
WordPress frontend.

- Laravel:
  - Add `ShipmentCommitmentForm` model and migration to track signed
    forms per shipment.
  - Implement `CommitmentFormController` to handle fetching available
    forms and uploading signed documents.
  - Add PDF download endpoints for AWB, labels, and invoices via
    `ShipmentPdfController`.
  - Extend `User` model with notification management methods.
  - Add `FinanceOverviewWidget` for Filament dashboard.

- WordPress:
  - Implement AJAX handlers in `user-bridge.php` for PDF downloads and
    commitment form management.
  - Update `shortcodes.php` to display document download grid and
    dynamic commitment form upload interface in order details.
  - Add styling for document buttons and form status indicators in
    `ifnex-orders.css`.
This commit is contained in:
Kazem Alghasi 2026-09-09 13:08:22 +03:30
parent b25bfda597
commit 7a8ff49baa
9 changed files with 689 additions and 1 deletions

View File

@ -760,4 +760,120 @@
@media (max-width: 640px) { @media (max-width: 640px) {
.ifnx-notif-item { padding: 12px 14px; } .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%;
}
} }

View File

@ -972,6 +972,13 @@ function ifnex_order_detail_shortcode($atts) {
return '<p class="ifnex-error">شناسه سفارش مشخص نشده است.</p>'; return '<p class="ifnex-error">شناسه سفارش مشخص نشده است.</p>';
} }
// 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(); $bridge = new IFNEX_User_Bridge();
$user_id = get_current_user_id(); $user_id = get_current_user_id();
@ -1147,6 +1154,41 @@ function ifnex_order_detail_shortcode($atts) {
</div> </div>
<?php endif; ?> <?php endif; ?>
<!-- دانلود مدارک -->
<div class="ifnex-detail-section">
<h3>📄 دانلود مدارک</h3>
<div class="ifnex-docs-grid">
<a href="<?php echo esc_url(home_url('/my-account/?tab=order-detail&order_id=' . $order_id . '&download=awb')); ?>" class="ifnex-doc-btn" target="_blank">
<span class="ifnex-doc-icon">📦</span>
<span class="ifnex-doc-label">AWB</span>
</a>
<a href="<?php echo esc_url(home_url('/my-account/?tab=order-detail&order_id=' . $order_id . '&download=label')); ?>" class="ifnex-doc-btn" target="_blank">
<span class="ifnex-doc-icon">🏷️</span>
<span class="ifnex-doc-label">Label</span>
</a>
<?php if (!empty($data['items']) || ($data['type'] ?? '') === 'PARCEL'): ?>
<a href="<?php echo esc_url(home_url('/my-account/?tab=order-detail&order_id=' . $order_id . '&download=invoice')); ?>" class="ifnex-doc-btn" target="_blank">
<span class="ifnex-doc-icon">🧾</span>
<span class="ifnex-doc-label">Invoice</span>
</a>
<?php endif; ?>
<?php if (($data['direction'] ?? '') === 'import'): ?>
<a href="<?php echo esc_url(home_url('/my-account/?tab=order-detail&order_id=' . $order_id . '&download=import-invoice')); ?>" class="ifnex-doc-btn" target="_blank">
<span class="ifnex-doc-icon">📋</span>
<span class="ifnex-doc-label">Service Invoice</span>
</a>
<?php endif; ?>
</div>
</div>
<!-- تعهدنامه‌ها -->
<div class="ifnex-detail-section">
<h3>📝 تعهدنامه‌ها</h3>
<div id="ifnex-commitment-forms">
<p>در حال بارگذاری...</p>
</div>
</div>
<!-- Timeline رهگیری --> <!-- Timeline رهگیری -->
<?php if (!empty($data['tracking_events'])): ?> <?php if (!empty($data['tracking_events'])): ?>
<div class="ifnex-detail-section"> <div class="ifnex-detail-section">
@ -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('<span class=\"ifnex-loading\">در حال آپلود...</span>');
$.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);
}
});
}
</script> </script>
<?php <?php
return ob_get_clean(); return ob_get_clean();
@ -1226,6 +1305,44 @@ function ifnex_order_detail_shortcode($atts) {
// آیکون‌های SVG مدرن (سبک Heroicons) // آیکون‌های SVG مدرن (سبک Heroicons)
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
function ifnex_handle_pdf_download($order_id, $download_type) {
$user_id = get_current_user_id();
if (!$user_id) wp_die('برای دانلود باید وارد شوید.');
$bridge = new IFNEX_User_Bridge();
$order = $bridge->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) { function ifnex_icon($name, $size = 20) {
$icons = [ $icons = [
'dashboard' => '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75"/>', 'dashboard' => '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75"/>',

View File

@ -659,4 +659,183 @@ function ifnex_enqueue_global_assets() {
'orders_url' => home_url('/my-account/?tab=orders'), 'orders_url' => home_url('/my-account/?tab=orders'),
'home_url' => home_url('/'), 'home_url' => home_url('/'),
)) . ';', 'before'); )) . ';', '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']);
} }

View File

@ -0,0 +1,55 @@
<?php
namespace App\Filament\Widgets;
use App\Models\Shipment;
use App\Models\Wallet;
use App\Models\WalletTransaction;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Support\Facades\DB;
class FinanceOverviewWidget extends StatsOverviewWidget
{
protected static ?string $pollingInterval = '30s';
protected function getStats(): array
{
$today = now()->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'),
];
}
}

View File

@ -4,7 +4,11 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\CommitmentForm; use App\Models\CommitmentForm;
use App\Models\Shipment;
use App\Models\ShipmentCommitmentForm;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class CommitmentFormController extends Controller class CommitmentFormController extends Controller
{ {
@ -63,4 +67,114 @@ class CommitmentFormController extends Controller
'data' => $forms, '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);
}
}
} }

View File

@ -0,0 +1,47 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ShipmentCommitmentForm extends Model
{
protected $fillable = [
'shipment_id',
'commitment_form_id',
'uploaded_file_path',
'uploaded_file_type',
'uploaded_file_size',
'status',
'notes',
'uploaded_by',
'verified_by',
'verified_at',
];
protected $casts = [
'uploaded_file_size' => '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');
}
}

View File

@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
use Spatie\Permission\Traits\HasRoles; use Spatie\Permission\Traits\HasRoles;
@ -44,6 +45,11 @@ class User extends Authenticatable
return $this->hasOne(Wallet::class); return $this->hasOne(Wallet::class);
} }
public function notifications()
{
return $this->morphMany(DatabaseNotification::class, 'notifiable')->orderByDesc('created_at');
}
// ─── Helper Methods (Role Checks) ──────────────── // ─── Helper Methods (Role Checks) ────────────────
public function isSuperAdmin(): bool public function isSuperAdmin(): bool
@ -66,6 +72,16 @@ class User extends Authenticatable
return $this->hasRole('customer'); 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 ────────────────────────────── // ─── Credit Methods ──────────────────────────────
public function getAvailableCreditAttribute(): float public function getAvailableCreditAttribute(): float

View File

@ -0,0 +1,36 @@
<?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_commitment_forms', function (Blueprint $table) {
$table->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');
}
};

View File

@ -76,11 +76,19 @@ Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
Route::get('/orders/{shipment}', [CustomerOrderController::class, 'show']); Route::get('/orders/{shipment}', [CustomerOrderController::class, 'show']);
Route::post('/orders/{shipment}/cancel', [CustomerOrderController::class, 'cancel']); 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::get('/notifications', [CustomerOrderController::class, 'notifications']);
Route::post('/notifications/{notification}/read', [CustomerOrderController::class, 'markNotificationRead']); Route::post('/notifications/{notification}/read', [CustomerOrderController::class, 'markNotificationRead']);
// پرداخت سفارش // پرداخت سفارش
Route::post('/orders/{shipment}/pay-wallet', [CustomerOrderController::class, 'payFromWallet']); Route::post('/orders/{shipment}/pay-wallet', [CustomerOrderController::class, 'payFromWallet']);