Add citas module: scheduling, calendar, blocked schedules
This commit is contained in:
212
app/Http/Controllers/Admin/CitaController.php
Normal file
212
app/Http/Controllers/Admin/CitaController.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\CitaRequest;
|
||||
use App\Models\Cita;
|
||||
use App\Models\HorarioBloqueado;
|
||||
use App\Models\Mensaje;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class CitaController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar lista de citas
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = Cita::with('mensaje')->orderBy('fecha', 'desc')->orderBy('hora_inicio', 'desc');
|
||||
|
||||
// Filtros
|
||||
if ($request->filled('estado')) {
|
||||
$query->where('estado', $request->estado);
|
||||
}
|
||||
|
||||
if ($request->filled('fecha')) {
|
||||
$query->whereDate('fecha', $request->fecha);
|
||||
}
|
||||
|
||||
if ($request->filled('fecha_inicio') && $request->filled('fecha_fin')) {
|
||||
$query->whereBetween('fecha', [$request->fecha_inicio, $request->fecha_fin]);
|
||||
}
|
||||
|
||||
if ($request->filled('buscar')) {
|
||||
$buscar = $request->buscar;
|
||||
$query->where(function ($q) use ($buscar) {
|
||||
$q->where('nombre_cliente', 'like', "%{$buscar}%")
|
||||
->orWhere('email_cliente', 'like', "%{$buscar}%")
|
||||
->orWhere('telefono_cliente', 'like', "%{$buscar}%")
|
||||
->orWhere('servicio', 'like', "%{$buscar}%");
|
||||
});
|
||||
}
|
||||
|
||||
$citas = $query->paginate(15)->appends($request->query());
|
||||
|
||||
return view('admin.citas.index', compact('citas'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario de creación
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
$mensajes = Mensaje::orderBy('created_at', 'desc')->get();
|
||||
|
||||
return view('admin.citas.create', compact('mensajes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardar nueva cita
|
||||
*/
|
||||
public function store(CitaRequest $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
// Calcular hora_fin basada en hora_inicio y duracion
|
||||
$horaInicio = Carbon::parse($data['hora_inicio']);
|
||||
$horaFin = $horaInicio->addMinutes($data['duracion']);
|
||||
$data['hora_fin'] = $horaFin->format('H:i:s');
|
||||
|
||||
// Verificar disponibilidad antes de crear
|
||||
$conflicto = Cita::whereDate('fecha', $data['fecha'])
|
||||
->where(function ($query) use ($data) {
|
||||
$query->where(function ($q) use ($data) {
|
||||
$q->whereTime('hora_inicio', '<', $data['hora_fin'])
|
||||
->whereTime('hora_fin', '>', $data['hora_inicio']);
|
||||
});
|
||||
})
|
||||
->whereIn('estado', ['pendiente', 'confirmada'])
|
||||
->exists();
|
||||
|
||||
if ($conflicto) {
|
||||
return back()->withErrors(['hora_inicio' => 'Ya existe una cita programada en este horario.'])->withInput();
|
||||
}
|
||||
|
||||
// Verificar si el horario está bloqueado
|
||||
if (HorarioBloqueado::estaBloqueado($data['fecha'], $data['hora_inicio'])) {
|
||||
return back()->withErrors(['hora_inicio' => 'El horario está bloqueado.'])->withInput();
|
||||
}
|
||||
|
||||
Cita::create($data);
|
||||
|
||||
return redirect()->route('admin.citas.index')->with('success', 'Cita creada correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar detalles de una cita
|
||||
*/
|
||||
public function show(Cita $cita): View
|
||||
{
|
||||
$cita->load('mensaje');
|
||||
|
||||
return view('admin.citas.show', compact('cita'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario de edición
|
||||
*/
|
||||
public function edit(Cita $cita): View
|
||||
{
|
||||
$mensajes = Mensaje::orderBy('created_at', 'desc')->get();
|
||||
|
||||
return view('admin.citas.edit', compact('cita', 'mensajes'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar cita
|
||||
*/
|
||||
public function update(CitaRequest $request, Cita $cita): RedirectResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
// Calcular hora_fin si cambió hora_inicio o duracion
|
||||
if ($request->has('hora_inicio') || $request->has('duracion')) {
|
||||
$horaInicio = Carbon::parse($data['hora_inicio'] ?? $cita->hora_inicio);
|
||||
$duracion = $data['duracion'] ?? $cita->duracion;
|
||||
$horaFin = $horaInicio->addMinutes($duracion);
|
||||
$data['hora_fin'] = $horaFin->format('H:i:s');
|
||||
}
|
||||
|
||||
// Verificar conflicto si se cambia fecha u hora
|
||||
$conflicto = Cita::where('id', '!=', $cita->id)
|
||||
->whereDate('fecha', $data['fecha'])
|
||||
->where(function ($query) use ($data) {
|
||||
$query->where(function ($q) use ($data) {
|
||||
$q->whereTime('hora_inicio', '<', $data['hora_fin'])
|
||||
->whereTime('hora_fin', '>', $data['hora_inicio']);
|
||||
});
|
||||
})
|
||||
->whereIn('estado', ['pendiente', 'confirmada'])
|
||||
->exists();
|
||||
|
||||
if ($conflicto) {
|
||||
return back()->withErrors(['hora_inicio' => 'Ya existe una cita programada en este horario.'])->withInput();
|
||||
}
|
||||
|
||||
$cita->update($data);
|
||||
|
||||
return redirect()->route('admin.citas.index')->with('success', 'Cita actualizada correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar cita
|
||||
*/
|
||||
public function destroy(Cita $cita): RedirectResponse
|
||||
{
|
||||
$cita->delete();
|
||||
|
||||
return redirect()->route('admin.citas.index')->with('success', 'Cita eliminada correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cambiar estado de la cita
|
||||
*/
|
||||
public function cambiarEstado(Request $request, Cita $cita): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'estado' => ['required', 'in:pendiente,confirmada,completada,cancelada'],
|
||||
]);
|
||||
|
||||
$cita->update(['estado' => $request->estado]);
|
||||
|
||||
return back()->with('success', 'Estado de la cita actualizado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar calendario de citas
|
||||
*/
|
||||
public function calendario(Request $request): View
|
||||
{
|
||||
$fecha = $request->filled('fecha') ? Carbon::parse($request->fecha) : Carbon::now();
|
||||
|
||||
$citas = Cita::with('mensaje')
|
||||
->whereDate('fecha', $fecha)
|
||||
->whereIn('estado', ['pendiente', 'confirmada'])
|
||||
->orderBy('hora_inicio')
|
||||
->get();
|
||||
|
||||
return view('admin.citas.calendario', compact('citas', 'fecha'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener citas para una fecha específica (API)
|
||||
*/
|
||||
public function porFecha(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'fecha' => ['required', 'date'],
|
||||
]);
|
||||
|
||||
$citas = Cita::whereDate('fecha', $request->fecha)
|
||||
->whereIn('estado', ['pendiente', 'confirmada'])
|
||||
->orderBy('hora_inicio')
|
||||
->get();
|
||||
|
||||
return response()->json($citas);
|
||||
}
|
||||
}
|
||||
210
app/Http/Controllers/Admin/HorarioBloqueadoController.php
Normal file
210
app/Http/Controllers/Admin/HorarioBloqueadoController.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\HorarioBloqueadoRequest;
|
||||
use App\Models\HorarioBloqueado;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HorarioBloqueadoController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mostrar lista de horarios bloqueados
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = HorarioBloqueado::orderBy('fecha', 'desc')->orderBy('hora_inicio', 'desc');
|
||||
|
||||
// Filtros
|
||||
if ($request->filled('estado')) {
|
||||
if ($request->estado === 'activos') {
|
||||
$query->where('activo', true);
|
||||
} elseif ($request->estado === 'inactivos') {
|
||||
$query->where('activo', false);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->filled('fecha')) {
|
||||
$query->whereDate('fecha', $request->fecha);
|
||||
}
|
||||
|
||||
if ($request->filled('fecha_inicio') && $request->filled('fecha_fin')) {
|
||||
$query->whereBetween('fecha', [$request->fecha_inicio, $request->fecha_fin]);
|
||||
}
|
||||
|
||||
$horarios = $query->paginate(15)->appends($request->query());
|
||||
|
||||
return view('admin.horarios-bloqueados.index', compact('horarios'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario de creación
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('admin.horarios-bloqueados.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Guardar nuevo horario bloqueado
|
||||
*/
|
||||
public function store(HorarioBloqueadoRequest $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
// Por defecto activo si no se especifica
|
||||
if (! isset($data['activo'])) {
|
||||
$data['activo'] = true;
|
||||
}
|
||||
|
||||
// Verificar superposición con otros bloques
|
||||
$superpuesto = HorarioBloqueado::whereDate('fecha', $data['fecha'])
|
||||
->where(function ($query) use ($data) {
|
||||
$query->where(function ($q) use ($data) {
|
||||
$q->whereTime('hora_inicio', '<', $data['hora_fin'])
|
||||
->whereTime('hora_fin', '>', $data['hora_inicio']);
|
||||
});
|
||||
})
|
||||
->exists();
|
||||
|
||||
if ($superpuesto) {
|
||||
return back()->withErrors(['hora_inicio' => 'Ya existe un horario bloqueado en este rango.'])->withInput();
|
||||
}
|
||||
|
||||
HorarioBloqueado::create($data);
|
||||
|
||||
return redirect()->route('admin.horarios-bloqueados.index')->with('success', 'Horario bloqueado creado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar detalles de un horario bloqueado
|
||||
*/
|
||||
public function show(HorarioBloqueado $horario_bloqueado): View
|
||||
{
|
||||
return view('admin.horarios-bloqueados.show', compact('horario_bloqueado'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar formulario de edición
|
||||
*/
|
||||
public function edit(HorarioBloqueado $horario_bloqueado): View
|
||||
{
|
||||
return view('admin.horarios-bloqueados.edit', compact('horario_bloqueado'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar horario bloqueado
|
||||
*/
|
||||
public function update(HorarioBloqueadoRequest $request, HorarioBloqueado $horario_bloqueado): RedirectResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
// Verificar superposición con otros bloques (excluyendo el actual)
|
||||
$superpuesto = HorarioBloqueado::where('id', '!=', $horario_bloqueado->id)
|
||||
->whereDate('fecha', $data['fecha'])
|
||||
->where(function ($query) use ($data) {
|
||||
$query->where(function ($q) use ($data) {
|
||||
$q->whereTime('hora_inicio', '<', $data['hora_fin'])
|
||||
->whereTime('hora_fin', '>', $data['hora_inicio']);
|
||||
});
|
||||
})
|
||||
->exists();
|
||||
|
||||
if ($superpuesto) {
|
||||
return back()->withErrors(['hora_inicio' => 'Ya existe un horario bloqueado en este rango.'])->withInput();
|
||||
}
|
||||
|
||||
$horario_bloqueado->update($data);
|
||||
|
||||
return redirect()->route('admin.horarios-bloqueados.index')->with('success', 'Horario bloqueado actualizado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Eliminar horario bloqueado
|
||||
*/
|
||||
public function destroy(HorarioBloqueado $horario_bloqueado): RedirectResponse
|
||||
{
|
||||
$horario_bloqueado->delete();
|
||||
|
||||
return redirect()->route('admin.horarios-bloqueados.index')->with('success', 'Horario bloqueado eliminado correctamente.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Activar horario bloqueado
|
||||
*/
|
||||
public function activar(HorarioBloqueado $horario_bloqueado): RedirectResponse
|
||||
{
|
||||
$horario_bloqueado->activar();
|
||||
|
||||
return back()->with('success', 'Horario bloqueado activado.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Desactivar horario bloqueado
|
||||
*/
|
||||
public function desactivar(HorarioBloqueado $horario_bloqueado): RedirectResponse
|
||||
{
|
||||
$horario_bloqueado->desactivar();
|
||||
|
||||
return back()->with('success', 'Horario bloqueado desactivado.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si una fecha y hora está bloqueada (API)
|
||||
*/
|
||||
public function verificar(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'fecha' => ['required', 'date'],
|
||||
'hora' => ['required', 'date_format:H:i'],
|
||||
]);
|
||||
|
||||
$estaBloqueado = HorarioBloqueado::estaBloqueado($request->fecha, $request->hora);
|
||||
|
||||
return response()->json([
|
||||
'bloqueado' => $estaBloqueado,
|
||||
'mensaje' => $estaBloqueado ? 'El horario está bloqueado.' : 'El horario está disponible.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener bloques para una fecha específica
|
||||
*/
|
||||
public function porFecha(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'fecha' => ['required', 'date'],
|
||||
]);
|
||||
|
||||
$bloques = HorarioBloqueado::getBloquesPorFecha($request->fecha);
|
||||
|
||||
return response()->json($bloques);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear bloqueo rápido desde el calendario
|
||||
*/
|
||||
public function bloqueoRapido(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'fecha' => ['required', 'date'],
|
||||
'hora_inicio' => ['required', 'date_format:H:i'],
|
||||
'hora_fin' => ['required', 'date_format:H:i', 'after:hora_inicio'],
|
||||
'motivo' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
HorarioBloqueado::create([
|
||||
'fecha' => $request->fecha,
|
||||
'hora_inicio' => $request->hora_inicio,
|
||||
'hora_fin' => $request->hora_fin,
|
||||
'motivo' => $request->motivo ?? 'Bloqueo rápido',
|
||||
'activo' => true,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Horario bloqueado correctamente.');
|
||||
}
|
||||
}
|
||||
41
app/Http/Requests/CitaRequest.php
Normal file
41
app/Http/Requests/CitaRequest.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class CitaRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$citaId = $this->route('cita')?->id;
|
||||
|
||||
return [
|
||||
'mensaje_id' => ['nullable', 'exists:mensajes,id'],
|
||||
'nombre_cliente' => ['required', 'string', 'max:255'],
|
||||
'email_cliente' => ['required', 'email', 'max:255'],
|
||||
'telefono_cliente' => ['required', 'string', 'max:50'],
|
||||
'servicio' => ['required', 'string', 'max:255'],
|
||||
'fecha' => ['required', 'date', 'after_or_equal:today'],
|
||||
'hora_inicio' => ['required', 'date_format:H:i'],
|
||||
'hora_fin' => ['nullable', 'date_format:H:i'],
|
||||
'duracion' => ['required', 'integer', 'min:15', 'max:480'],
|
||||
'estado' => ['required', Rule::in(['pendiente', 'confirmada', 'completada', 'cancelada'])],
|
||||
'notas' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'fecha.after_or_equal' => 'La fecha debe ser hoy o posterior.',
|
||||
'estado.in' => 'El estado debe ser: pendiente, confirmada, completada o cancelada.',
|
||||
];
|
||||
}
|
||||
}
|
||||
31
app/Http/Requests/HorarioBloqueadoRequest.php
Normal file
31
app/Http/Requests/HorarioBloqueadoRequest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class HorarioBloqueadoRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'fecha' => ['required', 'date'],
|
||||
'hora_inicio' => ['required', 'date_format:H:i'],
|
||||
'hora_fin' => ['required', 'date_format:H:i', 'after:hora_inicio'],
|
||||
'motivo' => ['nullable', 'string', 'max:255'],
|
||||
'activo' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'hora_fin.after' => 'La hora de fin debe ser posterior a la hora de inicio.',
|
||||
];
|
||||
}
|
||||
}
|
||||
176
app/Models/Cita.php
Normal file
176
app/Models/Cita.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Cita extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'citas';
|
||||
|
||||
protected $fillable = [
|
||||
'mensaje_id',
|
||||
'nombre_cliente',
|
||||
'email_cliente',
|
||||
'telefono_cliente',
|
||||
'servicio',
|
||||
'fecha',
|
||||
'hora_inicio',
|
||||
'hora_fin',
|
||||
'duracion',
|
||||
'estado',
|
||||
'notas',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'fecha' => 'date',
|
||||
'hora_inicio' => 'datetime:H:i',
|
||||
'hora_fin' => 'datetime:H:i',
|
||||
'duracion' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Relación con Mensaje
|
||||
*/
|
||||
public function mensaje(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Mensaje::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para filtrar citas pendientes
|
||||
*/
|
||||
public function scopePendiente(Builder $query): Builder
|
||||
{
|
||||
return $query->where('estado', 'pendiente');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para filtrar citas confirmadas
|
||||
*/
|
||||
public function scopeConfirmada(Builder $query): Builder
|
||||
{
|
||||
return $query->where('estado', 'confirmada');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para filtrar citas completadas
|
||||
*/
|
||||
public function scopeCompletada(Builder $query): Builder
|
||||
{
|
||||
return $query->where('estado', 'completada');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para filtrar citas canceladas
|
||||
*/
|
||||
public function scopeCancelada(Builder $query): Builder
|
||||
{
|
||||
return $query->where('estado', 'cancelada');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para filtrar citas por fecha específica
|
||||
*/
|
||||
public function scopePorFecha(Builder $query, $fecha): Builder
|
||||
{
|
||||
return $query->whereDate('fecha', $fecha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para filtrar citas entre dos fechas
|
||||
*/
|
||||
public function scopeEntreFechas(Builder $query, $inicio, $fin): Builder
|
||||
{
|
||||
return $query->whereBetween('fecha', [$inicio, $fin]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular hora_fin basado en hora_inicio + duracion
|
||||
*/
|
||||
public function calcularHoraFin(): string
|
||||
{
|
||||
$horaInicio = Carbon::parse($this->hora_inicio);
|
||||
$horaFin = $horaInicio->addMinutes($this->duracion);
|
||||
|
||||
$this->hora_fin = $horaFin->format('H:i:s');
|
||||
|
||||
return $this->hora_fin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener estado formateado para mostrar
|
||||
*/
|
||||
public function getEstadoFormateadoAttribute(): string
|
||||
{
|
||||
$estados = [
|
||||
'pendiente' => 'Pendiente',
|
||||
'confirmada' => 'Confirmada',
|
||||
'completada' => 'Completada',
|
||||
'cancelada' => 'Cancelada',
|
||||
];
|
||||
|
||||
return $estados[$this->estado] ?? $this->estado;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener hora de inicio formateada
|
||||
*/
|
||||
public function getHoraInicioFormatAttribute(): string
|
||||
{
|
||||
return Carbon::parse($this->hora_inicio)->format('H:i');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener hora de fin formateada
|
||||
*/
|
||||
public function getHoraFinFormatAttribute(): string
|
||||
{
|
||||
return Carbon::parse($this->hora_fin)->format('H:i');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener duración formateada (horas y minutos)
|
||||
*/
|
||||
public function getDuracionFormatAttribute(): string
|
||||
{
|
||||
$horas = floor($this->duracion / 60);
|
||||
$minutos = $this->duracion % 60;
|
||||
|
||||
if ($horas > 0 && $minutos > 0) {
|
||||
return "{$horas}h {$minutos}min";
|
||||
} elseif ($horas > 0) {
|
||||
return "{$horas}h";
|
||||
} else {
|
||||
return "{$minutos}min";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si la cita está activa (no cancelada ni completada)
|
||||
*/
|
||||
public function isActiva(): bool
|
||||
{
|
||||
return in_array($this->estado, ['pendiente', 'confirmada']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cambiar estado de la cita
|
||||
*/
|
||||
public function cambiarEstado(string $nuevoEstado): bool
|
||||
{
|
||||
$estadosValidos = ['pendiente', 'confirmada', 'completada', 'cancelada'];
|
||||
|
||||
if (! in_array($nuevoEstado, $estadosValidos)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->update(['estado' => $nuevoEstado]);
|
||||
}
|
||||
}
|
||||
119
app/Models/HorarioBloqueado.php
Normal file
119
app/Models/HorarioBloqueado.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class HorarioBloqueado extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'horarios_bloqueados';
|
||||
|
||||
protected $fillable = [
|
||||
'fecha',
|
||||
'hora_inicio',
|
||||
'hora_fin',
|
||||
'motivo',
|
||||
'activo',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'fecha' => 'date',
|
||||
'activo' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Scope para filtrar horarios activos
|
||||
*/
|
||||
public function scopeActivo(Builder $query): Builder
|
||||
{
|
||||
return $query->where('activo', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para filtrar horarios por fecha
|
||||
*/
|
||||
public function scopePorFecha(Builder $query, $fecha): Builder
|
||||
{
|
||||
return $query->whereDate('fecha', $fecha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si una fecha y hora está bloqueada
|
||||
*/
|
||||
public static function estaBloqueado($fecha, $hora): bool
|
||||
{
|
||||
$horaCarbon = Carbon::parse($hora)->format('H:i:s');
|
||||
|
||||
return self::whereDate('fecha', $fecha)
|
||||
->where('activo', true)
|
||||
->whereTime('hora_inicio', '<=', $horaCarbon)
|
||||
->whereTime('hora_fin', '>', $horaCarbon)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si una fecha tiene bloques activos
|
||||
*/
|
||||
public static function tieneBloques($fecha): bool
|
||||
{
|
||||
return self::whereDate('fecha', $fecha)
|
||||
->where('activo', true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener los bloques activos para una fecha
|
||||
*/
|
||||
public static function getBloquesPorFecha($fecha)
|
||||
{
|
||||
return self::whereDate('fecha', $fecha)
|
||||
->where('activo', true)
|
||||
->orderBy('hora_inicio')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Activar el horario bloqueado
|
||||
*/
|
||||
public function activar(): bool
|
||||
{
|
||||
return $this->update(['activo' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Desactivar el horario bloqueado
|
||||
*/
|
||||
public function desactivar(): bool
|
||||
{
|
||||
return $this->update(['activo' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener hora de inicio formateada
|
||||
*/
|
||||
public function getHoraInicioFormatAttribute(): string
|
||||
{
|
||||
return Carbon::parse($this->hora_inicio)->format('H:i');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener hora de fin formateada
|
||||
*/
|
||||
public function getHoraFinFormatAttribute(): string
|
||||
{
|
||||
return Carbon::parse($this->hora_fin)->format('H:i');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener rango de horario formateado
|
||||
*/
|
||||
public function getRangoHorarioAttribute(): string
|
||||
{
|
||||
return "{$this->hora_inicio_format} - {$this->hora_fin_format}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user