From 0209b6b32e508c08eba67f358d39a62981e3ca83 Mon Sep 17 00:00:00 2001 From: Kazem Alghasi Date: Sat, 3 Oct 2026 05:58:59 +0330 Subject: [PATCH] feat(commitment-forms): implement end-to-end management and secure file access Integrate a complete workflow for commitment forms including secure template downloads, customer uploads via WordPress AJAX, and administrative verification with automated notifications. - Implement `downloadTemplate` and `downloadSigned` logic in `CommitmentFormController` to replace public symlinks with ownership-verified routes. - Add `CommitmentFormVerifiedNotification` and `CommitmentFormRejectedNotification` to alert customers of status changes. - Enhance WordPress bridge with AJAX-driven form listing, status badges, and upload capabilities. - Update Filament `CommitmentFormsRelationManager` to provide better visibility into form directions and descriptions while restricting status edits to specific actions. - Refactor Laravel 11 bootstrap configuration to handle guest redirection for API and web routes correctly. - Add administrative controllers for secure file access within the admin panel. --- .gitignore | 5 +- .../ifnex-bridge/includes/shortcodes.php | 206 +++++++++++++++++- .../CommitmentFormsRelationManager.php | 144 +++++++++--- .../Admin/CommitmentFileController.php | 69 ++++++ .../Api/CommitmentFormController.php | 94 ++++++-- .../CommitmentFormRejectedNotification.php | 42 ++++ .../CommitmentFormVerifiedNotification.php | 41 ++++ 04_Laravel/bootstrap/app.php | 9 + 04_Laravel/routes/api.php | 5 +- 04_Laravel/routes/web.php | 7 + 10 files changed, 559 insertions(+), 63 deletions(-) create mode 100644 04_Laravel/app/Http/Controllers/Admin/CommitmentFileController.php create mode 100644 04_Laravel/app/Notifications/CommitmentFormRejectedNotification.php create mode 100644 04_Laravel/app/Notifications/CommitmentFormVerifiedNotification.php diff --git a/.gitignore b/.gitignore index 14cf3aa..7e5b437 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,7 @@ Thumbs.db # هسته لاراول (پکیج‌های دانلودی و تنظیمات) 04_Laravel/vendor/ 04_Laravel/node_modules/ -04_Laravel/.env \ No newline at end of file +04_Laravel/.env + +# سیم‌لینک storage:link — با artisan ساخته می‌شود، نباید کامیت شود +04_Laravel/public/storage/ 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 f27a35a..ecbd770 100644 --- a/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php +++ b/03_WordPress/wp-content/plugins/ifnex-bridge/includes/shortcodes.php @@ -497,7 +497,104 @@ function ifnex_orders_list_shortcode($atts) { } }); }); + + // 🔹 بارگذاری تعهدنامه‌های سفارش از طریق AJAX + var $cf = $('#ifnex-commitment-forms'); + var cfOrderId = $cf.data('order-id'); + if (cfOrderId) { + $.post(ifnex_ajax.ajax_url, { + action: 'ifnex_get_commitment_forms', + order_id: cfOrderId, + nonce: ifnex_ajax.nonce + }).done(function(res) { + if (!res || !res.success) { + $cf.html('

خطا: ' + ((res && res.data) || 'دریافت نشد') + '

'); + return; + } + var forms = res.data || []; + if (!forms.length) { + $cf.html('

تعهدنامه‌ای برای این سفارش ثبت نشده است.

