mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-19 04:40:32 -05:00
8bb42ddd02
- Add RecurringInvoiceRepository and RecurringInvoiceService; refactor recurring_invoice model - Add GanttService and move gantt logic from route to service - Expand ReportingService and simplify reports route - Add license_utils and user license template/settings - Refactor routes to use scope_filter, api_responses, and services (API v1, timer, admin, invoices, etc.) - Extend invoice_service for recurring; cache and scope_filter utils; base/template updates
226 lines
10 KiB
HTML
226 lines
10 KiB
HTML
{# DEPRECATED: Prefer components/ui.html for page_header, empty_state, and other UI macros. #}
|
|
{% macro page_header(icon_class, title_text, subtitle_text=None, actions_html=None) %}
|
|
<div class="card hover-lift mb-4 border-0" style="background: linear-gradient(135deg, var(--surface-color) 0%, var(--surface-variant) 100%);">
|
|
<div class="card-body d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center py-4">
|
|
<div class="mb-3 mb-md-0">
|
|
<div class="d-flex align-items-center mb-2">
|
|
{% if icon_class %}
|
|
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3 shadow-sm" style="width: 56px; height: 56px; backdrop-filter: blur(8px);">
|
|
<i class="{{ icon_class }} text-primary fa-lg"></i>
|
|
</div>
|
|
{% endif %}
|
|
<div>
|
|
<h1 class="h2 mb-1 fw-bold" style="background: linear-gradient(135deg, var(--text-primary), var(--primary-color)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">{{ _(title_text) }}</h1>
|
|
{% if subtitle_text %}
|
|
<p class="mb-0 text-muted fs-6">{{ _(subtitle_text) }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2 align-items-center">
|
|
{{ actions_html|safe if actions_html }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro summary_card(icon_class, icon_color, label, value, trend=None) %}
|
|
<div class="card h-100 hover-lift border-0 position-relative overflow-hidden">
|
|
<div class="position-absolute top-0 start-0 w-100 h-1" style="background: linear-gradient(90deg, var(--{{ icon_color }}-color), var(--{{ icon_color }}-light));"></div>
|
|
<div class="card-body text-center py-4">
|
|
<div class="bg-{{ icon_color }} bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center mx-auto mb-3 shadow-sm" style="width: 72px; height: 72px; backdrop-filter: blur(8px);">
|
|
<i class="{{ icon_class }} text-{{ icon_color }} fa-2x"></i>
|
|
</div>
|
|
<h3 class="h2 text-{{ icon_color }} mb-2 fw-bold" style="font-family: var(--font-family-mono);">{{ value }}</h3>
|
|
<p class="mb-0 text-muted fw-medium text-uppercase" style="font-size: 0.8rem; letter-spacing: 0.5px;">{{ _(label) }}</p>
|
|
{% if trend %}
|
|
<div class="mt-2">
|
|
<small class="text-{{ 'success' if trend > 0 else 'danger' if trend < 0 else 'muted' }}">
|
|
<i class="fas fa-{{ 'arrow-up' if trend > 0 else 'arrow-down' if trend < 0 else 'minus' }} me-1"></i>
|
|
{{ trend }}%
|
|
</small>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro empty_state(icon_class, title, message, actions_html=None, type="default") %}
|
|
<div class="empty-state empty-state-{{ type }} fade-in-up">
|
|
<div class="empty-state-icon empty-state-icon-animated">
|
|
<div class="empty-state-icon-circle">
|
|
<i class="{{ icon_class }}"></i>
|
|
</div>
|
|
</div>
|
|
<h3 class="empty-state-title">{{ _(title) }}</h3>
|
|
<p class="empty-state-description">{{ _(message) }}</p>
|
|
{% if actions_html %}
|
|
<div class="empty-state-actions">
|
|
{{ actions_html|safe }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro empty_state_with_features(icon_class, title, message, features, actions_html=None, type="default") %}
|
|
<div class="empty-state empty-state-{{ type }} fade-in-up">
|
|
<div class="empty-state-icon empty-state-icon-animated">
|
|
<div class="empty-state-icon-circle">
|
|
<i class="{{ icon_class }}"></i>
|
|
</div>
|
|
</div>
|
|
<h3 class="empty-state-title">{{ _(title) }}</h3>
|
|
<p class="empty-state-description">{{ _(message) }}</p>
|
|
|
|
{% if features %}
|
|
<div class="empty-state-features">
|
|
{% for feature in features %}
|
|
<div class="empty-state-feature">
|
|
<i class="{{ feature.icon }} empty-state-feature-icon"></i>
|
|
<div class="empty-state-feature-content">
|
|
<div class="empty-state-feature-title">{{ _(feature.title) }}</div>
|
|
<div class="empty-state-feature-description">{{ _(feature.description) }}</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if actions_html %}
|
|
<div class="empty-state-actions">
|
|
{{ actions_html|safe }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro skeleton_card() %}
|
|
<div class="skeleton-summary-card">
|
|
<div class="skeleton skeleton-summary-card-icon"></div>
|
|
<div class="skeleton skeleton-summary-card-label"></div>
|
|
<div class="skeleton skeleton-summary-card-value"></div>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro skeleton_table(rows=5, cols=4) %}
|
|
<div class="skeleton-table">
|
|
{% for i in range(rows) %}
|
|
<div class="skeleton-table-row">
|
|
{% for j in range(cols) %}
|
|
<div class="skeleton-table-cell">
|
|
<div class="skeleton skeleton-text"></div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro skeleton_list(items=5) %}
|
|
<div class="list-group">
|
|
{% for i in range(items) %}
|
|
<div class="skeleton-list-item">
|
|
<div class="skeleton skeleton-avatar"></div>
|
|
<div class="flex-grow-1">
|
|
<div class="skeleton skeleton-text" style="width: 70%;"></div>
|
|
<div class="skeleton skeleton-text" style="width: 40%;"></div>
|
|
</div>
|
|
<div class="skeleton skeleton-badge"></div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro loading_spinner(size="md", text=None) %}
|
|
<div class="text-center">
|
|
<div class="loading-spinner loading-spinner-{{ size }} mb-3"></div>
|
|
{% if text %}
|
|
<div class="text-muted">{{ _(text) }}</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro loading_overlay(text="Loading...") %}
|
|
<div class="loading-overlay">
|
|
<div class="loading-overlay-content">
|
|
<div class="loading-spinner loading-spinner-lg loading-overlay-spinner"></div>
|
|
<div class="mt-3">{{ _(text) }}</div>
|
|
</div>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro modern_button(text, url, icon_class=None, variant="primary", size="md", attributes="") %}
|
|
<a href="{{ url }}" class="btn btn-{{ variant }}{% if size == 'sm' %} btn-sm{% elif size == 'lg' %} btn-lg{% endif %} shadow-sm" {{ attributes|safe }} style="backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);">
|
|
{% if icon_class %}<i class="{{ icon_class }} me-2"></i>{% endif %}
|
|
{{ _(text) }}
|
|
</a>
|
|
{% endmacro %}
|
|
|
|
{% macro status_badge(status, text=None) %}
|
|
{% set status_map = {
|
|
'active': {'color': 'success', 'icon': 'fas fa-check-circle', 'bg': 'rgba(16, 185, 129, 0.1)'},
|
|
'inactive': {'color': 'secondary', 'icon': 'fas fa-pause-circle', 'bg': 'rgba(100, 116, 139, 0.1)'},
|
|
'pending': {'color': 'warning', 'icon': 'fas fa-clock', 'bg': 'rgba(245, 158, 11, 0.1)'},
|
|
'completed': {'color': 'success', 'icon': 'fas fa-check-circle', 'bg': 'rgba(16, 185, 129, 0.1)'},
|
|
'cancelled': {'color': 'danger', 'icon': 'fas fa-times-circle', 'bg': 'rgba(239, 68, 68, 0.1)'},
|
|
'draft': {'color': 'secondary', 'icon': 'fas fa-edit', 'bg': 'rgba(100, 116, 139, 0.1)'},
|
|
'published': {'color': 'primary', 'icon': 'fas fa-globe', 'bg': 'rgba(59, 130, 246, 0.1)'}
|
|
} %}
|
|
{% set config = status_map.get(status, {'color': 'secondary', 'icon': 'fas fa-circle', 'bg': 'rgba(100, 116, 139, 0.1)'}) %}
|
|
<span class="badge text-{{ config.color }} px-3 py-2 fw-medium border-0 shadow-sm" style="background: {{ config.bg }}; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); font-size: 0.8rem; letter-spacing: 0.025em;">
|
|
<i class="{{ config.icon }} me-1 opacity-75"></i>
|
|
{{ _(text or status.title()) }}
|
|
</span>
|
|
{% endmacro %}
|
|
|
|
{% macro info_card(title, content, icon_class=None, color="primary") %}
|
|
<div class="card border-0 shadow-sm position-relative overflow-hidden">
|
|
<div class="position-absolute top-0 start-0 w-100" style="height: 3px; background: linear-gradient(90deg, var(--{{ color }}-color), var(--{{ color }}-light));"></div>
|
|
<div class="card-body p-4">
|
|
<div class="d-flex align-items-start">
|
|
{% if icon_class %}
|
|
<div class="bg-{{ color }} bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3 flex-shrink-0 shadow-sm" style="width: 48px; height: 48px; backdrop-filter: blur(8px);">
|
|
<i class="{{ icon_class }} text-{{ color }}"></i>
|
|
</div>
|
|
{% endif %}
|
|
<div class="flex-grow-1">
|
|
<h6 class="fw-semibold mb-2 text-dark">{{ _(title) }}</h6>
|
|
<p class="mb-0 text-muted fs-6 lh-relaxed">{{ _(content) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro progress_card(title, current, total, color="primary", show_percentage=True) %}
|
|
{% set percentage = (current / total * 100) if total > 0 else 0 %}
|
|
<div class="card h-100 hover-lift border-0 shadow-sm position-relative overflow-hidden">
|
|
<div class="position-absolute top-0 start-0 w-100" style="height: 2px; background: linear-gradient(90deg, var(--{{ color }}-color), var(--{{ color }}-light));"></div>
|
|
<div class="card-body p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="fw-semibold mb-0 text-dark">{{ _(title) }}</h6>
|
|
{% if show_percentage %}
|
|
<span class="text-{{ color }} fw-bold fs-5" style="font-family: var(--font-family-mono);">{{ "%.0f%%"|format(percentage) }}</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="progress mb-3 shadow-sm" style="height: 10px; border-radius: var(--border-radius-full);">
|
|
<div class="progress-bar bg-{{ color }} position-relative overflow-hidden" role="progressbar" style="width: {{ percentage }}%; border-radius: var(--border-radius-full);">
|
|
<div class="position-absolute top-0 start-0 w-100 h-100" style="background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); animation: shimmer 2s infinite;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<small class="text-muted fw-medium">{{ current }} / {{ total }}</small>
|
|
<small class="text-muted">{{ total - current }} {{ _('remaining') }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
@keyframes shimmer {
|
|
0% { transform: translateX(-100%); }
|
|
100% { transform: translateX(100%); }
|
|
}
|
|
</style>
|
|
{% endmacro %}
|
|
|