validate( [ 'unidade_id' => 'required|integer', ] ); $unidadeId = request()->unidade_id; $paciente = auth('mobile')->user(); $especialidades = DB::table('cadastro_especialidade as especialidade') ->where('especialidade.permite_integracao_conectasus_cidadao', true) ->whereExists(function ($query) use ($unidadeId) { $query->select(DB::raw(1)) ->from('ambulatorio_jornada as jornada') ->join('cadastro_lotacao as lotacao', 'lotacao.id', 'jornada.lotacao_id') ->join('cadastro_infraestrutura as infraestrutura', 'infraestrutura.id', 'lotacao.infraestrutura_id') ->join('cadastro_unidade as unidade', 'unidade.id', 'infraestrutura.unidade_id') ->whereColumn('jornada.especialidade_id', 'especialidade.id') ->where('unidade.id', $unidadeId) ->whereNull('jornada.situacao_id') ->whereDate('jornada.data', '>=', Carbon::today()) ->where(function ($q) { $q->where('jornada.quantidade_consulta_conectasus_cidadao', '>', 0) ->orWhere('jornada.quantidade_retorno_conectasus_cidadao', '>', 0); }); }) ->where(fn ($q) => self::filtrarEspecialidadePorPaciente($q, $paciente, 'especialidade')) ->orderBy('especialidade.nome') ->get(['especialidade.id', 'especialidade.nome']); return $especialidades->map(function ($especialidade) use ($paciente) { $bloqueadoAte = self::validarBloqueioFaltas($paciente->id, $especialidade->id); $especialidade->bloqueado_ate = $bloqueadoAte ? $bloqueadoAte->format('Y-m-d') : null; return $especialidade; }); } public static function vagas() { request()->validate( [ 'unidade_id' => 'required|integer', 'especialidade_id' => 'required|integer', ] ); $unidadeId = request()->unidade_id; $especialidadeId = request()->especialidade_id; $paciente = auth('mobile')->user(); $vagas = collect(); $feriadoDatas = self::getFeriadoDatas(); $jornadas = Jornada::where('especialidade_id', $especialidadeId) ->whereNull('situacao_id') ->whereDate('data', '>=', Carbon::today()) ->whereNotIn('data', $feriadoDatas) ->where(function ($q) { $q->where('quantidade_consulta_conectasus_cidadao', '>', 0) ->orWhere('quantidade_retorno_conectasus_cidadao', '>', 0); }) ->whereHas('lotacao', function ($q) use ($unidadeId) { $q->whereHas('infraestrutura', function ($q2) use ($unidadeId) { $q2->where('unidade_id', $unidadeId); }); }) ->whereHas('especialidade', fn ($q) => self::filtrarEspecialidadePorPaciente($q, $paciente)) ->with(['lotacao.usuario.cidadao']) ->orderBy('data') ->orderBy('inicio') ->get(); foreach ($jornadas as $jornada) { self::calcularVagasJornada($vagas, $jornada); } return [ 'vagas' => $vagas->sortBy('data_hora')->values(), 'feriados' => self::getFeriados(), ]; } public static function store() { request()->validate( [ 'jornada_id' => 'required|integer|exists:ambulatorio_jornada,id', 'horario' => 'required|date_format:H:i', 'observacao' => 'nullable|string|max:500', ] ); $paciente = auth('mobile')->user(); $jornada = Jornada::findOrFail(request()->jornada_id); $unidadeJornada = $jornada->lotacao->infraestrutura->unidade; if ($unidadeJornada->id !== $paciente->unidade_id) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'A unidade informada não é a sua unidade de referência.'); } $especialidadePermitida = self::filtrarEspecialidadePorPaciente( $jornada->especialidade()->getQuery(), $paciente )->exists(); if (!$especialidadePermitida) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Você não está elegível para agendar nesta especialidade.'); } $bloqueadoAte = self::validarBloqueioFaltas($paciente->id, $jornada->especialidade_id); if ($bloqueadoAte) { abort(response()->json([ 'message' => 'Você está temporariamente bloqueado para agendamento nesta especialidade devido a faltas ou cancelamentos.', 'bloqueado_ate' => $bloqueadoAte->format('Y-m-d'), ], Response::HTTP_UNPROCESSABLE_ENTITY)); } $dataHoraAgendamento = Carbon::parse($jornada->data->format('Y-m-d') . ' ' . request()->horario); if ($dataHoraAgendamento->isPast()) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Não é possível agendar em horário retroativo.'); } $ehFeriado = Feriado::whereHas('modulos', fn($q) => $q->where('identificador', ModuloEnum::AMBULATORIO)) ->whereDate('data', $jornada->data->format('Y-m-d')) ->exists(); if ($ehFeriado) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Não é possível agendar em dias de feriado.'); } self::validarAgendamentoDuplicado($paciente->id, $jornada); self::validarVagaDisponivel($jornada, request()->horario); /** @var Agendamento $agendamento */ $agendamento = DB::transaction(function () use ($jornada, $paciente) { $agendamento = new Agendamento(); $agendamento->fill([ 'jornada_id' => $jornada->id, 'tipo_id' => self::getTipoAgendamento( $paciente->id, $jornada->lotacao->infraestrutura_id, $jornada->especialidade_id ), 'horario' => request()->horario, 'paciente_id' => $paciente->id, 'status_id' => AgendamentoStatusEnum::AGENDADO, 'origem_agendamento' => 'conectasus_cidadao', 'observacao' => request()->input('observacao'), ]); $agendamento->created_lotacao_id = $jornada->lotacao_id; $agendamento->created_centro_custo_id = $jornada->created_centro_custo_id; $agendamento->updated_lotacao_id = $jornada->lotacao_id; $agendamento->updated_centro_custo_id = $jornada->created_centro_custo_id; $agendamento->save(); return $agendamento; }); $agendamento->refresh(); $profissionalNome = $jornada->lotacao->usuario->cidadao->nome ?? null; return [ 'id' => $agendamento->id, 'data_hora' => self::formatarDataHora($jornada->data, $agendamento->horario), 'especialidade' => $jornada->especialidade->nome, 'unidade' => $unidadeJornada->nome_fantasia, 'profissional' => $profissionalNome, 'status' => 'confirmado', ]; } public static function cancelar($id) { $paciente = auth('mobile')->user(); $agendamento = Agendamento::findOrFail($id); if ($agendamento->paciente_id !== $paciente->id) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Este agendamento não pertence ao cidadão autenticado.'); } if ($agendamento->status_id !== AgendamentoStatusEnum::AGENDADO) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Apenas agendamentos com status "Agendado" podem ser cancelados.'); } $jornada = $agendamento->jornada; $dataHoraConsulta = Carbon::parse($jornada->data->format('Y-m-d') . ' ' . $agendamento->horario->format('H:i:s')); $horasAntecedencia = 48; if (Carbon::now()->diffInHours($dataHoraConsulta, false) < $horasAntecedencia) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Cancelamento permitido apenas com no mínimo 48 horas de antecedência.'); } DB::transaction(function () use ($agendamento) { $agendamento->update([ 'status_id' => AgendamentoStatusEnum::CANCELADO_PELO_PACIENTE, 'motivo_cancelamento' => 'Cancelado pelo cidadão via ConectaSUS Cidadão.', ]); }); return ['message' => 'Agendamento cancelado com sucesso.']; } public static function historico() { $paciente = auth('mobile')->user(); $perPage = request()->per_page ?? 15; $status = request()->status; $statusMap = [ 'confirmado' => AgendamentoStatusEnum::AGENDADO, 'cancelado' => [AgendamentoStatusEnum::CANCELADO_PELO_PACIENTE, AgendamentoStatusEnum::CANCELADO_PELO_MEDICO], 'realizado' => AgendamentoStatusEnum::REALIZADO, 'falta' => AgendamentoStatusEnum::FALTA_DO_PACIENTE, ]; $query = DB::table('ambulatorio_agendamento as agendamento') ->join('ambulatorio_jornada as jornada', 'jornada.id', 'agendamento.jornada_id') ->join('cadastro_especialidade as especialidade', 'especialidade.id', 'jornada.especialidade_id') ->join('cadastro_lotacao as lotacao', 'lotacao.id', 'jornada.lotacao_id') ->join('cadastro_infraestrutura as infraestrutura', 'infraestrutura.id', 'lotacao.infraestrutura_id') ->join('cadastro_unidade as unidade', 'unidade.id', 'infraestrutura.unidade_id') ->leftJoin('cadastro_usuario as usuario', 'usuario.id', 'lotacao.usuario_id') ->leftJoin('cadastro_cidadao as cidadao', 'cidadao.id', 'usuario.cidadao_id') ->join('ambulatorio_agendamento_status as agendamento_status', 'agendamento_status.id', 'agendamento.status_id') ->where('agendamento.paciente_id', $paciente->id) ->when($status && isset($statusMap[$status]), function ($q) use ($status, $statusMap) { $valor = $statusMap[$status]; if (is_array($valor)) { $q->whereIn('agendamento.status_id', $valor); } else { $q->where('agendamento.status_id', $valor); } }) ->select([ 'agendamento.id', DB::raw("CONCAT(DATE_FORMAT(jornada.data, '%Y-%m-%d'), 'T', DATE_FORMAT(agendamento.horario, '%H:%i:%s')) as data_hora"), 'especialidade.id as especialidade_id', 'especialidade.nome as especialidade_nome', 'unidade.id as unidade_id', 'unidade.nome_fantasia as unidade_nome', 'cidadao.id as profissional_id', 'cidadao.nome as profissional_nome', 'agendamento.status_id', 'agendamento.origem_agendamento', 'jornada.data as jornada_data', 'agendamento.horario', ]) ->orderByDesc('jornada.data') ->orderByDesc('agendamento.horario'); $paginator = $query->paginate((int) $perPage); $items = collect($paginator->items())->map(function ($item) { $statusLabel = self::statusLabel($item->status_id); $podeCancelar = self::podeCancelar($item->status_id, $item->jornada_data, $item->horario); return [ 'id' => $item->id, 'data_hora' => $item->data_hora, 'especialidade' => [ 'id' => $item->especialidade_id, 'nome' => $item->especialidade_nome, ], 'unidade' => [ 'id' => $item->unidade_id, 'nome' => $item->unidade_nome, ], 'profissional' => $item->profissional_id ? [ 'id' => $item->profissional_id, 'nome' => $item->profissional_nome, ] : null, 'status' => $statusLabel, 'origem' => $item->origem_agendamento, 'pode_cancelar' => $podeCancelar, ]; }); return [ 'data' => $items->toArray(), 'current_page' => $paginator->currentPage(), 'last_page' => $paginator->lastPage(), 'total' => $paginator->total(), ]; } private static function calcularVagasJornada($vagas, Jornada $jornada): void { $data = $jornada->data; $inicio = Carbon::parse($data->format('Y-m-d') . ' ' . $jornada->inicio->format('H:i:s')); $profissional = null; $usuario = $jornada->lotacao->usuario ?? null; if ($usuario && $usuario->cidadao) { $profissional = [ 'id' => $usuario->cidadao->id, 'nome' => $usuario->cidadao->nome, ]; } $quantidadeSistemaConsulta = $jornada->quantidade_consulta ?? 0; $tempoSistemaConsulta = $jornada->tempo_consulta ?? 0; $quantidadeSistemaRetorno = $jornada->quantidade_retorno ?? 0; $tempoSistemaRetorno = $jornada->tempo_retorno ?? 0; $quantidadeConsulta = $jornada->quantidade_consulta_conectasus_cidadao ?? 0; $tempoConsulta = $jornada->tempo_consulta_conectasus_cidadao ?? 0; $quantidadeRetorno = $jornada->quantidade_retorno_conectasus_cidadao ?? 0; $tempoRetorno = $jornada->tempo_retorno_conectasus_cidadao ?? 0; $statusLiberamHorario = AmbulatorioAgendamentoService::getStatusLiberamHorario(); $offsetSistemaConsulta = $quantidadeSistemaConsulta * $tempoSistemaConsulta; $offsetSistemaRetorno = $quantidadeSistemaRetorno * $tempoSistemaRetorno; if ($quantidadeConsulta > 0 && $tempoConsulta > 0) { $inicioConsultaApp = $inicio->copy()->addMinutes($offsetSistemaConsulta); self::gerarVagas( $vagas, $jornada, $inicioConsultaApp, $quantidadeConsulta, $tempoConsulta, $profissional, $statusLiberamHorario ); } if ($quantidadeRetorno > 0 && $tempoRetorno > 0) { $inicioRetornoApp = $inicio->copy()->addMinutes($offsetSistemaConsulta + $quantidadeConsulta * $tempoConsulta + $offsetSistemaRetorno); self::gerarVagas( $vagas, $jornada, $inicioRetornoApp, $quantidadeRetorno, $tempoRetorno, $profissional, $statusLiberamHorario ); } } private static function gerarVagas($vagas, Jornada $jornada, Carbon $inicio, int $quantidade, int $tempo, ?array $profissional, array $statusLiberamHorario): void { $horarioAtual = $inicio->copy(); for ($i = 0; $i < $quantidade; $i++) { if ($horarioAtual->isPast()) { $horarioAtual->addMinutes($tempo); continue; } $horarioStr = $horarioAtual->format('H:i'); $agendado = Agendamento::where('jornada_id', $jornada->id) ->whereTime('horario', $horarioStr) ->where('origem_agendamento', 'conectasus_cidadao') ->whereNotIn('status_id', $statusLiberamHorario) ->exists(); if (!$agendado) { $vagas->push([ 'jornada_id' => $jornada->id, 'horario' => $horarioStr, 'data_hora' => $jornada->data->format('Y-m-d') . 'T' . $horarioStr . ':00', 'profissional' => $profissional, ]); } $horarioAtual->addMinutes($tempo); } } private static function validarBloqueioFaltas(int $pacienteId, int $especialidadeId): ?Carbon { $especialidade = DB::table('cadastro_especialidade') ->where('id', $especialidadeId) ->first(['limite_ocorrencias_paciente', 'periodo_monitoramento', 'tempo_liberacao']); if ( is_null($especialidade) || is_null($especialidade->limite_ocorrencias_paciente) || is_null($especialidade->periodo_monitoramento) ) { return null; } $dataInicio = Carbon::today()->subDays($especialidade->periodo_monitoramento); $baseQuery = fn() => DB::table('ambulatorio_agendamento as agendamento') ->join('ambulatorio_jornada as jornada', 'jornada.id', 'agendamento.jornada_id') ->where('agendamento.paciente_id', $pacienteId) ->where('jornada.especialidade_id', $especialidadeId) ->whereDate('jornada.data', '>=', $dataInicio) ->whereIn('agendamento.status_id', [ AgendamentoStatusEnum::FALTA_DO_PACIENTE, AgendamentoStatusEnum::CANCELADO_PELO_PACIENTE, ]); $ocorrencias = $baseQuery()->count(); if ($ocorrencias < $especialidade->limite_ocorrencias_paciente) { return null; } $tempoLiberacao = $especialidade->tempo_liberacao ?? 0; if ($tempoLiberacao === 0) { return null; } $ultimaOcorrencia = $baseQuery()->max('jornada.data'); if (is_null($ultimaOcorrencia)) { return null; } $dataBase = Carbon::parse($ultimaOcorrencia)->min(Carbon::today()); $dataMinima = $dataBase->addDays($tempoLiberacao); return Carbon::today()->lt($dataMinima) ? $dataMinima : null; } private static function validarAgendamentoDuplicado(int $pacienteId, Jornada $jornada): void { $especialidade = $jornada->especialidade; if ($especialidade->tratamento || $especialidade->permite_agendamento_mesmo_dia) { return; } $existe = Agendamento::where('paciente_id', $pacienteId) ->where('status_id', AgendamentoStatusEnum::AGENDADO) ->whereHas('jornada', function ($q) use ($jornada) { $q->where('especialidade_id', $jornada->especialidade_id) ->whereDate('data', '>=', Carbon::today()); }) ->exists(); if ($existe) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Você já possui um agendamento ativo para esta especialidade.'); } } private static function validarVagaDisponivel(Jornada $jornada, string $horario): void { $statusLiberamHorario = AmbulatorioAgendamentoService::getStatusLiberamHorario(); $agendado = Agendamento::where('jornada_id', $jornada->id) ->whereTime('horario', $horario) ->where('origem_agendamento', 'conectasus_cidadao') ->whereNotIn('status_id', $statusLiberamHorario) ->exists(); if ($agendado) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Esta vaga não está mais disponível. Selecione outro horário.'); } } private static function getTipoAgendamento(int $pacienteId, int $infraestruturaId, int $especialidadeId): int { $ultimaData = self::getUltimoDataAgendamento($pacienteId, $infraestruturaId, $especialidadeId); $diasParaRetorno = ParametrizacaoService::getQuantidadeDiasRetorno($infraestruturaId, $especialidadeId); return $diasParaRetorno === 0 || is_null($ultimaData) || Carbon::today()->diff($ultimaData)->days > $diasParaRetorno ? AgendamentoTipoEnum::CONSULTA : AgendamentoTipoEnum::RETORNO; } private static function getUltimoDataAgendamento(int $pacienteId, int $infraestruturaId, int $especialidadeId) { $ultimaJornada = Jornada::where('especialidade_id', $especialidadeId) ->whereHas('lotacao', fn($q) => $q->where('infraestrutura_id', $infraestruturaId)) ->whereHas( 'agendamentos', fn($q) => $q ->where('paciente_id', $pacienteId) ->where('status_id', AgendamentoStatusEnum::REALIZADO) ) ->orderByDesc('data') ->first(['data']); return is_null($ultimaJornada) ? null : $ultimaJornada->data->startOfDay(); } private static function statusLabel(int $statusId): string { switch ($statusId) { case AgendamentoStatusEnum::AGENDADO: return 'confirmado'; case AgendamentoStatusEnum::CANCELADO_PELO_PACIENTE: case AgendamentoStatusEnum::CANCELADO_PELO_MEDICO: return 'cancelado'; case AgendamentoStatusEnum::FALTA_DO_PACIENTE: return 'falta'; case AgendamentoStatusEnum::REALIZADO: return 'realizado'; default: return 'outro'; } } private static function podeCancelar(int $statusId, $jornadaData, $horario): bool { if ($statusId !== AgendamentoStatusEnum::AGENDADO) { return false; } $dataHora = Carbon::parse($jornadaData . ' ' . $horario); return Carbon::now()->diffInHours($dataHora, false) >= 48; } private static function formatarDataHora($data, $horario): string { return Carbon::parse($data->format('Y-m-d') . ' ' . $horario->format('H:i:s'))->format('Y-m-d\TH:i:s'); } private static function getFeriados(): array { return Feriado::whereHas('modulos', fn($q) => $q->where('identificador', ModuloEnum::AMBULATORIO)) ->whereDate('data', '>=', Carbon::today()) ->orderBy('data') ->get() ->map(fn($f) => [ 'data' => Carbon::parse($f->data)->format('Y-m-d'), 'observacao' => $f->observacao, ]) ->values() ->toArray(); } private static function getFeriadoDatas(): Collection { return Feriado::whereHas('modulos', fn($q) => $q->where('identificador', ModuloEnum::AMBULATORIO)) ->whereDate('data', '>=', Carbon::today()) ->pluck('data'); } private static function filtrarEspecialidadePorPaciente($query, $paciente, string $tableAlias = '') { $alias = $tableAlias ? "{$tableAlias}." : ''; $cidadao = $paciente->cidadao; $idadeAnos = $cidadao->idade['anos'] ?? null; $sexoId = $cidadao->sexo_id; return $query ->when($idadeAnos !== null, fn ($q) => $q ->where(fn ($q) => $q ->whereNull("{$alias}idade_minima") ->orWhere("{$alias}idade_minima", '<=', $idadeAnos) ) ->where(fn ($q) => $q ->whereNull("{$alias}idade_maxima") ->orWhere("{$alias}idade_maxima", '>=', $idadeAnos) ) ) ->where(fn ($q) => $q ->whereNull("{$alias}sexo_id") ->orWhere("{$alias}sexo_id", $sexoId) ); } }