Goals working well

This commit is contained in:
2026-02-10 19:46:25 +01:00
parent 37190679ed
commit 9ef70e0436
5 changed files with 398 additions and 15 deletions

142
app.py
View File

@@ -2,7 +2,7 @@ from flask import Flask, render_template, request, redirect, url_for, session, f
from flask_bcrypt import Bcrypt from flask_bcrypt import Bcrypt
from pymongo import MongoClient from pymongo import MongoClient
from bson.objectid import ObjectId from bson.objectid import ObjectId
from datetime import datetime from datetime import datetime, timedelta
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -109,18 +109,23 @@ def add_activity():
'user_id': get_user_id(), 'user_id': get_user_id(),
'name': name, 'name': name,
'color': color, 'color': color,
'subcategories': [] # Initialize empty list 'subcategories': []
}).inserted_id }).inserted_id
# Add optional tasks # Add optional default tasks (Revised from list input)
tasks_text = request.form.get('tasks', '') tasks_str = request.form.get('tasks_list_data', '')
if tasks_text: if tasks_str:
tasks = [t.strip() for t in tasks_text.split(',') if t.strip()] tasks = [t.strip() for t in tasks_str.split(',') if t.strip()]
for t in tasks: for t in tasks:
db.task_templates.insert_one({ db.tasks.insert_one({
'activity_id': activity_id,
'user_id': get_user_id(), 'user_id': get_user_id(),
'name': t 'name': t,
'activity_id': ObjectId(activity_id),
'is_template': True,
'status': 'open',
'time_entry_id': None,
'created_at': datetime.now(),
'comments': []
}) })
return redirect(url_for('index')) return redirect(url_for('index'))
@@ -183,7 +188,7 @@ def start_timer_bg(activity_id):
'user_id': user_id, 'user_id': user_id,
'name': t['name'], 'name': t['name'],
'activity_id': ObjectId(activity_id), 'activity_id': ObjectId(activity_id),
'time_entry_id': new_entry_id, 'time_entry_id': new_entry_id, # Link to this specific session
'status': 'open', 'status': 'open',
'is_template': False, 'is_template': False,
'created_at': datetime.now(), 'created_at': datetime.now(),
@@ -498,5 +503,122 @@ def log_entry_detail(entry_id):
return render_template('logbook_detail.html', entry=entry, tasks=tasks) return render_template('logbook_detail.html', entry=entry, tasks=tasks)
@app.route('/goals', methods=['GET', 'POST'])
def goals():
if not is_logged_in(): return redirect(url_for('login'))
user_id = get_user_id()
# Handle creating new goal
if request.method == 'POST':
name = request.form['name']
target_hours = float(request.form['target_hours'])
frequency = request.form['frequency'] # daily, weekly, monthly, yearly
activity_id = request.form.get('activity_id') # Required now
subcategory = request.form.get('subcategory', '')
if not activity_id:
flash("You must select an activity for this goal.")
return redirect(url_for('goals'))
db.goals.insert_one({
'user_id': user_id,
'name': name,
'target_hours': target_hours,
'frequency': frequency,
'activity_id': ObjectId(activity_id),
'subcategory': subcategory,
'created_at': datetime.now()
})
return redirect(url_for('goals'))
# Fetch goals and calculate progress
goals_list = list(db.goals.find({'user_id': user_id}))
activities = list(db.activities.find({'user_id': user_id}))
today = datetime.now()
start_of_today = today.replace(hour=0, minute=0, second=0, microsecond=0)
# Determine date ranges
ranges = {
'daily': start_of_today,
'weekly': start_of_today - timedelta(days=today.weekday()), # Monday
'monthly': start_of_today.replace(day=1),
'yearly': start_of_today.replace(month=1, day=1)
}
for goal in goals_list:
start_date = ranges.get(goal['frequency'], start_of_today)
query = {
'user_id': user_id,
'start_time': {'$gte': start_date},
'end_time': {'$ne': None}
}
if goal.get('activity_id'):
query['activity_id'] = goal['activity_id']
if goal.get('subcategory'):
query['subcategory'] = goal['subcategory']
entries = list(db.time_entries.find(query))
total_seconds = sum([(e['end_time'] - e['start_time']).total_seconds() for e in entries])
current_hours = total_seconds / 3600
# Display formatting: Show minutes if < 1 hour, else show hours
if current_hours > 0 and current_hours < 1:
minutes = int(total_seconds / 60)
goal['display_progress'] = f"{minutes}m"
else:
goal['display_progress'] = f"{round(current_hours, 1)}h"
goal['current_hours'] = round(current_hours, 1) # Keep purely numeric for math/sorting if needed
goal['percent'] = min(100, int((current_hours / goal['target_hours']) * 100))
# Link activity name for display
if goal.get('activity_id'):
act = db.activities.find_one({'_id': goal['activity_id']})
goal['activity_name'] = act['name'] if act else 'Unknown'
goal['activity_color'] = act['color'] if act else '#ccc'
return render_template('goals.html', goals=goals_list, activities=activities)
@app.route('/edit_goal/<goal_id>', methods=['GET', 'POST'])
def edit_goal(goal_id):
if not is_logged_in(): return redirect(url_for('login'))
user_id = get_user_id()
if request.method == 'POST':
name = request.form['name']
target_hours = float(request.form['target_hours'])
frequency = request.form['frequency']
activity_id = request.form.get('activity_id')
subcategory = request.form.get('subcategory', '')
db.goals.update_one(
{'_id': ObjectId(goal_id), 'user_id': user_id},
{'$set': {
'name': name,
'target_hours': target_hours,
'frequency': frequency,
'activity_id': ObjectId(activity_id),
'subcategory': subcategory
}}
)
return redirect(url_for('goals'))
goal = db.goals.find_one({'_id': ObjectId(goal_id), 'user_id': user_id})
if not goal: return "Goal not found", 404
activities = list(db.activities.find({'user_id': user_id}))
return render_template('edit_goal.html', goal=goal, activities=activities)
@app.route('/delete_goal/<goal_id>')
def delete_goal(goal_id):
if not is_logged_in(): return redirect(url_for('login'))
db.goals.delete_one({'_id': ObjectId(goal_id), 'user_id': get_user_id()})
return redirect(url_for('goals'))
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True) app.run(debug=True)

