Skip to content

Instantly share code, notes, and snippets.

@wuilliam321
Last active August 5, 2025 00:04
Show Gist options
  • Save wuilliam321/a7a1acbe1eb200cbbc97a8265ed91bc5 to your computer and use it in GitHub Desktop.
Save wuilliam321/a7a1acbe1eb200cbbc97a8265ed91bc5 to your computer and use it in GitHub Desktop.
<!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