440 lines
18 KiB
PHP
440 lines
18 KiB
PHP
<?php include __DIR__ . "/../layouts/header.php"; ?>
|
|
|
|
<style>
|
|
.task-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
padding: 1rem;
|
|
background-color: #f9f9f9;
|
|
border-radius: 0.5rem;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.task-form__input-group {
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.task-form__input {
|
|
flex: 1;
|
|
}
|
|
|
|
.task-form__badge {
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
@media (max-width: 767.98px) {
|
|
|
|
#calendar {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.fc-header-toolbar {
|
|
font-size: 0.75rem;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="container-fluid mt-4">
|
|
<div class="row g-3">
|
|
<!-- Título del Dashboard -->
|
|
<div class="col-12 text-center my-4">
|
|
<h1 class="display-4 text-dark"><?php echo $_ENV["APP_NAME"]; ?></h1>
|
|
<p class="lead text-muted">Automatiza tus tareas diarias y mejora tu productividad con
|
|
<b><?php echo $_ENV["APP_NAME"]; ?></b></p>
|
|
</div>
|
|
|
|
<!-- Tarjetas de Resumen -->
|
|
<div class="row g-3 mb-4">
|
|
<!-- Balance General de Dinero -->
|
|
<div class="col-12 col-sm-6 col-xl-3">
|
|
<div class="card border-start border-success border-4 shadow h-100">
|
|
<div class="card-body py-3">
|
|
<div class="row align-items-center g-0">
|
|
<div class="col-8">
|
|
<div class="small fw-bold text-success text-uppercase mb-1">
|
|
Balance General</div>
|
|
<div class="h5 mb-0 fw-bold text-gray-800" id="balanceGeneralAmount">Q 0</div>
|
|
</div>
|
|
<div class="col-4 text-end">
|
|
<i class="fas fa-wallet fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Eventos Próximos -->
|
|
<div class="col-12 col-sm-6 col-xl-3">
|
|
<div class="card border-start border-warning border-4 shadow h-100">
|
|
<div class="card-body py-3">
|
|
<div class="row align-items-center g-0">
|
|
<div class="col-8">
|
|
<div class="small fw-bold text-warning text-uppercase mb-1">
|
|
Eventos Próximos</div>
|
|
<div class="h5 mb-0 fw-bold text-gray-800" id="upcomingEventsCount">0</div>
|
|
</div>
|
|
<div class="col-4 text-end">
|
|
<i class="fas fa-calendar-alt fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tareas En Proceso -->
|
|
<div class="col-12 col-sm-6 col-xl-3">
|
|
<div class="card border-start border-info border-4 shadow h-100">
|
|
<div class="card-body py-3">
|
|
<div class="row align-items-center g-0">
|
|
<div class="col-8">
|
|
<div class="small fw-bold text-info text-uppercase mb-1">
|
|
Tareas En Proceso</div>
|
|
<div class="h5 mb-0 fw-bold text-gray-800" id="inProgressTasksCount">0</div>
|
|
</div>
|
|
<div class="col-4 text-end">
|
|
<i class="fas fa-tasks fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tareas Completadas -->
|
|
<div class="col-12 col-sm-6 col-xl-3">
|
|
<div class="card border-start border-primary border-4 shadow h-100">
|
|
<div class="card-body py-3">
|
|
<div class="row align-items-center g-0">
|
|
<div class="col-8">
|
|
<div class="small fw-bold text-primary text-uppercase mb-1">
|
|
Tareas Completadas</div>
|
|
<div class="h5 mb-0 fw-bold text-gray-800" id="completedTasksCount">0</div>
|
|
</div>
|
|
<div class="col-4 text-end">
|
|
<i class="fas fa-check-circle fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Calendario y Gráficos -->
|
|
<div class="row mb-5 g-3">
|
|
<!-- Calendario -->
|
|
<div class="col-12 col-lg-8">
|
|
<div class="card shadow h-100">
|
|
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
|
<h6 class="my-0">Calendario de Eventos</h6>
|
|
<button class="btn btn-sm btn-light" id="newEventBtn">
|
|
<i class="fas fa-plus"></i> Nuevo Evento
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="calendar"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Panel Lateral -->
|
|
<div class="col-12 col-lg-4">
|
|
<!-- Gráfico de Actividades Fitness -->
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header bg-primary text-white">
|
|
<h6 class="my-0">Actividad Física Últimos 7 Días</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<canvas id="fitnessActivityChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lista de Próximos Eventos -->
|
|
<div class="card shadow">
|
|
<div class="card-header bg-warning text-white">
|
|
<h6 class="my-0">Próximos Eventos</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="upcomingEventsList" class="list-group list-group-flush">
|
|
<!-- Los eventos se cargarán dinámicamente aquí -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scripts -->
|
|
<link href='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.css' rel='stylesheet' />
|
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.js'></script>
|
|
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/locales-all.min.js'></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Datos simulados
|
|
const data = {
|
|
balanceGeneral: 3500,
|
|
upcomingEvents: 5,
|
|
inProgressTasks: 8,
|
|
completedTasks: 15,
|
|
fitnessActivities: [10000, 6000, 6500, 5000, 12000, 5500, 9500],
|
|
events: [
|
|
{
|
|
id: '1',
|
|
title: 'Reunión de equipo',
|
|
start: '2024-10-28T10:00:00',
|
|
end: '2024-10-28T11:30:00',
|
|
backgroundColor: '#0275d8',
|
|
description: 'Reunión semanal de seguimiento'
|
|
},
|
|
{
|
|
id: '2',
|
|
title: 'Entrenamiento',
|
|
start: '2024-10-29T15:00:00',
|
|
end: '2024-10-29T16:00:00',
|
|
backgroundColor: '#5cb85c',
|
|
description: 'Sesión de ejercicio'
|
|
}
|
|
// ... agrega más eventos si lo deseas
|
|
]
|
|
};
|
|
|
|
// Actualizar tarjetas de resumen
|
|
document.getElementById('balanceGeneralAmount').textContent = `Q ${data.balanceGeneral}`;
|
|
document.getElementById('upcomingEventsCount').textContent = data.upcomingEvents;
|
|
document.getElementById('inProgressTasksCount').textContent = data.inProgressTasks;
|
|
document.getElementById('completedTasksCount').textContent = data.completedTasks;
|
|
|
|
// Inicializar FullCalendar
|
|
const calendarEl = document.getElementById('calendar');
|
|
const calendar = new FullCalendar.Calendar(calendarEl, {
|
|
initialView: 'dayGridMonth',
|
|
headerToolbar: {
|
|
left: 'prev,next today',
|
|
center: 'title',
|
|
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
|
},
|
|
locale: 'es',
|
|
events: data.events,
|
|
editable: true,
|
|
selectable: true,
|
|
selectMirror: true,
|
|
dayMaxEvents: true,
|
|
eventClick: function (info) {
|
|
showEventDetails(info.event);
|
|
},
|
|
select: function (info) {
|
|
showEventForm(null, info);
|
|
}
|
|
});
|
|
calendar.render();
|
|
|
|
// Gráfico de Actividades Fitness
|
|
const fitnessActivityCtx = document.getElementById('fitnessActivityChart').getContext('2d');
|
|
new Chart(fitnessActivityCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['Hace 6 días', 'Hace 5 días', 'Hace 4 días', 'Hace 3 días', 'Hace 2 días', 'Ayer', 'Hoy'],
|
|
datasets: [{
|
|
label: 'Pasos diarios',
|
|
data: data.fitnessActivities,
|
|
backgroundColor: 'rgba(0, 123, 255, 0.7)',
|
|
borderColor: 'rgba(0, 123, 255, 1)',
|
|
borderWidth: 1,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Actualizar lista de próximos eventos
|
|
updateUpcomingEventsList();
|
|
|
|
// Manejadores de eventos
|
|
document.getElementById('newEventBtn').addEventListener('click', () => showEventForm());
|
|
|
|
// Funciones auxiliares
|
|
function showEventForm(event = null, selectInfo = null) {
|
|
let eventData = {
|
|
id: '',
|
|
title: '',
|
|
start: '',
|
|
end: '',
|
|
color: '#0275d8',
|
|
description: ''
|
|
};
|
|
|
|
if (event) {
|
|
// Editar evento existente
|
|
eventData = {
|
|
id: event.id,
|
|
title: event.title,
|
|
start: event.startStr,
|
|
end: event.endStr,
|
|
color: event.backgroundColor,
|
|
description: event.extendedProps.description
|
|
};
|
|
} else if (selectInfo) {
|
|
// Nuevo evento con fechas seleccionadas
|
|
eventData.start = selectInfo.startStr;
|
|
eventData.end = selectInfo.endStr;
|
|
}
|
|
|
|
Swal.fire({
|
|
title: event ? 'Editar Evento' : 'Nuevo Evento',
|
|
html: `
|
|
<div class="task-form">
|
|
<input type="text" id="swalEvtTitle" class="swal2-input" placeholder="Título" value="${escapeHtml(eventData.title)}">
|
|
<input type="datetime-local" id="swalEvtStart" class="swal2-input" value="${formatDateTimeInput(eventData.start)}">
|
|
<input type="datetime-local" id="swalEvtEnd" class="swal2-input" value="${formatDateTimeInput(eventData.end)}">
|
|
<div class="task-form__input-group">
|
|
<label for="swalEvtColor">Color:</label>
|
|
<select id="swalEvtColor" class="swal2-input task-form__input">
|
|
<option value="#0275d8" ${eventData.color === '#0275d8' ? 'selected' : ''}>Azul</option>
|
|
<option value="#5cb85c" ${eventData.color === '#5cb85c' ? 'selected' : ''}>Verde</option>
|
|
<option value="#f0ad4e" ${eventData.color === '#f0ad4e' ? 'selected' : ''}>Amarillo</option>
|
|
<option value="#d9534f" ${eventData.color === '#d9534f' ? 'selected' : ''}>Rojo</option>
|
|
</select>
|
|
</div>
|
|
<textarea id="swalEvtDesc" class="swal2-textarea" placeholder="Descripción">${escapeHtml(eventData.description)}</textarea>
|
|
</div>
|
|
`,
|
|
focusConfirm: false,
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Guardar',
|
|
cancelButtonText: 'Cancelar',
|
|
preConfirm: () => {
|
|
const title = document.getElementById('swalEvtTitle').value;
|
|
const start = document.getElementById('swalEvtStart').value;
|
|
const end = document.getElementById('swalEvtEnd').value;
|
|
const color = document.getElementById('swalEvtColor').value;
|
|
const description = document.getElementById('swalEvtDesc').value;
|
|
|
|
if (!title || !start || !end) {
|
|
Swal.showValidationMessage('Por favor completa todos los campos obligatorios');
|
|
return false;
|
|
}
|
|
|
|
return { title, start, end, color, description };
|
|
}
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
const formData = result.value;
|
|
if (event) {
|
|
// Actualizar evento existente
|
|
event.setProp('title', formData.title);
|
|
event.setStart(formData.start);
|
|
event.setEnd(formData.end);
|
|
event.setProp('backgroundColor', formData.color);
|
|
event.setExtendedProp('description', formData.description);
|
|
} else {
|
|
// Crear nuevo evento
|
|
calendar.addEvent({
|
|
id: String(Date.now()), // Generar un ID único
|
|
title: formData.title,
|
|
start: formData.start,
|
|
end: formData.end,
|
|
backgroundColor: formData.color,
|
|
description: formData.description
|
|
});
|
|
}
|
|
updateUpcomingEventsList();
|
|
}
|
|
});
|
|
}
|
|
|
|
function showEventDetails(event) {
|
|
Swal.fire({
|
|
title: event.title,
|
|
html: `
|
|
<div class="task-form">
|
|
<p><strong>Inicio:</strong> ${formatDateTimeDisplay(event.start)}</p>
|
|
<p><strong>Fin:</strong> ${formatDateTimeDisplay(event.end)}</p>
|
|
<p><strong>Descripción:</strong> ${escapeHtml(event.extendedProps.description || 'Sin descripción')}</p>
|
|
</div>
|
|
`,
|
|
showCancelButton: true,
|
|
showDenyButton: true,
|
|
confirmButtonText: 'Editar',
|
|
denyButtonText: 'Eliminar',
|
|
cancelButtonText: 'Cerrar',
|
|
confirmButtonColor: '#0d6efd',
|
|
denyButtonColor: '#dc3545',
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
// Editar evento
|
|
showEventForm(event);
|
|
} else if (result.isDenied) {
|
|
// Eliminar evento
|
|
Swal.fire({
|
|
title: '¿Estás seguro?',
|
|
text: 'Esta acción no se puede deshacer',
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Sí, eliminar',
|
|
cancelButtonText: 'Cancelar',
|
|
confirmButtonColor: '#dc3545',
|
|
}).then((res) => {
|
|
if (res.isConfirmed) {
|
|
event.remove();
|
|
updateUpcomingEventsList();
|
|
Swal.fire('Eliminado', 'El evento ha sido eliminado', 'success');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateUpcomingEventsList() {
|
|
const events = calendar.getEvents();
|
|
const upcomingEvents = events
|
|
.filter(event => event.start >= new Date())
|
|
.sort((a, b) => a.start - b.start)
|
|
.slice(0, 5);
|
|
|
|
const listContainer = document.getElementById('upcomingEventsList');
|
|
listContainer.innerHTML = '';
|
|
|
|
upcomingEvents.forEach(event => {
|
|
const item = document.createElement('div');
|
|
item.className = 'list-group-item';
|
|
item.innerHTML = `
|
|
<div class="d-flex w-100 justify-content-between">
|
|
<h6 class="mb-1">${event.title}</h6>
|
|
<small>${formatDateTimeDisplay(event.start)}</small>
|
|
</div>
|
|
<p class="mb-1">${event.extendedProps.description || ''}</p>
|
|
`;
|
|
listContainer.appendChild(item);
|
|
});
|
|
|
|
// Actualizar el contador de eventos próximos
|
|
document.getElementById('upcomingEventsCount').textContent = upcomingEvents.length;
|
|
}
|
|
|
|
function formatDateTimeInput(dateStr) {
|
|
if (!dateStr) return '';
|
|
const date = new Date(dateStr);
|
|
const isoString = date.toISOString();
|
|
return isoString.slice(0, 16);
|
|
}
|
|
|
|
function formatDateTimeDisplay(date) {
|
|
if (!date) return '';
|
|
const options = { dateStyle: 'medium', timeStyle: 'short' };
|
|
return new Intl.DateTimeFormat('es-ES', options).format(new Date(date));
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<?php include __DIR__ . "/../layouts/footer.php"; ?>
|