Преглед изворни кода

Merge branch 'feature/diariaapp-gus-agendamentos-apps' of Softpar/sfp_api_laravel_diarista into development

zntt пре 2 недеља
родитељ
комит
5dfb4f6265

+ 1 - 1
app/Http/Controllers/DashboardController.php

@@ -5,8 +5,8 @@ namespace App\Http\Controllers;
 use App\Services\DashboardService;
 use App\Http\Resources\DashboardClienteResource;
 use App\Http\Resources\DashboardPrestadorResource;
-use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
 use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Log;
 
 class DashboardController extends Controller

+ 19 - 1
app/Http/Controllers/ScheduleController.php

@@ -6,7 +6,7 @@ use App\Http\Requests\ScheduleRequest;
 use App\Http\Resources\ScheduleResource;
 use App\Services\ScheduleService;
 use Illuminate\Http\JsonResponse;
-use Illuminate\Support\Facades\Request;
+use Illuminate\Http\Request;
 
 class ScheduleController extends Controller
 {
@@ -102,4 +102,22 @@ class ScheduleController extends Controller
       return $this->errorResponse($e->getMessage(), 422);
     }
   }
+
+  public function cancelWithReason(string $id, Request $request): JsonResponse
+  {
+    try {
+      $validated = $request->validate([
+        'cancel_text' => 'required|string|min:5|max:1000',
+      ]);
+
+      $schedule = $this->scheduleService->cancelWithReason((int) $id, $validated['cancel_text']);
+
+      return $this->successResponse(
+        payload: new ScheduleResource($schedule),
+        message: __("messages.updated"),
+      );
+    } catch (\Exception $e) {
+      return $this->errorResponse($e->getMessage(), 422);
+    }
+  }
 }

+ 24 - 0
app/Http/Controllers/SearchController.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Services\SearchService;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Log;
+
+class SearchController extends Controller
+{
+  public function __construct(private readonly SearchService $service) {}
+
+  public function buscaPrestadores(Request $request): JsonResponse
+  {
+    try {
+      $dados = $this->service->buscaPrestadores($request->query('name'), $request->query('date'));
+      return $this->successResponse(payload: $dados);
+    } catch (\Exception $e) {
+      Log::error("Erro ao buscar prestadores: " . $e->getMessage());
+      return $this->errorResponse(message: __("messages.error_fetching_data"), code: 500);
+    }
+  }
+}

+ 0 - 1
app/Http/Controllers/UserController.php

