orderBy('date', 'desc') ->get(); } public function getAllByUser(int $userId): Collection { return Appointment::with(self::RELATIONS) ->where('user_id', $userId) ->orderByRaw('COALESCE(appointments.requested_at, appointments.created_at) DESC') ->orderBy('id', 'desc') ->get(); } public function getAllByPartnerUser(int $userId): Collection { return Appointment::with(self::RELATIONS) ->whereHas('partnerAgreement', fn($q) => $q->where('user_id', $userId)) ->orderBy('date', 'desc') ->get(); } public function findById(int $id): ?Appointment { return Appointment::with(self::RELATIONS)->find($id); } public function findByIdForPartnerUser(int $id, int $userId): ?Appointment { return Appointment::with(self::RELATIONS) ->whereHas('partnerAgreement', fn($q) => $q->where('user_id', $userId)) ->find($id); } public function create(array $data): Appointment { $data['order_number'] = $this->generateOrderNumber(); $data['requested_at'] = now(); return Appointment::create($data)->load(self::RELATIONS); } public function createConsulta(array $data, User $authUser): Appointment { $creatingForOther = isset($data['user_id']) && (int) $data['user_id'] !== $authUser->id; $data['type'] = AppointmentTypeEnum::CONSULTA; if ($creatingForOther || $authUser->status === UserStatusEnum::ACTIVE) { $data['status'] = AppointmentStatusEnum::CONFIRMADO; $data['auto_approved'] = true; } else { $data['status'] = AppointmentStatusEnum::PENDENTE; $data['auto_approved'] = false; } $appointment = $this->create($data); if ($creatingForOther) { $this->notifyCreation($appointment); } return $appointment; } public function notifyCreation(Appointment $model): void { $dateStr = $model->date ? Carbon::parse($model->date)->format('d/m/Y') : null; $dependent = $model->userDependent?->name; $target = $dependent ? "para o dependente {$dependent}" : 'para você'; $message = $dateStr ? "Um agendamento #{$model->order_number} foi criado {$target}" . ($model->time ? " para {$dateStr} às {$model->time}" : " em {$dateStr}") . "." : "Um agendamento #{$model->order_number} foi criado {$target}."; $this->notificationService->createAutoForUser([ 'title' => 'Novo agendamento', 'message' => $message, 'recipient' => NotificationRecipientEnum::ASSOCIADO, 'source' => 'appointment', 'source_id' => $model->id, ], $model->user_id); } public function update(int $id, array $data, ?int $approvingUserId = null): ?Appointment { $model = Appointment::find($id); if (!$model) { return null; } if ( isset($data['status']) && $data['status'] === AppointmentStatusEnum::CONFIRMADO->value && $model->status !== AppointmentStatusEnum::CONFIRMADO ) { $data['auto_approved'] = false; $data['approved_by_user_id'] = $approvingUserId; } $model->update($data); return $model->fresh(self::RELATIONS); } public function delete(int $id): bool { $model = Appointment::find($id); if (!$model) { return false; } return $model->delete(); } public function isFrozen(Appointment $appointment): bool { if ($appointment->status === AppointmentStatusEnum::RECUSADO) { return true; } return $appointment->isExame() && $appointment->accepted_at !== null; } public function getAdminCounters(): array { return [ 'pendentes' => Appointment::where('status', AppointmentStatusEnum::PENDENTE)->count(), 'aguardando_aceite' => Appointment::where('status', AppointmentStatusEnum::AGUARDANDO_ACEITE)->count(), 'aprovados' => Appointment::where('status', AppointmentStatusEnum::CONFIRMADO)->count(), 'recusados' => Appointment::where('status', AppointmentStatusEnum::RECUSADO)->count(), ]; } public function getAllPaginated(array $filters = [], int $perPage = 10): LengthAwarePaginator { $query = Appointment::with(self::RELATIONS) ->orderBy('requested_at', 'desc'); if (!empty($filters['status'])) { $query->where('status', $filters['status']); } if (!empty($filters['type'])) { $query->where('type', $filters['type']); } if (!empty($filters['search'])) { $term = '%' . mb_strtolower($filters['search']) . '%'; $query->where(function ($q) use ($term) { $q->whereHas('user', function ($uq) use ($term) { $uq->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term]); })->orWhereHas('userDependent', function ($dq) use ($term) { $dq->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term]); })->orWhereHas('partnerAgreement', function ($pq) use ($term) { $pq->whereRaw('UNACCENT(LOWER(company_name)) LIKE UNACCENT(?)', [$term]); })->orWhereHas('partnerAgreementService', function ($sq) use ($term) { $sq->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term]); })->orWhereHas('exams.partnerAgreementService', function ($eq) use ($term) { $eq->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term]); })->orWhereRaw('UNACCENT(LOWER(order_number)) LIKE UNACCENT(?)', [$term]); }); } return $query->paginate($perPage); } public function approve(int $id, string $date, string $time, ?int $approvedByUserId = null): ?Appointment { $model = Appointment::find($id); if (!$model) return null; $model->update([ 'status' => AppointmentStatusEnum::CONFIRMADO, 'date' => $date, 'time' => $time, 'auto_approved' => false, 'approved_by_user_id' => $approvedByUserId, ]); $this->notificationService->createAutoForUser([ 'title' => 'Agendamento confirmado', 'message' => "Seu agendamento #{$model->order_number} foi confirmado para " . Carbon::parse($date)->format('d/m/Y') . " às {$time}.", 'recipient' => NotificationRecipientEnum::ASSOCIADO, 'source' => 'appointment', 'source_id' => $model->id, ], $model->user_id); return $model->fresh(self::RELATIONS); } public function reject(int $id): ?Appointment { $model = Appointment::find($id); if (!$model) return null; $model->update(['status' => AppointmentStatusEnum::RECUSADO]); $this->notificationService->createAutoForUser([ 'title' => 'Agendamento recusado', 'message' => "Seu agendamento #{$model->order_number} foi recusado.", 'recipient' => NotificationRecipientEnum::ASSOCIADO, 'source' => 'appointment', 'source_id' => $model->id, ], $model->user_id); return $model->fresh(self::RELATIONS); } public function approveByPartner(int $id, int $userId, string $date, string $time): ?Appointment { $model = Appointment::whereHas('partnerAgreement', fn($q) => $q->where('user_id', $userId))->find($id); if (!$model) return null; $model->update([ 'status' => AppointmentStatusEnum::CONFIRMADO, 'date' => $date, 'time' => $time, 'auto_approved' => false, 'approved_by_user_id' => $userId, ]); $this->notificationService->createAutoForUser([ 'title' => 'Agendamento confirmado', 'message' => "Seu agendamento #{$model->order_number} foi confirmado para " . Carbon::parse($date)->format('d/m/Y') . " às {$time}.", 'recipient' => NotificationRecipientEnum::ASSOCIADO, 'source' => 'appointment', 'source_id' => $model->id, ], $model->user_id); return $model->fresh(self::RELATIONS); } public function rejectByPartner(int $id, int $userId): ?Appointment { $model = Appointment::whereHas('partnerAgreement', fn($q) => $q->where('user_id', $userId))->find($id); if (!$model) return null; $model->update(['status' => AppointmentStatusEnum::RECUSADO]); $this->notificationService->createAutoForUser([ 'title' => 'Agendamento recusado', 'message' => "Seu agendamento #{$model->order_number} foi recusado.", 'recipient' => NotificationRecipientEnum::ASSOCIADO, 'source' => 'appointment', 'source_id' => $model->id, ], $model->user_id); return $model->fresh(self::RELATIONS); } // ------------------------------------------------------------------ // Exames // ------------------------------------------------------------------ public function getExamsByPartnerUser(int $userId): Collection { return $this->examsForPartnerQuery($userId) ->orderBy('requested_at', 'desc') ->get(); } public function findExamForPartnerUser(int $id, int $userId): ?Appointment { return $this->examsForPartnerQuery($userId)->find($id); } private function examsForPartnerQuery(int $userId) { return Appointment::with(self::RELATIONS) ->where('type', AppointmentTypeEnum::EXAME) ->whereHas('partnerAgreement', fn($q) => $q ->where('user_id', $userId) ->where('type', PartnerAgreementTypeEnum::AGREEMENT)); } public function createExam(array $data): Appointment { $serviceIds = $data['service_ids']; unset($data['service_ids']); return DB::transaction(function () use ($data, $serviceIds) { $services = PartnerAgreementService::whereIn('id', $serviceIds)->get(); $data['type'] = AppointmentTypeEnum::EXAME; $data['status'] = AppointmentStatusEnum::AGUARDANDO_ACEITE; $data['auto_approved'] = false; $data['partner_agreement_service_id'] = null; $data['service_price'] = $services->sum(fn($s) => (float) $this->examPrice($s)); $appointment = $this->create($data); foreach ($services as $service) { $appointment->exams()->create([ 'partner_agreement_service_id' => $service->id, 'service_price' => $this->examPrice($service), ]); } $this->notifyExamIssued($appointment, $services->count()); return $appointment->fresh(self::RELATIONS); }); } public function acceptExam(int $id, int $userId): ?Appointment { $model = $this->pendingExamForUser($id, $userId); if (!$model) { return null; } $model->update([ 'status' => AppointmentStatusEnum::CONFIRMADO, 'accepted_at' => now(), ]); $this->notifyPartnerExamDecision($model, accepted: true); return $model->fresh(self::RELATIONS); } public function refuseExam(int $id, int $userId, ?string $reason = null): ?Appointment { $model = $this->pendingExamForUser($id, $userId); if (!$model) { return null; } $model->update([ 'status' => AppointmentStatusEnum::RECUSADO, 'refused_at' => now(), 'refused_by_user_id' => $userId, 'refusal_reason' => $reason, ]); $this->notifyPartnerExamDecision($model, accepted: false); return $model->fresh(self::RELATIONS); } private function pendingExamForUser(int $id, int $userId): ?Appointment { return Appointment::with(self::RELATIONS) ->where('user_id', $userId) ->where('type', AppointmentTypeEnum::EXAME) ->where('status', AppointmentStatusEnum::AGUARDANDO_ACEITE) ->find($id); } private function examPrice(PartnerAgreementService $service): ?string { return $service->associate_price ?? $service->price; } private function notifyExamIssued(Appointment $model, int $examCount): void { $clinic = $model->partnerAgreement?->company_name; $dependent = $model->userDependent?->name; $target = $dependent ? "para o dependente {$dependent}" : 'para você'; $plural = $examCount === 1 ? 'exame' : 'exames'; $this->notificationService->createAutoForUser([ 'title' => 'Guia de exames para aprovação', 'message' => "{$clinic} gerou uma guia com {$examCount} {$plural} {$target}. Acesse seus agendamentos para aceitar ou recusar a guia #{$model->order_number}.", 'recipient' => NotificationRecipientEnum::ASSOCIADO, 'source' => 'appointment', 'source_id' => $model->id, ], $model->user_id); } private function notifyPartnerExamDecision(Appointment $model, bool $accepted): void { $partnerUserId = $model->partnerAgreement?->user_id; if (!$partnerUserId) { return; } $associate = $model->user?->name; $message = $accepted ? "{$associate} aceitou a guia de exames #{$model->order_number}." : "{$associate} recusou a guia de exames #{$model->order_number}. Gere uma nova guia com os ajustes necessários." . ($model->refusal_reason ? " Motivo: {$model->refusal_reason}" : ''); $this->notificationService->createAutoForUser([ 'title' => $accepted ? 'Guia de exames aceita' : 'Guia de exames recusada', 'message' => $message, 'recipient' => NotificationRecipientEnum::PARCEIRO, 'source' => 'appointment', 'source_id' => $model->id, ], $partnerUserId); } private function generateOrderNumber(): string { do { $number = 'AGD-' . strtoupper(Str::random(8)); } while (Appointment::where('order_number', $number)->exists()); return $number; } }