add entry manually
This commit is contained in:
47
app.py
47
app.py
@@ -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):
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user