Last active
August 5, 2025 00:04
-
-
Save wuilliam321/a7a1acbe1eb200cbbc97a8265ed91bc5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="es"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Gestor de Días Libres Rotativo</title> | |
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
<style> | |
body { | |
font-family: 'Inter', sans-serif; | |
background-color: #f0f2f5; | |
} | |
.calendar-grid { | |
display: grid; | |
grid-template-columns: repeat(7, minmax(0, 1fr)); | |
gap: 4px; | |
} | |
.calendar-day { | |
min-height: 120px; | |
} | |
.day-number { | |
font-size: 0.8rem; | |
} | |
.employee-badge { | |
display: block; | |
padding: 2px 6px; | |
border-radius: 12px; | |
font-size: 0.75rem; | |
margin-top: 4px; | |
white-space: nowrap; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
} | |
.modal { | |
display: none; | |
position: fixed; | |
z-index: 50; | |
left: 0; | |
top: 0; | |
width: 100%; | |
height: 100%; | |
overflow: auto; | |
background-color: rgba(0,0,0,0.5); | |
align-items: center; | |
justify-content: center; | |
transition: opacity 0.3s ease; | |
} | |
.modal-content { | |
background-color: #fff; | |
padding: 24px; | |
border-radius: 8px; | |
width: 90%; | |
max-width: 500px; | |
transform: scale(0.95); | |
transition: transform 0.3s ease; | |
} | |
.modal.flex { | |
display: flex; | |
} | |
.modal.flex .modal-content { | |
transform: scale(1); | |
} | |
.weekend { | |
background-color: #f8fafc; | |
} | |
.employee-assign-item { | |
padding: 8px; | |
border-radius: 6px; | |
transition: background-color 0.2s; | |
} | |
.employee-assign-item:hover { | |
background-color: #f0f2f5; | |
} | |
</style> | |
<link rel="preconnect" href="https://fonts.googleapis.com"> | |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
</head> | |
<body class="text-gray-800"> | |
<div class="container mx-auto p-4 md:p-6"> | |
<!-- Header --> | |
<header class="flex flex-col md:flex-row justify-between items-center mb-6 pb-4 border-b border-gray-200"> | |
<h1 id="current-month-year" class="text-2xl md:text-3xl font-bold text-gray-700"></h1> | |
<div class="flex items-center space-x-2 mt-4 md:mt-0"> | |
<button id="prev-month-btn" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition"> | |
<i class="fas fa-chevron-left"></i> | |
</button> | |
<button id="next-month-btn" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition"> | |
<i class="fas fa-chevron-right"></i> | |
</button> | |
<button id="today-btn" class="px-4 py-2 bg-indigo-600 text-white rounded-md shadow-sm hover:bg-indigo-700 transition">Hoy</button> | |
</div> | |
</header> | |
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6"> | |
<!-- Left Panel: Employees & Config --> | |
<div class="lg:col-span-3 space-y-6"> | |
<!-- Employee Management --> | |
<div class="bg-white p-4 rounded-lg shadow"> | |
<h2 class="text-lg font-semibold mb-4">Empleados</h2> | |
<div id="employee-list" class="space-y-3 mb-4"></div> | |
<button id="add-employee-btn" class="w-full py-2 px-4 bg-green-500 text-white rounded-md hover:bg-green-600 transition flex items-center justify-center"> | |
<i class="fas fa-plus mr-2"></i> Agregar Empleado | |
</button> | |
</div> | |
<!-- Configuration --> | |
<div class="bg-white p-4 rounded-lg shadow"> | |
<h2 class="text-lg font-semibold mb-4">Configuración y Acciones</h2> | |
<div class="space-y-4"> | |
<div> | |
<label for="start-day-select" class="block text-sm font-medium text-gray-700">Inicio de semana</label> | |
<select id="start-day-select" class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"> | |
<option value="1">Lunes</option> | |
<option value="0">Domingo</option> | |
</select> | |
</div> | |
<button id="apply-rotation-btn" class="w-full py-2 px-4 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition flex items-center justify-center"> | |
<i class="fas fa-sync-alt mr-2"></i> Aplicar Rotación Automática | |
</button> | |
<button id="clear-month-btn" class="w-full py-2 px-4 bg-red-500 text-white rounded-md hover:bg-red-600 transition flex items-center justify-center"> | |
<i class="fas fa-trash-alt mr-2"></i> Limpiar Mes Actual | |
</button> | |
</div> | |
</div> | |
<!-- Data Persistence --> | |
<div class="bg-white p-4 rounded-lg shadow"> | |
<h2 class="text-lg font-semibold mb-4">Datos</h2> | |
<div class="space-y-2"> | |
<button id="save-json-btn" class="w-full py-2 px-4 bg-gray-700 text-white rounded-md hover:bg-gray-800 transition flex items-center justify-center"> | |
<i class="fas fa-save mr-2"></i> Guardar JSON | |
</button> | |
<label class="w-full py-2 px-4 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 transition flex items-center justify-center cursor-pointer"> | |
<i class="fas fa-upload mr-2"></i> Cargar JSON | |
<input type="file" id="load-json-input" class="hidden" accept=".json"> | |
</label> | |
</div> | |
</div> | |
<!-- Statistics --> | |
<div class="bg-white p-4 rounded-lg shadow"> | |
<h2 class="text-lg font-semibold mb-4">Estadísticas del Mes</h2> | |
<div id="stats-panel" class="space-y-2 text-sm"> | |
<p class="text-gray-500">No hay datos aún.</p> | |
</div> | |
</div> | |
</div> | |
<!-- Right Panel: Calendar --> | |
<div class="lg:col-span-9 bg-white p-4 rounded-lg shadow"> | |
<div id="calendar-header" class="calendar-grid mb-2"></div> | |
<div id="calendar-grid" class="calendar-grid"></div> | |
</div> | |
</div> | |
</div> | |
<!-- Modals --> | |
<div id="add-employee-modal" class="modal"> | |
<div class="modal-content"> | |
<h3 class="text-xl font-semibold mb-4">Agregar Nuevo Empleado</h3> | |
<input type="text" id="new-employee-name" class="w-full border border-gray-300 p-2 rounded-md" placeholder="Nombre del empleado"> | |
<div class="flex justify-end space-x-3 mt-6"> | |
<button id="cancel-add-employee" class="px-4 py-2 bg-gray-200 rounded-md hover:bg-gray-300">Cancelar</button> | |
<button id="confirm-add-employee" class="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600">Agregar</button> | |
</div> | |
</div> | |
</div> | |
<div id="assign-day-modal" class="modal"> | |
<div class="modal-content"> | |
<h3 class="text-xl font-semibold mb-4">Gestionar Día Libre</h3> | |
<p class="mb-4">Día: <strong id="modal-date-display"></strong></p> | |
<div id="employee-list-for-day" class="space-y-2 max-h-60 overflow-y-auto"> | |
<!-- Employee checklist will be injected here --> | |
</div> | |
<div class="flex justify-end space-x-3 mt-6"> | |
<button id="cancel-assign-day" class="px-4 py-2 bg-gray-200 rounded-md hover:bg-gray-300">Cancelar</button> | |
<button id="confirm-assign-day" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Guardar Cambios</button> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
// STATE MANAGEMENT | |
let state = { | |
employees: [ | |
{ id: 1, name: 'RR', active: true, color: '#ef4444' }, | |
{ id: 2, name: 'MM', active: true, color: '#3b82f6' }, | |
{ id: 3, name: 'GG', active: true, color: '#22c55e' }, | |
{ id: 4, name: 'OO', active: false, color: '#f97316' }, | |
], | |
// NEW DATA STRUCTURE: A date can have an array of employee IDs | |
daysOff: { | |
// Example: '2023-10-26': [1, 3] | |
}, | |
config: { | |
startOfWeek: 1, // 0 for Sunday, 1 for Monday | |
}, | |
currentDate: new Date(), | |
}; | |
// DOM ELEMENTS | |
const currentMonthYearEl = document.getElementById('current-month-year'); | |
const prevMonthBtn = document.getElementById('prev-month-btn'); | |
const nextMonthBtn = document.getElementById('next-month-btn'); | |
const todayBtn = document.getElementById('today-btn'); | |
const calendarGridEl = document.getElementById('calendar-grid'); | |
const calendarHeaderEl = document.getElementById('calendar-header'); | |
const employeeListEl = document.getElementById('employee-list'); | |
const addEmployeeBtn = document.getElementById('add-employee-btn'); | |
const addEmployeeModal = document.getElementById('add-employee-modal'); | |
const newEmployeeNameInput = document.getElementById('new-employee-name'); | |
const cancelAddEmployeeBtn = document.getElementById('cancel-add-employee'); | |
const confirmAddEmployeeBtn = document.getElementById('confirm-add-employee'); | |
const assignDayModal = document.getElementById('assign-day-modal'); | |
const modalDateDisplay = document.getElementById('modal-date-display'); | |
const employeeListForDay = document.getElementById('employee-list-for-day'); | |
const cancelAssignDayBtn = document.getElementById('cancel-assign-day'); | |
const confirmAssignDayBtn = document.getElementById('confirm-assign-day'); | |
const startDaySelect = document.getElementById('start-day-select'); | |
const applyRotationBtn = document.getElementById('apply-rotation-btn'); | |
const clearMonthBtn = document.getElementById('clear-month-btn'); | |
const saveJsonBtn = document.getElementById('save-json-btn'); | |
const loadJsonInput = document.getElementById('load-json-input'); | |
const statsPanelEl = document.getElementById('stats-panel'); | |
let selectedDate = null; | |
// --- CORE LOGIC --- | |
function renderAll() { | |
renderCalendar(); | |
renderEmployeeList(); | |
updateStats(); | |
} | |
function renderCalendar() { | |
calendarGridEl.innerHTML = ''; | |
const date = state.currentDate; | |
const year = date.getFullYear(); | |
const month = date.getMonth(); | |
currentMonthYearEl.textContent = `${date.toLocaleString('es-ES', { month: 'long' }).replace(/^\w/, c => c.toUpperCase())} ${year}`; | |
renderCalendarHeader(); | |
const firstDayOfMonth = new Date(year, month, 1); | |
const daysInMonth = new Date(year, month + 1, 0).getDate(); | |
let firstDayOfWeek = firstDayOfMonth.getDay(); | |
if (state.config.startOfWeek === 1) { | |
firstDayOfWeek = (firstDayOfWeek === 0) ? 6 : firstDayOfWeek - 1; | |
} | |
for (let i = 0; i < firstDayOfWeek; i++) { | |
calendarGridEl.appendChild(document.createElement('div')); | |
} | |
for (let day = 1; day <= daysInMonth; day++) { | |
const dayEl = document.createElement('div'); | |
const currentDate = new Date(year, month, day); | |
const dateString = toISODateString(currentDate); | |
dayEl.classList.add('calendar-day', 'p-2', 'border', 'border-gray-200', 'rounded-sm', 'transition', 'hover:bg-indigo-50', 'cursor-pointer', 'flex', 'flex-col'); | |
const dayOfWeek = currentDate.getDay(); | |
if ((state.config.startOfWeek === 1 && (dayOfWeek === 6 || dayOfWeek === 0)) || (state.config.startOfWeek === 0 && (dayOfWeek === 0 || dayOfWeek === 6))) { | |
dayEl.classList.add('weekend'); | |
} | |
const dayNumberEl = document.createElement('span'); | |
dayNumberEl.classList.add('day-number', 'font-semibold', 'text-gray-600'); | |
dayNumberEl.textContent = day; | |
dayEl.appendChild(dayNumberEl); | |
// RENDER MULTIPLE EMPLOYEES | |
const employeeIds = state.daysOff[dateString]; | |
if (employeeIds && employeeIds.length > 0) { | |
employeeIds.forEach(employeeId => { | |
const employee = state.employees.find(e => e.id === employeeId); | |
if (employee) { | |
const dayOffEl = document.createElement('div'); | |
dayOffEl.textContent = employee.name; | |
dayOffEl.classList.add('employee-badge', 'text-white'); | |
dayOffEl.style.backgroundColor = employee.color; | |
dayEl.appendChild(dayOffEl); | |
} | |
}); | |
} | |
dayEl.addEventListener('click', () => openAssignDayModal(dateString)); | |
calendarGridEl.appendChild(dayEl); | |
} | |
} | |
function renderCalendarHeader() { | |
calendarHeaderEl.innerHTML = ''; | |
const days = state.config.startOfWeek === 1 | |
? ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'] | |
: ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb']; | |
days.forEach(day => { | |
const dayHeaderEl = document.createElement('div'); | |
dayHeaderEl.textContent = day; | |
dayHeaderEl.classList.add('text-center', 'font-semibold', 'text-sm', 'text-gray-600'); | |
calendarHeaderEl.appendChild(dayHeaderEl); | |
}); | |
} | |
function renderEmployeeList() { | |
employeeListEl.innerHTML = ''; | |
state.employees.forEach(employee => { | |
const employeeEl = document.createElement('div'); | |
employeeEl.classList.add('flex', 'items-center', 'justify-between', 'p-2', 'rounded-md'); | |
employeeEl.innerHTML = ` | |
<div class="flex items-center"> | |
<span class="w-4 h-4 rounded-full mr-3" style="background-color: ${employee.color};"></span> | |
<span class="font-medium ${!employee.active ? 'line-through text-gray-400' : ''}">${employee.name}</span> | |
</div> | |
<div class="flex items-center space-x-3"> | |
<label class="flex items-center cursor-pointer"> | |
<input type="checkbox" data-id="${employee.id}" class="form-checkbox h-5 w-5 text-indigo-600 rounded toggle-active-btn" ${employee.active ? 'checked' : ''}> | |
</label> | |
<button data-id="${employee.id}" class="text-gray-400 hover:text-red-500 delete-employee-btn"> | |
<i class="fas fa-trash-alt"></i> | |
</button> | |
</div> | |
`; | |
employeeListEl.appendChild(employeeEl); | |
}); | |
document.querySelectorAll('.toggle-active-btn').forEach(checkbox => { | |
checkbox.addEventListener('change', (e) => { | |
const employeeId = parseInt(e.target.dataset.id); | |
const employee = state.employees.find(emp => emp.id === employeeId); | |
if (employee) { | |
employee.active = e.target.checked; | |
renderAll(); | |
} | |
}); | |
}); | |
document.querySelectorAll('.delete-employee-btn').forEach(button => { | |
button.addEventListener('click', (e) => { | |
const employeeId = parseInt(e.currentTarget.dataset.id); | |
if (confirm(`¿Seguro que quieres eliminar a este empleado? Esto borrará también sus días libres asignados.`)) { | |
state.employees = state.employees.filter(emp => emp.id !== employeeId); | |
// Remove employee from all daysOff arrays | |
for (const date in state.daysOff) { | |
state.daysOff[date] = state.daysOff[date].filter(id => id !== employeeId); | |
if (state.daysOff[date].length === 0) { | |
delete state.daysOff[date]; | |
} | |
} | |
renderAll(); | |
} | |
}); | |
}); | |
} | |
function updateStats() { | |
statsPanelEl.innerHTML = ''; | |
const activeEmployees = state.employees.filter(e => e.active); | |
if(activeEmployees.length === 0) { | |
statsPanelEl.innerHTML = '<p class="text-gray-500">No hay empleados activos.</p>'; | |
return; | |
} | |
const year = state.currentDate.getFullYear(); | |
const month = state.currentDate.getMonth(); | |
const monthDaysOff = {}; | |
activeEmployees.forEach(emp => monthDaysOff[emp.id] = 0); | |
for (const dateString in state.daysOff) { | |
const date = new Date(dateString); | |
if (date.getFullYear() === year && date.getMonth() === month) { | |
const empIds = state.daysOff[dateString]; | |
if (empIds) { | |
empIds.forEach(empId => { | |
if (monthDaysOff.hasOwnProperty(empId)) { | |
monthDaysOff[empId]++; | |
} | |
}); | |
} | |
} | |
} | |
activeEmployees.forEach(emp => { | |
const statEl = document.createElement('div'); | |
statEl.classList.add('flex', 'justify-between', 'items-center'); | |
statEl.innerHTML = ` | |
<div class="flex items-center"> | |
<span class="w-3 h-3 rounded-full mr-2" style="background-color: ${emp.color};"></span> | |
<span>${emp.name}</span> | |
</div> | |
<span class="font-bold">${monthDaysOff[emp.id]} días</span> | |
`; | |
statsPanelEl.appendChild(statEl); | |
}); | |
} | |
function applyAutomaticRotation() { | |
const year = state.currentDate.getFullYear(); | |
const month = state.currentDate.getMonth(); | |
const daysInMonth = new Date(year, month + 1, 0).getDate(); | |
const activeEmployees = state.employees.filter(e => e.active); | |
if (activeEmployees.length === 0) { | |
alert("No hay empleados activos para rotar."); | |
return; | |
} | |
// clearCurrentMonth(false); // borra sin confirmar | |
// Elimina solo los días del mes actual hacia adelante | |
for (const dateStr of Object.keys(state.daysOff)) { | |
const date = new Date(dateStr); | |
if ( | |
date.getFullYear() === year && | |
date.getMonth() === month && | |
date >= state.currentDate | |
) { | |
delete state.daysOff[dateStr]; | |
} | |
} | |
// Agrupar por semanas (de lunes a domingo) | |
const weeks = []; | |
let weekStart = new Date(year, month, 1); | |
// Avanzar al primer lunes | |
while (weekStart.getDay() !== 1) { | |
weekStart.setDate(weekStart.getDate() + 1); | |
} | |
while (weekStart.getMonth() === month) { | |
const week = []; | |
let d = new Date(weekStart); | |
for (let i = 0; i < 7 && d.getMonth() === month; i++) { | |
week.push(new Date(d)); | |
d.setDate(d.getDate() + 1); | |
} | |
weeks.push(week); | |
weekStart.setDate(weekStart.getDate() + 7); | |
} | |
// Map: empId -> último día de la semana (0=lun...6=dom) | |
const lastDayMap = new Map(); | |
for (const emp of activeEmployees) { | |
const last = Object.keys(state.daysOff) | |
.filter(dateStr => state.daysOff[dateStr].includes(emp.id)) | |
.map(d => new Date(d)) | |
.sort((a, b) => b - a)[0]; | |
if (last && last.getDay() >= 1 && last.getDay() <= 7) { | |
lastDayMap.set(emp.id, last.getDay()); // 0=lun...6=dom | |
} else { | |
lastDayMap.set(emp.id, -1); // nunca tuvo | |
} | |
} | |
// Rotar por semana | |
for (let w = 0; w < weeks.length; w++) { | |
const usedDays = new Set(); | |
for (const emp of activeEmployees) { | |
let lastDay = lastDayMap.get(emp.id); | |
let nextDay = (lastDay + 1) % 7; // avanza un día laboral | |
// Buscar el primer día libre disponible (idealmente el siguiente) | |
for (let offset = 0; offset < 7; offset++) { | |
const tryDay = (nextDay + offset) % 7; | |
if (!usedDays.has(tryDay)) { | |
const date = weeks[w][tryDay]; | |
if (date) { | |
const iso = toISODateString(date); | |
if (!state.daysOff[iso]) state.daysOff[iso] = []; | |
state.daysOff[iso].push(emp.id); | |
usedDays.add(tryDay); | |
lastDayMap.set(emp.id, tryDay); | |
break; | |
} | |
} | |
} | |
} | |
} | |
renderAll(); | |
} | |
function clearCurrentMonth(confirmFirst = true) { | |
if (confirmFirst && !confirm('¿Seguro que quieres borrar todas las asignaciones del mes actual?')) { | |
return; | |
} | |
const year = state.currentDate.getFullYear(); | |
const month = state.currentDate.getMonth(); | |
const daysInMonth = new Date(year, month + 1, 0).getDate(); | |
for (let day = 1; day <= daysInMonth; day++) { | |
const dateString = toISODateString(new Date(year, month, day)); | |
if (state.daysOff[dateString]) { | |
delete state.daysOff[dateString]; | |
} | |
} | |
renderAll(); | |
} | |
// --- MODAL HANDLING --- | |
function openAddEmployeeModal() { | |
newEmployeeNameInput.value = ''; | |
addEmployeeModal.classList.add('flex'); | |
newEmployeeNameInput.focus(); | |
} | |
function closeAddEmployeeModal() { | |
addEmployeeModal.classList.remove('flex'); | |
} | |
function confirmAddEmployee() { | |
const name = newEmployeeNameInput.value.trim(); | |
if (name) { | |
const newId = state.employees.length > 0 ? Math.max(...state.employees.map(e => e.id)) + 1 : 1; | |
const newColor = `hsl(${Math.random() * 360}, 70%, 50%)`; | |
state.employees.push({ id: newId, name, active: true, color: newColor }); | |
renderAll(); | |
closeAddEmployeeModal(); | |
} else { | |
alert('El nombre del empleado no puede estar vacío.'); | |
} | |
} | |
function openAssignDayModal(dateString) { | |
selectedDate = dateString; | |
modalDateDisplay.textContent = new Date(dateString).toLocaleDateString('es-ES', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); | |
employeeListForDay.innerHTML = ''; | |
const assignedEmployeeIds = state.daysOff[dateString] || []; | |
const activeEmployees = state.employees.filter(e => e.active); | |
if (activeEmployees.length > 0) { | |
activeEmployees.forEach(employee => { | |
const isChecked = assignedEmployeeIds.includes(employee.id); | |
const item = document.createElement('label'); | |
item.classList.add('employee-assign-item', 'flex', 'items-center', 'cursor-pointer'); | |
item.innerHTML = ` | |
<input type="checkbox" value="${employee.id}" class="form-checkbox h-5 w-5 text-indigo-600 rounded mr-3" ${isChecked ? 'checked' : ''}> | |
<span class="w-4 h-4 rounded-full mr-3" style="background-color: ${employee.color};"></span> | |
<span>${employee.name}</span> | |
`; | |
employeeListForDay.appendChild(item); | |
}); | |
} else { | |
employeeListForDay.innerHTML = `<p class="text-gray-500">No hay empleados activos para asignar.</p>`; | |
} | |
assignDayModal.classList.add('flex'); | |
} | |
function closeAssignDayModal() { | |
assignDayModal.classList.remove('flex'); | |
selectedDate = null; | |
} | |
function confirmAssignDay() { | |
if (!selectedDate) return; | |
const selectedIds = []; | |
const checkboxes = employeeListForDay.querySelectorAll('input[type="checkbox"]:checked'); | |
checkboxes.forEach(checkbox => { | |
selectedIds.push(parseInt(checkbox.value)); | |
}); | |
if (selectedIds.length > 0) { | |
state.daysOff[selectedDate] = selectedIds; | |
} else { | |
// If no one is selected, remove the entry for that day | |
delete state.daysOff[selectedDate]; | |
} | |
renderAll(); | |
closeAssignDayModal(); | |
} | |
// --- DATA PERSISTENCE --- | |
function saveStateToJSON() { | |
const dataStr = JSON.stringify(state, null, 2); | |
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); | |
const exportFileDefaultName = `dias_libres_${toISODateString(new Date())}.json`; | |
const linkElement = document.createElement('a'); | |
linkElement.setAttribute('href', dataUri); | |
linkElement.setAttribute('download', exportFileDefaultName); | |
linkElement.click(); | |
} | |
function loadStateFromJSON(event) { | |
const file = event.target.files[0]; | |
if (!file) return; | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
try { | |
const loadedState = JSON.parse(e.target.result); | |
if (loadedState.employees && loadedState.daysOff && loadedState.config) { | |
loadedState.currentDate = new Date(loadedState.currentDate); | |
state = loadedState; | |
startDaySelect.value = state.config.startOfWeek; | |
renderAll(); | |
alert('Datos cargados correctamente.'); | |
} else { | |
throw new Error('Formato de archivo inválido.'); | |
} | |
} catch (error) { | |
alert('Error al cargar el archivo: ' + error.message); | |
} | |
}; | |
reader.readAsText(file); | |
event.target.value = null; | |
} | |
// --- HELPERS --- | |
function toISODateString(date) { | |
return date.toISOString().split('T')[0]; | |
} | |
// --- EVENT LISTENERS --- | |
prevMonthBtn.addEventListener('click', () => { | |
state.currentDate.setMonth(state.currentDate.getMonth() - 1); | |
renderAll(); | |
}); | |
nextMonthBtn.addEventListener('click', () => { | |
state.currentDate.setMonth(state.currentDate.getMonth() + 1); | |
renderAll(); | |
}); | |
todayBtn.addEventListener('click', () => { | |
state.currentDate = new Date(); | |
renderAll(); | |
}); | |
addEmployeeBtn.addEventListener('click', openAddEmployeeModal); | |
cancelAddEmployeeBtn.addEventListener('click', closeAddEmployeeModal); | |
confirmAddEmployeeBtn.addEventListener('click', confirmAddEmployee); | |
cancelAssignDayBtn.addEventListener('click', closeAssignDayModal); | |
confirmAssignDayBtn.addEventListener('click', confirmAssignDay); | |
startDaySelect.addEventListener('change', (e) => { | |
state.config.startOfWeek = parseInt(e.target.value); | |
renderAll(); | |
}); | |
applyRotationBtn.addEventListener('click', () => { | |
if (confirm('Esto borrará las asignaciones del mes actual y aplicará una nueva rotación automática (un empleado por día laboral). ¿Continuar?')) { | |
applyAutomaticRotation(); | |
} | |
}); | |
clearMonthBtn.addEventListener('click', () => clearCurrentMonth(true)); | |
saveJsonBtn.addEventListener('click', saveStateToJSON); | |
loadJsonInput.addEventListener('change', loadStateFromJSON); | |
window.addEventListener('keydown', (e) => { | |
if (e.key === 'Escape') { | |
closeAddEmployeeModal(); | |
closeAssignDayModal(); | |
} | |
}); | |
addEmployeeModal.addEventListener('click', (e) => { | |
if (e.target === addEmployeeModal) closeAddEmployeeModal(); | |
}); | |
assignDayModal.addEventListener('click', (e) => { | |
if (e.target === assignDayModal) closeAssignDayModal(); | |
}); | |
// Initial render | |
startDaySelect.value = state.config.startOfWeek; | |
renderAll(); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment