mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-04 02:30:01 -06:00
Major Features: - Add project costs feature with full CRUD operations - Implement toast notification system for better user feedback - Enhance analytics dashboard with improved visualizations - Add OIDC authentication improvements and debug tools Improvements: - Enhance reports with new filtering and export capabilities - Update command palette with additional shortcuts - Improve mobile responsiveness across all pages - Refactor UI components for consistency Removals: - Remove license server integration and related dependencies - Clean up unused license-related templates and utilities Technical Changes: - Add new migration 018 for project_costs table - Update models: Project, Settings, User with new relationships - Refactor routes: admin, analytics, auth, invoices, projects, reports - Update static assets: CSS improvements, new JS modules - Enhance templates: analytics, admin, projects, reports Documentation: - Add comprehensive documentation for project costs feature - Document toast notification system with visual guides - Update README with new feature descriptions - Add migration instructions and quick start guides - Document OIDC improvements and Kanban enhancements Files Changed: - Modified: 56 files (core app, models, routes, templates, static assets) - Deleted: 6 files (license server integration) - Added: 28 files (new features, documentation, migrations)
692 lines
38 KiB
HTML
692 lines
38 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ project.name }} - {{ app_name }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div class="d-flex align-items-center">
|
|
<div>
|
|
<nav aria-label="breadcrumb" class="mb-1">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{{ url_for('projects.list_projects') }}">{{ _('Projects') }}</a></li>
|
|
<li class="breadcrumb-item active">{{ project.name }}</li>
|
|
</ol>
|
|
</nav>
|
|
<h1 class="h3 mb-0 me-3">
|
|
<i class="fas fa-project-diagram text-primary"></i>
|
|
{{ project.name }}
|
|
</h1>
|
|
</div>
|
|
<div class="ms-3">
|
|
{% if project.status == 'active' %}
|
|
<span class="status-badge bg-success text-white"><i class="fas fa-check-circle me-2"></i>{{ _('Active') }}</span>
|
|
{% else %}
|
|
<span class="status-badge bg-secondary text-white"><i class="fas fa-archive me-2"></i>{{ _('Archived') }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="btn-group" role="group">
|
|
{% if current_user.is_admin %}
|
|
<a href="{{ url_for('projects.edit_project', project_id=project.id) }}" class="btn btn-secondary">
|
|
<i class="fas fa-edit me-1"></i> {{ _('Edit') }}
|
|
</a>
|
|
{% if project.status == 'active' %}
|
|
<form method="POST" action="{{ url_for('projects.archive_project', project_id=project.id) }}" class="d-inline">
|
|
<button type="submit" class="btn btn-outline-secondary" data-confirm="{{ _('Archive this project?') }}">
|
|
<i class="fas fa-archive me-1"></i> {{ _('Archive') }}
|
|
</button>
|
|
</form>
|
|
{% else %}
|
|
<form method="POST" action="{{ url_for('projects.unarchive_project', project_id=project.id) }}" class="d-inline">
|
|
<button type="submit" class="btn btn-outline-secondary" data-confirm="{{ _('Unarchive this project?') }}">
|
|
<i class="fas fa-box-open me-1"></i> {{ _('Unarchive') }}
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endif %}
|
|
<a href="{{ url_for('main.dashboard') }}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-arrow-left me-1"></i> {{ _('Back') }}
|
|
</a>
|
|
<!-- Start Timer removed on project page -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Project Details -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-8">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="invoice-section">
|
|
<h6 class="section-title text-primary mb-3">
|
|
<i class="fas fa-info-circle me-2"></i>{{ _('General') }}
|
|
</h6>
|
|
<div class="detail-row"><span class="detail-label">{{ _('Name') }}</span><span class="detail-value">{{ project.name }}</span></div>
|
|
<div class="detail-row"><span class="detail-label">{{ _('Client') }}</span>
|
|
<span class="detail-value">
|
|
{% if project.client_obj %}
|
|
<a href="{{ url_for('clients.view_client', client_id=project.client_id) }}">{{ project.client_obj.name }}</a>
|
|
{% else %}<span class="text-muted">-</span>{% endif %}
|
|
</span>
|
|
</div>
|
|
<div class="detail-row"><span class="detail-label">{{ _('Status') }}</span>
|
|
<span class="detail-value">{% if project.status == 'active' %}{{ _('Active') }}{% else %}{{ _('Archived') }}{% endif %}</span>
|
|
</div>
|
|
<div class="detail-row"><span class="detail-label">{{ _('Created') }}</span><span class="detail-value">{{ project.created_at.strftime('%B %d, %Y') }}</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="invoice-section">
|
|
<h6 class="section-title text-primary mb-3">
|
|
<i class="fas fa-cog me-2"></i>{{ _('Billing') }}
|
|
</h6>
|
|
<div class="detail-row"><span class="detail-label">{{ _('Billable') }}</span>
|
|
<span class="detail-value">{% if project.billable %}{{ _('Yes') }}{% else %}{{ _('No') }}{% endif %}</span>
|
|
</div>
|
|
{% if project.billable and project.hourly_rate %}
|
|
<div class="detail-row"><span class="detail-label">{{ _('Hourly Rate') }}</span><span class="detail-value">{{ currency }} {{ "%.2f"|format(project.hourly_rate) }}</span></div>
|
|
{% endif %}
|
|
{% if project.billing_ref %}
|
|
<div class="detail-row"><span class="detail-label">{{ _('Billing Ref') }}</span><span class="detail-value">{{ project.billing_ref }}</span></div>
|
|
{% endif %}
|
|
<div class="detail-row"><span class="detail-label">{{ _('Last Updated') }}</span><span class="detail-value">{{ project.updated_at.strftime('%B %d, %Y') }}</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if project.description %}
|
|
<div class="mt-3">
|
|
<h6 class="section-title text-primary mb-2">{{ _('Description') }}</h6>
|
|
<div class="content-box">{{ project.description }}</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-light py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-chart-bar me-2"></i>{{ _('Statistics') }}
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row text-center">
|
|
<div class="col-6 mb-3">
|
|
<div class="h4 text-primary">{{ "%.1f"|format(project.total_hours) }}</div>
|
|
<small class="text-muted">{{ _('Total Hours') }}</small>
|
|
</div>
|
|
<div class="col-6 mb-3">
|
|
<div class="h4 text-success">{{ "%.1f"|format(project.total_billable_hours) }}</div>
|
|
<small class="text-muted">{{ _('Billable Hours') }}</small>
|
|
</div>
|
|
{% if project.estimated_hours %}
|
|
<div class="col-6 mb-3">
|
|
<div class="h4">{{ "%.1f"|format(project.estimated_hours) }}</div>
|
|
<small class="text-muted">{{ _('Estimated Hours') }}</small>
|
|
</div>
|
|
{% endif %}
|
|
<div class="col-6 mb-3">
|
|
<div class="h4 text-info">{{ currency }} {{ "%.2f"|format(project.total_costs) }}</div>
|
|
<small class="text-muted">{{ _('Total Costs') }}</small>
|
|
</div>
|
|
{% if project.budget_amount %}
|
|
<div class="col-6 mb-3">
|
|
<div class="h4">{{ currency }} {{ "%.2f"|format(project.budget_consumed_amount) }}</div>
|
|
<small class="text-muted">{{ _('Budget Used (Hours)') }}</small>
|
|
</div>
|
|
{% endif %}
|
|
{% if project.billable and project.hourly_rate %}
|
|
<div class="col-6">
|
|
<div class="h4 text-success">{{ currency }} {{ "%.2f"|format(project.total_project_value) }}</div>
|
|
<small class="text-muted">{{ _('Total Project Value') }}</small>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if project.budget_amount %}
|
|
<div class="mt-3">
|
|
<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100">
|
|
{% set pct = (project.budget_consumed_amount / project.budget_amount * 100) | round(0, 'floor') %}
|
|
<div class="progress-bar {% if pct >= project.budget_threshold_percent %}bg-danger{% else %}bg-primary{% endif %}" style="width: {{ pct }}%">{{ pct }}%</div>
|
|
</div>
|
|
<div class="d-flex justify-content-between small mt-1">
|
|
<span>{{ _('Budget') }}: {{ currency }} {{ "%.2f"|format(project.budget_amount) }}</span>
|
|
<span>{{ _('Threshold') }}: {{ project.budget_threshold_percent }}%</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if project.billable and project.hourly_rate %}
|
|
<div class="card mt-3 shadow-sm border-0">
|
|
<div class="card-header bg-light py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-users me-2"></i>{{ _('User Breakdown') }}
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
{% for user_total in project.get_user_totals() %}
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<span>{{ user_total.username }}</span>
|
|
<span class="text-primary">{{ "%.1f"|format(user_total.total_hours) }}h</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tasks -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-tasks"></i> {{ _('Tasks') }}
|
|
</h5>
|
|
<div>
|
|
<a href="{{ url_for('tasks.create_task') }}?project_id={{ project.id }}" class="btn-header btn-primary">
|
|
<i class="fas fa-plus"></i> {{ _('New Task') }}
|
|
</a>
|
|
<a href="{{ url_for('tasks.list_tasks', project_id=project.id) }}" class="btn btn-sm btn-outline-primary">
|
|
<i class="fas fa-list"></i> {{ _('View All') }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
{% set project_tasks = tasks %}
|
|
{% include 'tasks/_kanban.html' with context %}
|
|
{% if not tasks %}
|
|
<div class="mt-3">
|
|
{% from "_components.html" import empty_state %}
|
|
{% set actions %}
|
|
<a href="{{ url_for('tasks.create_task') }}?project_id={{ project.id }}" class="btn btn-success btn-sm">
|
|
<i class="fas fa-plus"></i> {{ _('Create First Task') }}
|
|
</a>
|
|
{% endset %}
|
|
{{ empty_state('fas fa-tasks', _('No Tasks Yet'), _('Break down this project into manageable tasks to track progress.'), actions) }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Project Costs -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-receipt me-2"></i>{{ _('Project Costs & Expenses') }}
|
|
</h6>
|
|
<a href="{{ url_for('projects.add_cost', project_id=project.id) }}" class="btn btn-sm btn-primary">
|
|
<i class="fas fa-plus me-1"></i>{{ _('Add Cost') }}
|
|
</a>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if total_costs_count > 0 %}
|
|
<div class="row mb-3">
|
|
<div class="col-md-4">
|
|
<div class="text-center p-3 bg-light rounded">
|
|
<div class="h5 text-primary mb-0">{{ currency }} {{ "%.2f"|format(project.total_costs) }}</div>
|
|
<small class="text-muted">{{ _('Total Costs') }}</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="text-center p-3 bg-light rounded">
|
|
<div class="h5 text-success mb-0">{{ currency }} {{ "%.2f"|format(project.total_billable_costs) }}</div>
|
|
<small class="text-muted">{{ _('Billable Costs') }}</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="text-center p-3 bg-light rounded">
|
|
<div class="h5 text-info mb-0">{{ currency }} {{ "%.2f"|format(project.total_project_value) }}</div>
|
|
<small class="text-muted">{{ _('Total Project Value') }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>{{ _('Date') }}</th>
|
|
<th>{{ _('Description') }}</th>
|
|
<th>{{ _('Category') }}</th>
|
|
<th class="text-end">{{ _('Amount') }}</th>
|
|
<th>{{ _('Billable') }}</th>
|
|
<th>{{ _('Actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for cost in recent_costs %}
|
|
<tr>
|
|
<td>{{ cost.cost_date.strftime('%Y-%m-%d') if cost.cost_date else 'N/A' }}</td>
|
|
<td>
|
|
{{ cost.description if cost.description else 'No description' }}
|
|
{% if cost.notes %}
|
|
<i class="fas fa-info-circle text-muted" title="{{ cost.notes }}"></i>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-secondary">{{ _(cost.category.title() if cost.category else 'Other') }}</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<strong>{{ cost.currency_code if cost.currency_code else 'EUR' }} {{ "%.2f"|format(cost.amount if cost.amount else 0) }}</strong>
|
|
</td>
|
|
<td>
|
|
{% if cost.billable %}
|
|
<span class="badge bg-success">{{ _('Yes') }}</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">{{ _('No') }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group" role="group">
|
|
<a href="{{ url_for('projects.edit_cost', project_id=project.id, cost_id=cost.id) }}"
|
|
class="btn btn-sm btn-action btn-action--edit" title="{{ _('Edit') }}">
|
|
<i class="fas fa-edit"></i>
|
|
</a>
|
|
{% if current_user.is_admin or cost.user_id == current_user.id %}
|
|
{% if not cost.is_invoiced %}
|
|
<button type="button" class="btn btn-sm btn-action btn-action--danger" title="{{ _('Delete') }}"
|
|
onclick="showDeleteCostModal('{{ cost.id }}', '{{ cost.description }}', '{{ cost.amount }}')">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% if total_costs_count > 5 %}
|
|
<div class="text-center mt-2">
|
|
<a href="{{ url_for('projects.list_costs', project_id=project.id) }}" class="btn btn-sm btn-outline-primary">
|
|
{{ _('View All Costs') }} ({{ total_costs_count }})
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
{% if not recent_costs %}
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-info-circle me-2"></i>{{ _('Recent costs will appear here. Total costs for this project: ') }}{{ total_costs_count }}
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
{% from "_components.html" import empty_state %}
|
|
{% set actions %}
|
|
<a href="{{ url_for('projects.add_cost', project_id=project.id) }}" class="btn btn-primary btn-sm">
|
|
<i class="fas fa-plus"></i> {{ _('Add First Cost') }}
|
|
</a>
|
|
{% endset %}
|
|
{{ empty_state('fas fa-receipt', _('No Costs Yet'), _('Track project expenses like travel, materials, and services.'), actions) }}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Rates (Overrides) -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-money-bill-wave me-2"></i>{{ _('Rate Overrides') }}
|
|
</h6>
|
|
<a href="{{ url_for('projects.edit_project', project_id=project.id) }}#rates" class="btn btn-sm btn-outline-primary">{{ _('Manage') }}</a>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="text-muted small">{{ _('Effective rates are applied in this order: user-specific override, project default override, project hourly rate, client default rate.') }}</div>
|
|
<div class="mt-2">
|
|
{% if project.hourly_rate %}
|
|
<div><strong>{{ _('Project rate') }}:</strong> {{ currency }} {{ "%.2f"|format(project.hourly_rate) }}</div>
|
|
{% else %}
|
|
<div class="text-muted">{{ _('Project has no hourly rate; relying on overrides or client default.') }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comments -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-white py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-comments me-2"></i>{{ _('Project Comments') }}
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
{% include 'comments/_comments_section.html' with context %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Time Entries -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-clock me-2"></i>{{ _('Time Entries') }}
|
|
</h6>
|
|
<div class="d-flex gap-2">
|
|
<a href="{{ url_for('reports.project_report', project_id=project.id) }}" class="btn btn-sm btn-outline-primary">
|
|
<i class="fas fa-chart-line"></i> {{ _('View Report') }}
|
|
</a>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="showBurndownBtn">
|
|
<i class="fas fa-fire"></i> {{ _('Burn-down') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="burndownContainer" class="mb-3" style="display:none;">
|
|
<canvas id="burndownChart" height="100"></canvas>
|
|
</div>
|
|
{% if entries %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>{{ _('User') }}</th>
|
|
<th>{{ _('Date') }}</th>
|
|
<th>{{ _('Time') }}</th>
|
|
<th>{{ _('Duration') }}</th>
|
|
<th>{{ _('Notes') }}</th>
|
|
<th>{{ _('Tags') }}</th>
|
|
<th>{{ _('Billable') }}</th>
|
|
<th>{{ _('Actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for entry in entries %}
|
|
<tr>
|
|
<td>{{ entry.user.display_name }}</td>
|
|
<td>{{ entry.start_time.strftime('%Y-%m-%d') }}</td>
|
|
<td>
|
|
{{ entry.start_time.strftime('%H:%M') }} -
|
|
{% if entry.end_time %}
|
|
{{ entry.end_time.strftime('%H:%M') }}
|
|
{% else %}
|
|
<span class="text-warning">{{ _('Running') }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<strong>{{ entry.duration_formatted }}</strong>
|
|
</td>
|
|
<td>
|
|
{% if entry.notes %}
|
|
<span title="{{ entry.notes }}">{{ entry.notes[:50] }}{% if entry.notes|length > 50 %}...{% endif %}</span>
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if entry.tag_list %}
|
|
{% for tag in entry.tag_list %}
|
|
<span class="badge bg-light text-dark">{{ tag }}</span>
|
|
{% endfor %}
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if entry.billable %}
|
|
<span class="badge bg-success">{{ _('Yes') }}</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">{{ _('No') }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group" role="group">
|
|
<a href="{{ url_for('timer.edit_timer', timer_id=entry.id) }}"
|
|
class="btn btn-sm btn-action btn-action--edit" title="{{ _('Edit') }}">
|
|
<i class="fas fa-edit"></i>
|
|
</a>
|
|
{% if current_user.is_admin or entry.user_id == current_user.id %}
|
|
<button type="button" class="btn btn-sm btn-action btn-action--danger" title="{{ _('Delete') }}"
|
|
onclick="showDeleteEntryModal('{{ entry.id }}', '{{ entry.project.name }}', '{{ entry.duration_formatted }}')">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if pagination.pages > 1 %}
|
|
<nav aria-label="Time entries pagination">
|
|
<ul class="pagination justify-content-center">
|
|
{% if pagination.has_prev %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="{{ url_for('projects.view_project', project_id=project.id, page=pagination.prev_num) }}">{{ _('Previous') }}</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% for page_num in pagination.iter_pages() %}
|
|
{% if page_num %}
|
|
{% if page_num != pagination.page %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="{{ url_for('projects.view_project', project_id=project.id, page=page_num) }}">{{ page_num }}</a>
|
|
</li>
|
|
{% else %}
|
|
<li class="page-item active">
|
|
<span class="page-link">{{ page_num }}</span>
|
|
</li>
|
|
{% endif %}
|
|
{% else %}
|
|
<li class="page-item disabled">
|
|
<span class="page-link">…</span>
|
|
</li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if pagination.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="{{ url_for('projects.view_project', project_id=project.id, page=pagination.next_num) }}">{{ _('Next') }}</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
{% endif %}
|
|
{% else %}
|
|
{% from "_components.html" import empty_state %}
|
|
{{ empty_state('fas fa-clock', _('No Time Entries'), _('No time has been tracked for this project yet.')) }}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
<!-- Delete Time Entry Modal -->
|
|
<div class="modal fade" id="deleteEntryModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-trash me-2 text-danger"></i>{{ _('Delete Time Entry') }}
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
<strong>{{ _('Warning:') }}</strong> {{ _('This action cannot be undone.') }}
|
|
</div>
|
|
<p>{{ _('Are you sure you want to delete the time entry for') }} <strong id="deleteEntryProjectName"></strong>?</p>
|
|
<p class="text-muted mb-0">{{ _('Duration:') }} <strong id="deleteEntryDuration"></strong></p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>{{ _('Cancel') }}
|
|
</button>
|
|
<form method="POST" id="deleteEntryForm" class="d-inline">
|
|
<button type="submit" class="btn btn-danger">
|
|
<i class="fas fa-trash me-2"></i>{{ _('Delete Entry') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Comment Modal -->
|
|
<div class="modal fade" id="deleteCommentModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-trash me-2 text-danger"></i>{{ _('Delete Comment') }}
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
<strong>{{ _('Warning:') }}</strong> {{ _('This action cannot be undone.') }}
|
|
</div>
|
|
<p>{{ _('Are you sure you want to delete this comment?') }}</p>
|
|
<div id="comment-preview" class="bg-light p-3 rounded mt-3" style="display: none;">
|
|
<div class="text-muted small mb-1">{{ _('Comment preview:') }}</div>
|
|
<div id="comment-preview-text"></div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>{{ _('Cancel') }}
|
|
</button>
|
|
<form method="POST" id="deleteCommentForm" class="d-inline">
|
|
<button type="submit" class="btn btn-danger">
|
|
<i class="fas fa-trash me-2"></i>{{ _('Delete Comment') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Cost Modal -->
|
|
<div class="modal fade" id="deleteCostModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-trash me-2 text-danger"></i>{{ _('Delete Cost') }}
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
<strong>{{ _('Warning:') }}</strong> {{ _('This action cannot be undone.') }}
|
|
</div>
|
|
<p>{{ _('Are you sure you want to delete this cost?') }}</p>
|
|
<p class="mb-0"><strong>{{ _('Description:') }}</strong> <span id="deleteCostDescription"></span></p>
|
|
<p class="mb-0"><strong>{{ _('Amount:') }}</strong> <span id="deleteCostAmount"></span></p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>{{ _('Cancel') }}
|
|
</button>
|
|
<form method="POST" id="deleteCostForm" class="d-inline">
|
|
<button type="submit" class="btn btn-danger">
|
|
<i class="fas fa-trash me-2"></i>{{ _('Delete Cost') }}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% block extra_js %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
|
<script>
|
|
// Function to show delete time entry modal
|
|
function showDeleteEntryModal(entryId, projectName, duration) {
|
|
document.getElementById('deleteEntryProjectName').textContent = projectName;
|
|
document.getElementById('deleteEntryDuration').textContent = duration;
|
|
document.getElementById('deleteEntryForm').action = "{{ url_for('timer.delete_timer', timer_id=0) }}".replace('0', entryId);
|
|
new bootstrap.Modal(document.getElementById('deleteEntryModal')).show();
|
|
}
|
|
|
|
// Function to show delete cost modal
|
|
function showDeleteCostModal(costId, description, amount) {
|
|
document.getElementById('deleteCostDescription').textContent = description;
|
|
document.getElementById('deleteCostAmount').textContent = '{{ currency }} ' + amount;
|
|
document.getElementById('deleteCostForm').action = "{{ url_for('projects.delete_cost', project_id=project.id, cost_id=0) }}".replace('0', costId);
|
|
new bootstrap.Modal(document.getElementById('deleteCostModal')).show();
|
|
}
|
|
|
|
// Add loading state to delete entry form
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Only add event listener if the form exists
|
|
const deleteEntryForm = document.getElementById('deleteEntryForm');
|
|
if (deleteEntryForm) {
|
|
deleteEntryForm.addEventListener('submit', function(e) {
|
|
const submitBtn = this.querySelector('button[type="submit"]');
|
|
submitBtn.innerHTML = '<div class="loading-spinner me-2"></div>{{ _('Deleting...') }}';
|
|
submitBtn.disabled = true;
|
|
});
|
|
}
|
|
|
|
// Add loading state to delete cost form
|
|
const deleteCostForm = document.getElementById('deleteCostForm');
|
|
if (deleteCostForm) {
|
|
deleteCostForm.addEventListener('submit', function(e) {
|
|
const submitBtn = this.querySelector('button[type="submit"]');
|
|
submitBtn.innerHTML = '<div class="loading-spinner me-2"></div>{{ _('Deleting...') }}';
|
|
submitBtn.disabled = true;
|
|
});
|
|
}
|
|
|
|
// Burndown chart toggle
|
|
const btn = document.getElementById('showBurndownBtn');
|
|
if (!btn) return;
|
|
const container = document.getElementById('burndownContainer');
|
|
let chart;
|
|
btn.addEventListener('click', async function(){
|
|
container.style.display = container.style.display === 'none' ? 'block' : 'none';
|
|
if (container.style.display === 'none') return;
|
|
try {
|
|
const res = await fetch(`/api/projects/{{ project.id }}/burndown`);
|
|
const data = await res.json();
|
|
const ctx = document.getElementById('burndownChart').getContext('2d');
|
|
if (chart) chart.destroy();
|
|
chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.labels,
|
|
datasets: [
|
|
{ label: '{{ _('Actual (cum hrs)') }}', data: data.actual_cumulative, borderColor: '#3b82f6', backgroundColor: 'rgba(59,130,246,.1)', tension:.2 },
|
|
{ label: '{{ _('Estimate') }}', data: data.estimated, borderColor: '#94a3b8', borderDash:[6,4], tension:0 }
|
|
]
|
|
},
|
|
options: { responsive: true, maintainAspectRatio: false }
|
|
});
|
|
} catch (e) { console.error(e); }
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|