Parcourir la source

feat: :sparkles: avaliacoes no agendamento e oportunidade + fluxo de bloqueio de cliente e prestador nas avaliacoes

avaliacoes no agendamento e oportunidade + fluxo de bloqueio de cliente e prestador nas avaliacoes
Gustavo Zanatta il y a 2 jours
Parent
commit
ccdbe0d42d
30 fichiers modifiés avec 862 ajouts et 36 suppressions
  1. 2 2
      app/Http/Controllers/ImprovementTypeController.php
  2. 73 0
      app/Http/Controllers/ReviewController.php
  3. 45 0
      app/Http/Controllers/ReviewImprovementController.php
  4. 8 0
      app/Http/Controllers/ScheduleController.php
  5. 1 0
      app/Http/Requests/ImprovementTypeRequest.php
  6. 47 0
      app/Http/Requests/ReviewImprovementRequest.php
  7. 62 0
      app/Http/Requests/ReviewRequest.php
  8. 1 0
      app/Http/Resources/CustomScheduleResource.php
  9. 21 0
      app/Http/Resources/ReviewImprovementResource.php
  10. 37 0
      app/Http/Resources/ReviewResource.php
  11. 31 20
      app/Models/ImprovementType.php
  12. 60 0
      app/Models/Review.php
  13. 36 0
      app/Models/ReviewImprovement.php
  14. 5 0
      app/Models/Schedule.php
  15. 42 0
      app/Rules/ScheduleBusinessRules.php
  16. 37 5
      app/Services/CustomScheduleService.php
  17. 10 6
      app/Services/ImprovementTypeService.php
  18. 43 0
      app/Services/ReviewImprovementService.php
  19. 90 0
      app/Services/ReviewService.php
  20. 52 3
      app/Services/ScheduleService.php
  21. 37 0
      database/migrations/2026_03_10_153004_create_reviews_table.php
  22. 34 0
      database/migrations/2026_03_11_091348_create_reviews_improvements_table.php
  23. 12 0
      database/seeders/PermissionSeeder.php
  24. 2 0
      database/seeders/UserTypePermissionSeeder.php
  25. 18 0
      lang/en/validation.php
  26. 18 0
      lang/es/validation.php
  27. 18 0
      lang/pt/validation.php
  28. 11 0
      routes/authRoutes/review.php
  29. 8 0
      routes/authRoutes/review_improvement.php
  30. 1 0
      routes/authRoutes/schedule.php

+ 2 - 2
app/Http/Controllers/ImprovementTypeController.php

@@ -14,9 +14,9 @@ class ImprovementTypeController extends Controller
         protected ImprovementTypeService $service,
     ) {}
 
-    public function index(): JsonResponse
+    public function index(ImprovementTypeRequest $request): JsonResponse
     {
-        $items = $this->service->getAll();
+        $items = $this->service->getAll($request->input('origin'));
         return $this->successResponse(payload: ImprovementTypeResource::collection($items));
     }
 

+ 73 - 0
app/Http/Controllers/ReviewController.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\ReviewRequest;
+use App\Http\Resources\ReviewResource;
+use App\Services\ReviewService;
+use Illuminate\Http\JsonResponse;
+
+class ReviewController extends Controller
+{
+    public function __construct(
+        private ReviewService $service
+    ) {}
+
+    public function index(): JsonResponse
+    {
+        $reviews = $this->service->getAll();
+
+        return $this->successResponse(
+            payload: ReviewResource::collection($reviews),
+        );
+    }
+
+    public function indexBySchedule(int $scheduleId): JsonResponse
+    {
+        $reviews = $this->service->getByScheduleId($scheduleId);
+
+        return $this->successResponse(
+            payload: ReviewResource::collection($reviews),
+        );
+    }
+
+    public function indexByOrigin(string $origin, int $originId): JsonResponse
+    {
+        $reviews = $this->service->getByOrigin($origin, $originId);
+
+        return $this->successResponse(
+            payload: ReviewResource::collection($reviews),
+        );
+    }
+
+    public function store(ReviewRequest $request): JsonResponse
+    {
+        $review = $this->service->create($request->validated());
+
+        return $this->successResponse(
+            payload: new ReviewResource($review),
+            message: __('messages.created'),
+            code: 201,
+        );
+    }
+
+    public function update(ReviewRequest $request, int $id): JsonResponse
+    {
+        $review = $this->service->update($id, $request->validated());
+
+        return $this->successResponse(
+            payload: new ReviewResource($review),
+            message: __('messages.updated'),
+        );
+    }
+
+    public function destroy(int $id): JsonResponse
+    {
+        $this->service->delete($id);
+
+        return $this->successResponse(
+            message: __('messages.deleted'),
+            code: 204,
+        );
+    }
+}

