app.py:
# 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'
                task.completion_date = date.today()
            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=0)  # Runs at 12:00 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.status = data.get('status', task.status)
            task.column = data.get('column', task.column)
            if data.get('status') == 'finished':
                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'
        task.completion_date = date.today()
    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())
            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)

templates/index.html:
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Task Productivity App</title>
        <link rel="stylesheet" href="{{ url_for('static', filename='common.css') }}">
        <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.3/Sortable.min.js"></script>
    </head>
<body>
    <div id="menu-toggle">&#9776;</div>
    <nav id="slide-menu">
        <ul>
            <li><a href="/">Habit Tracker</a></li>
            <li><a href="/yearly-grid">Yearly Grid</a></li>
            <li>
                <input type="text" id="new-habit" placeholder="New habit">
                <div id="day-select">
                    <label><input type="checkbox" value="0"> Sun</label>
                    <label><input type="checkbox" value="1"> Mon</label>
                    <label><input type="checkbox" value="2"> Tue</label>
                    <label><input type="checkbox" value="3"> Wed</label>
                    <label><input type="checkbox" value="4"> Thu</label>
                    <label><input type="checkbox" value="5"> Fri</label>
                    <label><input type="checkbox" value="6"> Sat</label>
                </div>
                <button onclick="addHabit()">Add Habit</button>
            </li>
        </ul>
    </nav>
    
    <div id="content">
        <div id="habit-tracker" class="page">
            <div class="habit-tracker">
                <h2>Habit Tracker</h2>
                <div class="habits-container" id="habits"></div>
            </div>
        </div>
    </div>

    <div class="container">
        <div class="column" id="to-do-week">
            <div class="column-header">
                <span class="column-title">To Do This Week</span>
                <span>...</span>
            </div>
        </div>
        <div class="column" id="to-do-today">
            <div class="column-header">
                <span class="column-title">To Do Today</span>
                <span>...</span>
            </div>
        </div>
        <div class="column" id="in-progress">
            <div class="column-header">
                <span class="column-title">In Progress</span>
                <span>...</span>
            </div>
        </div>
        <div class="column" id="finished">
            <div class="column-header">
                <span class="column-title">Finished</span>
                <span>...</span>
            </div>
        </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
    <script src="{{ url_for('static', filename='js/common.js') }}"></script>
    <script src="{{ url_for('static', filename='js/script.js') }}"></script>
</body>
</html>

templates/yearly_grid.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Yearly Contribution Grid</title>
    <!-- Your custom CSS -->
    <link rel="stylesheet" href="{{ url_for('static', filename='yearly_grid.css') }}">
    <link rel="stylesheet" href="{{ url_for('static', filename='common.css') }}">
</head>
<body>
    <!-- Menu Toggle -->
    <div id="menu-toggle">&#9776;</div>
    <nav id="slide-menu">
        <ul>
            <li><a href="/">Habit Tracker</a></li>
            <li><a href="/yearly-grid">Yearly Grid</a></li>
        </ul>
    </nav>

    <!-- Main Content -->
    <div id="content">
        <!-- Year Navigation -->
        <div id="year-navigation">
            <button id="prev-year">←</button>
            <span id="current-year"></span>
            <button id="next-year">→</button>
        </div>

        <div id="heatmap-container"></div>
    </div>

    <!-- Modal for displaying details -->
    <div id="details-modal" class="modal">
        <div class="modal-content">
            <span class="close-button" id="modal-close">&times;</span>
            <h2 id="modal-date"></h2>
            <div id="modal-content"></div>
        </div>
    </div>

    <!-- Scripts -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="{{ url_for('static', filename='js/common.js') }}"></script>
    <script src="{{ url_for('static', filename='js/yearly_grid.js') }}"></script>
</body>
</html>

static/styles.css:
body {
    font-family: Arial, sans-serif;
    background-color: #46474a;
    margin: 0;
    padding: 20px;
}
.container {
    display: flex;
    justify-content: space-between;
    gap: 20px;
    align-items: flex-start; /* Align columns at the top, not stretched */
}

.column {
    width: 23%;
    background-color: #d0d4d9;  /* Slightly darker than the add a card box */
    border-radius: 8px;
    padding: 10px;
}
.column-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;
}
.column-title {
    font-weight: bold;
    font-size: 14px;
    color: #172b4d;
}
.task-input {
    width: 100%;
    padding: 8px;
    font-size: 14px;
    border: 1px solid #ccc;
    border-radius: 3px;
    margin-bottom: 8px;
}
.task-card {
    border-radius: 3px;
    box-shadow: 0 1px 0 rgba(9,30,66,.25);
    padding: 10px;
    margin-bottom: 8px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    position: relative;
}
.task-card:hover {
    filter: brightness(95%);
}