View File

@@ -189,18 +189,83 @@
<!-- Create Activity Form --> <!-- Create Activity Form -->
<div id="newActivityForm" class="card" style="display:none; margin-top: 2rem;"> <div id="newActivityForm" class="card" style="display:none; margin-top: 2rem;">
<h3>Create New Activity Category</h3> <h3>Create New Activity Category</h3>
<form action="{{ url_for('add_activity') }}" method="POST"> <form action="{{ url_for('add_activity') }}" method="POST" onsubmit="prepareTasksList()">
<label>Name</label> <label>Name</label>
<input type="text" name="name" required placeholder="e.g. Household"> <input type="text" name="name" required placeholder="e.g. Household">
<label>Color</label> <label>Color</label>
<input type="color" name="color" value="#3498db" style="width:100%; height:40px; border:none;"> <input type="color" name="color" value="#3498db" style="width:100%; height:40px; border:none;">
<label>Default Tasks (comma separated)</label> <label>Default Tasks (Template)</label>
<input type="text" name="tasks" placeholder="e.g. Laundry, Dishes, Trash"> <p style="font-size: 0.8rem; color: var(--text-secondary); margin-top: -10px;">
These tasks will be created automatically every time you start this activity.
</p>
<button type="submit" class="btn">Create</button> <div style="display: flex; gap: 5px; margin-bottom: 10px;">
<button type="button" class="btn" style="background: #7f8c8d" onclick="this.parentElement.parentElement.style.display='none'">Cancel</button> <input type="text" id="newTaskInput" placeholder="Add task name..." style="margin-bottom: 0;">
<button type="button" class="btn" style="background: #27ae60;" onclick="addNewTask()">Add</button>
</div>
<ul id="newTasksListDisplay" style="list-style: none; padding: 0; margin-bottom: 1rem;">
<!-- Items will be injected here -->
</ul>
<input type="hidden" name="tasks_list_data" id="tasksListData">
<div style="margin-top: 1rem;">
<button type="submit" class="btn">Create</button>
<button type="button" class="btn" style="background: var(--text-secondary)" onclick="document.getElementById('newActivityForm').style.display='none'">Cancel</button>
</div>
</form> </form>
</div> </div>
<script>
// Existing timer scripts...
// ...existing code...
// New Task List Logic for Activity Creation
let newActivityTasks = [];
function addNewTask() {
const input = document.getElementById('newTaskInput');
const val = input.value.trim();
if (val) {
newActivityTasks.push(val);
input.value = '';
renderNewActivityTasks();
}
}
function removeNewTask(index) {
newActivityTasks.splice(index, 1);
renderNewActivityTasks();
}
function renderNewActivityTasks() {
const list = document.getElementById('newTasksListDisplay');
list.innerHTML = '';
newActivityTasks.forEach((item, index) => {
const li = document.createElement('li');
li.style.background = '#f7f7f5';
li.style.border = '1px solid var(--border-dim)';
li.style.margin = '5px 0';
li.style.padding = '8px';
li.style.borderRadius = '4px';
li.style.display = 'flex';
li.style.justifyContent = 'space-between';
li.style.alignItems = 'center';
li.style.fontSize = '0.9rem';
li.innerHTML = `
<span>${item}</span>
<span onclick="removeNewTask(${index})" style="cursor: pointer; color: var(--danger-color); font-weight: bold; padding: 0 5px;">&times;</span>
`;
list.appendChild(li);
});
}
function prepareTasksList() {
document.getElementById('tasksListData').value = newActivityTasks.join(',');
}
</script>
{% endblock %} {% endblock %}