+ 45 - 0
app/Http/Controllers/ReviewImprovementController.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Http\Requests\ReviewImprovementRequest;
+use App\Http\Resources\ReviewImprovementResource;
+use App\Services\ReviewImprovementService;
+use Illuminate\Http\JsonResponse;
+
+class ReviewImprovementController extends Controller
+{
+    public function __construct(
+        private ReviewImprovementService $service
+    ) {}
+
+    public function index(int $reviewId): JsonResponse
+    {
+        $items = $this->service->getByReviewId($reviewId);
+
+        return $this->successResponse(
+            payload: ReviewImprovementResource::collection($items),
+        );
+    }
+
+    public function store(ReviewImprovementRequest $request): JsonResponse
+    {
+        $item = $this->service->create($request->validated());
+
+        return $this->successResponse(
+            payload: new ReviewImprovementResource($item),
+            message: __('messages.created'),
+            code: 201,
+        );
+    }
+
+    public function destroy(int $id): JsonResponse
+    {
+        $this->service->delete($id);
+
+        return $this->successResponse(
+            message: __('messages.deleted'),
+            code: 204,
+        );
+    }
+}

+ 8 - 0
app/Http/Controllers/ScheduleController.php

@@ -77,6 +77,14 @@ class ScheduleController extends Controller
     return $this->successResponse($grouped);
   }
 
+  public function finished(): JsonResponse
+  {
+    $schedules = $this->scheduleService->getFinished();
+    return $this->successResponse(
+      ScheduleResource::collection($schedules),
+    );
+  }
+
   public function updateStatus(string $id, ScheduleRequest $request): JsonResponse
   {
     try {

+ 1 - 0
app/Http/Requests/ImprovementTypeRequest.php

@@ -13,6 +13,7 @@ class ImprovementTypeRequest extends FormRequest
             'description' => 'sometimes|string|max:255',
             'improvement_type' => ['sometimes', Rule::in(['client', 'provider', 'both'])],
             'is_active' => 'sometimes|boolean',
+            'origin' => ['sometimes', Rule::in(['client', 'provider'])] 
         ];
 
         if ($this->isMethod('POST')) {

+ 47 - 0
app/Http/Requests/ReviewImprovementRequest.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+
+class ReviewImprovementRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    public function rules(): array
+    {
+        $itemId   = $this->route('id');
+        $reviewId = $this->input('review_id');
+
+        $rules = [
+            'review_id'           => ['sometimes', 'integer', 'exists:reviews,id'],
+            'improvement_type_id' => ['sometimes', 'integer', 'exists:improvement_types,id'],
+        ];
+
+        if ($this->isMethod('POST')) {
+            $rules['review_id'] = ['required', 'integer', 'exists:reviews,id'];
+            $rules['improvement_type_id'] = [
+                'required',
+                'integer',
+                'exists:improvement_types,id',
+                Rule::unique('reviews_improvements', 'improvement_type_id')
+                    ->where('review_id', $reviewId)
+                    ->whereNull('deleted_at')
+                    ->ignore($itemId),
+            ];
+        }
+
+        return $rules;
+    }
+
+    public function messages(): array
+    {
+        return [
+            'improvement_type_id.unique' => __('validation.review_improvement.already_exists'),
+        ];
+    }
+}

+ 62 - 0
app/Http/Requests/ReviewRequest.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
+
+class ReviewRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    public function rules(): array
+    {
+        $reviewId = $this->route('id');
+
+        $rules = [
+            'schedule_id' => ['sometimes', 'integer', 'exists:schedules,id'],
+            'origin'      => ['sometimes', 'string', Rule::in(['provider', 'client'])],
+            'origin_id'   => ['sometimes', 'integer'],
+            'stars'       => ['sometimes', 'numeric', 'min:0', 'max:5'],
+            'comment'     => ['nullable', 'string'],
+            'improvements_ids' => ['nullable', 'array'],
+            'improvements_ids.*' => ['integer', 'exists:improvement_types,id'],
+            'block_provider' => ['sometimes', 'boolean'],
+            'block_client'   => ['sometimes', 'boolean'],
+        ];
+
+        if ($this->isMethod('POST')) {
+            $scheduleId = $this->input('schedule_id');
+            $origin     = $this->input('origin');
+            $originId   = $this->input('origin_id');
+
+            $rules['schedule_id'] = ['required', 'integer', 'exists:schedules,id'];
+            $rules['origin']      = ['required', 'string', Rule::in(['provider', 'client'])];
+            $rules['origin_id']   = [
+                'required',
+                'integer',
+                Rule::unique('reviews', 'origin_id')
+                    ->where('schedule_id', $scheduleId)
+                    ->where('origin', $origin)
+                    ->whereNull('deleted_at')
+                    ->ignore($reviewId),
+            ];
+            $rules['stars'] = ['required', 'numeric', 'min:0', 'max:5'];
+            $rules['improvements_ids'] = ['sometimes', 'array'];
+            $rules['improvements_ids.*'] = ['integer', 'exists:improvement_types,id'];
+        }
+
+        return $rules;
+    }
+
+    public function messages(): array
+    {
+        return [
+            'origin_id.unique' => __('validation.custom.review.already_reviewed'),
+            'origin.in'        => __('validation.custom.review.invalid_origin'),
+        ];
+    }
+}

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

