subcategories and editing for tasks
This commit is contained in:
127
app.py
127
app.py
@@ -108,7 +108,8 @@ def add_activity():
|
|||||||
activity_id = db.activities.insert_one({
|
activity_id = db.activities.insert_one({
|
||||||
'user_id': get_user_id(),
|
'user_id': get_user_id(),
|
||||||
'name': name,
|
'name': name,
|
||||||
'color': color
|
'color': color,
|
||||||
|
'subcategories': [] # Initialize empty list
|
||||||
}).inserted_id
|
}).inserted_id
|
||||||
|
|
||||||
# Add optional tasks
|
# Add optional tasks
|
||||||
@@ -124,11 +125,100 @@ def add_activity():
|
|||||||
|
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route('/edit_activity/<activity_id>', methods=['GET', 'POST'])
|
||||||
|
def edit_activity(activity_id):
|
||||||
|
if not is_logged_in(): return redirect(url_for('login'))
|
||||||
|
user_id = get_user_id()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
name = request.form['name']
|
||||||
|
color = request.form.get('color', '#3498db')
|
||||||
|
subcats_str = request.form.get('subcategories', '')
|
||||||
|
|
||||||
|
# Parse comma-separated string into list
|
||||||
|
subcategories = [s.strip() for s in subcats_str.split(',') if s.strip()]
|
||||||
|
|
||||||
|
db.activities.update_one(
|
||||||
|
{'_id': ObjectId(activity_id), 'user_id': user_id},
|
||||||
|
{'$set': {'name': name, 'color': color, 'subcategories': subcategories}}
|
||||||
|
)
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
activity = db.activities.find_one({'_id': ObjectId(activity_id), 'user_id': user_id})
|
||||||
|
if not activity: return "Activity not found", 404
|
||||||
|
|
||||||
|
return render_template('edit_activity.html', activity=activity)
|
||||||
|
|
||||||
|
@app.route('/start_timer_bg/<activity_id>', methods=['POST'])
|
||||||
|
def start_timer_bg(activity_id):
|
||||||
|
if not is_logged_in(): return jsonify({'error': 'auth'}), 401
|
||||||
|
user_id = get_user_id()
|
||||||
|
|
||||||
|
# Stop ANY running timer
|
||||||
|
db.time_entries.update_many(
|
||||||
|
{'user_id': user_id, 'end_time': None},
|
||||||
|
{'$set': {'end_time': datetime.now()}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start new
|
||||||
|
start_time = datetime.now()
|
||||||
|
new_entry_id = db.time_entries.insert_one({
|
||||||
|
'user_id': user_id,
|
||||||
|
'activity_id': ObjectId(activity_id),
|
||||||
|
'start_time': start_time,
|
||||||
|
'end_time': None,
|
||||||
|
'note': '',
|
||||||
|
'subcategory': ''
|
||||||
|
}).inserted_id
|
||||||
|
|
||||||
|
# Auto-add templates
|
||||||
|
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,
|
||||||
|
'status': 'open',
|
||||||
|
'is_template': False,
|
||||||
|
'created_at': datetime.now(),
|
||||||
|
'comments': []
|
||||||
|
})
|
||||||
|
|
||||||
|
activity = db.activities.find_one({'_id': ObjectId(activity_id)})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'start_time': start_time.isoformat(),
|
||||||
|
'activity_name': activity['name'],
|
||||||
|
'activity_color': activity.get('color', '#3498db')
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/update_active_entry', methods=['POST'])
|
||||||
|
def update_active_entry():
|
||||||
|
if not is_logged_in(): return redirect(url_for('login'))
|
||||||
|
user_id = get_user_id()
|
||||||
|
|
||||||
|
note = request.form.get('note', '')
|
||||||
|
subcategory = request.form.get('subcategory', '')
|
||||||
|
|
||||||
|
db.time_entries.update_one(
|
||||||
|
{'user_id': user_id, 'end_time': None},
|
||||||
|
{'$set': {'note': note, 'subcategory': subcategory}}
|
||||||
|
)
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
@app.route('/toggle_timer/<activity_id>', methods=['POST'])
|
@app.route('/toggle_timer/<activity_id>', methods=['POST'])
|
||||||
def toggle_timer(activity_id):
|
def toggle_timer(activity_id):
|
||||||
if not is_logged_in(): return redirect(url_for('login'))
|
if not is_logged_in(): return redirect(url_for('login'))
|
||||||
user_id = get_user_id()
|
user_id = get_user_id()
|
||||||
note = request.form.get('note', '')
|
note = request.form.get('note', '')
|
||||||
|
subcategory = request.form.get('subcategory', '') # Get subcategory
|
||||||
|
|
||||||
# Check if this activity is currently running
|
# Check if this activity is currently running
|
||||||
current = db.time_entries.find_one({
|
current = db.time_entries.find_one({
|
||||||
@@ -150,13 +240,14 @@ def toggle_timer(activity_id):
|
|||||||
{'$set': {'end_time': datetime.now()}}
|
{'$set': {'end_time': datetime.now()}}
|
||||||
)
|
)
|
||||||
# Start new
|
# Start new
|
||||||
new_entry_id = db.time_entries.insert_one({
|
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
|
'subcategory': subcategory # Store it
|
||||||
|
})
|
||||||
|
|
||||||
# Check for Task Templates and auto-add them
|
# Check for Task Templates and auto-add them
|
||||||
templates = db.tasks.find({
|
templates = db.tasks.find({
|
||||||
@@ -345,14 +436,36 @@ def logbook():
|
|||||||
def log_entry_detail(entry_id):
|
def log_entry_detail(entry_id):
|
||||||
if not is_logged_in(): return redirect(url_for('login'))
|
if not is_logged_in(): return redirect(url_for('login'))
|
||||||
|
|
||||||
# Handle Note Update
|
# Handle Updates and Deletion
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
action = request.form.get('action')
|
||||||
|
|
||||||
|
if action == 'delete':
|
||||||
|
db.time_entries.delete_one({'_id': ObjectId(entry_id), 'user_id': get_user_id()})
|
||||||
|
flash('Entry deleted')
|
||||||
|
return redirect(url_for('logbook'))
|
||||||
|
|
||||||
|
elif action == 'update':
|
||||||
new_note = request.form.get('note')
|
new_note = request.form.get('note')
|
||||||
|
start_str = request.form.get('start_time')
|
||||||
|
end_str = request.form.get('end_time')
|
||||||
|
|
||||||
|
update_fields = {'note': new_note}
|
||||||
|
|
||||||
|
try:
|
||||||
|
if start_str:
|
||||||
|
update_fields['start_time'] = datetime.strptime(start_str, '%Y-%m-%dT%H:%M')
|
||||||
|
if end_str:
|
||||||
|
update_fields['end_time'] = datetime.strptime(end_str, '%Y-%m-%dT%H:%M')
|
||||||
|
except ValueError:
|
||||||
|
flash('Invalid date format')
|
||||||
|
return redirect(url_for('log_entry_detail', entry_id=entry_id))
|
||||||
|
|
||||||
db.time_entries.update_one(
|
db.time_entries.update_one(
|
||||||
{'_id': ObjectId(entry_id), 'user_id': get_user_id()},
|
{'_id': ObjectId(entry_id), 'user_id': get_user_id()},
|
||||||
{'$set': {'note': new_note}}
|
{'$set': update_fields}
|
||||||
)
|
)
|
||||||
flash('Session note updated')
|
flash('Entry updated')
|
||||||
return redirect(url_for('log_entry_detail', entry_id=entry_id))
|
return redirect(url_for('log_entry_detail', entry_id=entry_id))
|
||||||
|
|
||||||
# Fetch Entry with Activity info
|
# Fetch Entry with Activity info
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Active Timer Section -->
|
<!-- Active Timer Section -->
|
||||||
{% if current_entry %}
|
<div class="card active-section" id="activeSection" style="{% if not current_entry %}display:none;{% endif %}">
|
||||||
<div class="card active-section">
|
<h3 id="activeName" style="text-align: center; color: {{ current_entry.activity_color if current_entry else '#333' }}">
|
||||||
<h3 style="text-align: center; color: {{ current_entry.activity_color }}">
|
Currently Tracking: {{ current_entry.activity_name if current_entry else '' }}
|
||||||
Currently Tracking: {{ current_entry.activity_name }}
|
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{% if current_entry.note %}
|
<div id="activeSubcatContainer" style="text-align: center; {% if not current_entry or not current_entry.subcategory %}display:none;{% endif %} margin: 0 auto 10px; display: table;">
|
||||||
<p style="text-align: center; color: #666; font-style: italic;">"{{ current_entry.note }}"</p>
|
<div id="activeSubcatDisplay" style="background: #eee; padding: 2px 10px; border-radius: 10px;">
|
||||||
{% endif %}
|
{{ current_entry.subcategory if current_entry else '' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p id="activeNote" style="text-align: center; color: #666; font-style: italic; {% if not current_entry or not current_entry.note %}display:none;{% endif %}">
|
||||||
|
"{{ current_entry.note if current_entry else '' }}"
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="timer-display" id="timer">00:00:00</div>
|
<div class="timer-display" id="timer">00:00:00</div>
|
||||||
|
|
||||||
@@ -33,12 +38,15 @@
|
|||||||
<form action="{{ url_for('stop_timer') }}" method="POST" style="text-align: center;">
|
<form action="{{ url_for('stop_timer') }}" method="POST" style="text-align: center;">
|
||||||
<button type="submit" class="btn btn-danger" style="font-size: 1.2rem; width: 100%;">Stop Activity</button>
|
<button type="submit" class="btn btn-danger" style="font-size: 1.2rem; width: 100%;">Stop Activity</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Script for Timer -->
|
||||||
<script>
|
<script>
|
||||||
// Timer Logic
|
let startTime = {% if current_entry %}{{ current_entry.start_time.timestamp() * 1000 }}{% else %}null{% endif %};
|
||||||
const startTime = new Date("{{ current_entry.start_time }}").getTime();
|
let timerInterval;
|
||||||
|
|
||||||
function updateTimer() {
|
function updateTimer() {
|
||||||
|
if (!startTime) return;
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
const diff = now - startTime;
|
const diff = now - startTime;
|
||||||
|
|
||||||
@@ -46,44 +54,43 @@
|
|||||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
document.getElementById("timer").innerHTML =
|
const timerEl = document.getElementById("timer");
|
||||||
|
if (timerEl) {
|
||||||
|
timerEl.innerHTML =
|
||||||
(hours < 10 ? "0" + hours : hours) + ":" +
|
(hours < 10 ? "0" + hours : hours) + ":" +
|
||||||
(minutes < 10 ? "0" + minutes : minutes) + ":" +
|
(minutes < 10 ? "0" + minutes : minutes) + ":" +
|
||||||
(seconds < 10 ? "0" + seconds : seconds);
|
(seconds < 10 ? "0" + seconds : seconds);
|
||||||
}
|
}
|
||||||
setInterval(updateTimer, 1000);
|
|
||||||
updateTimer(); // run immediately
|
|
||||||
|
|
||||||
// AJAX for Tasks
|
|
||||||
function toggleTask(taskId, checkbox) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('task_id', taskId);
|
|
||||||
formData.append('is_checked', checkbox.checked);
|
|
||||||
|
|
||||||
fetch('/complete_task', { method: 'POST', body: formData })
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
const label = checkbox.nextElementSibling;
|
|
||||||
if(checkbox.checked) {
|
|
||||||
label.style.textDecoration = "line-through";
|
|
||||||
} else {
|
|
||||||
label.style.textDecoration = "none";
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
if (startTime) {
|
||||||
|
timerInterval = setInterval(updateTimer, 1000);
|
||||||
|
updateTimer();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</div>
|
<!-- End Script -->
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Start Activity Section -->
|
<!-- Start Activity Section -->
|
||||||
<div class="activity-grid">
|
<div class="activity-grid" style="margin-top: 2rem;">
|
||||||
{% for act in activities %}
|
{% for act in activities %}
|
||||||
<!-- Clicking opens a small modal or submits form directly. Let's do a form with a prompt for note -->
|
<div style="position: relative;">
|
||||||
<div class="activity-card"
|
<div class="activity-card"
|
||||||
style="background-color: {{ act.color }}"
|
style="background-color: {{ act.color }}"
|
||||||
onclick="startActivity('{{ act._id }}', '{{ act.name }}')">
|
data-name="{{ act.name }}"
|
||||||
|
data-id="{{ act._id }}"
|
||||||
|
data-color="{{ act.color }}"
|
||||||
|
data-subcategories='{{ act.subcategories|default([])|tojson }}'
|
||||||
|
onclick="startActivityImmediate(this)">
|
||||||
<span>{{ act.name }}</span>
|
<span>{{ act.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Small Edit Link -->
|
||||||
|
<a href="{{ url_for('edit_activity', activity_id=act._id) }}"
|
||||||
|
style="position: absolute; top: 5px; right: 5px; color: rgba(255,255,255,0.8); text-decoration: none; font-size: 0.8rem; padding: 5px;"
|
||||||
|
title="Edit Activity">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<!-- Add New Button -->
|
<!-- Add New Button -->
|
||||||
@@ -93,19 +100,89 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hidden Form for Starting Activity with Note -->
|
<!-- Hidden Form for Starting Activity with Note -->
|
||||||
<form id="startForm" method="POST" style="display:none;">
|
|
||||||
<input type="hidden" name="note" id="startNote">
|
<!-- Custom Start Modal -->
|
||||||
|
<div id="startModal" style="display:none; position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center;">
|
||||||
|
<div class="card" style="width: 90%; max-width: 400px;">
|
||||||
|
<h3 id="modalTitle">Activity Started</h3>
|
||||||
|
|
||||||
|
<form id="startForm" action="{{ url_for('update_active_entry') }}" method="POST">
|
||||||
|
|
||||||
|
<div id="subcategoryContainer" style="display: none; margin-bottom: 1rem;">
|
||||||
|
<label>Subcategory / Context</label>
|
||||||
|
<select name="subcategory" id="modalSubcatSelect" style="width: 100%; padding: 0.5rem;">
|
||||||
|
<option value="">-- None --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>Note (Optional)</label>
|
||||||
|
<input type="text" name="note" placeholder="What are you working on?" autocomplete="off">
|
||||||
|
|
||||||
|
<div style="margin-top: 1rem; display: flex; justify-content: space-between;">
|
||||||
|
<!-- Cancel just closes modal, timer keeps running -->
|
||||||
|
<button type="button" class="btn" style="background: #95a5a6;" onclick="document.getElementById('startModal').style.display='none'">Skip</button>
|
||||||
|
<button type="submit" class="btn" style="background: #27ae60;">Update Details</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function startActivity(id, name) {
|
function startActivityImmediate(element) {
|
||||||
let note = prompt("Start " + name + "?\nAdd a note (optional):");
|
const id = element.getAttribute('data-id');
|
||||||
if (note === null) return; // Cancelled
|
const name = element.getAttribute('data-name');
|
||||||
|
const color = element.getAttribute('data-color');
|
||||||
|
const subcats = JSON.parse(element.getAttribute('data-subcategories') || '[]');
|
||||||
|
|
||||||
let form = document.getElementById('startForm');
|
// 1. Start Background Timer
|
||||||
form.action = "/toggle_timer/" + id;
|
fetch('/start_timer_bg/' + id, { method: 'POST' })
|
||||||
document.getElementById('startNote').value = note;
|
.then(res => res.json())
|
||||||
form.submit();
|
.then(data => {
|
||||||
|
if(data.status === 'success') {
|
||||||
|
// 2. Start Local Timer UI
|
||||||
|
startTime = new Date(data.start_time).getTime();
|
||||||
|
clearInterval(timerInterval);
|
||||||
|
timerInterval = setInterval(updateTimer, 1000);
|
||||||
|
updateTimer();
|
||||||
|
|
||||||
|
// Update UI State
|
||||||
|
document.getElementById('activeSection').style.display = 'block';
|
||||||
|
document.getElementById('activeName').innerText = "Currently Tracking: " + name;
|
||||||
|
document.getElementById('activeName').style.color = color;
|
||||||
|
|
||||||
|
// Reset Note/Subcat display
|
||||||
|
document.getElementById('activeNote').style.display = 'none';
|
||||||
|
document.getElementById('activeSubcatContainer').style.display = 'none';
|
||||||
|
|
||||||
|
// 3. Open Modal for Details
|
||||||
|
openDetailsModal(name, subcats);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetailsModal(name, subcats) {
|
||||||
|
document.getElementById('modalTitle').innerText = name + " Started";
|
||||||
|
|
||||||
|
// Setup Subcategories
|
||||||
|
const subcatContainer = document.getElementById('subcategoryContainer');
|
||||||
|
const select = document.getElementById('modalSubcatSelect');
|
||||||
|
select.innerHTML = '<option value="">-- None --</option>';
|
||||||
|
|
||||||
|
if (subcats && subcats.length > 0) {
|
||||||
|
subcatContainer.style.display = 'block';
|
||||||
|
subcats.forEach(sc => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = sc;
|
||||||
|
opt.innerText = sc;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
subcatContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show Modal
|
||||||
|
document.getElementById('startModal').style.display = 'flex';
|
||||||
|
document.querySelector('#startForm input[name="note"]').focus();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
83
templates/edit_activity.html
Normal file
83
templates/edit_activity.html
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="card" style="max-width: 500px; margin: auto;">
|
||||||
|
<h2>Edit Activity: {{ activity.name }}</h2>
|
||||||
|
<form method="POST" onsubmit="prepareSubcategories()">
|
||||||
|
<label>Name</label>
|
||||||
|
<input type="text" name="name" value="{{ activity.name }}" required>
|
||||||
|
|
||||||
|
<label>Color</label>
|
||||||
|
<input type="color" name="color" value="{{ activity.color }}" style="width:100%; height:40px; border:none; margin-bottom: 1rem;">
|
||||||
|
|
||||||
|
<label>Subcategories</label>
|
||||||
|
<p style="font-size: 0.8rem; color: #666; margin-top: -10px;">
|
||||||
|
Define specific contexts here (e.g. Boss, Team, Client A)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 5px; margin-bottom: 10px;">
|
||||||
|
<input type="text" id="newSubcat" placeholder="New subcategory..." style="margin-bottom: 0;">
|
||||||
|
<button type="button" class="btn" style="background: #27ae60;" onclick="addSubcat()">Add</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul id="subcatList" style="list-style: none; padding: 0; margin-bottom: 1rem;">
|
||||||
|
<!-- Items will be injected here -->
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Hidden input to store common separated list for backend -->
|
||||||
|
<input type="hidden" name="subcategories" id="subcatsInput">
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||||
|
<button type="submit" class="btn">Save Changes</button>
|
||||||
|
<a href="{{ url_for('index') }}" class="btn" style="background: #95a5a6; text-decoration: none;">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Initial data from server
|
||||||
|
let subcategories = {{ activity.subcategories|default([])|tojson }};
|
||||||
|
|
||||||
|
function renderList() {
|
||||||
|
const list = document.getElementById('subcatList');
|
||||||
|
list.innerHTML = '';
|
||||||
|
subcategories.forEach((item, index) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.style.background = '#f1f1f1';
|
||||||
|
li.style.margin = '5px 0';
|
||||||
|
li.style.padding = '8px';
|
||||||
|
li.style.borderRadius = '5px';
|
||||||
|
li.style.display = 'flex';
|
||||||
|
li.style.justifyContent = 'space-between';
|
||||||
|
li.style.alignItems = 'center';
|
||||||
|
|
||||||
|
li.innerHTML = `
|
||||||
|
<span>${item}</span>
|
||||||
|
<span onclick="removeSubcat(${index})" style="cursor: pointer; color: #e74c3c; font-weight: bold; padding: 0 5px;">×</span>
|
||||||
|
`;
|
||||||
|
list.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSubcat() {
|
||||||
|
const input = document.getElementById('newSubcat');
|
||||||
|
const val = input.value.trim();
|
||||||
|
if (val) {
|
||||||
|
subcategories.push(val);
|
||||||
|
input.value = '';
|
||||||
|
renderList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSubcat(index) {
|
||||||
|
subcategories.splice(index, 1);
|
||||||
|
renderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareSubcategories() {
|
||||||
|
document.getElementById('subcatsInput').value = subcategories.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render initially
|
||||||
|
renderList();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -10,7 +10,14 @@
|
|||||||
<div class="card" style="border-left: 5px solid {{ entry.activity.color }}; transition: transform 0.1s;">
|
<div class="card" style="border-left: 5px solid {{ entry.activity.color }}; transition: transform 0.1s;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
<div>
|
<div>
|
||||||
<h3 style="margin: 0;">{{ entry.activity.name }}</h3>
|
<h3 style="margin: 0;">
|
||||||
|
{{ entry.activity.name }}
|
||||||
|
{% if entry.subcategory %}
|
||||||
|
<span style="font-size: 0.8rem; background: #eee; padding: 2px 8px; border-radius: 10px; color: #555; vertical-align: middle;">
|
||||||
|
{{ entry.subcategory }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</h3>
|
||||||
<small style="color: #666;">
|
<small style="color: #666;">
|
||||||
{{ entry.start_time.strftime('%Y-%m-%d %H:%M') }} - {{ entry.end_time.strftime('%H:%M') }}
|
{{ entry.start_time.strftime('%Y-%m-%d %H:%M') }} - {{ entry.end_time.strftime('%H:%M') }}
|
||||||
</small>
|
</small>
|
||||||
|
|||||||
@@ -3,11 +3,16 @@
|
|||||||
<div class="card" style="border-left: 5px solid {{ entry.activity.color }};">
|
<div class="card" style="border-left: 5px solid {{ entry.activity.color }};">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: start;">
|
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||||||
<div>
|
<div>
|
||||||
<h2>{{ entry.activity.name }}</h2>
|
<h2>
|
||||||
|
{{ entry.activity.name }}
|
||||||
|
{% if entry.subcategory %}
|
||||||
|
<span style="font-size: 0.8rem; background: #eee; padding: 2px 8px; border-radius: 10px; color: #555; vertical-align: middle;">
|
||||||
|
{{ entry.subcategory }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
<p style="color: #666;">
|
<p style="color: #666;">
|
||||||
{{ entry.start_time.strftime('%A, %d. %B %Y') }}<br>
|
{{ entry.start_time.strftime('%A, %d. %B %Y') }}
|
||||||
{{ entry.start_time.strftime('%H:%M') }}
|
|
||||||
{% if entry.end_time %} - {{ entry.end_time.strftime('%H:%M') }} {% endif %}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: right;">
|
<div style="text-align: right;">
|
||||||
@@ -18,13 +23,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Session Notes</h3>
|
<h3>Edit Entry</h3>
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<textarea name="note" rows="4" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-family: inherit;">{{ entry.note }}</textarea>
|
<input type="hidden" name="action" value="update">
|
||||||
<div style="margin-top: 10px; text-align: right;">
|
|
||||||
<button type="submit" class="btn">Update Note</button>
|
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<label>Start Time</label>
|
||||||
|
<input type="datetime-local" name="start_time" value="{{ entry.start_time.strftime('%Y-%m-%dT%H:%M') }}" required>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<label>End Time</label>
|
||||||
|
<!-- If currently running (end_time is None), prevent editing end time or handle appropriately. Usually logbook is for past entries. -->
|
||||||
|
<input type="datetime-local" name="end_time" value="{{ entry.end_time.strftime('%Y-%m-%dT%H:%M') if entry.end_time else '' }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>Note</label>
|
||||||
|
<textarea name="note" rows="3" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-family: inherit;">{{ entry.note }}</textarea>
|
||||||
|
|
||||||
|
<div style="margin-top: 10px; display: flex; justify-content: space-between;">
|
||||||
|
<button type="submit" class="btn">Save Changes</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div style="border-top: 1px solid #eee; margin-top: 2rem; padding-top: 1rem;">
|
||||||
|
<form method="POST" onsubmit="return confirm('Are you sure you want to delete this entry? This action cannot be undone.');">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<button type="submit" class="btn btn-danger">Delete Entry</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -36,7 +64,7 @@
|
|||||||
{% for task in tasks %}
|
{% for task in tasks %}
|
||||||
<li style="border-bottom: 1px solid #eee; padding: 15px 0; display: flex; align-items: center;">
|
<li style="border-bottom: 1px solid #eee; padding: 15px 0; display: flex; align-items: center;">
|
||||||
<span style="margin-right: 15px; font-size: 1.2rem;">
|
<span style="margin-right: 15px; font-size: 1.2rem;">
|
||||||
{% if task.status == 'completed' %}✅{% else %}⬜{% endif %}
|
{% if task.status == 'completed' %}Done{% else %}Open{% endif %}
|
||||||
</span>
|
</span>
|
||||||
<div style="flex-grow: 1;">
|
<div style="flex-grow: 1;">
|
||||||
<a href="{{ url_for('task_detail', task_id=task._id) }}" style="font-weight: bold; display: block; font-size: 1.1rem;">{{ task.name }}</a>
|
<a href="{{ url_for('task_detail', task_id=task._id) }}" style="font-weight: bold; display: block; font-size: 1.1rem;">{{ task.name }}</a>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<form action="{{ url_for('toggle_timer', activity_id=task.activity_id) }}" method="POST" style="margin-top: 10px;">
|
<form action="{{ url_for('toggle_timer', activity_id=task.activity_id) }}" method="POST" style="margin-top: 10px;">
|
||||||
<input type="hidden" name="note" value="{{ task.name }}">
|
<input type="hidden" name="note" value="{{ task.name }}">
|
||||||
<button type="submit" class="btn" style="background: #27ae60;">
|
<button type="submit" class="btn" style="background: #27ae60;">
|
||||||
▶ Start Timer for this Task
|
Start Timer
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<!-- Automatically use the task name as the session note -->
|
<!-- Automatically use the task name as the session note -->
|
||||||
<input type="hidden" name="note" value="{{ task.name }}">
|
<input type="hidden" name="note" value="{{ task.name }}">
|
||||||
<button type="submit" class="btn" style="padding: 2px 8px; font-size: 0.8rem; background: #27ae60;" title="Start Timer for this Task">
|
<button type="submit" class="btn" style="padding: 2px 8px; font-size: 0.8rem; background: #27ae60;" title="Start Timer for this Task">
|
||||||
▶ Start
|
Start
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user