Преглед на файлове

Merge branch 'feature/DIARIA-kay-modulo-de-notificação' into development

Gustavo Zanatta преди 2 седмици
родител
ревизия
b652893f97

+ 29 - 0
app/Enums/NotificationTypeEnum.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Enums;
+
+enum NotificationTypeEnum: string
+{
+
+    // Notificação cliente
+    case SCHEDULE_CLIENT_PROVIDER_ACCEPTED = 'schedule_client_provider_accepted';
+
+    case SCHEDULE_CLIENT_PROVIDER_REFUSED = 'schedule_client_provider_refused';
+
+    case SCHEDULE_CLIENT_PROVIDER_CANCELLED = 'schedule_client_provider_cancelled';
+
+    case SCHEDULE_CLIENT_PROVIDER_COMING = 'schedule_client_provider_coming';
+
+    case SCHEDULE_CLIENT_PROVIDER_FINISHED = 'schedule_client_provider_finished';
+
+
+
+        // Notificação PRESTADO
+    case SCHEDULE_PROVIDER_CLIENT_NEW_SOLICITATION = 'schedule_provider_client_new_solicitation';
+
+    case SCHEDULE_PROVIDER_START = 'schedule_provider_start';
+
+    case SCHEDULE_PROVIDER_CLIENT_CANCELLED = 'schedule_provider_client_cancelled';
+
+    case SCHEDULE_PROVIDER_CLIENT_PROPOSAL_ACCEPTED = 'schedule_provider_client_proposal_accepted';
+}

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

@@ -0,0 +1,80 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\Notification;
+use Carbon\Carbon;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Support\Facades\Auth;
+
+class NotificationController extends Controller
+{
+    public function index(): JsonResponse
+    {
+        $user = Auth::user();
+
+        $notifications = Notification::where('user_id', $user->id)
+            ->orderBy('read', 'asc')
+            ->orderBy('created_at', 'desc')
+            ->limit(50)
+            ->get()
+            ->map(function ($notification) {
+
+                return [
+                    'id' => $notification->id,
+
+                    'title' => $notification->title,
+
+                    'description' => $notification->description,
+
+                    'origin' => $notification->origin,
+
+                    'origin_id' => $notification->origin_id,
+
+                    'type' => $notification->type,
+
+                    'read' => $notification->read,
+
+                    'time' => Carbon::parse(
+                        $notification->created_at
+                    )->diffForHumans(),
+                ];
+            });
+
+        return $this->successResponse(
+            payload: $notifications
+        );
+    }
+
+    public function markAsRead(int $id): JsonResponse
+    {
+        $notification = Notification::where('id', $id)
+            ->where('user_id', Auth::id())
+            ->firstOrFail();
+
+        $notification->update([
+            'read' => true,
+
+            'read_at' => now(),
+        ]);
+
+        return $this->successResponse(
+            message: __('messages.updated')
+        );
+    }
+
+    public function markAllAsRead(): JsonResponse
+    {
+        Notification::where('user_id', Auth::id())
+            ->where('read', false)
+            ->update([
+                'read' => true,
+
+                'read_at' => now(),
+            ]);
+
+        return $this->successResponse(
+            message: __('messages.updated')
+        );
+    }
+}

+ 1 - 0
app/Http/Resources/DashboardClienteResource.php

@@ -24,6 +24,7 @@ class DashboardClienteResource extends JsonResource
             'providersClose'      => $this['providersClose'],
             'schedulesProposals'  => $this['schedulesProposals'],
             'todaySchedules'      => $this['todaySchedules'],
+            'notifications' => $this['notifications'],
             'has_payment_methods' => $this['has_payment_methods'],
         ];
     }

+ 1 - 0
app/Http/Resources/DashboardPrestadorResource.php

@@ -22,6 +22,7 @@ class DashboardPrestadorResource extends JsonResource
             'solicitations'  => $this['solicitations'],
             'nextSchedules'  => $this['nextSchedules'],
             'opportunities'  => $this['opportunities'],
+            'notifications' => $this['notifications'],
         ];
     }
 }