@@ -33,6 +33,7 @@ class CustomScheduleResource extends JsonResource
                 return [
                     'id' => $this->schedule->id,
                     'client_id' => $this->schedule->client_id,
+                    'provider_id' => $this->schedule->provider_id,
                     'client_name' => $this->schedule->client?->user?->name,
                     'address_id' => $this->schedule->address_id,
                     'address' => $this->schedule->address ? [

+ 21 - 0
app/Http/Resources/ReviewImprovementResource.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class ReviewImprovementResource extends JsonResource
+{
+    public function toArray(Request $request): array
+    {
+        return [
+            'id'                   => $this->id,
+            'review_id'            => $this->review_id,
+            'improvement_type_id'  => $this->improvement_type_id,
+            'improvement_type_name'=> $this->improvementType->description ?? null,
+            'created_at'           => $this->created_at?->format('Y-m-d H:i'),
+            'updated_at'           => $this->updated_at?->format('Y-m-d H:i'),
+        ];
+    }
+}

+ 37 - 0
app/Http/Resources/ReviewResource.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace App\Http\Resources;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Resources\Json\JsonResource;
+
+class ReviewResource extends JsonResource
+{
+    public function toArray(Request $request): array
+    {
+        return [
+            'id'          => $this->id,
+            'schedule_id' => $this->schedule_id,
+            'schedule_label' => $this->schedule
+                ? ($this->schedule->id . ' - ' .
+                   ($this->schedule->client?->user?->name ?? '?') . ' - ' .
+                   ($this->schedule->provider?->user?->name ?? '?') . ' - ' .
+                   ($this->schedule->date?->format('d/m/Y') ?? '?'))
+                : null,
+            'origin'      => $this->origin,
+            'origin_id'   => $this->origin_id,
+            'stars'       => $this->stars,
+            'comment'     => $this->comment,
+            'reviews_improvements' => $this->whenLoaded('reviewsImprovements', function () {
+                return $this->reviewsImprovements->map(function ($type) {
+                    return [
+                        'id' => $type->improvementType->id,
+                        'description' => $type->improvementType->description,
+                    ];
+                });
+            }),
+            'created_at'  => $this->created_at?->format('Y-m-d H:i'),
+            'updated_at'  => $this->updated_at?->format('Y-m-d H:i'),
+        ];
+    }
+}

+ 31 - 20
app/Models/ImprovementType.php

@@ -5,6 +5,7 @@ namespace App\Models;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
 
 /**
  * @property int $id
@@ -17,24 +18,34 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  */
 class ImprovementType extends Model
 {
-    use HasFactory, SoftDeletes;
-
-    protected $table = 'improvement_types';
-
-    protected $guarded = [
-        'id',
-    ];
-
-    protected $casts = [
-        'created_at' => 'datetime',
-        'updated_at' => 'datetime',
-        'deleted_at' => 'datetime',
-        'is_active' => 'boolean',
-    ];
-
-    protected $fillable = [
-        'description',
-        'improvement_type',
-        'is_active',
-    ];
+  use HasFactory, SoftDeletes;
+
+  protected $table = 'improvement_types';
+
+  protected $guarded = [
+    'id',
+  ];
+
+  protected $casts = [
+    'created_at' => 'datetime',
+    'updated_at' => 'datetime',
+    'deleted_at' => 'datetime',
+    'is_active' => 'boolean',
+  ];
+
+  protected $fillable = [
+    'description',
+    'improvement_type',
+    'is_active',
+  ];
+
+  public function reviews(): BelongsToMany
+  {
+    return $this->belongsToMany(
+      Review::class,
+      'review_improvements',
+      'improvement_id',
+      'review_id'
+    );
+  }
 }

+ 60 - 0
app/Models/Review.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+
+class Review extends Model
+{
+  use HasFactory, SoftDeletes;
+
+  protected $fillable = [
+    'schedule_id',
+    'origin',
+    'origin_id',
+    'stars',
+    'comment',
+  ];
+
+  protected $casts = [
+    'stars'      => 'decimal:1',
+    'created_at' => 'datetime',
+    'updated_at' => 'datetime',
+    'deleted_at' => 'datetime',
+  ];
+
+  public function schedule(): BelongsTo
+  {
+    return $this->belongsTo(Schedule::class);
+  }
+
+  public function originProvider(): BelongsTo
+  {
+    return $this->belongsTo(Provider::class, 'origin_id');
+  }
+
+  public function originClient(): BelongsTo
+  {
+    return $this->belongsTo(Client::class, 'origin_id');
+  }
+
+  public function reviewsImprovements(): HasMany
+  {
+    return $this->hasMany(ReviewImprovement::class);
+  }
+
+  public function improvements(): BelongsToMany
+  {
+    return $this->belongsToMany(
+      ImprovementType::class,
+      'reviews_improvements',
+      'review_id',
+      'improvement_type_id'
+    )->withTimestamps();
+  }
+}

+ 36 - 0
app/Models/ReviewImprovement.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;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+class ReviewImprovement extends Model
+{
+    use HasFactory, SoftDeletes;
+
+    protected $table = 'reviews_improvements';
+
+    protected $fillable = [
+        'review_id',
+        'improvement_type_id',
+    ];
+
+    protected $casts = [
+        'created_at' => 'datetime',
+        'updated_at' => 'datetime',
+        'deleted_at' => 'datetime',
+    ];
+
+    public function review(): BelongsTo
+    {
+        return $this->belongsTo(Review::class);
+    }
+
+    public function improvementType(): BelongsTo
+    {
+        return $this->belongsTo(ImprovementType::class);
+    }
+}

+ 5 - 0
app/Models/Schedule.php

@@ -60,4 +60,9 @@ class Schedule extends Model
     {
         return $this->hasMany(ScheduleRefuse::class);
     }
+
+    public function reviews()
+    {
+        return $this->hasMany(Review::class);
+    }
 }

+ 42 - 0
app/Rules/ScheduleBusinessRules.php

@@ -2,8 +2,10 @@
 
 namespace App\Rules;
 
+use App\Models\ClientProviderBlock;
 use App\Models\Provider;
 use App\Models\ProviderBlockedDay;
+use App\Models\ProviderClientBlock;
 use App\Models\ProviderWorkingDay;
 use App\Models\Schedule;
 use App\Models\ScheduleProposal;
@@ -237,4 +239,44 @@ class ScheduleBusinessRules
 
       return true;
     }