@@ -45,7 +45,6 @@ class UserController extends Controller
 
   public function update(UserRequest $request, int $id): JsonResponse
   {
-    Log::info("Updating user with ID: $id", ['data' => $request->validated()]);
     $item = $this->service->update($id, $request->validated());
     return $this->successResponse(
       payload: new UserResource($item),

+ 2 - 0
app/Http/Requests/ScheduleRequest.php

@@ -26,6 +26,8 @@ class ScheduleRequest extends FormRequest
             'schedules.*.start_time' => 'required|date_format:H:i',
             'schedules.*.end_time' => 'required|date_format:H:i|after:schedules.*.start_time',
             'schedules.*.total_amount' => 'required|numeric|min:0',
+            'schedules.*.offers_meal' => 'nullable|boolean',
+            'offers_meal' => 'sometimes|nullable|boolean',
             'period_type' => 'sometimes|required|in:2,4,6,8',
             'schedule_type' => 'sometimes|in:default,custom',
             'start_time' => 'sometimes|required|date_format:H:i',

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

@@ -17,6 +17,7 @@ class DashboardClienteResource extends JsonResource
     return [
       'headerBar' => $this['headerBar'],
       'summaryInfos' => $this['summaryInfos'],
+      'pendingSchedules' => $this['pendingSchedules'],
       'nextSchedules' => $this['nextSchedules'],
       'lastDoneSchedules' => $this['lastDoneSchedules'],
       'favoriteProviders' => $this['favoriteProviders'],

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

@@ -18,6 +18,7 @@ class DashboardPrestadorResource extends JsonResource
       'headerBar' => $this['headerBar'],
       'summaryInfos' => $this['summaryInfos'],
       'priceSuggested' => $this['priceSuggested'],
+      'todayServices' => $this['todayServices'],
       'solicitations' => $this['solicitations'],
       'nextSchedules' => $this['nextSchedules'],
       'opportunities' => $this['opportunities'],

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

@@ -28,6 +28,7 @@ class ScheduleResource extends JsonResource
             'schedule_type' => $this->schedule_type,
             'start_time' => $this->start_time,
             'end_time' => $this->end_time,
+            'offers_meal' => $this->offers_meal,
             'status' => $this->status,
             'total_amount' => $this->total_amount,
             'code' => $this->code,

+ 5 - 0
app/Models/Client.php

@@ -64,4 +64,9 @@ class Client extends Model
 
       $this->save();
     }
+
+    public function schedules()
+    {
+      return $this->hasMany(Schedule::class);
+    }
 }

+ 83 - 76
app/Models/Provider.php

@@ -40,51 +40,51 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  */
 class Provider extends Model
 {
-    use HasFactory, SoftDeletes;
+  use HasFactory, SoftDeletes;
 
-    protected $table = "providers";
+  protected $table = "providers";
 
-    protected $guarded = ["id"];
+  protected $guarded = ["id"];
 
-    /**
-     * Get the attributes that should be cast.
-     *
-     * @return array<string, string>
-     */
-    protected function casts(): array
-    {
-        return [
-            "birth_date" => "date",
-            "selfie_verified" => "boolean",
-            "document_verified" => "boolean",
-            "is_approved" => "boolean",
-            "average_rating" => "decimal:1",
-            "daily_price_8h" => "decimal:2",
-            "daily_price_6h" => "decimal:2",
-            "daily_price_4h" => "decimal:2",
-            "daily_price_2h" => "decimal:2",
-            "total_services" => "integer",
-        ];
-    }
+  /**
+   * Get the attributes that should be cast.
+   *
+   * @return array<string, string>
+   */
+  protected function casts(): array
+  {
+    return [
+      "birth_date" => "date",
+      "selfie_verified" => "boolean",
+      "document_verified" => "boolean",
+      "is_approved" => "boolean",
+      "average_rating" => "decimal:1",
+      "daily_price_8h" => "decimal:2",
+      "daily_price_6h" => "decimal:2",
+      "daily_price_4h" => "decimal:2",
+      "daily_price_2h" => "decimal:2",
+      "total_services" => "integer",
+    ];
+  }
 
-    /**
-     * @return BelongsTo
-     */
-    public function user(): BelongsTo
-    {
-        return $this->belongsTo(User::class, "user_id");
-    }
+  /**
+   * @return BelongsTo
+   */
+  public function user(): BelongsTo
+  {
+    return $this->belongsTo(User::class, "user_id");
+  }
 
-    /**
-     * @return BelongsTo
-     */
-    public function profileMedia(): BelongsTo
-    {
-        return $this->belongsTo(Media::class, "profile_media_id");
-    }
+  /**
+   * @return BelongsTo
+   */
+  public function profileMedia(): BelongsTo
+  {
+    return $this->belongsTo(Media::class, "profile_media_id");
+  }
 
-    /**
-     * @return HasMany
+  /**
+   * @return HasMany
      */
     public function addresses(): HasMany
     {
@@ -92,47 +92,54 @@ class Provider extends Model
             ->where('source', 'provider');
     }
 
-    /**
-     * @return HasOne
-     */
-    public function primaryAddress(): HasOne
-    {
-        return $this->hasOne(Address::class, 'source_id')
-            ->where('source', 'provider')
-            ->where('is_primary', true);
-    }
+    // /**
+    //  * @return HasOne
+    //  */
+    // public function primaryAddress(): HasOne
+    // {
+    //     return $this->hasOne(Address::class, 'source_id')
+    //         ->where('source', 'provider')
+    //         ->where('is_primary', true);
+    // }
 
     /**
      * @return HasMany
-     */
-    public function blockedClients()
-    {
-        return $this->hasMany(ProviderClientBlock::class);
-    }
+   */
+  public function blockedClients()
+  {
+    return $this->hasMany(ProviderClientBlock::class);
+  }
 
-    /**
-     * @return \Illuminate\Database\Eloquent\Relations\HasMany
-     */
-    public function blockedByClients()
-    {
-        return $this->hasMany(ClientProviderBlock::class);
-    }
+  /**
+   * @return \Illuminate\Database\Eloquent\Relations\HasMany
+   */
+  public function blockedByClients()
+  {
+    return $this->hasMany(ClientProviderBlock::class);
+  }
 
-    public function updateAverageRating(float $newRating): void
-    {
-      $totalReviews = Review::where('reviews.origin', 'client')
-        ->leftJoin('schedules', 'schedules.id', '=', 'reviews.schedule_id')
-        ->where('schedules.provider_id', $this->id)
-        ->count();
-
-      if ($totalReviews === 0) {
-        $this->average_rating = $newRating;
-      } else {
-        $currentTotalRating = $this->average_rating * ($totalReviews - 1);
-        $newAverage = ($currentTotalRating + $newRating) / $totalReviews;
-        $this->average_rating = round($newAverage, 2);
-      }
-
-      $this->save();
+  public function updateAverageRating(float $newRating): void
+  {
+    $totalReviews = Review::where('reviews.origin', 'client')
+      ->leftJoin('schedules', 'schedules.id', '=', 'reviews.schedule_id')
+      ->where('schedules.provider_id', $this->id)
+      ->count();
+
+    if ($totalReviews === 0) {
+      $this->average_rating = $newRating;
+    } else {
+      $currentTotalRating = $this->average_rating * ($totalReviews - 1);
+      $newAverage = ($currentTotalRating + $newRating) / $totalReviews;
+      $this->average_rating = round($newAverage, 2);
     }
+
+    $this->save();
+  }
+
+  public function primaryAddress()
+  {
+    return $this->hasOne(Address::class, "source_id")
+      ->where("source", "provider")
+      ->orderBy("is_primary", "desc");
+  }
 }

+ 4 - 0
app/Models/Schedule.php

@@ -23,12 +23,16 @@ class Schedule extends Model
         'total_amount',
         'code',
         'code_verified',
+        'offers_meal',
+        'cancel_text',
+        'cancelled_by',
     ];
 
     protected $casts = [
         'date' => 'date',
         'code_verified' => 'boolean',
         'total_amount' => 'decimal:2',
+        'offers_meal' => 'boolean',
     ];
 
     public function client()

+ 32 - 0
app/Rules/ScheduleBusinessRules.php

@@ -109,6 +109,7 @@ class ScheduleBusinessRules
       return true;
     }
 
