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)
);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\Autenticacao;
use App\Models\Cadastro\SujeitoAtencao\Cidadao;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Hash;
class AutenticacaoService
{
public function login(array $dados): array
{
$cidadao = Cidadao::where('cpf', $dados['cpf'])
->whereHas('paciente', fn ($q) => $q->whereNotNull('senha'))
->first();
if (is_null($cidadao) || !Hash::check($dados['senha'], $cidadao->paciente->senha)) {
throw new \InvalidArgumentException('CPF e/ou senha incorreto(s).', Response::HTTP_UNAUTHORIZED);
}
$usuario = $cidadao->paciente;
$token = auth('mobile')->setTTL(15)->login($usuario);
$refreshToken = auth('mobile')
->claims(['type' => 'refresh'])
->setTTL(10080)
->tokenById($usuario->id);
return [
'expiration' => time() + 15 * 60,
'token' => $token,
'refresh_token' => $refreshToken,
'usuario' => [
'cpf' => $dados['cpf'],
'nome' => $cidadao->nome,
'paciente_id' => $cidadao->paciente->id,
],
];
}
public function refresh()
{
$payload = auth('mobile')->payload();
if ($payload->get('type') !== 'refresh') {
return response()->json(['error' => 'Token inválido'], 401);
}
$user = auth('mobile')->user();
auth('mobile')->invalidate();
$accessToken = auth('mobile')
->claims(['type' => 'access'])
->setTTL(15)
->tokenById($user->id);
$refreshToken = auth('mobile')
->claims(['type' => 'refresh'])
->setTTL(10080)
->tokenById($user->id);
return [
'token' => $accessToken,
'refresh_token' => $refreshToken,
'expiration' => time() + 15 * 60,
];
}
}

View File

