add tasks driectly from tracker
This commit is contained in:
42
app.py
42
app.py
@@ -148,11 +148,15 @@ def add_activity():
|
|||||||
name = request.form['name']
|
name = request.form['name']
|
||||||
color = request.form.get('color', '#3498db')
|
color = request.form.get('color', '#3498db')
|
||||||
|
|
||||||
|
# Parse subcategories
|
||||||
|
subcats_str = request.form.get('subcategories_data', '')
|
||||||
|
subcategories = [s.strip() for s in subcats_str.split(',') if s.strip()]
|
||||||
|
|
||||||
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': []
|
'subcategories': subcategories
|
||||||
}).inserted_id
|
}).inserted_id
|
||||||
|
|
||||||
# Add optional default tasks (Revised from list input)
|
# Add optional default tasks (Revised from list input)
|
||||||
@@ -427,6 +431,42 @@ def create_task():
|
|||||||
db.tasks.insert_one(task_doc)
|
db.tasks.insert_one(task_doc)
|
||||||
return redirect(url_for('tasks'))
|
return redirect(url_for('tasks'))
|
||||||
|
|
||||||
|
@app.route('/create_task_quick', methods=['POST'])
|
||||||
|
def create_task_quick():
|
||||||
|
if not is_logged_in(): return jsonify({'error': 'auth'}), 401
|
||||||
|
|
||||||
|
name = request.form['name']
|
||||||
|
activity_id = request.form.get('activity_id')
|
||||||
|
subcategory = request.form.get('subcategory', '')
|
||||||
|
|
||||||
|
# Check if there is an active time entry to link immediately
|
||||||
|
current_entry = db.time_entries.find_one({
|
||||||
|
'user_id': get_user_id(),
|
||||||
|
'end_time': None
|
||||||
|
})
|
||||||
|
|
||||||
|
task_doc = {
|
||||||
|
'user_id': get_user_id(),
|
||||||
|
'name': name,
|
||||||
|
'status': 'open',
|
||||||
|
'is_template': False,
|
||||||
|
'source': 'manual-quick',
|
||||||
|
'created_at': datetime.now(),
|
||||||
|
'comments': [],
|
||||||
|
'subcategory': subcategory,
|
||||||
|
'activity_id': ObjectId(activity_id) if activity_id else None,
|
||||||
|
# If we have an active entry matching this activity, link it for this session
|
||||||
|
'time_entry_id': current_entry['_id'] if current_entry and str(current_entry['activity_id']) == activity_id else None
|
||||||
|
}
|
||||||
|
|
||||||
|
new_id = db.tasks.insert_one(task_doc).inserted_id
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'task_id': str(new_id),
|
||||||
|
'name': name
|
||||||
|
})
|
||||||
|
|
||||||
@app.route('/task/<task_id>', methods=['GET', 'POST'])
|
@app.route('/task/<task_id>', methods=['GET', 'POST'])
|
||||||
def task_detail(task_id):
|
def task_detail(task_id):
|
||||||
if not is_logged_in(): return redirect(url_for('login'))
|
if not is_logged_in(): return redirect(url_for('login'))
|
||||||
|
|||||||
@@ -184,25 +184,32 @@
|
|||||||
<div class="timer-display" id="timer" style="margin: 1rem 0 2rem 0;">00:00:00</div>
|
<div class="timer-display" id="timer" style="margin: 1rem 0 2rem 0;">00:00:00</div>
|
||||||
|
|
||||||
<!-- Tasks List (Redesigned) -->
|
<!-- Tasks List (Redesigned) -->
|
||||||
{% if tasks %}
|
|
||||||
<div style="margin-bottom: 2rem;">
|
<div style="margin-bottom: 2rem;">
|
||||||
<h4 style="font-size: 0.8rem; text-transform: uppercase; color: var(--text-secondary); letter-spacing: 0.05em; border-bottom: 1px solid var(--border-dim); padding-bottom: 5px;">Active Tasks</h4>
|
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-dim); padding-bottom: 5px; margin-bottom: 10px;">
|
||||||
|
<h4 style="font-size: 0.8rem; text-transform: uppercase; color: var(--text-secondary); letter-spacing: 0.05em; margin: 0;">Active Tasks</h4>
|
||||||
|
<!-- Quick Add Button -->
|
||||||
|
<button type="button" onclick="openQuickTaskModal()" style="background: transparent; border: 1px solid var(--border-dim); cursor: pointer; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); margin-left: 10px;" title="Add Task">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="checklist-container" id="activeTasksList">
|
<div class="checklist-container" id="activeTasksList">
|
||||||
{% for task in tasks %}
|
{% if tasks %}
|
||||||
<div class="checklist-item {% if task.status == 'completed' %}completed{% endif %}" id="task-item-{{ task._id }}">
|
{% for task in tasks %}
|
||||||
<input type="checkbox" id="task_{{ task._id }}"
|
<div class="checklist-item {% if task.status == 'completed' %}completed{% endif %}" id="task-item-{{ task._id }}">
|
||||||
{% if task.status == 'completed' %}checked{% endif %}
|
<input type="checkbox" id="task_{{ task._id }}"
|
||||||
onchange="toggleTask('{{ task._id }}', this)">
|
{% if task.status == 'completed' %}checked{% endif %}
|
||||||
<label for="task_{{ task._id }}">
|
onchange="toggleTask('{{ task._id }}', this)">
|
||||||
<a href="{{ url_for('task_detail', task_id=task._id) }}" style="text-decoration: none; color: inherit;">
|
<label for="task_{{ task._id }}">
|
||||||
{{ task.name }}
|
<a href="{{ url_for('task_detail', task_id=task._id) }}" style="text-decoration: none; color: inherit;">
|
||||||
</a>
|
{{ task.name }}
|
||||||
</label>
|
</a>
|
||||||
</div>
|
</label>
|
||||||
{% endfor %}
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<form action="{{ url_for('stop_timer') }}" method="POST">
|
<form action="{{ url_for('stop_timer') }}" method="POST">
|
||||||
<button type="submit" class="btn btn-danger" style="width: 100%; justify-content: center; padding: 12px;">Stop Activity</button>
|
<button type="submit" class="btn btn-danger" style="width: 100%; justify-content: center; padding: 12px;">Stop Activity</button>
|
||||||
@@ -214,6 +221,10 @@
|
|||||||
|
|
||||||
<!-- Scripts (Timer, Tasks, Modal) -->
|
<!-- Scripts (Timer, Tasks, Modal) -->
|
||||||
<script>
|
<script>
|
||||||
|
// Global vars - MUST be declared before any functions that use them
|
||||||
|
let currentActivityId = "{{ current_entry.activity_id if current_entry else '' }}";
|
||||||
|
let currentSubcategory = "{{ current_entry.subcategory if current_entry else '' }}";
|
||||||
|
|
||||||
// Timer Logic
|
// Timer Logic
|
||||||
let startTime = {% if current_entry %}{{ current_entry.start_time.timestamp() * 1000 }}{% else %}null{% endif %};
|
let startTime = {% if current_entry %}{{ current_entry.start_time.timestamp() * 1000 }}{% else %}null{% endif %};
|
||||||
let timerInterval;
|
let timerInterval;
|
||||||
@@ -283,17 +294,30 @@
|
|||||||
document.getElementById('startModal').classList.remove('show');
|
document.getElementById('startModal').classList.remove('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeQuickTaskModal() {
|
||||||
|
document.getElementById('quickTaskModal').classList.remove('show');
|
||||||
|
document.getElementById('quickTaskName').value = ''; // Clean up
|
||||||
|
}
|
||||||
|
|
||||||
// Close on backdrop click
|
// Close on backdrop click
|
||||||
document.getElementById('startModal').addEventListener('click', function(e) {
|
document.getElementById('startModal').addEventListener('click', function(e) {
|
||||||
if (e.target === this) closeStartModal();
|
if (e.target === this) closeStartModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('quickTaskModal').addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) closeQuickTaskModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start Activity Immediate Function
|
||||||
function startActivityImmediate(element) {
|
function startActivityImmediate(element) {
|
||||||
const id = element.getAttribute('data-id');
|
const id = element.getAttribute('data-id');
|
||||||
const name = element.getAttribute('data-name');
|
const name = element.getAttribute('data-name');
|
||||||
const color = element.getAttribute('data-color');
|
const color = element.getAttribute('data-color');
|
||||||
const subcats = JSON.parse(element.getAttribute('data-subcategories') || '[]');
|
const subcats = JSON.parse(element.getAttribute('data-subcategories') || '[]');
|
||||||
|
|
||||||
|
// Save ID globally for quick add
|
||||||
|
currentActivityId = id;
|
||||||
|
|
||||||
// 1. Start Background Timer
|
// 1. Start Background Timer
|
||||||
fetch('/start_timer_bg/' + id, { method: 'POST' })
|
fetch('/start_timer_bg/' + id, { method: 'POST' })
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
@@ -321,6 +345,14 @@
|
|||||||
// Reset Note/Subcat display
|
// Reset Note/Subcat display
|
||||||
document.getElementById('activeNote').style.display = 'none';
|
document.getElementById('activeNote').style.display = 'none';
|
||||||
document.getElementById('activeSubcatContainer').style.display = 'none';
|
document.getElementById('activeSubcatContainer').style.display = 'none';
|
||||||
|
document.getElementById('activeSubcatDisplay').innerText = ''; // Reset text
|
||||||
|
currentSubcategory = ""; // Reset global
|
||||||
|
|
||||||
|
// Clear tasks list visually on new start (will define via sync or reload if templates exist)
|
||||||
|
// Ideally we'd fetch templates here, but sync will catch them in 2s.
|
||||||
|
// For smoother UX, we could inject template names if passed from backend.
|
||||||
|
document.getElementById('activeTasksList').innerHTML = '';
|
||||||
|
localTaskIds = []; // Reset local known tasks
|
||||||
|
|
||||||
// 3. Open Modal for Details
|
// 3. Open Modal for Details
|
||||||
openDetailsModal(name, subcats);
|
openDetailsModal(name, subcats);
|
||||||
@@ -328,6 +360,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hook into form update to set these values when modal is submitted/skipped
|
||||||
function openDetailsModal(name, subcats) {
|
function openDetailsModal(name, subcats) {
|
||||||
document.getElementById('modalTitle').innerText = name + " Started";
|
document.getElementById('modalTitle').innerText = name + " Started";
|
||||||
|
|
||||||
@@ -353,6 +386,58 @@
|
|||||||
document.querySelector('#startForm input[name="note"]').focus();
|
document.querySelector('#startForm input[name="note"]').focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open Quick Task Modal
|
||||||
|
function openQuickTaskModal() {
|
||||||
|
if(!currentActivityId) {
|
||||||
|
alert("No active activity context found. Please start an activity first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('quickTaskModal').classList.add('show');
|
||||||
|
document.getElementById('quickTaskName').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick Add Form Submit
|
||||||
|
function submitQuickTask(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = document.getElementById('quickTaskName');
|
||||||
|
const val = input.value.trim();
|
||||||
|
if(!val) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', val);
|
||||||
|
formData.append('activity_id', currentActivityId);
|
||||||
|
formData.append('subcategory', currentSubcategory);
|
||||||
|
|
||||||
|
fetch('/create_task_quick', { method: 'POST', body: formData })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if(data.status === 'success') {
|
||||||
|
input.value = '';
|
||||||
|
closeQuickTaskModal();
|
||||||
|
|
||||||
|
// Add to DOM
|
||||||
|
const list = document.getElementById('activeTasksList');
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'checklist-item';
|
||||||
|
div.id = 'task-item-' + data.task_id;
|
||||||
|
div.innerHTML = `
|
||||||
|
<input type="checkbox" id="task_${data.task_id}"
|
||||||
|
onchange="toggleTask('${data.task_id}', this)">
|
||||||
|
<label for="task_${data.task_id}">
|
||||||
|
<a href="/task/${data.task_id}" style="text-decoration: none; color: inherit;">
|
||||||
|
${data.name}
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
list.insertBefore(div, list.firstChild); // Add to top
|
||||||
|
|
||||||
|
// Register ID so sync doesn't reload
|
||||||
|
localTaskIds.push(data.task_id);
|
||||||
|
console.log("Quick task added. Sync updated.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// New Task List Logic for Activity Creation form (Reusing existing script)
|
// New Task List Logic for Activity Creation form (Reusing existing script)
|
||||||
let newActivityTasks = [];
|
let newActivityTasks = [];
|
||||||
function addNewTask() {
|
function addNewTask() {
|
||||||
@@ -395,15 +480,15 @@
|
|||||||
<div id="startModal" class="modal-backdrop">
|
<div id="startModal" class="modal-backdrop">
|
||||||
<div class="modal-card">
|
<div class="modal-card">
|
||||||
<h3 id="modalTitle" style="margin-bottom: 1.5rem;">Activity Started</h3>
|
<h3 id="modalTitle" style="margin-bottom: 1.5rem;">Activity Started</h3>
|
||||||
<form id="startForm" action="{{ url_for('update_active_entry') }}" method="POST">
|
<form id="startForm" onsubmit="submitDetailsForm(event)">
|
||||||
<div id="subcategoryContainer" style="display: none; margin-bottom: 1rem;">
|
<div id="subcategoryContainer" style="display: none; margin-bottom: 1rem;">
|
||||||
<label>Subcategory / Context</label>
|
<label>Subcategory / Context</label>
|
||||||
<select name="subcategory" id="modalSubcatSelect" style="width: 100%; padding: 0.5rem;">
|
<select name="subcategory" id="modalSubcatSelect" style="width: 100%; padding: 0.5rem;">
|
||||||
<option value="">-- None --</option>
|
<option value="">-- None --</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<label>Note (Optional)</label>
|
<label>Note (Optional)</label>
|
||||||
<input type="text" name="note" placeholder="What are you working on?" autocomplete="off">
|
<input type="text" name="note" id="modalNote" placeholder="What are you working on?" autocomplete="off">
|
||||||
<div style="margin-top: 2rem; display: flex; justify-content: space-between;">
|
<div style="margin-top: 2rem; display: flex; justify-content: space-between;">
|
||||||
<button type="button" class="btn" style="background: transparent; color: var(--text-secondary); border: 1px solid var(--border-dim);" onclick="closeStartModal()">Skip</button>
|
<button type="button" class="btn" style="background: transparent; color: var(--text-secondary); border: 1px solid var(--border-dim);" onclick="closeStartModal()">Skip</button>
|
||||||
<button type="submit" class="btn" style="background: #27ae60;">Update Details</button>
|
<button type="submit" class="btn" style="background: #27ae60;">Update Details</button>
|
||||||
@@ -412,6 +497,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Task Modal (For adding task to active session) -->
|
||||||
|
<div id="quickTaskModal" class="modal-backdrop">
|
||||||
|
<div class="modal-card">
|
||||||
|
<h3 style="margin-bottom: 1.5rem;">Add Task</h3>
|
||||||
|
<p style="color: var(--text-secondary); margin-bottom: 1rem; font-size: 0.9rem;">
|
||||||
|
This task will be linked to the current activity.
|
||||||
|
</p>
|
||||||
|
<form onsubmit="submitQuickTask(event)">
|
||||||
|
<label>Task Name</label>
|
||||||
|
<input type="text" id="quickTaskName" required placeholder="What needs to be done?" autocomplete="off">
|
||||||
|
|
||||||
|
<div style="margin-top: 2rem; display: flex; justify-content: flex-end; gap: 10px;">
|
||||||
|
<button type="button" class="btn" style="background: transparent; color: var(--text-secondary); border: 1px solid var(--border-dim);" onclick="closeQuickTaskModal()">Cancel</button>
|
||||||
|
<button type="submit" class="btn" style="background: #27ae60;">Add Task</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Replace standard form submit for details so page doesn't reload and lose state
|
||||||
|
function submitDetailsForm(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const sub = document.getElementById('modalSubcatSelect').value;
|
||||||
|
const note = document.getElementById('modalNote').value;
|
||||||
|
|
||||||
|
currentSubcategory = sub; // Update global for quick add task
|
||||||
|
|
||||||
|
// Update UI immediately
|
||||||
|
if(sub) {
|
||||||
|
document.getElementById('activeSubcatContainer').style.display = 'block';
|
||||||
|
document.getElementById('activeSubcatDisplay').innerText = sub;
|
||||||
|
}
|
||||||
|
if(note) {
|
||||||
|
document.getElementById('activeNote').style.display = 'block';
|
||||||
|
document.getElementById('activeNote').innerText = '"' + note + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('subcategory', sub);
|
||||||
|
fd.append('note', note);
|
||||||
|
|
||||||
|
fetch('/update_active_entry', { method: 'POST', body: fd });
|
||||||
|
closeStartModal();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Live Sync Script (Optimized for Animation) -->
|
<!-- Live Sync Script (Optimized for Animation) -->
|
||||||
<script>
|
<script>
|
||||||
// Generate initial state hashes based on what Jinja rendered
|
// Generate initial state hashes based on what Jinja rendered
|
||||||
@@ -431,8 +563,9 @@
|
|||||||
// Don't poll if page is hidden to save battery/data
|
// Don't poll if page is hidden to save battery/data
|
||||||
if (document.hidden) return;
|
if (document.hidden) return;
|
||||||
|
|
||||||
// Don't poll or reload if the Start Modal is open (User is interacting)
|
// Don't poll or reload if ANY Modal is open (User is interacting)
|
||||||
if (document.getElementById('startModal').classList.contains('show')) return;
|
if (document.getElementById('startModal').classList.contains('show') ||
|
||||||
|
document.getElementById('quickTaskModal').classList.contains('show')) return;
|
||||||
|
|
||||||
fetch('/api/sync_check')
|
fetch('/api/sync_check')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
|||||||
Reference in New Issue
Block a user