Files

550 lines
19 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Facades\DB;
class Agenda extends Model
{
use HasFactory;
protected $table = 'agendas';
protected $fillable = [
'estado',
'duracionturno',
'profesional_id',
];
public function profesional()
{
return $this->belongsTo(Profesional::class, 'profesional_id','id');
}
public function diaDeAtencion()
{
return $this->hasMany(DiaDeAtencion::class, 'agenda_id','id');
}
public function turno()
{
return $this->hasMany(Turno::class, 'agenda_id','id');
}
public function feriado()
{
return $this->hasMany(Feriado::class, 'agenda_id','id');
}
public function modoVacaciones()
{
return $this->hasMany(ModoVacaciones::class, 'agenda_id', 'id');
}
public function formularios()
{
return $this->belongsToMany(
\App\Models\Formulario::class,
'profesionales_formularios',
'profesional_id',
'formulario_id',
'profesional_id',
'id'
)->withPivot('estadoformulario')->withTimestamps();
}
public function estaDisponible($fecha, $hora, $agendaId = null) // Verificar si la agenda está disponible para una fecha y hora específicas
{
$agenda = $agendaId ? self::find($agendaId) : $this;
if (!$agenda) {
return false;
}
// Verificar si la fecha es un feriado
if ($agenda->feriado()->where('fecha', $fecha)->exists()) {
return false;
}
// Verificar si la fecha está dentro de un período de vacaciones
if ($agenda->modoVacaciones()->where('inicio', '<=', $fecha)->where('fin', '>=', $fecha)->exists()) {
return false;
}
// Verificar si el día de atención corresponde al día de la semana de la fecha
$diaSemana = date('N', strtotime($fecha)); // 1 (lunes) a 7 (domingo)
$diaAtencion = $agenda->diaDeAtencion()->where('dia_id', $diaSemana)->first();
if (!$diaAtencion) {
return false;
}
// Verificar si la hora está dentro del horario de atención
if (!$diaAtencion->horariosAtenciones()->where('horariocomienzo', '<=', $hora)->where('horariofin', '>=', $hora)->exists()) {
return false;
}
// Verificar si la hora no está dentro de un horario de receso
if ($diaAtencion->horariosRecesos()->where('comienzo', '<=', $hora)->where('fin', '>=', $hora)->exists()) {
return false;
}
// Verificar si ya existe un turno para esa fecha y hora
if ($agenda->turno()->where('inicio', $fecha . ' ' . $hora)->exists()) {
return false;
}
return true;
}
public function obtenerTurnoDisponible($idProfesional, $tipopreferencia = 'INDISTINTO', $diasPreferencia = []) //Devuelve el turno disponible más cercano según preferencias
{
// Inicializa estructuras de salida con valores por defecto.
$DiasDeAtenciones = array_fill(0, 7, array_fill(0, 5, null));
$tipo = null;
$recesos = [];
$vacaciones = [];
$feriados = [];
$turnoMasCercano = null;
// Busca la agenda asociada al profesional recibido por parámetro.
$agendaId = self::where('profesional_id', $idProfesional)->value('id');
// Si no existe agenda para ese profesional, devuelve la estructura vacía.
if (!$agendaId) {
return null;
}
// Obtiene días de atención y sus horarios AM/PM para la agenda encontrada.
// [fila][0]=dia_id, [fila][1]=inicio AM, [fila][2]=fin AM, [fila][3]=inicio PM, [fila][4]=fin PM
$filasAtencion = DB::table('diasdeatenciones as d')
->leftJoin('horariosatenciones as h', 'h.diadeatencion_id', '=', 'd.id')
->where('d.agenda_id', $agendaId)
->select('d.dia_id', 'h.horariocomienzo', 'h.horariofin', 'h.tipo')
->orderBy('d.dia_id')
->get();
// Recorre cada fila y la ubica en la matriz de 7x5 según día y tipo de horario.
foreach ($filasAtencion as $filaAtencion) {
$fila = (int) $filaAtencion->dia_id - 1;
// Ignora valores fuera del rango esperado de días (1 a 7).
if ($fila < 0 || $fila > 6) {
continue;
}
// Guarda el identificador del día en la primera columna.
$DiasDeAtenciones[$fila][0] = $filaAtencion->dia_id;
// Si no hay tipo de horario, salta al siguiente registro.
if ($filaAtencion->tipo === null) {
continue;
}
// Normaliza y guarda el tipo actual (AM o PM).
$tipoActual = strtoupper(trim((string) $filaAtencion->tipo));
$tipo = $tipoActual;
// Completa columnas de mañana (inicio y fin).
if ($tipoActual === 'AM') {
$DiasDeAtenciones[$fila][1] = $filaAtencion->horariocomienzo;
$DiasDeAtenciones[$fila][2] = $filaAtencion->horariofin;
}
// Completa columnas de tarde (inicio y fin).
if ($tipoActual === 'PM') {
$DiasDeAtenciones[$fila][3] = $filaAtencion->horariocomienzo;
$DiasDeAtenciones[$fila][4] = $filaAtencion->horariofin;
}
}
// Trae los recesos de la agenda y los transforma a una matriz [dia_id, comienzo, fin].
$recesos = DB::table('horariosrecesos as r')
->join('diasdeatenciones as d', 'd.id', '=', 'r.diadeatencion_id')
->where('d.agenda_id', $agendaId)
->select('d.dia_id', 'r.comienzo', 'r.fin')
->orderBy('d.dia_id')
->get()
->map(function ($receso) {
return [
$receso->dia_id,
$receso->comienzo,
$receso->fin,
];
})
->values()
->all();
// Trae períodos de vacaciones y los transforma a una matriz [inicio, fin].
$vacaciones = DB::table('modosvacaciones')
->where('agenda_id', $agendaId)
->select('inicio', 'fin')
->orderBy('inicio')
->get()
->map(function ($vacacion) {
return [
$vacacion->inicio,
$vacacion->fin,
];
})
->values()
->all();
// Trae todos los feriados de la agenda y los transforma en una matriz de fechas.
$feriados = DB::table('feriados')
->where('agenda_id', $agendaId)
->select('fecha')
->orderBy('fecha')
->pluck('fecha')
->values()
->all();
// Normaliza preferencia horaria (AM, PM o INDISTINTO).
$tipopreferencia = strtoupper(trim((string) $tipopreferencia));
if (!in_array($tipopreferencia, ['AM', 'PM', 'INDISTINTO'], true)) {
$tipopreferencia = 'INDISTINTO';
}
// Convierte los días preferidos en texto a ids de 1 (lunes) a 7 (domingo).
$mapaDias = [
'lunes' => 1,
'martes' => 2,
'miercoles' => 3,
'jueves' => 4,
'viernes' => 5,
'sabado' => 6,
'domingo' => 7,
];
$diasPreferidosIds = [];
foreach ((array) $diasPreferencia as $diaPreferido) {
$diaNormalizado = strtolower(trim((string) $diaPreferido));
$diaNormalizado = strtr($diaNormalizado, [
'á' => 'a',
'é' => 'e',
'í' => 'i',
'ó' => 'o',
'ú' => 'u',
]);
if (isset($mapaDias[$diaNormalizado])) {
$diasPreferidosIds[] = $mapaDias[$diaNormalizado];
}
}
$diasPreferidosIds = array_values(array_unique($diasPreferidosIds));
// Toma la duración de turno de la agenda; si no existe, usa 30 minutos.
$duracionTurno = (int) (self::where('id', $agendaId)->value('duracionturno') ?? 30);
if ($duracionTurno <= 0) {
$duracionTurno = 30;
}
// Define el punto de inicio de la búsqueda desde el día siguiente (para evitar asignar turnos inmediatos del día actual).
$baseTimestamp = strtotime('tomorrow');
// Crea índices rápidos para validar fechas bloqueadas y horarios ocupados.
$feriadosLookup = array_flip($feriados);
$recesosPorDia = [];
foreach ($recesos as $receso) {
$diaId = (int) $receso[0];
if (!isset($recesosPorDia[$diaId])) {
$recesosPorDia[$diaId] = [];
}
$recesosPorDia[$diaId][] = [$receso[1], $receso[2]];
}
$turnosOcupadosLookup = [];
$turnosOcupados = DB::table('turnos')
->where('agenda_id', $agendaId)
->where('inicio', '>=', date('Y-m-d', $baseTimestamp))
->select('inicio')
->get();
foreach ($turnosOcupados as $turnoOcupado) {
$tsTurno = strtotime((string) $turnoOcupado->inicio);
if ($tsTurno === false) {
continue;
}
$turnosOcupadosLookup[date('Y-m-d H:i:s', $tsTurno)] = true;
}
// Busca el primer turno disponible respetando tipo, días preferidos y reglas de agenda.
for ($offsetDias = 0; $offsetDias <= 120; $offsetDias++) {
$tsDia = strtotime(date('Y-m-d', $baseTimestamp) . ' +' . $offsetDias . ' day');
if ($tsDia === false) {
continue;
}
$fechaIterada = date('Y-m-d', $tsDia);
$diaId = (int) date('N', $tsDia);
if (!empty($diasPreferidosIds) && !in_array($diaId, $diasPreferidosIds, true)) {
continue;
}
if (isset($feriadosLookup[$fechaIterada])) {
continue;
}
$enVacaciones = false;
foreach ($vacaciones as $vacacion) {
if ($fechaIterada >= $vacacion[0] && $fechaIterada <= $vacacion[1]) {
$enVacaciones = true;
break;
}
}
if ($enVacaciones) {
continue;
}
$filaDia = $DiasDeAtenciones[$diaId - 1] ?? null;
if (!$filaDia || $filaDia[0] === null) {
continue;
}
$ventanas = [];
if (($tipopreferencia === 'AM' || $tipopreferencia === 'INDISTINTO') && $filaDia[1] !== null && $filaDia[2] !== null) {
$ventanas[] = ['tipo' => 'AM', 'inicio' => $filaDia[1], 'fin' => $filaDia[2]];
}
if (($tipopreferencia === 'PM' || $tipopreferencia === 'INDISTINTO') && $filaDia[3] !== null && $filaDia[4] !== null) {
$ventanas[] = ['tipo' => 'PM', 'inicio' => $filaDia[3], 'fin' => $filaDia[4]];
}
foreach ($ventanas as $ventana) {
$inicioVentanaTs = strtotime($fechaIterada . ' ' . $ventana['inicio']);
$finVentanaTs = strtotime($fechaIterada . ' ' . $ventana['fin']);
if ($inicioVentanaTs === false || $finVentanaTs === false || $inicioVentanaTs >= $finVentanaTs) {
continue;
}
for ($slotTs = $inicioVentanaTs; ($slotTs + ($duracionTurno * 60)) <= $finVentanaTs; $slotTs += ($duracionTurno * 60)) {
$slotFecha = date('Y-m-d', $slotTs);
$slotHora = date('H:i:s', $slotTs);
$slotDateTime = $slotFecha . ' ' . $slotHora;
$slotFinTs = $slotTs + ($duracionTurno * 60);
if (isset($turnosOcupadosLookup[$slotDateTime])) {
continue;
}
$solapaReceso = false;
foreach ($recesosPorDia[$diaId] ?? [] as $recesoDia) {
$recesoInicioTs = strtotime($slotFecha . ' ' . $recesoDia[0]);
$recesoFinTs = strtotime($slotFecha . ' ' . $recesoDia[1]);
if ($recesoInicioTs === false || $recesoFinTs === false) {
continue;
}
if ($slotTs < $recesoFinTs && $slotFinTs > $recesoInicioTs) {
$solapaReceso = true;
break;
}
}
if ($solapaReceso) {
continue;
}
if (!$this->estaDisponible($slotFecha, $slotHora, $agendaId)) {
continue;
}
$turnoMasCercano = [
'fecha' => $slotFecha,
'hora' => $slotHora,
'fechaHora' => $slotDateTime,
'tipo' => $ventana['tipo'],
'duracionMinutos' => $duracionTurno,
];
break 3;
}
}
}
// Devuelve solo la fecha/hora del turno más cercano o null si no hay disponibilidad.
return $turnoMasCercano['fechaHora'] ?? null;
}
public function crearDiaDeAtencion($diaId, $horarioComienzo, $horarioFin, $tipo)
{
$tipoNormalizado = strtoupper(trim((string) $tipo));
if (!in_array($tipoNormalizado, ['AM', 'PM'], true)) {
throw new \InvalidArgumentException('El tipo debe ser AM o PM.');
}
$horarioComienzoNormalizado = $this->normalizarHora($horarioComienzo, 'horarioComienzo');
$horarioFinNormalizado = $this->normalizarHora($horarioFin, 'horarioFin');
if (strtotime('1970-01-01 ' . $horarioComienzoNormalizado) >= strtotime('1970-01-01 ' . $horarioFinNormalizado)) {
throw new \InvalidArgumentException('horarioComienzo debe ser menor que horarioFin.');
}
return DB::transaction(function () use ($diaId, $tipoNormalizado, $horarioComienzoNormalizado, $horarioFinNormalizado) {
$diaAtencion = DiaDeAtencion::firstOrCreate(
['agenda_id' => $this->id, 'dia_id' => $diaId],
['descripcion' => 'Dia de atencion']
);
HorarioDeAtencion::updateOrCreate(
[
'diadeatencion_id' => $diaAtencion->id,
'tipo' => $tipoNormalizado,
],
[
'horariocomienzo' => $horarioComienzoNormalizado,
'horariofin' => $horarioFinNormalizado,
]
);
return $diaAtencion;
});
}
public function eliminarDiaDeAtencion($diaId)
{
return DB::transaction(function () use ($diaId) {
$diaAtencion = DiaDeAtencion::where('agenda_id', $this->id)->where('dia_id', $diaId)->first();
if ($diaAtencion) {
HorarioDeAtencion::where('diadeatencion_id', $diaAtencion->id)->delete();
HorarioReceso::where('diadeatencion_id', $diaAtencion->id)->delete();
$diaAtencion->delete();
}
});
}
public function crearModoVacaciones($inicio, $fin, $descripcion = null)
{
return ModoVacaciones::create([
'agenda_id' => $this->id,
'inicio' => $inicio,
'fin' => $fin,
'descripcion' => $descripcion,
]);
}
public function eliminarModoVacaciones($id)
{
$modoVacaciones = ModoVacaciones::where('agenda_id', $this->id)->where('id', $id)->first();
if ($modoVacaciones) {
$modoVacaciones->delete();
}
}
public function crearFeriado($fecha, $descripcion = null)
{
return Feriado::create([
'agenda_id' => $this->id,
'fecha' => $fecha,
'descripcion' => $descripcion,
]);
}
public function eliminarFeriado($id)
{
$feriado = Feriado::where('agenda_id', $this->id)->where('id', $id)->first();
if ($feriado) {
$feriado->delete();
}
}
public function guardarTurno($inicio, $correo = null, $nombrecompleto = null, $descripcion = null, $clienteId = null, $estadoturnoId = null, $profesionalId = null, $servicioId = null, $modalidadId = null, $agendaId = null)
{
$inicioNormalizado = $this->normalizarDatetime($inicio, 'inicio');
$turno = Turno::create([
'agenda_id' => $agendaId ?? $this->id,
'correo' => $correo,
'nombrecompleto' => $nombrecompleto,
'descripcion' => $descripcion,
'cliente_id' => $clienteId,
'estadoturno_id' => $estadoturnoId,
'profesional_id' => $profesionalId,
'servicio_id' => $servicioId,
'modalidad_id' => $modalidadId,
'inicio' => $inicioNormalizado,
]);
return $turno;
}
private function normalizarHora($valor, $campo)
{
if ($valor instanceof \DateTimeInterface) {
return $valor->format('H:i:s');
}
$timestamp = strtotime(trim((string) $valor));
if ($timestamp === false) {
throw new \InvalidArgumentException('El campo ' . $campo . ' debe ser una hora valida.');
}
return date('H:i:s', $timestamp);
}
private function normalizarDatetime($valor, $campo)
{
if ($valor instanceof \DateTimeInterface) {
return $valor->format('Y-m-d H:i:s');
}
$timestamp = strtotime(trim((string) $valor));
if ($timestamp === false) {
throw new \InvalidArgumentException('El campo ' . $campo . ' debe ser una fecha y hora valida.');
}
return date('Y-m-d H:i:s', $timestamp);
}
public function confirmarTurno($id)
{
$turno = $this->turno()->find($id);
if (!$turno) {
return null;
}
return $turno->confirmar();
}
public function cancelarTurno($id)
{
$turno = $this->turno()->find($id);
if (!$turno) {
return null;
}
return $turno->cancelar();
}
public function reprogramarTurno($id)
{
$turno = $this->turno()->find($id);
if (!$turno) {
return null;
}
return $turno->reprogramar();
}
public function marcarClienteAusente($id)
{
$turno = $this->turno()->find($id);
if (!$turno) {
return null;
}
return $turno->clienteAusente();
}
public function marcarClientePresente($id)
{
$turno = $this->turno()->find($id);
if (!$turno) {
return null;
}
return $turno->clientePresente();
}
}