+    // apenas para custom_schedules
     public static function validatePricePeriod($provider_id, $min_price, $max_price, $period_type)
     {
       if ($min_price < 0 || $max_price < 0) {
@@ -318,4 +319,35 @@ class ScheduleBusinessRules
             ->distinct()
             ->pluck('provider_id');
     }
+
+    /**
+     * Retorna os IDs de prestadores disponíveis em uma data específica,
+     * dentro de um conjunto pré-filtrado de IDs.
+     *
+     * Regras (em batch, sem iteração PHP):
+     *  1. Prestador tem pelo menos um ProviderWorkingDay para o day_of_week da data.
+     *  2. Prestador NÃO tem ProviderBlockedDay com period = 'all' nessa data.
+     *     (period = 'morning' ou 'afternoon' = bloqueio parcial → ainda disponível)
+     *
+     * @param string     $date_ymd   Y-m-d
+     * @param Collection $providerIds  conjunto de IDs a filtrar
+     * @return Collection
+     */
+    public static function getAvailableProviderIdsForDate(string $date_ymd, Collection $providerIds): Collection
+    {
+        $dayOfWeek = Carbon::parse($date_ymd)->dayOfWeek;
+
+        $withWorkingDay = ProviderWorkingDay::whereIn('provider_id', $providerIds)
+            ->where('day', $dayOfWeek)
+            ->pluck('provider_id')
+            ->unique();
+
+        $fullyBlockedIds = ProviderBlockedDay::whereIn('provider_id', $withWorkingDay)
+            ->where('date', $date_ymd)
+            ->where('period', 'all')
+            ->pluck('provider_id')
+            ->unique();
+
+        return $withWorkingDay->diff($fullyBlockedIds)->values();
+    }
 }