+
+    /**
+     * Valida se o cliente tem bloqueio cadastrado para o prestador
+      * @param int $client_id
+      * @param int $provider_id
+      * @return bool
+      * @throws \Exception
+     */
+    public static function validateClientNotBlockedByProvider($client_id, $provider_id)
+    {
+      $provider_client_block = ProviderClientBlock::where('provider_id', $provider_id)
+        ->where('client_id', $client_id)
+        ->first();
+
+      if ($provider_client_block) {
+        throw new \Exception(__('validation.custom.schedule.client_blocked_by_provider'));
+      }
+
+      return true;
+    }
+
+    /**
+     * Valida se o prestador tem bloqueio cadastrado para o cliente
+      * @param int $client_id
+      * @param int $provider_id
+      * @return bool
+      * @throws \Exception
+     */
+    public static function validateProviderNotBlockedByClient($client_id, $provider_id)
+    {
+      $client_provider_block = ClientProviderBlock::where('provider_id', $provider_id)
+        ->where('client_id', $client_id)
+        ->first();
+
+      if ($client_provider_block) {
+        throw new \Exception(__('validation.custom.schedule.provider_blocked_by_client'));
+      }
+
+      return true;
+    }
 }

+ 37 - 5
app/Services/CustomScheduleService.php

@@ -208,7 +208,7 @@ class CustomScheduleService
 
   public function getSchedulesCustomGroupedByClient()
   {
-    $schedules = Schedule::with(['client.user', 'provider.user', 'address', 'customSchedule.serviceType', 'customSchedule.specialities'])
+    $schedules = Schedule::with(['client.user', 'provider.user', 'address', 'customSchedule.serviceType', 'customSchedule.specialities', 'reviews.reviewsImprovements.improvementType'])
       ->orderBy('id', 'desc')
       ->where('schedule_type', 'custom')
       ->get();
@@ -364,12 +364,13 @@ class CustomScheduleService
 
   private function checkProviderAvailability($providerId, $schedule)
   {
-    $date = Carbon::parse($schedule->date);
-    $provider_id = $providerId;
     $client_id = $schedule->client_id;
+    $provider_id = $providerId;
+
+    $date = Carbon::parse($schedule->date);
+    $dayOfWeek = $date->dayOfWeek;//0-6
     $startTime = $schedule->start_time;
     $endTime = $schedule->end_time;
-    $dayOfWeek = $date->dayOfWeek;//0-6
     $date_ymd = $date->format('Y-m-d');
     $period = $startTime < '13:00:00' ? 'morning' : 'afternoon';
     $period_type = $schedule->period_type;//2,4,6,8
@@ -423,7 +424,19 @@ class CustomScheduleService
       $endTime,
       $schedule->id
     );
-    
+
+    // bloqueio caso o client tenha bloqueado o provider
+    ScheduleBusinessRules::validateClientNotBlockedByProvider(
+      $client_id,
+      $provider_id
+    );
+
+    // bloqueio caso o provider tenha bloqueado o client
+    ScheduleBusinessRules::validateProviderNotBlockedByClient(
+      $client_id,
+      $provider_id
+    );
+
     return true;
   }
 
@@ -458,6 +471,7 @@ class CustomScheduleService
             'code' => $schedule->code,
             'code_verified' => $schedule->code_verified,
             'provider_id' => $schedule->provider_id,
