feat(notifications): customer notifications on approve/request-changes + WP tab + badge + timezone Iran
This commit is contained in:
parent
8cf407553b
commit
09b511808d
@ -1476,6 +1476,7 @@ function ifnex_icon($name, $size = 20) {
|
||||
'truck' => '<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h1.125c.621 0 1.129-.504 1.09-1.124a17.902 17.902 0 00-3.213-9.193 2.056 2.056 0 00-1.58-.86H14.25M16.5 18.75h-2.25m0-11.177v-.958c0-.568-.422-1.048-.987-1.106a48.554 48.554 0 00-10.026 0 1.106 1.106 0 00-.987 1.106v7.635m12-6.677v6.677m0 4.5v-4.5m0 0h-12"/>',
|
||||
'check' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>',
|
||||
'clock' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/>',
|
||||
'bell' => '<path stroke-linecap="round" stroke-linejoin="round" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>',
|
||||
];
|
||||
|
||||
$path = $icons[$name] ?? $icons['dashboard'];
|
||||
@ -1503,7 +1504,7 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
}
|
||||
|
||||
$tab = sanitize_key($_GET['tab'] ?? 'dashboard');
|
||||
$allowed_tabs = ['dashboard', 'orders', 'new-order', 'wallet', 'transactions', 'tracking', 'profile'];
|
||||
$allowed_tabs = ['dashboard', 'notifications', 'orders', 'new-order', 'wallet', 'transactions', 'tracking', 'profile'];
|
||||
if (!in_array($tab, $allowed_tabs)) $tab = 'dashboard';
|
||||
|
||||
$bridge = new IFNEX_User_Bridge();
|
||||
@ -1517,6 +1518,13 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
$stats = $profile['stats'] ?? [];
|
||||
}
|
||||
|
||||
// تعداد اعلانهای خواندهنشده برای badge سایدبار
|
||||
$unread_notifications = 0;
|
||||
$notif_count_result = $bridge->get_notifications($user_id, 1);
|
||||
if (!is_wp_error($notif_count_result) && isset($notif_count_result['unread_count'])) {
|
||||
$unread_notifications = (int) $notif_count_result['unread_count'];
|
||||
}
|
||||
|
||||
$recent_orders = [];
|
||||
if ($tab === 'dashboard') {
|
||||
$orders_result = $bridge->get_customer_orders($user_id, 3);
|
||||
@ -1526,6 +1534,7 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
$base_url = get_permalink();
|
||||
$menu_items = [
|
||||
'dashboard' => ['icon' => 'dashboard', 'label' => 'داشبورد'],
|
||||
'notifications'=> ['icon' => 'bell', 'label' => 'اعلانها'],
|
||||
'orders' => ['icon' => 'orders', 'label' => 'سفارشات من'],
|
||||
'new-order' => ['icon' => 'plus', 'label' => 'ثبت سفارش جدید'],
|
||||
'wallet' => ['icon' => 'wallet', 'label' => 'کیف پول'],
|
||||
@ -1535,12 +1544,13 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
];
|
||||
|
||||
$tab_titles = [
|
||||
'dashboard' => 'داشبورد', 'orders' => 'سفارشات من', 'new-order' => 'ثبت سفارش جدید',
|
||||
'dashboard' => 'داشبورد', 'notifications' => 'اعلانها', 'orders' => 'سفارشات من', 'new-order' => 'ثبت سفارش جدید',
|
||||
'wallet' => 'کیف پول', 'transactions' => 'تراکنشها', 'tracking' => 'رهگیری مرسوله', 'profile' => 'پروفایل',
|
||||
];
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<style>.ifnx-menu-badge{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;border-radius:10px;background:#ef4444;color:#fff;font-size:11px;font-weight:700;margin-inline-start:auto}</style>
|
||||
<div class="ifnx-dashboard">
|
||||
<!-- سایدبار -->
|
||||
<aside class="ifnx-sidebar">
|
||||
@ -1555,6 +1565,9 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
class="ifnx-menu-item <?php echo $tab === $key ? 'active' : ''; ?>">
|
||||
<span class="ifnx-menu-icon"><?php echo ifnex_icon($item['icon'], 19); ?></span>
|
||||
<span><?php echo esc_html($item['label']); ?></span>
|
||||
<?php if ($key === 'notifications' && $unread_notifications > 0): ?>
|
||||
<span class="ifnx-menu-badge"><?php echo (int)$unread_notifications; ?></span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<div class="ifnx-menu-divider"></div>
|
||||
@ -1653,6 +1666,74 @@ function ifnex_customer_dashboard_shortcode($atts) {
|
||||
<?php echo do_shortcode('[ifnex_transactions]'); ?>
|
||||
<?php elseif ($tab === 'tracking'): ?>
|
||||
<?php echo do_shortcode('[ifnex_tracking_form]'); ?>
|
||||
<?php elseif ($tab === 'notifications'): ?>
|
||||
<?php
|
||||
$notif_result = $bridge->get_notifications($user_id);
|
||||
$notifications = [];
|
||||
if (!is_wp_error($notif_result)) {
|
||||
$notifications = $notif_result['data'] ?? $notif_result ?? [];
|
||||
if (!isset($notif_result['data']) && isset($notif_result[0])) {
|
||||
$notifications = $notif_result;
|
||||
}
|
||||
}
|
||||
$unread = 0;
|
||||
foreach ($notifications as $n) {
|
||||
if (empty($n['read_at'])) $unread++;
|
||||
}
|
||||
?>
|
||||
<style>
|
||||
.ifnex-notifications-list{display:flex;flex-direction:column;gap:10px}
|
||||
.ifnex-notification{display:flex;align-items:center;gap:14px;padding:16px;border-radius:12px;background:#f8fafc;border:1px solid #e2e8f0;transition:all .2s}
|
||||
.ifnex-notification.unread{background:#fffbeb;border-color:#fde68a}
|
||||
.ifnex-notification-icon{flex-shrink:0;width:42px;height:42px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.08)}
|
||||
.ifnex-notification.unread .ifnex-notification-icon{background:#fef3c7}
|
||||
.ifnex-notification-body{flex:1;min-width:0}
|
||||
.ifnex-notification-msg{font-weight:600;color:#0f172a;font-size:14px;line-height:1.5}
|
||||
.ifnex-notification-meta{font-size:12px;color:#64748b;margin-top:4px}
|
||||
.ifnex-notification-meta code{background:#e2e8f0;padding:1px 6px;border-radius:4px;font-size:11px}
|
||||
.ifnex-notification-time{font-size:11px;color:#94a3b8;margin-top:6px}
|
||||
.ifnex-notification-action{flex-shrink:0;font-size:13px;font-weight:600;color:#f97316;text-decoration:none;white-space:nowrap}
|
||||
.ifnex-notification-action:hover{text-decoration:underline}
|
||||
</style>
|
||||
<div class="ifnx-card">
|
||||
<div class="ifnx-card-header">
|
||||
<h3>اعلانها<?php if ($unread > 0): ?> <span class="ifnex-badge ifnex-badge-warning"><?php echo (int)$unread; ?></span><?php endif; ?></h3>
|
||||
</div>
|
||||
<?php if (empty($notifications)): ?>
|
||||
<div class="ifnx-empty">
|
||||
<?php echo ifnex_icon('bell', 40); ?>
|
||||
<p>اعلانی ندارید.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="ifnex-notifications-list">
|
||||
<?php foreach ($notifications as $n):
|
||||
$raw_data = $n['data'] ?? [];
|
||||
$data = is_string($raw_data) ? json_decode($raw_data, true) : $raw_data;
|
||||
$is_unread = empty($n['read_at']);
|
||||
$msg = $data['message'] ?? '';
|
||||
$awb = $data['awb_no'] ?? '';
|
||||
$action = $data['action_url'] ?? '';
|
||||
$created = $n['created_at'] ?? '';
|
||||
$n_type = $data['type'] ?? '';
|
||||
$icon_name = ($n_type === 'order_approved') ? 'check' : (($n_type === 'order_needs_changes') ? 'clock' : 'bell');
|
||||
?>
|
||||
<div class="ifnex-notification <?php echo $is_unread ? 'unread' : ''; ?>">
|
||||
<div class="ifnex-notification-icon"><?php echo ifnex_icon($icon_name, 22); ?></div>
|
||||
<div class="ifnex-notification-body">
|
||||
<div class="ifnex-notification-msg"><?php echo esc_html($msg); ?></div>
|
||||
<?php if ($awb): ?>
|
||||
<div class="ifnex-notification-meta">سفارش: <code><?php echo esc_html($awb); ?></code></div>
|
||||
<?php endif; ?>
|
||||
<div class="ifnex-notification-time"><?php echo esc_html($created); ?></div>
|
||||
</div>
|
||||
<?php if ($action): ?>
|
||||
<a href="<?php echo esc_url(home_url($action)); ?>" class="ifnex-notification-action">مشاهده ←</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php elseif ($tab === 'profile'): ?>
|
||||
<?php echo do_shortcode('[ifnex_user_profile]'); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
@ -783,6 +783,63 @@ class CustomerOrderController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* دریافت نوتیفیکیشنهای کاربر
|
||||
* GET /api/v1/customer/notifications
|
||||
*/
|
||||
public function notifications(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
$perPage = $request->integer('per_page', 20);
|
||||
|
||||
$notifications = $user->notifications()
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => array_map(fn ($n) => [
|
||||
'id' => $n->id,
|
||||
'type' => $n->type,
|
||||
'data' => $n->data,
|
||||
'read_at' => $n->read_at,
|
||||
'created_at' => $n->created_at?->toIso8601String(),
|
||||
], $notifications->items()),
|
||||
'unread_count' => $user->unreadNotifications()->count(),
|
||||
'pagination' => [
|
||||
'current_page' => $notifications->currentPage(),
|
||||
'last_page' => $notifications->lastPage(),
|
||||
'per_page' => $notifications->perPage(),
|
||||
'total' => $notifications->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* علامتگذاری نوتیفیکیشن بهعنوان خواندهشده
|
||||
* POST /api/v1/customer/notifications/{notification}/read
|
||||
*/
|
||||
public function markNotificationRead(Request $request, $notification): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$notif = $user->notifications()->where('id', $notification)->first();
|
||||
|
||||
if (!$notif) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'اعلان یافت نشد.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$notif->markAsRead();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'اعلان بهعنوان خواندهشده علامتگذاری شد.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* فرمت کردن Shipment برای خروجی API
|
||||
*/
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Shipment;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
/**
|
||||
* اعلان به مشتری: سفارش شما تأیید شد، اکنون میتوانید پرداخت کنید.
|
||||
* (Trigger: ShipmentReviewService::approve)
|
||||
*/
|
||||
class OrderApprovedForPaymentNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public Shipment $shipment,
|
||||
public ?User $approvedBy = null,
|
||||
) {}
|
||||
|
||||
public function via($notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
}
|
||||
|
||||
public function toArray($notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'order_approved',
|
||||
'shipment_id' => $this->shipment->id,
|
||||
'awb_no' => $this->shipment->awb_no,
|
||||
'total_fee' => $this->shipment->total_fee,
|
||||
'message' => "سفارش {$this->shipment->awb_no} تأیید شد. اکنون میتوانید پرداخت کنید.",
|
||||
'action_url' => '/order-detail/?order_id=' . $this->shipment->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Shipment;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
/**
|
||||
* اعلان به مشتری: سفارش شما نیازمند اصلاح است.
|
||||
* (Trigger: ShipmentReviewService::requestChanges)
|
||||
*/
|
||||
class OrderNeedsChangesNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public Shipment $shipment,
|
||||
public string $reason,
|
||||
public ?string $notes = null,
|
||||
) {}
|
||||
|
||||
public function via($notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
}
|
||||
|
||||
public function toArray($notifiable): array
|
||||
{
|
||||
return [
|
||||
'type' => 'order_needs_changes',
|
||||
'shipment_id' => $this->shipment->id,
|
||||
'awb_no' => $this->shipment->awb_no,
|
||||
'reason' => $this->reason,
|
||||
'notes' => $this->notes,
|
||||
'message' => "سفارش {$this->shipment->awb_no} نیازمند اصلاح است: {$this->reason}",
|
||||
'action_url' => '/order-detail/?order_id=' . $this->shipment->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -53,6 +53,11 @@ class ShipmentReviewService
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
|
||||
// اعلان به مشتری: سفارش نیازمند اصلاح است
|
||||
$shipment->user->notify(
|
||||
new \App\Notifications\OrderNeedsChangesNotification($shipment, $reason, $notes)
|
||||
);
|
||||
|
||||
return $shipment->fresh();
|
||||
});
|
||||
}
|
||||
@ -107,9 +112,14 @@ class ShipmentReviewService
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
|
||||
// خودکارسازی چکلیست از قالبهای فعال (بعد از تأیید سفارش)
|
||||
// خودکارسازی چکلیست از قالبهای فعال (بعد از تأیید سفارش)
|
||||
$this->instantiateChecklist($shipment);
|
||||
|
||||
// اعلان به مشتری: سفارش تأیید شد، میتونه پرداخت کنه
|
||||
$shipment->user->notify(
|
||||
new \App\Notifications\OrderApprovedForPaymentNotification($shipment, $user)
|
||||
);
|
||||
|
||||
return $shipment->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
@ -65,8 +65,8 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
|
||||
'timezone' => env('APP_TIMEZONE', 'Asia/Tehran'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|
||||
Loading…
Reference in New Issue
Block a user