# app.py
from flask import Flask, request, jsonify, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from datetime import date, datetime, timedelta
import os
from apscheduler.executors.pool import ThreadPoolExecutor, ProcessPoolExecutor
from apscheduler.schedulers.background import BackgroundScheduler
import logging
import requests

app = Flask(__name__)
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'app.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)
migrate = Migrate(app, db)


#####################################################################* ARCHIVING TASKS #####################################################################
def archive_tasks():
    with app.app_context():
        # Check last archive date
        last_archived_date = ArchiveInfo.get_last_archive()
        today = date.today()

        # If the last archive was not today, archive and update the record
        if last_archived_date is None or last_archived_date != today:
            # Perform the archive
            tasks = Task.query.filter(Task.status == 'finished')
            for task in tasks:
                task.status = 'archived'
                if task.completion_date == None:
                    task.completion_date = date.today() - timedelta(days=1)
            db.session.commit()

            # Update the archive info in the database
            ArchiveInfo.update_last_archive(today)
            logging.info(f"Archived tasks for {today}")
        

# Configure logging
logging.basicConfig(level=logging.INFO)
logging.getLogger('apscheduler').setLevel(logging.DEBUG)

# Initialize the scheduler
executors = {
    'default': ThreadPoolExecutor(20),
    'processpool': ProcessPoolExecutor(5)
}

job_defaults = {
    'coalesce': True, 
    'max_instances': 1
}

scheduler = BackgroundScheduler(executors=executors, job_defaults=job_defaults)
scheduler.add_job(archive_tasks, 'cron', hour=0, minute=1)  # Runs at 12:01 AM
# Periodic job to check every 15 minutes if archiving was missed
scheduler.add_job(archive_tasks, 'interval', minutes=15)
scheduler.start()

