mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-07 13:00:22 -05:00
ac465d9612
- 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
296 lines
19 KiB
HTML
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>
|