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
|
||||
Reference in New Issue
Block a user