+ 73 - 1
app/Services/DashboardService.php

@@ -110,6 +110,10 @@ class DashboardService
 
     $blockedProviderIds       = ScheduleBusinessRules::getBlockedProviderIdsForClient($cliente->id);
     $providersWithWorkingDays = ScheduleBusinessRules::getProviderIdsWithWorkingDays();
+    $clientAddress = Address::where('source', 'client')
+      ->where('source_id', $cliente->id)
+      ->orderBy('is_primary', 'desc')
+      ->first();
 
     $clientPrimaryAddress = Address::where('source', 'client')
       ->where('source_id', $cliente->id)
@@ -117,6 +121,16 @@ class DashboardService
       ->first();
 
     $providersClose = Provider::leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
+      ->leftJoin(DB::raw("
+        (
+          SELECT DISTINCT ON (source_id)
+            *
+          FROM addresses
+          WHERE source = 'provider'
+          ORDER BY source_id, is_primary DESC
+        ) as provider_address
+      "), 'provider_address.source_id', '=', 'providers.id')
+      ->where('provider_address.city_id', '=', $clientAddress->city_id)
       ->leftJoin('addresses as provider_address', function ($join) {
         $join->on('provider_address.source_id', '=', 'providers.id')
           ->where('provider_address.source', 'provider')
@@ -132,7 +146,6 @@ class DashboardService
       ->select(
         'providers.id as provider_id',
         'provider_user.name as provider_name',
-        'provider_address.district',
         'provider_address.id as address_id',
         'providers.average_rating',
         'providers.total_services',
@@ -150,9 +163,38 @@ class DashboardService
       )
       ->get();
 
+    $pendingSchedules = Schedule::with('address:district,address,number,source_id,source,id,address_type')
+      ->where('schedules.client_id', $cliente->id)
+      ->whereIn('schedules.status', ['pending', 'accepted'])
+      ->where('schedules.schedule_type', 'default')
+      ->leftJoin('providers', 'providers.id', '=', 'schedules.provider_id')
+      ->leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
+      ->select(
+        'schedules.id',
+        'schedules.provider_id',
+        'provider_user.name as provider_name',
+        'schedules.date',
+        DB::raw("TO_CHAR(schedules.date, 'DD \"de\" TMMonth \"de\" YYYY') as formatted_date"),
+        'schedules.start_time',
+        'schedules.end_time',
+        'schedules.period_type',
+        'schedules.address_id',
+        'schedules.status',
+        'schedules.total_amount',
+        DB::raw("(SELECT district FROM addresses WHERE source = 'provider' AND source_id = schedules.provider_id and deleted_at is null ORDER BY is_primary DESC LIMIT 1) as provider_district"),
+        DB::raw("CASE
+          WHEN (now() - schedules.created_at) < INTERVAL '1 hour' THEN CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 60), 'min')
+          WHEN (now() - schedules.created_at) < INTERVAL '1 day' THEN CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 3600), 'h')
+          ELSE CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 86400), 'd')
+        END as time_since_request"),
+      )
+      ->orderBy('schedules.date', 'asc')
+      ->get();
+
     return [
       'headerBar'        => $headerBar,
       'summaryInfos'     => $summaryInfos,
+      'pendingSchedules' => $pendingSchedules,
       'nextSchedules'    => $nextSchedules,
       'lastDoneSchedules' => $lastDoneSchedules,
       'favoriteProviders' => $favoriteProviders,
@@ -195,6 +237,7 @@ class DashboardService
       ->where('schedules.status', 'pending')
       ->leftJoin('clients', 'clients.id', '=', 'schedules.client_id')
       ->leftJoin('users as client_user', 'client_user.id', '=', 'clients.user_id')
+      ->leftJoin('custom_schedules', 'custom_schedules.schedule_id', '=', 'schedules.id')
       ->select(
         'schedules.id',
         'client_user.name as client_name',
@@ -207,6 +250,8 @@ class DashboardService
         'schedules.period_type',
         'schedules.schedule_type',
         'schedules.address_id',
+        'schedules.status',
+        'custom_schedules.offers_meal',
         DB::raw("CASE
           WHEN (now() - schedules.created_at) < INTERVAL '1 day' THEN CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 3600), ' hours ago')
           ELSE CONCAT(ROUND(EXTRACT(EPOCH FROM (now() - schedules.created_at)) / 86400), ' days ago')
@@ -215,6 +260,31 @@ class DashboardService
       ->orderBy('schedules.date', 'asc')
       ->get();
 
+    $todayServices = Schedule::with('address:district,address,number,source_id,source,id')
+      ->where('schedules.provider_id', $provider->id)
+      ->whereIn('schedules.status', ['accepted', 'paid', 'started'])
+      ->whereDate('schedules.date', now()->toDateString())
+      ->leftJoin('clients', 'clients.id', '=', 'schedules.client_id')
+      ->leftJoin('users as client_user', 'client_user.id', '=', 'clients.user_id')
+      ->leftJoin('custom_schedules', 'custom_schedules.schedule_id', '=', 'schedules.id')
+      ->select(
+        'schedules.id',
+        'client_user.name as client_name',
+        'schedules.date',
+        'schedules.start_time',
+        'schedules.end_time',
+        'schedules.total_amount',
+        'schedules.period_type',
+        'schedules.address_id',
+        'schedules.schedule_type',
+        'schedules.status',
+        'schedules.code_verified',
+        'schedules.status',
+        'custom_schedules.offers_meal',
+      )
+      ->orderBy('schedules.start_time', 'asc')
+      ->get();
+
     $nextSchedules = Schedule::with('address:district,address,number,source_id,source,id')
       ->where('schedules.provider_id', $provider->id)
       ->whereIn('schedules.status', ['accepted', 'paid'])
@@ -231,6 +301,7 @@ class DashboardService
         'schedules.period_type',
         'schedules.address_id',
         'schedules.schedule_type',
+        'schedules.status',
         'custom_schedules.offers_meal',
       )
       ->orderBy('schedules.date', 'asc')