84
templates/edit_goal.html Normal file
View File

@@ -0,0 +1,84 @@
{% extends "layout.html" %}
{% block content %}
<div class="card" style="max-width: 500px; margin: auto;">
<h2>Edit Goal</h2>
<form method="POST">
<label>Goal Name</label>
<input type="text" name="name" value="{{ goal.name }}" required>
<label>Activity</label>
<select name="activity_id" id="activitySelect" required onchange="updateSubcategories()">
<option value="">-- Select Activity --</option>
{% for act in activities %}
<option value="{{ act._id }}"
data-subcats='{{ act.subcategories|default([])|tojson }}'
{% if act._id|string == goal.activity_id|string %}selected{% endif %}>
{{ act.name }}
</option>
{% endfor %}
</select>
<div id="subcatWrapper" style="display:none;">
<label>Subcategory (Optional)</label>
<select name="subcategory" id="subcategorySelect">
<option value="">-- All --</option>
</select>
</div>
<label>Frequency</label>
<select name="frequency">
<option value="daily" {% if goal.frequency == 'daily' %}selected{% endif %}>Daily</option>
<option value="weekly" {% if goal.frequency == 'weekly' %}selected{% endif %}>Weekly</option>
<option value="monthly" {% if goal.frequency == 'monthly' %}selected{% endif %}>Monthly</option>
<option value="yearly" {% if goal.frequency == 'yearly' %}selected{% endif %}>Yearly</option>
</select>
<label>Target Hours</label>
<input type="number" name="target_hours" step="0.1" value="{{ goal.target_hours }}" required>
<div style="margin-top: 1rem; display: flex; gap: 10px;">
<button type="submit" class="btn">Save Changes</button>
<a href="{{ url_for('goals') }}" class="btn" style="background: var(--text-secondary); text-decoration: none;">Cancel</a>
</div>
</form>
</div>
<script>
// Logic to populate subcategories and set current value
const currentSubcat = "{{ goal.subcategory }}";
function updateSubcategories() {
const actSelect = document.getElementById('activitySelect');
const subWrapper = document.getElementById('subcatWrapper');
const subSelect = document.getElementById('subcategorySelect');
const selectedOption = actSelect.options[actSelect.selectedIndex];
if (!selectedOption.value) {
subWrapper.style.display = 'none';
return;
}
const subcats = JSON.parse(selectedOption.getAttribute('data-subcats') || '[]');
subSelect.innerHTML = '<option value="">-- All --</option>';
if (subcats && subcats.length > 0) {
subWrapper.style.display = 'block';
subcats.forEach(s => {
const opt = document.createElement('option');
opt.value = s;
opt.innerText = s;
if (s === currentSubcat && actSelect.value === "{{ goal.activity_id }}") {
opt.selected = true;
}
subSelect.appendChild(opt);
});
} else {
subWrapper.style.display = 'none';
}
}
// Initialize on load
updateSubcategories();
</script>
{% endblock %}

111
templates/goals.html Normal file
View File