+ 36 - 0
app/Models/Notification.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+class Notification extends Model
+{
+    use HasFactory, SoftDeletes;
+
+    protected $fillable = [
+        'title',
+        'description',
+        'origin',
+        'origin_id',
+        'type',
+        'read',
+        'read_at',
+        'user_id',
+    ];
+
+    protected $casts = [
+        'read' => 'boolean',
+        'read_at' => 'datetime',
+        'created_at' => 'datetime',
+        'updated_at' => 'datetime',
+        'deleted_at' => 'datetime',
+    ];
+
+    public function user()
+    {
+        return $this->belongsTo(User::class);
+    }
+}

+ 22 - 3
app/Services/CustomScheduleService.php

@@ -15,6 +15,8 @@ use Carbon\Carbon;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 use Illuminate\Support\Facades\Storage;
+use App\Enums\NotificationTypeEnum;
+use App\Services\NotificationService;
 
 class CustomScheduleService
 {
@@ -108,7 +110,7 @@ class CustomScheduleService
             return $createdCustomSchedules;
         } catch (\Exception $e) {
             DB::rollBack();
-            Log::error('Error creating custom schedule: '.$e->getMessage());
+            Log::error('Error creating custom schedule: ' . $e->getMessage());
             throw $e;
         }
     }
@@ -200,7 +202,7 @@ class CustomScheduleService
             ]);
         } catch (\Exception $e) {
             DB::rollBack();
-            Log::error('Error updating custom schedule: '.$e->getMessage());
+            Log::error('Error updating custom schedule: ' . $e->getMessage());
             throw $e;
         }
     }
@@ -224,7 +226,7 @@ class CustomScheduleService
             return $customSchedule;
         } catch (\Exception $e) {
             DB::rollBack();
-            Log::error('Error deleting custom schedule: '.$e->getMessage());
+            Log::error('Error deleting custom schedule: ' . $e->getMessage());
             throw $e;
         }
     }
@@ -402,6 +404,23 @@ class CustomScheduleService
                 'status'      => 'accepted',
             ]);
 
+            $notificationService = app(NotificationService::class);
+
+            $notificationService->create([
+                'title' => 'Proposta aceita!',
+
+                'description' =>
+                'O cliente aceitou sua proposta de diária.',
+
+                'origin' => 'schedule',
+
+                'origin_id' => $schedule->id,
+
+                'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_PROPOSAL_ACCEPTED->value,
+
+                'user_id' => $provider->user_id,
+            ]);
+
             ScheduleProposal::where('schedule_id', $schedule->id)
                 ->where('id', '!=', $proposalId)
                 ->delete();

+ 38 - 1
app/Services/DashboardService.php

@@ -12,6 +12,7 @@ use App\Models\Review;
 use App\Models\Schedule;
 use App\Models\ScheduleProposal;
 use App\Models\Speciality;
+use App\Models\Notification;
 use App\Rules\ScheduleBusinessRules;
 use App\Services\DistanceService;
 use Illuminate\Support\Facades\Auth;
@@ -332,8 +333,26 @@ class DashboardService
                 return $item;
             });
 
+        $notifications = Notification::where('user_id', $user->id)
+            ->orderBy('read', 'asc')
+            ->orderBy('created_at', 'desc')
+            ->limit(10)
+            ->get()
+            ->map(function ($notification) {
+                return [
+                    'id' => $notification->id,
+                    'title' => $notification->title,
+                    'description' => $notification->description,
+                    'time' => $notification->created_at->diffForHumans(),
+                    'read' => $notification->read,
+                    'avatar' => '/icons/avatar.svg',
+                ];
+            });
+
         $hasPaymentMethods = ClientPaymentMethod::where('client_id', $cliente->id)->exists();
 
+
+
         return [
             'headerBar'           => $headerBar,
             'summaryInfos'        => $summaryInfos,
@@ -344,6 +363,7 @@ class DashboardService
             'providersClose'      => $providersClose,
             'todaySchedules'      => $todaySchedules,
             'schedulesProposals'  => $schedulesProposals,
+            'notifications' => $notifications,
             'has_payment_methods' => $hasPaymentMethods,
         ];
     }
@@ -378,7 +398,7 @@ class DashboardService
             ->pluck('speciality_id')
             ->all();
 
