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 40 minutos. $duracionTurno = (int) (self::where('id', $agendaId)->value('duracionturno') ?? 40); if ($duracionTurno <= 0) { $duracionTurno = 40; } // 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(); } }