tasks
This commit is contained in:
208
app.py
208
app.py
@@ -40,20 +40,29 @@ def index():
|
|||||||
'end_time': None
|
'end_time': None
|
||||||
})
|
})
|
||||||
|
|
||||||
active_tasks_template = []
|
active_tasks = []
|
||||||
if current_entry:
|
if current_entry:
|
||||||
# Get tasks associated with the active activity type
|
# Get tasks associated with the active activity type
|
||||||
active_activity = db.activities.find_one({'_id': current_entry['activity_id']})
|
active_activity = db.activities.find_one({'_id': current_entry['activity_id']})
|
||||||
if active_activity:
|
if active_activity:
|
||||||
current_entry['activity_name'] = active_activity['name']
|
current_entry['activity_name'] = active_activity['name']
|
||||||
current_entry['activity_color'] = active_activity.get('color', '#3498db')
|
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',
|
return render_template('dashboard.html',
|
||||||
activities=activities,
|
activities=activities,
|
||||||
current_entry=current_entry,
|
current_entry=current_entry,
|
||||||
tasks=active_tasks_template)
|
tasks=active_tasks)
|
||||||
|
|
||||||
@app.route('/register', methods=['GET', 'POST'])
|
@app.route('/register', methods=['GET', 'POST'])
|
||||||
def register():
|
def register():
|
||||||
@@ -66,9 +75,12 @@ def register():
|
|||||||
return redirect(url_for('register'))
|
return redirect(url_for('register'))
|
||||||
|
|
||||||
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
|
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
|
||||||
db.users.insert_one({'username': username, 'password': hashed_password})
|
user_id = db.users.insert_one({'username': username, 'password': hashed_password}).inserted_id
|
||||||
flash('Account created! Please login.')
|
|
||||||
return redirect(url_for('login'))
|
# 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')
|
return render_template('register.html')
|
||||||
|
|
||||||
@@ -132,20 +144,39 @@ def toggle_timer(activity_id):
|
|||||||
{'$set': {'end_time': datetime.now()}}
|
{'$set': {'end_time': datetime.now()}}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Stop ANY other running timer first (single tasking)
|
# Stop ANY other running timer first
|
||||||
db.time_entries.update_many(
|
db.time_entries.update_many(
|
||||||
{'user_id': user_id, 'end_time': None},
|
{'user_id': user_id, 'end_time': None},
|
||||||
{'$set': {'end_time': datetime.now()}}
|
{'$set': {'end_time': datetime.now()}}
|
||||||
)
|
)
|
||||||
# Start new
|
# Start new
|
||||||
db.time_entries.insert_one({
|
new_entry_id = db.time_entries.insert_one({
|
||||||
'user_id': user_id,
|
'user_id': user_id,
|
||||||
'activity_id': ObjectId(activity_id),
|
'activity_id': ObjectId(activity_id),
|
||||||
'start_time': datetime.now(),
|
'start_time': datetime.now(),
|
||||||
'end_time': None,
|
'end_time': None,
|
||||||
'note': note
|
'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'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
@app.route('/stop_timer', methods=['POST'])
|
@app.route('/stop_timer', methods=['POST'])
|
||||||
@@ -161,18 +192,108 @@ def stop_timer():
|
|||||||
def complete_task():
|
def complete_task():
|
||||||
if not is_logged_in(): return jsonify({'error': 'auth'}), 401
|
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_id = request.form['task_id']
|
||||||
task_name = request.form['task_name']
|
is_checked = request.form['is_checked'] == 'true'
|
||||||
entry_id = request.form['entry_id']
|
|
||||||
|
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)
|
||||||
|
|
||||||
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'})
|
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')
|
@app.route('/logbook')
|
||||||
def logbook():
|
def logbook():
|
||||||
if not is_logged_in(): return redirect(url_for('login'))
|
if not is_logged_in(): return redirect(url_for('login'))
|
||||||
@@ -188,22 +309,69 @@ def logbook():
|
|||||||
'as': 'activity'
|
'as': 'activity'
|
||||||
}},
|
}},
|
||||||
{'$unwind': '$activity'},
|
{'$unwind': '$activity'},
|
||||||
|
# Join with tasks collection instead of completed_tasks
|
||||||
{'$lookup': {
|
{'$lookup': {
|
||||||
'from': 'completed_tasks',
|
'from': 'tasks',
|
||||||
'localField': '_id',
|
'localField': '_id',
|
||||||
'foreignField': 'time_entry_id',
|
'foreignField': 'time_entry_id',
|
||||||
'as': 'tasks'
|
'as': 'tasks'
|
||||||
}}
|
}},
|
||||||
]
|
]
|
||||||
|
|
||||||
log = list(db.time_entries.aggregate(pipeline))
|
log = list(db.time_entries.aggregate(pipeline))
|
||||||
|
|
||||||
# Basic formatting
|
# Basic formatting
|
||||||
for entry in log:
|
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']
|
duration = entry['end_time'] - entry['start_time']
|
||||||
entry['duration_str'] = str(duration).split('.')[0] # HH:MM:SS
|
entry['duration_str'] = str(duration).split('.')[0] # HH:MM:SS
|
||||||
|
|
||||||
return render_template('logbook.html', log=log)
|
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__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True)
|
app.run(debug=True)
|
||||||
|
|||||||
@@ -16,12 +16,15 @@
|
|||||||
<!-- Tasks for current activity -->
|
<!-- Tasks for current activity -->
|
||||||
{% if tasks %}
|
{% if tasks %}
|
||||||
<div style="margin: 1rem 0; padding: 1rem; background: rgba(255,255,255,0.7); border-radius: 5px;">
|
<div style="margin: 1rem 0; padding: 1rem; background: rgba(255,255,255,0.7); border-radius: 5px;">
|
||||||
<h4>Tasks related to {{ current_entry.activity_name }}:</h4>
|
<h4>Active Tasks:</h4>
|
||||||
{% for task in tasks %}
|
{% for task in tasks %}
|
||||||
<div style="margin-bottom: 5px;">
|
<div style="margin-bottom: 5px; display: flex; align-items: center;">
|
||||||
<input type="checkbox" id="task_{{ task._id }}"
|
<input type="checkbox" id="task_{{ task._id }}"
|
||||||
onchange="completeTask('{{ task.name }}', '{{ current_entry._id }}', this)">
|
{% if task.status == 'completed' %}checked{% endif %}
|
||||||
<label for="task_{{ task._id }}">{{ task.name }}</label>
|
onchange="toggleTask('{{ task._id }}', this)">
|
||||||
|
<label for="task_{{ task._id }}" style="margin-left: 10px; flex-grow: 1; {% if task.status == 'completed' %}text-decoration: line-through;{% endif %}">
|
||||||
|
<a href="{{ url_for('task_detail', task_id=task._id) }}">{{ task.name }}</a>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -52,18 +55,20 @@
|
|||||||
updateTimer(); // run immediately
|
updateTimer(); // run immediately
|
||||||
|
|
||||||
// AJAX for Tasks
|
// AJAX for Tasks
|
||||||
function completeTask(name, entryId, checkbox) {
|
function toggleTask(taskId, checkbox) {
|
||||||
if(!checkbox.checked) return; // Only track completion for now
|
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('task_name', name);
|
formData.append('task_id', taskId);
|
||||||
formData.append('entry_id', entryId);
|
formData.append('is_checked', checkbox.checked);
|
||||||
|
|
||||||
fetch('/complete_task', { method: 'POST', body: formData })
|
fetch('/complete_task', { method: 'POST', body: formData })
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
checkbox.disabled = true; // Prevent double logging
|
const label = checkbox.nextElementSibling;
|
||||||
checkbox.parentElement.style.textDecoration = "line-through";
|
if(checkbox.checked) {
|
||||||
|
label.style.textDecoration = "line-through";
|
||||||
|
} else {
|
||||||
|
label.style.textDecoration = "none";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
<div>
|
<div>
|
||||||
{% if session.user_id %}
|
{% if session.user_id %}
|
||||||
<a href="{{ url_for('index') }}">Tracker</a>
|
<a href="{{ url_for('index') }}">Tracker</a>
|
||||||
|
<a href="{{ url_for('tasks') }}">Tasks</a> <!-- Added -->
|
||||||
<a href="{{ url_for('logbook') }}">Logbook</a>
|
<a href="{{ url_for('logbook') }}">Logbook</a>
|
||||||
<a href="{{ url_for('logout') }}">Logout</a>
|
<a href="{{ url_for('logout') }}">Logout</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<strong>Tasks Completed:</strong>
|
<strong>Tasks Completed:</strong>
|
||||||
<ul style="margin: 5px 0; padding-left: 20px;">
|
<ul style="margin: 5px 0; padding-left: 20px;">
|
||||||
{% for task in entry.tasks %}
|
{% for task in entry.tasks %}
|
||||||
<li>{{ task.task_name }} <small>({{ task.completed_at.strftime('%H:%M') }})</small></li>
|
<li>{{ task.name }} <small>({{ task.completed_at.strftime('%H:%M') if task.completed_at else 'Done' }})</small></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
39
templates/task_detail.html
Normal file
39
templates/task_detail.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||||||
|
<div>
|
||||||
|
<h2>{{ task.name }}</h2>
|
||||||
|
<p>
|
||||||
|
<strong>Status:</strong> {{ task.status|upper }}<br>
|
||||||
|
{% if task.is_template %}
|
||||||
|
<span style="background: #f39c12; color: white; padding: 3px 8px; border-radius: 5px;">Template (Auto-Add)</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ url_for('tasks') }}" class="btn" style="background: #95a5a6;">Back to Tasks</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Comments</h3>
|
||||||
|
|
||||||
|
<div style="background: #f9f9f9; padding: 1rem; border-radius: 5px; margin-bottom: 2rem;">
|
||||||
|
{% if task.comments %}
|
||||||
|
{% for comment in task.comments %}
|
||||||
|
<div style="border-bottom: 1px solid #ddd; padding: 10px 0;">
|
||||||
|
<p style="margin: 0;">{{ comment.text }}</p>
|
||||||
|
<small style="color: #999;">{{ comment.created_at.strftime('%Y-%m-%d %H:%M') }}</small>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p style="color: #ccc;">No comments yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<textarea name="comment" rows="3" style="width: 100%; border: 1px solid #ddd; border-radius: 5px; padding: 0.5rem;" placeholder="Write a comment..." required></textarea>
|
||||||
|
<button type="submit" class="btn" style="margin-top: 10px;">Add Comment</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
79
templates/tasks.html
Normal file
79
templates/tasks.html
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div style="display: flex; gap: 2rem; flex-wrap: wrap;">
|
||||||
|
<!-- Create Task Form -->
|
||||||
|
<div class="card" style="flex: 1; min-width: 300px;">
|
||||||
|
<h3>Create New Task</h3>
|
||||||
|
<form action="{{ url_for('create_task') }}" method="POST">
|
||||||
|
<label>Task Name</label>
|
||||||
|
<input type="text" name="name" required placeholder="e.g. Write Report">
|
||||||
|
|
||||||
|
<label>Associate with Activity (Optional)</label>
|
||||||
|
<select name="activity_id" style="width: 100%; padding: 0.5rem; margin-bottom: 1rem;">
|
||||||
|
<option value="">-- None --</option>
|
||||||
|
{% for act in activities %}
|
||||||
|
<option value="{{ act._id }}">{{ act.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Due Date & Time (Optional)</label>
|
||||||
|
<input type="datetime-local" name="due_date" style="width: 100%; padding: 0.5rem; margin-bottom: 1rem;">
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1rem;">
|
||||||
|
<input type="checkbox" id="is_template" name="is_template" value="1">
|
||||||
|
<label for="is_template"><strong>Auto-add on Start?</strong> (If checked, this task is added every time the selected activity starts)</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn">Create Task</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Task Lists -->
|
||||||
|
<div style="flex: 2; min-width: 300px;">
|
||||||
|
<!-- Regular Tasks -->
|
||||||
|
<div class="card">
|
||||||
|
<h3>Pending Tasks</h3>
|
||||||
|
{% if not tasks %}
|
||||||
|
<p>No open tasks.</p>
|
||||||
|
{% else %}
|
||||||
|
<ul style="list-style: none; padding: 0;">
|
||||||
|
{% for task in tasks %}
|
||||||
|
<li style="border-bottom: 1px solid #eee; padding: 10px 0;">
|
||||||
|
<div style="display: flex; justify-content: space-between;">
|
||||||
|
<a href="{{ url_for('task_detail', task_id=task._id) }}" style="font-weight: bold;">{{ task.name }}</a>
|
||||||
|
{% if task.activity_name %}
|
||||||
|
<span style="background: {{ task.activity_color }}; color: white; padding: 2px 8px; border-radius: 10px; font-size: 0.8rem;">
|
||||||
|
{{ task.activity_name }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if task.due_date %}
|
||||||
|
<small style="color: #e74c3c;">Due: {{ task.due_date.strftime('%Y-%m-%d %H:%M') }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Templates -->
|
||||||
|
<div class="card" style="background: #fdfdfd;">
|
||||||
|
<h3>Auto-Added Templates</h3>
|
||||||
|
<small>These tasks are automatically created when you start the associated activity.</small>
|
||||||
|
{% if not templates %}
|
||||||
|
<p>No templates defined.</p>
|
||||||
|
{% else %}
|
||||||
|
<ul style="padding-left: 20px;">
|
||||||
|
{% for t in templates %}
|
||||||
|
<li>
|
||||||
|
<strong>{{ t.name }}</strong>
|
||||||
|
<span style="color: #666;">(on {{ t.activity_name|default('Unknown Activity') }})</span>
|
||||||
|
<a href="{{ url_for('task_detail', task_id=t._id) }}" style="font-size: 0.8rem;">[Edit]</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user