'); + return; + } + var html = ''; + for (var i = 0; i < forms.length; i++) { + var f = forms[i]; + var status = f.upload_status || 'pending'; + var isUploaded = (status === 'uploaded' || status === 'approved'); + var statusLabel = isUploaded ? '✅ آپلود شده' : '⏳ در انتظار آپلود'; + var statusClass = isUploaded ? 'ifnex-badge-success' : 'ifnex-badge-warning'; + var dlHref = '?order_id=' + cfOrderId + '&download=commitment-template&form_id=' + f.id; + var signedHref = '?order_id=' + cfOrderId + '&download=commitment-signed&form_id=' + f.id; + html += '
'; + html += '
'; + html += '
'; + html += '' + (f.title || '') + ''; + html += ' ' + statusLabel + ''; + if (f.file_type) html += ' (' + f.file_type + ')'; + if (f.description) html += '
' + f.description + '
'; + html += '
'; + html += '
'; + html += '📥 دانلود فرم'; + if (isUploaded) { + html += '📄 دانلود امضاشده'; + } + html += ''; + html += '
'; + html += '
'; + html += '
'; + } + $cf.html(html); + }).fail(function() { + $cf.html('

خطا در ارتباط با سرور.

'); + }); + } }); + + // تعریف روی window چون از onclick صدا زده می‌شود + window.uploadCommitmentForm = function(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); + + // جستجوی نزدیک‌ترین container با .ifnex-form-actions (والد دکمه) + var container = input.closest('.ifnex-form-actions') || input.parentElement; + var originalHtml = container.innerHTML; + container.innerHTML = 'در حال آپلود...'; + + jQuery.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 || 'نامشخص')); + container.innerHTML = originalHtml; + } + }, + error: function(xhr) { + var msg = 'خطا در ارتباط با سرور.'; + if (xhr && xhr.responseJSON && xhr.responseJSON.data) { + msg = 'خطا: ' + xhr.responseJSON.data; + } + alert(msg); + container.innerHTML = originalHtml; + } + }); + }; شناسه سفارش مشخص نشده است.

'; } + + // 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 ''; } + + // Handle commitment form downloads (template / signed) — proxied با توکن لاراول + if (in_array($download_type, ['commitment-template', 'commitment-signed'], true)) { + $form_id = intval($_GET['form_id'] ?? 0); + $type = ($download_type === 'commitment-signed') ? 'signed' : 'template'; + ifnex_handle_commitment_download($order_id, $form_id, $type); + return ''; + } + $bridge = new IFNEX_User_Bridge(); $user_id = get_current_user_id(); @@ -1199,7 +1308,9 @@ function ifnex_order_detail_shortcode($atts) {

📝 تعهدنامه‌ها

-

در حال بارگذاری...

+
+

در حال بارگذاری...

+
@@ -1239,6 +1350,8 @@ jQuery(document).ready(function($) { error: function() { alert('خطا در ارتباط با سرور.'); btn.prop('disabled', false).text('❌ لغو'); } }); + }); + // بارگذاری تعهدنامه‌ها $.ajax({ url: ifnex_ajax.ajax_url, @@ -1248,16 +1361,23 @@ jQuery(document).ready(function($) { if (res.success && res.data && res.data.length > 0) { var html = ''; res.data.forEach(function(form) { - var badge = form.upload_status === 'uploaded' ? '✅ آپلود شده' - : form.upload_status === 'verified' ? '✅ تأیید شده' + var badge = form.upload_status === 'verified' ? '✅ تأیید شده' + : form.upload_status === 'uploaded' ? '✅ آپلود شده (در انتظار بررسی)' + : form.upload_status === 'rejected' ? '❌ رد شده' : '⏳ آپلود نشده'; - html += '
'; + var reasonHtml = (form.upload_status === 'rejected' && form.notes) + ? '
⚠️ دلیل رد: ' + form.notes + '
' + : ''; + // دانلود از طریق پروکسی وردپرس (توکن لاراول سمت سرور ارسال می‌شود؛ لینک مستقیم به API بدون توکن 401/500 می‌دهد) + var dlHref = '?order_id=&download=commitment-template&form_id=' + form.id; + var signedHref = '?order_id=&download=commitment-signed&form_id=' + form.id; + html += '
'; html += '
'; - html += '
' + form.title + '
' + (form.description || '') + '
'; + html += '
' + form.title + '
' + (form.description || '') + '' + reasonHtml + '
'; html += badge; html += '
'; - if (form.file_url) html += '📥 دانلود فرم'; - if (form.uploaded_file_url) html += '📎 فایل آپلودشده'; + if (form.file_url) html += '📥 دانلود فرم'; + if (form.uploaded_file_url) html += '📎 فایل آپلودشده'; html += ''; html += '
'; }); @@ -1270,17 +1390,17 @@ jQuery(document).ready(function($) { $('#ifnex-commitment-forms').html('

