init commit
All checks were successful
build-api-image / build (push) Successful in 51s

This commit is contained in:
Antonio Lopes dos Santos
2026-08-04 16:52:02 -03:00
commit afec302c3e
11594 changed files with 1636728 additions and 0 deletions

View File

@@ -0,0 +1,618 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\Agendamento;
use App\Enums\Administracao\Modulo as ModuloEnum;
use App\Enums\Ambulatorio\AgendamentoStatus as AgendamentoStatusEnum;
use App\Enums\Ambulatorio\AgendamentoTipo as AgendamentoTipoEnum;
use App\Models\Ambulatorio\Agendamento;
use App\Models\Ambulatorio\Jornada;
use App\Models\Cadastro\Feriado\Feriado;
use App\Services\Ambulatorio\AgendamentoService as AmbulatorioAgendamentoService;
use App\Services\Ambulatorio\ParametrizacaoService;
use App\Services\Service;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\Response;
class AgendamentoService extends Service
{
public static function especialidades()
{
request()->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)
);
}
}