Files
TimeTracker/app/templates/reports/custom_view.html
T
Dries Peeters 2ee8da33a0 feat: TimeTracker polish and production readiness (plan implementation)
Quick wins (Phase A):
- A1: Quick timer actions — last timer context, Repeat last button, Quick start one-click form; pre-fill modal and tags from last entry
- A2: Unified empty states using empty_state macro on custom_view, time_entry_templates, saved_filters, issues; add loading_placeholder macro
- A3: Dashboard hierarchy — Activity and Support/Donate moved to secondary row below fold with reduced visual weight
- A4: Error/feedback consistency (flash-to-toast already in place)

Medium impact (Phase B):
- B5: Split API v1 — api_v1_common.py (shared helpers), api_v1_time_entries.py sub-blueprint for time-entries and timer/*; register api_v1_time_entries_bp
- B6: Start Timer UX — templates as prominent chips at top of modal; default last context and quick start from A1
- B7: Week in review — ReportingService.get_week_in_review(), route /reports/week-in-review, template and link from reports index
- B8: Tags discoverability — GET /api/tags, recent_tags in dashboard, tags input with datalist in Start Timer modal; last context includes tags
- B9: Frontend consolidation — document onboarding.js vs onboarding-enhanced.js in base.html
- B10: API validation — Marshmallow TimeEntryCreateSchema/TimeEntryUpdateSchema and handle_validation_error in api_v1_time_entries create/update

UX: Remove duplicate Timer actions — single Repeat last and Start Timer in header; body shows only Resume when recent entries exist (no duplicate Repeat last or Start new).
2026-03-11 08:00:47 +01:00

138 lines
5.7 KiB
HTML

{% extends "base.html" %}
{% from "components/ui.html" import page_header, empty_state %}
{% block title %}{{ saved_view.name }} - {{ _('Custom Report') }} - {{ app_name }}{% endblock %}
{% block content %}
{% set breadcrumbs = [
{'text': 'Reports', 'url': url_for('reports.reports')},
{'text': 'Report Builder', 'url': url_for('custom_reports.report_builder')},
{'text': saved_view.name}
] %}
{{ page_header(
icon_class='fas fa-chart-bar',
title_text=saved_view.name,
subtitle_text='Custom Report',
breadcrumbs=breadcrumbs
) }}
<div class="bg-card-light dark:bg-card-dark p-6 rounded-xl border border-border-light dark:border-border-dark shadow-sm">
{% if config.components %}
<div class="space-y-6">
{% for component in config.components %}
{% if component == 'table' %}
<div>
<h3 class="text-lg font-semibold mb-4">{{ _('Data Table') }}</h3>
{% if report_data.data and report_data.data|length > 0 %}
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-border-light dark:divide-border-dark responsive-cards">
<thead>
<tr>
{% for col in report_data.data[0].keys() %}
<th class="px-6 py-3 text-left text-xs font-medium text-text-muted-light dark:text-text-muted-dark uppercase">{{ col|replace('_', ' ')|title }}</th>
{% endfor %}
</tr>
</thead>
<tbody class="divide-y divide-border-light dark:divide-border-dark">
{% for row in report_data.data %}
<tr>
{% for key, value in row.items() %}
<td class="px-6 py-4 whitespace-nowrap text-sm" data-label="{{ key|replace('_', ' ')|title }}">{{ value }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
{% set edit_report_action %}<a href="{{ url_for('custom_reports.report_builder') }}" class="btn btn-primary"><i class="fas fa-edit mr-2"></i>{{ _('Edit Report') }}</a>{% endset %}
{{ empty_state(
'fas fa-inbox',
_('No data found'),
_('This report has no data matching the current filters. Try adjusting your date range or filters.'),
edit_report_action,
type='no-results'
) }}
{% endif %}
</div>
{% elif component == 'summary' %}
<div>
<h3 class="text-lg font-semibold mb-4">{{ _('Summary') }}</h3>
{% if report_data.summary and report_data.summary|length > 0 %}
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
{% for key, value in report_data.summary.items() %}
<div class="bg-background-light dark:bg-background-dark p-4 rounded-lg">
<div class="text-sm text-text-muted-light dark:text-text-muted-dark">{{ key|replace('_', ' ')|title }}</div>
<div class="text-2xl font-bold">{{ value }}</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-center text-text-muted-light dark:text-text-muted-dark py-4">{{ _('No summary data available.') }}</p>
{% endif %}
</div>
{% elif component == 'chart' %}
<div>
<h3 class="text-lg font-semibold mb-4">{{ _('Chart') }}</h3>
{% if report_data.data and report_data.data|length > 0 %}
<canvas id="reportChart" width="400" height="200"></canvas>
{% else %}
<p class="text-center text-text-muted-light dark:text-text-muted-dark py-4">{{ _('No data available for chart.') }}</p>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="text-center py-12">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gray-100 dark:bg-gray-800 mb-4">
<i class="fas fa-cog text-3xl text-gray-400"></i>
</div>
<h3 class="text-xl font-medium text-gray-900 dark:text-white mb-2">{{ _('No components configured') }}</h3>
<p class="text-gray-600 dark:text-gray-400 mb-6">
{{ _('This report has no components configured. Please edit the report to add components.') }}
</p>
<a href="{{ url_for('custom_reports.report_builder') }}" class="btn btn-primary">
<i class="fas fa-edit mr-2"></i>{{ _('Edit Report') }}
</a>
</div>
{% endif %}
</div>
<script>
// Render chart if chart component exists
{% if 'chart' in config.components and report_data.data and report_data.data|length > 0 %}
const ctx = document.getElementById('reportChart');
if (ctx) {
try {
new Chart(ctx, {
type: 'bar',
data: {
labels: {{ report_data.data|map(attribute='date')|list|tojson }},
datasets: [{
label: 'Hours',
data: {{ report_data.data|map(attribute='duration')|list|tojson }},
backgroundColor: 'rgba(59, 130, 246, 0.5)',
borderColor: 'rgba(59, 130, 246, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true
}
}
}
});
} catch (error) {
console.error('Error rendering chart:', error);
}
}
{% endif %}
</script>
{% endblock %}