@@ -269,6 +340,7 @@ class DashboardService
       'summaryInfos' => $summaryInfos,
       'priceSuggested' => $priceSuggested,
       'solicitations' => $solicitations,
+      'todayServices' => $todayServices,
       'nextSchedules' => $nextSchedules,
       'opportunities' => $opportunities,
     ];

+ 37 - 6
app/Services/ScheduleService.php

@@ -10,6 +10,7 @@ use App\Models\ProviderBlockedDay;
 use App\Models\ProviderWorkingDay;
 use App\Rules\ScheduleBusinessRules;
 use Carbon\Carbon;
+use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 
@@ -40,19 +41,21 @@ class ScheduleService
 
   public function createSingleOrMultiple(array $baseData, array $schedules)
   {
-    $createdSchedules = [];
-    foreach ($schedules as $schedule) {
-      try {
+    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);
-
-      } catch (\Exception $e) {
-        throw new \Exception(__($e->getMessage()));
       }
+      DB::commit();
+    } catch (\Exception $e) {
+      DB::rollBack();
+      throw new \Exception(__($e->getMessage()));
     }
 
     return $createdSchedules;
@@ -289,4 +292,32 @@ class ScheduleService
       throw new \Exception("Não foi possível atualizar o status do agendamento.");
     }
   }
