diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6dc9801..a473059 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: "8.3" + php-version: "8.4" - run: composer install --no-interaction --prefer-dist - run: ./vendor/bin/pint --test diff --git a/app/Http/Controllers/NotificationController.php b/app/Http/Controllers/NotificationController.php new file mode 100644 index 0000000..c5e399a --- /dev/null +++ b/app/Http/Controllers/NotificationController.php @@ -0,0 +1,77 @@ +service->getUserNotifications( + user: $request->user(), + filters: [ + 'read' => $request->has('read') + ? $request->boolean('read') + : null, + 'type' => $request->input('type'), + ], + pagination: 5 + ); + + return response()->json($notifications); + } + + public function unreadCount(Request $request): array + { + return [ + 'count' => $this->service->getUnreadCount( + $request->user() + ), + ]; + } + + public function markAsRead( + Request $request, + string $id + ): RedirectResponse { + $success = $this->service->markAsRead( + $request->user(), + $id + ); + + if (!$success) { + return back()->with( + 'error', + 'Notification not found.' + ); + } + + return back()->with( + 'success', + 'Notification marked as read.' + ); + } + + public function markAllAsRead( + Request $request + ): RedirectResponse { + $this->service->markAllAsRead( + $request->user() + ); + + return back()->with( + 'success', + 'All notifications marked as read.' + ); + } +} \ No newline at end of file diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 140c03f..ace6bf5 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -39,18 +39,30 @@ public function share(Request $request): array 'currentRoute' => Route::currentRouteName(), ], 'notifications' => function () use ($request) { - if (! $request->user()) { + if (!$request->user()) { return []; } return $request->user() - ->unreadNotifications + ->unreadNotifications() + ->latest() + ->limit(5) + ->get() ->map(fn ($notification) => [ 'id' => $notification->id, 'data' => $notification->data, 'created_at' => $notification->created_at, ]); }, + 'allUnreadCount' => function () use ($request) { + if (!$request->user()) { + return 0; + } + + return $request->user() + ->unreadNotifications() + ->count(); + }, ]; } } diff --git a/app/Services/NotificationService.php b/app/Services/NotificationService.php new file mode 100644 index 0000000..9f9b175 --- /dev/null +++ b/app/Services/NotificationService.php @@ -0,0 +1,71 @@ +notifications()->latest(); + + if (isset($filters['read'])) { + $filters['read'] + ? $query->whereNotNull('read_at') + : $query->whereNull('read_at'); + } + + if (!empty($filters['type'])) { + $query->where('type', $filters['type']); + } + + $notifications = $query->paginate($pagination); + + return [ + 'userNotifications' => $notifications->items(), + 'page' => $notifications->currentPage(), + 'pageCount' => $notifications->lastPage(), + 'total' => $notifications->total(), + 'unreadNotificationsCount' => $user->unreadNotifications()->count(), + ]; + } + + public function getUnreadCount(User $user): int + { + return $user->unreadNotifications()->count(); + } + + public function markAsRead(User $user, string $notificationId): bool + { + $notification = $this->findNotification($user, $notificationId); + + if (!$notification) { + return false; + } + + if (!$notification->read_at) { + $notification->markAsRead(); + } + + return true; + } + + public function markAllAsRead(User $user): void + { + $user->unreadNotifications->markAsRead(); + } + + private function findNotification( + User $user, + string $notificationId + ): ?DatabaseNotification { + return $user->notifications() + ->where('id', $notificationId) + ->first(); + } +} \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 40a416c..ba59ea8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,6 +3,7 @@ use App\Http\Controllers\DocumentController; use App\Http\Controllers\GroupController; use App\Http\Controllers\NoticeController; +use App\Http\Controllers\NotificationController; use App\Http\Controllers\OpeningController; use App\Http\Controllers\ProfileController; use App\Http\Controllers\ProjectController; @@ -68,6 +69,12 @@ Route::get('editais/{notice}/projetos/{project}', [ProjectController::class, 'projectDetail']) ->scopeBindings() ->name('notices.projects.show'); + Route::prefix('notificacoes')->group(function () { + Route::get('/', [NotificationController::class, 'index'])->name('notifications.index'); + Route::get('/nao-lidas', [NotificationController::class, 'unreadCount'])->name('notifications.unread-count'); + Route::patch('/{id}/ler', [NotificationController::class, 'markAsRead'])->name('notifications.mark-read'); + Route::patch('/ler-todas', [NotificationController::class, 'markAllAsRead'])->name('notifications.mark-all-read'); + }); }); Route::middleware('auth')->group(function () { diff --git a/tests/Feature/NotificationServiceTest.php b/tests/Feature/NotificationServiceTest.php new file mode 100644 index 0000000..aeab2bc --- /dev/null +++ b/tests/Feature/NotificationServiceTest.php @@ -0,0 +1,231 @@ +service = app(NotificationService::class); + } + + private function createNotification( + User $user, + array $attributes = [] + ): DatabaseNotification { + return DatabaseNotification::create(array_merge([ + 'id' => (string) Str::uuid(), + 'type' => 'App\Notifications\TestNotification', + 'notifiable_type' => User::class, + 'notifiable_id' => $user->id, + 'data' => ['message' => 'Test notification'], + 'read_at' => null, + ], $attributes)); + } + + #[Test] + public function test_it_gets_paginated_notifications(): void + { + $user = User::factory()->create(); + + foreach (range(1, 10) as $i) { + $this->createNotification($user); + } + + $result = $this->service->getUserNotifications( + user: $user, + pagination: 5 + ); + + $this->assertCount(5, $result['userNotifications']); + $this->assertEquals(1, $result['page']); + $this->assertEquals(2, $result['pageCount']); + $this->assertEquals(10, $result['total']); + $this->assertEquals(10, $result['unreadNotificationsCount']); + } + + #[Test] + public function test_it_filters_unread_notifications(): void + { + $user = User::factory()->create(); + + $this->createNotification($user, [ + 'read_at' => null, + ]); + + $this->createNotification($user, [ + 'read_at' => now(), + ]); + + $result = $this->service->getUserNotifications( + user: $user, + filters: ['read' => false] + ); + + $this->assertCount(1, $result['userNotifications']); + $this->assertNull($result['userNotifications'][0]->read_at); + } + + #[Test] + public function test_it_filters_read_notifications(): void + { + $user = User::factory()->create(); + + $this->createNotification($user, [ + 'read_at' => null, + ]); + + $this->createNotification($user, [ + 'read_at' => now(), + ]); + + $result = $this->service->getUserNotifications( + user: $user, + filters: ['read' => true] + ); + + $this->assertCount(1, $result['userNotifications']); + $this->assertNotNull($result['userNotifications'][0]->read_at); + } + + #[Test] + public function test_it_filters_notifications_by_type(): void + { + $user = User::factory()->create(); + + $this->createNotification($user, [ + 'type' => 'App\Notifications\OrderNotification', + ]); + + $this->createNotification($user, [ + 'type' => 'App\Notifications\MessageNotification', + ]); + + $result = $this->service->getUserNotifications( + user: $user, + filters: [ + 'type' => 'App\Notifications\OrderNotification', + ] + ); + + $this->assertCount(1, $result['userNotifications']); + + $this->assertEquals( + 'App\Notifications\OrderNotification', + $result['userNotifications'][0]->type + ); + } + + #[Test] + public function test_it_gets_unread_count(): void + { + $user = User::factory()->create(); + + foreach (range(1, 3) as $i) { + $this->createNotification($user, [ + 'read_at' => null, + ]); + } + + foreach (range(1, 2) as $i) { + $this->createNotification($user, [ + 'read_at' => now(), + ]); + } + + $count = $this->service->getUnreadCount($user); + + $this->assertEquals(3, $count); + } + + #[Test] + public function test_it_marks_notification_as_read(): void + { + $user = User::factory()->create(); + + $notification = $this->createNotification($user, [ + 'read_at' => null, + ]); + + $result = $this->service->markAsRead( + user: $user, + notificationId: $notification->id + ); + + $notification->refresh(); + + $this->assertTrue($result); + $this->assertNotNull($notification->read_at); + } + + #[Test] + public function test_it_returns_false_when_notification_does_not_exist(): void + { + $user = User::factory()->create(); + + $result = $this->service->markAsRead( + user: $user, + notificationId: (string) Str::uuid() + ); + + $this->assertFalse($result); + } + + #[Test] + public function test_it_marks_all_notifications_as_read(): void + { + $user = User::factory()->create(); + + foreach (range(1, 3) as $i) { + $this->createNotification($user, [ + 'read_at' => null, + ]); + } + + $this->service->markAllAsRead($user); + + $this->assertEquals( + 0, + $user->fresh()->unreadNotifications()->count() + ); + } + + #[Test] + public function test_it_does_not_change_already_read_notification(): void + { + $user = User::factory()->create(); + + $readAt = Carbon::now()->subDay(); + + $notification = $this->createNotification($user, [ + 'read_at' => $readAt, + ]); + + $this->service->markAsRead( + user: $user, + notificationId: $notification->id + ); + + $notification->refresh(); + + $this->assertEquals( + $readAt->timestamp, + $notification->read_at->timestamp + ); + } +}