${task.duration} minutes • ${task.type}
`;
routineTimeline.appendChild(itemDiv);
});
};
const renderEnergyLevels = () => {
energyLevelsContainer.innerHTML = '';
const startHour = parseInt(workStartTimeEl.value.split(':')[0]);
const endHour = parseInt(workEndTimeEl.value.split(':')[0]);
for (let hour = startHour; hour < endHour; hour++) {
const timeLabel = `${formatTime(hour + ':00')} - ${formatTime((hour + 1) + ':00')}`;
const row = document.createElement('div');
row.className = 'grid grid-cols-3 gap-4 items-center';
row.innerHTML = `
`;
energyLevelsContainer.appendChild(row);
}
};
const addTaskRow = (task = {}) => {
const newRow = document.createElement('div');
newRow.className = 'task-row p-4 border border-gray-200 rounded-lg bg-gray-50 grid grid-cols-1 md:grid-cols-12 gap-4 items-center';
newRow.innerHTML = `
`;
tasksContainer.appendChild(newRow);
newRow.querySelector('.remove-task-btn').addEventListener('click', () => newRow.remove());
};
// --- DATA & LOGIC ---
const generateRoutine = () => {
// 1. Get all inputs
const startHour = parseInt(workStartTimeEl.value.split(':')[0]);
const endHour = parseInt(workEndTimeEl.value.split(':')[0]);
state.energyMap = {};
document.querySelectorAll('.energy-level-select').forEach(sel => {
state.energyMap[sel.dataset.hour] = sel.value;
});
state.tasks = [];
document.querySelectorAll('.task-row').forEach(row => {
const name = row.querySelector('.task-name').value.trim();
const duration = parseInt(row.querySelector('.task-duration').value);
const type = row.querySelector('.task-type').value;
if (name && duration) state.tasks.push({ name, duration, type });
});
if (state.tasks.length === 0) {
alert('Please add at least one task.');
return;
}
// 2. Create time slots
const timeSlots = [];
for (let h = startHour; h < endHour; h++) {
for (let m = 0; m < 60; m += 15) {
timeSlots.push({
time: `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`,
energy: state.energyMap[h],
task: null
});
}
}
// 3. Sort tasks by cognitive load
const typePriority = { 'Deep Work': 3, 'Shallow Work': 2, 'Break': 1 };
const sortedTasks = [...state.tasks].sort((a, b) => typePriority[b.type] - typePriority[a.type]);
// 4. Allocate tasks to time slots
let unscheduledTasks = [];
for (const task of sortedTasks) {
let scheduled = false;
const requiredSlots = task.duration / 15;
// Try to find the best-fit slot
for (let i = 0; i <= timeSlots.length - requiredSlots; i++) {
const energyLevel = timeSlots[i].energy;
const canFit = timeSlots.slice(i, i + requiredSlots).every(slot => !slot.task);
// Matching logic: Deep work in High energy, Shallow in Medium/Low
const energyMatch = (task.type === 'Deep Work' && energyLevel === 'High') ||
(task.type === 'Shallow Work' && (energyLevel === 'Medium' || energyLevel === 'Low')) ||
(task.type === 'Break');
if (canFit && energyMatch) {
for (let j = 0; j < requiredSlots; j++) {
timeSlots[i + j].task = task;
}
scheduled = true;
break;
}
}
if (!scheduled) unscheduledTasks.push(task.name);
}
// 5. Consolidate schedule
state.routine = [];
let currentTask = null;
for (const slot of timeSlots) {
if (slot.task && slot.task !== currentTask?.task) {
currentTask = { task: slot.task, startTime: slot.time };
} else if (!slot.task && currentTask) {
const endTime = slot.time;
state.routine.push({ ...currentTask.task, startTime: currentTask.startTime, endTime });
currentTask = null;
}
}
if (currentTask) { // Add last task if it goes to the end of the day
const endTime = `${String(endHour).padStart(2, '0')}:00`;
state.routine.push({ ...currentTask.task, startTime: currentTask.startTime, endTime });
}
if(unscheduledTasks.length > 0) {
alert(`Routine generated, but could not schedule the following tasks: ${unscheduledTasks.join(', ')}`);
} else {
alert('Routine generated successfully!');
}
renderRoutine();
switchTab('routine');
};
// --- HELPERS & NAVIGATION ---
const formatTime = (timeStr) => {
const [h, m] = timeStr.split(':');
return new Date(0, 0, 0, h, m).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
};
const formatDuration = (mins) => `${Math.floor(mins / 60)}h ${mins % 60}m`;
const switchTab = (tabName) => {
state.activeTab = tabName;
Object.values(tabs).forEach(tab => {
tab.btn.classList.remove('active');
tab.panel.classList.add('hidden');
});
tabs[tabName].btn.classList.add('active');
tabs[tabName].panel.classList.remove('hidden');
updateNavButtons();
};
const updateNavButtons = () => {
const tabKeys = Object.keys(tabs);
const currentIndex = tabKeys.indexOf(state.activeTab);
navButtons.prev.disabled = currentIndex === 0;
navButtons.next.disabled = currentIndex === tabKeys.length - 1;
};
const handlePdfDownload = () => {
const { jsPDF } = window.jspdf;
const pdfContent = document.getElementById('pdf-export-area');
html2canvas(pdfContent, { scale: 2 }).then(canvas => {
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({ orientation: 'portrait', unit: 'pt', format: 'a4' });
const margin = 40;
const pdfWidth = pdf.internal.pageSize.getWidth() - (margin * 2);
const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
pdf.addImage(imgData, 'PNG', margin, margin, pdfWidth, pdfHeight);
pdf.save('daily-work-routine.pdf');
});
};
// --- EVENT LISTENERS ---
Object.keys(tabs).forEach(tabName => tabs[tabName].btn.addEventListener('click', () => switchTab(tabName)));
navButtons.next.addEventListener('click', () => {
const tabKeys = Object.keys(tabs);
const currentIndex = tabKeys.indexOf(state.activeTab);
if (currentIndex < tabKeys.length - 1) switchTab(tabKeys[currentIndex + 1]);
});
navButtons.prev.addEventListener('click', () => {
const tabKeys = Object.keys(tabs);
const currentIndex = tabKeys.indexOf(state.activeTab);
if (currentIndex > 0) switchTab(tabKeys[currentIndex - 1]);
});
[workStartTimeEl, workEndTimeEl].forEach(el => el.addEventListener('change', renderEnergyLevels));
addTaskBtn.addEventListener('click', () => addTaskRow());
generateRoutineBtn.addEventListener('click', generateRoutine);
downloadPdfBtn.addEventListener('click', handlePdfDownload);
// --- INITIALIZATION ---
const init = () => {
renderEnergyLevels();
addTaskRow({ name: 'Write project brief', duration: 90, type: 'Deep Work' });
addTaskRow({ name: 'Team stand-up', duration: 30, type: 'Shallow Work' });
addTaskRow({ name: 'Lunch', duration: 60, type: 'Break' });
addTaskRow({ name: 'Clear email inbox', duration: 45, type: 'Shallow Work' });
addTaskRow({ name: 'Code new feature', duration: 120, type: 'Deep Work' });
generateRoutine();
switchTab('routine');
};
init();
});