where('schedule_type', 'default') ->orderBy('date', 'desc') ->orderBy('start_time', 'desc') ->get(); } public function getById($id) { return Schedule::with(['client.user', 'provider.user', 'address'])->findOrFail($id); } public function create(array $data): Schedule { return data_get($this->createSingleOrMultiple([], [$data]), 0); } public function createSingleOrMultiple(array $baseData, array $schedules) { try { DB::beginTransaction(); $createdSchedules = []; foreach ($schedules as $schedule) { $datasMerged = array_merge($baseData, $schedule); if (data_get($datasMerged, 'schedule_type', 'default') === 'default') { $provider = Provider::findOrFail(data_get($datasMerged, 'provider_id')); $datasMerged['total_amount'] = $this->calculateAmount( $provider, (string) data_get($datasMerged, 'period_type'), ); } $this->validateProviderAvailability($datasMerged, null); $scheduleData = array_merge($datasMerged, [ 'code' => str_pad(random_int(0, 9999), 4, '0', STR_PAD_LEFT), ]); $newSchedule = Schedule::create($scheduleData); // NOTIFICAÇÃO PRESTADOR if ($newSchedule->provider_id) { $notificationService = app(NotificationService::class); $notificationService->create([ 'title' => __('notifications.new_schedule_request_title'), 'description' => __('notifications.new_schedule_request_description'), '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 $e; } return $createdSchedules; } public function update($id, array $data) { unset($data['status']); $schedule = Schedule::with(['provider.user', 'client.user', 'address'])->findOrFail($id); if (data_get($data, 'provider_id') !== null || data_get($data, 'period_type') !== null) { $providerId = data_get($data, 'provider_id', $schedule->provider_id); $periodType = data_get($data, 'period_type', $schedule->period_type); $provider = Provider::findOrFail($providerId); $data['total_amount'] = $this->calculateAmount($provider, $periodType); } if (data_get($data, 'date') !== null || data_get($data, 'start_time') !== null || data_get($data, 'provider_id') !== null) { $validationData = array_merge($schedule->toArray(), $data); $this->validateProviderAvailability($validationData, $id); } $schedule->update($data); return $schedule->fresh(['client.user', 'provider.user', 'address']); } public function delete($id) { $schedule = Schedule::findOrFail($id); $schedule->delete(); return $schedule; } // public function updateStatus($id, string $status, bool $fromPackage = false) { try { DB::beginTransaction(); $schedule = Schedule::with(['provider.user', 'client.user', 'address'])->findOrFail($id); if (! $fromPackage && in_array($status, ['accepted', 'rejected']) && Auth::user()->type === UserTypeEnum::PROVIDER) { $belongsToServicePackage = DB::table('service_package_items') ->where('schedule_id', $schedule->id) ->exists(); if ($belongsToServicePackage) { throw new \DomainException(__('messages.schedule_belongs_to_package_use_package_endpoint')); } } $allowedTransitions = [ 'pending' => ['accepted', 'rejected', 'cancelled'], 'accepted' => ['paid', 'cancelled'], 'paid' => ['cancelled', 'started'], 'started' => ['finished'], 'rejected' => [], 'cancelled' => [], 'finished' => [], ]; $currentStatus = $schedule->status; if (data_get($allowedTransitions, $currentStatus) === null) { throw new ScheduleStatusTransitionException; } if (! in_array($status, data_get($allowedTransitions, $currentStatus))) { throw new ScheduleStatusTransitionException; } $schedule->update(['status' => $status]); $schedule->refresh(); $currentStatus = $schedule->status; switch ($status) { case 'pending': break; case 'accepted': $notificationService = app(NotificationService::class); switch (Auth::user()->type) { case UserTypeEnum::PROVIDER: $notificationService->create([ 'title' => __('notifications.schedule_accepted_title'), 'description' => __('notifications.provider_accepted_schedule_description', ['provider' => $schedule->provider->user->name]), 'origin' => 'schedule', 'origin_id' => $schedule->id, 'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_ACCEPTED->value, 'user_id' => $schedule->client->user_id, ]); break; case UserTypeEnum::CLIENT: if ($schedule->provider_id) { $notificationService->create([ 'title' => __('notifications.proposal_accepted_title'), 'description' => __('notifications.proposal_accepted_description'), 'origin' => 'schedule', 'origin_id' => $schedule->id, 'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_PROPOSAL_ACCEPTED->value, 'user_id' => $schedule->provider->user_id, ]); } break; default: break; } break; case 'cancelled': $notificationService = app(NotificationService::class); switch (Auth::user()->type) { case UserTypeEnum::CLIENT: $notificationService->create([ 'title' => __('notifications.schedule_cancelled_title'), 'description' => __('notifications.client_cancelled_schedule_description'), 'origin' => 'schedule', 'origin_id' => $schedule->id, 'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_CLIENT_CANCELLED->value, 'user_id' => $schedule->provider->user_id, ]); break; case UserTypeEnum::PROVIDER: $notificationService->create([ 'title' => __('notifications.schedule_cancelled_title'), 'description' => __('notifications.provider_cancelled_schedule_description', ['provider' => $schedule->provider->user->name]), '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' => __('notifications.provider_on_the_way_title'), 'description' => __('notifications.provider_on_the_way_description', ['code' => $schedule->code]), 'origin' => 'schedule', 'origin_id' => $schedule->id, 'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_COMING->value, 'user_id' => $schedule->client->user_id, ]); // PRESTADOR $notificationService->create([ 'title' => __('notifications.service_start_title'), 'description' => __('notifications.service_start_description'), 'origin' => 'schedule', 'origin_id' => $schedule->id, 'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_START->value, 'user_id' => $schedule->provider->user_id, ]); break; case 'finished': $notificationService = app(NotificationService::class); // CLIENTE $notificationService->create([ 'title' => __('notifications.service_finished_title'), 'description' => __('notifications.service_finished_description'), 'origin' => 'schedule', 'origin_id' => $schedule->id, 'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_FINISHED->value, 'user_id' => $schedule->client->user_id, ]); break; case 'paid': $notificationService = app(NotificationService::class); switch (Auth::user()->type) { case UserTypeEnum::CLIENT: if ($schedule->provider_id) { $notificationService->create([ 'title' => __('notifications.payment_confirmed_title'), 'description' => __('notifications.payment_confirmed_description'), 'origin' => 'schedule', 'origin_id' => $schedule->id, 'type' => NotificationTypeEnum::SCHEDULE_PROVIDER_START->value, 'user_id' => $schedule->provider->user_id, ]); } break; default: break; } $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); break; case 'rejected': $notificationService = app(NotificationService::class); $notificationService->create([ 'title' => __('notifications.schedule_refused_title'), 'description' => __('notifications.schedule_refused_description'), 'origin' => 'schedule', 'origin_id' => $schedule->id, 'type' => NotificationTypeEnum::SCHEDULE_CLIENT_PROVIDER_REFUSED->value, 'user_id' => $schedule->client->user_id, ]); break; } DB::commit(); return $schedule->fresh(['client.user', 'provider.user', 'address']); } catch (ScheduleStatusTransitionException $e) { DB::rollBack(); throw $e; } catch (\Exception $e) { DB::rollBack(); Log::error('Erro ao atualizar status do agendamento: ' . $e->getMessage()); throw $e; } } // public function getClientProviderBlocks(int $clientId, int $providerId): array { $today = Carbon::today()->format('Y-m-d'); $schedules = Schedule::where('client_id', $clientId) ->where('provider_id', $providerId) ->whereNotIn('status', self::EXCLUDED_STATUSES) ->whereDate('date', '>=', $today) ->orderBy('date') ->orderBy('start_time') ->get(['id', 'date', 'start_time', 'end_time', 'status']); $existingSchedules = $schedules->map(function ($schedule) { return [ 'id' => $schedule->id, 'date' => Carbon::parse($schedule->date)->format('Y-m-d'), 'start_time' => $schedule->start_time, 'end_time' => $schedule->end_time, 'status' => $schedule->status, ]; })->values(); $fullyBlockedWeeks = $schedules ->groupBy(function ($schedule) { return Carbon::parse($schedule->date) ->startOfWeek(Carbon::SUNDAY) ->format('Y-m-d'); }) ->filter(function ($weekSchedules) { return $weekSchedules->count() >= 2; }) ->keys() ->values(); return [ 'existing_schedules' => $existingSchedules, 'fully_blocked_weeks' => $fullyBlockedWeeks, ]; } public function getFinished() { return Schedule::with(['client.user', 'provider.user']) ->where('status', 'finished') ->orderBy('date', 'desc') ->orderBy('start_time', 'desc') ->get(); } public function getSchedulesDefaultGroupedByClient() { $schedules = Schedule::with(['client.user', 'provider.user', 'address', 'reviews.reviewsImprovements.improvementType']) ->orderBy('id', 'desc') ->where('schedule_type', 'default') ->select( 'schedules.*' ) ->get(); $grouped = $schedules->groupBy('client_id')->map(function ($clientSchedules) { $firstSchedule = $clientSchedules->first(); return [ 'client_id' => $firstSchedule->client_id, 'client_name' => $firstSchedule->client->user->name ?? 'N/A', 'schedules' => $clientSchedules->map(function ($schedule) { return [ 'id' => $schedule->id, 'date' => $schedule->date ? Carbon::parse($schedule->date)->format('d/m/Y') : null, 'start_time' => $schedule->start_time, 'end_time' => $schedule->end_time, 'period_type' => $schedule->period_type, 'status' => $schedule->status, '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 ? [ 'id' => $schedule->address->id, 'address' => $schedule->address->address, 'complement' => $schedule->address->complement, 'zip_code' => $schedule->address->zip_code, 'city' => $schedule->address->city->name ?? '', '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(), ]; })->sortBy('id')->values(); return $grouped; } // public function cancelWithReason(int $id, string $cancelText) { try { DB::beginTransaction(); $schedule = Schedule::findOrFail($id); $allowedStatuses = ['accepted', 'paid', 'pending']; if (! in_array($schedule->status, $allowedStatuses)) { throw new ScheduleStatusTransitionException; } $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 (ScheduleStatusTransitionException $e) { DB::rollBack(); throw $e; } catch (\Exception $e) { DB::rollBack(); Log::error('Erro ao cancelar agendamento: '.$e->getMessage()); throw $e; } } // private function calculateAmount(Provider $provider, string $periodType): float { $hourlyRates = [ '2' => $provider->daily_price_2h ?? 0, '4' => $provider->daily_price_4h ?? 0, '6' => $provider->daily_price_6h ?? 0, '8' => $provider->daily_price_8h ?? 0, ]; return data_get($hourlyRates, $periodType, 0); } private function validateProviderAvailability(array $data, $excludeScheduleId = null) { $provider_id = data_get($data, 'provider_id'); $client_id = data_get($data, 'client_id'); $date = Carbon::parse(data_get($data, 'date')); $dayOfWeek = $date->dayOfWeek; $startTime = data_get($data, 'start_time'); $endTime = data_get($data, 'end_time'); $date_ymd = $date->format('Y-m-d'); $period = $startTime < '13:00:00' ? 'morning' : 'afternoon'; ScheduleBusinessRules::validateProviderVisibleToCustomers($provider_id); // bloqueio 2 schedules por semana para o mesmo client e provider ScheduleBusinessRules::validateWeeklyScheduleLimit( $client_id, $provider_id, data_get($data, 'date'), $excludeScheduleId ); // bloqueio provider trabalha no dia/periodo ScheduleBusinessRules::validateWorkingDay( $provider_id, $dayOfWeek, $period ); // bloqueio provider tem blockedday para dia/hora ScheduleBusinessRules::validateBlockedDay( $provider_id, $date->format('Y-m-d'), $startTime, $endTime ); // bloqueio provider tem outro agendamento para dia/hora ScheduleBusinessRules::validateConflictingSchedule( $provider_id, $date->format('Y-m-d'), $startTime, $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; } }