Files
OpenTimeTracker/templates/tasks.html

315 lines
16 KiB
HTML

{% extends "layout.html" %}
{% block content %}
<div style="max-width: 800px; margin: auto;">
<!-- Top Header -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2 style="margin: 0;">Tasks</h2>
<button class="btn" onclick="openTaskModal()">+ New Task</button>
</div>
<!-- Search Bar -->
<div style="margin-bottom: 1.5rem;">
<input type="text" id="taskSearchInput" placeholder="Search tasks..." onkeyup="applyFilters()"
style="background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22none%22%20stroke%3D%22%23999%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Ccircle%20cx%3D%2211%22%20cy%3D%2211%22%20r%3D%228%22%2F%3E%3Cline%20x1%3D%2221%22%20y1%3D%2221%22%20x2%3D%2216.65%22%20y2%3D%2216.65%22%2F%3E%3C%2Fsvg%3E'); background-repeat: no-repeat; background-position: 10px center; background-size: 16px; padding-left: 36px;">
</div>
<!-- Filters -->
<div style="border-bottom: 1px solid var(--border-dim); margin-bottom: 2rem; padding-bottom: 0.5rem; display: flex; gap: 20px; overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch;">
<div class="filter-tab active" id="filter-open" onclick="setFilter('open')" data-filter="open" style="cursor: pointer; font-weight: 600; font-size: 0.9rem; color: var(--text-primary); flex-shrink: 0;">
Open
</div>
<div class="filter-tab" id="filter-today" onclick="setFilter('today')" data-filter="today" style="cursor: pointer; font-weight: 600; font-size: 0.9rem; color: var(--text-secondary); flex-shrink: 0;">
Due Today
</div>
<div class="filter-tab" id="filter-completed" onclick="setFilter('completed')" data-filter="completed" style="cursor: pointer; font-weight: 600; font-size: 0.9rem; color: var(--text-secondary); flex-shrink: 0;">
Completed
</div>
</div>
<!-- Grouped Lists -->
<div id="tasks-container">
{% for act in activities %}
{% set act_tasks = [] %}
<!-- Poor man's filtering in Jinja: iterate all tasks to find matches -->
{% for t in tasks %}
{% if t.activity_id|string == act._id|string %}
{% if act_tasks.append(t) %}{% endif %}
{% endif %}
{% endfor %}
<div class="task-group">
<div style="display: flex; align-items: center; padding: 10px 0;">
<span style="width: 8px; height: 8px; border-radius: 50%; background: {{ act.color }}; margin-right: 10px;"></span>
<h3 style="margin: 0; font-size: 1rem; color: var(--text-primary);">{{ act.name }}</h3>
<button onclick="openTaskModal('{{ act._id }}')"
style="background: none; border: none; cursor: pointer; color: var(--text-secondary); margin-left: 8px; font-size: 1.1rem; padding: 0 5px;"
title="Add task to {{ act.name }}">+</button>
</div>
<!-- Always render the UL, just empty if no tasks initially -->
<ul style="list-style: none; padding: 0; margin-top: 0;">
{% for task in act_tasks %}
<!-- Inline Task Row -->
<li class="task-item-row"
data-due-date="{{ task.due_date.strftime('%Y-%m-%d') if task.due_date else '' }}"
data-status="{{ task.status }}"
style="padding: 8px 0; border-bottom: 1px solid var(--border-dim); display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 10px; overflow: hidden;">
<a href="{{ url_for('task_detail', task_id=task._id) }}" class="task-name-link" style="text-decoration: none; color: var(--text-primary); font-weight: 500; {% if task.status == 'completed' %}text-decoration: line-through; color: var(--text-secondary);{% endif %}">
{{ task.name }}
</a>
{% if task.subcategory %}
<span style="font-size: 0.75rem; color: #555; background: #eee; padding: 2px 6px; border-radius: 4px;">
{{ task.subcategory }}
</span>
{% endif %}
{% if task.due_date %}
<span style="font-size: 0.75rem; color: var(--text-secondary); background: var(--bg-input); padding: 2px 6px; border-radius: 4px;">
{{ task.due_date.strftime('%d. %b %H:%M') }}
</span>
{% endif %}
</div>
<div style="display: flex; gap: 10px;">
<form action="{{ url_for('toggle_timer', activity_id=task.activity_id) }}" method="POST" style="margin: 0;">
<input type="hidden" name="note" value="{{ task.name }}">
<button type="submit" title="Start Timer" style="background: none; border: 1px solid var(--border-dim); cursor: pointer; border-radius: 4px; padding: 2px 6px; color: var(--text-secondary); font-size: 0.8rem;">
</button>
</form>
</div>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
<!-- Tasks without Activity -->
{% set no_act_tasks = [] %}
{% for t in tasks %}
{% if not t.activity_id %}
{% if no_act_tasks.append(t) %}{% endif %}
{% endif %}
{% endfor %}
{% if no_act_tasks %}
<div class="task-group">
<div style="display: flex; align-items: center; padding: 10px 0;">
<span style="width: 8px; height: 8px; border-radius: 50%; background: #ccc; margin-right: 10px;"></span>
<h3 style="margin: 0; font-size: 1rem; color: var(--text-primary);">Inbox / No Activity</h3>
<button onclick="openTaskModal('')" style="background: none; border: none; cursor: pointer; color: var(--text-secondary); margin-left: 8px; font-size: 1.1rem; padding: 0 5px;">+</button>
</div>
<ul style="list-style: none; padding: 0; margin-top: 0;">
{% for task in no_act_tasks %}
<!-- Inline Task Row -->
<li class="task-item-row"
data-due-date="{{ task.due_date.strftime('%Y-%m-%d') if task.due_date else '' }}"
data-status="{{ task.status }}"
style="padding: 8px 0; border-bottom: 1px solid var(--border-dim); display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 10px; overflow: hidden;">
<a href="{{ url_for('task_detail', task_id=task._id) }}" style="text-decoration: none; color: var(--text-primary); font-weight: 500; {% if task.status == 'completed' %}text-decoration: line-through; color: var(--text-secondary);{% endif %}">
{{ task.name }}
</a>
{% if task.subcategory %}
<span style="font-size: 0.75rem; color: #555; background: #eee; padding: 2px 6px; border-radius: 4px;">
{{ task.subcategory }}
</span>
{% endif %}
{% if task.due_date %}
<span style="font-size: 0.75rem; color: var(--text-secondary); background: var(--bg-input); padding: 2px 6px; border-radius: 4px;">
{{ task.due_date.strftime('%d. %b %H:%M') }}
</span>
{% endif %}
</div>
<div style="display: flex; gap: 10px;">
<!-- No start button usually here if no activity linked -->
</div>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<!-- Templates Toggle (Kept minimal) -->
{% if templates %}
<div style="margin-top: 3rem; border-top: 1px solid var(--border-dim); padding-top: 1rem;">
<details>
<summary style="cursor: pointer; color: var(--text-secondary); font-size: 0.9rem; font-weight: 500;">Manage Auto-Added Templates</summary>
<div class="card" style="margin-top: 1rem; background: #fafafa;">
<ul style="padding-left: 20px; margin: 0;">
{% for t in templates %}
<li style="margin-bottom: 5px;">
<strong>{{ t.name }}</strong>
<span style="color: #666; font-size: 0.8rem;">(on {{ t.activity_name|default('Unknown') }})</span>
<a href="{{ url_for('task_detail', task_id=t._id) }}" style="font-size: 0.8rem; margin-left: 10px;">Edit</a>
</li>
{% endfor %}
</ul>
</div>
</details>
</div>
{% endif %}
</div>
<!-- Modal for New Task -->
<div id="taskModal" class="modal-backdrop">
<div class="modal-card">
<h3 style="margin-bottom: 1.5rem;">Create New Task</h3>
<form action="{{ url_for('create_task') }}" method="POST">
<label>Task Name</label>
<input type="text" name="name" id="modalInputName" required placeholder="What needs to be done?" autocomplete="off">
<label>Activity</label>
<select name="activity_id" id="modalSelectActivity" onchange="updateModalSubcategories()">
<option value="">-- No Activity --</option>
{% for act in activities %}
<option value="{{ act._id }}">{{ act.name }}</option>
{% endfor %}
</select>
<div id="modalSubcatWrapper" style="display:none;">
<label>Subcategory</label>
<select name="subcategory" id="modalSelectSubcategory">
<option value="">-- None --</option>
</select>
</div>
<label>Due Date</label>
<input type="datetime-local" name="due_date">
<div style="margin-bottom: 1.5rem; display: flex; align-items: start; gap: 10px; margin-top: 1.5rem;">
<input type="checkbox" id="is_template" name="is_template" value="1" style="width: auto; margin-top: 4px;">
<label for="is_template" style="margin-top: 0; text-transform: none; font-weight: 400; line-height: 1.4;">
<strong>Save as Template?</strong><br>
<small>Auto-add this task every time the selected activity starts.</small>
</label>
</div>
<div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 2rem;">
<button type="button" class="btn" style="background: transparent; color: var(--text-secondary); border: 1px solid var(--border-dim);" onclick="closeTaskModal()">Cancel</button>
<button type="submit" class="btn">Create Task</button>
</div>
</form>
</div>
</div>
<!-- Scripts removed dead macros -->
<script>
// Prepare Data for JS
const activitiesList = [
{% for act in activities %}
{
id: "{{ act._id }}",
subcategories: {{ act.subcategories|default([])|tojson }}
},
{% endfor %}
];
function updateModalSubcategories() {
const actSelect = document.getElementById('modalSelectActivity');
const subWrapper = document.getElementById('modalSubcatWrapper');
const subSelect = document.getElementById('modalSelectSubcategory');
const selectedId = actSelect.value;
const activity = activitiesList.find(a => a.id === selectedId);
subSelect.innerHTML = '<option value="">-- None --</option>';
if (activity && activity.subcategories && activity.subcategories.length > 0) {
subWrapper.style.display = 'block';
activity.subcategories.forEach(sub => {
const opt = document.createElement('option');
opt.value = sub;
opt.innerText = sub;
subSelect.appendChild(opt);
});
} else {
subWrapper.style.display = 'none';
}
}
function openTaskModal(activityId) {
const modal = document.getElementById('taskModal');
const select = document.getElementById('modalSelectActivity');
const nameInput = document.getElementById('modalInputName');
if (activityId !== undefined) {
select.value = activityId;
} else {
select.value = "";
}
// Trigger subcategory update based on pre-selection
updateModalSubcategories();
modal.classList.add('show');
nameInput.focus();
}
function closeTaskModal() {
document.getElementById('taskModal').classList.remove('show');
}
// Close on backdrop click
document.getElementById('taskModal').addEventListener('click', function(e) {
if (e.target === this) closeTaskModal();
});
// Filtering Logic
let currentFilterType = 'open';
function setFilter(type) {
currentFilterType = type;
document.querySelectorAll('.filter-tab').forEach(el => {
el.style.color = 'var(--text-secondary)';
el.classList.remove('active');
});
document.getElementById('filter-' + type).style.color = 'var(--text-primary)';
document.getElementById('filter-' + type).classList.add('active');
applyFilters();
}
function applyFilters() {
const searchVal = document.getElementById('taskSearchInput').value.toLowerCase();
const taskItems = document.querySelectorAll('.task-item-row');
const today = new Date().toISOString().split('T')[0];
taskItems.forEach(item => {
const status = item.getAttribute('data-status');
const dateStr = item.getAttribute('data-due-date');
const name = item.querySelector('.task-name-link').innerText.toLowerCase();
let show = false;
// 1. Check Tabs
if (currentFilterType === 'open') {
if (status !== 'completed') show = true;
} else if (currentFilterType === 'today') {
if (status !== 'completed' && dateStr && dateStr.startsWith(today)) show = true;
} else if (currentFilterType === 'completed') {
if (status === 'completed') show = true;
}
// 2. Check Search
if (show && searchVal) {
if (!name.includes(searchVal)) show = false;
}
item.style.display = show ? 'flex' : 'none';
});
}
// Initialize filter
setFilter('open');
</script>
{% endblock %}