Files
TimeTracker/app/templates/projects/_projects_list.html
T
Dries Peeters ac465d9612 feat: Enhance UI/UX with improved form validation and error handling
- Add comprehensive form validation system with real-time feedback
- Implement enhanced error handling with retry mechanisms and offline support
- Update route handlers for improved error responses
- Enhance list templates with better error handling and validation
- Update dashboard, timer, and report templates with enhanced UI
- Improve project service with better error handling
- Update config manager utilities
- Bump version to 4.2.0

Files updated:
- Routes: auth, clients, invoices, projects, quotes, tasks, timer, custom_reports
- Templates: base, dashboard, all list views, timer pages, reports
- Static: enhanced-ui.js, error-handling-enhanced.js, form-validation.js
- Services: project_service.py
- Utils: config_manager.py
- Version: setup.py
2025-11-30 10:51:09 +01:00

296 lines
19 KiB
HTML

<div id="projectsListContainer">
<div class="flex justify-between items-center mb-4">
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">
{{ projects|length }} project{{ 's' if projects|length != 1 else '' }} found
</h3>
<div class="flex items-center gap-2">
<!-- View Toggle -->
<div class="flex items-center gap-1 bg-background-light dark:bg-background-dark rounded-lg p-1">
<button type="button" id="listViewBtn" onclick="setViewMode('list')" class="px-3 py-1.5 text-sm rounded transition-colors view-mode-btn active" data-view="list" title="{{ _('List View') }}">
<i class="fas fa-list"></i>
</button>
<button type="button" id="gridViewBtn" onclick="setViewMode('grid')" class="px-3 py-1.5 text-sm rounded transition-colors view-mode-btn" data-view="grid" title="{{ _('Grid View') }}">
<i class="fas fa-th"></i>
</button>
</div>
<a href="{{ url_for('projects.export_projects', status=request.args.get('status', 'active'), client=request.args.get('client', ''), search=request.args.get('search', ''), favorites=request.args.get('favorites', '')) }}"
class="px-3 py-1.5 text-sm bg-background-light dark:bg-background-dark rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors inline-flex items-center export-btn"
title="{{ _('Export to CSV') }}"
onclick="showExportLoading(this); return true;">
<i class="fas fa-download mr-1"></i> <span class="export-text">Export</span>
</a>
{% if current_user.is_admin %}
<div class="relative">
<button type="button" id="bulkActionsBtn" class="px-3 py-1.5 text-sm border rounded-lg text-gray-700 dark:text-gray-200 border-gray-300 dark:border-gray-600 disabled:opacity-60" onclick="openMenu(this, 'projectsBulkMenu')" disabled>
<i class="fas fa-tasks mr-1"></i> Bulk Actions (<span id="selectedCount">0</span>)
</button>
<ul id="projectsBulkMenu" class="bulk-menu hidden absolute right-0 mt-2 w-56 bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark rounded-md shadow-lg z-50">
<li><a class="block px-4 py-2 text-sm text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700" href="#" onclick="return showBulkStatusChange('active')"><i class="fas fa-check-circle mr-2 text-green-600"></i>Mark as Active</a></li>
<li><a class="block px-4 py-2 text-sm text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700" href="#" onclick="return showBulkStatusChange('inactive')"><i class="fas fa-pause-circle mr-2 text-amber-500"></i>Mark as Inactive</a></li>
<li><a class="block px-4 py-2 text-sm text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700" href="#" onclick="return showBulkArchiveDialog()"><i class="fas fa-archive mr-2 text-gray-600"></i>Archive</a></li>
<li><hr class="my-1 border-border-light dark:border-border-dark"></li>
<li><a class="block px-4 py-2 text-sm text-rose-600 hover:bg-gray-100 dark:hover:bg-gray-700" href="#" onclick="return showBulkDeleteConfirm()"><i class="fas fa-trash mr-2"></i>Delete</a></li>
</ul>
</div>
{% endif %}
</div>
</div>
<!-- List View -->
<div id="listView" class="view-container">
<table class="table table-zebra w-full text-left" data-table-enhanced data-page-size="25" data-sticky-header="true" data-column-visibility="true">
<thead class="border-b border-border-light dark:border-border-dark">
<tr>
{% if current_user.is_admin %}
<th class="p-4 w-12">
<input type="checkbox" id="selectAll" class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary" onchange="toggleAllProjects()">
</th>
{% endif %}
<th class="p-4 w-10"></th>
<th class="p-4" data-sortable>Name</th>
<th class="p-4" data-sortable>Client</th>
<th class="p-4" data-sortable>Status</th>
<th class="p-4" data-sortable>Billable</th>
<th class="p-4" data-sortable>Rate</th>
<th class="p-4" data-sortable>Budget</th>
<th class="p-4">Actions</th>
</tr>
</thead>
<tbody>
{% for project in projects %}
<tr class="border-b border-border-light dark:border-border-dark hover:bg-gray-50 dark:hover:bg-gray-800">
{% if current_user.is_admin %}
<td class="p-4">
<input type="checkbox" class="project-checkbox h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary" value="{{ project.id }}" onchange="updateBulkDeleteButton()">
</td>
{% endif %}
<td class="p-4 text-center">
{% set is_fav = favorite_project_ids and project.id in favorite_project_ids %}
<button type="button"
class="favorite-btn text-xl hover:scale-110 transition-transform"
data-project-id="{{ project.id }}"
data-is-favorited="{{ 'true' if is_fav else 'false' }}"
onclick="toggleFavorite({{ project.id }}, this)"
title="{{ 'Remove from favorites' if is_fav else 'Add to favorites' }}">
<i class="{{ 'fas fa-star text-yellow-500' if is_fav else 'far fa-star text-gray-400' }}"></i>
</button>
</td>
<td class="p-4 font-medium"><a href="{{ url_for('projects.view_project', project_id=project.id) }}" class="text-primary hover:underline">{{ project.name }}</a></td>
<td class="p-4">
{% if project.client_id %}
<a href="{{ url_for('clients.view_client', client_id=project.client_id) }}" class="text-primary hover:underline">{{ project.client }}</a>
{% else %}
{{ project.client }}
{% endif %}
</td>
<td class="p-4">
{% if project.status == 'active' %}
<span class="px-2 py-1 rounded-full text-xs font-medium whitespace-nowrap bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300">Active</span>
{% elif project.status == 'inactive' %}
<span class="px-2 py-1 rounded-full text-xs font-medium whitespace-nowrap bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">Inactive</span>
{% else %}
<span class="px-2 py-1 rounded-full text-xs font-medium whitespace-nowrap bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200">Archived</span>
{% endif %}
</td>
<td class="p-4">
{% if project.billable %}
<span class="px-2 py-1 rounded-full text-xs font-medium whitespace-nowrap bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300">Billable</span>
{% else %}
<span class="px-2 py-1 rounded-full text-xs font-medium whitespace-nowrap bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300">Non-billable</span>
{% endif %}
</td>
<td class="p-4">
{% if project.hourly_rate %}
<span class="px-2 py-1 rounded-md text-xs font-medium bg-primary/10 text-primary">{{ '%.2f'|format(project.hourly_rate|float) }}/h</span>
{% else %}
<span class="text-text-muted-light dark:text-text-muted-dark"></span>
{% endif %}
</td>
<td class="p-4">
{% if project.budget_amount %}
{% set consumed = (project.budget_consumed_amount or 0.0) %}
{% set total = project.budget_amount|float %}
{% set pct = (consumed / total * 100) if total > 0 else 0 %}
{% if pct >= 90 %}
{% set badge_classes = 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300' %}
{% elif pct >= 70 %}
{% set badge_classes = 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300' %}
{% else %}
{% set badge_classes = 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300' %}
{% endif %}
<span class="px-2 py-1 rounded-full text-xs font-medium whitespace-nowrap {{ badge_classes }}">{{ pct|round(0) }}%</span>
{% else %}
<span class="text-text-muted-light dark:text-text-muted-dark"></span>
{% endif %}
</td>
<td class="p-4">
<a href="{{ url_for('projects.view_project', project_id=project.id) }}" class="text-primary hover:underline">View</a>
</td>
</tr>
{% else %}
{% endfor %}
</tbody>
</table>
</div>
<!-- Grid View -->
<div id="gridView" class="view-container hidden">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for project in projects %}
<div class="project-card bg-card-light dark:bg-card-dark rounded-lg shadow-md hover:shadow-lg transition-all duration-200 relative overflow-hidden group">
<!-- Quick Actions on Hover -->
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10">
<div class="flex gap-1">
<a href="{{ url_for('projects.view_project', project_id=project.id) }}"
class="p-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors"
title="{{ _('View Project') }}">
<i class="fas fa-eye text-xs"></i>
</a>
{% if current_user.is_admin or has_permission('edit_projects') %}
<a href="{{ url_for('projects.edit_project', project_id=project.id) }}"
class="p-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
title="{{ _('Edit Project') }}">
<i class="fas fa-edit text-xs"></i>
</a>
{% endif %}
<button type="button"
class="favorite-btn p-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition-colors"
data-project-id="{{ project.id }}"
data-is-favorited="{{ 'true' if (favorite_project_ids and project.id in favorite_project_ids) else 'false' }}"
onclick="toggleFavorite({{ project.id }}, this)"
title="{{ 'Remove from favorites' if (favorite_project_ids and project.id in favorite_project_ids) else 'Add to favorites' }}">
<i class="{{ 'fas' if (favorite_project_ids and project.id in favorite_project_ids) else 'far' }} fa-star text-xs"></i>
</button>
</div>
</div>
<!-- Project Header -->
<div class="p-6 pb-4">
<div class="flex items-start justify-between mb-2">
<h3 class="text-lg font-semibold text-text-light dark:text-text-dark flex-1 pr-2">
{{ project.name }}
</h3>
<!-- Status Badge -->
<span class="px-2 py-1 rounded-full text-xs font-medium whitespace-nowrap
{% if project.status == 'active' %}
bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300
{% elif project.status == 'inactive' %}
bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300
{% else %}
bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200
{% endif %}">
{{ project.status|title }}
</span>
</div>
{% if project.client %}
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mb-3">
<i class="fas fa-building mr-1"></i>{{ project.client }}
</p>
{% endif %}
{% if project.description %}
<div class="text-sm text-text-muted-light dark:text-text-muted-dark mb-4 line-clamp-2 prose prose-sm dark:prose-invert max-w-none">
{{ project.description | markdown | safe }}
</div>
{% endif %}
</div>
<!-- Progress Indicators -->
<div class="px-6 pb-4 space-y-3">
{% if project.budget_amount %}
{% set consumed = (project.budget_consumed_amount or 0.0) %}
{% set total = project.budget_amount|float %}
{% set pct = (consumed / total * 100) if total > 0 else 0 %}
<div>
<div class="flex justify-between items-center mb-1">
<span class="text-xs font-medium text-text-muted-light dark:text-text-muted-dark">Budget</span>
<span class="text-xs font-semibold
{% if pct >= 90 %}text-red-600
{% elif pct >= 70 %}text-amber-600
{% else %}text-green-600{% endif %}">
{{ pct|round(0) }}%
</span>
</div>
<div class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div class="h-full rounded-full transition-all duration-300
{% if pct >= 90 %}bg-red-500
{% elif pct >= 70 %}bg-amber-500
{% else %}bg-green-500{% endif %}"
style="width: {{ [pct,100]|min }}%">
</div>
</div>
<div class="text-xs text-text-muted-light dark:text-text-muted-dark mt-1">
{{ "%.2f"|format(consumed) }} / {{ "%.2f"|format(total) }}
</div>
</div>
{% endif %}
<!-- Hours Progress -->
<div>
<div class="flex justify-between items-center mb-1">
<span class="text-xs font-medium text-text-muted-light dark:text-text-muted-dark">Hours</span>
<span class="text-xs font-semibold text-text-light dark:text-text-dark">
{{ "%.1f"|format(project.total_hours) }}h
</span>
</div>
{% if project.estimated_hours %}
{% set hours_pct = (project.total_hours / project.estimated_hours * 100) if project.estimated_hours > 0 else 0 %}
<div class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div class="h-full bg-primary rounded-full transition-all duration-300"
style="width: {{ [hours_pct,100]|min }}%">
</div>
</div>
<div class="text-xs text-text-muted-light dark:text-text-muted-dark mt-1">
{{ "%.1f"|format(project.total_hours) }} / {{ "%.1f"|format(project.estimated_hours) }}h
</div>
{% endif %}
</div>
</div>
<!-- Project Footer -->
<div class="px-6 py-4 border-t border-border-light dark:border-border-dark bg-background-light dark:bg-background-dark">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3 text-sm">
{% if project.billable %}
<span class="px-2 py-1 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300">
<i class="fas fa-dollar-sign mr-1"></i>Billable
</span>
{% endif %}
{% if project.hourly_rate %}
<span class="text-xs text-text-muted-light dark:text-text-muted-dark">
{{ '%.2f'|format(project.hourly_rate|float) }}/h
</span>
{% endif %}
</div>
<a href="{{ url_for('projects.view_project', project_id=project.id) }}"
class="text-primary hover:text-primary/80 text-sm font-medium">
View <i class="fas fa-arrow-right ml-1"></i>
</a>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% if not projects %}
{% from "components/ui.html" import empty_state %}
{% set actions %}
{% if current_user.is_admin %}
<a href="{{ url_for('projects.create_project') }}" class="bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary/90 transition-colors">
<i class="fas fa-plus mr-2"></i>Create Your First Project
</a>
{% endif %}
<a href="{{ url_for('main.help') }}" class="bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-4 py-2 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
<i class="fas fa-question-circle mr-2"></i>Learn More
</a>
{% endset %}
{% if request.args.get('search') or request.args.get('client') or request.args.get('status') != 'all' %}
{{ empty_state('fas fa-search', 'No Projects Match Your Filters', 'Try adjusting your filters to see more results. You can clear filters or create a new project that matches your criteria.', actions, type='no-results') }}
{% else %}
{{ empty_state('fas fa-folder-open', 'No Projects Yet', 'Projects help you organize your work and track time efficiently. Create your first project to get started!', actions, type='no-data') }}
{% endif %}
{% endif %}
</div>