@@ -0,0 +1,111 @@
{% extends "layout.html" %}
{% block content %}
<div class="card" style="margin-bottom: 2rem;">
<h2>My Goals</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem;">
{% for goal in goals %}
<div style="border: 1px solid var(--border-dim); border-radius: 8px; padding: 1.5rem; position: relative;">
<div style="position: absolute; top: 10px; right: 10px;">
<a href="{{ url_for('edit_goal', goal_id=goal._id) }}" style="color: var(--text-secondary); text-decoration: none; font-size: 0.9rem; margin-right: 8px;">Edit</a>
<a href="{{ url_for('delete_goal', goal_id=goal._id) }}" style="color: var(--danger-color); text-decoration: none; font-size: 1.2rem;">&times;</a>
</div>
<h3 style="font-size: 1.2rem; margin-bottom: 0.5rem; padding-right: 40px;">{{ goal.name }}</h3>
<div style="font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 1rem;">
{{ goal.frequency|capitalize }} Target: {{ goal.target_hours }}h
<br>
<div style="margin-top: 5px;">
<span style="background: {{ goal.activity_color }}; color: white; padding: 2px 6px; border-radius: 4px; font-size: 0.8rem;">
{{ goal.activity_name }}
</span>
{% if goal.subcategory %}
<span style="color: var(--text-secondary); font-size: 0.8rem;"> &gt; {{ goal.subcategory }}</span>
{% endif %}
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 5px;">
<!-- Use smart formatted progress (mins or hours) -->
<span style="font-weight: bold; font-size: 1.5rem; color: var(--primary-color);">{{ goal.display_progress }}</span>
<span style="font-size: 0.9rem; color: var(--text-secondary);">{{ goal.percent }}%</span>
</div>
<!-- Progress Bar -->
<div style="width: 100%; height: 8px; background: var(--bg-input); border-radius: 4px; overflow: hidden;">
<div style="width: {{ goal.percent }}%; height: 100%; background: {% if goal.percent >= 100 %}#27ae60{% else %}var(--primary-color){% endif %}; transition: width 0.5s;"></div>
</div>
</div>
{% else %}
<p style="color: var(--text-secondary);">No goals set yet.</p>
{% endfor %}
</div>
</div>
<div class="card" style="max-width: 500px;">
<h3>Set New Goal</h3>
<form method="POST">
<label>Goal Name</label>
<input type="text" name="name" placeholder="e.g. Learn Python" required>
<label>Activity (Required)</label>
<select name="activity_id" id="activitySelect" required onchange="updateSubcategories()">
<option value="">-- Select Activity --</option>
{% for act in activities %}
<option value="{{ act._id }}" data-subcats='{{ act.subcategories|default([])|tojson }}'>{{ act.name }}</option>
{% endfor %}
</select>
<div id="subcatWrapper" style="display:none;">
<label>Subcategory (Optional)</label>
<select name="subcategory" id="subcategorySelect">
<!-- Javascript will populate this -->
</select>
</div>
<label>Frequency</label>
<select name="frequency">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="yearly">Yearly</option>
</select>
<label>Target Hours</label>
<input type="number" name="target_hours" step="0.5" placeholder="e.g. 1.0" required>
<button type="submit" class="btn" style="margin-top: 1rem;">Create Goal</button>
</form>
</div>
<script>
function updateSubcategories() {
const actSelect = document.getElementById('activitySelect');
const subWrapper = document.getElementById('subcatWrapper');
const subSelect = document.getElementById('subcategorySelect');
const selectedOption = actSelect.options[actSelect.selectedIndex];
// Handle case where no activity is selected
if (!selectedOption.value) {
subWrapper.style.display = 'none';
return;
}
const subcats = JSON.parse(selectedOption.getAttribute('data-subcats') || '[]');
subSelect.innerHTML = '<option value="">-- All --</option>';
if (subcats && subcats.length > 0) {
subWrapper.style.display = 'block';
subcats.forEach(s => {
const opt = document.createElement('option');
opt.value = s;
opt.innerText = s;
subSelect.appendChild(opt);
});
} else {
subWrapper.style.display = 'none';
}
}
</script>
{% endblock %}

View File

@@ -160,6 +160,7 @@
{% 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> <a href="{{ url_for('tasks') }}">Tasks</a>
<a href="{{ url_for('goals') }}">Goals</a>
<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 %}