Add citas module: scheduling, calendar, blocked schedules
This commit is contained in:
472
PLAN_AGENDAMIENTO_CITAS.md
Normal file
472
PLAN_AGENDAMIENTO_CITAS.md
Normal file
@@ -0,0 +1,472 @@
|
||||
# Plan Técnico: Módulo de Agendamiento de Citas - Lash Vanshy
|
||||
|
||||
## 1. Estructura de Base de Datos
|
||||
|
||||
### 1.1 Tabla: citas
|
||||
|
||||
```php
|
||||
// database/migrations/2024_01_01_000008_create_citas_table.php
|
||||
|
||||
Schema::create('citas', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('mensaje_id')->nullable()->constrained('mensajes')->onDelete('set null');
|
||||
$table->string('cliente_nombre');
|
||||
$table->string('cliente_email');
|
||||
$table->string('cliente_telefono', 50)->nullable();
|
||||
$table->string('servicio')->default('Lash Extensions');
|
||||
$table->date('fecha');
|
||||
$table->time('hora_inicio');
|
||||
$table->time('hora_fin');
|
||||
$table->enum('estado', ['pendiente', 'confirmada', 'completada', 'cancelada'])->default('pendiente');
|
||||
$table->text('notas')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
|
||||
### 1.2 Tabla: horarios_bloqueados
|
||||
|
||||
```php
|
||||
// database/migrations/2024_01_01_000009_create_horarios_bloqueados_table.php
|
||||
|
||||
Schema::create('horarios_bloqueados', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->date('fecha');
|
||||
$table->time('hora_inicio');
|
||||
$table->time('hora_fin');
|
||||
$table->string('motivo')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
```
|
||||
|
||||
## 2. Modelos
|
||||
|
||||
### 2.1 App/Models/Cita.php
|
||||
|
||||
```php
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Cita extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'mensaje_id',
|
||||
'cliente_nombre',
|
||||
'cliente_email',
|
||||
'cliente_telefono',
|
||||
'servicio',
|
||||
'fecha',
|
||||
'hora_inicio',
|
||||
'hora_fin',
|
||||
'estado',
|
||||
'notas',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'fecha' => 'date',
|
||||
'hora_inicio' => 'datetime:H:i',
|
||||
'hora_fin' => 'datetime:H:i',
|
||||
];
|
||||
|
||||
public function mensaje(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Mensaje::class);
|
||||
}
|
||||
|
||||
public function scopePendientes($query) { /* ... */ }
|
||||
public function scopeConfirmadas($query) { /* ... */ }
|
||||
public function scopePorFecha($query, $fecha) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 App/Models/HorarioBloqueado.php
|
||||
|
||||
```php
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class HorarioBloqueado extends Model
|
||||
{
|
||||
protected $fillable = ['fecha', 'hora_inicio', 'hora_fin', 'motivo'];
|
||||
|
||||
protected $casts = ['fecha' => 'date'];
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Backend - Controladores y API
|
||||
|
||||
### 3.1 App/Http/Controllers/Admin/CitaController.php
|
||||
|
||||
```php
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Citas;
|
||||
use App\Models\HorarioBloqueado;
|
||||
use Illuminate\Http\Request;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class CitaController extends Controller
|
||||
{
|
||||
// --- CRUD BÁSICO ---
|
||||
public function index(Request $request)
|
||||
{
|
||||
$citas = Cita::with('mensaje')
|
||||
->when($request->fecha, fn($q) => $q->whereDate('fecha', $request->fecha))
|
||||
->when($request->estado, fn($q) => $q->where('estado', $request->estado))
|
||||
->orderBy('fecha')->orderBy('hora_inicio')
|
||||
->paginate(20);
|
||||
|
||||
return view('admin.citas.index', compact('citas'));
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$mensajeId = $request->mensaje_id;
|
||||
$mensaje = $mensajeId ? Mensaje::find($mensajeId) : null;
|
||||
$fechasBloqueadas = HorarioBloqueado::pluck('fecha')->toArray();
|
||||
|
||||
return view('admin.citas.create', compact('mensaje', 'fechasBloqueadas'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'cliente_nombre' => 'required',
|
||||
'cliente_email' => 'required|email',
|
||||
'cliente_telefono' => 'nullable',
|
||||
'fecha' => 'required|date|after_or_equal:today',
|
||||
'hora_inicio' => 'required',
|
||||
'notas' => 'nullable',
|
||||
]);
|
||||
|
||||
// Calcular hora fin (60 minutos)
|
||||
$horaFin = Carbon::parse($validated['hora_inicio'])->addMinutes(60)->format('H:i:s');
|
||||
|
||||
// Verificar disponibilidad
|
||||
if (!self::verificarDisponibilidad($validated['fecha'], $validated['hora_inicio'], $horaFin)) {
|
||||
return back()->with('error', 'El horario no está disponible.');
|
||||
}
|
||||
|
||||
Cita::create([
|
||||
...$validated,
|
||||
'mensaje_id' => $request->mensaje_id,
|
||||
'hora_fin' => $horaFin,
|
||||
'servicio' => 'Lash Extensions',
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.citas.index')->with('success', 'Cita agendada correctamente.');
|
||||
}
|
||||
|
||||
public function edit(Cita $cita) { /* ... */ }
|
||||
|
||||
public function update(Request $request, Cita $cita) { /* ... */ }
|
||||
|
||||
public function destroy(Cita $cita)
|
||||
{
|
||||
$cita->delete();
|
||||
return back()->with('success', 'Cita eliminada.');
|
||||
}
|
||||
|
||||
// --- API DE DISPONIBILIDAD ---
|
||||
public function getHorariosDisponibles(Request $request)
|
||||
{
|
||||
$fecha = $request->fecha;
|
||||
$citas = Cita::whereDate('fecha', $fecha)->where('estado', '!=', 'cancelada')->get();
|
||||
$bloqueados = HorarioBloqueado::whereDate('fecha', $fecha)->get();
|
||||
|
||||
$horarios = self::generarHorariosDisponibles($citas, $bloqueados);
|
||||
|
||||
return response()->json($horarios);
|
||||
}
|
||||
|
||||
public function getCitasMes(Request $request)
|
||||
{
|
||||
$inicio = Carbon::parse($request->fecha)->startOfMonth();
|
||||
$fin = Carbon::parse($request->fecha)->endOfMonth();
|
||||
|
||||
$citas = Cita::whereBetween('fecha', [$inicio, $fin])->get();
|
||||
|
||||
return response()->json($citas);
|
||||
}
|
||||
|
||||
// --- HELPER: Verificar disponibilidad ---
|
||||
private function verificarDisponibilidad($fecha, $horaInicio, $horaFin)
|
||||
{
|
||||
// Verificar citas existentes
|
||||
$conflicto = Cita::whereDate('fecha', $fecha)
|
||||
->where('estado', '!=', 'cancelada')
|
||||
->where(function ($q) use ($horaInicio, $horaFin) {
|
||||
$q->whereBetween('hora_inicio', [$horaInicio, $horaFin])
|
||||
->orWhereBetween('hora_fin', [$horaInicio, $horaFin])
|
||||
->orWhere(function ($q2) use ($horaInicio, $horaFin) {
|
||||
$q2->where('hora_inicio', '<=', $horaInicio)->where('hora_fin', '>=', $horaFin);
|
||||
});
|
||||
})->exists();
|
||||
|
||||
if ($conflicto) return false;
|
||||
|
||||
// Verificar horarios bloqueados
|
||||
$bloqueado = HorarioBloqueado::whereDate('fecha', $fecha)
|
||||
->where(function ($q) use ($horaInicio, $horaFin) {
|
||||
$q->whereBetween('hora_inicio', [$horaInicio, $horaFin])
|
||||
->orWhereBetween('hora_fin', [$horaInicio, $horaFin]);
|
||||
})->exists();
|
||||
|
||||
return !$bloqueado;
|
||||
}
|
||||
|
||||
// --- HELPER: Generar array de horarios disponibles ---
|
||||
private function generarHorariosDisponibles($citas, $bloqueados)
|
||||
{
|
||||
$horarios = [];
|
||||
$inicio = 9; // 9:00 AM
|
||||
$fin = 18; // 7:00 PM (último inicio a las 6pm para 60min)
|
||||
|
||||
for ($hora = $inicio; $hora < $fin; $hora++) {
|
||||
$horaStr = str_pad($hora, 2, '0', STR_PAD_LEFT) . ':00:00';
|
||||
$horaFinStr = Carbon::parse($horaStr)->addMinutes(60)->format('H:i:s');
|
||||
|
||||
// Verificar si está libre
|
||||
$libre = true;
|
||||
|
||||
foreach ($citas as $cita) {
|
||||
if ($this->horarioConflicta($horaStr, $horaFinStr, $cita->hora_inicio, $cita->hora_fin)) {
|
||||
$libre = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($libre) {
|
||||
foreach ($bloqueados as $bloqueado) {
|
||||
if ($this->horarioConflicta($horaStr, $horaFinStr, $bloqueado->hora_inicio, $bloqueado->hora_fin)) {
|
||||
$libre = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($libre) {
|
||||
$horarios[] = ['hora' => $horaStr, 'label' => Carbon::parse($horaStr)->format('h:i A')];
|
||||
}
|
||||
}
|
||||
|
||||
return $horarios;
|
||||
}
|
||||
|
||||
private function horarioConflicta($inicio, $fin, $existenteInicio, $existenteFin)
|
||||
{
|
||||
return $inicio < $existenteFin && $fin > $existenteInicio;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 App/Http/Controllers/Admin/HorarioBloqueadoController.php
|
||||
|
||||
```php
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
class HorarioBloqueadoController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$bloqueados = HorarioBloqueado::orderBy('fecha')->paginate(15);
|
||||
return view('admin.horarios-bloqueados.index', compact('bloqueados'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'fecha' => 'required|date',
|
||||
'hora_inicio' => 'required',
|
||||
'hora_fin' => 'required',
|
||||
'motivo' => 'nullable',
|
||||
]);
|
||||
|
||||
HorarioBloqueado::create($request->all());
|
||||
return back()->with('success', 'Horario bloqueado.');
|
||||
}
|
||||
|
||||
public function destroy(HorarioBloqueado $bloqueado)
|
||||
{
|
||||
$bloqueado->delete();
|
||||
return back()->with('success', 'Bloqueo eliminado.');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Actualizar MensajeController para integrar botón de agendar
|
||||
|
||||
```php
|
||||
// En show() del MensajeController, pasar flag para mostrar botón
|
||||
public function show(Mensaje $mensaje): View
|
||||
{
|
||||
if (!$mensaje->leido) {
|
||||
$mensaje->marcarLeido();
|
||||
}
|
||||
|
||||
$yaTieneCita = Cita::where('mensaje_id', $mensaje->id)->exists();
|
||||
|
||||
return view('admin.mensajes.show', compact('mensaje', 'yaTieneCita'));
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Rutas
|
||||
|
||||
### routes/admin.php
|
||||
|
||||
```php
|
||||
// Citas
|
||||
Route::prefix('citas')->name('admin.citas.')->group(function () {
|
||||
Route::get('/', [CitaController::class, 'index'])->name('index');
|
||||
Route::get('/create', [CitaController::class, 'create'])->name('create');
|
||||
Route::get('/create/{mensaje_id}', [CitaController::class, 'create'])->name('create-from-mensaje');
|
||||
Route::post('/', [CitaController::class, 'store'])->name('store');
|
||||
Route::get('/{cita}/edit', [CitaController::class, 'edit'])->name('edit');
|
||||
Route::put('/{cita}', [CitaController::class, 'update'])->name('update');
|
||||
Route::delete('/{cita}', [CitaController::class, 'destroy'])->name('destroy');
|
||||
Route::patch('/{cita}/estado', [CitaController::class, 'cambiarEstado'])->name('estado');
|
||||
});
|
||||
|
||||
// API de disponibilidad
|
||||
Route::get('/citas/disponibles', [CitaController::class, 'getHorariosDisponibles'])->name('admin.citas.disponibles');
|
||||
Route::get('/citas/calendario', [CitaController::class, 'getCitasMes'])->name('admin.citas.calendario');
|
||||
|
||||
// Horarios bloqueados
|
||||
Route::prefix('horarios-bloqueados')->name('admin.horarios.')->group(function () {
|
||||
Route::get('/', [HorarioBloqueadoController::class, 'index'])->name('index');
|
||||
Route::post('/', [HorarioBloqueadoController::class, 'store'])->name('store');
|
||||
Route::delete('/{bloqueado}', [HorarioBloqueadoController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
```
|
||||
|
||||
## 5. Frontend - Vistas
|
||||
|
||||
### 5.1 admin/citas/index.blade.php
|
||||
|
||||
- Tabla con filtros: por fecha, por estado
|
||||
- Calendario mensual (vista)
|
||||
- Buttons: crear, editar, eliminar, cambiar estado
|
||||
|
||||
### 5.2 admin/citas/create.blade.php
|
||||
|
||||
```html
|
||||
<form method="POST">
|
||||
@csrf
|
||||
|
||||
<!-- Datos del cliente (prellenados si viene de mensaje) -->
|
||||
<input type="text" name="cliente_nombre" value="{{ $mensaje->nombre ?? old('cliente_nombre') }}">
|
||||
<input type="email" name="cliente_email" value="{{ $mensaje->email ?? old('cliente_email') }}">
|
||||
<input type="tel" name="cliente_telefono" value="{{ $mensaje->telefono ?? old('cliente_telefono') }}">
|
||||
|
||||
<!-- Selector de fecha con calendario -->
|
||||
<input type="date" name="fecha" id="fecha" required>
|
||||
|
||||
<!-- Selector de hora (se llena vía AJAX según disponibilidad) -->
|
||||
<select name="hora_inicio" id="hora_inicio" required>
|
||||
<option value="">Seleccione un horario</option>
|
||||
</select>
|
||||
|
||||
<textarea name="notas" placeholder="Notas adicionales"></textarea>
|
||||
|
||||
<button type="submit">Agendar Cita</button>
|
||||
</form>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.getElementById('fecha').addEventListener('change', function() {
|
||||
fetch(`/admin/citas/disponibles?fecha=${this.value}`)
|
||||
.then(r => r.json())
|
||||
.then(horarios => {
|
||||
const select = document.getElementById('hora_inicio');
|
||||
select.innerHTML = '<option value="">Seleccione un horario</option>';
|
||||
horarios.forEach(h => {
|
||||
select.innerHTML += `<option value="${h.hora}">${h.label}</option>`;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
```
|
||||
|
||||
### 5.3 admin/citas/calendario.blade.php (Página completa)
|
||||
|
||||
- Vista de mes con FullCalendar o tabla HTML
|
||||
- Clic en día muestra horarios disponibles
|
||||
- Arrastrar para crear cita (opcional)
|
||||
|
||||
### 5.4 admin/mensajes/show.blade.php (Actualizar)
|
||||
|
||||
```html
|
||||
<div class="actions">
|
||||
<a href="{{ route('admin.mensajes.index') }}" class="btn">← Volver</a>
|
||||
|
||||
@if(!$yaTieneCita)
|
||||
<a href="{{ route('admin.citas.create-from-mensaje', $mensaje->id) }}" class="btn btn-primary">
|
||||
📅 Agendar Cita
|
||||
</a>
|
||||
@else
|
||||
<span class="badge">✓ Ya tiene cita</span>
|
||||
@endif
|
||||
</div>
|
||||
```
|
||||
|
||||
### 5.5 admin/horarios-bloqueados/index.blade.php
|
||||
|
||||
- Formulario para bloquear horarios
|
||||
- Lista de bloqueos activos
|
||||
- Eliminar bloqueo
|
||||
|
||||
## 6. Configuración del Negocio (Constantes)
|
||||
|
||||
```php
|
||||
// app/Constants/Negocio.php
|
||||
namespace App\Constants;
|
||||
|
||||
class Negocio
|
||||
{
|
||||
const DURACION_CITA_MINUTOS = 60;
|
||||
const HORA_APERTURA = '09:00';
|
||||
const HORA_CIERRE = '19:00';
|
||||
const DIAS_ATENCION = [1, 2, 3, 4, 5, 6]; // Lunes-Sábado (Carbon: 1=Lunes)
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Flujo de Usuario
|
||||
|
||||
### Desde Mensaje:
|
||||
1. Admin recibe mensaje en admin/mensajes
|
||||
2. Visualiza el mensaje en admin/mensajes/{id}
|
||||
3. Ve botón "Agendar Cita" (si no tiene cita)
|
||||
4. Clic -> admin/citas/create/{mensaje_id}
|
||||
5. Formulario prellenado con datos del mensaje
|
||||
6. Selecciona fecha y horario disponible
|
||||
7. Confirma -> Cita creada -> Redirección a lista de citas
|
||||
|
||||
### Adminsitración Directa:
|
||||
1. Va a admin/citas
|
||||
2. Clic "Nueva Cita" o usa el calendario
|
||||
3. Completa formulario
|
||||
4. Confirma
|
||||
|
||||
## 8. Testing Checklist
|
||||
|
||||
- [ ] Crear cita exitosamente
|
||||
- [ ] Crear cita desde mensaje (datos prellenados)
|
||||
- [ ] Validar conflicto de horarios (dos citas mismo horario)
|
||||
- [ ] Validar horario bloqueado no permite cita
|
||||
- [ ] Cambiar estado de cita (pendiente -> confirmadas -> completada -> cancelada)
|
||||
- [ ] Eliminar cita
|
||||
- [ ] Bloquear horario y verificar no disponible
|
||||
- [ ] Ver calendario con citas del mes
|
||||
- [ ] Validar días no laborables (Domingo)
|
||||
- [ ] Validar horarios fuera de rango (antes 9am, después 7pm)
|
||||
|
||||
## 9. Notas Adicionales
|
||||
|
||||
- Usar SQLite existente (compatible)
|
||||
- Implementar validación de rango de horario en frontend y backend
|
||||
- Considerar agregar notificaciones por email al agendar (futuro)
|
||||
- Los horarios bloqueados deben poder ser recurrentes (opcional v2)
|
||||
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}";
|
||||
}
|
||||
}
|
||||
38
database/migrations/2024_01_01_000008_create_citas_table.php
Normal file
38
database/migrations/2024_01_01_000008_create_citas_table.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('citas', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('mensaje_id')->nullable();
|
||||
$table->string('nombre_cliente', 255);
|
||||
$table->string('email_cliente', 255);
|
||||
$table->string('telefono_cliente', 50);
|
||||
$table->string('servicio', 255);
|
||||
$table->date('fecha');
|
||||
$table->time('hora_inicio');
|
||||
$table->time('hora_fin');
|
||||
$table->integer('duracion')->default(60);
|
||||
$table->enum('estado', ['pendiente', 'confirmada', 'completada', 'cancelada'])->default('pendiente');
|
||||
$table->text('notas')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Foreign key to mensajes table
|
||||
$table->foreign('mensaje_id')
|
||||
->references('id')
|
||||
->on('mensajes')
|
||||
->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('citas');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('horarios_bloqueados', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->date('fecha');
|
||||
$table->time('hora_inicio');
|
||||
$table->time('hora_fin');
|
||||
$table->string('motivo', 255)->nullable();
|
||||
$table->boolean('activo')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('horarios_bloqueados');
|
||||
}
|
||||
};
|
||||
@@ -604,8 +604,8 @@ body {
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SEARCH & FILTERS
|
||||
============================================ */
|
||||
SEARCH & FILTERS
|
||||
============================================ */
|
||||
.search-box {
|
||||
position: relative;
|
||||
max-width: 300px;
|
||||
@@ -632,6 +632,42 @@ body {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
DROPDOWN MENU IN SIDEBAR
|
||||
============================================ */
|
||||
.sidebar-nav .dropdown-menu {
|
||||
position: static;
|
||||
background: white;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
box-shadow: 0 5px 20px var(--shadow);
|
||||
}
|
||||
|
||||
.sidebar-nav .dropdown-item {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-nav .dropdown-item:hover {
|
||||
background: rgba(248, 180, 196, 0.2);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.sidebar-nav .dropdown-item.active {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar-nav .dropdown-divider {
|
||||
margin: 0.5rem 0;
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
|
||||
478
resources/views/admin/citas/calendario.blade.php
Normal file
478
resources/views/admin/citas/calendario.blade.php
Normal file
@@ -0,0 +1,478 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Calendario de Citas - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Calendario de Citas')
|
||||
|
||||
@section('styles')
|
||||
<!-- FullCalendar CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/main.min.css" rel="stylesheet">
|
||||
<style>
|
||||
#calendar {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.fc {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.fc .fc-button {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fc .fc-button:hover {
|
||||
background: linear-gradient(135deg, var(--primary-dark), var(--primary));
|
||||
}
|
||||
|
||||
.fc .fc-button-primary:not(:disabled).fc-button-active {
|
||||
background: linear-gradient(135deg, var(--primary-dark), var(--primary));
|
||||
}
|
||||
|
||||
.fc .fc-toolbar-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.fc .fc-daygrid-day-number {
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.fc .fc-col-header-cell-cushion {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.fc-event {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fc-event:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Event colors by status */
|
||||
.fc-event.estado-pendiente {
|
||||
background: linear-gradient(135deg, #ffc107, #fd7e14);
|
||||
}
|
||||
|
||||
.fc-event.estado-confirmada {
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
}
|
||||
|
||||
.fc-event.estado-completada {
|
||||
background: linear-gradient(135deg, #17a2b8, var(--primary-dark));
|
||||
}
|
||||
|
||||
.fc-event.estado-cancelada {
|
||||
background: linear-gradient(135deg, #dc3545, #bd2130);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* Bloqueos */
|
||||
.fc-event.bloqueado {
|
||||
background: linear-gradient(135deg, #6c757d, #495057);
|
||||
border: 2px dashed #343a40;
|
||||
}
|
||||
|
||||
/* Today highlight */
|
||||
.fc .fc-day-today {
|
||||
background: rgba(248, 180, 196, 0.2) !important;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal-event {
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.citas.index') }}">Citas</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Calendario</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Actions Bar -->
|
||||
<div class="card-admin mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<a href="{{ route('admin.citas.create') }}" class="btn btn-primary-admin">
|
||||
<i class="fas fa-plus me-2"></i>Nueva Cita
|
||||
</a>
|
||||
<a href="{{ route('admin.citas.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-list me-2"></i>Ver Lista
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ route('admin.horarios.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-clock me-2"></i>Horarios Bloqueados
|
||||
</a>
|
||||
<a href="{{ route('admin.horarios.create') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-ban me-2"></i>Bloquear Horario
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="card-admin mb-4">
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex flex-wrap gap-4 align-items-center">
|
||||
<span class="text-muted fw-bold">Leyenda:</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="legend-color" style="background: linear-gradient(135deg, #ffc107, #fd7e14);"></span>
|
||||
<small>Pendiente</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="legend-color" style="background: linear-gradient(135deg, #28a745, #20c997);"></span>
|
||||
<small>Confirmada</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="legend-color" style="background: linear-gradient(135deg, #17a2b8, var(--primary-dark));"></span>
|
||||
<small>Completada</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="legend-color" style="background: linear-gradient(135deg, #dc3545, #bd2130);"></span>
|
||||
<small>Cancelada</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="legend-color" style="background: linear-gradient(135deg, #6c757d, #495057); border: 2px dashed #343a40;"></span>
|
||||
<small>Bloqueado</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.legend-color {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Calendar -->
|
||||
<div class="card-admin">
|
||||
<div class="card-body">
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Modal -->
|
||||
<div class="modal fade" id="eventModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-event">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="eventModalTitle">Detalles de la Cita</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="eventModalBody">
|
||||
<!-- Dynamic content -->
|
||||
</div>
|
||||
<div class="modal-footer" id="eventModalFooter">
|
||||
<button type="button" class="btn btn-secondary-admin" data-bs-dismiss="modal">Cerrar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<!-- FullCalendar JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/locales/es.min.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const calendarEl = document.getElementById('calendar');
|
||||
const eventModal = new bootstrap.Modal(document.getElementById('eventModal'));
|
||||
|
||||
// Get initial events
|
||||
fetchEvents().then(events => {
|
||||
initCalendar(events);
|
||||
});
|
||||
|
||||
async function fetchEvents() {
|
||||
const today = new Date();
|
||||
const start = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
||||
const end = new Date(today.getFullYear(), today.getMonth() + 2, 0);
|
||||
|
||||
const response = await fetch(
|
||||
`/admin/citas/por-fecha?inicio=${formatDate(start)}&fin=${formatDate(end)}`
|
||||
);
|
||||
const citas = await response.json();
|
||||
|
||||
const citasEvents = citas.map(cita => ({
|
||||
id: `cita-${cita.id}`,
|
||||
title: `${cita.cliente_nombre} - ${cita.servicio}`,
|
||||
start: `${cita.fecha}T${cita.hora_inicio}`,
|
||||
end: `${cita.fecha}T${cita.hora_fin}`,
|
||||
classNames: [`estado-${cita.estado}`],
|
||||
extendedProps: {
|
||||
type: 'cita',
|
||||
data: cita
|
||||
}
|
||||
}));
|
||||
|
||||
// Fetch bloqueos
|
||||
const bloqueosResponse = await fetch(
|
||||
`/admin/horarios/por-fecha?inicio=${formatDate(start)}&fin=${formatDate(end)}`
|
||||
);
|
||||
const bloqueos = await bloqueosResponse.json();
|
||||
|
||||
const bloqueosEvents = bloqueos.map(bloqueado => ({
|
||||
id: `bloqueado-${bloqueado.id}`,
|
||||
title: `Bloqueado: ${bloqueado.motivo || 'Sin motivo'}`,
|
||||
start: `${bloqueado.fecha}T${bloqueado.hora_inicio}`,
|
||||
end: `${bloqueado.fecha}T${bloqueado.hora_fin}`,
|
||||
classNames: ['bloqueado'],
|
||||
extendedProps: {
|
||||
type: 'bloqueado',
|
||||
data: bloqueado
|
||||
}
|
||||
}));
|
||||
|
||||
return [...citasEvents, ...bloqueosEvents];
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
function initCalendar(events) {
|
||||
const calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
locale: 'es',
|
||||
initialView: 'dayGridMonth',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,listWeek'
|
||||
},
|
||||
events: events,
|
||||
editable: false,
|
||||
selectable: true,
|
||||
selectMirror: true,
|
||||
dayMaxEvents: 3,
|
||||
weekends: true,
|
||||
nowIndicator: true,
|
||||
slotMinTime: '09:00:00',
|
||||
slotMaxTime: '19:00:00',
|
||||
allDaySlot: false,
|
||||
|
||||
// Click on event
|
||||
eventClick: function(info) {
|
||||
showEventModal(info.event);
|
||||
},
|
||||
|
||||
// Click on date
|
||||
dateClick: function(info) {
|
||||
window.location.href = `/admin/citas/create?fecha=${info.dateStr}`;
|
||||
},
|
||||
|
||||
// Dates rendered (fetch more events)
|
||||
datesSet: function(info) {
|
||||
refreshEvents(calendar, info.start, info.end);
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
}
|
||||
|
||||
async function refreshEvents(calendar, start, end) {
|
||||
const response = await fetch(
|
||||
`/admin/citas/por-fecha?inicio=${formatDate(start)}&fin=${formatDate(end)}`
|
||||
);
|
||||
const citas = await response.json();
|
||||
|
||||
const bloqueosResponse = await fetch(
|
||||
`/admin/horarios/por-fecha?inicio=${formatDate(start)}&fin=${formatDate(end)}`
|
||||
);
|
||||
const bloqueos = await bloqueosResponse.json();
|
||||
|
||||
// Remove old events
|
||||
calendar.getEvents().forEach(event => event.remove());
|
||||
|
||||
// Add new events
|
||||
citas.forEach(cita => {
|
||||
calendar.addEvent({
|
||||
id: `cita-${cita.id}`,
|
||||
title: `${cita.cliente_nombre} - ${cita.servicio}`,
|
||||
start: `${cita.fecha}T${cita.hora_inicio}`,
|
||||
end: `${cita.fecha}T${cita.hora_fin}`,
|
||||
classNames: [`estado-${cita.estado}`],
|
||||
extendedProps: {
|
||||
type: 'cita',
|
||||
data: cita
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
bloqueos.forEach(bloqueado => {
|
||||
calendar.addEvent({
|
||||
id: `bloqueado-${bloqueado.id}`,
|
||||
title: `Bloqueado: ${bloqueado.motivo || 'Sin motivo'}`,
|
||||
start: `${bloqueado.fecha}T${bloqueado.hora_inicio}`,
|
||||
end: `${bloqueado.fecha}T${bloqueado.hora_fin}`,
|
||||
classNames: ['bloqueado'],
|
||||
extendedProps: {
|
||||
type: 'bloqueado',
|
||||
data: bloqueado
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showEventModal(event) {
|
||||
const props = event.extendedProps;
|
||||
const titleEl = document.getElementById('eventModalTitle');
|
||||
const bodyEl = document.getElementById('eventModalBody');
|
||||
const footerEl = document.getElementById('eventModalFooter');
|
||||
|
||||
if (props.type === 'cita') {
|
||||
const cita = props.data;
|
||||
titleEl.textContent = `Cita: ${cita.cliente_nombre}`;
|
||||
|
||||
bodyEl.innerHTML = `
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div>
|
||||
<label class="text-muted small">Fecha y Hora</label>
|
||||
<p class="mb-0 fw-bold">
|
||||
<i class="fas fa-calendar-alt me-2"></i>
|
||||
${formatDateDisplay(cita.fecha)}
|
||||
${formatTime(cita.hora_inicio)} - ${formatTime(cita.hora_fin)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-muted small">Cliente</label>
|
||||
<p class="mb-0">${cita.cliente_nombre}</p>
|
||||
<p class="mb-0 small"><a href="mailto:${cita.cliente_email}">${cita.cliente_email}</a></p>
|
||||
${cita.cliente_telefono ? `<p class="mb-0 small"><a href="tel:${cita.cliente_telefono}">${cita.cliente_telefono}</a></p>` : ''}
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-muted small">Servicio</label>
|
||||
<p class="mb-0">${cita.servicio}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-muted small">Estado</label>
|
||||
<p class="mb-0">
|
||||
<span class="badge-admin bg-${getBadgeClass(cita.estado)}">
|
||||
${getEstadoLabel(cita.estado)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
${cita.notas ? `
|
||||
<div>
|
||||
<label class="text-muted small">Notas</label>
|
||||
<p class="mb-0">${cita.notas}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
footerEl.innerHTML = `
|
||||
<a href="/admin/citas/${cita.id}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-eye me-2"></i>Ver Detalles
|
||||
</a>
|
||||
<a href="/admin/citas/${cita.id}/edit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-edit me-2"></i>Editar
|
||||
</a>
|
||||
`;
|
||||
} else {
|
||||
const bloqueado = props.data;
|
||||
titleEl.textContent = 'Horario Bloqueado';
|
||||
|
||||
bodyEl.innerHTML = `
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div>
|
||||
<label class="text-muted small">Fecha</label>
|
||||
<p class="mb-0 fw-bold">
|
||||
<i class="fas fa-calendar-alt me-2"></i>
|
||||
${formatDateDisplay(bloqueado.fecha)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-muted small">Horario</label>
|
||||
<p class="mb-0">
|
||||
${formatTime(bloqueado.hora_inicio)} - ${formatTime(bloqueado.hora_fin)}
|
||||
</p>
|
||||
</div>
|
||||
${bloqueado.motivo ? `
|
||||
<div>
|
||||
<label class="text-muted small">Motivo</label>
|
||||
<p class="mb-0">${bloqueado.motivo}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
footerEl.innerHTML = `
|
||||
<button type="button" class="btn btn-secondary-admin" data-bs-dismiss="modal">Cerrar</button>
|
||||
<a href="/admin/horarios/${bloqueado.id}/edit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-edit me-2"></i>Editar
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
eventModal.show();
|
||||
}
|
||||
|
||||
function formatDateDisplay(fecha) {
|
||||
const date = new Date(fecha + 'T00:00:00');
|
||||
return date.toLocaleDateString('es-ES', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
function formatTime(hora) {
|
||||
const [hours, minutes] = hora.split(':');
|
||||
const date = new Date();
|
||||
date.setHours(parseInt(hours));
|
||||
date.setMinutes(parseInt(minutes));
|
||||
return date.toLocaleTimeString('es-ES', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function getBadgeClass(estado) {
|
||||
const classes = {
|
||||
'pendiente': 'warning',
|
||||
'confirmada': 'success',
|
||||
'completada': 'info',
|
||||
'cancelada': 'danger'
|
||||
};
|
||||
return classes[estado] || 'secondary';
|
||||
}
|
||||
|
||||
function getEstadoLabel(estado) {
|
||||
const labels = {
|
||||
'pendiente': 'Pendiente',
|
||||
'confirmada': 'Confirmada',
|
||||
'completada': 'Completada',
|
||||
'cancelada': 'Cancelada'
|
||||
};
|
||||
return labels[estado] || estado;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
293
resources/views/admin/citas/create.blade.php
Normal file
293
resources/views/admin/citas/create.blade.php
Normal file
@@ -0,0 +1,293 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Crear Cita - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Nueva Cita')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.citas.index') }}">Citas</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Nueva Cita</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-calendar-plus me-2"></i>Agendar Nueva Cita
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.citas.store') }}" id="citaForm">
|
||||
@csrf
|
||||
|
||||
@if($mensaje)
|
||||
<input type="hidden" name="mensaje_id" value="{{ $mensaje->id }}">
|
||||
<div class="alert alert-info mb-4">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Nota:</strong> Esta cita se creará a partir del mensaje de
|
||||
<strong>{{ $mensaje->nombre }}</strong>
|
||||
<a href="{{ route('admin.mensajes.show', $mensaje) }}" class="text-decoration-underline ms-1">
|
||||
(Ver mensaje)
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<!-- Cliente Nombre -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="cliente_nombre" class="form-label">
|
||||
Nombre del Cliente <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
class="form-control @error('cliente_nombre') is-invalid @enderror"
|
||||
id="cliente_nombre"
|
||||
name="cliente_nombre"
|
||||
value="{{ $mensaje->nombre ?? old('cliente_nombre') }}"
|
||||
required>
|
||||
@error('cliente_nombre')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Cliente Email -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="cliente_email" class="form-label">
|
||||
Email <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="email"
|
||||
class="form-control @error('cliente_email') is-invalid @enderror"
|
||||
id="cliente_email"
|
||||
name="cliente_email"
|
||||
value="{{ $mensaje->email ?? old('cliente_email') }}"
|
||||
required>
|
||||
@error('cliente_email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Cliente Teléfono -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="cliente_telefono" class="form-label">
|
||||
Teléfono
|
||||
</label>
|
||||
<input type="tel"
|
||||
class="form-control @error('cliente_telefono') is-invalid @enderror"
|
||||
id="cliente_telefono"
|
||||
name="cliente_telefono"
|
||||
value="{{ $mensaje->telefono ?? old('cliente_telefono') }}">
|
||||
@error('cliente_telefono')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Servicio -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="servicio" class="form-label">
|
||||
Servicio
|
||||
</label>
|
||||
<select class="form-select @error('servicio') is-invalid @enderror"
|
||||
id="servicio"
|
||||
name="servicio">
|
||||
<option value="Lash Extensions" selected>Lash Extensions</option>
|
||||
<option value="Lash Lift">Lash Lift</option>
|
||||
<option value="Lash Removal">Lash Removal</option>
|
||||
<option value="Relleno">Relleno</option>
|
||||
<option value="Otro">Otro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="row">
|
||||
<!-- Fecha -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="fecha" class="form-label">
|
||||
Fecha <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="date"
|
||||
class="form-control @error('fecha') is-invalid @enderror"
|
||||
id="fecha"
|
||||
name="fecha"
|
||||
value="{{ old('fecha') }}"
|
||||
min="{{ date('Y-m-d') }}"
|
||||
required>
|
||||
@error('fecha')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@else
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Horario: 9:00 AM - 7:00 PM (Lun-Sáb)
|
||||
</small>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Hora Inicio -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="hora_inicio" class="form-label">
|
||||
Hora <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select class="form-select @error('hora_inicio') is-invalid @enderror"
|
||||
id="hora_inicio"
|
||||
name="hora_inicio"
|
||||
required
|
||||
{{ old('fecha') ? '' : 'disabled' }}>
|
||||
<option value="">Seleccione fecha primero</option>
|
||||
@if(old('fecha'))
|
||||
@php
|
||||
$oldFecha = old('fecha');
|
||||
$oldHora = old('hora_inicio');
|
||||
@endphp
|
||||
@foreach($horariosDisponibles ?? [] as $hora)
|
||||
<option value="{{ $hora['hora'] }}"
|
||||
{{ $oldHora == $hora['hora'] ? 'selected' : '' }}>
|
||||
{{ $hora['label'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
@endif
|
||||
</select>
|
||||
@error('hora_inicio')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@else
|
||||
<small class="text-muted" id="horaHelp">
|
||||
Duración: 60 minutos
|
||||
</small>
|
||||
@enderror
|
||||
<div class="loading-spinner d-none" id="horaLoading">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notas -->
|
||||
<div class="mb-3">
|
||||
<label for="notas" class="form-label">Notas</label>
|
||||
<textarea class="form-control @error('notas') is-invalid @enderror"
|
||||
id="notas"
|
||||
name="notas"
|
||||
rows="3"
|
||||
placeholder="Notas adicionales sobre la cita...">{{ old('notas') }}</textarea>
|
||||
@error('notas')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ route('admin.citas.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-arrow-left me-2"></i>Volver
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary-admin" id="submitBtn">
|
||||
<i class="fas fa-calendar-check me-2"></i>Agendar Cita
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card-admin mb-3">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Información
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="text-muted">
|
||||
<li class="mb-2">Duración de cada cita: <strong>60 minutos</strong></li>
|
||||
<li class="mb-2">Horario de atención: <strong>9:00 AM - 7:00 PM</strong></li>
|
||||
<li class="mb-2">Días laborables: <strong>Lunes a Sábado</strong></li>
|
||||
<li class="mb-2">Último turno: <strong>6:00 PM</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-calendar-alt me-2"></i>Calendario
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<a href="{{ route('admin.citas.calendario') }}" class="btn btn-secondary-admin w-100 mb-2">
|
||||
<i class="fas fa-calendar-alt me-2"></i>Ver Calendario
|
||||
</a>
|
||||
<a href="{{ route('admin.horarios.index') }}" class="btn btn-secondary-admin w-100">
|
||||
<i class="fas fa-clock me-2"></i>Horarios Bloqueados
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const fechaInput = document.getElementById('fecha');
|
||||
const horaSelect = document.getElementById('hora_inicio');
|
||||
const horaLoading = document.getElementById('horaLoading');
|
||||
const horaHelp = document.getElementById('horaHelp');
|
||||
const hoy = new Date().toISOString().split('T')[0];
|
||||
fechaInput.setAttribute('min', hoy);
|
||||
|
||||
// Sundays disabled
|
||||
fechaInput.addEventListener('change', function() {
|
||||
const fecha = new Date(this.value + 'T00:00:00');
|
||||
const diaSemana = fecha.getDay();
|
||||
|
||||
if (diaSemana === 0) {
|
||||
alert('Los domingos no hay atención.');
|
||||
this.value = '';
|
||||
horaSelect.innerHTML = '<option value="">Seleccione fecha primero</option>';
|
||||
horaSelect.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
cargarHorariosDisponibles(this.value);
|
||||
});
|
||||
|
||||
function cargarHorariosDisponibles(fecha) {
|
||||
if (!fecha) return;
|
||||
|
||||
horaSelect.disabled = true;
|
||||
horaLoading.classList.remove('d-none');
|
||||
horaHelp.classList.add('d-none');
|
||||
|
||||
fetch(`/admin/citas/disponibles?fecha=${fecha}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
horaSelect.innerHTML = '<option value="">Seleccione un horario</option>';
|
||||
|
||||
if (data.length === 0) {
|
||||
horaSelect.innerHTML = '<option value="">No hay horarios disponibles</option>';
|
||||
} else {
|
||||
data.forEach(hora => {
|
||||
const option = document.createElement('option');
|
||||
option.value = hora.hora;
|
||||
option.textContent = hora.label;
|
||||
horaSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
horaSelect.disabled = false;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
horaSelect.innerHTML = '<option value="">Error al cargar horarios</option>';
|
||||
horaSelect.disabled = false;
|
||||
})
|
||||
.finally(() => {
|
||||
horaLoading.classList.add('d-none');
|
||||
horaHelp.classList.remove('d-none');
|
||||
});
|
||||
}
|
||||
|
||||
// Trigger on page load if fecha has value
|
||||
if (fechaInput.value) {
|
||||
cargarHorariosDisponibles(fechaInput.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
226
resources/views/admin/citas/edit.blade.php
Normal file
226
resources/views/admin/citas/edit.blade.php
Normal file
@@ -0,0 +1,226 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Editar Cita - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Editar Cita')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.citas.index') }}">Citas</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Editar Cita</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-edit me-2"></i>Editar Cita
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.citas.update', $cita) }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="row">
|
||||
<!-- Cliente Nombre -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="cliente_nombre" class="form-label">
|
||||
Nombre del Cliente <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
class="form-control @error('cliente_nombre') is-invalid @enderror"
|
||||
id="cliente_nombre"
|
||||
name="cliente_nombre"
|
||||
value="{{ old('cliente_nombre', $cita->cliente_nombre) }}"
|
||||
required>
|
||||
@error('cliente_nombre')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Cliente Email -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="cliente_email" class="form-label">
|
||||
Email <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="email"
|
||||
class="form-control @error('cliente_email') is-invalid @enderror"
|
||||
id="cliente_email"
|
||||
name="cliente_email"
|
||||
value="{{ old('cliente_email', $cita->cliente_email) }}"
|
||||
required>
|
||||
@error('cliente_email')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Cliente Teléfono -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="cliente_telefono" class="form-label">
|
||||
Teléfono
|
||||
</label>
|
||||
<input type="tel"
|
||||
class="form-control @error('cliente_telefono') is-invalid @enderror"
|
||||
id="cliente_telefono"
|
||||
name="cliente_telefono"
|
||||
value="{{ old('cliente_telefono', $cita->cliente_telefono) }}">
|
||||
@error('cliente_telefono')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Servicio -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="servicio" class="form-label">
|
||||
Servicio
|
||||
</label>
|
||||
<select class="form-select @error('servicio') is-invalid @enderror"
|
||||
id="servicio"
|
||||
name="servicio">
|
||||
<option value="Lash Extensions" {{ old('servicio', $cita->servicio) == 'Lash Extensions' ? 'selected' : '' }}>Lash Extensions</option>
|
||||
<option value="Lash Lift" {{ old('servicio', $cita->servicio) == 'Lash Lift' ? 'selected' : '' }}>Lash Lift</option>
|
||||
<option value="Lash Removal" {{ old('servicio', $cita->servicio) == 'Lash Removal' ? 'selected' : '' }}>Lash Removal</option>
|
||||
<option value="Relleno" {{ old('servicio', $cita->servicio) == 'Relleno' ? 'selected' : '' }}>Relleno</option>
|
||||
<option value="Otro" {{ old('servicio', $cita->servicio) == 'Otro' ? 'selected' : '' }}>Otro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="row">
|
||||
<!-- Fecha -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="fecha" class="form-label">
|
||||
Fecha <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="date"
|
||||
class="form-control @error('fecha') is-invalid @enderror"
|
||||
id="fecha"
|
||||
name="fecha"
|
||||
value="{{ old('fecha', $cita->fecha->format('Y-m-d')) }}"
|
||||
min="{{ date('Y-m-d') }}"
|
||||
required>
|
||||
@error('fecha')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@else
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Horario: 9:00 AM - 7:00 PM
|
||||
</small>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Hora Inicio -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="hora_inicio" class="form-label">
|
||||
Hora <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select class="form-select @error('hora_inicio') is-invalid @enderror"
|
||||
id="hora_inicio"
|
||||
name="hora_inicio"
|
||||
required
|
||||
disabled>
|
||||
<option value="{{ $cita->hora_inicio }}">{{ \Carbon\Carbon::parse($cita->hora_inicio)->format('h:i A') }}</option>
|
||||
</select>
|
||||
@error('hora_inicio')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@else
|
||||
<small class="text-muted" id="horaHelp">
|
||||
Duración: 60 minutos
|
||||
</small>
|
||||
@enderror
|
||||
<div class="loading-spinner d-none" id="horaLoading">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estado -->
|
||||
<div class="mb-3">
|
||||
<label for="estado" class="form-label">Estado</label>
|
||||
<select class="form-select @error('estado') is-invalid @enderror"
|
||||
id="estado"
|
||||
name="estado">
|
||||
<option value="pendiente" {{ old('estado', $cita->estado) == 'pendiente' ? 'selected' : '' }}>Pendiente</option>
|
||||
<option value="confirmada" {{ old('estado', $cita->estado) == 'confirmada' ? 'selected' : '' }}>Confirmada</option>
|
||||
<option value="completada" {{ old('estado', $cita->estado) == 'completada' ? 'selected' : '' }}>Completada</option>
|
||||
<option value="cancelada" {{ old('estado', $cita->estado) == 'cancelada' ? 'selected' : '' }}>Cancelada</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Notas -->
|
||||
<div class="mb-3">
|
||||
<label for="notas" class="form-label">Notas</label>
|
||||
<textarea class="form-control @error('notas') is-invalid @enderror"
|
||||
id="notas"
|
||||
name="notas"
|
||||
rows="3"
|
||||
placeholder="Notas adicionales...">{{ old('notas', $cita->notas) }}</textarea>
|
||||
@error('notas')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ route('admin.citas.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-arrow-left me-2"></i>Volver
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-save me-2"></i>Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Info -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card-admin mb-3">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Información de la Cita
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-5">Fecha Creación:</dt>
|
||||
<dd class="col-sm-7">{{ $cita->created_at->format('d/m/Y H:i') }}</dd>
|
||||
|
||||
<dt class="col-sm-5">Última Actualización:</dt>
|
||||
<dd class="col-sm-7">{{ $cita->updated_at->format('d/m/Y H:i') }}</dd>
|
||||
|
||||
@if($cita->mensaje)
|
||||
<dt class="col-sm-5">Desde Mensaje:</dt>
|
||||
<dd class="col-sm-7">
|
||||
<a href="{{ route('admin.mensajes.show', $cita->mensaje) }}">Ver mensaje</a>
|
||||
</dd>
|
||||
@endif
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>¿Cancelar Cita?
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-3">
|
||||
¿Necesita cancelar esta cita? Puede cambiar el estado a "Cancelada" desde el formulario o eliminarla permanentemente.
|
||||
</p>
|
||||
<form action="{{ route('admin.citas.destroy', $cita) }}" method="POST"
|
||||
onsubmit="return confirm('¿Estás seguro de que deseas eliminar esta cita? Esta acción no se puede deshacer.')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-danger-admin w-100">
|
||||
<i class="fas fa-trash me-2"></i>Eliminar Cita
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
272
resources/views/admin/citas/index.blade.php
Normal file
272
resources/views/admin/citas/index.blade.php
Normal file
@@ -0,0 +1,272 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Citas - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Citas')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Citas</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ $stats['pendientes'] ?? 0 }}</h3>
|
||||
<p>Pendientes</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ $stats['confirmadas'] ?? 0 }}</h3>
|
||||
<p>Confirmadas</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon info">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ $stats['completadas'] ?? 0 }}</h3>
|
||||
<p>Completadas</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon warning">
|
||||
<i class="fas fa-times-circle"></i>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<h3>{{ $stats['canceladas'] ?? 0 }}</h3>
|
||||
<p>Canceladas</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters & Actions -->
|
||||
<div class="card-admin mb-4">
|
||||
<div class="card-body">
|
||||
<div class="filters-bar">
|
||||
<form method="GET" class="d-flex gap-3 flex-wrap align-items-center">
|
||||
<div class="search-box">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" name="buscar" class="form-control"
|
||||
placeholder="Buscar cliente..."
|
||||
value="{{ request('buscar') }}">
|
||||
</div>
|
||||
|
||||
<div class="search-box">
|
||||
<i class="fas fa-calendar"></i>
|
||||
<input type="date" name="fecha" class="form-control"
|
||||
value="{{ request('fecha') }}">
|
||||
</div>
|
||||
|
||||
<select name="estado" class="form-select" style="width: auto;">
|
||||
<option value="">Todos los estados</option>
|
||||
<option value="pendiente" {{ request('estado') == 'pendiente' ? 'selected' : '' }}>Pendiente</option>
|
||||
<option value="confirmada" {{ request('estado') == 'confirmada' ? 'selected' : '' }}>Confirmada</option>
|
||||
<option value="completada" {{ request('estado') == 'completada' ? 'selected' : '' }}>Completada</option>
|
||||
<option value="cancelada" {{ request('estado') == 'cancelada' ? 'selected' : '' }}>Cancelada</option>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-filter me-2"></i>Filtrar
|
||||
</button>
|
||||
|
||||
@if(request()->hasAny(['buscar', 'fecha', 'estado']))
|
||||
<a href="{{ route('admin.citas.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-times me-2"></i>Limpiar
|
||||
</a>
|
||||
@endif
|
||||
</form>
|
||||
|
||||
<div class="d-flex gap-2 ms-auto">
|
||||
<a href="{{ route('admin.citas.create') }}" class="btn btn-primary-admin">
|
||||
<i class="fas fa-plus me-2"></i>Nueva Cita
|
||||
</a>
|
||||
<a href="{{ route('admin.citas.calendario') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-calendar-alt me-2"></i>Calendario
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Citas Table -->
|
||||
<div class="card-admin">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="fas fa-calendar me-2"></i>Listado de Citas
|
||||
</span>
|
||||
<span class="text-muted small">
|
||||
Total: {{ $citas->total() }} citas
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($citas->isEmpty())
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-calendar-times"></i>
|
||||
<h4>No hay citas</h4>
|
||||
<p>No se encontraron citas con los filtros seleccionados.</p>
|
||||
<a href="{{ route('admin.citas.create') }}" class="btn btn-primary-admin mt-3">
|
||||
<i class="fas fa-plus me-2"></i>Crear Primera Cita
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="table-admin">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha/Hora</th>
|
||||
<th>Cliente</th>
|
||||
<th>Servicio</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($citas as $cita)
|
||||
<tr>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="fw-bold">
|
||||
<i class="fas fa-calendar-alt me-1 text-primary"></i>
|
||||
{{ $cita->fecha->format('d/m/Y') }}
|
||||
</span>
|
||||
<span class="text-muted small">
|
||||
{{ $cita->hora_inicio }} - {{ $cita->hora_fin }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column">
|
||||
<span class="fw-bold">{{ $cita->cliente_nombre }}</span>
|
||||
<span class="text-muted small">{{ $cita->cliente_email }}</span>
|
||||
@if($cita->cliente_telefono)
|
||||
<span class="text-muted small">{{ $cita->cliente_telefono }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ $cita->servicio }}</td>
|
||||
<td>
|
||||
@switch($cita->estado)
|
||||
@case('pendiente')
|
||||
<span class="badge-admin bg-warning">Pendiente</span>
|
||||
@break
|
||||
@case('confirmada')
|
||||
<span class="badge-admin bg-success">Confirmada</span>
|
||||
@break
|
||||
@case('completada')
|
||||
<span class="badge-admin bg-info">Completada</span>
|
||||
@break
|
||||
@case('cancelada')
|
||||
<span class="badge-admin bg-danger">Cancelada</span>
|
||||
@break
|
||||
@endswitch
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<a href="{{ route('admin.citas.show', $cita) }}"
|
||||
class="btn btn-sm btn-secondary-admin"
|
||||
title="Ver detalles">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{{ route('admin.citas.edit', $cita) }}"
|
||||
class="btn btn-sm btn-secondary-admin"
|
||||
title="Editar">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-secondary-admin dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
title="Cambiar estado">
|
||||
<i class="fas fa-flag"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<form action="{{ route('admin.citas.estado', $cita) }}" method="POST">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<input type="hidden" name="estado" value="pendiente">
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="fas fa-clock me-2"></i>Pendiente
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
<form action="{{ route('admin.citas.estado', $cita) }}" method="POST">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<input type="hidden" name="estado" value="confirmada">
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="fas fa-check me-2"></i>Confirmar
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
<li>
|
||||
<form action="{{ route('admin.citas.estado', $cita) }}" method="POST">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<input type="hidden" name="estado" value="completada">
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="fas fa-check-circle me-2"></i>Completar
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form action="{{ route('admin.citas.estado', $cita) }}" method="POST">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<input type="hidden" name="estado" value="cancelada">
|
||||
<button type="submit" class="dropdown-item text-danger">
|
||||
<i class="fas fa-times me-2"></i>Cancelar
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('admin.citas.destroy', $cita) }}" method="POST"
|
||||
onsubmit="return confirm('¿Estás seguro de eliminar esta cita?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger-admin" title="Eliminar">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination-wrapper p-3">
|
||||
{{ $citas->withQueryString()->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
199
resources/views/admin/citas/show.blade.php
Normal file
199
resources/views/admin/citas/show.blade.php
Normal file
@@ -0,0 +1,199 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Ver Cita - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Detalle de Cita')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.citas.index') }}">Citas</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Ver Cita</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="fas fa-calendar me-2"></i>Cita de {{ $cita->cliente_nombre }}
|
||||
</span>
|
||||
@switch($cita->estado)
|
||||
@case('pendiente')
|
||||
<span class="badge-admin bg-warning">Pendiente</span>
|
||||
@break
|
||||
@case('confirmada')
|
||||
<span class="badge-admin bg-success">Confirmada</span>
|
||||
@break
|
||||
@case('completada')
|
||||
<span class="badge-admin bg-info">Completada</span>
|
||||
@break
|
||||
@case('cancelada')
|
||||
<span class="badge-admin bg-danger">Cancelada</span>
|
||||
@break
|
||||
@endswitch
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Date & Time -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small">Fecha</label>
|
||||
<p class="mb-0 fw-bold">
|
||||
<i class="fas fa-calendar-alt me-2 text-primary"></i>
|
||||
{{ $cita->fecha->format('d/m/Y') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small">Horario</label>
|
||||
<p class="mb-0 fw-bold">
|
||||
<i class="fas fa-clock me-2 text-primary"></i>
|
||||
{{ \Carbon\Carbon::parse($cita->hora_inicio)->format('h:i A') }}
|
||||
-
|
||||
{{ \Carbon\Carbon::parse($cita->hora_fin)->format('h:i A') }}
|
||||
</p>
|
||||
<small class="text-muted">Duración: 60 minutos</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Client Info -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="text-muted small">Nombre del Cliente</label>
|
||||
<p class="mb-0 fw-bold">{{ $cita->cliente_nombre }}</p>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="text-muted small">Email</label>
|
||||
<p class="mb-0">
|
||||
<a href="mailto:{{ $cita->cliente_email }}">{{ $cita->cliente_email }}</a>
|
||||
</p>
|
||||
</div>
|
||||
@if($cita->cliente_telefono)
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="text-muted small">Teléfono</label>
|
||||
<p class="mb-0">
|
||||
<a href="tel:{{ $cita->cliente_telefono }}">{{ $cita->cliente_telefono }}</a>
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="text-muted small">Servicio</label>
|
||||
<p class="mb-0">{{ $cita->servicio }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($cita->notas)
|
||||
<hr>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="mb-4">
|
||||
<label class="text-muted small d-block mb-2">Notas</label>
|
||||
<div class="p-3 bg-light rounded">
|
||||
{{ $cita->notas }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Metadata -->
|
||||
<div class="row text-muted small">
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small">Fecha de creación</label>
|
||||
<p class="mb-0">{{ $cita->created_at->format('d/m/Y H:i') }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="text-muted small">Última actualización</label>
|
||||
<p class="mb-0">{{ $cita->updated_at->format('d/m/Y H:i') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ route('admin.citas.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-arrow-left me-2"></i>Volver
|
||||
</a>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ route('admin.citas.edit', $cita) }}" class="btn btn-primary-admin">
|
||||
<i class="fas fa-edit me-2"></i>Editar
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Quick Status Change -->
|
||||
<div class="card-admin mb-3">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-flag me-2"></i>Cambiar Estado
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route('admin.citas.estado', $cita) }}" method="POST" class="d-flex flex-column gap-2">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
|
||||
<button type="submit" name="estado" value="pendiente"
|
||||
class="btn btn-sm {{ $cita->estado == 'pendiente' ? 'btn-primary-admin' : 'btn-secondary-admin' }}">
|
||||
<i class="fas fa-clock me-2"></i>Pendiente
|
||||
</button>
|
||||
<button type="submit" name="estado" value="confirmada"
|
||||
class="btn btn-sm {{ $cita->estado == 'confirmada' ? 'btn-primary-admin' : 'btn-secondary-admin' }}">
|
||||
<i class="fas fa-check me-2"></i>Confirmar
|
||||
</button>
|
||||
<button type="submit" name="estado" value="completada"
|
||||
class="btn btn-sm {{ $cita->estado == 'completada' ? 'btn-primary-admin' : 'btn-secondary-admin' }}">
|
||||
<i class="fas fa-check-circle me-2"></i>Completar
|
||||
</button>
|
||||
<button type="submit" name="estado" value="cancelada"
|
||||
class="btn btn-sm {{ $cita->estado == 'cancelada' ? 'btn-danger-admin' : 'btn-secondary-admin' }}">
|
||||
<i class="fas fa-times me-2"></i>Cancelar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Related Message -->
|
||||
@if($cita->mensaje)
|
||||
<div class="card-admin mb-3">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-envelope me-2"></i>Mensaje Relacionado
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-2"><strong>{{ $cita->mensaje->nombre }}</strong></p>
|
||||
<p class="text-muted small mb-3">{{ $cita->mensaje->mensaje }}</p>
|
||||
<a href="{{ route('admin.mensajes.show', $cita->mensaje) }}" class="btn btn-secondary-admin w-100">
|
||||
<i class="fas fa-eye me-2"></i>Ver Mensaje
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<div class="card-admin">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Zona de Peligro
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-3">
|
||||
Eliminar esta cita es una acción permanente. ¿Estás seguro?
|
||||
</p>
|
||||
<form action="{{ route('admin.citas.destroy', $cita) }}" method="POST"
|
||||
onsubmit="return confirm('¿Estás seguro de eliminar esta cita? Esta acción no se puede deshacer.')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-danger-admin w-100">
|
||||
<i class="fas fa-trash me-2"></i>Eliminar Cita
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
209
resources/views/admin/horarios/create.blade.php
Normal file
209
resources/views/admin/horarios/create.blade.php
Normal file
@@ -0,0 +1,209 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Bloquear Horario - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Bloquear Horario')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.horarios.index') }}">Horarios Bloqueados</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Bloquear Horario</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-ban me-2"></i>Bloquear Horario
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.horarios.store') }}">
|
||||
@csrf
|
||||
|
||||
<div class="row">
|
||||
<!-- Fecha -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="fecha" class="form-label">
|
||||
Fecha <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="date"
|
||||
class="form-control @error('fecha') is-invalid @enderror"
|
||||
id="fecha"
|
||||
name="fecha"
|
||||
value="{{ old('fecha') }}"
|
||||
min="{{ date('Y-m-d') }}"
|
||||
required>
|
||||
@error('fecha')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@else
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
No se podrán agendar citas en esta fecha
|
||||
</small>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Hora Inicio -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="hora_inicio" class="form-label">
|
||||
Hora de Inicio <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select class="form-select @error('hora_inicio') is-invalid @enderror"
|
||||
id="hora_inicio"
|
||||
name="hora_inicio"
|
||||
required>
|
||||
<option value="">Seleccionar hora</option>
|
||||
@for($hora = 9; $hora < 19; $hora++)
|
||||
<option value="{{ sprintf('%02d:00:00', $hora) }}"
|
||||
{{ old('hora_inicio') == sprintf('%02d:00:00', $hora) ? 'selected' : '' }}>
|
||||
{{ sprintf('%02d:00', $hora) }} AM
|
||||
</option>
|
||||
@endfor
|
||||
@for($hora = 12; $hora < 19; $hora++)
|
||||
<option value="{{ sprintf('%02d:00:00', $hora) }}"
|
||||
{{ old('hora_inicio') == sprintf('%02d:00:00', $hora) ? 'selected' : '' }}>
|
||||
{{ $hora > 12 ? sprintf('%02d:00', $hora - 12) : sprintf('%02d:00', $hora) }}:00 {{ $hora >= 12 ? 'PM' : 'AM' }}
|
||||
</option>
|
||||
@endfor
|
||||
</select>
|
||||
@error('hora_inicio')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Hora Fin -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="hora_fin" class="form-label">
|
||||
Hora de Fin <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select class="form-select @error('hora_fin') is-invalid @enderror"
|
||||
id="hora_fin"
|
||||
name="hora_fin"
|
||||
required>
|
||||
<option value="">Seleccionar hora</option>
|
||||
@for($hora = 9; $hora < 19; $hora++)
|
||||
<option value="{{ sprintf('%02d:00:00', $hora) }}"
|
||||
{{ old('hora_fin') == sprintf('%02d:00:00', $hora) ? 'selected' : '' }}>
|
||||
{{ sprintf('%02d:00', $hora) }}:00 {{ $hora >= 12 ? 'PM' : 'AM' }}
|
||||
</option>
|
||||
@endfor
|
||||
</select>
|
||||
@error('hora_fin')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@else
|
||||
<small class="text-muted">
|
||||
El bloqueo dura hasta esta hora
|
||||
</small>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Motivo -->
|
||||
<div class="mb-3">
|
||||
<label for="motivo" class="form-label">Motivo</label>
|
||||
<input type="text"
|
||||
class="form-control @error('motivo') is-invalid @enderror"
|
||||
id="motivo"
|
||||
name="motivo"
|
||||
value="{{ old('motivo') }}"
|
||||
placeholder="Ej: Vacaciones, Mantenimiento, Evento especial...">
|
||||
@error('motivo')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@else
|
||||
<small class="text-muted">
|
||||
Opcional. Ej: "Vacaciones de Semana Santa"
|
||||
</small>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Quick Options -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Opciones Rápidas</label>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button type="button" class="btn btn-secondary-admin btn-sm" onclick="bloquearDiaCompleto()">
|
||||
<i class="fas fa-calendar me-2"></i>Bloquear Día Completo
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary-admin btn-sm" onclick="bloquearManana()">
|
||||
<i class="fas fa-sun me-2"></i>Bloquear Mañana (9am-12pm)
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary-admin btn-sm" onclick="bloquearTarde()">
|
||||
<i class="fas fa-moon me-2"></i>Bloquear Tarde (12pm-7pm)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ route('admin.horarios.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-arrow-left me-2"></i>Volver
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-lock me-2"></i>Bloquear Horario
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card-admin mb-3">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Información
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="text-muted">
|
||||
<li class="mb-2">Los horarios bloqueados prevents the agendamiento de citas.</li>
|
||||
<li class="mb-2">Puedes bloquear días completos o solo tramos específicos.</li>
|
||||
<li class="mb-2">El bloqueo no afecta las citas ya existentes.</li>
|
||||
<li class="mb-2">Los clientes no podrán ver estos horarios.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-calendar-alt me-2"></i>Horario de Atención
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="text-muted mb-0">
|
||||
<li>Lunes a Sábado: <strong>9:00 AM - 7:00 PM</strong></li>
|
||||
<li>Último turno: <strong>6:00 PM</strong></li>
|
||||
<li>Duración cita: <strong>60 minutos</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const hoy = new Date().toISOString().split('T')[0];
|
||||
document.getElementById('fecha').setAttribute('min', hoy);
|
||||
|
||||
function bloquearDiaCompleto() {
|
||||
document.getElementById('hora_inicio').value = '09:00:00';
|
||||
document.getElementById('hora_fin').value = '19:00:00';
|
||||
document.getElementById('motivo').value = 'Día completo bloqueado';
|
||||
}
|
||||
|
||||
function bloquearManana() {
|
||||
document.getElementById('hora_inicio').value = '09:00:00';
|
||||
document.getElementById('hora_fin').value = '12:00:00';
|
||||
document.getElementById('motivo').value = 'Mañana bloqueada';
|
||||
}
|
||||
|
||||
function bloquearTarde() {
|
||||
document.getElementById('hora_inicio').value = '12:00:00';
|
||||
document.getElementById('hora_fin').value = '19:00:00';
|
||||
document.getElementById('motivo').value = 'Tarde bloqueada';
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
154
resources/views/admin/horarios/edit.blade.php
Normal file
154
resources/views/admin/horarios/edit.blade.php
Normal file
@@ -0,0 +1,154 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Editar Bloqueo - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Editar Bloqueo')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.horarios.index') }}">Horarios Bloqueados</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Editar Bloqueo</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card-admin">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-edit me-2"></i>Editar Bloqueo
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.horarios.update', $bloqueado) }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="row">
|
||||
<!-- Fecha -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="fecha" class="form-label">
|
||||
Fecha <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="date"
|
||||
class="form-control @error('fecha') is-invalid @enderror"
|
||||
id="fecha"
|
||||
name="fecha"
|
||||
value="{{ old('fecha', $bloqueado->fecha->format('Y-m-d')) }}"
|
||||
required>
|
||||
@error('fecha')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Hora Inicio -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="hora_inicio" class="form-label">
|
||||
Hora de Inicio <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select class="form-select @error('hora_inicio') is-invalid @enderror"
|
||||
id="hora_inicio"
|
||||
name="hora_inicio"
|
||||
required>
|
||||
@for($hora = 9; $hora < 19; $hora++)
|
||||
<option value="{{ sprintf('%02d:00:00', $hora) }}"
|
||||
{{ old('hora_inicio', $bloqueado->hora_inicio) == sprintf('%02d:00:00', $hora) ? 'selected' : '' }}>
|
||||
{{ $hora <= 12 ? sprintf('%02d:00', $hora) : sprintf('%02d', $hora - 12) }}:00 {{ $hora >= 12 ? 'PM' : 'AM' }}
|
||||
</option>
|
||||
@endfor
|
||||
</select>
|
||||
@error('hora_inicio')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Hora Fin -->
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="hora_fin" class="form-label">
|
||||
Hora de Fin <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select class="form-select @error('hora_fin') is-invalid @enderror"
|
||||
id="hora_fin"
|
||||
name="hora_fin"
|
||||
required>
|
||||
@for($hora = 9; $hora < 19; $hora++)
|
||||
<option value="{{ sprintf('%02d:00:00', $hora) }}"
|
||||
{{ old('hora_fin', $bloqueado->hora_fin) == sprintf('%02d:00:00', $hora) ? 'selected' : '' }}>
|
||||
{{ $hora <= 12 ? sprintf('%02d:00', $hora) : sprintf('%02d', $hora - 12) }}:00 {{ $hora >= 12 ? 'PM' : 'AM' }}
|
||||
</option>
|
||||
@endfor
|
||||
</select>
|
||||
@error('hora_fin')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Motivo -->
|
||||
<div class="mb-3">
|
||||
<label for="motivo" class="form-label">Motivo</label>
|
||||
<input type="text"
|
||||
class="form-control @error('motivo') is-invalid @enderror"
|
||||
id="motivo"
|
||||
name="motivo"
|
||||
value="{{ old('motivo', $bloqueado->motivo) }}"
|
||||
placeholder="Ej: Vacaciones, Mantenimiento...">
|
||||
@error('motivo')
|
||||
<div class="invalid-feedback">{{ $message }}</div>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<a href="{{ route('admin.horarios.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-arrow-left me-2"></i>Volver
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-save me-2"></i>Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card-admin mb-3">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Información del Bloqueo
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-5">Creado:</dt>
|
||||
<dd class="col-sm-7">{{ $bloqueado->created_at->format('d/m/Y H:i') }}</dd>
|
||||
|
||||
<dt class="col-sm-5">Actualizado:</dt>
|
||||
<dd class="col-sm-7">{{ $bloqueado->updated_at->format('d/m/Y H:i') }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-admin">
|
||||
<div class="card-header bg-danger text-white">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Eliminar Bloqueo
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-3">
|
||||
¿Estás seguro de desbloquear este horario? Los clientes podrán agendar citas en este espacio.
|
||||
</p>
|
||||
<form action="{{ route('admin.horarios.destroy', $bloqueado) }}" method="POST"
|
||||
onsubmit="return confirm('¿Estás seguro de eliminar este bloqueo?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-danger-admin w-100">
|
||||
<i class="fas fa-unlock me-2"></i>Desbloquear
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
140
resources/views/admin/horarios/index.blade.php
Normal file
140
resources/views/admin/horarios/index.blade.php
Normal file
@@ -0,0 +1,140 @@
|
||||
@extends('admin.layouts.master')
|
||||
|
||||
@section('title', 'Horarios Bloqueados - Lash Vanshy')
|
||||
|
||||
@section('page-title', 'Horarios Bloqueados')
|
||||
|
||||
@section('content')
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Horarios Bloqueados</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Actions & Filters -->
|
||||
<div class="card-admin mb-4">
|
||||
<div class="card-body">
|
||||
<div class="filters-bar">
|
||||
<form method="GET" class="d-flex gap-3 flex-wrap align-items-center">
|
||||
<div class="search-box">
|
||||
<i class="fas fa-calendar"></i>
|
||||
<input type="date" name="fecha" class="form-control"
|
||||
value="{{ request('fecha') }}">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-filter me-2"></i>Filtrar
|
||||
</button>
|
||||
|
||||
@if(request('fecha'))
|
||||
<a href="{{ route('admin.horarios.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-times me-2"></i>Limpiar
|
||||
</a>
|
||||
@endif
|
||||
</form>
|
||||
|
||||
<div class="d-flex gap-2 ms-auto">
|
||||
<a href="{{ route('admin.horarios.create') }}" class="btn btn-primary-admin">
|
||||
<i class="fas fa-plus me-2"></i>Bloquear Horario
|
||||
</a>
|
||||
<a href="{{ route('admin.citas.index') }}" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-arrow-left me-2"></i>Volver a Citas
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Horarios Table -->
|
||||
<div class="card-admin">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="fas fa-clock me-2"></i>Horarios Bloqueados
|
||||
</span>
|
||||
<span class="text-muted small">
|
||||
Total: {{ $bloqueados->total() }} bloqueos
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($bloqueados->isEmpty())
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-calendar-check"></i>
|
||||
<h4>No hay horarios bloqueados</h4>
|
||||
<p>No se encontraron horarios bloqueados.</p>
|
||||
<a href="{{ route('admin.horarios.create') }}" class="btn btn-primary-admin mt-3">
|
||||
<i class="fas fa-plus me-2"></i>Bloquear Primer Horario
|
||||
</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="table-admin">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fecha</th>
|
||||
<th>Hora Inicio</th>
|
||||
<th>Hora Fin</th>
|
||||
<th>Motivo</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($bloqueados as $bloqueado)
|
||||
<tr>
|
||||
<td>
|
||||
<span class="fw-bold">
|
||||
<i class="fas fa-calendar-alt me-1 text-primary"></i>
|
||||
{{ \Carbon\Carbon::parse($bloqueado->fecha)->format('d/m/Y') }}
|
||||
</span>
|
||||
<span class="text-muted small d-block">
|
||||
{{ \Carbon\Carbon::parse($bloqueado->fecha)->format('l') }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ \Carbon\Carbon::parse($bloqueado->hora_inicio)->format('h:i A') }}</td>
|
||||
<td>{{ \Carbon\Carbon::parse($bloqueado->hora_fin)->format('h:i A') }}</td>
|
||||
<td>
|
||||
@if($bloqueado->motivo)
|
||||
<span class="text-muted">{{ $bloqueado->motivo }}</span>
|
||||
@else
|
||||
<span class="text-muted">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<a href="{{ route('admin.horarios.edit', $bloqueado) }}"
|
||||
class="btn btn-sm btn-secondary-admin"
|
||||
title="Editar">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form action="{{ route('admin.horarios.destroy', $bloqueado) }}" method="POST"
|
||||
onsubmit="return confirm('¿Estás seguro de desbloquear este horario?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger-admin" title="Eliminar">
|
||||
<i class="fas fa-unlock"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination-wrapper p-3">
|
||||
{{ $bloqueados->withQueryString()->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Box -->
|
||||
<div class="alert alert-info mt-4">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Nota:</strong> Los horarios bloqueados impedirán que se agenden citas en esos horarios.
|
||||
Utilice esta función para bloquear días completos o tramos específicos (ej: vacaciones, mantenimiento).
|
||||
</div>
|
||||
@endsection
|
||||
@@ -75,6 +75,31 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<span>Citas</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="{{ route('admin.citas.index') }}" class="dropdown-item {{ request()->routeIs('admin.citas.index') ? 'active' : '' }}">
|
||||
<i class="fas fa-list me-2"></i>Lista de Citas
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ route('admin.citas.calendario') }}" class="dropdown-item {{ request()->routeIs('admin.citas.calendario') ? 'active' : '' }}">
|
||||
<i class="fas fa-calendar me-2"></i>Calendario
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a href="{{ route('admin.horarios.index') }}" class="dropdown-item {{ request()->routeIs('admin.horarios.*') ? 'active' : '' }}">
|
||||
<i class="fas fa-clock me-2"></i>Horarios Bloqueados
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
@if(Auth::guard('admin')->user()->rol === 'super_admin')
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.configuracion.index') }}"
|
||||
|
||||
@@ -70,15 +70,26 @@
|
||||
<i class="fas fa-arrow-left me-2"></i>Volver
|
||||
</a>
|
||||
<div class="d-flex gap-2">
|
||||
@if(!isset($yaTieneCita) || !$yaTieneCita)
|
||||
<a href="{{ route('admin.citas.create', ['mensaje_id' => $mensaje->id]) }}" class="btn btn-primary-admin">
|
||||
<i class="fas fa-calendar-plus me-2"></i>Agendar Cita
|
||||
</a>
|
||||
@else
|
||||
<span class="badge-admin bg-success">
|
||||
<i class="fas fa-check-circle me-1"></i>Ya tiene cita
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if(!$mensaje->leido)
|
||||
<form action="{{ route('admin.mensajes.leido', $mensaje) }}" method="POST">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<button type="submit" class="btn btn-primary-admin">
|
||||
<i class="fas fa-check me-2"></i>Marcar como leído
|
||||
<button type="submit" class="btn btn-secondary-admin">
|
||||
<i class="fas fa-check me-2"></i>Marcar leído
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('admin.mensajes.destroy', $mensaje) }}" method="POST" onsubmit="return confirm('¿Estás seguro de que deseas eliminar este mensaje?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
use App\Http\Controllers\Admin\AdminUserController;
|
||||
use App\Http\Controllers\Admin\AuthController;
|
||||
use App\Http\Controllers\Admin\CitaController;
|
||||
use App\Http\Controllers\Admin\ConfiguracionController;
|
||||
use App\Http\Controllers\Admin\DashboardController;
|
||||
use App\Http\Controllers\Admin\GaleriaController;
|
||||
use App\Http\Controllers\Admin\HorarioBloqueadoController;
|
||||
use App\Http\Controllers\Admin\MensajeController;
|
||||
use App\Http\Controllers\Admin\ProductoController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -76,4 +78,36 @@ Route::middleware(['admin.auth', 'security.headers'])->group(function () {
|
||||
Route::get('/', [ConfiguracionController::class, 'index'])->name('index');
|
||||
Route::put('/', [ConfiguracionController::class, 'update'])->name('update');
|
||||
});
|
||||
|
||||
// Citas
|
||||
Route::prefix('citas')->name('admin.citas.')->group(function () {
|
||||
Route::get('/', [CitaController::class, 'index'])->name('index');
|
||||
Route::get('/create', [CitaController::class, 'create'])->name('create');
|
||||
Route::get('/create/{mensaje_id}', [CitaController::class, 'createFromMensaje'])->name('create-from-mensaje');
|
||||
Route::post('/', [CitaController::class, 'store'])->name('store');
|
||||
Route::get('/calendario', [CitaController::class, 'calendario'])->name('calendario');
|
||||
Route::get('/{cita}', [CitaController::class, 'show'])->name('show');
|
||||
Route::get('/{cita}/edit', [CitaController::class, 'edit'])->name('edit');
|
||||
Route::put('/{cita}', [CitaController::class, 'update'])->name('update');
|
||||
Route::delete('/{cita}', [CitaController::class, 'destroy'])->name('destroy');
|
||||
Route::patch('/{cita}/estado', [CitaController::class, 'cambiarEstado'])->name('estado');
|
||||
});
|
||||
|
||||
// API de Citas
|
||||
Route::get('/citas/disponibles', [CitaController::class, 'getHorariosDisponibles'])->name('admin.citas.disponibles');
|
||||
Route::get('/citas/por-fecha', [CitaController::class, 'getCitasPorFecha'])->name('admin.citas.por-fecha');
|
||||
|
||||
// Horarios Bloqueados
|
||||
Route::prefix('horarios')->name('admin.horarios.')->group(function () {
|
||||
Route::get('/', [HorarioBloqueadoController::class, 'index'])->name('index');
|
||||
Route::get('/create', [HorarioBloqueadoController::class, 'create'])->name('create');
|
||||
Route::post('/', [HorarioBloqueadoController::class, 'store'])->name('store');
|
||||
Route::get('/{bloqueado}/edit', [HorarioBloqueadoController::class, 'edit'])->name('edit');
|
||||
Route::put('/{bloqueado}', [HorarioBloqueadoController::class, 'update'])->name('update');
|
||||
Route::delete('/{bloqueado}', [HorarioBloqueadoController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
|
||||
// API de Horarios
|
||||
Route::get('/horarios/por-fecha', [HorarioBloqueadoController::class, 'getHorariosPorFecha'])->name('admin.horarios.por-fecha');
|
||||
Route::get('/horarios/verificar', [HorarioBloqueadoController::class, 'verificar'])->name('admin.horarios.verificar');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user