From e5a8728285821e8bfe2427bf8ee37994129f28d2 Mon Sep 17 00:00:00 2001 From: Dries Peeters Date: Thu, 11 Sep 2025 20:35:19 +0200 Subject: [PATCH] feat(kanban): add Kanban board view, drag-and-drop, and API support - Add Kanban board partial and integrate into tasks list - app/templates/tasks/_kanban.html - app/templates/tasks/list.html - Update API to support task status moves and ordering - app/routes/api.py - Expose Kanban in layout/navigation and polish UI - app/templates/base.html - app/static/base.css - Link/entry points from related pages - templates/projects/view.html - templates/reports/index.html - Minor profile page adjustments - app/templates/auth/profile.html --- app/routes/api.py | 14 ++++- app/static/base.css | 63 +++++++++++++++++++ app/templates/auth/profile.html | 9 ++- app/templates/base.html | 61 +++++++++++++++++- app/templates/tasks/_kanban.html | 98 +++++++++++++++++++++-------- app/templates/tasks/list.html | 102 +++++++++++++++++++++++++------ templates/projects/view.html | 19 +++--- templates/reports/index.html | 2 +- 8 files changed, 307 insertions(+), 61 deletions(-) diff --git a/app/routes/api.py b/app/routes/api.py index 0137843..6e8afba 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -31,6 +31,7 @@ def timer_status(): 'id': active_timer.id, 'project_name': active_timer.project.name, 'project_id': active_timer.project_id, + 'task_id': active_timer.task_id, 'start_time': active_timer.start_time.isoformat(), 'current_duration': active_timer.current_duration_seconds, 'duration_formatted': active_timer.duration_formatted @@ -74,6 +75,7 @@ def api_start_timer(): """Start timer via API""" data = request.get_json() project_id = data.get('project_id') + task_id = data.get('task_id') if not project_id: return jsonify({'error': 'Project ID is required'}), 400 @@ -83,6 +85,13 @@ def api_start_timer(): if not project: return jsonify({'error': 'Invalid project'}), 400 + # Validate task if provided + task = None + if task_id: + task = Task.query.filter_by(id=task_id, project_id=project_id).first() + if not task: + return jsonify({'error': 'Invalid task for selected project'}), 400 + # Check if user already has an active timer active_timer = current_user.active_timer if active_timer: @@ -93,6 +102,7 @@ def api_start_timer(): new_timer = TimeEntry( user_id=current_user.id, project_id=project_id, + task_id=task.id if task else None, start_time=local_now(), source='auto' ) @@ -105,13 +115,15 @@ def api_start_timer(): 'user_id': current_user.id, 'timer_id': new_timer.id, 'project_name': project.name, + 'task_id': task.id if task else None, 'start_time': new_timer.start_time.isoformat() }) return jsonify({ 'success': True, 'timer_id': new_timer.id, - 'project_name': project.name + 'project_name': project.name, + 'task_id': task.id if task else None }) @api_bp.route('/api/timer/stop', methods=['POST']) diff --git a/app/static/base.css b/app/static/base.css index af9d48a..d0ea83e 100644 --- a/app/static/base.css +++ b/app/static/base.css @@ -346,6 +346,33 @@ main { .btn-icon.btn-sm { width: 32px; height: 32px; min-height: 32px; } .btn-icon i { font-size: 0.95rem; } +/* Global theme toggle button */ +.theme-toggle { + border: 1px solid var(--border-color); + background: linear-gradient(180deg, #ffffff, #f8fafc); + color: var(--text-secondary); + transition: var(--transition); + opacity: 0.9; +} +.theme-toggle:hover { + transform: translateY(-1px); + box-shadow: 0 6px 20px rgba(0,0,0,0.12); + opacity: 1; + color: var(--text-primary); +} +.theme-toggle:active { transform: translateY(0); } +[data-theme="dark"] .theme-toggle { + border-color: var(--border-color); + background: #0f172a; + color: var(--text-secondary); + box-shadow: inset 0 0 0 1px rgba(255,255,255,0.03); +} +[data-theme="dark"] .theme-toggle:hover { + background: #111827; + color: var(--text-primary); + box-shadow: 0 8px 24px rgba(0,0,0,0.5); +} + .btn-success { background: var(--success-color); color: white; @@ -2002,6 +2029,33 @@ h6 { font-size: 1rem; } margin-bottom: 1rem; } +/* Ensure header subtitle text is white within stats-card headers */ +.stats-card .text-muted, +.stats-card p { + color: rgba(255, 255, 255, 0.9) !important; +} +.stats-card h1, .stats-card .h1, +.stats-card h2, .stats-card .h2, +.stats-card h3, .stats-card .h3 { + color: #ffffff !important; +} + +/* Header action buttons on stats cards: low opacity default, high on hover */ +.stats-card .btn-header { + color: rgba(255, 255, 255, 0.9); + border-color: rgba(255, 255, 255, 0.45); + background: transparent; + opacity: 0.85; +} +.stats-card .btn-header:hover { + opacity: 1; + color: #ffffff; + border-color: rgba(255, 255, 255, 0.9); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(255, 255, 255, 0.15); +} +.stats-card .btn-header i { color: inherit; } + .stats-card h4 { font-size: 2.25rem; font-weight: 700; @@ -2193,6 +2247,15 @@ h6 { font-size: 1rem; } margin-right: 0.375rem; } +/* Add small spacing after icons in regular buttons (avoid icon-only/header buttons) */ +.btn:not(.btn-icon):not(.btn-header) i { + margin-right: 0.375rem; +} + +/* Ensure primary (blue) buttons render white text/icons in light mode */ +.btn.btn-primary { color: #ffffff; } +.btn.btn-primary i { color: inherit; } + /* Consistent badge sizing used next to page titles */ .badge.fs-6 { line-height: 1; diff --git a/app/templates/auth/profile.html b/app/templates/auth/profile.html index 4c5140e..f561857 100644 --- a/app/templates/auth/profile.html +++ b/app/templates/auth/profile.html @@ -7,12 +7,11 @@
{% set actions %} - + Edit Profile - {% endset %} {{ page_header('fas fa-user-circle', 'Your Profile', 'Manage your account details', actions) }} @@ -70,7 +69,7 @@ document.addEventListener('DOMContentLoaded', function() { document.documentElement.setAttribute('data-theme', theme); if (meta) meta.setAttribute('content', theme === 'dark' ? '#0b1220' : '#3b82f6'); const icon = btn.querySelector('i'); - if (icon) icon.className = theme === 'dark' ? 'fas fa-sun me-1' : 'fas fa-moon me-1'; + if (icon) icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon'; } function currentTheme() { diff --git a/app/templates/base.html b/app/templates/base.html index 440412f..50f2ccd 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -124,6 +124,7 @@