550 lines
19 KiB
PHP
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();
|
|
}
|
|
|
|
}
|