خطا در بارگذاری تعهدنامه‌ها.

'); } }); - }); }); function uploadCommitmentForm(orderId, formId, input) { + // بدون $ — وردپرس jQuery را در حالت noConflict لود می‌کند و $ سراسری وجود ندارد var file = input.files[0]; if (!file) return; var fd = new FormData(); fd.append('action', 'ifnex_upload_commitment_form'); fd.append('order_id', orderId); fd.append('form_id', formId); fd.append('file', file); fd.append('nonce', ifnex_ajax.nonce); - var btn = $(input).closest('.ifnex-form-actions'); var orig = btn.html(); + var btn = jQuery(input).closest('.ifnex-form-actions'); var orig = btn.html(); btn.html('در حال آپلود...'); - $.ajax({ + jQuery.ajax({ url: ifnex_ajax.ajax_url, method: 'POST', data: fd, processData: false, contentType: false, success: function(r) { if (r.success) { alert('آپلود شد.'); location.reload(); } else { alert('خطا: ' + (r.data||'')); btn.html(orig); } }, error: function() { alert('خطا در ارتباط با سرور.'); btn.html(orig); } @@ -1333,6 +1453,72 @@ function ifnex_handle_pdf_download($order_id, $download_type) { exit; } +// ══════════════════════════════════════════════════════════════ +// پروکسی دانلود تعهدنامه (قالب خام یا فایل امضاشده) با توکن لاراول +// ══════════════════════════════════════════════════════════════ +function ifnex_handle_commitment_download($order_id, $form_id, $type = 'template') { + $user_id = get_current_user_id(); + if (!$user_id) wp_die('برای دانلود باید وارد شوید.'); + + $form_id = intval($form_id); + if (!$form_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'); + $endpoint = ($type === 'signed') ? 'download' : 'template'; + $download_url = rtrim($api_url, '/') . "/customer/orders/{$order_id}/commitment-forms/{$form_id}/{$endpoint}"; + + $token = get_user_meta($user_id, 'ifnex_laravel_token', true); + if (!$token) wp_die('توکن احراز هویت یافت نشد.'); + + $response = wp_remote_get($download_url, array( + 'headers' => array( + 'Authorization' => 'Bearer ' . $token, + 'Accept' => '*/*', + ), + 'timeout' => 60, + )); + + if (is_wp_error($response)) wp_die('خطا در ارتباط با سرور.'); + + $status_code = wp_remote_retrieve_response_code($response); + if ($status_code !== 200) { + $body = wp_remote_retrieve_body($response); + $err = json_decode($body, true); + $msg = $err['message'] ?? ('خطا در دانلود فایل (کد: ' . $status_code . ')'); + wp_die(esc_html($msg)); + } + + $file_content = wp_remote_retrieve_body($response); + $awb = $order['data']['awb_no'] ?? ('order-' . $order_id); + + // تشخیص نوع محتوا و نام فایل از روی هدر Laravel + $ctype = wp_remote_retrieve_header($response, 'content-type'); + if (empty($ctype)) $ctype = 'application/octet-stream'; + + $disp = wp_remote_retrieve_header($response, 'content-disposition'); + $filename = ($type === 'signed' ? 'signed-' : 'template-') . $form_id . '-' . $awb; + if ($disp && preg_match('/filename="?([^";]+)"?/i', $disp, $m)) { + $filename = trim($m[1]); + } else { + // fallback بر اساس content-type + $ext = (stripos($ctype, 'pdf') !== false) ? 'pdf' + : ((stripos($ctype, 'png') !== false) ? 'png' + : ((stripos($ctype, 'jpeg') !== false) ? 'jpg' : 'bin')); + $filename .= '.' . $ext; + } + + nocache_headers(); + header('Content-Type: ' . $ctype); + 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/04_Laravel/app/Filament/Resources/ShipmentResource/RelationManagers/CommitmentFormsRelationManager.php b/04_Laravel/app/Filament/Resources/ShipmentResource/RelationManagers/CommitmentFormsRelationManager.php index bf472f7..1f53917 100644 --- a/04_Laravel/app/Filament/Resources/ShipmentResource/RelationManagers/CommitmentFormsRelationManager.php +++ b/04_Laravel/app/Filament/Resources/ShipmentResource/RelationManagers/CommitmentFormsRelationManager.php @@ -2,6 +2,9 @@ namespace App\Filament\Resources\ShipmentResource\RelationManagers; +use App\Models\ShipmentCommitmentForm; +use App\Notifications\CommitmentFormRejectedNotification; +use App\Notifications\CommitmentFormVerifiedNotification; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\RelationManagers\RelationManager; @@ -18,23 +21,13 @@ class CommitmentFormsRelationManager extends RelationManager public function form(Form $form): Form { + // رکوردها فقط از آپلود مشتری یا مقداردهی خودکار ساخته می‌شوند؛ + // از این فرم فقط یادداشت قابل ویرایش است (وضعیت با اکشن‌های تأیید/رد تغییر می‌کند) return $form->schema([ - Forms\Components\Select::make('status') - ->options([ - 'pending' => 'در انتظار', - 'uploaded' => 'بارگذاری شده', - 'verified' => 'تأیید شده', - 'rejected' => 'رد شده', - ]) - ->label('وضعیت'), Forms\Components\Textarea::make('notes') ->rows(3) - ->label('یادداشت'), - Forms\Components\Select::make('verified_by') - ->relationship('verifier', 'name') - ->label('تأییدکننده'), - Forms\Components\DateTimePicker::make('verified_at') - ->label('تاریخ تأیید'), + ->label('یادداشت') + ->maxLength(1000), ]); } @@ -42,7 +35,21 @@ class CommitmentFormsRelationManager extends RelationManager { return $table ->columns([ - Tables\Columns\TextColumn::make('form.title')->label('فرم تعهدنامه')->limit(40), + Tables\Columns\TextColumn::make('form.title') + ->label('فرم تعهدنامه') + ->description(fn ($record) => $record->form?->description) + ->limit(40), + Tables\Columns\TextColumn::make('form.direction') + ->label('جهت') + ->badge() + ->color('gray') + ->formatStateUsing(fn ($state): string => match ($state) { + 'export' => 'صادرات', + 'import' => 'واردات', + 'both' => 'هردو', + default => (string) $state, + }) + ->toggleable(), Tables\Columns\TextColumn::make('status') ->label('وضعیت') ->badge() @@ -54,29 +61,112 @@ class CommitmentFormsRelationManager extends RelationManager default => 'gray', }) ->formatStateUsing(fn (string $state): string => match ($state) { - 'pending' => 'در انتظار', - 'uploaded' => 'بارگذاری شده', + 'pending' => 'در انتظار آپلود', + 'uploaded' => 'آپلود شده', 'verified' => 'تأیید شده', 'rejected' => 'رد شده', default => $state, }), - Tables\Columns\TextColumn::make('uploader.name')->label('بارگذاری توسط')->toggleable(), - Tables\Columns\TextColumn::make('verifier.name')->label('تأییدکننده')->toggleable(), - Tables\Columns\TextColumn::make('verified_at')->label('تاریخ تأیید')->dateTime('Y/m/d H:i')->toggleable(), - Tables\Columns\TextColumn::make('notes')->label('یادداشت')->limit(40)->toggleable(), + Tables\Columns\TextColumn::make('uploaded_file_type') + ->label('فایل مشتری') + ->formatStateUsing(function ($state, $record) { + if (!$state) return '—'; + $size = $record->uploaded_file_size ? ' · ' . number_format($record->uploaded_file_size / 1024, 0) . ' KB' : ''; + return strtoupper($state) . $size; + }), + Tables\Columns\TextColumn::make('created_at') + ->label('تاریخ آپلود') + ->dateTime('Y/m/d H:i') + ->toggleable(), + Tables\Columns\TextColumn::make('uploader.name') + ->label('بارگذاری توسط') + ->toggleable(isToggledHiddenByDefault: true), + Tables\Columns\TextColumn::make('verifier.name') + ->label('تأییدکننده') + ->toggleable(), + Tables\Columns\TextColumn::make('verified_at') + ->label('تاریخ تأیید') + ->dateTime('Y/m/d H:i') + ->toggleable(), + Tables\Columns\TextColumn::make('notes') + ->label('یادداشت / دلیل') + ->limit(40) + ->toggleable(), ]) ->defaultSort('created_at', 'desc') // ساخت دستی مجاز نیست — این رکوردها از آپلود تعهدنامه توسط مشتری ایجاد می‌شوند ->headerActions([]) ->actions([ - Tables\Actions\EditAction::make(), - Tables\Actions\Action::make('download') - ->label('دانلود فایل') + Tables\Actions\Action::make('downloadTemplate') + ->label('قالب') ->icon('heroicon-o-arrow-down-tray') - ->url(fn ($record) => $record->uploaded_file_path ? asset('storage/' . $record->uploaded_file_path) : null) + ->color('gray') + ->url(fn ($record) => $record->form?->file_path + ? route('admin.commitment-forms.template', ['shipment' => $record->shipment_id, 'form' => $record->commitment_form_id]) + : null) ->openUrlInNewTab() - ->visible(fn ($record) => !empty($record->uploaded_file_path)), + ->visible(fn ($record) => !empty($record->form?->file_path)) + ->tooltip('دانلود قالب خام تعهدنامه'), + Tables\Actions\Action::make('downloadSigned') + ->label('فایل مشتری') + ->icon('heroicon-o-document-arrow-down') + ->color('info') + ->url(fn ($record) => $record->uploaded_file_path + ? route('admin.commitment-forms.signed', ['shipment' => $record->shipment_id, 'record' => $record->id]) + : null) + ->openUrlInNewTab() + ->visible(fn ($record) => !empty($record->uploaded_file_path)) + ->tooltip('دانلود فایل امضاشدهٔ آپلودشده توسط مشتری'), + Tables\Actions\Action::make('verify') + ->label('تأیید') + ->icon('heroicon-o-check-circle') + ->color('success') + ->requiresConfirmation() + ->modalDescription('با تأیید، به مشتری اعلان ارسال می‌شود.') + ->form([ + Forms\Components\Textarea::make('notes') + ->label('یادداشت برای مشتری (اختیاری)') + ->rows(2) + ->maxLength(1000), + ]) + ->action(function (ShipmentCommitmentForm $record, array $data): void { + $record->update([ + 'status' => 'verified', + 'notes' => $data['notes'] ?: $record->notes, + 'verified_by' => auth()->id(), + 'verified_at' => now(), + ]); + + $record->shipment->user?->notify(new CommitmentFormVerifiedNotification($record)); + }) + ->visible(fn ($record) => in_array($record->status, ['uploaded', 'rejected'])), + Tables\Actions\Action::make('reject') + ->label('رد') + ->icon('heroicon-o-x-circle') + ->color('danger') + ->form([ + Forms\Components\Textarea::make('notes') + ->label('دلیل رد (برای مشتری نمایش داده می‌شود)') + ->rows(2) + ->required() + ->maxLength(1000), + ]) + ->action(function (ShipmentCommitmentForm $record, array $data): void { + $record->update([ + 'status' => 'rejected', + 'notes' => $data['notes'], + 'verified_by' => auth()->id(), + 'verified_at' => now(), + ]); + + $record->shipment->user?->notify(new CommitmentFormRejectedNotification($record)); + }) + ->visible(fn ($record) => in_array($record->status, ['uploaded', 'verified'])), + Tables\Actions\EditAction::make() + ->label('یادداشت') + ->icon('heroicon-o-pencil-square') + ->tooltip('ویرایش یادداشت'), ]) ->bulkActions([]); } -} \ No newline at end of file +} diff --git a/04_Laravel/app/Http/Controllers/Admin/CommitmentFileController.php b/04_Laravel/app/Http/Controllers/Admin/CommitmentFileController.php new file mode 100644 index 0000000..ef646f9 --- /dev/null +++ b/04_Laravel/app/Http/Controllers/Admin/CommitmentFileController.php @@ -0,0 +1,69 @@ +authorizePanel(); + + if (!$form->file_path || !Storage::disk('public')->exists($form->file_path)) { + abort(404, 'فایل قالب یافت نشد.'); + } + + $ext = pathinfo($form->file_path, PATHINFO_EXTENSION); + + return Storage::disk('public')->download( + $form->file_path, + 'template-' . $form->id . '-' . $shipment->awb_no . '.' . $ext + ); + } + + /** + * دانلود فایل امضاشده‌ای که مشتری آپلود کرده است. + * GET /admin/shipments/{shipment}/commitment-forms/{record}/signed + */ + public function signed(Shipment $shipment, ShipmentCommitmentForm $record) + { + $this->authorizePanel(); + + if ($record->shipment_id !== $shipment->id) { + abort(404); + } + + if (!$record->uploaded_file_path || !Storage::disk('secure')->exists($record->uploaded_file_path)) { + abort(404, 'فایل امضاشده یافت نشد.'); + } + + return Storage::disk('secure')->download( + $record->uploaded_file_path, + 'commitment-' . $record->commitment_form_id . '-' . $shipment->awb_no . '.' . $record->uploaded_file_type + ); + } + + /** + * فقط کاربران پنل ادمین (super_admin/admin/staff) مجاز به دانلود هستند. + * همان منطق User::canAccessPanel — بدون نیاز به نمونه پنل (بیرون از کانتکست Filament) + */ + private function authorizePanel(): void + { + abort_unless(auth()->user()?->hasAnyRole(['super_admin', 'admin', 'staff']), 403); + } +} diff --git a/04_Laravel/app/Http/Controllers/Api/CommitmentFormController.php b/04_Laravel/app/Http/Controllers/Api/CommitmentFormController.php index c7dda72..a19fca1 100644 --- a/04_Laravel/app/Http/Controllers/Api/CommitmentFormController.php +++ b/04_Laravel/app/Http/Controllers/Api/CommitmentFormController.php @@ -92,34 +92,82 @@ class CommitmentFormController extends Controller }) ->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(); + ->get(); - 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 - ? route('customer.commitment-forms.download', ['shipment' => $shipment->id, 'form' => $form->id]) - : null, - 'uploaded_at' => $upload ? $upload->created_at : null, - 'notes' => $upload ? $upload->notes : null, - ]; - }); + $rows = ShipmentCommitmentForm::where('shipment_id', $shipment->id) + ->whereIn('commitment_form_id', $forms->pluck('id')) + ->get() + ->keyBy('commitment_form_id'); + + // ساخت ردیف pending برای فرم‌هایی که هنوز رکورد ندارند — + // تا بخش تعهدنامه‌ها در پنل ادمین همه فرم‌ها را همراه وضعیت نمایش دهد + foreach ($forms as $form) { + if (!isset($rows[$form->id])) { + $rows[$form->id] = ShipmentCommitmentForm::create([ + 'shipment_id' => $shipment->id, + 'commitment_form_id' => $form->id, + 'status' => 'pending', + ]); + } + } + + $data = $forms->map(function ($form) use ($shipment, $rows) { + $upload = $rows[$form->id]; + + return [ + 'id' => $form->id, + 'title' => $form->title, + 'description' => $form->description, + // 🛡️ URL دانلود قالب از طریق route محافظت‌شده (نه asset عمومی) + 'file_url' => route('customer.commitment-forms.template', [ + 'shipment' => $shipment->id, + 'form' => $form->id, + ]), + 'file_type' => strtoupper(pathinfo($form->file_path, PATHINFO_EXTENSION)), + 'direction' => $form->direction, + 'upload_status' => $upload->status, + 'uploaded_file_url' => $upload->uploaded_file_path + ? route('customer.commitment-forms.download', ['shipment' => $shipment->id, 'form' => $form->id]) + : null, + 'uploaded_at' => $upload->created_at, + 'notes' => $upload->notes, + ]; + }); return response()->json([ 'success' => true, - 'data' => $forms, + 'data' => $data, ]); } + /** + * دانلود قالب خام تعهدنامه (محافظت‌شده با بررسی مالکیت سفارش). + * GET /api/v1/customer/orders/{shipment}/commitment-forms/{form}/template + */ + public function downloadTemplate(Shipment $shipment, CommitmentForm $form) + { + $user = auth()->user(); + + if ($shipment->user_id !== $user->id) { + return response()->json([ + 'success' => false, + 'message' => 'شما به این سفارش دسترسی ندارید.', + ], 403); + } + + if (!$form->file_path || !Storage::disk('public')->exists($form->file_path)) { + return response()->json([ + 'success' => false, + 'message' => 'فایل قالب یافت نشد.', + ], 404); + } + + $ext = pathinfo($form->file_path, PATHINFO_EXTENSION); + $filename = 'template-' . $form->id . '-' . $shipment->awb_no . '.' . $ext; + + return Storage::disk('public')->download($form->file_path, $filename); + } + /** * آپلود فرم تعهدنامه امضاشده * POST /api/v1/customer/orders/{shipment}/commitment-forms/{form}/upload @@ -214,6 +262,4 @@ class CommitmentFormController extends Controller 'commitment-' . $form->id . '-' . $shipment->awb_no . '.' . $upload->uploaded_file_type ); } -} -} - +} \ No newline at end of file diff --git a/04_Laravel/app/Notifications/CommitmentFormRejectedNotification.php b/04_Laravel/app/Notifications/CommitmentFormRejectedNotification.php new file mode 100644 index 0000000..785bf6b --- /dev/null +++ b/04_Laravel/app/Notifications/CommitmentFormRejectedNotification.php @@ -0,0 +1,42 @@ +record->loadMissing(['shipment', 'form']); + + $awb = $this->record->shipment->awb_no; + $title = $this->record->form?->title ?? ('فرم #' . $this->record->commitment_form_id); + $reason = $this->record->notes ? " دلیل: {$this->record->notes}" : ''; + + return [ + 'type' => 'commitment_rejected', + 'shipment_id' => $this->record->shipment_id, + 'awb_no' => $awb, + 'message' => "تعهدنامه «{$title}» سفارش {$awb} رد شد. لطفاً فایل اصلاح‌شده را دوباره آپلود کنید.{$reason}", + 'action_url' => '/order-detail/?order_id=' . $this->record->shipment_id, + ]; + } +} diff --git a/04_Laravel/app/Notifications/CommitmentFormVerifiedNotification.php b/04_Laravel/app/Notifications/CommitmentFormVerifiedNotification.php new file mode 100644 index 0000000..d2f2dcf --- /dev/null +++ b/04_Laravel/app/Notifications/CommitmentFormVerifiedNotification.php @@ -0,0 +1,41 @@ +record->loadMissing(['shipment', 'form']); + + $awb = $this->record->shipment->awb_no; + $title = $this->record->form?->title ?? ('فرم #' . $this->record->commitment_form_id); + + return [ + 'type' => 'commitment_verified', + 'shipment_id' => $this->record->shipment_id, + 'awb_no' => $awb, + 'message' => "تعهدنامه «{$title}» سفارش {$awb} تأیید شد. ممنون از همکاری شما.", + 'action_url' => '/order-detail/?order_id=' . $this->record->shipment_id, + ]; + } +} diff --git a/04_Laravel/bootstrap/app.php b/04_Laravel/bootstrap/app.php index f5f89b1..b214cef 100644 --- a/04_Laravel/bootstrap/app.php +++ b/04_Laravel/bootstrap/app.php @@ -26,6 +26,11 @@ return Application::configure(basePath: dirname(__DIR__)) 'api_key' => ApiKeyAuth::class, ]); + // پیش‌فرض لاراول ۱۱ مهمان‌ها را به route('login') می‌فرستد که در این پروژه وجود ندارد + // و باعث 500 می‌شود؛ با null، AuthenticationException به هندلر exceptions می‌رسد + // و برای api/* پاسخ JSON 401 برگردانده می‌شود. + $middleware->redirectGuestsTo(fn () => null); + // Stateful API برای Sanctum $middleware->statefulApi(); @@ -52,5 +57,9 @@ return Application::configure(basePath: dirname(__DIR__)) 'error' => 'توکن نامعتبر است یا منقضی شده است. لطفاً دوباره وارد شوید.', ], 401); } + + // مهمان‌های وب (مثلاً بازکردن مستقیم روت‌های دانلود ادمین) به ورود پنل هدایت شوند — + // هندلر پیش‌فرض لاراول به route('login') می‌رود که در این پروژه وجود ندارد + return redirect()->guest(route('filament.admin.auth.login')); }); })->create(); \ No newline at end of file diff --git a/04_Laravel/routes/api.php b/04_Laravel/routes/api.php index d6c3f4c..61262df 100644 --- a/04_Laravel/routes/api.php +++ b/04_Laravel/routes/api.php @@ -97,7 +97,10 @@ Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () { Route::get('/orders/{shipment}/commitment-forms', [CommitmentFormController::class, 'shipmentForms']); Route::post('/orders/{shipment}/commitment-forms/{form}/upload', [CommitmentFormController::class, 'uploadSigned']); Route::get('/orders/{shipment}/commitment-forms/{form}/download', [CommitmentFormController::class, 'downloadSigned']) - ->name('customer.commitment-forms.download'); + ->name('customer.commitment-forms.download'); + // دانلود قالب خام (محافظت‌شده با auth به جای symlink عمومی) + Route::get('/orders/{shipment}/commitment-forms/{form}/template', [CommitmentFormController::class, 'downloadTemplate']) + ->name('customer.commitment-forms.template'); // نوتیفیکیشن‌ها Route::get('/notifications', [CustomerOrderController::class, 'notifications']); Route::post('/notifications/{notification}/read', [CustomerOrderController::class, 'markNotificationRead']); diff --git a/04_Laravel/routes/web.php b/04_Laravel/routes/web.php index b5e6930..1d7b0da 100644 --- a/04_Laravel/routes/web.php +++ b/04_Laravel/routes/web.php @@ -1,5 +1,6 @@ group(function () { 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'); + + // دانلود امن فایل‌های تعهدنامه برای پنل ادمین (کنترل دسترسی داخل کنترلر با canAccessPanel) + Route::get('/admin/shipments/{shipment}/commitment-forms/{form}/template', [CommitmentFileController::class, 'template']) + ->name('admin.commitment-forms.template'); + Route::get('/admin/shipments/{shipment}/commitment-forms/{record}/signed', [CommitmentFileController::class, 'signed']) + ->name('admin.commitment-forms.signed'); }); // صفحات نتیجه پرداخت