Add citas module: scheduling, calendar, blocked schedules

This commit is contained in:
2026-04-08 00:48:36 -06:00
parent e19eb205db
commit 91da97685f
21 changed files with 3406 additions and 4 deletions

View 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

View 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

View 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

View 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

View 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