Explorar o código

read all notifications

Alexander Musikhin hai 1 mes
pai
achega
929505a9ea

+ 17 - 0
app/Http/Controllers/UserNotificationController.php

@@ -76,6 +76,23 @@ class UserNotificationController extends Controller
         ]);
     }
 
+    public function markAllRead(Request $request): JsonResponse
+    {
+        $now = now();
+
+        $updated = UserNotification::query()
+            ->where('user_id', $request->user()->id)
+            ->whereNull('read_at')
+            ->update(['read_at' => $now]);
+
+        return response()->json([
+            'ok' => true,
+            'updated' => $updated,
+            'read_at' => $now->format('d.m.Y H:i:s'),
+            'unread' => 0,
+        ]);
+    }
+
     public function unreadCount(Request $request): JsonResponse
     {
         return response()->json([

+ 66 - 10
resources/views/notifications/index.blade.php

@@ -2,6 +2,15 @@
 
 @section('content')
 
+    <div class="d-flex justify-content-end mb-3">
+        <button type="button"
+                id="mark-all-notifications-read"
+                class="btn btn-outline-primary"
+                @disabled(auth()->user()->unreadUserNotifications()->count() === 0)>
+            Прочитано все
+        </button>
+    </div>
+
     @include('partials.table', [
         'id'      => $id,
         'header'  => $header,
@@ -20,6 +29,26 @@
             const safeCount = Math.max(0, parseInt(count || 0, 10));
             badge.dataset.count = String(safeCount);
             badge.classList.toggle('d-none', safeCount === 0);
+
+            const markAllButton = document.getElementById('mark-all-notifications-read');
+            if (markAllButton) {
+                markAllButton.disabled = safeCount === 0;
+            }
+        }
+
+        function applyReadState($row, readAt) {
+            if (!$row || !$row.length) {
+                return;
+            }
+
+            $row.removeClass('notification-unread');
+            const typeClass = $row.data('read-class');
+            if (typeClass) $row.addClass(typeClass);
+            $row.data('notification-read', '1');
+
+            if (readAt) {
+                $row.find('.column_read_at').text(readAt);
+            }
         }
 
         async function markNotificationRead(id, $row) {
@@ -36,17 +65,36 @@
                 if (typeof data.unread !== 'undefined') {
                     updateNotificationBadge(data.unread);
                 }
-                if ($row) {
-                    $row.removeClass('notification-unread');
-                    const typeClass = $row.data('read-class');
-                    if (typeClass) $row.addClass(typeClass);
-                    $row.data('notification-read', '1');
-
-                    if (data.read_at) {
-                        $row.find('.column_read_at').text(data.read_at);
-                    }
-                }
+                applyReadState($row, data.read_at);
+            }
+        }
+
+        async function markAllNotificationsRead() {
+            const response = await fetch(`{{ route('notifications.read-all') }}`, {
+                method: 'POST',
+                headers: {
+                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
+                    'Accept': 'application/json',
+                },
+            });
+
+            if (!response.ok) {
+                return;
+            }
+
+            const data = await response.json();
+            if (typeof data.unread !== 'undefined') {
+                updateNotificationBadge(data.unread);
             }
+
+            $('#tbl tbody tr[data-notification-id]').each(function () {
+                const $row = $(this);
+                const isRead = $row.data('notification-read') === '1' || $row.data('notification-read') === 1;
+
+                if (!isRead) {
+                    applyReadState($row, data.read_at);
+                }
+            });
         }
 
         $(document).on('dblclick', '#tbl tbody tr[data-notification-id]', function (e) {
@@ -59,5 +107,13 @@
                 markNotificationRead(id, $row);
             }
         });
+
+        $(document).on('click', '#mark-all-notifications-read', function () {
+            if (this.disabled) {
+                return;
+            }
+
+            markAllNotificationsRead();
+        });
     </script>
 @endpush

+ 1 - 0
routes/web.php

@@ -133,6 +133,7 @@ Route::middleware('auth:web')->group(function () {
     Route::delete('profile', [UserController::class, 'deleteProfile'])->name('profile.delete');
     Route::post('impersonate/leave', [UserController::class, 'leaveImpersonation'])->name('user.impersonate.leave');
     Route::get('notifications', [UserNotificationController::class, 'index'])->name('notifications.index');
+    Route::post('notifications/read-all', [UserNotificationController::class, 'markAllRead'])->name('notifications.read-all');
     Route::post('notifications/{notification}/read', [UserNotificationController::class, 'markRead'])->name('notifications.read');
     Route::get('notifications/unread/count', [UserNotificationController::class, 'unreadCount'])->name('notifications.unread-count');
 

+ 61 - 0
tests/Feature/UserNotificationControllerTest.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\Models\User;
+use App\Models\UserNotification;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Tests\TestCase;
+
+class UserNotificationControllerTest extends TestCase
+{
+    use RefreshDatabase;
+
+    public function test_user_can_mark_all_own_notifications_as_read(): void
+    {
+        $user = User::factory()->create();
+        $otherUser = User::factory()->create();
+
+        $unreadNotifications = collect([
+            $this->createNotification($user->id),
+            $this->createNotification($user->id),
+        ]);
+
+        $alreadyReadAt = now()->subDay();
+        $alreadyReadNotification = $this->createNotification($user->id, $alreadyReadAt);
+        $otherUserNotification = $this->createNotification($otherUser->id);
+
+        $response = $this->actingAs($user)->postJson(route('notifications.read-all'));
+
+        $response
+            ->assertOk()
+            ->assertJson([
+                'ok' => true,
+                'updated' => 2,
+                'unread' => 0,
+            ]);
+
+        foreach ($unreadNotifications as $notification) {
+            $this->assertNotNull($notification->fresh()->read_at);
+        }
+
+        $this->assertSame(
+            $alreadyReadAt->format('Y-m-d H:i:s'),
+            $alreadyReadNotification->fresh()->read_at?->format('Y-m-d H:i:s')
+        );
+        $this->assertNull($otherUserNotification->fresh()->read_at);
+    }
+
+    private function createNotification(int $userId, $readAt = null): UserNotification
+    {
+        return UserNotification::query()->create([
+            'user_id' => $userId,
+            'type' => UserNotification::TYPE_PLATFORM,
+            'event' => UserNotification::EVENT_CREATED,
+            'title' => 'Тестовое уведомление',
+            'message' => 'Тестовое сообщение',
+            'message_html' => '<p>Тестовое сообщение</p>',
+            'read_at' => $readAt,
+        ]);
+    }
+}