From dfb1fbf1e9b9122f2100d2ea889fadaef92ff468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s=20de=20Jes=C3=BAs=20Cort=C3=A9s=20Castellanos?= <96152034+notsoweb@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:10:56 -0600 Subject: [PATCH] ADD: Notificaciones en tiempo real (#3) * ADD: Notificaciones * ADD: Usuarios conectados en tiempo real --- .../System/NotificationController.php | 50 +++--- app/Http/Controllers/UserController.php | 2 +- app/Http/Traits/HasDatabaseNotifications.php | 39 +++++ app/Http/Traits/IsNotifiable.php | 13 ++ app/Models/Notification.php | 161 ++++++++++++++++++ app/Models/User.php | 4 +- .../ForgotPasswordNotification.php | 2 +- app/Notifications/UserNotification.php | 22 ++- app/Providers/AppServiceProvider.php | 2 +- ...2_13_135639_create_notifications_table.php | 5 + database/seeders/RoleSeeder.php | 4 +- database/seeders/UserSeeder.php | 9 + routes/api.php | 3 + routes/channels.php | 11 +- vite.config.js | 2 +- 15 files changed, 289 insertions(+), 40 deletions(-) create mode 100644 app/Http/Traits/HasDatabaseNotifications.php create mode 100644 app/Http/Traits/IsNotifiable.php create mode 100644 app/Models/Notification.php diff --git a/app/Http/Controllers/System/NotificationController.php b/app/Http/Controllers/System/NotificationController.php index f2b16cf..735072a 100755 --- a/app/Http/Controllers/System/NotificationController.php +++ b/app/Http/Controllers/System/NotificationController.php @@ -3,9 +3,9 @@ * @copyright 2024 Notsoweb (https://notsoweb.com) - All rights reserved. */ +use App\Models\Notification; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Notifications\DatabaseNotification; use Notsoweb\ApiResponse\Enums\ApiResponse; use Notsoweb\LaravelCore\Controllers\VueController; @@ -31,7 +31,7 @@ public function __construct() */ public function index() { - $q = request()->get('query'); + $q = request()->get('q'); $model = auth()->user() ->notifications(); @@ -41,45 +41,38 @@ public function index() ->orWhere('data->message', "LIKE", "%{$q}%"); } - return $this->view('index', [ + return ApiResponse::OK->response([ 'models' => $model ->paginate(config('app.pagination')) ]); } - /** - * Todas las notificaciones - */ - public function all(): JsonResponse - { - $query = request()->get('query'); - - $model = auth()->user() - ->notifications(); - - if($query) { - $model = $model->where('data->title', 'LIKE', "%{$query}%") - ->orWhere('data->message', "LIKE", "%{$query}%"); - } - - return ApiResponse::OK->axios([ - 'notifications' => $model - ->paginate(config('app.pagination')) - ]); - } - /** * Marcar notificación como leída */ public function read(Request $request): JsonResponse { - $notification = DatabaseNotification::find($request->get('id')); + $notification = Notification::find($request->get('id')); if ($notification) { $notification->markAsRead(); } - return ApiResponse::OK->axios(); + return ApiResponse::OK->response(); + } + + /** + * Marcar notificación como cerrada + */ + public function close(Request $request): JsonResponse + { + $notification = Notification::find($request->get('id')); + + if ($notification) { + $notification->markAsClosed(); + } + + return ApiResponse::OK->response(); } /** @@ -87,9 +80,10 @@ public function read(Request $request): JsonResponse */ public function allUnread(): JsonResponse { - return ApiResponse::OK->axios([ + return ApiResponse::OK->response([ 'total' => auth()->user()->unreadNotifications()->count(), - 'notifications' => auth()->user()->unreadNotifications()->limit(10)->get(), + 'unread_closed' => auth()->user()->unreadNotifications()->where('is_closed', true)->count(), + 'notifications' => auth()->user()->unreadNotifications()->where('is_closed', false)->limit(10)->get(), ]); } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index a7cb0f7..00d8dc7 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -33,7 +33,7 @@ public function index() QuerySupport::queryByKeys($users, ['name', 'email']); return ApiResponse::OK->response([ - 'users' => $users->paginate(config('app.pagination')) + 'models' => $users->paginate(config('app.pagination')) ]); } diff --git a/app/Http/Traits/HasDatabaseNotifications.php b/app/Http/Traits/HasDatabaseNotifications.php new file mode 100644 index 0000000..7aeeb73 --- /dev/null +++ b/app/Http/Traits/HasDatabaseNotifications.php @@ -0,0 +1,39 @@ + + */ +trait HasDatabaseNotifications +{ + /** + * Get the entity's notifications. + */ + public function notifications() + { + return $this->morphMany(Notification::class, 'notifiable')->with('user:id,name,paternal,maternal,profile_photo_path')->latest(); + } + + /** + * Get the entity's read notifications. + * + * @return \Illuminate\Database\Query\Builder + */ + public function readNotifications() + { + return $this->notifications()->read(); + } + + /** + * Get the entity's unread notifications. + * + * @return \Illuminate\Database\Query\Builder + */ + public function unreadNotifications() + { + return $this->notifications()->unread(); + } +} diff --git a/app/Http/Traits/IsNotifiable.php b/app/Http/Traits/IsNotifiable.php new file mode 100644 index 0000000..066e436 --- /dev/null +++ b/app/Http/Traits/IsNotifiable.php @@ -0,0 +1,13 @@ + + */ +trait IsNotifiable +{ + use HasDatabaseNotifications, RoutesNotifications; +} diff --git a/app/Models/Notification.php b/app/Models/Notification.php new file mode 100644 index 0000000..1382a39 --- /dev/null +++ b/app/Models/Notification.php @@ -0,0 +1,161 @@ + + */ +class Notification extends Model +{ + use HasCollection; + + /** + * The "type" of the primary key ID. + * + * @var string + */ + protected $keyType = 'string'; + + /** + * Indicates if the IDs are auto-incrementing. + * + * @var bool + */ + public $incrementing = false; + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'notifications'; + + /** + * The guarded attributes on the model. + * + * @var array + */ + protected $guarded = []; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'data' => 'array', + 'read_at' => 'datetime' + ]; + + /** + * + */ + protected static function boot() + { + parent::boot(); + + static::creating(fn($model) => $model->user_id = auth()?->user()?->id); + } + + /** + * The type of collection that should be used for the model. + */ + protected static string $collectionClass = DatabaseNotificationCollection::class; + + /** + * Get the notifiable entity that the notification belongs to. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, $this> + */ + public function notifiable() + { + return $this->morphTo(); + } + + /** + * Una notificación puede ser creada por un usuario + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Mark the notification as read. + * + * @return void + */ + public function markAsRead() + { + if (is_null($this->read_at)) { + $this->forceFill(['read_at' => $this->freshTimestamp()])->save(); + } + } + + /** + * Marcar notificación como cerrada + */ + public function markAsClosed() + { + $this->forceFill(['is_closed' => true])->save(); + } + + /** + * Mark the notification as unread. + * + * @return void + */ + public function markAsUnread() + { + if (! is_null($this->read_at)) { + $this->forceFill(['read_at' => null])->save(); + } + } + + /** + * Determine if a notification has been read. + * + * @return bool + */ + public function read() + { + return $this->read_at !== null; + } + + /** + * Determine if a notification has not been read. + * + * @return bool + */ + public function unread() + { + return $this->read_at === null; + } + + /** + * Scope a query to only include read notifications. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeRead(Builder $query) + { + return $query->whereNotNull('read_at'); + } + + /** + * Scope a query to only include unread notifications. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeUnread(Builder $query) + { + return $query->whereNull('read_at'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index f32efdd..e05a8be 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,10 +5,10 @@ // use Illuminate\Contracts\Auth\MustVerifyEmail; use App\Http\Traits\HasProfilePhoto; +use App\Http\Traits\IsNotifiable; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; -use Illuminate\Notifications\Notifiable; use Laravel\Passport\HasApiTokens; use Spatie\Permission\Traits\HasRoles; @@ -25,7 +25,7 @@ class User extends Authenticatable HasFactory, HasRoles, HasProfilePhoto, - Notifiable; + IsNotifiable; /** * Atributos permitidos diff --git a/app/Notifications/ForgotPasswordNotification.php b/app/Notifications/ForgotPasswordNotification.php index 9b5ac26..8829da6 100644 --- a/app/Notifications/ForgotPasswordNotification.php +++ b/app/Notifications/ForgotPasswordNotification.php @@ -9,7 +9,7 @@ use Illuminate\Notifications\Notification; /** - * Descripción + * Notificación de recuperación de contraseña * * @author Moisés Cortés C. * diff --git a/app/Notifications/UserNotification.php b/app/Notifications/UserNotification.php index 044fe27..e443ed1 100644 --- a/app/Notifications/UserNotification.php +++ b/app/Notifications/UserNotification.php @@ -1,9 +1,19 @@ + * + * @version 1.0.0 + */ class UserNotification extends Notification { use Queueable; @@ -17,6 +27,7 @@ public function __construct( public ?string $message = null, public string $type = 'info', public int $timeout = 20, + public bool $save = true, ) {} /** @@ -26,10 +37,13 @@ public function __construct( */ public function via(object $notifiable): array { - return [ - 'broadcast', - 'database' - ]; + $vias = ['broadcast']; + + if ($this->save) { + $vias[] = 'database'; + } + + return $vias; } /** diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 043e86e..9f7b833 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -22,7 +22,7 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - if ($this->app->environment('local')) { + if ($this->app->environment('local') && config('telescope.enabled') == 'true') { $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class); $this->app->register(TelescopeServiceProvider::class); } diff --git a/database/migrations/2024_12_13_135639_create_notifications_table.php b/database/migrations/2024_12_13_135639_create_notifications_table.php index d738032..52f065d 100644 --- a/database/migrations/2024_12_13_135639_create_notifications_table.php +++ b/database/migrations/2024_12_13_135639_create_notifications_table.php @@ -17,6 +17,11 @@ public function up(): void $table->morphs('notifiable'); $table->text('data'); $table->timestamp('read_at')->nullable(); + $table->boolean('is_closed')->default(false); + $table->foreignId('user_id') // Usuario que crea la notificación + ->nullable() + ->constrained('users') + ->nullOnDelete(); $table->timestamps(); }); } diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php index f785051..2a5b577 100644 --- a/database/seeders/RoleSeeder.php +++ b/database/seeders/RoleSeeder.php @@ -27,6 +27,7 @@ public function run(): void ] = $this->onCRUD('users', $users); $userSettings = $this->onPermission('users.settings', 'Configuración de usuarios', $users); + $userOnline = $this->onPermission('users.online', 'Usuarios en linea', $users); // Desarrollador Role::create([ @@ -43,7 +44,8 @@ public function run(): void $userCreate, $userEdit, $userDestroy, - $userSettings + $userSettings, + $userOnline ); } } diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index 69cf436..274cf68 100644 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -28,5 +28,14 @@ public function run(): void 'email' => $admin->email, 'password' => $admin->hash, ])->assignRole(__('admin')); + + $demo = UserSecureSupport::create('demo@notsoweb.com'); + + User::create([ + 'name' => 'Demo', + 'paternal' => 'Notsoweb', + 'email' => $demo->email, + 'password' => $demo->hash, + ]); } } diff --git a/routes/api.php b/routes/api.php index 2fd835f..cd0dc30 100644 --- a/routes/api.php +++ b/routes/api.php @@ -43,8 +43,10 @@ Route::get('permissions', [SystemController::class, 'permissions'])->name('permissions'); Route::get('roles', [SystemController::class, 'roles'])->name('roles'); Route::prefix('notifications')->name('notifications.')->group(function() { + Route::get('all', [NotificationController::class, 'index'])->name('all'); Route::get('all-unread', [NotificationController::class, 'allUnread'])->name('all-unread'); Route::post('read', [NotificationController::class, 'read'])->name('read'); + Route::post('close', [NotificationController::class, 'close'])->name('close'); }); }); @@ -61,3 +63,4 @@ Route::post('forgot-password', [LoginController::class, 'forgotPassword'])->name('forgot-password'); Route::post('reset-password', [LoginController::class, 'resetPassword'])->name('reset-password'); }); + diff --git a/routes/channels.php b/routes/channels.php index 81a4ce0..2e379e9 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -1,9 +1,18 @@ -id === (int) $id; }); +// Usuarios en linea +Broadcast::channel('online', function ($user) { + return $user; +}); + +// Notificación global Broadcast::channel('Global', function ($user) { return $user->id !== null; }); \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 421b569..6bc7b53 100644 --- a/vite.config.js +++ b/vite.config.js @@ -6,6 +6,6 @@ export default defineConfig({ laravel({ input: ['resources/css/app.css', 'resources/js/app.js'], refresh: true, - }), + }) ], });