diff --git a/app.py b/app.py index 619b544..331aeb5 100644 --- a/app.py +++ b/app.py @@ -40,20 +40,29 @@ def index(): 'end_time': None }) - active_tasks_template = [] + active_tasks = [] if current_entry: # Get tasks associated with the active activity type active_activity = db.activities.find_one({'_id': current_entry['activity_id']}) if active_activity: current_entry['activity_name'] = active_activity['name'] current_entry['activity_color'] = active_activity.get('color', '#3498db') - # Find template tasks for this activity - active_tasks_template = list(db.task_templates.find({'activity_id': active_activity['_id']})) + + # 1. Tasks generated specifically for this session (time_entry_id linked) + # 2. Open tasks linked to this activity type (contextual todos) + active_tasks = list(db.tasks.find({ + 'user_id': user_id, + 'status': 'open', + '$or': [ + {'time_entry_id': current_entry['_id']}, + {'activity_id': current_entry['activity_id'], 'is_template': False, 'time_entry_id': None} + ] + })) return render_template('dashboard.html', activities=activities, current_entry=current_entry, - tasks=active_tasks_template) + tasks=active_tasks) @app.route('/register', methods=['GET', 'POST']) def register(): @@ -66,9 +75,12 @@ def register(): return redirect(url_for('register')) hashed_password = bcrypt.generate_password_hash(password).decode('utf-8') - db.users.insert_one({'username': username, 'password': hashed_password}) - flash('Account created! Please login.') - return redirect(url_for('login')) + user_id = db.users.insert_one({'username': username, 'password': hashed_password}).inserted_id + + # Auto login + session['user_id'] = str(user_id) + flash('Account created! You are now logged in.') + return redirect(url_for('index')) return render_template('register.html') @@ -132,20 +144,39 @@ def toggle_timer(activity_id): {'$set': {'end_time': datetime.now()}} ) else: - # Stop ANY other running timer first (single tasking) + # Stop ANY other running timer first db.time_entries.update_many( {'user_id': user_id, 'end_time': None}, {'$set': {'end_time': datetime.now()}} ) # Start new - db.time_entries.insert_one({ + new_entry_id = db.time_entries.insert_one({ 'user_id': user_id, 'activity_id': ObjectId(activity_id), 'start_time': datetime.now(), 'end_time': None, 'note': note + }).inserted_id + + # Check for Task Templates and auto-add them + templates = db.tasks.find({ + 'user_id': user_id, + 'activity_id': ObjectId(activity_id), + 'is_template': True }) + for t in templates: + db.tasks.insert_one({ + 'user_id': user_id, + 'name': t['name'], + 'activity_id': ObjectId(activity_id), + 'time_entry_id': new_entry_id, # Link to this specific session + 'status': 'open', + 'is_template': False, + 'created_at': datetime.now(), + 'comments': [] + }) + return redirect(url_for('index')) @app.route('/stop_timer', methods=['POST']) @@ -161,18 +192,108 @@ def stop_timer(): def complete_task(): if not is_logged_in(): return jsonify({'error': 'auth'}), 401 - # We log that a specific task template was completed during a specific time entry - task_name = request.form['task_name'] - entry_id = request.form['entry_id'] + task_id = request.form['task_id'] + is_checked = request.form['is_checked'] == 'true' - db.completed_tasks.insert_one({ - 'user_id': get_user_id(), - 'task_name': task_name, - 'time_entry_id': ObjectId(entry_id), - 'completed_at': datetime.now() - }) + status = 'completed' if is_checked else 'open' + completed_at = datetime.now() if is_checked else None + + db.tasks.update_one( + {'_id': ObjectId(task_id), 'user_id': get_user_id()}, + {'$set': {'status': status, 'completed_at': completed_at}} + ) + + # If it was a generic activity task, bind it to current entry if one acts so it shows in log + # (Optional logic, skipping for simplicity) + return jsonify({'status': 'success'}) +# --- New Task Management Routes --- + +@app.route('/tasks') +def tasks(): + if not is_logged_in(): return redirect(url_for('login')) + user_id = get_user_id() + + # Fetch all activities for the dropdown + activities = list(db.activities.find({'user_id': user_id})) + + # Categorize tasks + # 1. Standalone / Manual Tasks + tasks_list = list(db.tasks.find({ + 'user_id': user_id, + 'is_template': False, + 'time_entry_id': None, # Don't show session-specific generated tasks here to avoid clutter? or show all? + # Let's show manual tasks only. Session tasks are transient. + 'status': 'open' + }).sort('due_date', 1)) + + # 2. Templates + templates = list(db.tasks.find({'user_id': user_id, 'is_template': True})) + + # Enhance tasks with activity names + act_map = {str(a['_id']): a for a in activities} + for t in tasks_list + templates: + if t.get('activity_id'): + aid = str(t['activity_id']) + if aid in act_map: + t['activity_name'] = act_map[aid]['name'] + t['activity_color'] = act_map[aid].get('color', '#ccc') + + return render_template('tasks.html', tasks=tasks_list, templates=templates, activities=activities) + +@app.route('/create_task', methods=['POST']) +def create_task(): + if not is_logged_in(): return redirect(url_for('login')) + + name = request.form['name'] + activity_id = request.form.get('activity_id') + due_date_str = request.form.get('due_date') + is_template = 'is_template' in request.form + + task_doc = { + 'user_id': get_user_id(), + 'name': name, + 'status': 'open', + 'is_template': is_template, + 'created_at': datetime.now(), + 'comments': [] + } + + if activity_id: + task_doc['activity_id'] = ObjectId(activity_id) + + if due_date_str: + task_doc['due_date'] = datetime.strptime(due_date_str, '%Y-%m-%dT%H:%M') + + # Standard task or template doesn't belong to a specific time entry yet + task_doc['time_entry_id'] = None + + db.tasks.insert_one(task_doc) + return redirect(url_for('tasks')) + +@app.route('/task/', methods=['GET', 'POST']) +def task_detail(task_id): + if not is_logged_in(): return redirect(url_for('login')) + + task = db.tasks.find_one({'_id': ObjectId(task_id), 'user_id': get_user_id()}) + if not task: return "Task not found", 404 + + if request.method == 'POST': + comment = request.form['comment'] + if comment: + db.tasks.update_one( + {'_id': ObjectId(task_id)}, + {'$push': {'comments': { + 'text': comment, + 'created_at': datetime.now(), + 'user_id': get_user_id() # In case we add teams later + }}} + ) + return redirect(url_for('task_detail', task_id=task_id)) + + return render_template('task_detail.html', task=task) + @app.route('/logbook') def logbook(): if not is_logged_in(): return redirect(url_for('login')) @@ -188,22 +309,69 @@ def logbook(): 'as': 'activity' }}, {'$unwind': '$activity'}, + # Join with tasks collection instead of completed_tasks {'$lookup': { - 'from': 'completed_tasks', + 'from': 'tasks', 'localField': '_id', 'foreignField': 'time_entry_id', 'as': 'tasks' - }} + }}, ] log = list(db.time_entries.aggregate(pipeline)) # Basic formatting for entry in log: + # Filter tasks to only completed ones for display cleanly in list view + entry['tasks'] = [t for t in entry['tasks'] if t['status'] == 'completed'] duration = entry['end_time'] - entry['start_time'] entry['duration_str'] = str(duration).split('.')[0] # HH:MM:SS return render_template('logbook.html', log=log) +@app.route('/logbook/', methods=['GET', 'POST']) +def log_entry_detail(entry_id): + if not is_logged_in(): return redirect(url_for('login')) + + # Handle Note Update + if request.method == 'POST': + new_note = request.form.get('note') + db.time_entries.update_one( + {'_id': ObjectId(entry_id), 'user_id': get_user_id()}, + {'$set': {'note': new_note}} + ) + flash('Session note updated') + return redirect(url_for('log_entry_detail', entry_id=entry_id)) + + # Fetch Entry with Activity info + pipeline = [ + {'$match': {'_id': ObjectId(entry_id), 'user_id': get_user_id()}}, + {'$lookup': { + 'from': 'activities', + 'localField': 'activity_id', + 'foreignField': '_id', + 'as': 'activity' + }}, + {'$unwind': '$activity'} + ] + + results = list(db.time_entries.aggregate(pipeline)) + if not results: + return "Entry not found", 404 + + entry = results[0] + + # Calculate duration + if entry.get('end_time') and entry.get('start_time'): + duration = entry['end_time'] - entry['start_time'] + entry['duration_str'] = str(duration).split('.')[0] + else: + entry['duration_str'] = "Running..." + + # Fetch ALL Tasks linked to this entry (completed or not) + tasks = list(db.tasks.find({'time_entry_id': ObjectId(entry_id)})) + + return render_template('logbook_detail.html', entry=entry, tasks=tasks) + if __name__ == '__main__': app.run(debug=True) diff --git a/templates/dashboard.html b/templates/dashboard.html index d8134f9..13b7e9e 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -16,12 +16,15 @@ {% if tasks %}
-

