478 lines
16 KiB
PHP
Executable File
478 lines
16 KiB
PHP
Executable File
@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
|