-        $specialities = $allSpecialities->map(fn ($sp) => [
+        $specialities = $allSpecialities->map(fn($sp) => [
             'id'             => $sp->id,
             'description'    => $sp->description,
             'has_speciality' => in_array($sp->id, $providerSpecialityIds),
@@ -586,6 +606,22 @@ class DashboardService
         //   ->orderBy('schedules.date', 'asc')
         //   ->get();
 
+        $notifications = Notification::where('user_id', $user->id)
+            ->orderBy('read', 'asc')
+            ->orderBy('created_at', 'desc')
+            ->limit(10)
+            ->get()
+            ->map(function ($notification) {
+                return [
+                    'id' => $notification->id,
+                    'title' => $notification->title,
+                    'description' => $notification->description,
+                    'time' => $notification->created_at->diffForHumans(),
+                    'read' => $notification->read,
+                    'avatar' => '/icons/avatar.svg',
+                ];
+            });
+
         $opportunities = $this->customScheduleService->getAvailableOpportunities($provider->id);
 
         $opportunities->each(function ($o) {
@@ -602,6 +638,7 @@ class DashboardService
             'todayServices'  => $todayServices,
             'nextSchedules'  => $nextSchedules,
             'opportunities'  => $opportunities,
+            'notifications' => $notifications,
         ];
     }
 

+ 59 - 0
app/Services/NotificationService.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\Notification;
+use Illuminate\Database\Eloquent\Collection;
+
+class NotificationService
+{
+    public function getByUser(int $userId): Collection
+    {
+        return Notification::where('user_id', $userId)
+            ->orderBy('read', 'asc')
+            ->orderBy('created_at', 'desc')
+            ->get();
+    }
+
+    public function findById(int $id): Notification
+    {
+        return Notification::findOrFail($id);
+    }
+
+    public function create(array $data): Notification
+    {
+        return Notification::create($data);
+    }
+
+    public function markAsRead(Notification $notification): Notification
+    {
+        $notification->update([
+            'read' => true,
+            'read_at' => now(),
+        ]);
+
+        return $notification;
+    }
+
+    public function markAllAsRead(int $userId): void
+    {
+        Notification::where('user_id', $userId)
+            ->where('read', false)
+            ->update([
+                'read' => true,
+                'read_at' => now(),
+            ]);
+    }
+
+    public function unreadCount(int $userId): int
+    {
+        return Notification::where('user_id', $userId)
+            ->where('read', false)
+            ->count();
+    }
+
+    public function delete(Notification $notification): void
+    {
+        $notification->delete();
+    }
+}

+ 205 - 8
app/Services/ScheduleService.php

@@ -6,6 +6,8 @@ use App\Jobs\StartScheduleJob;
 use App\Models\Provider;
 use App\Models\Schedule;
 use App\Rules\ScheduleBusinessRules;
+use App\Services\NotificationService;
+use App\Enums\NotificationTypeEnum;
 use Carbon\Carbon;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\DB;
@@ -41,22 +43,54 @@ class ScheduleService
     public function createSingleOrMultiple(array $baseData, array $schedules)
     {
         try {
+
             DB::beginTransaction();
+
             $createdSchedules = [];
 
+
             foreach ($schedules as $schedule) {
+
                 $datasMerged = array_merge($baseData, $schedule);
 
+
                 $this->validateProviderAvailability($datasMerged, null);
 
+
                 $scheduleData = array_merge($datasMerged, [
                     'code' => str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT),
                 ]);
 
-                $createdSchedules[] = Schedule::create($scheduleData);
+
+                $newSchedule = Schedule::create($scheduleData);
+
+                /*NOTIFICAÇÃO PRESTADOR*/
+                if ($newSchedule->provider_id) {
+
+                    $notificationService = app(NotificationService::class);
+
+                    $notificationService->create([
+                        'title' => 'Nova solicitação de diária!',
+
+                        'description' =>
+                        'Você recebeu uma nova solicitação de diária.',
+
+                        'origin' => 'schedule',
+
+                        'origin_id' => $newSchedule->id,
+
+                        'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_NEW_SOLICITATION->value,
+
+                        'user_id' => $newSchedule->provider->user_id,
+                    ]);
+                }
+
+                $createdSchedules[] = $newSchedule;
             }
+
             DB::commit();
         } catch (\Exception $e) {
+
             DB::rollBack();
 
             throw new \Exception(__($e->getMessage()));
@@ -270,26 +304,189 @@ class ScheduleService
             $schedule->update(['status' => $status]);
 
             switch ($status) {
+
                 case 'pending':
+
                     break;
+
                 case 'accepted':
+
+                    $notificationService = app(NotificationService::class);
+
+                    // CLIENTE
+                    $notificationService->create([
+                        'title' => 'Agendamento aceito!',
+
+                        'description' =>
+                        $schedule->provider->user->name .
+                            ' aceitou sua solicitação de diária.',
+
+                        'origin' => 'schedule',
+
+                        'origin_id' => $schedule->id,
+
+                        'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_ACCEPTED->value,
+
+                        'user_id' => $schedule->client->user_id,
+                    ]);
+
                     break;
+
                 case 'rejected':
+
+                    $notificationService = app(NotificationService::class);
+
+                    // CLIENTE
+                    $notificationService->create([
+                        'title' => 'Agendamento recusado!',
+
+                        'description' =>
+                        'O diarista não poderá atender. Veja outros profissionais disponíveis.',
+
+                        'origin' => 'schedule',
+
+                        'origin_id' => $schedule->id,
+
+                        'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_REFUSED->value,
+
+                        'user_id' => $schedule->client->user_id,
+                    ]);
+
                     break;
+
                 case 'paid':
-                    $date_cleaned = Carbon::parse($schedule->date)->format('Y-m-d');
-                    $date_time_dispatch = Carbon::parse($date_cleaned.' '.$schedule->start_time);
 
-                    // StartScheduleJob::dispatch($schedule->id)->delay($date_time_dispatch);
+                    $notificationService = app(NotificationService::class);
+
+                    // PRESTADOR
+                    if ($schedule->provider_id) {
+
+                        $notificationService->create([
+                            'title' => 'Pagamento confirmado!',
+
+                            'description' =>
+                            'O cliente confirmou o pagamento da diária.',
+
+                            'origin' => 'schedule',
+
+                            'origin_id' => $schedule->id,
+
+                            'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_START->value,
+
+                            'user_id' => $schedule->provider->user_id,
+                        ]);
+                    }
+
+                    $date_cleaned = Carbon::parse($schedule->date)
+                        ->format('Y-m-d');
+
+                    $date_time_dispatch = Carbon::parse(
+                        $date_cleaned . ' ' . $schedule->start_time
+                    )->subHour();
+
+                    StartScheduleJob::dispatch($schedule->id)
+                        ->delay($date_time_dispatch);
 
-                    // dispatch de teste em local
-                    StartScheduleJob::dispatch($schedule->id)->delay(now()->addSeconds(15));
                     break;
+
                 case 'cancelled':
+
+                    $notificationService = app(NotificationService::class);
+
+
+                    switch (Auth::user()->type) {
+                        case 'client':
+
+                            // PRESTADOR
+                            if ($schedule->provider_id) {
+
+                                $notificationService->create(['title' => 'Agendamento cancelado!',
+
+                                    'description' =>'O cliente cancelou a diária.',
+
+                                    'origin' => 'schedule',
+
+                                    'origin_id' => $schedule->id,
+
+                                    'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_CANCELLED->value,
+
+                                    'user_id' => $schedule->provider->user_id,
+                                ]);
+                            }
+
+                            break;
+
+                        case 'provider':
+
+                            // CLIENTE
+                            $notificationService->create([
+                                'title' => 'Agendamento cancelado!',
+
+                                'description' =>
+                                $schedule->provider->user->name .
+                                    ' cancelou a diária.',
+
+                                'origin' => 'schedule',
+
+                                'origin_id' => $schedule->id,
+
+                                'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_CANCELLED->value,
+
+                                'user_id' => $schedule->client->user_id,
+                            ]);
+
+                            break;
+
+                        default:
+                            break;
+                    }
+
                     break;
+
                 case 'started':
+
+                    $notificationService = app(NotificationService::class);
+
+                    // CLIENTE
+                    $notificationService->create([
+                        'title' => 'Diarista a caminho!',
+
+                        'description' =>
+                        'Informe o código ' .
+                            $schedule->code .
+                            ' para confirmar a chegada da diarista e liberar o início do serviço.',
+
+                        'origin' => 'schedule',
+
+                        'origin_id' => $schedule->id,
+
+                        'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_COMING->value,
+
+                        'user_id' => $schedule->client->user_id,
+                    ]);
+
                     break;
+
                 case 'finished':
+
+                    $notificationService = app(NotificationService::class);
+
+                    // CLIENTE
+                    $notificationService->create([
+                        'title' => 'Diária finalizada!',
+
+                        'description' =>
+                        'Avalie o serviço feito pelo diarista e conte-nos como foi sua experiência.',
+
+                        'origin' => 'schedule',
+
+                        'origin_id' => $schedule->id,
+
+                        'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_FINISHED->value,
+
+                        'user_id' => $schedule->client->user_id,
+                    ]);
+
                     break;
             }
 
@@ -298,7 +495,7 @@ class ScheduleService
             return $schedule->fresh(['client.user', 'provider.user', 'address']);
         } catch (\Exception $e) {
             DB::rollBack();
-            Log::error('Erro ao atualizar status do agendamento: '.$e->getMessage());
+            Log::error('Erro ao atualizar status do agendamento: ' . $e->getMessage());
             throw new \Exception('Não foi possível atualizar o status do agendamento.');
         }
     }
@@ -330,7 +527,7 @@ class ScheduleService
         } catch (\Exception $e) {
             DB::rollBack();
 
-            Log::error('Erro ao cancelar agendamento: '.$e->getMessage());
+            Log::error('Erro ao cancelar agendamento: ' . $e->getMessage());
 
             throw new \Exception('Não foi possível cancelar o agendamento.');
         }

+ 41 - 0
database/migrations/2026_05_25_102711_create_notifications_table.php

@@ -0,0 +1,41 @@
+<?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('notifications', function (Blueprint $table) {
+
+            $table->id();
+
+            $table->string('title');
+
+            $table->text('description');
+
+            $table->string('origin');
+
+            $table->unsignedBigInteger('origin_id');
+
+            $table->string('type');
+
+            $table->boolean('read')->default(false);
+
+            $table->dateTime('read_at')->nullable();
+
+           $table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
+
+            $table->softDeletes();
+
+            $table->timestamps();
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::dropIfExists('notifications');
+    }
+};

+ 7 - 0
database/seeders/PermissionSeeder.php

@@ -196,6 +196,13 @@ class PermissionSeeder extends Seeder
                         'bits'        => 271,
                         'children'    => [],
                     ],
+
+                    [
+                        'scope'       => 'notification',
+                        'description' => 'Notificações',
+                        'bits'        => 271,
+                        'children'    => [],
+                    ],
                 ],
             ],
         ];

