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:
parent
b25bfda597
commit
7a8ff49baa
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -972,6 +972,13 @@ 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 '';
|
||||
}
|
||||
|
||||
$bridge = new IFNEX_User_Bridge();
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
@ -1147,6 +1154,41 @@ function ifnex_order_detail_shortcode($atts) {
|
||||
</div>
|
||||
<?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 رهگیری -->
|
||||
<?php if (!empty($data['tracking_events'])): ?>
|
||||
<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>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
@ -1226,6 +1305,44 @@ function ifnex_order_detail_shortcode($atts) {
|
||||
// آیکونهای 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) {
|
||||
$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"/>',
|
||||
|
||||
@ -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']);
|
||||
}
|
||||
55
04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php
Normal file
55
04_Laravel/app/Filament/Widgets/FinanceOverviewWidget.php
Normal 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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
47
04_Laravel/app/Models/ShipmentCommitmentForm.php
Normal file
47
04_Laravel/app/Models/ShipmentCommitmentForm.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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']);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user