tasks
This commit is contained in:
208
app.py
208
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/<task_id>', 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/<entry_id>', 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)
|
||||
|
||||
Reference in New Issue
Block a user