.task-card.dragging {
    opacity: 0.5;
    cursor: move;
}

.task-card.placeholder {
    background-color: #e0e0e0;
    border: 2px dashed #999;
}

#to-do-week .task-card, #to-do-today .task-card { background-color: #c22f2f; }
#in-progress .task-card { background-color: #ffff99; }
#finished .task-card { background-color: #99ff99; }
.task-title {
    font-size: 14px;
    color: #172b4d;
    flex-grow: 1;
    margin-right: 10px;
}
.task-buttons {
    display: none;
}
.task-card:hover .task-buttons {
    display: flex;
    gap: 5px;
}
.task-button {
    background-color: #ffffff;
    border: none;
    border-radius: 3px;
    padding: 3px 6px;
    cursor: pointer;
    font-size: 12px;
}
.task-button:hover {
    background-color: #f4f5f7;
}
.add-card {
    color: #5e6c84;
    font-size: 14px;
    cursor: pointer;
    background-color: #ebecf0;
    padding: 5px;
}
.add-card:hover {
    background-color: rgba(9,30,66,.08);
    border-radius: 3px;
}
.habit-tracker {
    margin-top: 20px;
    align-items: center;
    text-align: center;
}
.habits-container {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
    margin-top: 20px;
    justify-content: center;
}

.habit {
    background-color: #fff;
    border-radius: 8px;
    width: 150px;
    height: 100px;
    box-shadow: 0px 3px 6px rgba(0,0,0,0.16);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    position: relative;
    transition: transform 0.3s ease, opacity 0.3s ease;
}

.habit:hover {
    transform: translateY(-5px);
    box-shadow: 0px 6px 12px rgba(0,0,0,0.2);
}

.habit-incomplete {
    opacity: 1;
    transform: translateX(0) scale(1);
}

.habit-complete {
    transform: translateY(-40px) scale(0.3); /* Move to the right and slightly enlarge */
    opacity: 0.15; /* Fade out */
}

.habit-complete:hover {
    opacity: 0.4;
}

.habit::after {
    content: '';
    position: absolute;
    bottom: -10px;
    left: 50%;
    transform: translateX(-50%);
    border-top: 10px solid #fff;
    border-left: 10px solid transparent;
    border-right: 10px solid transparent;
}

.habit h3 {
    margin: 0;
    font-size: 16px;
    text-align: center;
    color: #333;
}

.habit p {
    margin: 5px 0 0 0;
    font-size: 12px;
    color: #666;
}
.habit-incomplete { background-color: #ffff99; }
.habit-complete { background-color: #99ff99; }

.slide-menu button {
    padding: 5px 10px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 3px;
    cursor: pointer;
}

.slide-menu button:hover {
    background-color: #45a049;
}

/* Modal styles */
.modal {
    display: none; /* Hidden by default */
    position: fixed; /* Stay in place */
    z-index: 1; /* Sit on top */
    left: 0;
    top: 0;
    width: 100%; /* Full width */
    height: 100%; /* Full height */
    overflow: auto; /* Enable scroll if needed */
    background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}

.modal-content {
    background-color: #fefefe;
    margin: 15% auto; /* Centered */
    padding: 20px;
    border: 1px solid #888;
    width: 80%; /* Could be more or less, depending on screen size */
}

.close {
    color: #aaa;
    float: right;
    font-size: 28px;
    font-weight: bold;
}

.close:hover, .close:focus {
    color: black;
    text-decoration: none;
    cursor: pointer;
}

static/yearly_grid.css:
/* static/yearly_grid.css */

/* Set the background color of the body or container to your dark background color */
body {
    background-color: #222;  /* Example dark background */
    color: #fff;  /* Default text color to white */
}

#content {
    color: #fff;
}

#heatmap-container {
    position: relative;
    padding: 20px 40px;
    margin-top: 30px;
}

#year-navigation {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-bottom: 20px;
}

#year-navigation button {
    background-color: #444;
    color: #fff;
    border: none;
    padding: 5px 10px;
    font-size: 18px;
    cursor: pointer;
}

#year-navigation button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