+            'client_id' => $schedule->client_id,
             'provider_name' => $schedule->provider?->user->name ?? 'N/A',
             'address' => $schedule->address ? [
               'id' => $schedule->address->id,
@@ -484,6 +498,24 @@ class CustomScheduleService
                 ];
               })->values()
             ] : null,
+            'reviews' => $schedule->reviews->map(function ($review) {
+              return [
+                'id' => $review->id,
+                'stars' => $review->stars,
+                'comment' => $review->comment,
+                'origin' => $review->origin,
+                'origin_id' => $review->origin_id,
+                'created_at' => Carbon::parse($review->created_at)->format('Y-m-d H:i'),
+                'updated_at' => Carbon::parse($review->updated_at)->format('Y-m-d H:i'),
+                'improvements' => $review->reviewsImprovements->map(function ($ri) {
+                  return [
+                    'id' => $ri->id,
+                    'improvement_type_id' => $ri->improvement_type_id,
+                    'improvement_type_name' => $ri->improvementType ? $ri->improvementType->description : null,
+                  ];
+                })->values(),
+              ];
+            })
           ];
         })->values()
       ];

+ 10 - 6
app/Services/ImprovementTypeService.php

@@ -7,11 +7,17 @@ use Illuminate\Database\Eloquent\Collection;
 
 class ImprovementTypeService
 {
-    public function getAll(): Collection
+    public function getAll(string $origin): Collection
     {
-        return ImprovementType::query()
-            ->orderBy('created_at', 'desc')
-            ->get();
+        $improvement_types = ImprovementType::query()
+          ->orderBy('created_at', 'desc')
+          ->when($origin, function ($query) use ($origin) {
+              $query->where('improvement_type', $origin)
+                ->orWhere('improvement_type', 'both');
+          })
+          ->get();
+
+        return $improvement_types;
     }
 
     public function findById(int $id): ?ImprovementType
@@ -46,6 +52,4 @@ class ImprovementTypeService
 
         return $model->delete();
     }
-
-    // Add custom business logic methods here
 }

+ 43 - 0
app/Services/ReviewImprovementService.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\ReviewImprovement;
+use Exception;
+
+class ReviewImprovementService
+{
+    public function getByReviewId(int $reviewId)
+    {
+        return ReviewImprovement::with(['improvementType'])
+            ->where('review_id', $reviewId)
+            ->orderBy('created_at', 'desc')
+            ->get();
+    }
+
+    public function create(array $data): ReviewImprovement
+    {
+        $existing = ReviewImprovement::where('review_id', $data['review_id'])
+            ->where('improvement_type_id', $data['improvement_type_id'])
+            ->first();
+
+        if ($existing) {
+            if ($existing->trashed()) {
+                $existing->restore();
+                return $existing->fresh(['improvementType']);
+            } else {
+                throw new Exception(__('validation.review_improvement.already_exists'));
+            }
+        }
+
+        $item = ReviewImprovement::create($data);
+        $item->load(['improvementType']);
+        return $item;
+    }
+
+    public function delete(int $id): bool
+    {
+        $item = ReviewImprovement::findOrFail($id);
+        return $item->delete();
+    }
+}

+ 90 - 0
app/Services/ReviewService.php

@@ -0,0 +1,90 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\ClientProviderBlock;
+use App\Models\ProviderClientBlock;
+use App\Models\Review;
+use App\Models\Schedule;
+use Exception;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+class ReviewService
+{
+  public function getAll()
+  {
+    return Review::with(['schedule.client.user', 'schedule.provider.user', 'reviewsImprovements.improvementType'])
+      ->orderBy('created_at', 'desc')
+      ->get();
+  }
+
+  public function getByScheduleId(int $scheduleId)
+  {
+    return Review::with(['schedule.client.user', 'schedule.provider.user'])
+      ->where('schedule_id', $scheduleId)
+      ->orderBy('created_at', 'desc')
+      ->get();
+  }
+
+  public function getByOrigin(string $origin, int $originId)
+  {
+    return Review::with(['schedule.client.user', 'schedule.provider.user'])
+      ->where('origin', $origin)
+      ->where('origin_id', $originId)
+      ->orderBy('created_at', 'desc')
+      ->get();
+  }
+
+  public function create(array $data): Review
+  {
+    try {
+      DB::beginTransaction();
+      $review = new Review();
+      $review->fill($data);
+      $review->save();
+      $review->refresh();
+  
+      if (isset($data['improvements_ids'])) {
+        $review->improvements()->sync($data['improvements_ids']);
+      }
+  
+      if($data['block_provider'] == true) {
+        $schedule = Schedule::find($data['schedule_id']);
+
+        $client_provider_block = new ClientProviderBlock();
+        $client_provider_block->client_id = $schedule->client_id;
+        $client_provider_block->provider_id = $schedule->provider_id;
+        $client_provider_block->save();
+      }
+  
+      if($data['block_client'] == true) {
+        $schedule = Schedule::find($data['schedule_id']);
+        $provider_client_block = new ProviderClientBlock();
+        $provider_client_block->provider_id = $schedule->provider_id;
+        $provider_client_block->client_id = $schedule->client_id;
+        $provider_client_block->save();
+      }
+  
+      DB::commit();
+      return $review;
+    } catch (Exception $e) {
+      DB::rollBack();
+      Log::error('Error creating review: ' . $e->getMessage());
+      throw $e;
+    }
+  }
+
+  public function update(int $id, array $data): Review
+  {
+    $review = Review::findOrFail($id);
+    $review->update($data);
+    return $review->fresh();
+  }
+
+  public function delete(int $id): bool
+  {
+    $review = Review::findOrFail($id);
+    return $review->delete();
+  }
+}