Tasks related to {{ current_entry.activity_name }}:

+

Active Tasks:

{% for task in tasks %} -
+
- + {% if task.status == 'completed' %}checked{% endif %} + onchange="toggleTask('{{ task._id }}', this)"> +
{% endfor %}
@@ -52,18 +55,20 @@ updateTimer(); // run immediately // AJAX for Tasks - function completeTask(name, entryId, checkbox) { - if(!checkbox.checked) return; // Only track completion for now - + function toggleTask(taskId, checkbox) { const formData = new FormData(); - formData.append('task_name', name); - formData.append('entry_id', entryId); + formData.append('task_id', taskId); + formData.append('is_checked', checkbox.checked); fetch('/complete_task', { method: 'POST', body: formData }) .then(r => r.json()) .then(data => { - checkbox.disabled = true; // Prevent double logging - checkbox.parentElement.style.textDecoration = "line-through"; + const label = checkbox.nextElementSibling; + if(checkbox.checked) { + label.style.textDecoration = "line-through"; + } else { + label.style.textDecoration = "none"; + } }); } diff --git a/templates/layout.html b/templates/layout.html index a4335c5..cec307a 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -31,6 +31,7 @@
{% if session.user_id %} Tracker + Tasks Logbook Logout {% else %} diff --git a/templates/logbook.html b/templates/logbook.html index 31d0a99..d7966b7 100644 --- a/templates/logbook.html +++ b/templates/logbook.html @@ -26,7 +26,7 @@ Tasks Completed:
    {% for task in entry.tasks %} -
  • {{ task.task_name }} ({{ task.completed_at.strftime('%H:%M') }})
  • +
  • {{ task.name }} ({{ task.completed_at.strftime('%H:%M') if task.completed_at else 'Done' }})
  • {% endfor %}
