Browse Source

✨ feat(notification): adicionar módulo de notificações com envio a usuários

Fase: dev | Origin: melhoria-interna
Gustavo Zanatta 1 tuần trước cách đây
mục cha
commit
8c50db9553

+ 55 - 0
app/Http/Controllers/NotificationController.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\NotificationRequest;
+use App\Http\Resources\NotificationResource;
+use App\Http\Resources\NotificationSendResource;
+use App\Services\NotificationService;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Support\Facades\Auth;
+
+class NotificationController extends Controller
+{
+    public function __construct(protected NotificationService $service) {}
+
+    public function index(): JsonResponse
+    {
+        $items = $this->service->getAll();
+        return $this->successResponse(payload: NotificationResource::collection($items));
+    }
+
+    public function store(NotificationRequest $request): JsonResponse
+    {
+        $item = $this->service->create($request->validated());
+        return $this->successResponse(
+            payload: new NotificationResource($item),
+            message: __('messages.created'),
+            code: 201,
+        );
+    }
+
+    public function show(int $id): JsonResponse
+    {
+        $item = $this->service->findById($id);
+        return $this->successResponse(payload: new NotificationResource($item));
+    }
+
+    public function destroy(int $id): JsonResponse
+    {
+        $this->service->delete($id);
+        return $this->successResponse(message: __('messages.deleted'), code: 204);
+    }
+
+    public function myUnread(): JsonResponse
+    {
+        $items = $this->service->getUnreadByUser(Auth::id());
+        return $this->successResponse(payload: NotificationSendResource::collection($items));
+    }
+
+    public function markAsRead(int $sendId): JsonResponse
+    {
+        $this->service->markAsRead($sendId, Auth::id());
+        return $this->successResponse(message: __('messages.updated'));
+    }
+}

+ 33 - 0
app/Http/Requests/NotificationRequest.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace App\Http\Requests;
+
+use App\Enums\NotificationRecipientEnum;
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+
+class NotificationRequest extends FormRequest
+{
+    public function rules(): array
+    {
+        $rules = [
+            'type'               => 'sometimes|string|max:50',
+            'source'             => 'sometimes|nullable|string|max:100',
+            'source_id'          => 'sometimes|nullable|integer',
+            'title'              => 'sometimes|string|max:255',
+            'message'            => 'sometimes|string',
+            'recipient'          => ['sometimes', Rule::enum(NotificationRecipientEnum::class)],
+            'recipient_position' => 'sometimes|nullable|string|max:100',
+            'recipient_sector'   => 'sometimes|nullable|string|max:100',
+        ];
+
+        if ($this->isMethod('post')) {
+            $rules['type']      = 'required|string|max:50';
+            $rules['title']     = 'required|string|max:255';
+            $rules['message']   = 'required|string';
+            $rules['recipient'] = ['required', Rule::enum(NotificationRecipientEnum::class)];
+        }
+
+        return $rules;
+    }
+}

+ 29 - 0
app/Http/Resources/NotificationResource.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Carbon\Carbon;
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class NotificationResource extends JsonResource
+{
+    public function toArray(Request $request): array
+    {
+        return [
+            'id'                 => $this->id,
+            'type'               => $this->type,
+            'source'             => $this->source,
+            'source_id'          => $this->source_id,
+            'title'              => $this->title,
+            'message'            => $this->message,
+            'recipient'          => $this->recipient,
+            'recipient_position' => $this->recipient_position,
+            'recipient_sector'   => $this->recipient_sector,
+            'created_by'         => $this->created_by,
+            'created_by_user'    => $this->whenLoaded('createdBy', fn() => new UserResource($this->createdBy)),
+            'created_at'         => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
+            'updated_at'         => Carbon::parse($this->updated_at)->format('Y-m-d H:i:s'),
+        ];
+    }
+}

+ 23 - 0
app/Http/Resources/NotificationSendResource.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Carbon\Carbon;
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class NotificationSendResource extends JsonResource
+{
+    public function toArray(Request $request): array
+    {
+        return [
+            'id'              => $this->id,
+            'notification_id' => $this->notification_id,
+            'notification'    => $this->whenLoaded('notification', fn() => new NotificationResource($this->notification)),
+            'user_id'         => $this->user_id,
+            'read'            => $this->read,
+            'read_at'         => $this->read_at?->format('Y-m-d H:i:s'),
+            'created_at'      => Carbon::parse($this->created_at)->format('Y-m-d H:i:s'),
+        ];
+    }
+}

+ 35 - 0
app/Models/Notification.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace App\Models;
+
+use App\Enums\NotificationRecipientEnum;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+class Notification extends Model
+{
+    use SoftDeletes;
+
+    protected $table = 'custom_notifications';
+
+    protected $guarded = ['id'];
+
+    protected function casts(): array
+    {
+        return [
+            'recipient' => NotificationRecipientEnum::class,
+        ];
+    }
+
+    public function createdBy(): BelongsTo
+    {
+        return $this->belongsTo(User::class, 'created_by');
+    }
+
+    public function sends(): HasMany
+    {
+        return $this->hasMany(NotificationSend::class);
+    }
+}