#####################################################################* DB CLASSES #####################################################################
class ArchiveInfo(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    last_archive = db.Column(db.Date, nullable=False)

    @staticmethod
    def get_last_archive():
        archive_info = ArchiveInfo.query.first()
        if archive_info:
            return archive_info.last_archive
        return None

    @staticmethod
    def update_last_archive(new_archive_date):
        archive_info = ArchiveInfo.query.first()
        if not archive_info:
            archive_info = ArchiveInfo(last_archive=new_archive_date)
            db.session.add(archive_info)
        else:
            archive_info.last_archive = new_archive_date
        db.session.commit()

class Task(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    status = db.Column(db.String(20), nullable=False)
    completion_date = db.Column(db.Date)
    column = db.Column(db.String(20), default='To Do')
    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'status': self.status,
            'completion_date': self.completion_date.isoformat() if self.completion_date else None,
            'column': self.column
        }

class Habit(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    #days that the habit is available
    days = db.Column(db.String(100))  # Store as comma-separated string
    #list of completed dates
    dates = db.relationship('HabitDate', backref='habit', lazy=True)

class HabitDate(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    habit_id = db.Column(db.Integer, db.ForeignKey('habit.id'), nullable=False)
    date = db.Column(db.Date, nullable=False)

class JournalEntry(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text, nullable=False)
    date = db.Column(db.Date, nullable=False)
    ai_comment = db.Column(db.Text)

#####################################################################* BASE ROUTES #####################################################################
@app.route('/')
def index():
    return render_template('index.html')

@app.route('/yearly-grid')
def yearly_grid():
    return render_template('yearly_grid.html')

#####################################################################* API routes #####################################################################
############################################## TASKS ##############################################
@app.route('/api/tasks', methods=['GET', 'POST'])
def handle_tasks():
    '''
    POST request: Creates a new task in the database and returns its details.
    GET  request: Retrieves and returns all tasks from the database as a list in JSON format.
    '''
    if request.method == 'POST':
        data = request.json
        new_task = Task(
            title=data['title'],
            status=data['status'],
            column=data.get('column', 'To Do')
        )
        db.session.add(new_task)
        db.session.commit()
        return jsonify(id=new_task.id, title=new_task.title, status=new_task.status, column=new_task.column)
    else:
        tasks = Task.query.all()
        return jsonify([{
            'id': task.id,
            'title': task.title,
            'status': task.status,
            'completion_date': str(task.completion_date) if task.completion_date else None,
            'column': task.column
        } for task in tasks])

@app.route('/api/tasks/<int:task_id>', methods=['PUT', 'DELETE'])
def update_task(task_id):
    task = Task.query.get_or_404(task_id)
    if request.method == 'PUT':
        data = request.json
        if 'title' in data and not data['title'].strip():
            # If the title is empty, delete the task
            db.session.delete(task)
            db.session.commit()
            return jsonify({'success': True, 'deleted': True})
        else:
            task.title = data.get('title', task.title)
            task.column = data.get('column', task.column)
            if 'status' in data:
                task.status = data['status']
                if task.status == 'finished':
                    # Accept completion_date from client or default to date.today()
                    completion_date_str = data.get('completion_date')
                    if completion_date_str:
                        task.completion_date = datetime.strptime(completion_date_str, '%Y-%m-%d').date()
                    else:
                        task.completion_date = date.today()
                else:
                    task.completion_date = None
            db.session.commit()
            return jsonify({'success': True, 'deleted': False})
    elif request.method == 'DELETE':
        db.session.delete(task)
        db.session.commit()
        return jsonify({'success': True})
    
@app.route('/api/tasks/archive', methods=["POST"])
def archive_completed_tasks():
    tasks = Task.query.filter(Task.status == 'finished')
    for task in tasks:
        task.status = 'archived'
        if task.completion_date == None:
            task.completion_date = date.today() - timedelta(days=1)
    db.session.commit()
    return jsonify({'success': True})

############################################## HABITS ##############################################

@app.route('/api/habits', methods=['GET', 'POST'])
def handle_habits():
    '''
    POST request: Creates a new habit in db and returns related JSON
    GET  request: Returns a JSON of all habits and related fields
    '''
    if request.method == 'POST':
        data = request.json
        new_habit = Habit(name=data['name'], days=','.join(map(str, data.get('days', []))))
        db.session.add(new_habit)
        db.session.commit()
        return jsonify(id=new_habit.id, name=new_habit.name, days=new_habit.days.split(','))
    else:
        habits_data = []
        for habit in Habit.query.all():
            habit_data = {
                'id': habit.id,
                'name': habit.name,
                'days': habit.days.split(',') if habit.days else [],
                'dates': [habit_date.date.isoformat() for habit_date in habit.dates]
            }
            habits_data.append(habit_data)
        
        return jsonify(habits_data)

@app.route('/api/habits/<int:habit_id>', methods=['PUT', 'DELETE'])
def update_habit(habit_id):
    habit = Habit.query.get_or_404(habit_id)
    if request.method == 'PUT':
        data = request.json
        if 'name' in data:
            habit.name = data['name']
        if 'days' in data:
            habit.days = ','.join(map(str, data['days']))
        if 'toggle_date' in data:
            toggle_date = date.fromisoformat(data['toggle_date'])
            existing_date = HabitDate.query.filter_by(habit_id=habit.id, date=toggle_date).first()
            if existing_date:
                db.session.delete(existing_date)
            else:
                new_date = HabitDate(habit_id=habit.id, date=toggle_date)
                db.session.add(new_date)
        db.session.commit()
        return jsonify({'success': True})
    elif request.method == 'DELETE':
        db.session.delete(habit)
        db.session.commit()
        return jsonify({'success': True})

############################################## JOURNAL ##############################################
@app.route('/api/journal', methods=['GET', 'POST'])
def handle_journal():
    if request.method == 'POST':
        data = request.json
        new_entry = JournalEntry(
            title=data['title'],
            content=data['content'],
            date=date.today(),
            ai_comment=data.get('ai_comment', '')
        )
        db.session.add(new_entry)
        db.session.commit()
        return jsonify(id=new_entry.id, title=new_entry.title, content=new_entry.content, date=str(new_entry.date), ai_comment=new_entry.ai_comment)
    else:
        entries = JournalEntry.query.all()
        return jsonify([{
            'id': entry.id,
            'title': entry.title,
            'content': entry.content,
            'date': str(entry.date),
            'ai_comment': entry.ai_comment
        } for entry in entries])

@app.route('/api/journal/<int:entry_id>', methods=['PUT', 'DELETE'])
def update_journal(entry_id):
    entry = JournalEntry.query.get_or_404(entry_id)
    if request.method == 'PUT':
        data = request.json
        entry.title = data.get('title', entry.title)
        entry.content = data.get('content', entry.content)
        entry.ai_comment = data.get('ai_comment', entry.ai_comment)
        db.session.commit()
        return jsonify({'success': True})
    elif request.method == 'DELETE':
        db.session.delete(entry)
        db.session.commit()
        return jsonify({'success': True})
    
############################################## HISTORY ##############################################

@app.route('/api/history', methods=['GET'])
def get_yearly_data():
    year_param = request.args.get('year', default=date.today().year, type=int)
    
    # Define the start and end dates for the specified year
    start_date = date(year_param, 1, 1)
    end_date = date(year_param, 12, 31)
    
    # Handle future years gracefully
    today = date.today()
    if end_date > today:
        end_date = today  # Limit end_date to today if in future
        
    # Generate the date range for the specified year
    total_days = (end_date - start_date).days + 1
    yearly_data = {
        (start_date + timedelta(days=i)).isoformat(): {
            "availableHabits": 0,
            "completedHabits": 0,
            "completedTasks": [],
            "completedHabitsList": [],
            "completionRate": 0
        } for i in range(total_days)
    }
    
    habits = Habit.query.all()
    tasks = Task.query.filter(
        Task.completion_date >= start_date,
        Task.completion_date <= end_date
    ).all()
    journal_entries = JournalEntry.query.all()
    
    # Build a mapping of dates to available habits
    for habit in habits:
        habit_days = habit.days.split(',') if habit.days else []
        for date_str in yearly_data.keys():
            date_obj = date.fromisoformat(date_str)
            # Adjusting weekday calculation since strftime('%w') returns 0 for Sunday
            weekday = str((date_obj.weekday() + 1) % 7)
            if weekday in habit_days:
                yearly_data[date_str]["availableHabits"] += 1
                
    # Mark completed habits
    for habit in habits:
        for habit_date in habit.dates:
            if start_date <= habit_date.date <= end_date:
                date_str = habit_date.date.isoformat()
                if date_str in yearly_data:
                    yearly_data[date_str]["completedHabits"] += 1
                    yearly_data[date_str]["completedHabitsList"].append(habit.name)
    
     # Record completed tasks
    for task in tasks:
        date_str = task.completion_date.isoformat()
        if date_str in yearly_data:
            yearly_data[date_str]["completedTasks"].append(task.title)
            
    # Calculate completion rate
    for date_str, data in yearly_data.items():
        available = data["availableHabits"]
        completed = data["completedHabits"]
        data["completionRate"] = completed / available if available > 0 else None
    
    for entry in journal_entries:
        date_str = str(entry.date)
        if date_str not in yearly_data:
            yearly_data[date_str] = {"completedHabits": [], "completedTasks": [], "journalEntries": []}
        yearly_data[date_str]["journalEntries"].append(entry.title)
    
    for date_str, data in yearly_data.items():
        available = data["availableHabits"]
        completed = data["completedHabits"]
        if available > 0:
            data["completionRate"] = completed / available
        else:
            data["completionRate"] = None
    
    # Return the data with date strings as keys
    return jsonify(yearly_data)

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(debug=True)

script.js:
const columnOrder = ['to-do-week', 'to-do-today', 'in-progress', 'finished'];

function createTaskElement(task) {
  const taskElement = document.createElement('div');
  taskElement.className = 'task-card';
  taskElement.dataset.taskId = task.id;

  taskElement.innerHTML = `
    <div class="task-title" contenteditable="true" onblur="updateTaskTitle(${task.id}, this)">${task.title}</div>
    <div class="task-buttons">
      ${getButtonsForTask(task)}
    </div>
  `;
  return taskElement;
}

function getLocalDateString() {
    const today = new Date();
    const year = today.getFullYear();
    const month = String(today.getMonth() + 1).padStart(2,'0');
    const day = String(today.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
}

function initializeSortable() {
    columnOrder.forEach(columnId => {
      const columnElement = document.getElementById(columnId);
  
      Sortable.create(columnElement, {
        group: 'columns', // Enable moving items between columns
        animation: 150,
        handle: '.task-card',
        draggable: '.task-card',
        onEnd: function (evt) {
          // Update task order in the client-side state here
          // Optionally, save to localStorage or send to the server when appropriate
        },
      });
    });
}
  
function loadTasks() {
    axios.get('/api/tasks')
      .then(response => {
        const tasks = response.data;
  
        // Clear columns
        columnOrder.forEach(status => {
          const column = document.getElementById(status);
          column.innerHTML = `
            <div class="column-header">
              <span class="column-title">${column.id.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')}</span>
              <span>...</span>
            </div>
          `;
        });
  
        tasks.forEach(task => {
          const columnElement = document.getElementById(task.status);
          if (!columnElement) return;
  
          const taskElement = createTaskElement(task);
          columnElement.appendChild(taskElement);
        });
  
        // Add "Add a card" buttons to columns
        columnOrder.forEach(status => {
          const column = document.getElementById(status);
          const addCard = document.createElement('div');
          addCard.className = 'add-card';
          addCard.textContent = '+ Add a card';
          addCard.onclick = () => addTask(status);
          column.appendChild(addCard);
        });
  
        // Initialize Sortable
        initializeSortable();
      })
      .catch(error => {
        console.error('Error loading tasks:', error);
      });
  }

function getButtonsForTask(task) {
    switch (task.status) {
        case 'to-do-week':
            return `<button class="task-button" onclick="moveTask(${task.id}, 'to-do-today')">→ Today</button>`;
        case 'to-do-today':
            return `
                <button class="task-button" onclick="moveTask(${task.id}, 'in-progress')">▶ IP</button>
                <button class="task-button" onclick="moveTask(${task.id}, 'finished')">✓</button>
            `;
        case 'in-progress':
            return `
                <button class="task-button" onclick="moveTask(${task.id}, 'to-do-week')">↺ Week</button>
                <button class="task-button" onclick="moveTask(${task.id}, 'to-do-today')">↺ Today</button>
                <button class="task-button" onclick="moveTask(${task.id}, 'finished')">✓</button>
            `;
        case 'finished':
            return `
                <button class="task-button" onclick="moveTask(${task.id}, 'to-do-week')">↺ Week</button>
                <button class="task-button" onclick="moveTask(${task.id}, 'to-do-today')">↺ Today</button>
            `;
        default:
            return '';
    }
}

function addTask(status) {
    const column = document.getElementById(status);
    
    // Hide the "Add a card" button
    const addCardButton = column.querySelector('.add-card');
    addCardButton.style.display = 'none';

    // Create a new task card div
    const newCard = document.createElement('div');
    newCard.classList.add('task-card');
    newCard.contentEditable = 'true';  // Make the new card editable

    // Add event listener to handle saving when 'Enter' is pressed
    newCard.addEventListener('keydown', function(event) {
        if (event.key === 'Enter') {
            event.preventDefault();  // Prevent new lines when pressing Enter
            const taskTitle = newCard.textContent.trim();
            if (taskTitle) {
                // Send task title and status to the backend
                axios.post('/api/tasks', { title: taskTitle, status })
                    .then(() => {
                        loadTasks();  // Reload tasks after adding new one
                        addCardButton.style.display = 'block';  // Show "Add a card" button again
                    });
            } else {
                // Remove the card if no text was entered
                newCard.remove();
                addCardButton.style.display = 'block';  // Show "Add a card" button again
            }
        }
    });

    // Add event listener for "Escape" key to discard the card
    newCard.addEventListener('keydown', function(event) {
        if (event.key === 'Escape') {
            newCard.remove();  // Discard the new card
            addCardButton.style.display = 'block';  // Show "Add a card" button again
        }
    });

    // Append the new card to the column
    column.appendChild(newCard);
    // Focus on the new card to allow typing immediately
    newCard.focus();
}

function moveTask(taskId, newStatus) {
    let data = { status: newStatus };
    if (newStatus === 'finished') {
        data['completion_date'] = getLocalDateString();
    } else {
        data['completion_date'] = null; // Clear the completion_date if not finished
    }
    axios.put(`/api/tasks/${taskId}`, data)
        .then(() => loadTasks())
        .catch(error => {
            console.error('Error moving task:', error);
        });
}

function updateTaskTitle(taskId, element) {
    const newTitle = element.textContent.trim();
    axios.put(`/api/tasks/${taskId}`, { title: newTitle })
    .then((response) => {
        if (response.data.deleted) {
            // If the task was deleted (empty title), remove it from the UI
            element.closest('.task-card').remove();
        } else {
            // If the task was updated, reload tasks to reflect any changes
            loadTasks();
        }
    })
    .catch((error) => {
        console.error('Error updating task:', error);
        loadTasks(); // Reload tasks to ensure UI is in sync with server
    });
}

function loadHabits() {
    axios.get('/api/habits')
        .then(response => {
            const habits = response.data;
            const habitsContainer = document.getElementById('habits');
            habitsContainer.innerHTML = '';
            const todayStr = getLocalDateString(); // 'YYYY-MM-DD'
            const todayWeekday = new Date().getDay().toString(); // '0' (Sunday) to '6' (Saturday)

            habits.forEach(habit => {
                const isAvailableToday = habit.days.includes(todayWeekday);
                if (!isAvailableToday) {
                    // Skip habits not scheduled for today
                    return;
                }

                const habitElement = document.createElement('div');
                const habitDates = habit.dates || [];
                const isCompletedToday = habitDates.includes(todayStr);

                habitElement.className = `habit ${isCompletedToday ? 'habit-complete' : 'habit-incomplete'}`;
                habitElement.innerHTML = `<h3>${habit.name}</h3>`;
                habitElement.onclick = () => toggleHabit(habitElement, habit.id);
                habitsContainer.appendChild(habitElement);
            });
        })
        .catch(error => {
            console.error('Error loading habits:', error);
        });
}

function addHabit() {
    const name = document.getElementById('new-habit').value;
    const dayCheckboxes = document.querySelectorAll('#day-select input[type="checkbox"]:checked');
    const days = Array.from(dayCheckboxes).map(cb => cb.value);
    // console.log(days)
    if (name) {
        axios.post('/api/habits', { name, days })
            .then(() => {
                document.getElementById('new-habit').value = '';
                // Clear checkboxes
                document.querySelectorAll('#day-select input[type="checkbox"]').forEach(cb => cb.checked = false);
                loadHabits();
            });
    }
}

function toggleHabit(element, habitId) {
    const today = getLocalDateString();

    // Toggle the visual state immediately
    const isCurrentlyCompleted = element.classList.contains('habit-complete');

    if (isCurrentlyCompleted) {
        // Remove 'habit-complete', add 'habit-incomplete'
        element.classList.remove('habit-complete');
        element.classList.add('habit-incomplete');
    } else {
        // Add 'habit-complete', remove 'habit-incomplete'
        element.classList.add('habit-complete');
        element.classList.remove('habit-incomplete');
    }

    // Send the request to the backend
    axios.put(`/api/habits/${habitId}`, { toggle_date: today })
        .then(response => {
            if (!response.data.success) {
                console.error(`Failed to toggle habit ${habitId}`);
                // Optionally revert the visual change if the backend fails
                toggleVisualState(element);
            }
        })
        .catch(error => {
            console.error('Error toggling habit:', error.response ? error.response.data : error.message);
            // Optionally revert the visual change if the backend fails
            toggleVisualState(element);
        });
}

// Helper function to revert visual state if needed
function toggleVisualState(element) {
    const isCompleted = element.classList.contains('habit-complete');
    if (isCompleted) {
        element.classList.remove('habit-complete');
        element.classList.add('habit-incomplete');
    } else {
        element.classList.add('habit-complete');
        element.classList.remove('habit-incomplete');
    }
}

document.addEventListener('DOMContentLoaded', function () {
    // Load tasks and habits on page load
    loadTasks();
    loadHabits();
});

yearly_grid.js:
let currentYear = new Date().getFullYear();
let displayMode = 'combined';
const dayWidth = 30;  // Updated width of each day square
const dayHeight = 30; // Updated height of each day square
const gutter = 2;     // Space between squares

function getLocalDateString(dateObj) {
    const year = dateObj.getFullYear();
    const month = String(dateObj.getMonth() + 1).padStart(2,'0');
    const day = String(dateObj.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
}

document.addEventListener('DOMContentLoaded', function() {
    // Set the current year display
    document.getElementById('current-year').textContent = currentYear;

    // Add event listeners to navigation buttons
    document.getElementById('prev-year').addEventListener('click', () => {
        currentYear--;
        updateYearlyGrid();
    });

    document.getElementById('next-year').addEventListener('click', () => {
        currentYear++;
        updateYearlyGrid();
    });

    // Add event listener to the display mode toggle
    const displayModeSelect = document.getElementById('display-mode');
    displayModeSelect.value = displayMode; // Set default value
    displayModeSelect.addEventListener('change', () => {
        displayMode = displayModeSelect.value;
        updateYearlyGrid();
    });

    // Fetch the initial data
    updateYearlyGrid();
});

function updateYearlyGrid() {
    document.getElementById('current-year').textContent = currentYear;

    // Disable the next button if currentYear is the same as the current year
    const nextYearButton = document.getElementById('next-year');
    if (currentYear >= new Date().getFullYear()) {
        nextYearButton.disabled = true;
    } else {
        nextYearButton.disabled = false;
    }

    fetchHistoryData(currentYear);
}

function fetchHistoryData(year) {
    axios.get(`/api/history?year=${year}`)
        .then(response => {
            const data = response.data;
            renderHeatmap(data, year, displayMode); // Pass displayMode
        })
        .catch(error => {
            console.error('Error fetching history data:', error);
            alert('No data available for this year.');
        });
}

function renderHeatmap(data, year, displayMode) {
    const container = document.getElementById('heatmap-container');
    container.innerHTML = '';
    const currentDate = new Date();

    const dataMap = new Map(Object.entries(data));

    // Create the main grid container
    const grid = document.createElement('div');
    grid.classList.add('heatmap-grid');

    // Iterate over each month
    for (let month = 0; month < 12; month++) {
        if (currentYear >= currentDate.getFullYear() && month > currentDate.getMonth()) {
            break;
        }

        const monthColumn = document.createElement('div');
        monthColumn.classList.add('month-column');

        const monthDate = new Date(year, month, 1);
        const monthName = monthDate.toLocaleString('default', { month: 'short' });

        // Create the month label
        const monthLabel = document.createElement('div');
        monthLabel.classList.add('month-label');
        monthLabel.textContent = monthName;
        monthColumn.appendChild(monthLabel);

        // Optionally, add weekday labels
        const weekdaysRow = document.createElement('div');
        weekdaysRow.classList.add('weekdays-row');
        const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
        weekdays.forEach(dayName => {
            const weekdayLabel = document.createElement('div');
            weekdayLabel.classList.add('weekday-label');
            weekdayLabel.textContent = dayName;
            weekdaysRow.appendChild(weekdayLabel);
        });
        monthColumn.appendChild(weekdaysRow);

        // Get the weekday of the first day of the month
        const firstWeekday = monthDate.getDay();

        // Create a grid for days
        const daysGrid = document.createElement('div');
        daysGrid.classList.add('days-grid');

        // Add empty cells for days before the first of the month
        for (let i = 0; i < firstWeekday; i++) {
            const emptyCell = document.createElement('div');
            emptyCell.classList.add('day-cell', 'empty-cell');
            daysGrid.appendChild(emptyCell);
        }

        // Get total days in the month
        const daysInMonth = new Date(year, month + 1, 0).getDate();

        // Create day cells
        for (let day = 1; day <= daysInMonth; day++) {

            if (currentYear >= currentDate.getFullYear() && (month == currentDate.getMonth() && day > currentDate.getDate() || (month > currentDate.getMonth()))) {
                break;
            }

            const date = new Date(year, month, day);
            const dateStr = getLocalDateString(date);

            const dayCell = document.createElement('div');
            dayCell.classList.add('day-cell');
            dayCell.dataset.date = dateStr;

            const dayData = dataMap.get(dateStr);
            let shade = '#444';

            if (dayData) {
                if (displayMode === 'combined') {
                    shade = getCombinedShade(dayData);
                } else if (displayMode === 'habits') {
                    shade = getHabitShade(dayData);
                } else if (displayMode === 'tasks') {
                    shade = getTaskShade(dayData);
                }
            } else {
                shade = '#444'; // Default shade when no data
            }
            dayCell.style.backgroundColor = shade;

            // Make the cell clickable
            dayCell.classList.add('clickable');
            dayCell.addEventListener('click', () => {
                if (dayData) {
                    showDetailsModal(dateStr, dayData);
                } else {
                    showDetailsModal(dateStr, {
                        availableHabits: 0,
                        completedHabits: 0,
                        completedTasks: [],
                        completedHabitsList: [],
                        completionRate: null
                    });
                }
            });

            daysGrid.appendChild(dayCell);
        }

        monthColumn.appendChild(daysGrid);
        grid.appendChild(monthColumn);
    }

    // Append grid to container
    container.appendChild(grid);

    // Remove calls to renderMonthLabels and renderWeekdayLabels
    // renderMonthLabels(container, startDate, endDate, totalWeeks);
    // renderWeekdayLabels(container);
}

////////////////////* https://yeun.github.io/open-color/ for colors *////////////////////////
function getCombinedShade(dayData) {
    const shades = [
        '#3b3b3b', // No tasks completed
        '#106030', // 1 task
        '#2b8a3e', // 2 tasks
        '#2f9e44', // 3 tasks
        '#37b24d', // 4 tasks
        '#40c057', // 5 tasks
        '#51cf66', // 6 tasks
        '#69db7c', // 7 tasks
        '#8ce99a', // 8 tasks
        '#b2f2bb', // 9 tasks
        '#d3f9d8', // 10 tasks
        '#ebfbee', // 11+ tasks
    ];

    if (!dayData || dayData.completedTasks.length + dayData.completedHabits === 0) {
        return shades[0];
    } else {
        const combinedCompleted = dayData.completedTasks.length + dayData.completedHabits;
        if (combinedCompleted === 1) {
            return shades[1];
        } else if (combinedCompleted === 2) {
            return shades[2];
        } else if (combinedCompleted === 3) {
            return shades[3];
        } else if (combinedCompleted === 4) {
            return shades[4];
        } else if (combinedCompleted === 5) {
            return shades[5];
        } else if (combinedCompleted === 6) {
            return shades[6];
        } else if (combinedCompleted === 7) {
            return shades[7];
        } else if (combinedCompleted === 8) {
            return shades[8];
        } else if (combinedCompleted === 9) {
            return shades[9];
        } else if (combinedCompleted === 10) {
            return shades[10];
        } else {
            return shades[11];
        }
    }
}
function getHabitShade(dayData) {
    const shades = [
        '#2c2c2c', // No habits available (darkest)
        '#3b3b3b', // 0% completion
        '#1864ab', // 10% completion
        '#1971c2', // 25% completion
        '#1c7ed6', // 40% completion
        '#228be6', // 65% completion
        '#339af0', // 75% completion
        '#4dabf7', // 85% completion
        '#74c0fc'  // 100% completion
    ];

    if (!dayData || dayData.completionRate === null) {
        return shades[0]; // No habits available
    } else {
        const completionRate = dayData.completionRate;
        if (completionRate === 0) {
            return shades[1];
        } else if (completionRate <= 0.1) {
            return shades[2];
        } else if (completionRate <= 0.25) {
            return shades[3];
        } else if (completionRate <= 0.40) {
            return shades[4];
        } else if (completionRate <= 0.65) {
            return shades[5];
        } else if (completionRate <= 0.75) {
            return shades[6];
        } else if (completionRate <= 0.85) {
            return shades[7];
        } else {
            return shades[8];
        }
    }
}
function getTaskShade(dayData) {
    const shades = [
        '#3b3b3b', // No tasks completed
        '#3E0101', // 1 task
        '#8B0000', // 2 tasks
        '#C92A2A', // 3 tasks
        '#E03030', // 4 tasks
        '#F03E3E', // 5 tasks
        '#FA5353', // 6 tasks
        '#FE6A6B', // 7 tasks
        '#FF8787', // 8 tasks
        '#FFC9C9', // 9 tasks
        '#FFE2E2', // 10 tasks
        '#FFF5F5', // 11+ tasks
    ];

    if (!dayData || dayData.completedTasks.length === 0) {
        return shades[0];
    } else {
        const tasksCompleted = dayData.completedTasks.length;
        if (tasksCompleted === 1) {
            return shades[1];
        } else if (tasksCompleted === 2) {
            return shades[2];
        } else if (tasksCompleted === 3) {
            return shades[3];
        } else if (tasksCompleted === 4) {
            return shades[4];
        } else if (tasksCompleted === 5) {
            return shades[5];
        } else if (tasksCompleted === 6) {
            return shades[6];
        } else if (tasksCompleted === 7) {
            return shades[7];
        } else if (tasksCompleted === 8) {
            return shades[8];
        } else if (tasksCompleted === 9) {
            return shades[9];
        } else if (tasksCompleted === 10) {
            return shades[10];
        } else {
            return shades[11];
        }
    }
}

function renderMonthLabels(container, startDate, endDate, totalWeeks) {
    const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                        'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

    const monthsRow = document.createElement('div');
    monthsRow.classList.add('months-row');

    let currentMonth = -1;
    let weeksInMonth = 0;
    let weekIndex = 0;
    let dateIterator = new Date(startDate);

    while (dateIterator <= endDate) {
        const month = dateIterator.getMonth();
        if (month !== currentMonth) {
            // New month
            if (currentMonth !== -1) {
                // Set the width of the previous month label
                monthsRow.children[monthsRow.children.length - 1].style.width = (weeksInMonth * (dayWidth + gutter)) + 'px';
            }
            // Create new month label
            currentMonth = month;
            weeksInMonth = 1;

            const monthLabel = document.createElement('div');
            monthLabel.classList.add('month-label');
            monthLabel.textContent = monthNames[currentMonth];
            monthsRow.appendChild(monthLabel);
        } else {
            // Same month
            weeksInMonth++;
        }

        // Move to the next week
        dateIterator.setDate(dateIterator.getDate() + 7);
        weekIndex++;
    }

     // Set the width for the last month label
    monthsRow.children[monthsRow.children.length - 1].style.width = (weeksInMonth * (dayWidth + gutter)) + 'px';

    // Append monthsRow to container
    container.insertBefore(monthsRow, container.firstChild);
}

function renderWeekdayLabels(container) {
    const weekdayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

    const weekdaysColumn = document.createElement('div');
    weekdaysColumn.classList.add('weekdays-column');

    for (let i = 0; i < 7; i++) {
        const dayLabel = document.createElement('div');
        dayLabel.classList.add('weekday-label');

        // Show labels for Sun, Tue, Thu (or adjust as desired)
        if (i === 0 || i === 2 || i === 4) {
            dayLabel.textContent = weekdayNames[i];
        } else {
            dayLabel.textContent = ''; // Empty label for alignment
        }

        dayLabel.style.height = dayHeight + 'px';
        dayLabel.style.margin = (gutter / 2) + 'px 0'; // Adjust vertical margin

        weekdaysColumn.appendChild(dayLabel);
    }

    // Append the weekdaysColumn to container
    container.appendChild(weekdaysColumn);
}

function showDetailsModal(dateStr, dayData) {
    const modal = document.getElementById('details-modal');
    const modalDate = document.getElementById('modal-date');
    const modalContent = document.getElementById('modal-content');
    const closeButton = document.getElementById('modal-close');

    modalDate.textContent = dateStr;
    modalContent.innerHTML = '';

    // Display completed habits
    const habitsDiv = document.createElement('div');
    habitsDiv.innerHTML = `<h3>Completed Habits (${dayData.completedHabits}/${dayData.availableHabits})</h3>`;
    const habitsList = document.createElement('ul');

    dayData.completedHabitsList.forEach(habitName => {
        const li = document.createElement('li');
        li.textContent = habitName;
        habitsList.appendChild(li);
    });
    
    if (dayData.completedHabitsList.length > 0) {
        habitsDiv.appendChild(habitsList);
    } else {
        const noHabits = document.createElement('p');
        noHabits.textContent = 'No habits completed on this day.';
        habitsDiv.appendChild(noHabits);
    }

    modalContent.appendChild(habitsDiv);

    // Display completed tasks
    if (dayData.completedTasks.length > 0) {
        const tasksDiv = document.createElement('div');
        tasksDiv.innerHTML = `<h3>Completed Tasks</h3>`;
        const tasksList = document.createElement('ul');

        dayData.completedTasks.forEach(taskTitle => {
            const li = document.createElement('li');
            li.textContent = taskTitle;
            tasksList.appendChild(li);
        });
        tasksDiv.appendChild(tasksList);
        modalContent.appendChild(tasksDiv);
    } else {
        const noTasks = document.createElement('p');
        noTasks.textContent = 'No tasks completed on this day.';
        modalContent.appendChild(noTasks);
    }

    // Show the modal
    modal.style.display = 'block';

    // Close modal when clicking on the close button
    closeButton.onclick = function() {
        modal.style.display = 'none';
    };

    // Close modal when clicking outside the modal content
    window.onclick = function(event) {
        if (event.target === modal) {
            modal.style.display = 'none';
        }
    };
}

The tasks that are archived at 12:01 am show up as completed today instead of yesterday on the yearly_grid page, which is a behavior I don't want.