add entry manually

This commit is contained in:
2026-02-12 10:33:32 +01:00
parent ef9b54b4be
commit 0a18e08581
2 changed files with 156 additions and 1 deletions

47
app.py
View File

@@ -513,6 +513,12 @@ def logbook():
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()
# Fetch activities for the manual entry dropdown
activities = list(db.activities.find({'user_id': user_id}))
# Ensure keys exist for template using them safely
for a in activities:
if 'subcategories' not in a: a['subcategories'] = []
# --- Statistics Logic --- # --- Statistics Logic ---
time_range = request.args.get('range', '24h') time_range = request.args.get('range', '24h')
now = datetime.now() now = datetime.now()
@@ -616,7 +622,46 @@ def logbook():
entry['duration_str'] = str(duration).split('.')[0] entry['duration_str'] = str(duration).split('.')[0]
entry['is_running'] = True entry['is_running'] = True
return render_template('logbook.html', log=log, chart_data=chart_data, current_range=time_range, total_time_display=total_time_display) return render_template('logbook.html', log=log, chart_data=chart_data, current_range=time_range, total_time_display=total_time_display, activities=activities)
@app.route('/add_manual_entry', methods=['POST'])
def add_manual_entry():
if not is_logged_in(): return redirect(url_for('login'))
user_id = get_user_id()
activity_id = request.form['activity_id']
start_str = request.form['start_time']
end_str = request.form['end_time']
note = request.form.get('note', '')
# Check both inputs (one will be disabled in frontend, but check priority)
subcategory = request.form.get('subcategory_select')
if not subcategory:
subcategory = request.form.get('subcategory', '')
try:
start_time = datetime.strptime(start_str, '%Y-%m-%dT%H:%M')
end_time = datetime.strptime(end_str, '%Y-%m-%dT%H:%M') if end_str else datetime.now()
# Validation: End time cannot be before start time
if end_time < start_time:
flash('End time cannot be before start time')
return redirect(url_for('logbook'))
except ValueError:
flash('Invalid date format')
return redirect(url_for('logbook'))
db.time_entries.insert_one({
'user_id': user_id,
'activity_id': ObjectId(activity_id),
'start_time': start_time,
'end_time': end_time,
'note': note,
'subcategory': subcategory
})
return redirect(url_for('logbook'))
@app.route('/logbook/<entry_id>', methods=['GET', 'POST']) @app.route('/logbook/<entry_id>', methods=['GET', 'POST'])
def log_entry_detail(entry_id): def log_entry_detail(entry_id):

View File

@@ -6,6 +6,59 @@
<div style="flex: 1; min-width: 300px;"> <div style="flex: 1; min-width: 300px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
<h2 style="margin: 0;">Logbook</h2> <h2 style="margin: 0;">Logbook</h2>
<button onclick="document.getElementById('manualEntryForm').style.display = document.getElementById('manualEntryForm').style.display === 'none' ? 'block' : 'none'"
class="btn" style="padding: 5px 12px; font-weight: bold;">
+ Add Entry
</button>
</div>
<!-- Manual Entry Form (Hidden by default) -->
<div id="manualEntryForm" style="display: none; margin-bottom: 2rem; background: #fff; padding: 1.5rem; border: 1px solid var(--border-dim); border-radius: var(--radius-md); box-shadow: 0 4px 6px rgba(0,0,0,0.05);">
<h3 style="margin-top: 0; margin-bottom: 1rem; font-size: 1.1rem;">Add Log Entry Retrospectively</h3>
<form action="{{ url_for('add_manual_entry') }}" method="POST">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem;">
<div>
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Activity</label>
<select name="activity_id" id="activitySelect" onchange="updateSubcategories()" required style="width: 100%; padding: 8px; border: 1px solid var(--border-dim); border-radius: 4px;">
<option value="" disabled selected>Select Activity</option>
{% for activity in activities %}
<option value="{{ activity._id }}" data-subcategories='{{ activity.get("subcategories", [])|tojson }}'>{{ activity.name }}</option>
{% endfor %}
</select>
</div>
<div>
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Subcategory</label>
<!-- Text Input (Default / Fallback) -->
<input type="text" id="subcatInput" name="subcategory" placeholder="e.g. Development" style="width: 100%; padding: 8px; border: 1px solid var(--border-dim); border-radius: 4px;">
<!-- Select Input (Hidden by default) -->
<select id="subcatSelect" name="subcategory_select" disabled style="display: none; width: 100%; padding: 8px; border: 1px solid var(--border-dim); border-radius: 4px;">
<option value="">-- Select Subcategory --</option>
</select>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem;">
<div>
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Start Time</label>
<input type="datetime-local" name="start_time" required class="date-input" style="width: 100%; padding: 8px; border: 1px solid var(--border-dim); border-radius: 4px;">
</div>
<div>
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">End Time</label>
<input type="datetime-local" name="end_time" required class="date-input" style="width: 100%; padding: 8px; border: 1px solid var(--border-dim); border-radius: 4px;">
</div>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.9rem;">Note</label>
<textarea name="note" rows="2" style="width: 100%; padding: 8px; border: 1px solid var(--border-dim); border-radius: 4px; resize: vertical;"></textarea>
</div>
<div style="text-align: right;">
<button type="button" onclick="document.getElementById('manualEntryForm').style.display = 'none'" class="btn" style="background: transparent; color: #666; margin-right: 0.5rem;">Cancel</button>
<button type="submit" class="btn">Save Entry</button>
</div>
</form>
</div> </div>
<!-- Search Bar --> <!-- Search Bar -->
@@ -139,6 +192,63 @@
}); });
}, 1000); }, 1000);
// Initialize datetime inputs with safe defaults if needed
window.addEventListener('load', () => {
const now = new Date();
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
const nowStr = now.toISOString().slice(0, 16);
// Optionally set default values for inputs that are empty
document.querySelectorAll('input[type="datetime-local"].date-input').forEach(input => {
if (!input.value) input.value = nowStr;
});
// Initialize subcategories check
updateSubcategories();
});
function updateSubcategories() {
const activitySelect = document.getElementById('activitySelect');
const subcatInput = document.getElementById('subcatInput');
const subcatSelect = document.getElementById('subcatSelect');
const selectedOption = activitySelect.options[activitySelect.selectedIndex];
let subcats = [];
try {
if (selectedOption && selectedOption.dataset.subcategories) {
subcats = JSON.parse(selectedOption.dataset.subcategories);
}
} catch (e) {
console.error("Error parsing subcategories", e);
}
if (subcats && subcats.length > 0) {
// Show Dropdown
subcatInput.style.display = 'none';
subcatInput.disabled = true; // Disable so it's not sent
subcatSelect.style.display = 'block';
subcatSelect.disabled = false;
// Clear and populate
subcatSelect.innerHTML = '<option value="">-- Select Subcategory --</option>';
subcats.forEach(sc => {
const opt = document.createElement('option');
opt.value = sc;
opt.textContent = sc;
subcatSelect.appendChild(opt);
});
} else {
// Show Text Input
subcatInput.style.display = 'block';
subcatInput.disabled = false;
subcatSelect.style.display = 'none';
subcatSelect.disabled = true;
}
}
// Background Sync (Polls for new tasks or status changes) // Background Sync (Polls for new tasks or status changes)
let lastContentHash = ""; let lastContentHash = "";
setInterval(() => { setInterval(() => {