+ 3 - 0
database/seeders/UserTypePermissionSeeder.php

@@ -52,6 +52,7 @@ class UserTypePermissionSeeder extends Seeder
                         ['scope' => 'config.speciality', 'bits' => 271],
                         ['scope' => 'config.review', 'bits' => 271],
                         ['scope' => 'config.review_improvement', 'bits' => 271],
+                        ['scope' => 'notification', 'bits' => 271],
                     ];
                     $this->seedUserTypePermissions($userPermissions, UserTypeEnum::USER->value);
                     break;
@@ -72,6 +73,7 @@ class UserTypePermissionSeeder extends Seeder
                         ['scope' => 'config.improvement_type', 'bits' => 1],
                         ['scope' => 'config.review', 'bits' => 271],
                         ['scope' => 'config.provider_client_block', 'bits' => 9],
+                        ['scope' => 'notification', 'bits' => 271],
                     ];
                     $this->seedUserTypePermissions($providerPermissions, UserTypeEnum::PROVIDER->value);
                     break;
@@ -93,6 +95,7 @@ class UserTypePermissionSeeder extends Seeder
                         ['scope' => 'config.custom_schedule', 'bits' => 271],
                         ['scope' => 'config.speciality', 'bits' => 271],
                         ['scope' => 'config.service_type', 'bits' => 271],
+                        ['scope' => 'notification', 'bits' => 271],
                     ];
                     $this->seedUserTypePermissions($clientPermissions, UserTypeEnum::CLIENT->value);
                     break;

+ 10 - 0
routes/authRoutes/notifications.php

@@ -0,0 +1,10 @@
+<?php
+
+use App\Http\Controllers\NotificationController;
+use Illuminate\Support\Facades\Route;
+
+Route::get('/notifications',[NotificationController::class, 'index'])->middleware('permission:notification,view');
+
+Route::put('/notifications/{id}/read',[NotificationController::class, 'markAsRead'])->middleware('permission:notification,add');
+
+Route::put('/notifications/read-all',[NotificationController::class, 'markAllAsRead'])->middleware('permission:notification,add');