Files
TimeTracker/app/templates/projects/_kanban_tailwind.html
T
Dries Peeters a83aabb006 feat: Add user filter and flexible column layout to Kanban board
- Add 'Assigned To' filter dropdown to filter tasks by assigned user
  - Enables users to view their personal todo landscape
  - Filter works in combination with existing project filter
  - Shows all active users sorted by display name

- Replace fixed grid layout with flexible flexbox layout
  - Columns now expand evenly to fill available horizontal space
  - Added min-width constraint to prevent columns from becoming too narrow
  - Maintains horizontal scrolling on smaller screens for better mobile UX

This improves the Kanban board usability by allowing users to focus on
their assigned tasks and provides a better visual layout that adapts
to the number of columns and screen size.
2025-12-12 22:24:19 +01:00

93 lines
7.5 KiB
HTML

{# Reusable Kanban board for tasks. Expects `tasks` and `kanban_columns` in context. #}
<div class="kanban-board-wrapper p-4">
<div id="kanbanBoard" class="flex flex-row gap-6 overflow-x-auto" role="list" aria-label="{{ _('Kanban board columns') }}">
{% for col in kanban_columns %}
<div class="bg-card-light dark:bg-card-dark rounded-xl shadow-sm ring-1 ring-border-light/60 dark:ring-border-dark/60 flex flex-col flex-1 min-w-[280px] max-w-full" role="region" aria-labelledby="col-{{ col.key }}-title">
<div class="p-4 border-b border-border-light dark:border-border-dark flex justify-between items-center bg-gradient-to-b from-background-light/60 dark:from-background-dark/40 to-transparent rounded-t-xl">
<h3 id="col-{{ col.key }}-title" class="text-base font-semibold flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full" style="background-color: {{ col.color or '#4A90E2' }}"></span>
<span>{{ col.label }}</span>
</h3>
<div class="flex items-center gap-2">
<span class="kanban-count bg-gray-200 dark:bg-gray-700 text-xs font-semibold px-2 py-1 rounded-full" data-status="{{ col.key }}" aria-live="polite" aria-atomic="true">
{{ tasks|selectattr('status', 'equalto', col.key)|list|length }}
</span>
{% if current_user.is_admin %}
<a href="{{ url_for('kanban.edit_column', column_id=col.id) }}" class="text-text-muted-light dark:text-text-muted-dark hover:text-primary transition-colors" title="{{ _('Edit swimlane') }}">
<i class="fas fa-pen"></i>
</a>
{% endif %}
</div>
</div>
<div class="kanban-column-body p-3 space-y-3 overflow-y-auto max-h-[75vh] flex-grow" data-status="{{ col.key }}" role="list" aria-label="{{ _('Column') }}: {{ col.label }}" tabindex="0">
{% set column_tasks = tasks|selectattr('status', 'equalto', col.key)|list %}
{% if column_tasks %}
{% for task in column_tasks %}
<div class="kanban-card group bg-background-light dark:bg-background-dark p-4 rounded-lg shadow-sm border border-border-light dark:border-border-dark hover:shadow-md transition-all cursor-grab active:cursor-grabbing" draggable="true" data-task-id="{{ task.id }}" data-status="{{ task.status }}" data-project-id="{{ task.project_id }}" role="listitem" aria-grabbed="false" aria-label="{{ _('Task') }}: {{ task.name }} — {{ _('Status') }}: {{ col.label }}">
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="block">
<h4 class="font-semibold mb-2 group-hover:text-primary transition-colors">{{ task.name }}</h4>
{% if task.project %}
<div class="mb-2">
<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" data-testid="kanban-project-code">{{ task.project.code_display }}</span>
</div>
{% endif %}
{% if task.description %}
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mb-4">{{ task.description | truncate(80) }}</p>
{% endif %}
<div class="flex items-center justify-between text-sm text-text-muted-light dark:text-text-muted-dark">
<span class="text-sm font-semibold px-2 py-1 rounded-full
{% if task.priority == 'urgent' %} bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200
{% elif task.priority == 'high' %} bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200
{% elif task.priority == 'medium' %} bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200
{% else %} bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200 {% endif %}">
{{ task.priority | capitalize }}
</span>
{% if task.due_date %}
<span class="{{ 'text-red-500 dark:text-red-400' if task.is_overdue else 'text-text-muted-light dark:text-text-muted-dark' }}">
<i class="fas fa-calendar-alt mr-1"></i>
{{ task.due_date.strftime('%b %d') }}
</span>
{% endif %}
</div>
</a>
<div class="mt-4 flex justify-between items-center">
{% if task.assigned_user %}
{% if task.assigned_user.get_avatar_url() %}
<img src="{{ task.assigned_user.get_avatar_url() }}" alt="{{ task.assigned_user.display_name }}" class="w-8 h-8 rounded-full object-cover border-2 border-primary" title="{{ task.assigned_user.display_name }}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-semibold text-sm" style="display: none;" title="{{ task.assigned_user.display_name }}">
{{ task.assigned_user.display_name[0:1].upper() }}
</div>
{% else %}
<div class="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-semibold text-sm" title="{{ task.assigned_user.display_name }}">
{{ task.assigned_user.display_name[0:1].upper() }}
</div>
{% endif %}
{% else %}
<div class="w-8"></div> <!-- Placeholder for alignment -->
{% endif %}
</div>
<div class="mt-3 flex items-center justify-end gap-2">
<div class="flex items-center gap-2">
<a href="{{ url_for('timer.start_timer_for_project', project_id=task.project_id) }}?task_id={{ task.id }}" class="text-xs px-2 py-1 rounded bg-primary text-white hover:opacity-90">{{ _('Start') }}</a>
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="text-xs px-2 py-1 rounded border border-border-light dark:border-border-dark hover:bg-background-light dark:hover:bg-background-dark">{{ _('View') }}</a>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="kanban-empty text-center text-text-muted-light dark:text-text-muted-dark py-10 border border-dashed border-border-light dark:border-border-dark rounded-lg" aria-live="polite">
<p>{{ _('No tasks in this column.') }}</p>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>