+ 52 - 3
app/Services/ScheduleService.php

@@ -24,6 +24,15 @@ class ScheduleService
       ->get();
   }
 
+  public function getFinished()
+  {
+    return Schedule::with(['client.user', 'provider.user'])
+      ->where('status', 'finished')
+      ->orderBy('date', 'desc')
+      ->orderBy('start_time', 'desc')
+      ->get();
+  }
+
   public function getById($id)
   {
     return Schedule::with(['client.user', 'provider.user', 'address'])->findOrFail($id);
@@ -42,7 +51,7 @@ class ScheduleService
         $createdSchedules[] = Schedule::create($scheduleData);
 
       } catch (\Exception $e) {
-        throw new \Exception(__("validation.provider_unavailable"));
+        throw new \Exception(__($e->getMessage()));
       }
     }
 
@@ -97,6 +106,7 @@ class ScheduleService
     $dayOfWeek = $date->dayOfWeek;
     $startTime = $data['start_time'];
     $endTime = $data['end_time'];
+    $date_ymd = $date->format('Y-m-d');
     $period = $startTime < '13:00:00' ? 'morning' : 'afternoon';
 
     // bloqueio 2 schedules por semana para o mesmo client e provider
@@ -130,18 +140,38 @@ class ScheduleService
       $endTime,
       $excludeScheduleId
     );
+
+    // bloqueio provider tem outra proposta na mesma data
+    ScheduleBusinessRules::validateConflictingProposalSameDate(
+      $provider_id,
+      $date_ymd,
+      $startTime,
+      $endTime,
+      null
+    );
+
+    // bloqueio caso o client tenha bloqueado o provider
+    ScheduleBusinessRules::validateClientNotBlockedByProvider(
+      $client_id,
+      $provider_id
+    );
+
+    // bloqueio caso o provider tenha bloqueado o client
+    ScheduleBusinessRules::validateProviderNotBlockedByClient(
+      $client_id,
+      $provider_id
+    );
     
     return true;
   }
 
   public function getSchedulesDefaultGroupedByClient()
   {
-    $schedules = Schedule::with(['client.user', 'provider.user', 'address'])
+    $schedules = Schedule::with(['client.user', 'provider.user', 'address', 'reviews.reviewsImprovements.improvementType'])
       ->orderBy('id', 'desc')
       ->where('schedule_type', 'default')
       ->select(
         'schedules.*'
-
       )
       ->get();
 
@@ -161,6 +191,7 @@ class ScheduleService
             'total_amount' => $schedule->total_amount,
             'code' => $schedule->code,
             'code_verified' => $schedule->code_verified,
+            'client_id' => $schedule->client_id,
             'provider_id' => $schedule->provider_id,
             'provider_name' => $schedule->provider->user->name ?? 'N/A',
             'address' => $schedule->address ? [
@@ -172,6 +203,24 @@ class ScheduleService
               'state' => $schedule->address->city->state->name ?? '',
             ] : null,
             'client_name' => $schedule->client->user->name ?? 'N/A',
+            'reviews' => $schedule->reviews->map(function ($review) {
+              return [
+                'id' => $review->id,
+                'stars' => $review->stars,
+                'comment' => $review->comment,
+                'origin' => $review->origin,
+                'origin_id' => $review->origin_id,
+                'created_at' => Carbon::parse($review->created_at)->format('Y-m-d H:i'),
+                'updated_at' => Carbon::parse($review->updated_at)->format('Y-m-d H:i'),
+                'improvements' => $review->reviewsImprovements->map(function ($ri) {
+                  return [
+                    'id' => $ri->id,
+                    'improvement_type_id' => $ri->improvement_type_id,
+                    'improvement_type_name' => $ri->improvementType ? $ri->improvementType->description : null,
+                  ];
+                })->values(),
+              ];
+            })
           ];
         })->values()
       ];

+ 37 - 0
database/migrations/2026_03_10_153004_create_reviews_table.php

@@ -0,0 +1,37 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('reviews', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('schedule_id')->constrained('schedules')->onDelete('cascade');
+            $table->string('origin'); // 'providers' | 'clients'
+            $table->unsignedBigInteger('origin_id');
+            $table->decimal('stars', 2, 1);
+            $table->text('comment')->nullable();
+            $table->timestamps();
+            $table->softDeletes();
+
+            $table->index('schedule_id');
+            $table->index(['origin', 'origin_id']);
+            $table->index('deleted_at');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('reviews');
+    }
+};

