Add citas module: scheduling, calendar, blocked schedules
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user