@@ -0,0 +1,145 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\Autenticacao;
use App\Enums\Mobile\ConectaCidadao\PrimeiroAcesso;
use App\Models\Cadastro\SujeitoAtencao\Cidadao;
use App\Models\Mobile\ConectaCidadao\ConectaCidadaoSmsCode;
use App\Services\Sms\SmsService;
use Carbon\Carbon;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Hash;
class PrimeiroAcessoService
{
public function iniciar(array $dados): array
{
$cidadao = Cidadao::where('cpf', $dados['cpf'])->first();
$telefonePrincipal = $cidadao ? $cidadao->telefone_principal : null;
$paciente = $cidadao ? $cidadao->paciente : null;
if (!is_null($paciente) && !is_null($paciente->senha)) {
throw new \InvalidArgumentException('Primeiro acesso já realizado. Utilize a tela de login.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
if (
is_null($cidadao) ||
is_null($paciente) ||
is_null($telefonePrincipal) ||
$cidadao->data_nascimento->format('Y-m-d') !== $dados['data_nascimento'] ||
$telefonePrincipal->numero !== $dados['telefone']
) {
throw new \InvalidArgumentException('Dados não conferem.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$codigo = (string) random_int(100000, 999999);
$agora = Carbon::now();
$registro = ConectaCidadaoSmsCode::updateOrCreate(
[
'cidadao_id' => $cidadao->id,
'telefone' => $telefonePrincipal->numero,
'usado' => false,
],
[
'codigo' => $codigo,
'expires_at' => $agora->copy()->addMinutes(PrimeiroAcesso::SMS_CODE_TTL_MINUTOS),
'last_sent_at' => $agora,
'tentativas_envio' => 1,
'tentativas_validacao' => 0,
]
);
$mensagem = "Seu código de segurança ConectaSus Cidadão é: {$codigo}";
if (app()->environment('production')) {
(new SmsService())->send($registro->telefone, $mensagem);
}
$retorno = [
'token' => $registro->id,
];
if (!app()->environment('production')) {
$retorno['codigo'] = $codigo;
}
return $retorno;
}
public function reenviarCodigo(int $token): void
{
$registro = ConectaCidadaoSmsCode::findOrFail($token);
if ($registro->usado || $registro->expires_at->isPast()) {
throw new \InvalidArgumentException('Código expirado. Inicie novamente o processo.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$agora = Carbon::now();
if (!is_null($registro->last_sent_at) && $agora->diffInSeconds($registro->last_sent_at) < PrimeiroAcesso::SMS_REENVIO_INTERVALO_SEGUNDOS) {
throw new \InvalidArgumentException('Aguarde antes de solicitar um novo código.', Response::HTTP_TOO_MANY_REQUESTS);
}
if ($registro->tentativas_envio >= PrimeiroAcesso::SMS_REENVIO_LIMITE) {
throw new \InvalidArgumentException('Limite de reenvios atingido.', Response::HTTP_TOO_MANY_REQUESTS);
}
$registro->tentativas_envio++;
$registro->last_sent_at = $agora;
$registro->save();
$mensagem = "Seu código de segurança ConectaSus Cidadão é: {$registro->codigo}";
if (app()->environment('production')) {
(new SmsService())->send($registro->telefone, $mensagem);
}
}
public function validarCodigo(array $dados): void
{
$registro = ConectaCidadaoSmsCode::findOrFail($dados['token']);
if ($registro->usado || $registro->expires_at->isPast()) {
throw new \InvalidArgumentException('Código expirado. Inicie novamente o processo.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
if ($registro->codigo !== $dados['codigo']) {
$registro->tentativas_validacao++;
$registro->save();
throw new \InvalidArgumentException('Código inválido.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$registro->usado = true;
$registro->save();
}
public function definirSenha(array $dados): void
{
$registro = ConectaCidadaoSmsCode::findOrFail($dados['token']);
if (!$registro->usado || $registro->expires_at->isPast()) {
throw new \InvalidArgumentException('Fluxo inválido. Valide o código de segurança novamente.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$cidadao = Cidadao::findOrFail($registro->cidadao_id);
if (is_null($cidadao->paciente)) {
throw new \InvalidArgumentException('Cidadão não encontrado.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$cidadao->paciente->senha = Hash::make($dados['senha']);
$cidadao->paciente->save();
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\Autenticacao;
use App\Enums\Mobile\ConectaCidadao\PrimeiroAcesso;
use App\Models\Cadastro\SujeitoAtencao\Cidadao;
use App\Models\Mobile\ConectaCidadao\ConectaCidadaoSmsCode;
use App\Services\Sms\SmsService;
use Carbon\Carbon;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Hash;
class RecuperacaoSenhaService
{
public function iniciar(string $cpf): array
{
$cidadao = Cidadao::where('cpf', $cpf)->first();
if ($cidadao && empty($cidadao->paciente->senha)) {
throw new \InvalidArgumentException('Cidadão não possui senha cadastrada.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$telefonePrincipal = $cidadao ? $cidadao->telefone_principal : null;
$paciente = $cidadao ? $cidadao->paciente : null;
if (is_null($cidadao) || is_null($paciente) || is_null($telefonePrincipal)) {
throw new \InvalidArgumentException('Cidadão não encontrado ou sem telefone principal cadastrado.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$codigo = (string) random_int(100000, 999999);
$agora = Carbon::now();
$registro = ConectaCidadaoSmsCode::updateOrCreate(
[
'cidadao_id' => $cidadao->id,
'telefone' => $telefonePrincipal->numero,
'usado' => false,
],
[
'codigo' => $codigo,
'expires_at' => $agora->copy()->addMinutes(PrimeiroAcesso::SMS_CODE_TTL_MINUTOS),
'last_sent_at' => $agora,
'tentativas_envio' => 1,
'tentativas_validacao' => 0,
]
);
$mensagem = "Seu código de recuperação de senha ConectaSus Cidadão é: {$codigo}";
if (app()->environment('production')) {
(new SmsService())->send($registro->telefone, $mensagem);
}
$retorno = [
'token' => $registro->id,
'telefone' => $this->mascararTelefone($telefonePrincipal->numero),
];
if (!app()->environment('production')) {
$retorno['codigo'] = $codigo;
}
return $retorno;
}
public function reenviarCodigo(int $token): void
{
$registro = ConectaCidadaoSmsCode::findOrFail($token);
if ($registro->usado || $registro->expires_at->isPast()) {
throw new \InvalidArgumentException('Código expirado. Inicie novamente o processo.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$agora = Carbon::now();
if (!is_null($registro->last_sent_at) && $agora->diffInSeconds($registro->last_sent_at) < PrimeiroAcesso::SMS_REENVIO_INTERVALO_SEGUNDOS) {
throw new \InvalidArgumentException('Aguarde antes de solicitar um novo código.', Response::HTTP_TOO_MANY_REQUESTS);
}
if ($registro->tentativas_envio >= PrimeiroAcesso::SMS_REENVIO_LIMITE) {
throw new \InvalidArgumentException('Limite de reenvios atingido.', Response::HTTP_TOO_MANY_REQUESTS);
}
$registro->tentativas_envio++;
$registro->last_sent_at = $agora;
$registro->save();
$mensagem = "Seu código de recuperação de senha ConectaSus Cidadão é: {$registro->codigo}";
if (app()->environment('production')) {
(new SmsService())->send($registro->telefone, $mensagem);
}
}
public function validarCodigo(array $dados): void
{
$registro = ConectaCidadaoSmsCode::findOrFail($dados['token']);
if ($registro->usado || $registro->expires_at->isPast()) {
throw new \InvalidArgumentException('Código expirado. Inicie novamente o processo.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
if ($registro->codigo !== $dados['codigo']) {
$registro->tentativas_validacao++;
$registro->save();
throw new \InvalidArgumentException('Código inválido.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$registro->usado = true;
$registro->save();
}
public function redefinirSenha(array $dados): void
{
$registro = ConectaCidadaoSmsCode::findOrFail($dados['token']);
if (!$registro->usado || $registro->expires_at->isPast()) {
throw new \InvalidArgumentException('Fluxo inválido. Valide o código de segurança novamente.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$cidadao = Cidadao::findOrFail($registro->cidadao_id);
if (is_null($cidadao->paciente)) {
throw new \InvalidArgumentException('Cidadão não encontrado.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$cidadao->paciente->senha = Hash::make($dados['senha']);
$cidadao->paciente->save();
}
private function mascararTelefone(string $telefone): string
{
$apenasNumeros = preg_replace('/[^0-9]/', '', $telefone);
$tamanho = strlen($apenasNumeros);
if ($tamanho < 8) {
return $telefone;
}
$fimVisivel = substr($apenasNumeros, -4);
$ddd = substr($apenasNumeros, 0, 2);
return "({$ddd}) *****-{$fimVisivel}";
}
}

View File

@@ -0,0 +1,456 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\CarteiraVacinacao;
use App\Enums\Cadastro\Vacina as VacinaEnum;
use App\Enums\Imunizacao\Estrategia;
use App\Enums\Mobile\ConectaCidadao\CarteiraVacinacao\Mensagem;
use App\Enums\Mobile\ConectaCidadao\CarteiraVacinacao\Prazo;
use App\Models\Cadastro\SujeitoAtencao\Paciente;
use App\Services\Service;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class CarteiraVacinacaoService extends Service
{
private const VACINAS_COVID = [
VacinaEnum::COVID_19_ASTRAZENECA_FIOCRUZ,
VacinaEnum::COVID_19_SINOVAC_BUTANTAN,
VacinaEnum::COVID_19_PFIZER,
VacinaEnum::COVID_19_JANSSEN,
VacinaEnum::COVID_19_ASTRAZENECA,
VacinaEnum::COVID_19_GAMALEYA,
VacinaEnum::COVID_19_BHARAT,
VacinaEnum::COVID_19_MODERNA_SPIKEVAX,
VacinaEnum::COVID_19_SINOVAC_CORONAVAC,
VacinaEnum::COVID_19_PFIZER_COMIRNATY_PEDIATRICA,
VacinaEnum::COVID_19_PFIZER_COMIRNATY_PEDIATRICA_MENOR_DE_5_ANOS,
VacinaEnum::COVID_19_PFIZER_COMIRNATYBIVALENTE,
];
public static function vacinas(): array
{
$paciente = auth('mobile')->user();
$dataNascimento = $paciente->cidadao->data_nascimento;
$regrasAtuais = $dataNascimento
? self::getRegrasAtuais($dataNascimento)
: collect();
$aplicacoes = self::getAplicacoes($paciente);
$calendario = self::getCalendarioVacinal($paciente, $regrasAtuais);
$alertas = array_values(
array_filter(
$calendario,
fn($v) => in_array(
$v['status'],
[
'em_atraso',
'proximo_do_prazo'
]
)
)
);
return [
'paciente' => self::getDadosPaciente($paciente),
'aplicacoes' => $aplicacoes,
'calendario_vacinal' => $calendario,
'alertas' => $alertas,
'mensagem_orientativa' => self::getMensagemOrientativa($calendario),
'mensagem_civp' => Mensagem::CIVP,
];
}
private static function getDadosPaciente(Paciente $paciente): array
{
$cidadao = $paciente->cidadao;
$dataNascimento = $cidadao->data_nascimento;
$hoje = Carbon::now();
$diff = $dataNascimento->diff($hoje);
return [
'nome' => $cidadao->nome,
'cpf' => $cidadao->cpf,
'cns' => $cidadao->cns,
'nome_mae' => $cidadao->nome_mae,
'sexo' => $cidadao->sexo ? $cidadao->sexo->descricao : null,
'nacionalidade' => $cidadao->nacionalidade ? $cidadao->nacionalidade->descricao : null,
'data_nascimento' => $dataNascimento->format('Y-m-d'),
'idade' => $dataNascimento->diff($hoje)->format('%ya, %mm e %dd'),
'idade_anos' => (int) $diff->y,
'idade_meses' => (int) $diff->m,
'idade_dias' => (int) $diff->d,
];
}
private static function getRegrasAtuais(Carbon $dataNascimento): Collection
{
$hoje = Carbon::now();
$idade = $dataNascimento->diff($hoje);
return DB::table('imunizacao_esquema_vacinal_regra as regra')
->join('imunizacao_esquema_vacinal_faixa_etaria as faixa', 'faixa.id', 'regra.faixa_etaria_id')
->join('cadastro_vacina as vacina', 'vacina.id', 'regra.vacina_id')
->join('imunizacao_dose as dose', 'dose.id', 'regra.dose_id')
->join('imunizacao_estrategia as estrategia', 'estrategia.id', 'regra.estrategia_id')
->where('regra.estrategia_id', Estrategia::ROTINA)
->select(
[
'vacina.id as vacina_id',
'vacina.codigo as vacina_codigo',
'vacina.nome as vacina_nome',
'vacina.abreviacao as vacina_abreviacao',
'vacina.sigla as vacina_sigla',
'dose.id as dose_id',
'dose.nome as dose_nome',
'estrategia.nome as estrategia_nome',
'faixa.inicio_ano',
'faixa.inicio_mes',
'faixa.inicio_dia',
'faixa.fim_ano',
'faixa.fim_mes',
'faixa.fim_dia'
]
)
->where(
fn($q) => $q
->where(
fn($q) => $q
->where('faixa.inicio_ano', '<', $idade->y)
->orWhere(
fn($q) => $q
->where('faixa.inicio_ano', '=', $idade->y)
->where('faixa.inicio_mes', '<', $idade->m)
)
->orWhere(
fn($q) => $q
->where('faixa.inicio_ano', '=', $idade->y)
->where('faixa.inicio_mes', '=', $idade->m)
->where('faixa.inicio_dia', '<=', $idade->d)
)
)
->where(
fn($q) => $q
->where('faixa.fim_ano', '>', $idade->y)
->orWhere(
fn($q) => $q
->where('faixa.fim_ano', '=', $idade->y)
->where('faixa.fim_mes', '>', $idade->m)
)
->orWhere(
fn($q) => $q
->where('faixa.fim_ano', '=', $idade->y)
->where('faixa.fim_mes', '=', $idade->m)
->where('faixa.fim_dia', '>=', $idade->d)
)
)
)
->get()
->keyBy(fn($r) => "{$r->vacina_id}_{$r->dose_id}");
}
private static function getAplicacoes(Paciente $paciente): array
{
$aplicacoes = DB::table('imunizacao_registro_aplicacao as ira')
->select(
[
'ira.id',
'vacina.id as vacina_id',
'vacina.codigo as vacina_codigo',
'vacina.nome as vacina_nome',
'vacina.abreviacao as vacina_abreviacao',
'vacina.sigla as vacina_sigla',
'dose.id as dose_id',
'dose.nome as dose_nome',
'estrategia_reg.nome as estrategia_nome',
'ira.data_aplicacao',
'iev.lote',
'iev.data_validade_lote',
'produtor.nome as produtor_nome',
'unidade.cnes as unidade_cnes',
'unidade.nome_fantasia as unidade_nome',
DB::raw("CASE WHEN infra.principal THEN unidade.nome_fantasia ELSE infra.nome END AS sala_vacinacao_nome"),
'aplicador_cidadao.nome as aplicador_nome',
'mun.nome as municipio_nome',
'est.uf as estado_uf'
]
)
->leftJoin('imunizacao_entrada_vacina as iev', 'iev.id', 'ira.entrada_vacina_id')
->leftJoin('imunizacao_entrada as entrada', 'entrada.id', 'iev.entrada_id')
->leftJoin('imunizacao_sala_vacinacao as sala', 'sala.id', 'entrada.sala_vacinacao_id')
->leftJoin('cadastro_infraestrutura as infra', 'infra.id', 'sala.infraestrutura_id')
->leftJoin('cadastro_unidade as unidade', 'unidade.id', 'infra.unidade_id')
->leftJoin('cadastro_endereco as end_infra', 'end_infra.id', 'infra.endereco_id')
->leftJoin('cadastro_logradouro as logr_infra', 'logr_infra.id', 'end_infra.logradouro_id')
->leftJoin('intranet.cadastro_municipio as mun', 'mun.id', 'logr_infra.municipio_id')
->leftJoin('intranet.cadastro_estado as est', 'est.id', 'mun.estado_id')
->leftJoin('cadastro_vacina as vacina', 'vacina.id', 'iev.vacina_id')
->leftJoin('imunizacao_produtor as produtor', 'produtor.id', 'iev.produtor_id')
->leftJoin('imunizacao_dose as dose', 'dose.id', 'ira.dose_id')
->leftJoin('imunizacao_estrategia as estrategia_reg', 'estrategia_reg.id', 'ira.estrategia_id')
->leftJoin('cadastro_usuario as aplicador', 'aplicador.id', 'ira.aplicador_id')
->leftJoin('cadastro_cidadao as aplicador_cidadao', 'aplicador_cidadao.id', 'aplicador.cidadao_id')
->where('ira.paciente_id', $paciente->id)
->get()
->map(
fn($item) => [
'id' => $item->id,
'tipo' => 'aplicacao',
'vacina' => [
'id' => $item->vacina_id,
'nome' => $item->vacina_nome,
'abreviacao' => $item->vacina_abreviacao,
'sigla' => $item->vacina_sigla,
'is_covid' => in_array($item->vacina_codigo, self::VACINAS_COVID),
],
'dose' => [
'id' => $item->dose_id,
'nome' => $item->dose_nome
],
'estrategia' => $item->estrategia_nome,
'data_aplicacao' => $item->data_aplicacao,
'lote' => $item->lote,
'data_validade_lote' => $item->data_validade_lote,
'fabricante' => $item->produtor_nome,
'cnes' => $item->unidade_cnes,
'unidade_saude' => $item->unidade_nome,
'sala_vacinacao' => $item->sala_vacinacao_nome,
'municipio' => $item->municipio_nome,
'uf' => $item->estado_uf,
'profissional' => $item->aplicador_nome
]
);
$atualizacoes = DB::table('imunizacao_registro_atualizacao as ira')
->select(
[
'ira.id',
'vacina.id as vacina_id',
'vacina.codigo as vacina_codigo',
'vacina.nome as vacina_nome',
'vacina.abreviacao as vacina_abreviacao',
'vacina.sigla as vacina_sigla',
'dose.id as dose_id',
'dose.nome as dose_nome',
'estrategia_reg.nome as estrategia_nome',
'ira.data_atualizacao as data_aplicacao',
'ira.lote',
'unidade.cnes as unidade_cnes',
'unidade.nome_fantasia as unidade_nome',
DB::raw("CASE WHEN infra.principal THEN unidade.nome_fantasia ELSE infra.nome END AS sala_vacinacao_nome"),
'aplicador_cidadao.nome as aplicador_nome',
'mun.nome as municipio_nome',
'est.uf as estado_uf'
]
)
->leftJoin('cadastro_vacina as vacina', 'vacina.id', 'ira.vacina_id')
->leftJoin('imunizacao_dose as dose', 'dose.id', 'ira.dose_id')
->leftJoin('imunizacao_estrategia as estrategia_reg', 'estrategia_reg.id', 'ira.estrategia_id')
->leftJoin('imunizacao_sala_vacinacao as sala', 'sala.id', 'ira.sala_vacinacao_id')
->leftJoin('cadastro_infraestrutura as infra', 'infra.id', 'sala.infraestrutura_id')
->leftJoin('cadastro_unidade as unidade', 'unidade.id', 'infra.unidade_id')
->leftJoin('cadastro_endereco as end_infra', 'end_infra.id', 'infra.endereco_id')
->leftJoin('cadastro_logradouro as logr_infra', 'logr_infra.id', 'end_infra.logradouro_id')
->leftJoin('intranet.cadastro_municipio as mun', 'mun.id', 'logr_infra.municipio_id')
->leftJoin('intranet.cadastro_estado as est', 'est.id', 'mun.estado_id')
->leftJoin('cadastro_usuario as aplicador', 'aplicador.id', 'ira.aplicador_id')
->leftJoin('cadastro_cidadao as aplicador_cidadao', 'aplicador_cidadao.id', 'aplicador.cidadao_id')
->where('ira.paciente_id', $paciente->id)
->get()
->map(
fn($item) => [
'id' => $item->id,
'tipo' => 'atualizacao',
'vacina' => [
'id' => $item->vacina_id,
'nome' => $item->vacina_nome,
'abreviacao' => $item->vacina_abreviacao,
'sigla' => $item->vacina_sigla,
'is_covid' => in_array($item->vacina_codigo, self::VACINAS_COVID),
],
'dose' => [
'id' => $item->dose_id,
'nome' => $item->dose_nome
],
'estrategia' => $item->estrategia_nome,
'data_aplicacao' => $item->data_aplicacao,
'lote' => $item->lote,
'data_validade_lote' => null,
'fabricante' => null,
'cnes' => $item->unidade_cnes,
'unidade_saude' => $item->unidade_nome,
'sala_vacinacao' => $item->sala_vacinacao_nome,
'municipio' => $item->municipio_nome,
'uf' => $item->estado_uf,
'profissional' => $item->aplicador_nome,
]
);
return $aplicacoes
->merge($atualizacoes)
->sortByDesc('data_aplicacao')
->values()
->toArray();
}
private static function getCalendarioVacinal(Paciente $paciente, Collection $regrasAtuais): array
{
if ($regrasAtuais->isEmpty()) {
return [];
}
$hoje = Carbon::now();
$aplicacoes = DB::table('imunizacao_registro_aplicacao as ira')
->join('imunizacao_entrada_vacina as iev', 'iev.id', 'ira.entrada_vacina_id')
->where('ira.paciente_id', $paciente->id)
->select(
[
'iev.vacina_id',
'ira.dose_id',
'ira.data_aplicacao'
]
)
->get()
->merge(
DB::table('imunizacao_registro_atualizacao as ira')
->where('ira.paciente_id', $paciente->id)
->select(
[
'ira.vacina_id',
'ira.dose_id',
'ira.data_atualizacao as data_aplicacao'
]
)
->get()
)
->keyBy(fn($a) => "{$a->vacina_id}_{$a->dose_id}");
$aprazamentos = DB::table('imunizacao_registro_aprazamento')
->where('paciente_id', $paciente->id)
->whereNull('registro_aplicacao_id')
->select(
[
'vacina_id',
'dose_id',
'data_aprazamento'
]
)
->get()
->keyBy(fn($a) => "{$a->vacina_id}_{$a->dose_id}");
return $regrasAtuais->map(
function ($regra) use ($aplicacoes, $aprazamentos, $hoje) {
$key = "{$regra->vacina_id}_{$regra->dose_id}";
$aplicacao = $aplicacoes->get($key);
$aprazamento = $aprazamentos->get($key);
$status = self::calcularStatus($aplicacao, $aprazamento, $hoje);
return [
'vacina' => [
'id' => $regra->vacina_id,
'nome' => $regra->vacina_nome,
'abreviacao' => $regra->vacina_abreviacao,
'sigla' => $regra->vacina_sigla,
'is_covid' => in_array($regra->vacina_codigo, self::VACINAS_COVID),
],
'dose' => [
'id' => $regra->dose_id,
'nome' => $regra->dose_nome,
],
'faixa_etaria' => self::formatarFaixaEtaria($regra),
'estrategia' => $regra->estrategia_nome,
'data_aprazamento' => $aprazamento ? $aprazamento->data_aprazamento : null,
'data_aplicacao' => $aplicacao ? $aplicacao->data_aplicacao : null,
'status' => $status,
'alerta' => in_array($status, ['em_atraso', 'proximo_do_prazo']),
];
}
)->values()->toArray();
}
private static function calcularStatus(?object $aplicacao, ?object $aprazamento, Carbon $hoje): string
{
if ($aprazamento) {
$dataAprazamento = Carbon::parse($aprazamento->data_aprazamento)->startOfDay();
$hoje = $hoje->copy()->startOfDay();
if ($dataAprazamento->lt($hoje)) {
return 'em_atraso';
}
if ($dataAprazamento->lte($hoje->addDays(Prazo::DIAS_PROXIMO_DO_PRAZO))) {
return 'proximo_do_prazo';
}
return 'em_dia';
}
if ($aplicacao) {
return 'em_dia';
}
return 'sem_registro';
}
private static function getMensagemOrientativa(array $calendario): ?string
{
$statuses = array_column($calendario, 'status');
if (in_array('em_atraso', $statuses)) {
return Mensagem::CARTEIRA_EM_ATRASO;
}
if (in_array('proximo_do_prazo', $statuses)) {
return Mensagem::CARTEIRA_PROXIMO_PRAZO;
}
if (in_array('sem_registro', $statuses)) {
return Mensagem::CARTEIRA_SEM_REGISTRO;
}
return null;
}
private static function formatarFaixaEtaria(object $regra): string
{
$inicio = self::formatarIdade($regra->inicio_ano, $regra->inicio_mes, $regra->inicio_dia);
$fim = self::formatarIdade($regra->fim_ano, $regra->fim_mes, $regra->fim_dia);
return "{$inicio} a {$fim}";
}
private static function formatarIdade(int $anos, int $meses, int $dias): string
{
$partes = [];
if ($anos > 0) {
$partes[] = $anos . ($anos === 1 ? ' ano' : ' anos');
}
if ($meses > 0) {
$partes[] = $meses . ($meses === 1 ? ' mês' : ' meses');
}
if ($dias > 0) {
$partes[] = $dias . ($dias === 1 ? ' dia' : ' dias');
}
return implode(' e ', $partes) ?: '0 dias';
}
}

View File

@@ -0,0 +1,707 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\EncaminhamentoRegulado;
use App\Enums\Parametrizacao\Exame\Tipo as ExameTipo;
use App\Enums\Regulacao\ParametrizacaoDivulgacaoAba;
use App\Enums\Regulacao\ParametrizacaoDivulgacaoColuna;
use App\Enums\Regulacao\StatusTipo;
use App\Models\Regulacao\Parametrizacao;
use App\Models\Regulacao\Solicitacao;
use App\Services\Service;
use App\Utils\MaskUtil;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class EncaminhamentoReguladoService extends Service
{
public static function meusEncaminhamentos(): array
{
$paciente = auth('mobile')->user();
$perPage = max(1, min((int) request()->input('per_page', 15), 50));
$page = max((int) request()->input('page', 1), 1);
$colunasVisiveis = self::getColunasVisiveis();
$query = self::getSolicitacoesQuery($paciente->id);
$total = $query->count();
$solicitacoes = (clone $query)
->limit($perPage)
->offset(($page - 1) * $perPage)
->get();
if ($solicitacoes->isEmpty()) {
return [
'data' => [],
'total' => 0,
'page' => $page,
'per_page' => $perPage,
'last_page' => 1,
];
}
$ids = $solicitacoes->pluck('id');
$exibeStatus = $colunasVisiveis->contains(ParametrizacaoDivulgacaoColuna::STATUS);
$itensPorSolicitacao = self::getItensPorSolicitacao($ids);
$prestadoresInternos = self::getPrestadoresInternos($solicitacoes->whereNotNull('prestador_interno_id')->pluck('prestador_interno_id')->unique());
$prestadoresExternos = self::getPrestadoresExternos($solicitacoes->whereNotNull('prestador_externo_id')->pluck('prestador_externo_id')->unique());
return [
'data' => $solicitacoes->map(
fn($s) => self::formatarSolicitacao($s, $itensPorSolicitacao, $prestadoresInternos, $prestadoresExternos, $exibeStatus)
)->values()->toArray(),
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'last_page' => (int) ceil($total / $perPage),
];
}
public static function documentos(int $id): array
{
$paciente = auth('mobile')->user();
$solicitacao = Solicitacao::where('id', $id)
->where('paciente_id', $paciente->id)
->whereNotNull('confirmado_em')
->with('itens')
->firstOrFail();
$tipos = [['tipo' => 'solicitacao', 'descricao' => 'Protocolo de Solicitação']];
if (self::hasPreparos($solicitacao)) {
$tipos[] = ['tipo' => 'preparo', 'descricao' => 'Preparos'];
}
$isAgendado = in_array($solicitacao->status->tipo_id, [StatusTipo::AGENDADO, StatusTipo::REAGENDADO]);
if ($isAgendado) {
$tipoExame = self::hasExame($solicitacao);
if ($tipoExame === ExameTipo::LABORATORIAL) {
$tipos[] = ['tipo' => 'protocolo_agendamento_exame_laboratorial', 'descricao' => 'Protocolo de Agendamento de Exame Laboratorial'];
} else if ($tipoExame === ExameTipo::IMAGEM) {
$tipos[] = ['tipo' => 'protocolo_agendamento_exame_imagem', 'descricao' => 'Protocolo de Agendamento de Exame de Imagem'];
} else if (self::hasProcedimentoCirurgico($solicitacao) && !$solicitacao->prestador_externo_id) {
$tipos[] = ['tipo' => 'protocolo_agendamento_procedimento_cirurgico', 'descricao' => 'Protocolo de Agendamento de Procedimento Cirúrgico'];
} else if (self::hasEspecialidade($solicitacao)) {
$tipos[] = ['tipo' => 'protocolo_agendamento_especialidade', 'descricao' => 'Protocolo de Agendamento de Consulta'];
}
}
return $tipos;
}
public static function documentoPorTipo(int $id, string $tipo): array
{
$paciente = auth('mobile')->user();
$solicitacao = Solicitacao::where('id', $id)
->where('paciente_id', $paciente->id)
->whereNotNull('confirmado_em')
->firstOrFail();
$parametrizacao = Parametrizacao::first();
if ($tipo === 'solicitacao') {
return self::documentoSolicitacao($solicitacao, $parametrizacao);
} else if ($tipo === 'preparo') {
return self::documentoPreparo($solicitacao);
} else if (in_array($tipo, ['protocolo_agendamento_exame_laboratorial', 'protocolo_agendamento_exame_imagem'])) {
return self::documentoProtocoloAgendamentoExame($solicitacao, $tipo, $parametrizacao);
} else if ($tipo === 'protocolo_agendamento_procedimento_cirurgico') {
return self::documentoProtocoloAgendamentoProcedimentoCirurgico($solicitacao, $parametrizacao);
} else if ($tipo === 'protocolo_agendamento_especialidade') {
return self::documentoProtocoloAgendamentoEspecialidade($solicitacao, $parametrizacao);
}
abort(404, 'Tipo de documento não encontrado.');
}
private static function getColunasVisiveis(): Collection
{
return DB::table('regulacao_parametrizacao_divulgacao_aba_coluna')
->whereIn('aba_id', [ParametrizacaoDivulgacaoAba::SOLICITACOES_EM_ESPERA, ParametrizacaoDivulgacaoAba::SOLICITACOES_ATENDIDAS])
->where('visivel', true)
->pluck('coluna_id');
}
private static function getSolicitacoesQuery(int $pacienteId)
{
$statusTiposVisiveis = DB::table('regulacao_parametrizacao_divulgacao_aba_status_tipo')
->whereIn('aba_id', [ParametrizacaoDivulgacaoAba::SOLICITACOES_EM_ESPERA, ParametrizacaoDivulgacaoAba::SOLICITACOES_ATENDIDAS])
->where('visivel', true)
->pluck('status_tipo_id');
return DB::table('regulacao_solicitacao as solicitacao')
->join('regulacao_status as status', 'status.id', 'solicitacao.status_id')
->join('regulacao_status_tipo as status_tipo', 'status_tipo.id', 'status.tipo_id')
->join('regulacao_solicitacao_prioridade as prioridade', 'prioridade.id', 'solicitacao.prioridade_id')
->join('cadastro_usuario as usuario_solicitante', 'usuario_solicitante.id', 'solicitacao.usuario_solicitante_id')
->join('cadastro_cidadao as cidadao_solicitante', 'cidadao_solicitante.id', 'usuario_solicitante.cidadao_id')
->join(
'regulacao_parametrizacao_divulgacao_regulador as regulador',
'regulador.regulacao_parametrizacao_regulador_id',
'solicitacao.regulador_id'
)
->leftJoin('sigtap.cid as cid', 'cid.id', 'solicitacao.cid_id')
->where('solicitacao.paciente_id', $pacienteId)
->whereNotNull('solicitacao.confirmado_em')
->where('regulador.ativo', true)
->whereIn('status.tipo_id', $statusTiposVisiveis)
->selectRaw("
solicitacao.id,
DATE_FORMAT(solicitacao.data, '%d/%m/%Y') as data_pedido,
cidadao_solicitante.nome as medico_solicitante,
prioridade.nome as prioridade,
IF(cid.id IS NOT NULL, CONCAT(cid.codigo, ' - ', cid.nome), NULL) as cid,
TIMESTAMPDIFF(
DAY,
(
SELECT STR_TO_DATE(CONCAT(sh.data, ' 00:00:00'), '%Y-%m-%d %H:%i:%s')
FROM regulacao_solicitacao_historico sh
JOIN regulacao_status hs ON hs.id = sh.status_id
WHERE sh.solicitacao_id = solicitacao.id
AND hs.tipo_id = " . StatusTipo::SOLICITADO . "
ORDER BY sh.created_at
LIMIT 1
),
COALESCE((
SELECT sh2.created_at
FROM regulacao_solicitacao_historico sh2
JOIN regulacao_status hs2 ON hs2.id = sh2.status_id
WHERE sh2.solicitacao_id = solicitacao.id
AND hs2.tipo_id IN (" . StatusTipo::AGENDADO . ", " . StatusTipo::REAGENDADO . ")
ORDER BY sh2.created_at
LIMIT 1
), CURRENT_TIMESTAMP)
) as tempo_espera_dias,
status_tipo.id as status_tipo_id,
status_tipo.nome as status_nome,
solicitacao.prestador_interno_id,
solicitacao.prestador_externo_id,
DATE_FORMAT(solicitacao.data_agendada, '%d/%m/%Y') as data_agendada,
DATE_FORMAT(solicitacao.hora_agendada, '%H:%i') as hora_agendada
")
->orderByDesc('solicitacao.data')
->orderByDesc('solicitacao.id');
}
private static function getItensPorSolicitacao(Collection $ids): Collection
{
return DB::table('regulacao_solicitacao_item as item')
->leftJoin('parametrizacao_exame as exame', 'exame.id', 'item.exame_id')
->leftJoin('parametrizacao_procedimento_cirurgico as procedimento_cirurgico', 'procedimento_cirurgico.id', 'item.procedimento_cirurgico_id')
->leftJoin('cadastro_especialidade as especialidade', 'especialidade.id', 'item.especialidade_id')
->whereIn('item.solicitacao_id', $ids)
->selectRaw("
item.solicitacao_id,
COALESCE(exame.nome, procedimento_cirurgico.nome, especialidade.nome) as descricao
")
->get()
->groupBy('solicitacao_id');
}
private static function getPrestadoresInternos(Collection $ids): Collection
{
if ($ids->isEmpty()) {
return collect();
}
return DB::table('regulacao_parametrizacao_prestador_interno as prestador_interno')
->join('cadastro_infraestrutura as infraestrutura', 'infraestrutura.id', 'prestador_interno.infraestrutura_id')
->join('cadastro_unidade as unidade', 'unidade.id', 'infraestrutura.unidade_id')
->leftJoin('cadastro_endereco as endereco', 'endereco.id', 'infraestrutura.endereco_id')
->leftJoin('cadastro_logradouro as logradouro', 'logradouro.id', 'endereco.logradouro_id')
->whereIn('prestador_interno.id', $ids)
->selectRaw("
prestador_interno.id,
IF(infraestrutura.principal = 1, unidade.nome_fantasia, COALESCE(infraestrutura.nome, unidade.nome_fantasia)) as nome,
infraestrutura.telefone,
logradouro.nome as logradouro_nome,
IF(endereco.numero IS NULL, 'S/N', endereco.numero) as logradouro_numero,
endereco.complemento as logradouro_complemento
")
->get()
->keyBy('id');
}
private static function getPrestadoresExternos(Collection $ids): Collection
{
if ($ids->isEmpty()) {
return collect();
}
return DB::table('regulacao_parametrizacao_prestador_externo as prestador_externo')
->join('transporte_paciente_destino as destino', 'destino.id', 'prestador_externo.destino_id')
->whereIn('prestador_externo.id', $ids)
->selectRaw("
prestador_externo.id,
destino.descricao_destino as nome,
COALESCE(destino.numero_telefone_comercial, destino.numero_telefone_fax) as telefone,
TRIM(CONCAT_WS(', ',
NULLIF(destino.nome_logradouro, ''),
NULLIF(destino.numero_logradouro, '')
)) as endereco
")
->get()
->keyBy('id');
}
private static function formatarSolicitacao(
object $s,
Collection $itensPorSolicitacao,
Collection $prestadoresInternos,
Collection $prestadoresExternos,
bool $exibeStatus
): array {
$result = [
'id' => $s->id,
'procedimentos' => self::formatarItens($itensPorSolicitacao[$s->id] ?? collect()),
'data_pedido' => $s->data_pedido,
'medico_solicitante' => $s->medico_solicitante,
'prioridade' => $s->prioridade,
'cid' => $s->cid,
'tempo_espera_dias' => is_null($s->tempo_espera_dias) ? null : (int) $s->tempo_espera_dias,
];
if ($exibeStatus) {
$result['status'] = $s->status_nome;
}
if (in_array($s->status_tipo_id, [StatusTipo::AGENDADO, StatusTipo::REAGENDADO])) {
$result['agendamento'] = self::formatarAgendamento($s, $prestadoresInternos, $prestadoresExternos);
}
return $result;
}
private static function formatarItens(Collection $itens): array
{
return $itens->pluck('descricao')->filter()->unique()->values()->toArray();
}
private static function formatarAgendamento(
object $s,
Collection $prestadoresInternos,
Collection $prestadoresExternos
): array {
$prestador = null;
if (!is_null($s->prestador_interno_id) && $prestadoresInternos->has($s->prestador_interno_id)) {
$p = $prestadoresInternos->get($s->prestador_interno_id);
$prestador = [
'nome' => $p->nome,
'telefone' => $p->telefone,
'endereco' => self::formatarEnderecoInterno($p),
];
} else if (!is_null($s->prestador_externo_id) && $prestadoresExternos->has($s->prestador_externo_id)) {
$p = $prestadoresExternos->get($s->prestador_externo_id);
$prestador = [
'nome' => $p->nome,
'telefone' => $p->telefone,
'endereco' => $p->endereco ?: null,
];
}
return [
'data' => $s->data_agendada,
'hora' => $s->hora_agendada,
'prestador' => $prestador,
];
}
private static function formatarEnderecoInterno(object $prestador): ?string
{
if (!$prestador->logradouro_nome) {
return null;
}
return trim(
$prestador->logradouro_nome
. ($prestador->logradouro_numero ? ', ' . $prestador->logradouro_numero : '')
. ($prestador->logradouro_complemento ? ' - ' . $prestador->logradouro_complemento : '')
);
}
private static function getNomeAtendente(Solicitacao $solicitacao): ?string
{
$lotacao = $solicitacao->createdLotacao()->withTrashed()->first();
if (!$lotacao || !$lotacao->usuario || !$lotacao->usuario->cidadao) {
return null;
}
return $lotacao->usuario->cidadao->nome;
}
private static function documentoSolicitacao(Solicitacao $solicitacao, ?Parametrizacao $parametrizacao): array
{
$itens = $solicitacao->itens()->with(
[
'exame',
'procedimentoCirurgico',
'especialidade.ocupacao',
]
)->get();
$examesLaboratoriais = $itens
->filter(fn($i) => !is_null($i->exame_id) && $i->exame->tipo_id === ExameTipo::LABORATORIAL)
->map(fn($i) => self::formatarItemDocumentoExame($i->exame))
->sortBy('descricao')
->values();
$exameImagem = $itens
->filter(fn($i) => !is_null($i->exame_id) && $i->exame->tipo_id === ExameTipo::IMAGEM)
->first();
$procedimentoCirurgico = $itens
->filter(fn($i) => !is_null($i->procedimento_cirurgico_id))
->first();
$especialidade = $itens
->filter(fn($i) => !is_null($i->especialidade_id))
->first();
return [
'tipo' => 'solicitacao',
'data_solicitacao' => $solicitacao->data->format('d/m/Y'),
'hipotese_diagnostica' => $solicitacao->hipotese_diagnostica_justificativa,
'peso' => $solicitacao->peso ? str_replace('.', ',', $solicitacao->peso) . ' kg' : null,
'altura' => $solicitacao->altura ? str_replace('.', ',', $solicitacao->altura) . ' cm' : null,
'exames_laboratoriais' => $examesLaboratoriais->toArray(),
'exame_imagem' => $exameImagem ? self::formatarItemDocumentoExame($exameImagem->exame) : null,
'procedimento_cirurgico' => $procedimentoCirurgico ? self::formatarItemDocumentoProcedimento($procedimentoCirurgico->procedimentoCirurgico) : null,
'especialidade' => $especialidade ? self::formatarItemDocumentoEspecialidade($especialidade->especialidade) : null,
'solicitante' => [
'medico' => $solicitacao->usuarioSolicitante->cidadao->nome,
'unidade' => $solicitacao->infraestruturaSolicitante->unidade->nome_fantasia,
'infraestrutura' => $solicitacao->infraestruturaSolicitante->nome,
],
'atendente' => self::getNomeAtendente($solicitacao),
'mensagem' => $parametrizacao ? $parametrizacao->mensagem_protocolo_solicitacao : null,
];
}
private static function formatarItemDocumentoExame(object $exame): array
{
$codigo = empty($exame->codigo) ? null : MaskUtil::getProcedimentoMasked($exame->codigo);
return [
'codigo' => $codigo,
'descricao' => $exame->nome,
];
}
private static function formatarItemDocumentoProcedimento(object $procedimento): array
{
return [
'codigo' => MaskUtil::getProcedimentoMasked($procedimento->procedimento_codigo),
'descricao' => $procedimento->nome,
];
}
private static function formatarItemDocumentoEspecialidade(object $especialidade): array
{
return [
'codigo' => $especialidade->ocupacao->codigo ?? null,
'descricao' => $especialidade->nome,
];
}
private static function hasPreparos(Solicitacao $solicitacao): bool
{
if (is_null($solicitacao->prestador_interno_id) && is_null($solicitacao->prestador_externo_id)) {
return false;
}
$hasPreparo = false;
$prestadorRelationName = is_null($solicitacao->prestador_interno_id) ? 'prestadorExterno' : 'prestadorInterno';
$solicitacao->itens->each(function ($item) use ($solicitacao, &$hasPreparo, $prestadorRelationName) {
if ($hasPreparo) {
return false;
}
$hasPreparoExame = fn() => !is_null($item->exame_id)
&& $solicitacao->{$prestadorRelationName}->exames()
->where('id', $item->exame_id)
->wherePivotNotNull('observacoes')
->exists();
$hasPreparoProcedimentoCirurgico = fn() => !is_null($item->procedimento_cirurgico_id)
&& $solicitacao->{$prestadorRelationName}->procedimentosCirurgicos()
->where('id', $item->procedimento_cirurgico_id)
->wherePivotNotNull('observacoes')
->exists();
$hasPreparoEspecialidade = fn() => !is_null($item->especialidade_id)
&& $solicitacao->{$prestadorRelationName}->especialidades()
->where('id', $item->especialidade_id)
->wherePivotNotNull('observacoes')
->exists();
if ($hasPreparoExame() || $hasPreparoProcedimentoCirurgico() || $hasPreparoEspecialidade()) {
$hasPreparo = true;
}
});
return $hasPreparo;
}
private static function hasExame(Solicitacao $solicitacao)
{
$item = $solicitacao->itens()->with('exame')->whereNotNull('exame_id')->first();
return ($item && $item->exame) ? $item->exame->tipo_id : false;
}
private static function hasProcedimentoCirurgico(Solicitacao $solicitacao): bool
{
return $solicitacao->itens()->whereNotNull('procedimento_cirurgico_id')->exists();
}
private static function hasEspecialidade(Solicitacao $solicitacao): bool
{
return $solicitacao->itens()->whereNotNull('especialidade_id')->exists();
}
private static function documentoPreparo(Solicitacao $solicitacao): array
{
$itens = collect();
$prestadorRelationName = is_null($solicitacao->prestador_interno_id) ? 'prestadorExterno' : 'prestadorInterno';
$solicitacao->itens->each(function ($item) use ($solicitacao, $prestadorRelationName, $itens) {
if (!is_null($item->exame_id)) {
$solicitacao->{$prestadorRelationName}->exames()
->where('id', $item->exame_id)
->wherePivotNotNull('observacoes')
->get()
->each(function ($exame) use ($itens) {
$itens->push(
[
'descricao' => empty($exame->codigo)
? $exame->nome
: MaskUtil::getProcedimentoMasked($exame->codigo) . " - {$exame->nome}",
'observacoes' => $exame->pivotPrestadorExame->observacoes,
]
);
});
}
if (!is_null($item->procedimento_cirurgico_id)) {
$solicitacao->{$prestadorRelationName}->procedimentosCirurgicos()
->where('id', $item->procedimento_cirurgico_id)
->wherePivotNotNull('observacoes')
->get()
->each(function ($procedimento) use ($itens) {
$itens->push(
[
'descricao' => MaskUtil::getProcedimentoMasked($procedimento->procedimento_codigo) . " - {$procedimento->nome}",
'observacoes' => $procedimento->pivotPrestadorProcedimentoCirurgico->observacoes,
]
);
});
}
if (!is_null($item->especialidade_id)) {
$solicitacao->{$prestadorRelationName}->especialidades()
->where('id', $item->especialidade_id)
->wherePivotNotNull('observacoes')
->get()
->each(function ($especialidade) use ($itens) {
$itens->push(
[
'descricao' => "{$especialidade->ocupacao->codigo} - {$especialidade->nome}",
'observacoes' => $especialidade->pivotPrestadorEspecialidade->observacoes,
]
);
});
}
});
return [
'tipo' => 'preparo',
'itens' => $itens->sortBy('descricao')->values()->toArray(),
];
}
private static function formatarPrestadorProtocolo(Solicitacao $solicitacao): ?array
{
if ($solicitacao->prestadorInterno) {
$infra = $solicitacao->prestadorInterno->infraestrutura;
return [
'nome' => $infra->principal ? $infra->unidade->nome_fantasia : ($infra->nome ?? $infra->unidade->nome_fantasia),
'telefone' => $infra->telefone,
'endereco' => $infra->endereco ? $infra->endereco->enderecoFormatado : null,
];
}
if ($solicitacao->prestadorExterno) {
$destino = $solicitacao->prestadorExterno->destino;
return [
'nome' => $destino->descricao_destino,
'telefone' => $destino->numero_telefone_comercial ?? $destino->numero_telefone_fax,
'endereco' => trim(implode(', ', array_filter(
[
$destino->nome_logradouro,
$destino->numero_logradouro,
$destino->nome_bairro,
]
))) ?: null,
];
}
return null;
}
private static function formatarPacienteProtocolo(Solicitacao $solicitacao): array
{
$cidadao = $solicitacao->paciente->cidadao;
return [
'nome' => $cidadao->nome,
'nome_social' => $cidadao->nome_social,
'prontuario' => $solicitacao->paciente->prontuario,
'data_nascimento' => $cidadao->data_nascimento
? \Carbon\Carbon::parse($cidadao->data_nascimento)->format('d/m/Y')
: null,
];
}
private static function documentoProtocoloAgendamentoExame(
Solicitacao $solicitacao,
string $tipo,
?Parametrizacao $parametrizacao
): array {
$tipoExame = $tipo === 'protocolo_agendamento_exame_laboratorial' ? ExameTipo::LABORATORIAL : ExameTipo::IMAGEM;
$itens = $solicitacao->itens()->with('exame')
->whereHas('exame', function ($q) use ($tipoExame) {
$q->where('tipo_id', $tipoExame);
})
->get()
->map(function ($item) {
return [
'codigo' => empty($item->exame->codigo) ? null : MaskUtil::getProcedimentoMasked($item->exame->codigo),
'descricao' => $item->exame->nome,
];
})
->sortBy('descricao')
->values()
->toArray();
return [
'tipo' => $tipo,
'paciente' => self::formatarPacienteProtocolo($solicitacao),
'solicitante' => [
'medico' => $solicitacao->usuarioSolicitante->cidadao->nome,
'unidade' => $solicitacao->infraestruturaSolicitante->unidade->nome_fantasia,
],
'data_agendamento' => $solicitacao->data_agendada ? $solicitacao->data_agendada->format('d/m/Y') : null,
'hora_agendamento' => $solicitacao->hora_agendada ? $solicitacao->hora_agendada->format('H:i') : null,
'prestador' => self::formatarPrestadorProtocolo($solicitacao),
'itens' => $itens,
'mensagem' => $parametrizacao ? $parametrizacao->mensagem_comprovante_agendamento : null,
];
}
private static function documentoProtocoloAgendamentoProcedimentoCirurgico(
Solicitacao $solicitacao,
?Parametrizacao $parametrizacao
): array {
$item = $solicitacao->itens()->with('procedimentoCirurgico')
->whereNotNull('procedimento_cirurgico_id')
->first();
$procedimento = null;
if ($item && $item->procedimentoCirurgico) {
$procedimento = [
'codigo' => MaskUtil::getProcedimentoMasked($item->procedimentoCirurgico->procedimento_codigo),
'descricao' => $item->procedimentoCirurgico->nome,
];
}
return [
'tipo' => 'protocolo_agendamento_procedimento_cirurgico',
'paciente' => self::formatarPacienteProtocolo($solicitacao),
'solicitante' => [
'medico' => $solicitacao->usuarioSolicitante->cidadao->nome,
'unidade' => $solicitacao->infraestruturaSolicitante->unidade->nome_fantasia,
],
'data_agendamento' => $solicitacao->data_agendada ? $solicitacao->data_agendada->format('d/m/Y') : null,
'hora_agendamento' => $solicitacao->hora_agendada ? $solicitacao->hora_agendada->format('H:i') : null,
'prestador' => self::formatarPrestadorProtocolo($solicitacao),
'procedimento' => $procedimento,
'mensagem' => $parametrizacao ? $parametrizacao->mensagem_comprovante_agendamento : null,
];
}
private static function documentoProtocoloAgendamentoEspecialidade(
Solicitacao $solicitacao,
?Parametrizacao $parametrizacao
): array {
$item = $solicitacao->itens()->with('especialidade.ocupacao')
->whereNotNull('especialidade_id')
->first();
$especialidade = null;
if ($item && $item->especialidade) {
$especialidade = [
'codigo' => $item->especialidade->ocupacao->codigo ?? null,
'descricao' => $item->especialidade->nome,
];
}
return [
'tipo' => 'protocolo_agendamento_especialidade',
'paciente' => self::formatarPacienteProtocolo($solicitacao),
'solicitante' => [
'medico' => $solicitacao->usuarioSolicitante->cidadao->nome,
'unidade' => $solicitacao->infraestruturaSolicitante->unidade->nome_fantasia,
],
'data_agendamento' => $solicitacao->data_agendada ? $solicitacao->data_agendada->format('d/m/Y') : null,
'hora_agendamento' => $solicitacao->hora_agendada ? $solicitacao->hora_agendada->format('H:i') : null,
'prestador' => self::formatarPrestadorProtocolo($solicitacao),
'especialidade' => $especialidade,
'mensagem' => $parametrizacao ? $parametrizacao->mensagem_comprovante_agendamento : null,
];
}
}

View File

@@ -0,0 +1,630 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\ExameImagem;
use App\Enums\ExameImagem\Fechamento\Status;
use App\Enums\ExameImagem\Parametrizacao\ControleJornada\Tipo as ControleJornadaTipo;
use App\Enums\ExameImagem\Tipo;
use App\Enums\Parametrizacao\Exame\Tipo as ExameTipo;
use App\Models\Ambulatorio\AtendimentoSoapSolicitacaoExameItem;
use App\Models\Cadastro\Feriado\Feriado;
use App\Models\ExameImagem\Agendamento\Agendamento;
use App\Models\ExameImagem\Agendamento\AgendamentoExame;
use App\Services\Service;
use Carbon\Carbon;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
class AgendamentoImagemService extends Service
{
public static function elegibilidade()
{
$habilitado = DB::table('exame_imagem_parametrizacao_integracao_aplicativo')
->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.');
}
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\ExameImagem;
use App\Enums\ExameImagem\Fechamento\Status;
use App\Models\ExameImagem\Parametrizacao\IntegracaoAplicativo;
use App\Services\Service;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class LaudoService extends Service
{
public static function laudos(): array
{
self::validarVisualizacaoHabilitada();
$paciente = auth('mobile')->user();
return DB::table('exame_imagem_agendamento as agendamento')
->join('exame_imagem_jornada as jornada', 'jornada.id', 'agendamento.jornada_id')
->join('cadastro_infraestrutura as infraestrutura', 'infraestrutura.id', 'agendamento.infraestrutura_id')
->join('cadastro_unidade as unidade', 'unidade.id', 'infraestrutura.unidade_id')
->leftJoin('cadastro_infraestrutura as infra_sol', 'infra_sol.id', 'agendamento.infraestrutura_solicitante_id')
->leftJoin('cadastro_unidade as unidade_sol', 'unidade_sol.id', 'infra_sol.unidade_id')
->leftJoin('cadastro_usuario as medico_usuario', 'medico_usuario.id', 'agendamento.profissional_id')
->leftJoin('cadastro_cidadao as medico_cidadao', 'medico_cidadao.id', 'medico_usuario.cidadao_id')
->where('agendamento.paciente_id', $paciente->id)
->whereExists(function ($sub) {
$sub->select(DB::raw(1))
->from('exame_imagem_agendamento_exame as exame')
->whereColumn('exame.agendamento_id', 'agendamento.id')
->where('exame.status_id', Status::REALIZADO);
})
->orderByDesc('jornada.data')
->get([
'agendamento.id',
'jornada.data',
'agendamento.hora_exame',
'unidade.nome_fantasia as unidade_nome',
'unidade_sol.nome_fantasia as unidade_solicitante',
'medico_cidadao.nome as medico_nome',
])
->map(
fn($item) =>
[
'id' => $item->id,
'data' => $item->data,
'hora_exame' => $item->hora_exame,
'unidade' => $item->unidade_nome,
'unidade_solicitante' => $item->unidade_solicitante,
'medico' => $item->medico_nome,
'exames' => self::getExamesComResultado($item->id),
]
)
->values()
->toArray();
}
private static function getExamesComResultado(int $agendamentoId): array
{
return DB::table('exame_imagem_agendamento_exame as exame')
->join('parametrizacao_exame as param_exame', 'param_exame.id', 'exame.exame_id')
->leftJoin('cadastro_usuario as usuario', 'usuario.id', 'exame.usuario_id')
->leftJoin('cadastro_cidadao as cidadao_usuario', 'cidadao_usuario.id', 'usuario.cidadao_id')
->where('exame.agendamento_id', $agendamentoId)
->where('exame.status_id', Status::REALIZADO)
->get(
[
'exame.id',
'param_exame.nome',
'param_exame.codigo',
'exame.arquivo',
'exame.resultado',
'exame.laudo_nome',
'cidadao_usuario.nome as lancado_por',
]
)
->map(
fn($exame) =>
[
'id' => $exame->id,
'nome' => $exame->nome,
'codigo' => $exame->codigo,
'resultado' => $exame->resultado,
'laudo_nome' => $exame->laudo_nome,
'lancado_por' => $exame->lancado_por,
'url_download' => $exame->arquivo
? url("api/mobile/conecta-cidadao/exame-imagem/laudos/{$exame->id}/download")
: null,
]
)
->toArray();
}
public static function download(int $exameId): array
{
$paciente = auth('mobile')->user();
$exame = DB::table('exame_imagem_agendamento_exame as exame')
->join('exame_imagem_agendamento as agendamento', 'agendamento.id', 'exame.agendamento_id')
->where('exame.id', $exameId)
->where('agendamento.paciente_id', $paciente->id)
->whereNotNull('exame.arquivo')
->first(['exame.arquivo']);
abort_if(is_null($exame), Response::HTTP_NOT_FOUND, 'Laudo não encontrado.');
$caminho = 'exame-imagem' . DIRECTORY_SEPARATOR . 'laudos' . DIRECTORY_SEPARATOR . app('config')->get('database.default') . DIRECTORY_SEPARATOR . $exame->arquivo;
abort_if(!Storage::exists($caminho), Response::HTTP_NOT_FOUND, 'Arquivo do laudo não encontrado.');
return [
'nome' => $exame->arquivo,
'caminho' => Storage::path($caminho),
];
}
private static function validarVisualizacaoHabilitada(): void
{
$integracao = IntegracaoAplicativo::first();
if (!($integracao->habilita_consulta_imagem_conectasus_cidadao ?? false)) {
abort(Response::HTTP_FORBIDDEN, 'A consulta de exames de imagem pelo ConectaSUS Cidadão não está habilitada.');
}
}
}

View File

@@ -0,0 +1,708 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\ExameLaboratorial;
use App\Enums\ExameLaboratorial\Acao;
use App\Enums\ExameLaboratorial\StatusAgendamento;
use App\Enums\ExameLaboratorial\TipoCota;
use App\Enums\ExameLaboratorial\TipoUnidade;
use App\Enums\Parametrizacao\Exame\Tipo;
use App\Enums\WebService\Laboratorio\Cliente\PedidoStatus;
use App\Models\Ambulatorio\AtendimentoSoapSolicitacaoExameItem;
use App\Models\Cadastro\Feriado\Feriado;
use App\Models\ExameLaboratorial\AgendamentoExame\Agendamento;
use App\Models\ExameLaboratorial\AgendamentoExame\AgendamentoExame;
use App\Services\ExameLaboratorial\AgendamentoExameHistoricoService;
use App\Services\Service;
use Carbon\Carbon;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
class AgendamentoExameService extends Service
{
public static function elegibilidade(): array
{
$parametrizacao = DB::table('exame_laboratorial_parametrizacao_integracao_aplicativo')->first();
return [
'agendamento' => [
'habilitado' => (bool) ($parametrizacao->habilita_agendamento_conectasus_cidadao ?? false),
],
'visualizacao_laudo' => [
'habilitada' => (bool) ($parametrizacao->habilita_visualizacao_laudo_conectasus_cidadao ?? false),
],
];
}
public static function unidadesColeta(): array
{
self::validarParametrizacaoHabilitada();
$pacienteUnidadeId = auth('mobile')->user()->unidade_id;
$agora = now();
return DB::table('cadastro_infraestrutura as infra_coleta')
->join('cadastro_unidade as unidade_coleta', 'unidade_coleta.id', 'infra_coleta.unidade_id')
->join('exame_laboratorial_parametrizacao_unidade as param_unidade', function ($join) {
$join->on('param_unidade.infraestrutura_id', 'infra_coleta.id')
->where('param_unidade.unidade_coleta', true)
->whereNull('param_unidade.deleted_at');
})
->join('exame_laboratorial_parametrizacao_unidade_solicitante as sol', function ($join) {
$join->on('sol.infraestrutura_id', 'infra_coleta.id')
->whereNull('sol.deleted_at');
})
->join('cadastro_infraestrutura as infra_sol', 'infra_sol.id', 'sol.unidade_solicitante_infraestrutura_id')
->where('infra_sol.unidade_id', $pacienteUnidadeId)
->whereNull('infra_coleta.deleted_at')
->whereExists(function ($q) use ($agora) {
$q->select(DB::raw(1))
->from('exame_laboratorial_jornada as jornada_coleta')
->whereColumn('jornada_coleta.infraestrutura_id', 'infra_coleta.id')
->where('jornada_coleta.vagas_app', '>', 0)
->where(function ($q2) use ($agora) {
$q2->whereDate('jornada_coleta.data', '>', $agora->toDateString())
->orWhere(function ($q3) use ($agora) {
$q3->whereDate('jornada_coleta.data', $agora->toDateString())
->whereTime('jornada_coleta.hora_fim', '>', $agora->format('H:i:s'));
});
})
->whereNull('jornada_coleta.deleted_at');
})
->orderBy('unidade_coleta.nome_fantasia')
->orderBy('infra_coleta.nome')
->get(
[
'infra_coleta.id as infraestrutura_id',
'unidade_coleta.id as unidade_id',
'unidade_coleta.nome_fantasia',
DB::raw(
"CASE WHEN infra_coleta.nome IS NOT NULL
THEN infra_coleta.nome
ELSE unidade_coleta.nome_fantasia
END AS infraestrutura_nome"
),
'sol.unidade_solicitante_infraestrutura_id',
]
)
->unique('infraestrutura_id')
->map(
fn($item) => [
'infraestrutura_id' => $item->infraestrutura_id,
'unidade_id' => $item->unidade_id,
'nome_fantasia' => $item->nome_fantasia,
'infraestrutura_nome' => $item->infraestrutura_nome,
'unidade_solicitante_infraestrutura_id' => $item->unidade_solicitante_infraestrutura_id,
]
)
->values()
->toArray();
}
public static function exames(): array
{
self::validarParametrizacaoHabilitada();
$pacienteId = auth('mobile')->user()->id;
$statusAtivos = [
StatusAgendamento::AGENDADO,
StatusAgendamento::PENDENTE_ASSINATURA,
StatusAgendamento::NOVA_COLETA,
StatusAgendamento::PENDENTE_ENTREGA,
];
$statusRealizados = [
StatusAgendamento::REALIZADO,
StatusAgendamento::ASSINADO,
StatusAgendamento::ENTREGUE,
];
$exameIdsAtivos = DB::table('exame_laboratorial_agendamento_exame')
->where('paciente_id', $pacienteId)
->whereIn('status_id', $statusAtivos)
->pluck('exame_id')
->unique()
->toArray();
$exameIdsBloqueadosPorValidade = DB::table('parametrizacao_exame as exame')
->join('exame_laboratorial_agendamento_exame as ae', 'ae.exame_id', 'exame.id')
->join('exame_laboratorial_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();
$exameIdsBloqueados = array_unique(array_merge($exameIdsAtivos, $exameIdsBloqueadosPorValidade));
$pedidosBloqueados = DB::table('ambulatorio_atendimento_soap_solicitacao_exame_item')
->where(function ($q) use ($exameIdsBloqueados) {
$q->where('agendado_exame_laboratorial', true);
if (!empty($exameIdsBloqueados)) {
$q->orWhereIn('exame_id', $exameIdsBloqueados);
}
})
->pluck('solicitacao_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')
->where('parametrizacao_exame.tipo_id', Tipo::LABORATORIAL)
->whereNull('parametrizacao_exame.regulacao_tipo_id')
->whereNull('parametrizacao_exame.deleted_at')
->where('ambulatorio_atendimento_soap_solicitacao_exame_item.descartado_exame_laboratorial', '!=', true)
->whereNotIn('ambulatorio_atendimento_soap_solicitacao_exame_item.solicitacao_exame_id', $pedidosBloqueados)
->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 as exame_id',
'parametrizacao_exame.codigo',
'parametrizacao_exame.nome',
'parametrizacao_exame.sigla_exame',
'parametrizacao_exame.prazo_entrega',
'parametrizacao_exame.observacao',
'parametrizacao_exame.sexo_id',
'profissional.id as profissional_id',
'cadastro_cidadao.nome as profissional_nome',
]);
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($row) => [
'exame_item_id' => $row->exame_item_id,
'id' => $row->exame_id,
'codigo' => $row->codigo,
'nome' => $row->nome,
'sigla_exame' => $row->sigla_exame,
'prazo_entrega' => $row->prazo_entrega,
'observacao' => $row->observacao,
'sexo_id' => $row->sexo_id,
])->values()->toArray(),
])
->values()
->toArray();
}
public static function jornadas(): array
{
self::validarParametrizacaoHabilitada();
request()->validate([
'coleta_infraestrutura_id' => 'required|integer|exists:cadastro_infraestrutura,id',
'unidade_solicitante_infraestrutura_id' => 'required|integer|exists:cadastro_infraestrutura,id',
'exame_ids' => 'nullable|array',
'exame_ids.*' => 'integer|exists:parametrizacao_exame,id',
]);
$coletaInfraId = (int) request()->coleta_infraestrutura_id;
$unidadeSolInfraId = (int) request()->unidade_solicitante_infraestrutura_id;
$exameIds = request()->exame_ids ?? [];
$statusPermitidos = [
StatusAgendamento::AGENDADO,
StatusAgendamento::REALIZADO,
StatusAgendamento::PENDENTE_ASSINATURA,
StatusAgendamento::ASSINADO,
StatusAgendamento::NOVA_COLETA,
StatusAgendamento::PENDENTE_ENTREGA,
StatusAgendamento::ENTREGUE,
];
$agora = now();
$jornadasRaw = DB::table('exame_laboratorial_jornada as jornada')
->where('jornada.infraestrutura_id', $coletaInfraId)
->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'));
});
})
->whereNull('jornada.deleted_at')
->orderBy('jornada.data')
->orderBy('jornada.hora_inicial')
->get([
'jornada.id',
'jornada.data',
'jornada.hora_inicial',
'jornada.hora_fim',
'jornada.tempo_atendimento',
'jornada.vagas_app',
'jornada.laboratorio_prestador_infraestrutura_id',
]);
$jornadaIds = $jornadasRaw->pluck('id')->toArray();
$consumoPorJornada = DB::table('exame_laboratorial_agendamento as ag')
->join('exame_laboratorial_agendamento_exame as ag_exame', 'ag_exame.agendamento_id', '=', 'ag.id')
->whereIn('ag.jornada_id', $jornadaIds)
->where('ag.app_conectasus_cidadao', true)
->whereIn('ag_exame.status_id', $statusPermitidos)
->groupBy('ag.jornada_id')
->select('ag.jornada_id', DB::raw('COUNT(DISTINCT ag.id) as visitas'))
->get()
->keyBy('jornada_id');
$horariosOcupadosPorJornada = DB::table('exame_laboratorial_agendamento as ag')
->join('exame_laboratorial_agendamento_exame as ag_exame', 'ag_exame.agendamento_id', '=', 'ag.id')
->whereIn('ag.jornada_id', $jornadaIds)
->whereIn('ag_exame.status_id', $statusPermitidos)
->select('ag.jornada_id', DB::raw("DATE_FORMAT(ag.horario, '%H:%i') as horario"))
->distinct()
->get()
->groupBy('jornada_id')
->map(fn($rows) => $rows->pluck('horario')->toArray());
$jornadas = $jornadasRaw->map(function ($jornada) use ($consumoPorJornada, $horariosOcupadosPorJornada) {
$consumido = $consumoPorJornada[$jornada->id]->visitas ?? 0;
return [
'jornada_id' => $jornada->id,
'data' => Carbon::parse($jornada->data)->format('Y-m-d'),
'hora_inicial' => Carbon::parse($jornada->hora_inicial)->format('H:i'),
'hora_fim' => Carbon::parse($jornada->hora_fim)->format('H:i'),
'tempo_atendimento' => $jornada->tempo_atendimento,
'vagas_app' => $jornada->vagas_app,
'vagas_app_disponiveis' => max(0, $jornada->vagas_app - $consumido),
'laboratorio_prestador_infraestrutura_id' => $jornada->laboratorio_prestador_infraestrutura_id,
'horarios' => self::gerarHorariosDisponiveis(
$jornada->data,
$jornada->hora_inicial,
$jornada->hora_fim,
$jornada->tempo_atendimento,
$horariosOcupadosPorJornada[$jornada->id] ?? []
),
];
})->values()->toArray();
$feriados = Feriado::orderBy('data')
->get()
->map(
fn($feriado) =>
[
'data' => (new Carbon($feriado->data))->format('Y-m-d'),
'tipo_id' => $feriado->tipo_id,
'tipo_nome' => $feriado->tipo->descricao,
'observacao' => $feriado->observacao,
]
)
->values()
->toArray();
return [
'jornadas' => $jornadas,
'feriados' => $feriados,
'cotas' => self::computarCotasApp($coletaInfraId, $unidadeSolInfraId, $exameIds),
];
}
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;
}
private static function computarCotasApp(int $coletaInfraId, int $unidadeSolInfraId, array $exameIds): array
{
$hoje = now()->toDateString();
$cotaLaboratorio = null;
$labInfraIds = DB::table('exame_laboratorial_jornada')
->where('infraestrutura_id', $coletaInfraId)
->whereNotNull('laboratorio_prestador_infraestrutura_id')
->whereNull('deleted_at')
->distinct()
->pluck('laboratorio_prestador_infraestrutura_id');
foreach ($labInfraIds as $labInfraId) {
$cota = DB::table('exame_laboratorial_parametrizacao_cota')
->where('infraestrutura_id', $labInfraId)
->where('unidade_tipo_id', TipoUnidade::LABORATORIO)
->where('cota_tipo_id', TipoCota::QUANTIDADE)
->where('inicio', '<=', $hoje)
->where('fim', '>=', $hoje)
->whereNull('deleted_at')
->first();
$appLab = DB::table('exame_laboratorial_param_app_conectasus_cidadao_laboratorio')
->where('infraestrutura_id', $labInfraId)
->whereNull('deleted_at')
->first();
if (!$cota || !$appLab || $appLab->percentual_quantitativo <= 0) {
continue;
}
$limite = (int) round($cota->cota_quantidade * $appLab->percentual_quantitativo / 100);
$consumido = DB::table('exame_laboratorial_agendamento')
->where('laboratorio_infraestrutura_id', $labInfraId)
->where('app_conectasus_cidadao', true)
->where('data', '>=', $cota->inicio)
->where('data', '<=', $cota->fim)
->count();
$cotaLaboratorio = [
'infraestrutura_id' => $labInfraId,
'percentual_limite' => (float) $appLab->percentual_quantitativo,
'limite_vagas' => $limite,
'consumido' => $consumido,
'percentual_consumido' => $limite > 0 ? round($consumido / $limite * 100, 2) : 0,
'bloqueado' => $consumido >= $limite,
];
break;
}
$cotasExames = [];
foreach ($exameIds as $exameId) {
$periodo = DB::table('exame_laboratorial_parametrizacao_cota_exame_periodo')
->where('infraestrutura_id', $coletaInfraId)
->where('exame_id', $exameId)
->where('cota_tipo_id', TipoCota::QUANTIDADE)
->where('inicio', '<=', $hoje)
->where('fim', '>=', $hoje)
->whereNull('deleted_at')
->first();
if (!$periodo) {
continue;
}
$appExame = DB::table('exame_laboratorial_parametrizacao_app_conectasus_cidadao_exame')
->where('cota_exame_periodo_id', $periodo->id)
->whereNull('deleted_at')
->first();
if (!$appExame || $appExame->percentual_quantitativo <= 0) {
continue;
}
$limite = (int) round($periodo->cota_quantidade * $appExame->percentual_quantitativo / 100);
$consumido = DB::table('exame_laboratorial_agendamento_exame as ae')
->join('exame_laboratorial_agendamento as ag', 'ag.id', '=', 'ae.agendamento_id')
->where('ae.exame_id', $exameId)
->where('ag.app_conectasus_cidadao', true)
->where('ag.coleta_infraestrutura_id', $coletaInfraId)
->where('ag.data', '>=', $periodo->inicio)
->where('ag.data', '<=', $periodo->fim)
->count();
$cotasExames[] = [
'exame_id' => (int) $exameId,
'percentual_limite' => (float) $appExame->percentual_quantitativo,
'limite_vagas' => $limite,
'consumido' => $consumido,
'percentual_consumido' => $limite > 0 ? round($consumido / $limite * 100, 2) : 0,
'bloqueado' => $consumido >= $limite,
];
}
$cotaUnidadeSolicitante = null;
$appSol = DB::table('exame_laboratorial_param_app_conectasus_cidadao_unidade_sol')
->where('infraestrutura_id', $unidadeSolInfraId)
->whereNull('deleted_at')
->first();
if ($appSol && $appSol->percentual_quantitativo > 0) {
$cotaSol = DB::table('exame_laboratorial_parametrizacao_cota_unidade_solicitante as sol')
->join('exame_laboratorial_parametrizacao_cota as cota', 'cota.id', '=', 'sol.cota_id')
->where('sol.infraestrutura_solicitante_id', $unidadeSolInfraId)
->where('cota.cota_tipo_id', TipoCota::QUANTIDADE)
->where('cota.inicio', '<=', $hoje)
->where('cota.fim', '>=', $hoje)
->whereNull('cota.deleted_at')
->whereNull('sol.deleted_at')
->first(['sol.cota_quantidade', 'cota.inicio', 'cota.fim']);
if ($cotaSol) {
$limite = (int) round($cotaSol->cota_quantidade * $appSol->percentual_quantitativo / 100);
$consumido = DB::table('exame_laboratorial_agendamento')
->where('infraestrutura_id', $unidadeSolInfraId)
->where('app_conectasus_cidadao', true)
->where('data', '>=', $cotaSol->inicio)
->where('data', '<=', $cotaSol->fim)
->count();
$cotaUnidadeSolicitante = [
'infraestrutura_id' => $unidadeSolInfraId,
'percentual_limite' => (float) $appSol->percentual_quantitativo,
'limite_vagas' => $limite,
'consumido' => $consumido,
'percentual_consumido' => $limite > 0 ? round($consumido / $limite * 100, 2) : 0,
'bloqueado' => $consumido >= $limite,
];
}
}
return [
'laboratorio' => $cotaLaboratorio,
'exames' => $cotasExames,
'unidade_solicitante' => $cotaUnidadeSolicitante,
];
}
public static function agendar(): array
{
self::validarParametrizacaoHabilitada();
request()->validate([
'jornada_id' => 'required|integer|exists:exame_laboratorial_jornada,id',
'coleta_infraestrutura_id' => 'required|integer|exists:cadastro_infraestrutura,id',
'unidade_solicitante_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',
]);
$paciente = auth('mobile')->user();
$jornada = DB::table('exame_laboratorial_jornada')
->where('id', request()->jornada_id)
->whereNull('deleted_at')
->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);
$dataHoraAgendamento = Carbon::parse($dataJornada . ' ' . request()->horario);
if ($dataHoraAgendamento->isPast()) {
abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Não é possível agendar em horário retroativo.');
}
if ($dataHoraAgendamento->lt($horaInicialJornada) || $dataHoraAgendamento->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_laboratorial_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)
->whereIn('ae.status_id', [
StatusAgendamento::AGENDADO,
StatusAgendamento::PENDENTE_ASSINATURA,
StatusAgendamento::NOVA_COLETA,
StatusAgendamento::PENDENTE_ENTREGA,
])
->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);
$coletaUnidadeId = DB::table('cadastro_infraestrutura')
->where('id', request()->coleta_infraestrutura_id)
->value('unidade_id');
$solicitanteUnidadeId = DB::table('cadastro_infraestrutura')
->where('id', request()->unidade_solicitante_infraestrutura_id)
->value('unidade_id');
$labInfraId = $jornada->laboratorio_prestador_infraestrutura_id;
$labUnidadeId = $labInfraId
? DB::table('cadastro_infraestrutura')->where('id', $labInfraId)->value('unidade_id')
: null;
$ultimoNumero = DB::table('exame_laboratorial_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->data = $jornada->data;
$agendamento->horario = request()->horario;
$agendamento->unidade_id = $solicitanteUnidadeId;
$agendamento->infraestrutura_id = (int) request()->unidade_solicitante_infraestrutura_id;
$agendamento->coleta_unidade_id = $coletaUnidadeId;
$agendamento->coleta_infraestrutura_id = (int) request()->coleta_infraestrutura_id;
$agendamento->laboratorio_unidade_id = $labUnidadeId;
$agendamento->laboratorio_infraestrutura_id = $labInfraId;
$agendamento->observacao = request()->observacao;
$agendamento->app_conectasus_cidadao = true;
$agendamento->web_service_laboratorio_cliente_pedido_status_id = PedidoStatus::AGUARDANDO_PROCESSAMENTO;
$agendamento->save();
foreach (request()->exames as $exameData) {
$exame = new AgendamentoExame();
$exame->agendamento_id = $agendamento->id;
$exame->paciente_id = $paciente->id;
$exame->exame_id = $exameData['exame_id'];
$exame->status_id = StatusAgendamento::AGENDADO;
$exame->soap_solicitacao_exame_item_id = $exameData['exame_item_id'];
$exame->quantidade = 1;
$exame->save();
AtendimentoSoapSolicitacaoExameItem::findOrFail($exameData['exame_item_id'])
->update(['agendado_exame_laboratorial' => true]);
AgendamentoExameHistoricoService::store($exame, $agendamento, Acao::INSERCAO);
}
return [
'agendamento_id' => $agendamento->id,
'agendamento_numero' => $agendamento->agendamento_numero,
];
}
private static function validarHorarioDisponivel(object $jornada, string $horario): void
{
$statusPermitidos = [
StatusAgendamento::AGENDADO,
StatusAgendamento::REALIZADO,
StatusAgendamento::PENDENTE_ASSINATURA,
StatusAgendamento::ASSINADO,
StatusAgendamento::NOVA_COLETA,
StatusAgendamento::PENDENTE_ENTREGA,
StatusAgendamento::ENTREGUE,
];
$ocupado = DB::table('exame_laboratorial_agendamento as ag')
->join('exame_laboratorial_agendamento_exame as ag_exame', 'ag_exame.agendamento_id', '=', 'ag.id')
->where('ag.jornada_id', $jornada->id)
->whereTime('ag.horario', $horario)
->whereIn('ag_exame.status_id', $statusPermitidos)
->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): void
{
$statusPermitidos = [
StatusAgendamento::AGENDADO,
StatusAgendamento::REALIZADO,
StatusAgendamento::PENDENTE_ASSINATURA,
StatusAgendamento::ASSINADO,
StatusAgendamento::NOVA_COLETA,
StatusAgendamento::PENDENTE_ENTREGA,
StatusAgendamento::ENTREGUE,
];
$agendamentosApp = DB::table('exame_laboratorial_agendamento as ag')
->join('exame_laboratorial_agendamento_exame as ag_exame', 'ag_exame.agendamento_id', '=', 'ag.id')
->where('ag.jornada_id', $jornada->id)
->where('ag.app_conectasus_cidadao', true)
->whereIn('ag_exame.status_id', $statusPermitidos)
->distinct()
->count('ag.id');
if ($agendamentosApp >= $jornada->vagas_app) {
abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Não há vagas disponíveis para o aplicativo nesta jornada.');
}
}
private static function validarParametrizacaoHabilitada(): void
{
$habilitado = DB::table('exame_laboratorial_parametrizacao_integracao_aplicativo')
->value('habilita_agendamento_conectasus_cidadao');
if (!$habilitado) {
abort(Response::HTTP_FORBIDDEN, 'O agendamento de exames laboratoriais não está habilitado para o aplicativo.');
}
}
}

View File

@@ -0,0 +1,180 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\ExameLaboratorial;
use App\Enums\ExameLaboratorial\StatusAgendamento;
use App\Models\ExameLaboratorial\Parametrizacao\IntegracaoAplicativo;
use App\Services\Service;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class LaudoService extends Service
{
public static function laudos(): array
{
self::validarVisualizacaoHabilitada();
$paciente = auth('mobile')->user();
return DB::table('exame_laboratorial_agendamento as agendamento')
->join('cadastro_unidade as unidade_sol', 'unidade_sol.id', 'agendamento.unidade_id')
->join('cadastro_infraestrutura as infra_sol', 'infra_sol.id', 'agendamento.infraestrutura_id')
->join('cadastro_unidade as unidade_col', 'unidade_col.id', 'agendamento.coleta_unidade_id')
->leftJoin('cadastro_usuario as medico', 'medico.id', 'agendamento.medico_id')
->leftJoin('cadastro_cidadao as cidadao_medico', 'cidadao_medico.id', 'medico.cidadao_id')
->where('agendamento.paciente_id', $paciente->id)
->where(function ($q) {
$q->whereExists(function ($sub) {
$sub->select(DB::raw(1))
->from('exame_laboratorial_agendamento_exame as exame')
->whereColumn('exame.agendamento_id', 'agendamento.id')
->where('exame.status_id', StatusAgendamento::ASSINADO);
})
->orWhereExists(function ($sub) {
$sub->select(DB::raw(1))
->from('exame_laboratorial_agendamento_laudo as laudo')
->whereColumn('laudo.agendamento_id', 'agendamento.id')
->where(function ($l) {
$l->whereNotNull('laudo.arquivo')
->orWhereNotNull('laudo.url');
});
});
})
->orderByDesc('agendamento.data')
->get(
[
'agendamento.id',
'agendamento.data',
'unidade_sol.nome_fantasia as unidade_solicitante',
'unidade_col.nome_fantasia as unidade_coleta',
'cidadao_medico.nome as medico_nome',
]
)
->map(
fn($item) =>
[
'id' => $item->id,
'data' => $item->data,
'unidade_solicitante' => $item->unidade_solicitante,
'unidade_coleta' => $item->unidade_coleta,
'medico' => $item->medico_nome,
'exames' => self::getExamesComLaudo($item->id),
]
)
->values()
->toArray();
}
private static function getExamesComLaudo(int $agendamentoId): array
{
$examesAssinados = DB::table('exame_laboratorial_agendamento_exame as exame')
->join('parametrizacao_exame as param_exame', 'param_exame.id', 'exame.exame_id')
->leftJoin('cadastro_usuario as prof_assinatura', 'prof_assinatura.id', 'exame.assinatura_profissional_id')
->leftJoin('cadastro_cidadao as cidadao_assinatura', 'cidadao_assinatura.id', 'prof_assinatura.cidadao_id')
->where('exame.agendamento_id', $agendamentoId)
->where('exame.status_id', StatusAgendamento::ASSINADO)
->get(
[
'exame.id',
'param_exame.nome',
'param_exame.sigla_exame',
'exame.data_assinatura',
'cidadao_assinatura.nome as assinado_por',
'exame.entregue',
'exame.impressao',
]
)
->map(
fn($exame) =>
[
'id' => $exame->id,
'nome' => $exame->nome,
'sigla_exame' => $exame->sigla_exame,
'data_assinatura' => $exame->data_assinatura,
'assinado_por' => $exame->assinado_por,
'status_laudo' => self::resolverStatusLaudo($exame->entregue, $exame->impressao),
'origem' => 'assinatura_eletronica',
]
)
->toArray();
$laudosAnexados = DB::table('exame_laboratorial_agendamento_laudo as laudo')
->where('laudo.agendamento_id', $agendamentoId)
->where(function ($q) {
$q->whereNotNull('laudo.arquivo')
->orWhereNotNull('laudo.url');
})
->orderBy('laudo.ordem')
->get(
[
'laudo.id',
'laudo.arquivo',
'laudo.url',
'laudo.is_enviado_webservice',
'laudo.ordem',
]
)
->map(
fn($laudo) =>
[
'id' => $laudo->id,
'ordem' => $laudo->ordem,
'origem' => $laudo->is_enviado_webservice ? 'externo_prestador' : 'importacao_manual',
'url_download' => $laudo->url
?? ($laudo->arquivo
? url("api/mobile/conecta-cidadao/exame-laboratorial/laudo/{$laudo->id}/download")
: null),
]
)
->toArray();
return [
'assinados' => $examesAssinados,
'laudos_anexados' => $laudosAnexados,
];
}
public static function download(int $laudoId): array
{
$paciente = auth('mobile')->user();
$laudo = DB::table('exame_laboratorial_agendamento_laudo as laudo')
->join('exame_laboratorial_agendamento as agendamento', 'agendamento.id', 'laudo.agendamento_id')
->where('laudo.id', $laudoId)
->where('agendamento.paciente_id', $paciente->id)
->whereNotNull('laudo.arquivo')
->first(['laudo.arquivo']);
abort_if(is_null($laudo), Response::HTTP_NOT_FOUND, 'Laudo não encontrado.');
$caminho = 'exame-laboratorial' . DIRECTORY_SEPARATOR . 'laudos' . DIRECTORY_SEPARATOR . app('config')->get('database.default') . DIRECTORY_SEPARATOR . $laudo->arquivo;
abort_if(!Storage::exists($caminho), Response::HTTP_NOT_FOUND, 'Arquivo do laudo não encontrado.');
return [
'nome' => $laudo->arquivo,
'caminho' => Storage::path($caminho),
];
}
private static function resolverStatusLaudo(bool $entregue, bool $impressao): string
{
if ($entregue) {
return 'entregue';
}
return $impressao ? 'pendente_entrega' : 'pendente_impressao';
}
private static function validarVisualizacaoHabilitada(): void
{
$parametrizacao = IntegracaoAplicativo::first();
if (!($parametrizacao->habilita_visualizacao_laudo_conectasus_cidadao ?? false)) {
abort(Response::HTTP_FORBIDDEN, 'A visualização de laudos não está habilitada para o aplicativo.');
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\ExclusaoConta;
use App\Models\Cadastro\SujeitoAtencao\Paciente;
use App\Services\Service;
use Illuminate\Http\Response;
class ExclusaoContaService extends Service
{
public static function excluirConta(int $id): array
{
$paciente = Paciente::find($id);
if (!$paciente) {
abort(Response::HTTP_NOT_FOUND, 'Paciente não encontrado.');
}
if (auth('mobile')->user()->id !== $paciente->id) {
abort(Response::HTTP_FORBIDDEN, 'Acesso negado.');
}
$paciente->update(['senha' => null]);
return [
'message' => 'Conta excluída com sucesso.'
];
}
}

View File

@@ -0,0 +1,475 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\Farmacia;
use App\Enums\Geral\MedidaTempo;
use App\Enums\Mobile\ConectaCidadao\Farmacia\MonitoramentoPosologia;
use App\Models\Farmacia\AppCidadaoLog;
use App\Models\Farmacia\ParametrizacaoAppCidadao;
use App\Models\Farmacia\ReceitaItem;
use App\Models\Farmacia\ReceitaMonitoramentoPosologia;
use App\Models\Farmacia\ReceitaTratamentoInicio;
use App\Services\Service;
use Carbon\Carbon;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class MonitoramentoPosologiaService extends Service
{
public static function medicamentos(): array
{
self::verificarParametrizacao();
$pacienteId = auth('mobile')->user()->id;
$rows = DB::table('farmacia_receita_item as fri')
->select(
'fri.id as item_id',
'fr.id as receita_id',
'fr.data_receita',
'cm.descricao as material_descricao',
'um.nome as unidade_medida_nome',
'fri.quantidade',
'fri.intervalo_tempo',
'fri.intervalo_medida_tempo_id',
'mt.nome as intervalo_medida_tempo_nome',
'fri.durante_tempo',
'ti.inicio_em',
'ti.interrompido_em',
DB::raw("(SELECT COUNT(*) FROM farmacia_receita_monitoramento_posologia WHERE receita_item_id = fri.id AND paciente_id = {$pacienteId}) as total_confirmadas")
)
->join('farmacia_receita as fr', 'fr.id', 'fri.receita_id')
->join('cadastro_material as cm', 'cm.id', 'fri.material_id')
->leftJoin('cadastro_unidade_medida as um', 'um.id', 'cm.apresentacao_unidade_medida_id')
->leftJoin('geral_medida_tempo as mt', 'mt.id', 'fri.intervalo_medida_tempo_id')
->leftJoin('farmacia_receita_tratamento_inicio as ti', function ($join) use ($pacienteId) {
$join->on('ti.receita_item_id', '=', 'fri.id')
->where('ti.paciente_id', '=', $pacienteId);
})
->where('fr.paciente_id', $pacienteId)
->where(function ($q) {
$q->whereDate('fri.proxima_retirada', '>=', now()->toDateString())
->orWhereNotNull('ti.inicio_em');
})
->orderBy('fr.data_receita', 'desc')
->orderBy('fri.id', 'asc')
->get();
$receitas = [];
foreach ($rows as $row) {
$receitaId = $row->receita_id;
if (!isset($receitas[$receitaId])) {
$receitas[$receitaId] = [
'id' => $receitaId,
'data_receita' => $row->data_receita,
'itens' => [],
];
}
$inicioEm = $row->inicio_em ? Carbon::parse($row->inicio_em) : null;
$interrompidoEm = $row->interrompido_em ? Carbon::parse($row->interrompido_em) : null;
$receitas[$receitaId]['itens'][] = [
'id' => $row->item_id,
'medicamento' => $row->material_descricao,
'posologia_texto' => self::montarPosologiaTexto($row),
'posologia_incompleta' => is_null($row->intervalo_tempo) || is_null($row->durante_tempo),
'status' => self::calcularStatus(
$row->durante_tempo,
$row->intervalo_tempo,
$row->intervalo_medida_tempo_id,
$inicioEm,
$interrompidoEm,
(int) $row->total_confirmadas
),
];
}
return [
'habilitado' => true,
'data' => array_values($receitas),
];
}
public static function iniciar(int $receitaItemId): array
{
self::verificarParametrizacao();
$pacienteId = auth('mobile')->user()->id;
$item = ReceitaItem::findOrFail($receitaItemId);
abort_if($item->receita->paciente_id !== $pacienteId, Response::HTTP_FORBIDDEN);
request()->validate([
'inicio_em' => ['required', 'date'],
'aderiu_tratamento' => ['nullable', 'boolean'],
]);
$inicioEm = Carbon::parse(request()->input('inicio_em'));
ReceitaTratamentoInicio::updateOrCreate(
['receita_item_id' => $receitaItemId, 'paciente_id' => $pacienteId],
[
'inicio_em' => $inicioEm,
'interrompido_em' => null,
'aderiu_tratamento' => request()->boolean('aderiu_tratamento'),
]
);
if ($inicioEm->isPast()) {
ReceitaMonitoramentoPosologia::firstOrCreate(
['receita_item_id' => $receitaItemId, 'paciente_id' => $pacienteId, 'dose_numero' => 1],
['confirmado_em' => $inicioEm, 'observacao' => null]
);
}
self::log($pacienteId, 'INICIO_TRATAMENTO', $item->receita_id, $receitaItemId);
return ['registrado' => true];
}
public static function index(int $receitaItemId): array
{
self::verificarParametrizacao();
$pacienteId = auth('mobile')->user()->id;
$item = ReceitaItem::findOrFail($receitaItemId);
abort_if($item->receita->paciente_id !== $pacienteId, Response::HTTP_FORBIDDEN);
$inicio = ReceitaTratamentoInicio::where('receita_item_id', $receitaItemId)
->where('paciente_id', $pacienteId)
->first();
$confirmacoes = ReceitaMonitoramentoPosologia::where('receita_item_id', $receitaItemId)
->where('paciente_id', $pacienteId)
->orderBy('dose_numero')
->get();
$inicioEm = $inicio ? $inicio->inicio_em : null;
$interrompidoEm = $inicio ? $inicio->interrompido_em : null;
$status = self::calcularStatus(
$item->durante_tempo,
$item->intervalo_tempo,
$item->intervalo_medida_tempo_id,
$inicioEm,
$interrompidoEm,
$confirmacoes->count()
);
$cronograma = self::calcularCronograma($item, $inicio, $confirmacoes);
$adesao = self::calcularAdesao($item, $inicioEm, $confirmacoes->count());
self::log($pacienteId, 'CONSULTA_MONITORAMENTO', $item->receita_id, $receitaItemId);
return [
'receita_item_id' => $receitaItemId,
'medicamento' => $item->material->descricao,
'posologia_incompleta' => is_null($item->intervalo_tempo) || is_null($item->durante_tempo),
'status' => $status,
'inicio_em' => $inicioEm,
'interrompido_em' => $interrompidoEm,
'adesao' => $adesao,
'cronograma' => $cronograma,
];
}
public static function store(int $receitaItemId): array
{
self::verificarParametrizacao();
$pacienteId = auth('mobile')->user()->id;
$item = ReceitaItem::findOrFail($receitaItemId);
abort_if($item->receita->paciente_id !== $pacienteId, Response::HTTP_FORBIDDEN);
$inicio = ReceitaTratamentoInicio::where('receita_item_id', $receitaItemId)
->where('paciente_id', $pacienteId)
->first();
abort_if(is_null($inicio), Response::HTTP_UNPROCESSABLE_ENTITY);
abort_if(!is_null($inicio->interrompido_em), Response::HTTP_FORBIDDEN);
request()->validate([
'confirmado_em' => ['required', 'date'],
'dose_numero' => ['required', 'integer', 'min:1'],
'observacao' => ['nullable', 'string', 'max:500'],
]);
$monitoramento = ReceitaMonitoramentoPosologia::create([
'receita_item_id' => $receitaItemId,
'paciente_id' => $pacienteId,
'confirmado_em' => request()->input('confirmado_em'),
'dose_numero' => request()->input('dose_numero'),
'observacao' => request()->input('observacao'),
]);
self::log($pacienteId, 'CONFIRMAR_DOSE', $item->receita_id, $receitaItemId);
return ['id' => $monitoramento->id];
}
public static function destroy(int $id): array
{
self::verificarParametrizacao();
$pacienteId = auth('mobile')->user()->id;
$monitoramento = ReceitaMonitoramentoPosologia::findOrFail($id);
abort_if($monitoramento->paciente_id !== $pacienteId, Response::HTTP_FORBIDDEN);
$receitaId = $monitoramento->receitaItem->receita_id;
$receitaItemId = $monitoramento->receita_item_id;
$monitoramento->delete();
self::log($pacienteId, 'CANCELAR_DOSE', $receitaId, $receitaItemId);
return ['removido' => true];
}
public static function interromper(int $receitaItemId): array
{
self::verificarParametrizacao();
$pacienteId = auth('mobile')->user()->id;
$item = ReceitaItem::findOrFail($receitaItemId);
abort_if($item->receita->paciente_id !== $pacienteId, Response::HTTP_FORBIDDEN);
$inicio = ReceitaTratamentoInicio::where('receita_item_id', $receitaItemId)
->where('paciente_id', $pacienteId)
->whereNull('interrompido_em')
->firstOrFail();
$inicio->update(['interrompido_em' => Carbon::now()]);
self::log($pacienteId, 'INTERROMPER_MONITORAMENTO', $item->receita_id, $receitaItemId);
return ['interrompido' => true];
}
private static function calcularStatus(
?int $duranteTempo,
?int $intervaloTempo,
?int $intervaloMedidaTempoId,
?Carbon $inicioEm,
?Carbon $interrompidoEm,
int $totalConfirmadas
): string {
if (is_null($inicioEm)) {
return MonitoramentoPosologia::STATUS_NAO_INICIADO;
}
if (!is_null($interrompidoEm)) {
return MonitoramentoPosologia::STATUS_INTERROMPIDO;
}
if (is_null($duranteTempo) || is_null($intervaloTempo)) {
return MonitoramentoPosologia::STATUS_EM_ANDAMENTO;
}
$intervaloMinutos = self::calcularIntervaloEmMinutos($intervaloTempo, $intervaloMedidaTempoId);
if ($intervaloMinutos > 0) {
$totalDosesPrevistas = (int) ceil(($duranteTempo * 24 * 60) / $intervaloMinutos);
if ($totalConfirmadas >= $totalDosesPrevistas) {
return MonitoramentoPosologia::STATUS_FINALIZADO;
}
}
$fimTratamento = $inicioEm->copy()->addDays($duranteTempo);
if (Carbon::now()->lessThanOrEqualTo($fimTratamento)) {
return MonitoramentoPosologia::STATUS_EM_ANDAMENTO;
}
return $totalConfirmadas > 0 ? MonitoramentoPosologia::STATUS_FINALIZADO : MonitoramentoPosologia::STATUS_EXPIRADO;
}
private static function calcularCronograma(ReceitaItem $item, ?ReceitaTratamentoInicio $inicio, Collection $confirmacoes): array
{
if (is_null($inicio) || is_null($item->intervalo_tempo) || is_null($item->durante_tempo)) {
return [];
}
$intervaloMinutos = self::calcularIntervaloEmMinutos($item->intervalo_tempo, $item->intervalo_medida_tempo_id);
if ($intervaloMinutos <= 0) {
return [];
}
$duracaoMinutos = $item->durante_tempo * 24 * 60;
$totalDoses = (int) ceil($duracaoMinutos / $intervaloMinutos);
$confirmacoesPorNumero = $confirmacoes->keyBy('dose_numero');
$cronograma = [];
for ($i = 0; $i < $totalDoses; $i++) {
$doseNumero = $i + 1;
$esperadoEm = $inicio->inicio_em->copy()->addMinutes($i * $intervaloMinutos);
$confirmacao = $confirmacoesPorNumero->get($doseNumero);
if ($confirmacao) {
$statusDose = MonitoramentoPosologia::STATUS_DOSE_CONFIRMADA;
} elseif ($esperadoEm->copy()->addMinutes(MonitoramentoPosologia::TOLERANCIA_ATRASO_MINUTOS)->isPast()) {
$statusDose = MonitoramentoPosologia::STATUS_DOSE_ATRASADA;
} else {
$statusDose = MonitoramentoPosologia::STATUS_DOSE_PENDENTE;
}
$cronograma[] = [
'id' => $confirmacao ? $confirmacao->id : null,
'numero' => $doseNumero,
'esperado_em' => $esperadoEm->toDateTimeString(),
'confirmado_em' => $confirmacao ? $confirmacao->confirmado_em : null,
'status' => $statusDose,
'observacao' => $confirmacao ? $confirmacao->observacao : null,
];
}
return $cronograma;
}
private static function calcularIntervaloEmMinutos(int $intervalo, ?int $medidaTempoId): int
{
if ($medidaTempoId === MedidaTempo::MINUTOS) {
return $intervalo;
}
if ($medidaTempoId === MedidaTempo::DIAS) {
return $intervalo * 24 * 60;
}
if ($medidaTempoId === MedidaTempo::SEMANAS) {
return $intervalo * 7 * 24 * 60;
}
return $intervalo * 60;
}
private static function calcularAdesao(ReceitaItem $item, ?Carbon $inicioEm, int $confirmadas): array
{
if (is_null($inicioEm) || is_null($item->intervalo_tempo) || is_null($item->durante_tempo)) {
return [
'doses_previstas' => null,
'doses_confirmadas' => $confirmadas,
'percentual' => null,
];
}
$intervaloMinutos = self::calcularIntervaloEmMinutos($item->intervalo_tempo, $item->intervalo_medida_tempo_id);
$duracaoMinutos = $item->durante_tempo * 24 * 60;
$decorridoMinutos = min($inicioEm->diffInMinutes(Carbon::now()), $duracaoMinutos);
$previstas = $intervaloMinutos > 0 ? (int) ceil($decorridoMinutos / $intervaloMinutos) : 0;
$percentual = $previstas > 0 ? round(($confirmadas / $previstas) * 100, 1) : null;
return [
'doses_previstas' => $previstas,
'doses_confirmadas' => $confirmadas,
'percentual' => $percentual,
];
}
private static function montarPosologiaTexto(object $row): ?string
{
$texto = '';
if (!is_null($row->quantidade)) {
$quantidade = (float) $row->quantidade;
$texto .= fmod($quantidade, 1) == 0 ? (int) $quantidade : number_format($quantidade, 2, ',', '.');
if ($row->unidade_medida_nome) {
$texto .= ' ' . strtolower($row->unidade_medida_nome);
}
}
if (!is_null($row->intervalo_tempo)) {
$intervalo = (int) $row->intervalo_tempo;
if ($row->intervalo_medida_tempo_nome) {
$unidade = strtolower(self::pluralizar($row->intervalo_medida_tempo_nome, $intervalo));
$texto .= $intervalo === 1
? " a cada {$unidade}"
: " a cada {$intervalo} {$unidade}";
} else {
$texto .= " a cada {$intervalo}";
}
}
if (!is_null($row->durante_tempo)) {
$dias = (int) $row->durante_tempo;
$texto .= $dias === 1 ? ' durante 1 dia' : " durante {$dias} dias";
}
return empty($texto) ? null : $texto;
}
private static function pluralizar(string $nome, int $valor): string
{
if (strpos($nome, '(es)') !== false) {
return $valor === 1
? str_replace('(es)', '', $nome)
: str_replace('(es)', 'es', $nome);
}
if (strpos($nome, '(s)') !== false) {
return $valor === 1
? str_replace('(s)', '', $nome)
: str_replace('(s)', 's', $nome);
}
return $nome;
}
private static function verificarParametrizacao(): void
{
$parametrizacao = ParametrizacaoAppCidadao::first();
abort_if(!($parametrizacao->permite_envio_receitas_medicas ?? false), Response::HTTP_FORBIDDEN);
}
private static function log(int $pacienteId, string $tipoAcao, ?int $receitaId = null, ?int $receitaItemId = null): void
{
AppCidadaoLog::create([
'paciente_id' => $pacienteId,
'receita_id' => $receitaId,
'receita_item_id' => $receitaItemId,
'tipo_acao' => $tipoAcao,
]);
}
}

View File

@@ -0,0 +1,326 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\Farmacia;
use App\Enums\Geral\MedidaTempo;
use App\Models\Farmacia\AppCidadaoLog;
use App\Models\Farmacia\ParametrizacaoAppCidadao;
use App\Services\Service;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
class ReceitaService extends Service
{
public static function index(): array
{
$parametrizacao = ParametrizacaoAppCidadao::first();
if (!($parametrizacao->permite_envio_receitas_medicas ?? false)) {
return ['habilitado' => false];
}
$pacienteId = auth('mobile')->user()->id;
$perPage = (int) request()->input('per_page', 15);
$page = (int) request()->input('page', 1);
$todos = self::queryAmbulatorioIds($pacienteId)->get()->map(
function ($r) {
return (array) $r;
}
);
$total = $todos->count();
$lastPage = max(1, (int) ceil($total / $perPage));
$page = max(1, min($page, $lastPage));
$slice = $todos->sortByDesc('data')->slice(($page - 1) * $perPage, $perPage)->values();
$ids = $slice->pluck('id')->toArray();
$receitas = collect();
if (!empty($ids)) {
$rows = self::queryAmbulatorioDetalhes($ids)->get();
foreach ($rows as $row) {
$receitas->push(
[
'id' => $row->prescricao_id,
'data_emissao' => $row->data_receita,
'validade' => null,
'situacao' => 'ativa',
'unidade' => $row->unidade_nome,
'prescritor' => $row->prescritor_nome,
'total_itens' => $row->total_itens,
]
);
}
}
self::log($pacienteId, 'LISTA_RECEITAS');
return [
'habilitado' => true,
'data' => $receitas->sortByDesc('data_emissao')->values(),
'current_page' => $page,
'last_page' => $lastPage,
'total' => $total,
'per_page' => $perPage,
];
}
public static function show(int $id): array
{
$parametrizacao = ParametrizacaoAppCidadao::first();
if (!($parametrizacao->permite_envio_receitas_medicas ?? false)) {
return ['habilitado' => false];
}
$pacienteId = auth('mobile')->user()->id;
$pacienteNome = DB::table('cadastro_paciente')
->join('cadastro_cidadao', 'cadastro_cidadao.id', 'cadastro_paciente.cidadao_id')
->where('cadastro_paciente.id', $pacienteId)
->value('cadastro_cidadao.nome');
$header = DB::table('ambulatorio_atendimento_soap_prescricao_medicamento as prescricao')
->select(
'prescricao.id',
'prescricao.created_at as data_receita',
'prescritor_cidadao.nome as prescritor_nome',
'cadastro_unidade.nome_fantasia as unidade_nome',
'cid.codigo as cid_codigo',
'cid.nome as cid_nome'
)
->join('ambulatorio_atendimento_soap as soap', 'soap.id', 'prescricao.soap_id')
->join('ambulatorio_atendimento as atendimento', 'atendimento.id', 'soap.atendimento_id')
->leftJoin('cadastro_lotacao', 'cadastro_lotacao.id', 'prescricao.created_lotacao_id')
->leftJoin('cadastro_usuario', 'cadastro_usuario.id', 'cadastro_lotacao.usuario_id')
->leftJoin('cadastro_cidadao as prescritor_cidadao', 'prescritor_cidadao.id', 'cadastro_usuario.cidadao_id')
->leftJoin('cadastro_infraestrutura', 'cadastro_infraestrutura.id', 'cadastro_lotacao.infraestrutura_id')
->leftJoin('cadastro_unidade', 'cadastro_unidade.id', 'cadastro_infraestrutura.unidade_id')
->leftJoin(
DB::raw('(SELECT soap_id, MIN(cid_id) as cid_id FROM ambulatorio_atendimento_soap_problema_condicao WHERE cid_id IS NOT NULL GROUP BY soap_id) as cond'),
'cond.soap_id',
'=',
'soap.id'
)
->leftJoin('sigtap.cid as cid', 'cid.id', 'cond.cid_id')
->where('prescricao.id', $id)
->where(function ($q) use ($pacienteId) {
$q->whereExists(function ($q2) use ($pacienteId) {
$q2->select(DB::raw(1))
->from('ambulatorio_agendamento as ag')
->whereColumn('ag.id', 'atendimento.agendamento_id')
->where('ag.paciente_id', $pacienteId);
})->orWhereExists(function ($q2) use ($pacienteId) {
$q2->select(DB::raw(1))
->from('ambulatorio_demanda_espontanea as de')
->whereColumn('de.id', 'atendimento.demanda_espontanea_id')
->where('de.paciente_id', $pacienteId);
});
})
->first();
abort_if(!$header, Response::HTTP_NOT_FOUND);
$itens = DB::table('ambulatorio_atendimento_soap_prescricao_medicamento_item as item')
->select(
'item.id',
'item.material_id',
'item.descricao as item_descricao',
'item.justificativa',
'item.dose',
'item.intervalo',
'item.duracao',
'item.duracao_medida_tempo_id',
'item.recomendacoes',
'medida_tempo.nome as intervalo_medida_tempo_nome',
'unidade_medida.nome as apresentacao_unidade_medida',
'cadastro_via_administracao.nome as via_administracao',
DB::raw("CASE WHEN item.material_id IS NOT NULL THEN cadastro_material.descricao ELSE item.descricao END AS material_descricao")
)
->leftJoin('cadastro_material', 'cadastro_material.id', 'item.material_id')
->leftJoin('cadastro_unidade_medida AS unidade_medida', 'unidade_medida.id', 'item.unidade_medida_id')
->leftJoin('geral_medida_tempo as medida_tempo', 'medida_tempo.id', 'item.intervalo_medida_tempo_id')
->leftJoin('cadastro_via_administracao', 'cadastro_via_administracao.id', 'cadastro_material.via_administracao_id')
->where('item.prescricao_medicamento_id', $id)
->get()
->map(
function ($item) {
return [
'id' => $item->id,
'tipo' => is_null($item->material_id) ? 'nao_padronizado' : 'padronizado',
'medicamento' => $item->material_descricao,
'justificativa' => is_null($item->material_id) ? $item->justificativa : null,
'posologia_texto' => self::montarPosologiaTexto($item),
'posologia_incompleta' => is_null($item->intervalo),
'unidade_medida' => $item->apresentacao_unidade_medida,
'intervalo_tempo' => $item->intervalo,
'intervalo_medida_tempo' => $item->intervalo_medida_tempo_nome,
'durante_tempo' => self::calcularDuracaoEmDias($item->duracao, $item->duracao_medida_tempo_id),
'via_administracao' => $item->via_administracao,
'recomendacoes' => $item->recomendacoes,
];
}
);
self::log($pacienteId, 'DETALHE_RECEITA', $id);
return [
'id' => $header->id,
'paciente' => $pacienteNome,
'data_emissao' => $header->data_receita,
'validade' => null,
'situacao' => 'ativa',
'cid' => [
'codigo' => $header->cid_codigo,
'descricao' => $header->cid_nome,
],
'prescritor' => $header->prescritor_nome,
'unidade' => $header->unidade_nome,
'itens' => $itens,
];
}
private static function queryAmbulatorioIds(int $pacienteId)
{
return DB::table('ambulatorio_atendimento_soap_prescricao_medicamento as prescricao')
->select('prescricao.id', 'prescricao.created_at as data')
->join('ambulatorio_atendimento_soap as soap', 'soap.id', 'prescricao.soap_id')
->join('ambulatorio_atendimento as atendimento', 'atendimento.id', 'soap.atendimento_id')
->where('prescricao.solicitacao_fechada', false)
->where(function ($q) use ($pacienteId) {
$q->whereExists(function ($q2) use ($pacienteId) {
$q2->select(DB::raw(1))
->from('ambulatorio_agendamento as ag')
->whereColumn('ag.id', 'atendimento.agendamento_id')
->where('ag.paciente_id', $pacienteId);
})->orWhereExists(function ($q2) use ($pacienteId) {
$q2->select(DB::raw(1))
->from('ambulatorio_demanda_espontanea as de')
->whereColumn('de.id', 'atendimento.demanda_espontanea_id')
->where('de.paciente_id', $pacienteId);
});
});
}
private static function queryAmbulatorioDetalhes(array $ids)
{
return DB::table('ambulatorio_atendimento_soap_prescricao_medicamento as prescricao')
->select(
'prescricao.id as prescricao_id',
'prescricao.created_at as data_receita',
'prescritor_cidadao.nome as prescritor_nome',
'cadastro_unidade.nome_fantasia as unidade_nome',
DB::raw('(SELECT COUNT(*) FROM ambulatorio_atendimento_soap_prescricao_medicamento_item WHERE prescricao_medicamento_id = prescricao.id) as total_itens')
)
->leftJoin('cadastro_lotacao', 'cadastro_lotacao.id', 'prescricao.created_lotacao_id')
->leftJoin('cadastro_usuario', 'cadastro_usuario.id', 'cadastro_lotacao.usuario_id')
->leftJoin('cadastro_cidadao as prescritor_cidadao', 'prescritor_cidadao.id', 'cadastro_usuario.cidadao_id')
->leftJoin('cadastro_infraestrutura', 'cadastro_infraestrutura.id', 'cadastro_lotacao.infraestrutura_id')
->leftJoin('cadastro_unidade', 'cadastro_unidade.id', 'cadastro_infraestrutura.unidade_id')
->whereIn('prescricao.id', $ids);
}
private static function calcularDuracaoEmDias(?int $duracao, ?int $medidaTempoId): ?int
{
if (is_null($duracao)) {
return null;
}
if ($medidaTempoId === MedidaTempo::HORAS) {
return max(1, (int) ceil($duracao / 24));
}
if ($medidaTempoId === MedidaTempo::SEMANAS) {
return $duracao * 7;
}
if ($medidaTempoId === MedidaTempo::MESES) {
return $duracao * 30;
}
return $duracao;
}
private static function pluralizar(string $nome, int $valor): string
{
if (strpos($nome, '(es)') !== false) {
return $valor === 1
? str_replace('(es)', '', $nome)
: str_replace('(es)', 'es', $nome);
}
if (strpos($nome, '(s)') !== false) {
return $valor === 1
? str_replace('(s)', '', $nome)
: str_replace('(s)', 's', $nome);
}
return $nome;
}
private static function montarPosologiaTexto($row): ?string
{
$texto = '';
if (!is_null($row->dose)) {
$dose = (float) trim($row->dose);
$texto .= fmod($dose, 1) == 0 ? (int) $dose : number_format($dose, 2, ',', '.');
if ($row->apresentacao_unidade_medida) {
$texto .= ' ' . strtolower($row->apresentacao_unidade_medida);
}
}
if (!is_null($row->intervalo)) {
$intervalo = (int) $row->intervalo;
if ($row->intervalo_medida_tempo_nome) {
$unidade = strtolower(self::pluralizar($row->intervalo_medida_tempo_nome, $intervalo));
$texto .= $intervalo === 1
? " a cada {$unidade}"
: " a cada {$intervalo} {$unidade}";
} else {
$texto .= " a cada {$intervalo}";
}
}
if (!is_null($row->duracao)) {
$dias = self::calcularDuracaoEmDias((int) $row->duracao, $row->duracao_medida_tempo_id);
if (!is_null($dias)) {
$texto .= $dias === 1 ? ' durante 1 dia' : " durante {$dias} dias";
}
}
return empty($texto) ? null : $texto;
}
private static function log(int $pacienteId, string $tipoAcao, ?int $receitaId = null): void
{
AppCidadaoLog::create(
[
'paciente_id' => $pacienteId,
'receita_id' => $receitaId,
'tipo_acao' => $tipoAcao,
]
);
}
}

View File

@@ -0,0 +1,388 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\HistoricoAtendimento;
use App\Services\Service;
use App\Services\Ambulatorio\AtendimentoHistoricoService as AmbulatorioHistoricoService;
use App\Services\ProntoAtendimento\HistoricoService as ProntoAtendimentoHistoricoService;
use App\Models\Cadastro\SujeitoAtencao\Paciente;
use App\Models\Mobile\ConectaCidadao\ConectaCidadaoAvaliacaoAtendimento;
use App\Models\Ambulatorio\AtendimentoPreAtendimento;
use App\Models\Ambulatorio\AtendimentoSoap;
use App\Models\Ambulatorio\AtendimentoPosConsulta;
use App\Models\ProntoAtendimento\Atendimento\Atendimento;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
use Exception;
class HistoricoAtendimentoService extends Service
{
public function getHistorico(Paciente $paciente, $limit = 15)
{
$preAtendimentos = self::getPreAtendimentoQuery($paciente->id);
$atendimentosSoap = self::getAtendimentoSoapQuery($paciente->id);
$posConsulta = self::getPosConsultaQuery($paciente->id);
$prontoAtendimento = self::getProntoAtendimentosQuery($paciente->id);
$query = $preAtendimentos
->unionAll($atendimentosSoap)
->unionAll($posConsulta)
->unionAll($prontoAtendimento)
->orderBy('data_atendimento', 'desc');
$paginator = $query->paginate((int) $limit);
return [
'current_page' => $paginator->currentPage(),
'data' => array_map(function ($item) {
return self::formatItem($item);
}, $paginator->items()),
'first_page_url' => $paginator->url(1),
'from' => $paginator->firstItem(),
'last_page' => $paginator->lastPage(),
'last_page_url' => $paginator->url($paginator->lastPage()),
'next_page_url' => $paginator->nextPageUrl(),
'path' => $paginator->path(),
'per_page' => $paginator->perPage(),
'prev_page_url' => $paginator->previousPageUrl(),
'to' => $paginator->lastItem(),
'total' => $paginator->total(),
];
}
public function getDetalhes($id, $sourceType)
{
switch ($sourceType) {
case 'pre-atendimento':
return AmbulatorioHistoricoService::getPreAtendimento($id);
case 'soap':
return AmbulatorioHistoricoService::getAtendimentoSoap($id);
case 'pos-consulta':
return AmbulatorioHistoricoService::getPosConsulta($id);
case 'pronto-atendimento':
return ProntoAtendimentoHistoricoService::getAtendimento($id);
default:
throw new Exception('Tipo de atendimento inválido.');
}
}
public function avaliar(string $sourceType, int $id, array $params): void
{
$avaliacao = new ConectaCidadaoAvaliacaoAtendimento();
switch ($sourceType) {
case 'pre-atendimento':
AtendimentoPreAtendimento::findOrFail($id);
$avaliacao->ambulatorio_atendimento_pre_atendimento_id = $id;
break;
case 'soap':
AtendimentoSoap::findOrFail($id);
$avaliacao->ambulatorio_atendimento_soap_id = $id;
break;
case 'pos-consulta':
AtendimentoPosConsulta::findOrFail($id);
$avaliacao->ambulatorio_atendimento_pos_consulta_id = $id;
break;
case 'pronto-atendimento':
Atendimento::findOrFail($id);
$avaliacao->pronto_atendimento_atendimento_id = $id;
break;
default:
throw new \InvalidArgumentException('Tipo de origem inválido.');
}
$avaliacao->nota = $params['nota'] ?? null;
$avaliacao->comentario = $params['comentario'] ?? null;
$avaliacao->save();
}
private static function formatItem($item)
{
$item->created_at = Carbon::create($item->data_atendimento);
$dadosHistorico = [
'tipo' => $item->source_type,
'data_hora' => $item->created_at->format('d/m/Y \à\s H:i'),
'lotacao' => [
'ocupacao' => [
'codigo' => $item->ocupacao_codigo,
'nome' => $item->ocupacao_nome,
],
'profissional' => [
'nome' => $item->cidadao_nome,
],
],
'centro_custo' => [
'setor' => [
'nome' => $item->setor_nome,
],
'unidade' => [
'nome_fantasia' => $item->unidade_nome_fantasia,
],
],
'tipo_atendimento' => $item->tipo_atendimento,
'id' => $item->id,
'atendimento_id' => $item->atendimento_id,
'is_atendimento_enfermagem' => (bool) $item->is_atendimento_enfermagem,
'is_atendimento_soap_assinado' => (bool) $item->is_atendimento_soap_assinado,
];
return (object) $dadosHistorico;
}
private static function getProntoAtendimentosQuery($pacienteId)
{
return DB::table('pronto_atendimento_atendimento')
->select([
'pronto_atendimento_atendimento.id',
'pronto_atendimento_atendimento.id as atendimento_id',
'pronto_atendimento_atendimento_recepcao.atendido_em as data_atendimento',
'ocupacao.id AS ocupacao_id',
'ocupacao.codigo AS ocupacao_codigo',
'ocupacao.nome AS ocupacao_nome',
'cadastro_cidadao.nome AS cidadao_nome',
'cadastro_setor.nome AS setor_nome',
'cadastro_centro_custo.infraestrutura_id AS centro_custo_infraestrutura_id',
'cadastro_unidade.nome_fantasia AS unidade_nome_fantasia',
DB::raw("'Pronto Atendimento' AS tipo_atendimento"),
DB::raw("false as is_atendimento_enfermagem"),
DB::raw("false as is_atendimento_soap_assinado"),
DB::raw("'pronto-atendimento' as source_type")
])
->join('pronto_atendimento_faa', 'pronto_atendimento_faa.id', 'pronto_atendimento_atendimento.faa_id')
->join('pronto_atendimento_atendimento_recepcao', 'pronto_atendimento_atendimento_recepcao.atendimento_id', 'pronto_atendimento_atendimento.id')
->join('cadastro_lotacao', 'cadastro_lotacao.id', 'pronto_atendimento_faa.lotacao_id')
->join('cadastro_usuario', 'cadastro_usuario.id', 'cadastro_lotacao.usuario_id')
->join('cadastro_cidadao', 'cadastro_cidadao.id', 'cadastro_usuario.cidadao_id')
->join('sigtap.ocupacao', 'ocupacao.id', 'cadastro_lotacao.ocupacao_id')
->join('cadastro_centro_custo', 'cadastro_centro_custo.id', 'pronto_atendimento_faa.created_centro_custo_id')
->join('cadastro_setor', 'cadastro_setor.id', 'cadastro_centro_custo.setor_id')
->join('cadastro_infraestrutura', 'cadastro_infraestrutura.id', 'cadastro_centro_custo.infraestrutura_id')
->join('cadastro_unidade', 'cadastro_unidade.id', 'cadastro_infraestrutura.unidade_id')
->where('pronto_atendimento_faa.paciente_id', $pacienteId)
->whereNull('pronto_atendimento_atendimento.deleted_at');
}
private static function getPreAtendimentoQuery($pacienteId)
{
return DB::table('ambulatorio_atendimento_pre_atendimento')
->select([
'ambulatorio_atendimento_pre_atendimento.id',
'ambulatorio_atendimento_pre_atendimento.atendimento_id',
'ambulatorio_atendimento_pre_atendimento.created_at as data_atendimento',
'ocupacao.id AS ocupacao_id',
'ocupacao.codigo AS ocupacao_codigo',
'ocupacao.nome AS ocupacao_nome',
'cadastro_cidadao.nome AS cidadao_nome',
'cadastro_setor.nome AS setor_nome',
'cadastro_centro_custo.infraestrutura_id AS centro_custo_infraestrutura_id',
'cadastro_unidade.nome_fantasia AS unidade_nome_fantasia',
DB::raw("'Pré-atendimento' AS tipo_atendimento"),
DB::raw("false as is_atendimento_enfermagem"),
DB::raw("false as is_atendimento_soap_assinado"),
DB::raw("'pre-atendimento' as source_type")
])
->join('ambulatorio_atendimento', 'ambulatorio_atendimento.id', 'ambulatorio_atendimento_pre_atendimento.atendimento_id')
->join('ambulatorio_agendamento', 'ambulatorio_agendamento.id', 'ambulatorio_atendimento.agendamento_id')
->join('cadastro_lotacao', 'cadastro_lotacao.id', 'ambulatorio_atendimento_pre_atendimento.created_lotacao_id')
->join('cadastro_usuario', 'cadastro_usuario.id', 'cadastro_lotacao.usuario_id')
->join('cadastro_cidadao', 'cadastro_cidadao.id', 'cadastro_usuario.cidadao_id')
->join('sigtap.ocupacao', 'ocupacao.id', 'cadastro_lotacao.ocupacao_id')
->join('cadastro_centro_custo', 'cadastro_centro_custo.id', 'ambulatorio_atendimento_pre_atendimento.created_centro_custo_id')
->join('cadastro_setor', 'cadastro_setor.id', 'cadastro_centro_custo.setor_id')
->join('cadastro_infraestrutura', 'cadastro_infraestrutura.id', 'cadastro_centro_custo.infraestrutura_id')
->join('cadastro_unidade', 'cadastro_unidade.id', 'cadastro_infraestrutura.unidade_id')
->where('ambulatorio_agendamento.paciente_id', $pacienteId)
->unionAll(
DB::table('ambulatorio_atendimento_pre_atendimento')
->select([
'ambulatorio_atendimento_pre_atendimento.id',
'ambulatorio_atendimento_pre_atendimento.atendimento_id',
'ambulatorio_atendimento_pre_atendimento.created_at as data_atendimento',
'ocupacao.id AS ocupacao_id',
'ocupacao.codigo AS ocupacao_codigo',
'ocupacao.nome AS ocupacao_nome',
'cadastro_cidadao.nome AS cidadao_nome',
'cadastro_setor.nome AS setor_nome',
'cadastro_centro_custo.infraestrutura_id AS centro_custo_infraestrutura_id',
'cadastro_unidade.nome_fantasia AS unidade_nome_fantasia',
DB::raw("'Pré-atendimento' AS tipo_atendimento"),
DB::raw("false as is_atendimento_enfermagem"),
DB::raw("false as is_atendimento_soap_assinado"),
DB::raw("'pre-atendimento' as source_type")
])
->join('ambulatorio_atendimento', 'ambulatorio_atendimento.id', 'ambulatorio_atendimento_pre_atendimento.atendimento_id')
->join('ambulatorio_demanda_espontanea', 'ambulatorio_demanda_espontanea.id', 'ambulatorio_atendimento.demanda_espontanea_id')
->join('cadastro_lotacao', 'cadastro_lotacao.id', 'ambulatorio_atendimento_pre_atendimento.created_lotacao_id')
->join('cadastro_usuario', 'cadastro_usuario.id', 'cadastro_lotacao.usuario_id')
->join('cadastro_cidadao', 'cadastro_cidadao.id', 'cadastro_usuario.cidadao_id')
->join('sigtap.ocupacao', 'ocupacao.id', 'cadastro_lotacao.ocupacao_id')
->join('cadastro_centro_custo', 'cadastro_centro_custo.id', 'ambulatorio_atendimento_pre_atendimento.created_centro_custo_id')
->join('cadastro_setor', 'cadastro_setor.id', 'cadastro_centro_custo.setor_id')
->join('cadastro_infraestrutura', 'cadastro_infraestrutura.id', 'cadastro_centro_custo.infraestrutura_id')
->join('cadastro_unidade', 'cadastro_unidade.id', 'cadastro_infraestrutura.unidade_id')
->where('ambulatorio_demanda_espontanea.paciente_id', $pacienteId)
);
}
private static function getAtendimentoSoapQuery($pacienteId)
{
return DB::table('ambulatorio_atendimento_soap')
->select([
'ambulatorio_atendimento_soap.id',
'ambulatorio_atendimento_soap.atendimento_id',
'jornada.data as data_atendimento',
'ocupacao.id AS ocupacao_id',
'ocupacao.codigo AS ocupacao_codigo',
'ocupacao.nome AS ocupacao_nome',
'cadastro_cidadao.nome AS cidadao_nome',
'cadastro_setor.nome AS setor_nome',
'cadastro_centro_custo.infraestrutura_id AS centro_custo_infraestrutura_id',
'cadastro_unidade.nome_fantasia AS unidade_nome_fantasia',
DB::raw("'Atendimento' AS tipo_atendimento"),
'ambulatorio_atendimento_soap.is_atendimento_enfermagem',
'ambulatorio_atendimento_soap.is_atendimento_soap_assinado',
DB::raw("'soap' as source_type")
])
->join('ambulatorio_atendimento', 'ambulatorio_atendimento.id', 'ambulatorio_atendimento_soap.atendimento_id')
->join('ambulatorio_agendamento', 'ambulatorio_agendamento.id', 'ambulatorio_atendimento.agendamento_id')
->join('ambulatorio_jornada as jornada', 'jornada.id', 'ambulatorio_agendamento.jornada_id')
->join('cadastro_lotacao', 'cadastro_lotacao.id', 'jornada.lotacao_id')
->join('cadastro_usuario', 'cadastro_usuario.id', 'cadastro_lotacao.usuario_id')
->join('cadastro_cidadao', 'cadastro_cidadao.id', 'cadastro_usuario.cidadao_id')
->join('sigtap.ocupacao', 'ocupacao.id', 'cadastro_lotacao.ocupacao_id')
->join('cadastro_centro_custo', 'cadastro_centro_custo.id', 'ambulatorio_atendimento_soap.created_centro_custo_id')
->join('cadastro_setor', 'cadastro_setor.id', 'cadastro_centro_custo.setor_id')
->join('cadastro_infraestrutura', 'cadastro_infraestrutura.id', 'cadastro_centro_custo.infraestrutura_id')
->join('cadastro_unidade', 'cadastro_unidade.id', 'cadastro_infraestrutura.unidade_id')
->where('ambulatorio_atendimento_soap.is_rascunho', false)
->where('ambulatorio_agendamento.paciente_id', $pacienteId)
->unionAll(
DB::table('ambulatorio_atendimento_soap')
->select([
'ambulatorio_atendimento_soap.id',
'ambulatorio_atendimento_soap.atendimento_id',
'ambulatorio_demanda_espontanea.data as data_atendimento',
'ocupacao.id AS ocupacao_id',
'ocupacao.codigo AS ocupacao_codigo',
'ocupacao.nome AS ocupacao_nome',
'cadastro_cidadao.nome AS cidadao_nome',
'cadastro_setor.nome AS setor_nome',
'cadastro_centro_custo.infraestrutura_id AS centro_custo_infraestrutura_id',
'cadastro_unidade.nome_fantasia AS unidade_nome_fantasia',
DB::raw("'Atendimento' AS tipo_atendimento"),
'ambulatorio_atendimento_soap.is_atendimento_enfermagem',
'ambulatorio_atendimento_soap.is_atendimento_soap_assinado',
DB::raw("'soap' as source_type")
])
->join('ambulatorio_atendimento', 'ambulatorio_atendimento.id', 'ambulatorio_atendimento_soap.atendimento_id')
->join('ambulatorio_demanda_espontanea', 'ambulatorio_demanda_espontanea.id', 'ambulatorio_atendimento.demanda_espontanea_id')
->join('cadastro_lotacao', 'cadastro_lotacao.id', 'ambulatorio_atendimento_soap.created_lotacao_id')
->join('cadastro_usuario', 'cadastro_usuario.id', 'cadastro_lotacao.usuario_id')
->join('cadastro_cidadao', 'cadastro_cidadao.id', 'cadastro_usuario.cidadao_id')
->join('sigtap.ocupacao', 'ocupacao.id', 'cadastro_lotacao.ocupacao_id')
->join('cadastro_centro_custo', 'cadastro_centro_custo.id', 'ambulatorio_atendimento_soap.created_centro_custo_id')
->join('cadastro_setor', 'cadastro_setor.id', 'cadastro_centro_custo.setor_id')
->join('cadastro_infraestrutura', 'cadastro_infraestrutura.id', 'cadastro_centro_custo.infraestrutura_id')
->join('cadastro_unidade', 'cadastro_unidade.id', 'cadastro_infraestrutura.unidade_id')
->where('ambulatorio_atendimento_soap.is_rascunho', false)
->where('ambulatorio_demanda_espontanea.paciente_id', $pacienteId)
);
}
private static function getPosConsultaQuery($pacienteId)
{
return DB::table('ambulatorio_atendimento_pos_consulta')
->select([
'ambulatorio_atendimento_pos_consulta.id',
'ambulatorio_atendimento_pos_consulta.atendimento_id',
'ambulatorio_atendimento_pos_consulta.created_at as data_atendimento',
'ocupacao.id AS ocupacao_id',
'ocupacao.codigo AS ocupacao_codigo',
'ocupacao.nome AS ocupacao_nome',
'cadastro_cidadao.nome AS cidadao_nome',
'cadastro_setor.nome AS setor_nome',
'cadastro_centro_custo.infraestrutura_id AS centro_custo_infraestrutura_id',
'cadastro_unidade.nome_fantasia AS unidade_nome_fantasia',
DB::raw("'Pós-consulta' AS tipo_atendimento"),
DB::raw("false as is_atendimento_enfermagem"),
DB::raw("false as is_atendimento_soap_assinado"),
DB::raw("'pos-consulta' as source_type")
])
->join('ambulatorio_atendimento', 'ambulatorio_atendimento.id', 'ambulatorio_atendimento_pos_consulta.atendimento_id')
->join('ambulatorio_agendamento', 'ambulatorio_agendamento.id', 'ambulatorio_atendimento.agendamento_id')
->join('cadastro_lotacao', 'cadastro_lotacao.id', 'ambulatorio_atendimento_pos_consulta.created_lotacao_id')
->join('cadastro_usuario', 'cadastro_usuario.id', 'cadastro_lotacao.usuario_id')
->join('cadastro_cidadao', 'cadastro_cidadao.id', 'cadastro_usuario.cidadao_id')
->join('sigtap.ocupacao', 'ocupacao.id', 'cadastro_lotacao.ocupacao_id')
->join('cadastro_centro_custo', 'cadastro_centro_custo.id', 'ambulatorio_atendimento_pos_consulta.created_centro_custo_id')
->join('cadastro_setor', 'cadastro_setor.id', 'cadastro_centro_custo.setor_id')
->join('cadastro_infraestrutura', 'cadastro_infraestrutura.id', 'cadastro_centro_custo.infraestrutura_id')
->join('cadastro_unidade', 'cadastro_unidade.id', 'cadastro_infraestrutura.unidade_id')
->where('ambulatorio_agendamento.paciente_id', $pacienteId)
->unionAll(
DB::table('ambulatorio_atendimento_pos_consulta')
->select([
'ambulatorio_atendimento_pos_consulta.id',
'ambulatorio_atendimento_pos_consulta.atendimento_id',
'ambulatorio_atendimento_pos_consulta.created_at as data_atendimento',
'ocupacao.id AS ocupacao_id',
'ocupacao.codigo AS ocupacao_codigo',
'ocupacao.nome AS ocupacao_nome',
'cadastro_cidadao.nome AS cidadao_nome',
'cadastro_setor.nome AS setor_nome',
'cadastro_centro_custo.infraestrutura_id AS centro_custo_infraestrutura_id',
'cadastro_unidade.nome_fantasia AS unidade_nome_fantasia',
DB::raw("'Pós-consulta' AS tipo_atendimento"),
DB::raw("false as is_atendimento_enfermagem"),
DB::raw("false as is_atendimento_soap_assinado"),
DB::raw("'pos-consulta' as source_type")
])
->join('ambulatorio_atendimento', 'ambulatorio_atendimento.id', 'ambulatorio_atendimento_pos_consulta.atendimento_id')
->join('ambulatorio_demanda_espontanea', 'ambulatorio_demanda_espontanea.id', 'ambulatorio_atendimento.demanda_espontanea_id')
->join('cadastro_lotacao', 'cadastro_lotacao.id', 'ambulatorio_atendimento_pos_consulta.created_lotacao_id')
->join('cadastro_usuario', 'cadastro_usuario.id', 'cadastro_lotacao.usuario_id')
->join('cadastro_cidadao', 'cadastro_cidadao.id', 'cadastro_usuario.cidadao_id')
->join('sigtap.ocupacao', 'ocupacao.id', 'cadastro_lotacao.ocupacao_id')
->join('cadastro_centro_custo', 'cadastro_centro_custo.id', 'ambulatorio_atendimento_pos_consulta.created_centro_custo_id')
->join('cadastro_setor', 'cadastro_setor.id', 'cadastro_centro_custo.setor_id')
->join('cadastro_infraestrutura', 'cadastro_infraestrutura.id', 'cadastro_centro_custo.infraestrutura_id')
->join('cadastro_unidade', 'cadastro_unidade.id', 'cadastro_infraestrutura.unidade_id')
->where('ambulatorio_demanda_espontanea.paciente_id', $pacienteId)
);
}
public function avaliacao(string $sourceType, int $id, array $params)
{
$campoMap = [
'pre-atendimento' => 'ambulatorio_atendimento_pre_atendimento_id',
'soap' => 'ambulatorio_atendimento_soap_id',
'pos-consulta' => 'ambulatorio_atendimento_pos_consulta_id',
'pronto-atendimento' => 'pronto_atendimento_atendimento_id',
];
if (!isset($campoMap[$sourceType])) {
throw new \InvalidArgumentException("Tipo de atendimento inválido: {$sourceType}");
}
$campo = $campoMap[$sourceType];
$avaliacao = DB::table('conecta_cidadao_avaliacao_atendimento')
->where($campo, $id)
->first();
if (!$avaliacao) {
throw new ModelNotFoundException('Avaliação não encontrada.');
}
return $avaliacao;
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\PerfilCidadao;
use App\Services\Service;
use App\Models\Cadastro\SujeitoAtencao\Paciente;
class PerfilCidadaoService extends Service
{
public function getDadosCidadao(Paciente $paciente): array
{
$cidadao = $paciente->cidadao;
$telefones = $cidadao->telefones->map(function ($telefone) {
return [
'numero' => $telefone->numero,
'principal' => (bool) $telefone->principal,
'autoriza_sms' => (bool) $telefone->autoriza_sms,
];
})->values();
$endereco = null;
$domicilio = $cidadao->domicilio;
if ($domicilio && $domicilio->endereco) {
$end = $domicilio->endereco;
$logradouro = $end->logradouro;
$endereco = [
'logradouro' => $logradouro ? $logradouro->nome : null,
'tipo_logradouro' => ($logradouro && $logradouro->tipo) ? $logradouro->tipo->descricao : null,
'cep' => $logradouro ? $logradouro->cep : null,
'numero' => $end->numero ?? null,
'complemento' => $end->complemento ?? null,
'referencia' => $end->referencia ?? null,
'bairro' => ($logradouro && $logradouro->bairro) ? $logradouro->bairro->nome : null,
'municipio' => ($logradouro && $logradouro->municipio) ? $logradouro->municipio->nome : null,
'uf' => ($logradouro && $logradouro->municipio && $logradouro->municipio->estado) ? $logradouro->municipio->estado->uf : null,
];
}
$cns = $cidadao->cns;
$nome = $cidadao->nome_social ? $cidadao->nome_social : $cidadao->nome;
$unidade = $paciente->unidade;
return [
'paciente_id' => $paciente->id,
'prontuario' => $paciente->prontuario,
'nome' => $nome,
'nome_completo' => $cidadao->nome,
'nome_social' => $cidadao->nome_social ?? null,
'cpf' => $cidadao->cpf,
'cns' => $cns,
'data_nascimento' => $cidadao->data_nascimento,
'idade' => $cidadao->idade_formatada,
'sexo' => ($cidadao->sexo) ? $cidadao->sexo->descricao : null,
'email' => $paciente->email ?? null,
'telefones' => $telefones,
'endereco' => $endereco,
'unidade_referencia' => $unidade ? [
'id' => $unidade->id,
'nome' => $unidade->nome_fantasia,
'cnes' => $unidade->cnes,
] : null,
];
}
}

View File

@@ -0,0 +1,114 @@
<?php
namespace App\Services\Mobile\ConectaCidadao\Termos;
use App\Enums\Mobile\ConectaCidadao\TermoTipo;
use App\Models\Mobile\ConectaCidadao\ConectaCidadaoAceitacaoCidadao;
use App\Models\Mobile\ConectaCidadao\ConectaCidadaoTermo;
use App\Models\Mobile\ConectaCidadao\ConectaCidadaoTermoTipo;
use App\Services\Service;
use Carbon\Carbon;
use Illuminate\Http\Response;
class TermoService extends Service
{
public static function getAtivoByTipo(string $tipo): ?ConectaCidadaoTermo
{
$tipoId = ConectaCidadaoTermoTipo::where('codigo', $tipo)->value('id');
return ConectaCidadaoTermo::where('tipo_id', $tipoId)
->where('ativo', true)
->latest('data_publicacao')
->first();
}
public static function getAtivo(): ConectaCidadaoTermo
{
$tipo = request()->validate(
[
'tipo' => 'required|string|in:' . TermoTipo::TERMO_USO,
]
)['tipo'];
$tipoId = ConectaCidadaoTermoTipo::where('codigo', $tipo)->value('id');
$termo = ConectaCidadaoTermo::where('tipo_id', $tipoId)
->where('ativo', true)
->latest('data_publicacao')
->first();
if (!$termo) {
abort(Response::HTTP_NOT_FOUND, "Nenhum termo ativo encontrado para o tipo '{$tipo}'.");
}
return $termo;
}
public static function getById(int $id): ConectaCidadaoTermo
{
return ConectaCidadaoTermo::findOrFail($id);
}
public static function getPendentes(): array
{
$cidadaoId = self::getCidadaoId();
return ConectaCidadaoTermo::where('ativo', true)
->where('obrigatorio', true)
->whereNotIn('id', function ($query) use ($cidadaoId) {
$query->select('termo_id')
->from('conecta_cidadao_aceitacao_cidadao')
->where('cidadao_id', $cidadaoId);
})
->get(
[
'id',
'tipo_id',
'versao',
'data_publicacao',
'obrigatorio'
]
)
->toArray();
}
public static function aceitar(int $id): ConectaCidadaoAceitacaoCidadao
{
ConectaCidadaoTermo::findOrFail($id);
$cidadaoId = self::getCidadaoId();
$jaAceito = ConectaCidadaoAceitacaoCidadao::where('cidadao_id', $cidadaoId)
->where('termo_id', $id)
->exists();
if ($jaAceito) {
abort(Response::HTTP_UNPROCESSABLE_ENTITY, 'Este termo já foi aceito anteriormente.');
}
return ConectaCidadaoAceitacaoCidadao::create([
'cidadao_id' => $cidadaoId,
'termo_id' => $id,
'aceitado_em' => Carbon::now(),
]);
}
public static function getAceitacoes(): array
{
$cidadaoId = self::getCidadaoId();
return ConectaCidadaoAceitacaoCidadao::where('cidadao_id', $cidadaoId)
->with('termo:id,tipo_id,versao,data_publicacao')
->orderByDesc('aceitado_em')
->get()
->toArray();
}
private static function getCidadaoId(): int
{
return auth('mobile')->user()->cidadao_id;
}
}