mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-19 12:50:11 -05:00
2ee8da33a0
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).
138 lines
5.7 KiB
HTML
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 %}
|
|
|