#year-navigation span {
    margin: 0 10px;
    font-size: 28px;
    font-weight: bold;
    color: #fff;
}

.heatmap-grid {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap; /* Allow wrapping if needed */
    justify-content: center;
    align-items: flex-start;
    margin-left: 150px;
    margin-right: 150px;
}

.month-column {
    display: flex;
    flex-direction: column;
    margin: 10px; /* Adjust spacing between months */
}

.month-label {
    text-align: center;
    font-size: 18px;
    font-weight: bold;
    color: #fff;
    margin-bottom: 5px;
}

.weekdays-row {
    display: grid;
    grid-template-columns: repeat(7, 30px); /* Adjust width to match day-cell */
    gap: 4px;
    margin-bottom: 5px;
}

.weekday-label {
    text-align: center;
    font-size: 12px;
    color: #ccc;
}

.days-grid {
    display: grid;
    grid-template-columns: repeat(7, 30px);
    gap: 4px;
}

.day-cell {
    width: 30px;
    height: 30px;
    /* margin: 2px; */
    background-color: #444;
    cursor: pointer;
}

.empty-cell {
    background-color: transparent;
    cursor: default;
}

.clickable {
    cursor: pointer;
}

.modal {
    display: none; /* Hidden by default */
    position: fixed;
    z-index: 1000;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    overflow: auto;
    background-color: rgba(0,0,0,0.7);
}

.modal-content {
    background-color: #333;  /* Dark modal background */
    color: #fff;
    margin: 5% auto;
    padding: 20px;
    border-radius: 4px;
    max-width: 500px;
}

.close-button {
    float: right;
    font-size: 24px;
    cursor: pointer;
    color: #fff;
}

.close-button:hover {
    color: red;
}

static/js/common.js:
document.addEventListener('DOMContentLoaded', function () {

    // Handle menu toggle
    const menuState = localStorage.getItem('menuState');
    if (menuState === 'open') {
        document.body.classList.add('menu-active');
    }

    // Handle menu toggle
    const menuToggle = document.getElementById('menu-toggle');
    if (menuToggle) {
        menuToggle.addEventListener('click', function() {
            document.body.classList.toggle('menu-active');

            // Save the new state to localStorage
            if (document.body.classList.contains('menu-active')) {
                localStorage.setItem('menuState', 'open');
            } else {
                localStorage.setItem('menuState', 'closed');
            }
        });
    }

    // Add any other common initialization code here

});

static/js/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 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) {
    axios.put(`/api/tasks/${taskId}`, { status: newStatus })
        .then(() => loadTasks());
}

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 today = new Date();
            const todayStr = today.toISOString().split('T')[0]; // 'YYYY-MM-DD'
            const todayWeekday = today.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);
    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 = new Date().toISOString().split('T')[0];

    // 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();
});

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

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();
    });

    // 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);
        })
        .catch(error => {
            console.error('Error fetching history data:', error);
            alert('No data available for this year.');
        });
}

function renderHeatmap(data, year) {
    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 = date.toISOString().split('T')[0];

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

            const dayData = dataMap.get(dateStr);

            if (dayData) {
                const completionRate = dayData.completionRate;

                if (completionRate !== null) {
                    const shade = getShade(completionRate);
                    dayCell.style.backgroundColor = shade;
                } else {
                    // No habits available on this day
                    dayCell.style.backgroundColor = '#444'; // Darker shade for no habits
                }
            } else {
                // Dates with no data
                dayCell.style.backgroundColor = '#444';
            }

            // 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);
}
function getShade(completionRate) {
    // Shades suitable for dark background
    const shades = [
        '#2c2c2c', // No habits available (darkest)
        '#3b3b3b', // 0% (no habits completed)
        '#255f3f', // >0% up to 25%
        '#2e8b57', // >25% up to 50%
        '#3cb371', // >50% up to 75%
        '#66cdaa'  // >75% to 100% (lightest)
    ];

    if (completionRate === null) {
        return shades[0]; // No habits available
    } else if (completionRate === 0) {
        return shades[1]; // No habits completed
    } else if (completionRate <= 0.25) {
        return shades[2];
    } else if (completionRate <= 0.5) {
        return shades[3];
    } else if (completionRate <= 0.75) {
        return shades[4];
    } else {
        return shades[5];
    }
}

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';
        }
    };
}

For some reason, the habits aren't showing up for the correct day. For example, I cleaned yesterday (and clicked the field for it yesterday), but it shows that I completed the habit today. Help fix this bug!