diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0e67d5e1..09a203e3 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,11 @@ { "permissions": { "allow": [ - "PowerShell(netstat *)" + "PowerShell(netstat *)", + "Bash(git add *)", + "Bash(git commit -m ' *)", + "Bash(git push *)", + "Bash(php artisan *)" ] } } diff --git a/app/Events/NewNotification.php b/app/Events/NewNotification.php new file mode 100644 index 00000000..84771bd9 --- /dev/null +++ b/app/Events/NewNotification.php @@ -0,0 +1,40 @@ +userId)]; + } + + public function broadcastWith(): array + { + return [ + 'title' => $this->title, + 'body' => $this->body, + 'icon' => $this->icon, + 'color' => $this->color, + 'url' => $this->url, + 'time' => now()->format('H:i'), + ]; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ce4e4b08..f584ed63 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -1,36 +1,30 @@ message->getHeaders()->addTextHeader( + 'List-Unsubscribe', + ', ' + ); + $event->message->getHeaders()->addTextHeader( + 'List-Unsubscribe-Post', + 'List-Unsubscribe=One-Click' + ); + }); } - - /** - * Bootstrap any application services. - */ - public function boot(): void -{ - Event::listen(MessageSending::class, function ($event) { - - $event->message->getHeaders()->addTextHeader( - 'List-Unsubscribe', - ', ' - ); - - $event->message->getHeaders()->addTextHeader( - 'List-Unsubscribe-Post', - 'List-Unsubscribe=One-Click' - ); - }); -} } diff --git a/app/View/Composers/HeaderComposer.php b/app/View/Composers/HeaderComposer.php new file mode 100644 index 00000000..7bdb43b6 --- /dev/null +++ b/app/View/Composers/HeaderComposer.php @@ -0,0 +1,47 @@ +check()) { + return; + } + + $userId = auth()->id(); + + // Últimas conversaciones con mensajes no leídos + $unreadConversations = Message::where('receiver_id', $userId) + ->whereNull('read_at') + ->with('sender') + ->orderBy('created_at', 'desc') + ->get() + ->unique('user_id') + ->take(5); + + $unreadMessagesCount = Message::where('receiver_id', $userId) + ->whereNull('read_at') + ->count(); + + // Notificaciones no leídas + $unreadNotifications = auth()->user() + ->unreadNotifications() + ->latest() + ->take(5) + ->get(); + + $unreadNotificationsCount = auth()->user()->unreadNotifications()->count(); + + $view->with([ + 'headerUnreadConversations' => $unreadConversations, + 'headerUnreadMessagesCount' => $unreadMessagesCount, + 'headerUnreadNotifications' => $unreadNotifications, + 'headerUnreadNotificationsCount' => $unreadNotificationsCount, + ]); + } +} diff --git a/database/migrations/2026_04_26_141319_create_notifications_table.php b/database/migrations/2026_04_26_141319_create_notifications_table.php new file mode 100644 index 00000000..d7380322 --- /dev/null +++ b/database/migrations/2026_04_26_141319_create_notifications_table.php @@ -0,0 +1,31 @@ +uuid('id')->primary(); + $table->string('type'); + $table->morphs('notifiable'); + $table->text('data'); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('notifications'); + } +}; diff --git a/resources/views/layouts/_partials/header.blade.php b/resources/views/layouts/_partials/header.blade.php index e076a380..cac320d3 100644 --- a/resources/views/layouts/_partials/header.blade.php +++ b/resources/views/layouts/_partials/header.blade.php @@ -4,13 +4,12 @@ + + diff --git a/routes/channels.php b/routes/channels.php index 05c9854b..1f6f4f34 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -9,3 +9,7 @@ Broadcast::channel('App.Models.User.{id}', function ($user, $id) { Broadcast::channel('chat.{receiverId}', function ($user, $receiverId) { return (int) $user->id === (int) $receiverId; }); + +Broadcast::channel('notifications.{userId}', function ($user, $userId) { + return (int) $user->id === (int) $userId; +}); diff --git a/routes/web.php b/routes/web.php index 62116bde..8bff89d6 100644 --- a/routes/web.php +++ b/routes/web.php @@ -56,7 +56,9 @@ Route::middleware(['auth:sanctum',config('jetstream.auth_session'),'verified', Route::get('/chat', [ChatController::class, 'index'])->name('chat.index'); Route::get('/chat/{user}', [ChatController::class, 'show'])->name('chat.show'); Route::post('/chat/send', [ChatController::class, 'send'])->name('chat.send'); - Route::get('/files/{path}', [FileController::class, 'serve'])->where('path', '.*')->name('files.serve');Route::resource('/docente',DocenteController::class); + Route::get('/files/{path}', [FileController::class, 'serve'])->where('path', '.*')->name('files.serve'); + Route::post('/notifications/{id}/read', fn($id) => auth()->user()->notifications()->findOrFail($id)->markAsRead())->name('notifications.read'); + Route::post('/notifications/read-all', fn() => auth()->user()->unreadNotifications->markAsRead())->name('notifications.read-all');Route::resource('/docente',DocenteController::class); Route::resource('/documento',DocumentoController::class); Route::get('/documento/ver/{id}', function ($id) { $doc = Documento::findOrFail($id); $alumno = auth()->user()->alumnos()->first(); if (!$alumno || $doc->alumno_id !== $alumno->id) { abort(403); } return response()->file( storage_path('app/public/'.$doc->archivo) ); }); Route::get('/documento/ver/{id}', [DocumentoController::class, 'ver'])->name('documento.ver');