diff --git a/templates/task_detail.html b/templates/task_detail.html new file mode 100644 index 0000000..95a9ac6 --- /dev/null +++ b/templates/task_detail.html @@ -0,0 +1,39 @@ +{% extends "layout.html" %} +{% block content %} +
+
+
+

{{ task.name }}

+

+ Status: {{ task.status|upper }}
+ {% if task.is_template %} + Template (Auto-Add) + {% endif %} +

+
+ Back to Tasks +
+
+ +
+

Comments

+ +
+ {% if task.comments %} + {% for comment in task.comments %} +
+

{{ comment.text }}

+ {{ comment.created_at.strftime('%Y-%m-%d %H:%M') }} +
+ {% endfor %} + {% else %} +

No comments yet.

+ {% endif %} +
+ +
+ + +
+
+{% endblock %} diff --git a/templates/tasks.html b/templates/tasks.html new file mode 100644 index 0000000..d23934a --- /dev/null +++ b/templates/tasks.html @@ -0,0 +1,79 @@ +{% extends "layout.html" %} +{% block content %} +
+ +
+

Create New Task

+
+ + + + + + + + + +
+ + +
+ + +
+
+ + +
+ +
+

Pending Tasks

+ {% if not tasks %} +

No open tasks.

+ {% else %} +
    + {% for task in tasks %} +
  • +
    + {{ task.name }} + {% if task.activity_name %} + + {{ task.activity_name }} + + {% endif %} +
    + {% if task.due_date %} + Due: {{ task.due_date.strftime('%Y-%m-%d %H:%M') }} + {% endif %} +
  • + {% endfor %} +
+ {% endif %} +
+ + +
+

Auto-Added Templates

+ These tasks are automatically created when you start the associated activity. + {% if not templates %} +

No templates defined.

+ {% else %} +
    + {% for t in templates %} +
  • + {{ t.name }} + (on {{ t.activity_name|default('Unknown Activity') }}) + [Edit] +
  • + {% endfor %} +
+ {% endif %} +
+
+
+{% endblock %}