+
+  public function cancelWithReason(int $id, string $cancelText)
+  {
+    try {
+      DB::beginTransaction();
+      $schedule = Schedule::findOrFail($id);
+
+      $allowedStatuses = ['accepted', 'paid'];
+      if (!in_array($schedule->status, $allowedStatuses)) {
+        throw new \Exception("Cancelamento não permitido para o status atual: {$schedule->status}");
+      }
+
+      $cancelled_by = Auth::user()->type;
+
+      $schedule->update([
+        'status'      => 'cancelled',
+        'cancel_text' => $cancelText,
+        'cancelled_by' => $cancelled_by,
+      ]);
+
+      DB::commit();
+      return $schedule->fresh(['client.user', 'provider.user', 'address']);
+    } catch (\Exception $e) {
+      DB::rollBack();
+      Log::error("Erro ao cancelar agendamento: " . $e->getMessage());
+      throw new \Exception("Não foi possível cancelar o agendamento.");
+    }
+  }
 }

+ 87 - 0
app/Services/SearchService.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\Address;
+use App\Models\Client;
+use App\Models\ClientFavoriteProvider;
+use App\Models\Provider;
+use App\Models\Review;
+use App\Models\Schedule;
+use App\Rules\ScheduleBusinessRules;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+class SearchService
+{
+  public function __construct() {}
+
+
+  public function buscaPrestadores(?string $name = null, ?string $date = null): array
+  {
+    $user    = Auth::user();
+    $cliente = Client::where('user_id', $user->id)->first();
+
+    $blockedProviderIds       = ScheduleBusinessRules::getBlockedProviderIdsForClient($cliente->id);
+    $providersWithWorkingDays = ScheduleBusinessRules::getProviderIdsWithWorkingDays();
+
+    $clientPrimaryAddress = Address::where('source', 'client')
+      ->where('source_id', $cliente->id)
+      ->orderBy('is_primary', 'desc')
+      ->first();
+
+    return Provider::leftJoin('users as provider_user', 'provider_user.id', '=', 'providers.user_id')
+      ->leftJoin(DB::raw("
+        (
+          SELECT DISTINCT ON (source_id)
+            *
+          FROM addresses
+          WHERE source = 'provider'
+          ORDER BY source_id, is_primary DESC
+        ) as provider_address
+      "), 'provider_address.source_id', '=', 'providers.id')
+      ->whereNotNull('provider_address.id')
+      ->where('provider_address.city_id', $clientPrimaryAddress?->city_id)
+      ->whereNotIn('providers.id', $blockedProviderIds)
+      ->whereIn('providers.id', $providersWithWorkingDays)
+      ->whereNotNull('providers.daily_price_8h')
+      ->whereNotNull('providers.daily_price_6h')
+      ->whereNotNull('providers.daily_price_4h')
+      ->whereNotNull('providers.daily_price_2h')
+      ->when($name, fn($q) => $q->where('provider_user.name', 'ILIKE', "%{$name}%"))
+      ->select(
+        'providers.id as provider_id',
+        'provider_user.name as provider_name',
+        'provider_address.district',
+        'providers.average_rating',
+        'providers.total_services',
+        'providers.daily_price_8h',
+        'providers.daily_price_6h',
+        'providers.daily_price_4h',
+        'providers.daily_price_2h',
+        'providers.created_at',
+        DB::raw("(
+          SELECT COUNT(*)
+          FROM reviews
+          LEFT JOIN schedules ON schedules.id = reviews.schedule_id
+          WHERE reviews.origin = 'provider'
+          AND schedules.provider_id = providers.id
+        ) as total_reviews"),
+      )
+      ->orderBy('providers.average_rating', 'desc')
+      ->get()
+      ->when(
+        $date,
+        fn($collection) => $collection->whereIn(
+          'provider_id',
+          ScheduleBusinessRules::getAvailableProviderIdsForDate(
+            $date,
+            $collection->pluck('provider_id')
+          )->toArray()
+        )->values()
+      )
+      ->toArray();
+  }
+
+}

+ 1 - 1
database/migrations/0001_01_01_000000_create_users_table.php

@@ -16,7 +16,7 @@ return new class extends Migration
             $table->string('name');
             $table->string('email')->unique();
             $table->timestamp('email_verified_at')->nullable();
-            $table->string('password');
+            $table->string('password')->nullable();
             $table->string('type')->default('USER');
             $table->string('language')->default('pt');
             $table->timestamps();

+ 28 - 0
database/migrations/2026_04_09_110218_add_offers_meal_to_schedules_table.php

@@ -0,0 +1,28 @@
+<?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::table('schedules', function (Blueprint $table) {
+            $table->boolean('offers_meal')->nullable()->default(null)->after('end_time');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('schedules', function (Blueprint $table) {
+            $table->dropColumn('offers_meal');
+        });
+    }
+};

+ 24 - 0
database/migrations/2026_04_14_000001_add_cancel_text_to_schedules_table.php

@@ -0,0 +1,24 @@
+<?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::table('schedules', function (Blueprint $table) {
+            $table->text('cancel_text')->nullable()->after('offers_meal');
+            $table->string('cancelled_by')->nullable()->after('cancel_text');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('schedules', function (Blueprint $table) {
+            $table->dropColumn('cancel_text');
+            $table->dropColumn('cancelled_by');
+        });
+    }
+};

+ 5 - 0
database/seeders/UserTypePermissionSeeder.php

@@ -70,6 +70,7 @@ class UserTypePermissionSeeder extends Seeder
             ['scope' => 'config.provider_blocked_day', 'bits' => 271],
             ['scope' => 'config.provider_services_types', 'bits' => 271],
             ['scope' => 'config.service_type', 'bits' => 1],
+            ['scope' => 'config.schedule', 'bits' => 271],
           ];
           $this->seedUserTypePermissions($providerPermissions, UserTypeEnum::PROVIDER->value);
           break;
@@ -83,6 +84,10 @@ class UserTypePermissionSeeder extends Seeder
             ['scope' => 'config.provider_payment_method', 'bits' => 271],
             ['scope' => 'config.client_favorite_provider', 'bits' => 271],
             ['scope' => 'config.client_payment_method', 'bits' => 271],
+            ['scope' => 'config.provider_working_day', 'bits' => 271],
+            ['scope' => 'config.provider_blocked_day', 'bits' => 271],
+            ['scope' => 'config.review', 'bits' => 271],
+            ['scope' => 'config.schedule', 'bits' => 271],
           ];
           $this->seedUserTypePermissions($clientPermissions, UserTypeEnum::CLIENT->value);
           break;

+ 1 - 0
routes/authRoutes/schedule.php

@@ -10,4 +10,5 @@ Route::get('/schedule/{id}', [ScheduleController::class, 'show'])->middleware('p
 Route::post('/schedule', [ScheduleController::class, 'store'])->middleware('permission:config.schedule,add');
 Route::put('/schedule/{id}', [ScheduleController::class, 'update'])->middleware('permission:config.schedule,edit');
 Route::patch('/schedule/{id}/status', [ScheduleController::class, 'updateStatus'])->middleware('permission:config.schedule,edit');
+Route::patch('/schedule/{id}/cancel', [ScheduleController::class, 'cancelWithReason'])->middleware('permission:config.schedule,edit');
 Route::delete('/schedule/{id}', [ScheduleController::class, 'destroy'])->middleware('permission:config.schedule,delete');

+ 6 - 0
routes/authRoutes/search.php

@@ -0,0 +1,6 @@
+<?php
+
+use App\Http\Controllers\SearchController;
+use Illuminate\Support\Facades\Route;
+
+Route::get('/prestadores-busca', [SearchController::class, 'buscaPrestadores'])->middleware('permission:dashboard,view');