value('habilita_agendamento_conectasus_cidadao'); return ['habilitado' => (bool) $habilitado]; } public static function prestadores() { self::validarParametrizacaoHabilitada(); $agora = now(); return DB::table('exame_imagem_parametrizacao_unidade as parametrizacao') ->leftJoin('cadastro_unidade as unidade', 'unidade.id', 'parametrizacao.unidade_id') ->leftJoin('cadastro_infraestrutura as infraestrutura', 'infraestrutura.id', 'parametrizacao.infraestrutura_id') ->where(function ($q) { $q->where('parametrizacao.executante', true)->orWhere('parametrizacao.prestador', true); }) ->whereNull('parametrizacao.deleted_at') ->whereExists(function ($q) use ($agora) { $q->select(DB::raw(1)) ->from('exame_imagem_jornada as jornada') ->whereColumn('jornada.infraestrutura_id', 'parametrizacao.infraestrutura_id') ->whereNotNull('jornada.vagas_app') ->where('jornada.vagas_app', '>', 0) ->where(function ($q2) use ($agora) { $q2->whereDate('jornada.data', '>', $agora->toDateString()) ->orWhere(function ($q3) use ($agora) { $q3->whereDate('jornada.data', $agora->toDateString()) ->whereTime('jornada.hora_fim', '>', $agora->format('H:i:s')); }); }) ->whereNull('jornada.deleted_at'); }) ->get([ 'parametrizacao.unidade_id', 'unidade.nome_fantasia', 'parametrizacao.infraestrutura_id', 'infraestrutura.nome as infraestrutura_nome', ]) ->map( fn($param) => [ 'unidade_id' => $param->unidade_id, 'nome_fantasia' => $param->nome_fantasia, 'infraestrutura_id' => $param->infraestrutura_id, 'infraestrutura_nome' => $param->infraestrutura_nome, ] ) ->unique(fn($item) => $item['unidade_id'] . '-' . $item['infraestrutura_id']) ->sortBy('nome_fantasia') ->values(); } public static function exames(): array { self::validarParametrizacaoHabilitada(); $pacienteId = auth('mobile')->user()->id; $statusRealizados = [ Status::REALIZADO, Status::RESULTADO, ]; $exameIdsBloqueadosPorValidade = DB::table('parametrizacao_exame as exame') ->join('exame_imagem_agendamento_exame as ae', 'ae.exame_id', 'exame.id') ->join('exame_imagem_agendamento as ag', 'ag.id', 'ae.agendamento_id') ->where('ae.paciente_id', $pacienteId) ->whereIn('ae.status_id', $statusRealizados) ->whereNotNull('exame.validade') ->where('exame.validade', '>', 0) ->where(fn($q) => $q->where('exame.permite_repeticao', false)->orWhereNull('exame.permite_repeticao')) ->whereRaw('ag.data >= DATE_SUB(CURDATE(), INTERVAL exame.validade DAY)') ->pluck('exame.id') ->unique() ->toArray(); $rows = DB::table('ambulatorio_atendimento_soap_solicitacao_exame_item') ->join('ambulatorio_atendimento_soap_solicitacao_exame', 'ambulatorio_atendimento_soap_solicitacao_exame.id', '=', 'ambulatorio_atendimento_soap_solicitacao_exame_item.solicitacao_exame_id') ->join('parametrizacao_exame', 'parametrizacao_exame.id', '=', 'ambulatorio_atendimento_soap_solicitacao_exame_item.exame_id') ->join('ambulatorio_atendimento_soap', 'ambulatorio_atendimento_soap.id', '=', 'ambulatorio_atendimento_soap_solicitacao_exame.soap_id') ->join('ambulatorio_atendimento', 'ambulatorio_atendimento.id', '=', 'ambulatorio_atendimento_soap.atendimento_id') ->join('painel_senha_fila', 'painel_senha_fila.id', '=', 'ambulatorio_atendimento.fila_id') ->join('cadastro_lotacao', 'cadastro_lotacao.id', '=', 'ambulatorio_atendimento_soap_solicitacao_exame.created_lotacao_id') ->join('cadastro_usuario as profissional', 'profissional.id', '=', 'cadastro_lotacao.usuario_id') ->join('cadastro_cidadao', 'cadastro_cidadao.id', '=', 'profissional.cidadao_id') ->leftJoin('cadastro_grupo', 'cadastro_grupo.id', 'parametrizacao_exame.grupo_tipo_id') ->where('parametrizacao_exame.tipo_id', ExameTipo::IMAGEM) ->whereNull('parametrizacao_exame.regulacao_tipo_id') ->whereNull('parametrizacao_exame.deleted_at') ->where('ambulatorio_atendimento_soap_solicitacao_exame_item.descartado_exame_imagem', '!=', true) ->where('ambulatorio_atendimento_soap_solicitacao_exame_item.agendado_exame_imagem', '!=', true) ->whereNotIn('parametrizacao_exame.id', $exameIdsBloqueadosPorValidade) ->where('painel_senha_fila.paciente_id', $pacienteId) ->where('ambulatorio_atendimento_soap.is_rascunho', false) ->orderBy('ambulatorio_atendimento_soap_solicitacao_exame.id') ->orderBy('parametrizacao_exame.nome') ->get( [ 'ambulatorio_atendimento_soap_solicitacao_exame.id as solicitacao_id', 'ambulatorio_atendimento_soap_solicitacao_exame.created_at as data_solicitacao', 'ambulatorio_atendimento_soap_solicitacao_exame_item.id as exame_item_id', 'parametrizacao_exame.id', 'parametrizacao_exame.codigo', 'parametrizacao_exame.nome', 'parametrizacao_exame.observacao', 'parametrizacao_exame.sexo_id', 'cadastro_grupo.id as grupo_id', 'cadastro_cidadao.nome as profissional_nome', 'profissional.id as profissional_id', ] ); return $rows ->groupBy('solicitacao_id') ->map(fn($group) => [ 'solicitacao_id' => $group->first()->solicitacao_id, 'data_solicitacao' => Carbon::parse($group->first()->data_solicitacao)->format('Y-m-d'), 'profissional_id' => $group->first()->profissional_id, 'profissional_nome' => $group->first()->profissional_nome, 'exames' => $group->map(fn($exame) => [ 'exame_item_id' => $exame->exame_item_id, 'id' => $exame->id, 'codigo' => $exame->codigo, 'nome' => $exame->nome, 'observacao' => $exame->observacao, 'sexo_id' => $exame->sexo_id, 'grupo_id' => $exame->grupo_id, ])->values()->toArray(), ]) ->values() ->toArray(); } public static function jornadas(): array { self::validarParametrizacaoHabilitada(); request()->validate( [ 'infraestrutura_id' => 'required|integer|exists:cadastro_infraestrutura,id', 'exame_id' => 'required|integer|exists:parametrizacao_exame,id', ] ); $infraestruturaId = (int) request()->infraestrutura_id; $exameId = (int) request()->exame_id; $grupoId = DB::table('parametrizacao_exame') ->where('id', $exameId) ->value('grupo_tipo_id'); $hoje = Carbon::today(); $fim = Carbon::today()->addMonths(3); $agora = now(); $jornadasRaw = DB::table('exame_imagem_jornada as jornada') ->where('jornada.infraestrutura_id', $infraestruturaId) ->when($grupoId, fn($q) => $q->where('jornada.exame_id', $grupoId)) ->whereNotNull('jornada.vagas_app') ->where('jornada.vagas_app', '>', 0) ->where(function ($q) use ($agora) { $q->whereDate('jornada.data', '>', $agora->toDateString()) ->orWhere(function ($q2) use ($agora) { $q2->whereDate('jornada.data', $agora->toDateString()) ->whereTime('jornada.hora_fim', '>', $agora->format('H:i:s')); }); }) ->whereDate('jornada.data', '<=', $fim->toDateString()) ->whereNull('jornada.deleted_at') ->orderBy('jornada.data') ->orderBy('jornada.hora_inicial') ->get( [ 'jornada.id', 'jornada.data', 'jornada.hora_inicial', 'jornada.hora_fim', 'jornada.vagas_app', 'jornada.exame_id as grupo_id', 'jornada.controle_jornada_id', 'jornada.tempo_atendimento', ] ); $jornadaIds = $jornadasRaw->pluck('id')->toArray(); $statusExcluidos = [ Status::CANCELADO, Status::FALTOU, Status::FALTA_MEDICA, ]; $consumoPorJornada = DB::table('exame_imagem_agendamento as ag') ->join('exame_imagem_agendamento_exame as ag_exame', 'ag_exame.agendamento_id', '=', 'ag.id') ->whereIn('ag.jornada_id', $jornadaIds) ->where('ag.app_conectasus_cidadao', true) ->whereNotIn('ag_exame.status_id', $statusExcluidos) ->groupBy('ag.jornada_id') ->select( 'ag.jornada_id', DB::raw('COUNT(DISTINCT ag.id) as visitas'), DB::raw('COUNT(ag_exame.id) as exames') ) ->get() ->keyBy('jornada_id'); $horariosOcupadosPorJornada = DB::table('exame_imagem_agendamento as ag') ->join('exame_imagem_agendamento_exame as ag_exame', 'ag_exame.agendamento_id', '=', 'ag.id') ->whereIn('ag.jornada_id', $jornadaIds) ->whereNotIn('ag_exame.status_id', $statusExcluidos) ->select('ag.jornada_id', DB::raw("DATE_FORMAT(ag.hora_exame, '%H:%i') as horario")) ->distinct() ->get() ->groupBy('jornada_id') ->map(fn($rows) => $rows->pluck('horario')->toArray()); $jornadas = $jornadasRaw->map(function ($jornada) use ($consumoPorJornada, $horariosOcupadosPorJornada) { $consumo = $consumoPorJornada[$jornada->id] ?? null; $consumido = $jornada->controle_jornada_id == ControleJornadaTipo::EXAME ? ($consumo->exames ?? 0) : ($consumo->visitas ?? 0); $disponivel = max(0, $jornada->vagas_app - $consumido); return [ 'id' => $jornada->id, 'data' => Carbon::parse($jornada->data)->format('Y-m-d'), 'hora_inicial' => $jornada->hora_inicial, 'hora_fim' => $jornada->hora_fim, 'tempo_atendimento' => $jornada->tempo_atendimento, 'vagas_app' => $jornada->vagas_app, 'consumido' => $consumido, 'disponivel' => $disponivel, 'controle_jornada_id' => $jornada->controle_jornada_id, 'horarios' => self::gerarHorariosDisponiveis( $jornada->data, $jornada->hora_inicial, $jornada->hora_fim, $jornada->tempo_atendimento, $horariosOcupadosPorJornada[$jornada->id] ?? [] ), ]; })->values()->toArray(); $feriados = Feriado::whereDate('data', '>=', $hoje->toDateString()) ->whereDate('data', '<=', $fim->toDateString()) ->whereNull('deleted_at') ->orderBy('data') ->get(['data', 'observacao']) ->map( fn($f) => [ 'data' => Carbon::parse($f->data)->format('Y-m-d'), 'observacao' => $f->observacao, ] ) ->values() ->toArray(); return [ 'jornadas' => $jornadas, 'feriados' => $feriados, 'cota' => self::computarCotaApp($infraestruturaId), ]; } private static function gerarHorariosDisponiveis( string $data, string $horaInicial, string $horaFim, ?int $tempoAtendimento, array $horariosOcupados ): array { if (!$tempoAtendimento || $tempoAtendimento <= 0) { return []; } $dataStr = Carbon::parse($data)->format('Y-m-d'); $horarioAtual = Carbon::parse($dataStr . ' ' . $horaInicial); $fim = Carbon::parse($dataStr . ' ' . $horaFim); $horarios = []; while ($horarioAtual->lt($fim)) { if ($horarioAtual->isPast()) { $horarioAtual->addMinutes($tempoAtendimento); continue; } $horarioStr = $horarioAtual->format('H:i'); if (!in_array($horarioStr, $horariosOcupados)) { $horarios[] = [ 'horario' => $horarioStr, 'data_hora' => $dataStr . 'T' . $horarioStr . ':00', ]; } $horarioAtual->addMinutes($tempoAtendimento); } return $horarios; } public static function agendar(): array { self::validarParametrizacaoHabilitada(); request()->validate( [ 'jornada_id' => 'required|integer|exists:exame_imagem_jornada,id', 'infraestrutura_id' => 'required|integer|exists:cadastro_infraestrutura,id', 'horario' => 'required|date_format:H:i', 'exames' => 'required|array|min:1', 'exames.*.exame_item_id' => 'required|integer|exists:ambulatorio_atendimento_soap_solicitacao_exame_item,id', 'exames.*.exame_id' => 'required|integer|exists:parametrizacao_exame,id', 'exames.*.profissional_id' => 'required|integer|exists:cadastro_usuario,id', 'observacao' => 'nullable|string', 'gestante' => 'nullable|boolean', 'nome_responsavel' => 'nullable|string', ] ); $paciente = auth('mobile')->user(); $jornada = DB::table('exame_imagem_jornada')->where('id', request()->jornada_id)->first(); if (!$jornada) { abort(Response::HTTP_NOT_FOUND, 'Jornada não encontrada.'); } $dataJornada = Carbon::parse($jornada->data)->format('Y-m-d'); $horaInicialJornada = Carbon::parse($dataJornada . ' ' . $jornada->hora_inicial); $horaFimJornada = Carbon::parse($dataJornada . ' ' . $jornada->hora_fim); $dataHoraExame = Carbon::parse($dataJornada . ' ' . request()->horario); if ($dataHoraExame->isPast()) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Não é possível agendar em horário retroativo.'); } if ($dataHoraExame->lt($horaInicialJornada) || $dataHoraExame->gte($horaFimJornada)) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'O horário informado está fora do intervalo da jornada.'); } self::validarHorarioDisponivel($jornada, request()->horario); $exameIds = collect(request()->exames)->pluck('exame_id')->toArray(); $examesAtivos = DB::table('exame_imagem_agendamento_exame as ae') ->leftJoin('parametrizacao_exame as exame', 'exame.id', 'ae.exame_id') ->where('ae.paciente_id', $paciente->id) ->whereIn('ae.exame_id', $exameIds) ->where('ae.status_id', Status::AGENDADO) ->get(['ae.exame_id', 'exame.nome as exame_nome']); if ($examesAtivos->isNotEmpty()) { $nomes = $examesAtivos->map(fn($ae) => $ae->exame_nome ?? "Exame #{$ae->exame_id}")->unique()->values(); abort(response()->json([ 'message' => 'Um ou mais exames do pedido já possuem agendamento ativo.', 'exames' => $nomes, ], Response::HTTP_UNPROCESSABLE_ENTITY)); } self::verificarVagasApp($jornada, count(request()->exames)); self::verificarCotaApp((int) request()->infraestrutura_id); $solicitanteInfraId = DB::table('cadastro_infraestrutura') ->where('unidade_id', $paciente->unidade_id) ->where('principal', true) ->value('id'); $solicitanteUnidadeId = $paciente->unidade_id; $ultimoNumero = DB::table('exame_imagem_agendamento') ->orderByDesc('agendamento_numero') ->value('agendamento_numero'); $agendamentoNumero = $ultimoNumero ? $ultimoNumero + 1 : 1; $agendamento = new Agendamento(); $agendamento->agendamento_numero = $agendamentoNumero; $agendamento->paciente_id = $paciente->id; $agendamento->jornada_id = $jornada->id; $agendamento->medico_id = request()->exames[0]['profissional_id']; $agendamento->profissional_id = $jornada->usuario_id; $agendamento->data = $jornada->data; $agendamento->exame_id = $jornada->exame_id; $agendamento->hora_exame = request()->horario; $agendamento->unidade_id = $jornada->unidade_id; $agendamento->infraestrutura_id = $jornada->infraestrutura_id; $agendamento->unidade_solicitante_id = $solicitanteUnidadeId; $agendamento->infraestrutura_solicitante_id = $solicitanteInfraId; $agendamento->observacao = request()->observacao; $agendamento->gestante = request()->gestante ?? false; $agendamento->urgente = false; $agendamento->nome_responsavel = request()->nome_responsavel; $agendamento->retroativo = false; $agendamento->app_conectasus_cidadao = true; $agendamento->save(); foreach (request()->exames as $exameData) { $agendamentoExame = new AgendamentoExame(); $agendamentoExame->agendamento_id = $agendamento->id; $agendamentoExame->paciente_id = $paciente->id; $agendamentoExame->exame_id = $exameData['exame_id']; $agendamentoExame->status_id = Status::AGENDADO; $agendamentoExame->soap_solicitacao_exame_item_id = $exameData['exame_item_id']; $agendamentoExame->save(); AtendimentoSoapSolicitacaoExameItem::findOrFail($exameData['exame_item_id']) ->update(['agendado_exame_imagem' => true]); } return [ 'agendamento_id' => $agendamento->id, 'agendamento_numero' => $agendamento->agendamento_numero, ]; } private static function validarHorarioDisponivel(object $jornada, string $horario): void { $statusExcluidos = [ Status::CANCELADO, Status::FALTOU, Status::FALTA_MEDICA, ]; $ocupado = DB::table('exame_imagem_agendamento as ag') ->join('exame_imagem_agendamento_exame as ag_exame', 'ag_exame.agendamento_id', '=', 'ag.id') ->where('ag.jornada_id', $jornada->id) ->whereTime('ag.hora_exame', $horario) ->whereNotIn('ag_exame.status_id', $statusExcluidos) ->exists(); if ($ocupado) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Este horário não está mais disponível. Selecione outro horário.'); } } private static function verificarVagasApp(object $jornada, int $quantidadeExames): void { $statusExcluidos = [ Status::CANCELADO, Status::FALTOU, Status::FALTA_MEDICA, ]; $query = DB::table('exame_imagem_agendamento as ag') ->join('exame_imagem_agendamento_exame as ag_exame', 'ag_exame.agendamento_id', '=', 'ag.id') ->where('ag.jornada_id', $jornada->id) ->where('ag.app_conectasus_cidadao', true) ->whereNotIn('ag_exame.status_id', $statusExcluidos); if ($jornada->controle_jornada_id == ControleJornadaTipo::EXAME) { $consumido = $query->count('ag_exame.id'); $necessario = $quantidadeExames; } else { $consumido = $query->distinct()->count('ag.id'); $necessario = 1; } if ($consumido + $necessario > $jornada->vagas_app) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Não há vagas disponíveis para o aplicativo nesta jornada.'); } } private static function verificarCotaApp(int $infraestruturaId): void { $cota = self::computarCotaApp($infraestruturaId); if ($cota && $cota['bloqueado']) { abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'O limite de agendamentos pelo aplicativo foi atingido para este período.'); } } private static function computarCotaApp(int $infraestruturaId): ?array { $appConfig = DB::table('exame_imagem_parametrizacao_app_conectasus_cidadao_prestador') ->where('infraestrutura_id', $infraestruturaId) ->whereNull('deleted_at') ->first(); if (!$appConfig) { return null; } $hoje = now()->toDateString(); $statusExcluidos = [Status::CANCELADO, Status::FALTOU, Status::FALTA_MEDICA]; $cotaQuantidade = null; $cotaValor = null; if ($appConfig->percentual_quantitativo) { $prestadorQtd = DB::table('exame_imagem_parametrizacao_cota_prestador') ->where('infraestrutura_id', $infraestruturaId) ->where('cota_tipo_id', Tipo::QUANTIDADE) ->where('inicio', '<=', $hoje) ->where('fim', '>=', $hoje) ->whereNull('deleted_at') ->first(); if ($prestadorQtd) { $limite = (int) floor($prestadorQtd->cota_unidade * $appConfig->percentual_quantitativo / 100); $consumido = DB::table('exame_imagem_agendamento as ag') ->join('exame_imagem_agendamento_exame as ag_exame', 'ag_exame.agendamento_id', '=', 'ag.id') ->where('ag.infraestrutura_id', $infraestruturaId) ->where('ag.app_conectasus_cidadao', true) ->whereDate('ag.data', '>=', $prestadorQtd->inicio) ->whereDate('ag.data', '<=', $prestadorQtd->fim) ->whereNotIn('ag_exame.status_id', $statusExcluidos) ->distinct() ->count('ag.id'); $cotaQuantidade = [ 'percentual_limite' => (float) $appConfig->percentual_quantitativo, 'limite' => $limite, 'consumido' => $consumido, 'percentual_consumido' => $limite > 0 ? round($consumido / $limite * 100, 2) : 0, 'bloqueado' => $consumido >= $limite, 'periodo_inicio' => $prestadorQtd->inicio, 'periodo_fim' => $prestadorQtd->fim, ]; } } if ($appConfig->percentual_financeiro) { $prestadorValor = DB::table('exame_imagem_parametrizacao_cota_prestador') ->where('infraestrutura_id', $infraestruturaId) ->where('cota_tipo_id', Tipo::VALOR) ->where('inicio', '<=', $hoje) ->where('fim', '>=', $hoje) ->whereNull('deleted_at') ->first(); if ($prestadorValor) { $limite = round($prestadorValor->cota_valor * $appConfig->percentual_financeiro / 100, 2); $consumido = DB::table('exame_imagem_agendamento as ag') ->join('exame_imagem_agendamento_exame as ag_exame', 'ag_exame.agendamento_id', '=', 'ag.id') ->join('parametrizacao_exame as exame', 'exame.id', '=', 'ag_exame.exame_id') ->where('ag.infraestrutura_id', $infraestruturaId) ->where('ag.app_conectasus_cidadao', true) ->whereDate('ag.data', '>=', $prestadorValor->inicio) ->whereDate('ag.data', '<=', $prestadorValor->fim) ->whereNotIn('ag_exame.status_id', $statusExcluidos) ->sum('exame.valor'); $consumido = round((float) $consumido, 2); $cotaValor = [ 'percentual_limite' => (float) $appConfig->percentual_financeiro, 'limite' => $limite, 'consumido' => $consumido, 'percentual_consumido' => $limite > 0 ? round($consumido / $limite * 100, 2) : 0, 'bloqueado' => $consumido >= $limite, 'periodo_inicio' => $prestadorValor->inicio, 'periodo_fim' => $prestadorValor->fim, ]; } } if (!$cotaQuantidade && !$cotaValor) { return null; } $bloqueado = ($cotaQuantidade['bloqueado'] ?? false) || ($cotaValor['bloqueado'] ?? false); return [ 'bloqueado' => $bloqueado, 'quantidade' => $cotaQuantidade, 'valor' => $cotaValor, ]; } private static function validarParametrizacaoHabilitada(): void { $habilitado = DB::table('exame_imagem_parametrizacao_integracao_aplicativo') ->value('habilita_agendamento_conectasus_cidadao'); if (!$habilitado) { abort(Response::HTTP_FORBIDDEN, 'O agendamento de exame de imagem pelo ConectaSUS Cidadão não está habilitado.'); } } }