| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- <?php
- namespace App\Services;
- use App\Enums\AppointmentStatusEnum;
- use App\Enums\AppointmentTypeEnum;
- use App\Enums\NotificationRecipientEnum;
- use App\Enums\PartnerAgreementTypeEnum;
- use App\Enums\UserStatusEnum;
- use App\Models\Appointment;
- use App\Models\PartnerAgreementService;
- use App\Models\User;
- use Carbon\Carbon;
- use Illuminate\Database\Eloquent\Collection;
- use Illuminate\Pagination\LengthAwarePaginator;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Str;
- class AppointmentService
- {
- private const RELATIONS = [
- 'user',
- 'userDependent',
- 'partnerAgreement',
- 'partnerAgreementService',
- 'exams.partnerAgreementService',
- ];
- public function __construct(protected NotificationService $notificationService) {}
- public function getAll(): Collection
- {
- return Appointment::with(self::RELATIONS)
- ->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;
- }
- }
|