feat(notifications): customer notifications on approve/request-changes + WP tab + badge + timezone Iran

This commit is contained in:
Kazem Alghasi 2026-10-02 19:08:41 +03:30
parent 8cf407553b
commit 09b511808d
6 changed files with 232 additions and 5 deletions

View File

@ -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; ?>

View File

@ -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
*/

View File

@ -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,
];
}
}

View File

@ -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,
];
}
}

View File

@ -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();
});
}

View File

@ -65,8 +65,8 @@ return [
|
*/
'timezone' => 'UTC',
'timezone' => env('APP_TIMEZONE', 'Asia/Tehran'),
/*
|--------------------------------------------------------------------------
| Application Locale Configuration