+ 34 - 0
database/migrations/2026_03_11_091348_create_reviews_improvements_table.php

@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('reviews_improvements', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('review_id')->constrained('reviews')->onDelete('cascade');
+            $table->foreignId('improvement_type_id')->constrained('improvement_types')->onDelete('cascade');
+            $table->timestamps();
+            $table->softDeletes();
+
+            $table->index('review_id');
+            $table->index('improvement_type_id');
+            $table->index('deleted_at');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('reviews_improvements');
+    }
+};

+ 12 - 0
database/seeders/PermissionSeeder.php

@@ -190,6 +190,18 @@ class PermissionSeeder extends Seeder
                         "bits" => 271,
                         "children" => [],
                     ],
+                    [
+                        "scope" => "config.review",
+                        "description" => "Avaliações",
+                        "bits" => 271,
+                        "children" => [],
+                    ],
+                    [
+                        "scope" => "config.review_improvement",
+                        "description" => "Melhorias das Avaliações",
+                        "bits" => 271,
+                        "children" => [],
+                    ],
                 ],
             ],
         ];

+ 2 - 0
database/seeders/UserTypePermissionSeeder.php

@@ -52,6 +52,8 @@ class UserTypePermissionSeeder extends Seeder
                         ['scope' => 'config.media', 'bits' => 271],
                         ['scope' => 'config.service_type', 'bits' => 271],
                         ['scope' => 'config.speciality', 'bits' => 271],
+                        ['scope' => 'config.review', 'bits' => 271],
+                        ['scope' => 'config.review_improvement', 'bits' => 271],
                     ];
                     $this->seedUserTypePermissions($userPermissions, UserTypeEnum::USER->value);
                     break;

+ 18 - 0
lang/en/validation.php

