where('schedule_type', 'default') ->orderBy('date', 'desc') ->orderBy('start_time', 'desc') ->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); } 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), ]); $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())); } return $createdSchedules; } public function update($id, array $data) { unset($data['status']); $schedule = Schedule::with(['provider.user', 'client.user', 'address'])->findOrFail($id); if (isset($data['provider_id']) || isset($data['period_type'])) { $providerId = $data['provider_id'] ?? $schedule->provider_id; $periodType = $data['period_type'] ?? $schedule->period_type; $provider = Provider::findOrFail($providerId); $data['total_amount'] = $this->calculateAmount($provider, $periodType); } if (isset($data['date']) || isset($data['start_time']) || isset($data['provider_id'])) { $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; } 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 $hourlyRates[$periodType] ?? 0; } private function validateProviderAvailability(array $data, $excludeScheduleId = null) { $provider_id = $data['provider_id']; $client_id = $data['client_id']; $date = Carbon::parse($data['date']); $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 ScheduleBusinessRules::validateWeeklyScheduleLimit( $client_id, $provider_id, $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; } 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 updateStatus($id, string $status) { try { DB::beginTransaction(); $schedule = Schedule::with(['provider.user', 'client.user', 'address'])->findOrFail($id); $allowedTransitions = [ 'pending' => ['accepted', 'rejected', 'cancelled'], 'accepted' => ['paid', 'cancelled'], 'paid' => ['cancelled', 'started'], 'started' => ['finished'], 'rejected' => [], 'cancelled' => [], 'finished' => [], ]; $currentStatus = $schedule->status; if (! isset($allowedTransitions[$currentStatus])) { throw new \Exception('Status atual inválido.'); } if (! in_array($status, $allowedTransitions[$currentStatus])) { throw new \Exception("Transição de status não permitida: {$currentStatus} → {$status}"); } $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' => '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 UserTypeEnum::CLIENT: if ($schedule->provider_id) { $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' => $schedule->provider->user_id, ]); } break; default: break; } break; case 'cancelled': $notificationService = app(NotificationService::class); switch (Auth::user()->type) { case UserTypeEnum::CLIENT: $notificationService->create([ 'title' => 'Agendamento cancelado!', 'description' => 'O cliente cancelou o agendamento.', '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' => 'Agendamento cancelado!', 'description' => $schedule->provider->user->name . ' cancelou sua solicitação de 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 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, ]); // PRESTADOR $notificationService->create([ 'title' => 'Início do serviço!', 'description' => 'Solicite o código ao cliente para iniciar a diária.', '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' => 'Serviço finalizado!', 'description' => 'Sua diária foi finalizada com sucesso.', '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' => '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, ]); } 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' => '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; } DB::commit(); return $schedule->fresh(['client.user', 'provider.user', 'address']); } catch (\Exception $e) { DB::rollBack(); Log::error('Erro ao atualizar status do agendamento: ' . $e->getMessage()); 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', 'pending']; 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([ 'cancel_text' => $cancelText, 'cancelled_by' => $cancelled_by, ]); $this->updateStatus($schedule->id, 'cancelled'); 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.'); } } 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, ]; } }