mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-19 12:50:11 -05:00
b50ce512fa
The timer blueprint had two view functions named resume_timer, both registering as endpoint 'timer.resume_timer' and causing Flask to raise AssertionError on app load. - Give the 'resume by id' route a unique endpoint: resume_timer_by_id - Rename the view for GET /timer/resume/<timer_id> to resume_timer_by_id - Update templates to use timer.resume_timer_by_id for links with timer_id - Keep timer.resume_timer for POST (resume current paused timer)
186 lines
11 KiB
HTML
186 lines
11 KiB
HTML
{% extends "base.html" %}
|
|
{% from "components/ui.html" import confirm_dialog, page_header %}
|
|
|
|
{% block content %}
|
|
{% set actions %}
|
|
{% if current_user.is_admin or task.created_by == current_user.id %}
|
|
<div class="flex flex-wrap gap-2">
|
|
<a href="{{ url_for('tasks.edit_task', task_id=task.id) }}" class="btn btn-primary">{{ _('Edit Task') }}</a>
|
|
{% if task.status in ['todo','review'] %}
|
|
<form method="POST" action="{{ url_for('tasks.update_task_status', task_id=task.id) }}" onsubmit="return false;" class="inline">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="status" value="in_progress">
|
|
<button type="button" class="btn bg-emerald-600 text-white hover:bg-emerald-700"
|
|
onclick="var f = this.form; window.showConfirm('{{ _('Start task and mark as In Progress?') }}', { title: '{{ _('Change Task Status') }}', confirmText: '{{ _('Start') }}' }).then(function(ok){ if(ok) f.submit(); });">{{ _('Start') }}</button>
|
|
</form>
|
|
{% elif task.status == 'in_progress' %}
|
|
<form method="POST" action="{{ url_for('tasks.update_task_status', task_id=task.id) }}" onsubmit="return false;" class="inline">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="status" value="todo">
|
|
<button type="button" class="btn bg-amber-500 text-white hover:bg-amber-600"
|
|
onclick="var f = this.form; window.showConfirm('{{ _('Mark task as To Do?') }}', { title: '{{ _('Change Task Status') }}', confirmText: '{{ _('Change') }}' }).then(function(ok){ if(ok) f.submit(); });">{{ _('Pause') }}</button>
|
|
</form>
|
|
<form method="POST" action="{{ url_for('tasks.update_task_status', task_id=task.id) }}" onsubmit="return false;" class="inline">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="status" value="done">
|
|
<button type="button" class="btn bg-sky-600 text-white hover:bg-sky-700"
|
|
onclick="var f = this.form; window.showConfirm('{{ _('Mark task as Done?') }}', { title: '{{ _('Complete Task') }}', confirmText: '{{ _('Complete') }}' }).then(function(ok){ if(ok) f.submit(); });">{{ _('Complete') }}</button>
|
|
</form>
|
|
{% elif task.status == 'done' %}
|
|
<form method="POST" action="{{ url_for('tasks.update_task_status', task_id=task.id) }}" onsubmit="return false;" class="inline">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="status" value="review">
|
|
<button type="button" class="btn bg-gray-600 text-white hover:bg-gray-700"
|
|
onclick="var f = this.form; window.showConfirm('{{ _('Reopen task to Review?') }}', { title: '{{ _('Reopen Task') }}', confirmText: '{{ _('Reopen') }}' }).then(function(ok){ if(ok) f.submit(); });">{{ _('Reopen') }}</button>
|
|
</form>
|
|
{% endif %}
|
|
<button type="button" class="btn btn-danger"
|
|
onclick="document.getElementById('confirmDeleteTask-{{ task.id }}').classList.remove('hidden')">
|
|
{{ _('Delete Task') }}
|
|
</button>
|
|
<form id="confirmDeleteTask-{{ task.id }}-form" method="POST" action="{{ url_for('tasks.delete_task', task_id=task.id) }}" class="hidden">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
{% endset %}
|
|
{{ page_header('fas fa-tasks', task.name, subtitle_text=_('Task details and history.'), actions_html=actions, breadcrumbs=[{'text': _('Tasks'), 'url': url_for('tasks.list_tasks')}, {'text': task.name}]) }}
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Left Column: Task Details -->
|
|
<div class="lg:col-span-2 space-y-6">
|
|
{% if task.description %}
|
|
<div class="bg-card-light dark:bg-card-dark p-6 rounded-xl border border-border-light dark:border-border-dark shadow-sm">
|
|
<h2 class="text-lg font-semibold mb-4">Description</h2>
|
|
<div class="prose prose-sm dark:prose-invert max-w-none">{{ task.description | markdown | safe }}</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="bg-card-light dark:bg-card-dark p-6 rounded-xl border border-border-light dark:border-border-dark shadow-sm">
|
|
<h2 class="text-lg font-semibold mb-4">Time Entries</h2>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left responsive-cards">
|
|
<thead class="border-b border-border-light dark:border-border-dark">
|
|
<tr>
|
|
<th class="p-4">Date</th>
|
|
<th class="p-4">Duration</th>
|
|
<th class="p-4">User</th>
|
|
<th class="p-4">Notes</th>
|
|
<th class="p-4">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for entry in time_entries %}
|
|
<tr class="border-b border-border-light dark:border-border-dark">
|
|
<td class="p-4 mobile-card-header" data-label="{{ _('Date') }}">{{ entry.start_time|user_date }}</td>
|
|
<td class="p-4" data-label="{{ _('Duration') }}">{{ entry.duration_formatted }}</td>
|
|
<td class="p-4" data-label="{{ _('User') }}">{{ entry.user.display_name }}</td>
|
|
<td class="p-4" data-label="{{ _('Notes') }}">{% if entry.notes %}<span title="{{ entry.notes|striptags }}">{{ entry.notes|striptags|truncate(40) }}</span>{% else %}-{% endif %}</td>
|
|
<td class="p-4 mobile-actions" data-label="{{ _('Actions') }}">
|
|
<div class="flex gap-2">
|
|
<a href="{{ url_for('timer.resume_timer_by_id', timer_id=entry.id) }}" class="text-green-600 hover:text-green-800" title="{{ _('Resume - Start a new timer with same properties') }}">
|
|
<i class="fas fa-play"></i>
|
|
</a>
|
|
<a href="{{ url_for('timer.edit_timer', timer_id=entry.id) }}" class="text-primary hover:text-primary-dark" title="{{ _('Edit entry') }}">
|
|
<i class="fas fa-edit"></i>
|
|
</a>
|
|
{% if current_user.is_admin or entry.user_id == current_user.id %}
|
|
<form id="confirmDeleteEntry-{{ entry.id }}-form" method="POST" action="{{ url_for('timer.delete_timer', timer_id=entry.id) }}" class="hidden">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
</form>
|
|
<button type="button" class="text-red-600 hover:text-red-800" title="{{ _('Delete entry') }}" onclick="document.getElementById('confirmDeleteEntry-{{ entry.id }}').classList.remove('hidden')">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="5" class="p-4 text-center text-text-muted-light dark:text-text-muted-dark">No time has been logged for this task.</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Metadata -->
|
|
<div class="lg:col-span-1 space-y-6">
|
|
<div class="bg-card-light dark:bg-card-dark p-6 rounded-xl border border-border-light dark:border-border-dark shadow-sm">
|
|
<h2 class="text-lg font-semibold mb-4">Details</h2>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">Status</h3>
|
|
<p>{{ task.status_display }}</p>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">Priority</h3>
|
|
<p>{{ task.priority_display }}</p>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">Project</h3>
|
|
<p class="flex items-center gap-2">
|
|
<span>{{ task.project.name }}</span>
|
|
{% if task.project.code_display %}
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold tracking-wide uppercase bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-gray-200">{{ task.project.code_display }}</span>
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
{% if task.assigned_user %}
|
|
<div>
|
|
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">Assigned To</h3>
|
|
<p>{{ task.assigned_user.display_name }}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if task.tags %}
|
|
<div>
|
|
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Tags') }}</h3>
|
|
<p class="flex flex-wrap gap-1">
|
|
{% for tag in task.tag_list %}
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-gray-200">{{ tag }}</span>
|
|
{% endfor %}
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if task.due_date %}
|
|
<div>
|
|
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">Due Date</h3>
|
|
<p class="{{ 'text-red-500' if task.is_overdue else '' }}">{{ task.due_date|format_date }}</p>
|
|
</div>
|
|
{% endif %}
|
|
<div>
|
|
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Estimated') }}</h3>
|
|
<p>{% if task.estimated_hours %}{{ task.estimated_hours }} {{ _('h') }}{% else %}<span class="text-text-muted-light dark:text-text-muted-dark">—</span>{% endif %}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% if current_user.is_admin or task.created_by == current_user.id %}
|
|
{{ confirm_dialog(
|
|
'confirmDeleteTask-' ~ task.id,
|
|
'Delete Task',
|
|
'Are you sure you want to delete this task? This action cannot be undone.',
|
|
'Delete',
|
|
'Cancel',
|
|
'danger'
|
|
) }}
|
|
{% endif %}
|
|
|
|
<!-- Delete Entry Confirmation Dialogs -->
|
|
{% for entry in time_entries %}
|
|
{% if current_user.is_admin or entry.user_id == current_user.id %}
|
|
{{ confirm_dialog(
|
|
'confirmDeleteEntry-' ~ entry.id,
|
|
'Delete Time Entry',
|
|
'Are you sure you want to delete this time entry? This action cannot be undone.',
|
|
'Delete',
|
|
'Cancel',
|
|
'danger'
|
|
) }}
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% endblock %}
|