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.
This commit is contained in:
parent
d6f9ce5553
commit
0209b6b32e
5
.gitignore
vendored
5
.gitignore
vendored
@ -17,4 +17,7 @@ Thumbs.db
|
||||
# هسته لاراول (پکیجهای دانلودی و تنظیمات)
|
||||
04_Laravel/vendor/
|
||||
04_Laravel/node_modules/
|
||||
04_Laravel/.env
|
||||
04_Laravel/.env
|
||||
|
||||
# سیملینک storage:link — با artisan ساخته میشود، نباید کامیت شود
|
||||
04_Laravel/public/storage/
|
||||
|
||||
@ -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('<p style="color:#dc2626">خطا: ' + ((res && res.data) || 'دریافت نشد') + '</p>');
|
||||
return;
|
||||
}
|
||||
var forms = res.data || [];
|
||||
if (!forms.length) {
|
||||
$cf.html('<p style="color:#94a3b8;font-size:13px">تعهدنامهای برای این سفارش ثبت نشده است.</p>');
|
||||
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 += '<div class="ifnex-form-actions" style="border:1px solid #e2e8f0;border-radius:8px;padding:12px;margin-bottom:10px;background:#fff">';
|
||||
html += '<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">';
|
||||
html += '<div>';
|
||||
html += '<strong>' + (f.title || '') + '</strong>';
|
||||
html += ' <span class="ifnex-badge ' + statusClass + '">' + statusLabel + '</span>';
|
||||
if (f.file_type) html += ' <span style="color:#94a3b8;font-size:11px">(' + f.file_type + ')</span>';
|
||||
if (f.description) html += '<div style="color:#64748b;font-size:12px;margin-top:2px">' + f.description + '</div>';
|
||||
html += '</div>';
|
||||
html += '<div style="display:flex;gap:8px;flex-wrap:wrap">';
|
||||
html += '<a href="' + dlHref + '" class="ifnex-btn ifnex-btn-sm ifnex-btn-info" target="_blank">📥 دانلود فرم</a>';
|
||||
if (isUploaded) {
|
||||
html += '<a href="' + signedHref + '" class="ifnex-btn ifnex-btn-sm ifnex-btn-success" target="_blank">📄 دانلود امضاشده</a>';
|
||||
}
|
||||
html += '<label class="ifnex-btn ifnex-btn-sm ifnex-btn-primary" style="cursor:pointer;margin:0">';
|
||||
html += '📤 آپلود امضاشده';
|
||||
html += '<input type="file" accept=".pdf,.jpg,.jpeg,.png" style="display:none" onchange="uploadCommitmentForm(' + cfOrderId + ', ' + f.id + ', this)">';
|
||||
html += '</label>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
$cf.html(html);
|
||||
}).fail(function() {
|
||||
$cf.html('<p style="color:#dc2626">خطا در ارتباط با سرور.</p>');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// تعریف روی 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 = '<span class="ifnex-loading">در حال آپلود...</span>';
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
@ -971,12 +1068,24 @@ function ifnex_order_detail_shortcode($atts) {
|
||||
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 '';
|
||||
}
|
||||
|
||||
|
||||
// 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) {
|
||||
</div>
|
||||
<div class="ifnex-od-card">
|
||||
<h3>📝 تعهدنامهها</h3>
|
||||
<div id="ifnex-commitment-forms"><p style="color:#94a3b8">در حال بارگذاری...</p></div>
|
||||
<div id="ifnex-commitment-forms" data-order-id="<?php echo esc_attr($order_id); ?>">
|
||||
<p style="color:#94a3b8">در حال بارگذاری...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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' ? '<span style="color:#10b981">✅ آپلود شده</span>'
|
||||
: form.upload_status === 'verified' ? '<span style="color:#10b981">✅ تأیید شده</span>'
|
||||
var badge = form.upload_status === 'verified' ? '<span style="color:#10b981">✅ تأیید شده</span>'
|
||||
: form.upload_status === 'uploaded' ? '<span style="color:#3b82f6">✅ آپلود شده (در انتظار بررسی)</span>'
|
||||
: form.upload_status === 'rejected' ? '<span style="color:#ef4444">❌ رد شده</span>'
|
||||
: '<span style="color:#94a3b8">⏳ آپلود نشده</span>';
|
||||
html += '<div style="border:1px solid #e2e8f0;border-radius:10px;padding:12px;margin-bottom:8px">';
|
||||
var reasonHtml = (form.upload_status === 'rejected' && form.notes)
|
||||
? '<div style="color:#ef4444;font-size:12px;margin-top:4px">⚠️ دلیل رد: ' + form.notes + '</div>'
|
||||
: '';
|
||||
// دانلود از طریق پروکسی وردپرس (توکن لاراول سمت سرور ارسال میشود؛ لینک مستقیم به API بدون توکن 401/500 میدهد)
|
||||
var dlHref = '?order_id=<?php echo $oid; ?>&download=commitment-template&form_id=' + form.id;
|
||||
var signedHref = '?order_id=<?php echo $oid; ?>&download=commitment-signed&form_id=' + form.id;
|
||||
html += '<div class="ifnex-form-actions" style="border:1px solid #e2e8f0;border-radius:10px;padding:12px;margin-bottom:8px">';
|
||||
html += '<div style="display:flex;justify-content:space-between;align-items:start;gap:8px;flex-wrap:wrap">';
|
||||
html += '<div><strong>' + form.title + '</strong><br><small style="color:#64748b">' + (form.description || '') + '</small></div>';
|
||||
html += '<div><strong>' + form.title + '</strong><br><small style="color:#64748b">' + (form.description || '') + '</small>' + reasonHtml + '</div>';
|
||||
html += badge;
|
||||
html += '</div><div style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap">';
|
||||
if (form.file_url) html += '<a href="' + form.file_url + '" target="_blank" class="ifnex-btn ifnex-btn-sm ifnex-btn-info">📥 دانلود فرم</a>';
|
||||
if (form.uploaded_file_url) html += '<a href="' + form.uploaded_file_url + '" target="_blank" class="ifnex-btn ifnex-btn-sm ifnex-btn-success">📎 فایل آپلودشده</a>';
|
||||
if (form.file_url) html += '<a href="' + dlHref + '" target="_blank" class="ifnex-btn ifnex-btn-sm ifnex-btn-info">📥 دانلود فرم</a>';
|
||||
if (form.uploaded_file_url) html += '<a href="' + signedHref + '" target="_blank" class="ifnex-btn ifnex-btn-sm ifnex-btn-success">📎 فایل آپلودشده</a>';
|
||||
html += '<label class="ifnex-btn ifnex-btn-sm ifnex-btn-warning" style="cursor:pointer">📤 آپلود امضاشده<input type="file" accept=".pdf,.jpg,.jpeg,.png" style="display:none" onchange="uploadCommitmentForm(<?php echo $oid; ?>,' + form.id + ',this)"></label>';
|
||||
html += '</div></div>';
|
||||
});
|
||||
@ -1270,17 +1390,17 @@ jQuery(document).ready(function($) {
|
||||
$('#ifnex-commitment-forms').html('<p style="color:#ef4444;font-size:13px">خطا در بارگذاری تعهدنامهها.</p>');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
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('<span>در حال آپلود...</span>');
|
||||
$.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' => '<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"/>',
|
||||
|
||||
@ -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([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CommitmentForm;
|
||||
use App\Models\Shipment;
|
||||
use App\Models\ShipmentCommitmentForm;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* دانلود امن فایلهای تعهدنامه برای پنل ادمین (Filament).
|
||||
*
|
||||
* فایلهای قالب روی دیسک public و فایلهای امضاشده روی دیسک secure ذخیره میشوند؛
|
||||
* دیسک secure هیچ URL عمومی ندارد و فایل فقط از طریق این روت (با مجوز پنل) استریم میشود.
|
||||
*/
|
||||
class CommitmentFileController extends Controller
|
||||
{
|
||||
/**
|
||||
* دانلود قالب خام تعهدنامه برای یک سفارش.
|
||||
* GET /admin/shipments/{shipment}/commitment-forms/{form}/template
|
||||
*/
|
||||
public function template(Shipment $shipment, CommitmentForm $form)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\ShipmentCommitmentForm;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
/**
|
||||
* اعلان به مشتری: تعهدنامهٔ آپلودشدهٔ شما رد شد — باید فایل جدید آپلود کنید.
|
||||
* (Trigger: CommitmentFormsRelationManager::reject)
|
||||
*/
|
||||
class CommitmentFormRejectedNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public ShipmentCommitmentForm $record,
|
||||
) {}
|
||||
|
||||
public function via($notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
}
|
||||
|
||||
public function toArray($notifiable): array
|
||||
{
|
||||
$this->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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\ShipmentCommitmentForm;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
/**
|
||||
* اعلان به مشتری: تعهدنامهٔ آپلودشدهٔ شما تأیید شد.
|
||||
* (Trigger: CommitmentFormsRelationManager::verify)
|
||||
*/
|
||||
class CommitmentFormVerifiedNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public ShipmentCommitmentForm $record,
|
||||
) {}
|
||||
|
||||
public function via($notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
}
|
||||
|
||||
public function toArray($notifiable): array
|
||||
{
|
||||
$this->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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
@ -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']);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\CommitmentFileController;
|
||||
use App\Http\Controllers\OrderController;
|
||||
use App\Http\Controllers\PricingPageController;
|
||||
use App\Http\Controllers\ShipmentPdfController;
|
||||
@ -20,6 +21,12 @@ Route::middleware('auth')->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');
|
||||
});
|
||||
|
||||
// صفحات نتیجه پرداخت
|
||||
|
||||
Loading…
Reference in New Issue
Block a user