@@ -202,6 +202,17 @@ return [
         ],
         'schedule' => [
             'weekly_limit_exceeded' => 'This provider already has 2 schedules with this client in the same week.',
+            'provider_not_working' => 'Provider does not work on this day/period.',
+            'provider_blocked' => 'Provider has a block on this day/time.',
+            'invalid_price' => 'The provided price is invalid for the selected period type.',
+            'invalid_price_range' => 'The minimum price cannot be greater than the maximum price.',
+            'invalid_period_type' => 'The period type is invalid. It must be 2, 4, 6, or 8.',
+            'price_not_in_range' => 'The price must be between the minimum and maximum price defined for this opportunity.',
+            'provider_conflicting_schedule' => 'The provider has a conflicting schedule at this day/time.',
+            'provider_conflicting_same_proposal' => 'The provider already has a proposal sent for this schedule.',
+            'provider_conflicting_proposal_same_date' => 'The provider already has a proposal for a schedule on the same day.',
+            'client_blocked_by_provider' => 'The client is blocked by this provider.',
+            'provider_blocked_by_client' => 'The provider is blocked by this client.',
         ],
         'opportunity' => [
             'already_assigned' => 'This opportunity already has a provider assigned.',
@@ -219,6 +230,13 @@ return [
         'client_provider_block' => [
             'already_blocked' => 'This provider is already blocked by this client.',
         ],
+        'review' => [
+            'already_reviewed' => 'This schedule has already been reviewed by this user.',
+            'invalid_origin'   => 'The origin field must be "providers" or "clients".',
+        ],
+        'review_improvement' => [
+            'already_exists' => 'This improvement type is already linked to this review.',
+        ],
     ],
 
     /*

+ 18 - 0
lang/es/validation.php

@@ -202,6 +202,17 @@ return [
         ],
         'schedule' => [
             'weekly_limit_exceeded' => 'Este proveedor ya tiene 2 agendas con este cliente en la misma semana.',
+            'provider_not_working' => 'El proveedor no trabaja en este día/período.',
+            'provider_blocked' => 'El proveedor tiene un bloqueo en este día/horario.',
+            'invalid_price' => 'El precio informado es inválido para el tipo de período seleccionado.',
+            'invalid_price_range' => 'El precio mínimo no puede ser mayor que el precio máximo.',
+            'invalid_period_type' => 'El tipo de período es inválido. Debe ser 2, 4, 6 u 8.',
+            'price_not_in_range' => 'El precio debe estar entre el precio mínimo y máximo definido para esta oportunidad.',
+            'provider_conflicting_schedule' => 'El proveedor tiene un agendamiento conflictivo en este día/horario.',
+            'provider_conflicting_same_proposal' => 'El proveedor ya tiene una propuesta enviada para este agendamiento.',
+            'provider_conflicting_proposal_same_date' => 'El proveedor ya tiene una propuesta para un agendamiento en el mismo día.',
+            'client_blocked_by_provider' => 'El cliente está bloqueado por este proveedor.',
+            'provider_blocked_by_client' => 'El proveedor está bloqueado por este cliente.',
         ],
         'opportunity' => [
             'already_assigned' => 'Esta oportunidad ya tiene un proveedor asignado.',
@@ -219,6 +230,13 @@ return [
         'client_provider_block' => [
             'already_blocked' => 'Este proveedor ya está bloqueado por este cliente.',
         ],
+        'review' => [
+            'already_reviewed' => 'Esta agenda ya fue evaluada por este usuario.',
+            'invalid_origin'   => 'El campo origen debe ser "providers" o "clients".',
+        ],
+        'review_improvement' => [
+            'already_exists' => 'Este tipo de mejora ya está vinculado a esta evaluación.',
+        ],
     ],
 
     /*

+ 18 - 0
lang/pt/validation.php

@@ -203,6 +203,17 @@ return [
         ],
         'schedule' => [
             'weekly_limit_exceeded' => 'Este prestador já possui 2 agendamentos com este cliente na mesma semana.',
+            'provider_not_working' => 'Prestador não trabalha neste dia/período.',
+            'provider_blocked' => 'Prestador possui bloqueio neste dia/horário.',
+            'invalid_price' => 'O preço informado é inválido para o tipo de período selecionado.',
+            'invalid_price_range' => 'O preço mínimo não pode ser maior que o preço máximo.',
+            'invalid_period_type' => 'O tipo de período é inválido. Deve ser 2, 4, 6 ou 8.',
+            'price_not_in_range' => 'O preço deve estar entre o preço mínimo e máximo definido para esta oportunidade.',
+            'provider_conflicting_schedule' => 'O prestador possui um agendamento conflitante neste dia/horário.',
+            'provider_conflicting_same_proposal' => 'O prestador já possui uma proposta enviada para este agendamento.',
+            'provider_conflicting_proposal_same_date' => 'O prestador já possui uma proposta para um agendamento no mesmo dia.',
+            'client_blocked_by_provider' => 'O cliente está bloqueado por este prestador.',
+            'provider_blocked_by_client' => 'O prestador está bloqueado por este cliente.',
         ],
         'opportunity' => [
             'already_assigned' => 'Esta oportunidade já possui um prestador atribuído.',
@@ -220,6 +231,13 @@ return [
         'client_provider_block' => [
             'already_blocked' => 'Este prestador já está bloqueado por este cliente.',
         ],
+        'review' => [
+            'already_reviewed' => 'Esta agenda já foi avaliada por este usuário.',
+            'invalid_origin'   => 'O campo origem deve ser "providers" ou "clients".',
+        ],
+        'review_improvement' => [
+            'already_exists' => 'Este tipo de melhoria já está vinculado a esta avaliação.',
+        ],
     ],
 
     /*

+ 11 - 0
routes/authRoutes/review.php

@@ -0,0 +1,11 @@
+<?php
+
+use App\Http\Controllers\ReviewController;
+use Illuminate\Support\Facades\Route;
+
+Route::get('/reviews', [ReviewController::class, 'index'])->middleware('permission:config.review,view');
+Route::get('/reviews/schedule/{scheduleId}', [ReviewController::class, 'indexBySchedule'])->middleware('permission:config.review,view');
+Route::get('/reviews/{origin}/{originId}', [ReviewController::class, 'indexByOrigin'])->middleware('permission:config.review,view');
+Route::post('/reviews', [ReviewController::class, 'store'])->middleware('permission:config.review,add');
+Route::put('/reviews/{id}', [ReviewController::class, 'update'])->middleware('permission:config.review,edit');
+Route::delete('/reviews/{id}', [ReviewController::class, 'destroy'])->middleware('permission:config.review,delete');

+ 8 - 0
routes/authRoutes/review_improvement.php

@@ -0,0 +1,8 @@
+<?php
+
+use App\Http\Controllers\ReviewImprovementController;
+use Illuminate\Support\Facades\Route;
+
+Route::get('/review-improvements/{reviewId}', [ReviewImprovementController::class, 'index'])->middleware('permission:config.review_improvement,view');
+Route::post('/review-improvements', [ReviewImprovementController::class, 'store'])->middleware('permission:config.review_improvement,add');
+Route::delete('/review-improvements/{id}', [ReviewImprovementController::class, 'destroy'])->middleware('permission:config.review_improvement,delete');

+ 1 - 0
routes/authRoutes/schedule.php

@@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Route;
 
 Route::get('/schedules', [ScheduleController::class, 'index'])->middleware('permission:config.schedule,view');
 Route::get('/schedules/grouped-by-client', [ScheduleController::class, 'groupedByClient'])->middleware('permission:config.schedule,view');
+Route::get('/schedules/finished', [ScheduleController::class, 'finished'])->middleware('permission:config.schedule,view');
 Route::get('/schedule/{id}', [ScheduleController::class, 'show'])->middleware('permission:config.schedule,view');
 Route::post('/schedule', [ScheduleController::class, 'store'])->middleware('permission:config.schedule,add');
 Route::put('/schedule/{id}', [ScheduleController::class, 'update'])->middleware('permission:config.schedule,edit');