from flask import Flask, render_template, request, redirect, url_for, session, flash, jsonify from flask_bcrypt import Bcrypt from pymongo import MongoClient from bson.objectid import ObjectId from datetime import datetime import os from dotenv import load_dotenv load_dotenv() app = Flask(__name__) app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') client = MongoClient(os.getenv('MONGO_URI')) db = client.get_default_database() bcrypt = Bcrypt(app) # --- Helpers --- def get_user_id(): return session.get('user_id') def is_logged_in(): return 'user_id' in session # --- Routes --- @app.route('/') def index(): if not is_logged_in(): return redirect(url_for('login')) user_id = get_user_id() # Get all activities activities = list(db.activities.find({'user_id': user_id})) # Check for active time entry current_entry = db.time_entries.find_one({ 'user_id': user_id, 'end_time': None }) active_tasks_template = [] 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']})) return render_template('dashboard.html', activities=activities, current_entry=current_entry, tasks=active_tasks_template) @app.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] if db.users.find_one({'username': username}): flash('Username already exists') 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')) return render_template('register.html') @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': user = db.users.find_one({'username': request.form['username']}) if user and bcrypt.check_password_hash(user['password'], request.form['password']): session['user_id'] = str(user['_id']) return redirect(url_for('index')) flash('Login Unsuccessful. Please check username and password') return render_template('login.html') @app.route('/logout') def logout(): session.clear() return redirect(url_for('login')) @app.route('/add_activity', methods=['POST']) def add_activity(): if not is_logged_in(): return redirect(url_for('login')) name = request.form['name'] color = request.form.get('color', '#3498db') activity_id = db.activities.insert_one({ 'user_id': get_user_id(), 'name': name, 'color': color }).inserted_id # Add optional tasks tasks_text = request.form.get('tasks', '') if tasks_text: tasks = [t.strip() for t in tasks_text.split(',') if t.strip()] for t in tasks: db.task_templates.insert_one({ 'activity_id': activity_id, 'user_id': get_user_id(), 'name': t }) return redirect(url_for('index')) @app.route('/toggle_timer/', methods=['POST']) def toggle_timer(activity_id): if not is_logged_in(): return redirect(url_for('login')) user_id = get_user_id() note = request.form.get('note', '') # Check if this activity is currently running current = db.time_entries.find_one({ 'user_id': user_id, 'activity_id': ObjectId(activity_id), 'end_time': None }) if current: # Stop it db.time_entries.update_one( {'_id': current['_id']}, {'$set': {'end_time': datetime.now()}} ) else: # Stop ANY other running timer first (single tasking) db.time_entries.update_many( {'user_id': user_id, 'end_time': None}, {'$set': {'end_time': datetime.now()}} ) # Start new db.time_entries.insert_one({ 'user_id': user_id, 'activity_id': ObjectId(activity_id), 'start_time': datetime.now(), 'end_time': None, 'note': note }) return redirect(url_for('index')) @app.route('/stop_timer', methods=['POST']) def stop_timer(): if not is_logged_in(): return redirect(url_for('login')) db.time_entries.update_many( {'user_id': get_user_id(), 'end_time': None}, {'$set': {'end_time': datetime.now()}} ) return redirect(url_for('index')) @app.route('/complete_task', methods=['POST']) 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'] db.completed_tasks.insert_one({ 'user_id': get_user_id(), 'task_name': task_name, 'time_entry_id': ObjectId(entry_id), 'completed_at': datetime.now() }) return jsonify({'status': 'success'}) @app.route('/logbook') def logbook(): if not is_logged_in(): return redirect(url_for('login')) # Agrregation to join activities and tasks pipeline = [ {'$match': {'user_id': get_user_id(), 'end_time': {'$ne': None}}}, {'$sort': {'start_time': -1}}, {'$lookup': { 'from': 'activities', 'localField': 'activity_id', 'foreignField': '_id', 'as': 'activity' }}, {'$unwind': '$activity'}, {'$lookup': { 'from': 'completed_tasks', 'localField': '_id', 'foreignField': 'time_entry_id', 'as': 'tasks' }} ] log = list(db.time_entries.aggregate(pipeline)) # Basic formatting for entry in log: duration = entry['end_time'] - entry['start_time'] entry['duration_str'] = str(duration).split('.')[0] # HH:MM:SS return render_template('logbook.html', log=log) if __name__ == '__main__': app.run(debug=True)