Files
TimeTracker/templates/projects/view.html
Dries Peeters 77aec94b86 feat: Add project costs tracking and remove license server integration
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)
2025-10-09 11:50:26 +02:00

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 %}