+ 29 - 0
app/Models/NotificationSend.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+class NotificationSend extends Model
+{
+    protected $guarded = ['id'];
+
+    protected function casts(): array
+    {
+        return [
+            'read'    => 'boolean',
+            'read_at' => 'datetime',
+        ];
+    }
+
+    public function notification(): BelongsTo
+    {
+        return $this->belongsTo(Notification::class);
+    }
+
+    public function user(): BelongsTo
+    {
+        return $this->belongsTo(User::class);
+    }
+}

+ 102 - 0
app/Services/NotificationService.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace App\Services;
+
+use App\Enums\NotificationRecipientEnum;
+use App\Enums\UserTypeEnum;
+use App\Models\Notification;
+use App\Models\NotificationSend;
+use App\Models\User;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\DB;
+
+class NotificationService
+{
+    public function getAll(): Collection
+    {
+        return Notification::with('createdBy')->orderBy('created_at', 'desc')->get();
+    }
+
+    public function findById(int $id): ?Notification
+    {
+        return Notification::with(['createdBy', 'sends'])->find($id);
+    }
+
+    public function create(array $data): Notification
+    {
+        return DB::transaction(function () use ($data) {
+            $data['created_by'] = Auth::id();
+            $notification = Notification::create($data);
+
+            $this->dispatchSends($notification);
+
+            return $notification;
+        });
+    }
+
+    public function getUnreadByUser(int $userId): Collection
+    {
+        return NotificationSend::with('notification')
+            ->where('user_id', $userId)
+            ->where('read', false)
+            ->orderBy('created_at', 'desc')
+            ->get();
+    }
+
+    public function markAsRead(int $sendId, int $userId): bool
+    {
+        $send = NotificationSend::where('id', $sendId)
+            ->where('user_id', $userId)
+            ->first();
+
+        if (!$send) {
+            return false;
+        }
+
+        $send->update(['read' => true, 'read_at' => now()]);
+        return true;
+    }
+
+    public function delete(int $id): bool
+    {
+        $model = Notification::find($id);
+
+        if (!$model) {
+            return false;
+        }
+
+        return $model->delete();
+    }
+
+    private function dispatchSends(Notification $notification): void
+    {
+        $query = User::query();
+
+        match ($notification->recipient) {
+            NotificationRecipientEnum::ASSOCIADO => $query->where('type', UserTypeEnum::ASSOCIADO->value),
+            NotificationRecipientEnum::PARCEIRO  => $query->where('type', UserTypeEnum::PARCEIRO->value),
+            default                              => null,
+        };
+
+        if ($notification->recipient_position) {
+            $query->whereHas('position', fn($q) => $q->where('name', $notification->recipient_position));
+        }
+
+        if ($notification->recipient_sector) {
+            $query->whereHas('sector', fn($q) => $q->where('name', $notification->recipient_sector));
+        }
+
+        $users = $query->pluck('id');
+
+        $sends = $users->map(fn($userId) => [
+            'notification_id' => $notification->id,
+            'user_id'         => $userId,
+            'read'            => false,
+            'created_at'      => now(),
+            'updated_at'      => now(),
+        ])->toArray();
+
+        NotificationSend::insert($sends);
+    }
+}

+ 35 - 0
database/migrations/2025_01_01_000008_create_custom_notifications_table.php

@@ -0,0 +1,35 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::create('custom_notifications', function (Blueprint $table) {
+            $table->id();
+            $table->string('type');
+            $table->string('source')->nullable();
+            $table->unsignedBigInteger('source_id')->nullable();
+            $table->string('title');
+            $table->text('message');
+            $table->string('recipient');
+            $table->string('recipient_position')->nullable();
+            $table->string('recipient_sector')->nullable();
+            $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
+            $table->timestamps();
+            $table->softDeletes();
+
+            $table->index(['source', 'source_id']);
+            $table->index('recipient');
+            $table->index('created_by');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::dropIfExists('custom_notifications');
+    }
+};

+ 29 - 0
database/migrations/2025_01_01_000009_create_notification_sends_table.php

@@ -0,0 +1,29 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::create('notification_sends', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('notification_id')->constrained('custom_notifications')->cascadeOnDelete();
+            $table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
+            $table->boolean('read')->default(false);
+            $table->timestamp('read_at')->nullable();
+            $table->timestamps();
+
+            $table->unique(['notification_id', 'user_id']);
+            $table->index('user_id');
+            $table->index('read');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::dropIfExists('notification_sends');
+    }
+};

+ 18 - 0
routes/authRoutes/notification.php

@@ -0,0 +1,18 @@
+<?php
+
+use App\Http\Controllers\NotificationController;
+use Illuminate\Support\Facades\Route;
+
+Route::controller(NotificationController::class)->prefix('notification')->group(function () {
+    Route::get('/', 'index')->middleware('permission:notificacao,view');
+
+    Route::post('/', 'store')->middleware('permission:notificacao,add');
+
+    Route::get('/my/unread', 'myUnread')->middleware('permission:notificacao,view');
+
+    Route::patch('/{sendId}/read', 'markAsRead')->middleware('permission:notificacao,view');
+
+    Route::get('/{id}', 'show')->middleware('permission:notificacao,view');
+
+    Route::delete('/{id}', 'destroy')->middleware('permission:notificacao,delete');
+});