Files
TimeTracker/app/templates/base.html
Dries Peeters a582e2af62 feat: improve error handling, performance logging, and PWA install UI
- Add session state clearing (expunge_all) after rollbacks in custom field
  definition error handlers to prevent stale session state
- Add graceful error handling for missing link_templates table with proper
  rollback and session cleanup, preventing app crashes when migrations
  haven't been run
- Add detailed performance logging to TaskService.list_tasks method to track
  timing of each query step for performance monitoring
- Improve PWA install prompt UI with better toast integration, dismiss button,
  and proper DOM manipulation using requestAnimationFrame
- Bump version to 4.5.0
2025-12-12 21:49:26 +01:00

1708 lines
126 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="{{ current_language_code or 'en' }}" dir="{{ 'rtl' if is_rtl else 'ltr' }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ app_name }}{% endblock %}</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="description" content="Professional time tracking and project management">
<meta name="theme-color" content="#3b82f6">
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='images/timetracker-logo.svg') }}">
<link rel="alternate icon" href="{{ url_for('static', filename='images/timetracker-logo.svg') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='images/timetracker-logo.svg') }}">
<!--
PostHog Session Replay Privacy Protection:
Add the CSS class 'ph-no-capture' to any element containing sensitive data
to prevent it from being recorded in session replays.
Examples:
- Project names: <span class="ph-no-capture">Project ABC</span>
- Client names: <div class="ph-no-capture">Client XYZ</div>
- Time entry notes: <textarea class="ph-no-capture" name="notes"></textarea>
- User emails: <span class="ph-no-capture">user@example.com</span>
PostHog will mask these elements automatically in recordings.
For more details, see: POSTHOG_SESSION_REPLAY_PRIVACY.md
-->
<link rel="manifest" href="{{ url_for('static', filename='manifest.webmanifest') }}">
<meta name="vapid-public-key" content="{{ config.get('VAPID_PUBLIC_KEY', '') }}">
<script src="{{ url_for('static', filename='pwa-enhancements.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='dist/output.css') }}">
<!-- Font Awesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='form-bridge.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='form-validation.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='enhanced-ui.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='data-tables-enhanced.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='toast-notifications.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='keyboard-shortcuts.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='ui-enhancements.css') }}">
<style>
/* RTL Support */
html[dir="rtl"] {
direction: rtl;
}
html[dir="rtl"] .ml-auto { margin-left: 0; margin-right: auto; }
html[dir="rtl"] .mr-auto { margin-right: 0; margin-left: auto; }
html[dir="rtl"] .text-left { text-align: right; }
html[dir="rtl"] .text-right { text-align: left; }
html[dir="rtl"] #sidebar { left: auto; right: 0; }
html[dir="rtl"] #mainContent { margin-left: 0; margin-right: 16rem; }
@media (max-width: 1024px) {
html[dir="rtl"] #mainContent { margin-right: 0; }
}
/* Minimal styles to properly align enhanced search UI */
.search-enhanced .search-input-wrapper { position: relative; }
.search-enhanced .search-icon { position: absolute; left: 0.75rem; top: 50%; transform: translateY(-50%); color: #A0AEC0; pointer-events: none; }
.dark .search-enhanced .search-icon { color: #718096; }
.search-enhanced .search-actions { position: absolute; right: 0.5rem; top: 50%; transform: translateY(-50%); display: flex; align-items: center; gap: 0.25rem; }
.search-enhanced .search-actions .search-kbd { font-size: 11px; line-height: 1; padding: 2px 6px; border: 1px solid #E2E8F0; border-radius: 4px; color: #A0AEC0; }
.dark .search-enhanced .search-actions .search-kbd { border-color: #4A5568; color: #718096; }
/* Autocomplete dropdown: overlay below input */
.search-enhanced { position: relative; }
.search-enhanced .search-autocomplete { position: absolute; top: calc(100% + 4px); left: 0; right: 0; display: none; z-index: 50; background: #FFFFFF; color: #2D3748; border: 1px solid #E2E8F0; border-radius: 0.5rem; box-shadow: 0 8px 24px rgba(0,0,0,0.12); max-height: 22rem; overflow-y: auto; }
.dark .search-enhanced .search-autocomplete { background: #2D3748; color: #E2E8F0; border-color: #4A5568; }
.search-enhanced .search-autocomplete.show { display: block; }
.search-enhanced .search-section { padding: 0.5rem 0.5rem; }
.search-enhanced .search-section-title { font-size: 0.75rem; text-transform: uppercase; letter-spacing: .06em; margin: 0.25rem 0.5rem; color: #A0AEC0; }
.dark .search-enhanced .search-section-title { color: #718096; }
.search-enhanced .search-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0.75rem; text-decoration: none; color: inherit; }
.search-enhanced .search-item:hover, .search-enhanced .search-item.keyboard-focus { background: #F7F9FB; }
.dark .search-enhanced .search-item:hover, .dark .search-enhanced .search-item.keyboard-focus { background: #1A202C; }
.search-enhanced .search-item-title { font-weight: 500; }
/* Sidebar collapsed helpers */
.sidebar-collapsed .sidebar-label { display: none; }
.sidebar-collapsed .sidebar-header-title { display: none; }
.sidebar-collapsed #workDropdown, .sidebar-collapsed #insightsDropdown { display: none; }
.sidebar-collapsed #sidebar { width: 4rem !important; overflow-x: hidden; }
/* Shared bulk menu */
.bulk-menu { z-index: 50; max-height: 16rem; overflow-y: auto; }
/* Layout fixes */
#mainContent {
display: flex !important;
flex-direction: column !important;
overflow-x: hidden;
width: 100%;
}
#mainContentAnchor {
flex: 1 0 auto;
min-height: 0;
width: 100%;
overflow-x: auto;
}
/* Ensure body and html don't cause horizontal overflow */
html, body { overflow-x: hidden; }
#appShell { overflow-x: hidden; width: 100%; }
/* Custom scrollbar styling for sidebar */
#sidebar {
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
}
.dark #sidebar {
scrollbar-color: rgba(156, 163, 175, 0.3) transparent;
}
/* Webkit scrollbar styling (Chrome, Edge, Safari) */
#sidebar::-webkit-scrollbar {
width: 8px;
}
#sidebar::-webkit-scrollbar-track {
background: transparent;
}
#sidebar::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 4px;
border: 2px solid transparent;
background-clip: padding-box;
}
#sidebar::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.7);
}
.dark #sidebar::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.3);
}
.dark #sidebar::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.5);
}
</style>
<script>
// Theme init (unchanged)
{% if current_user.is_authenticated and current_user.theme %}
var userTheme = '{{ current_user.theme }}';
if (userTheme === 'dark') {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
} else if (userTheme === 'light') {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else if (userTheme === 'system') {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
{% else %}
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
{% endif %}
</script>
<!-- i18n translations for JavaScript -->
<script>
window.i18n = {
toast: {
success: '{{ _("Success") }}',
error: '{{ _("Error") }}',
warning: '{{ _("Warning") }}',
info: '{{ _("Information") }}'
},
common: {
loading: '{{ _("Loading...") }}',
saving: '{{ _("Saving...") }}',
deleting: '{{ _("Deleting...") }}',
cancel: '{{ _("Cancel") }}',
confirm: '{{ _("Confirm") }}',
close: '{{ _("Close") }}',
save: '{{ _("Save") }}',
delete: '{{ _("Delete") }}',
edit: '{{ _("Edit") }}',
add: '{{ _("Add") }}',
remove: '{{ _("Remove") }}',
yes: '{{ _("Yes") }}',
no: '{{ _("No") }}',
ok: '{{ _("OK") }}'
},
messages: {
confirmDelete: '{{ _("Are you sure you want to delete this?") }}',
unsavedChanges: '{{ _("You have unsaved changes. Are you sure you want to leave?") }}',
operationFailed: '{{ _("Operation failed") }}',
operationSuccess: '{{ _("Operation completed successfully") }}',
noItemsSelected: '{{ _("No items selected") }}',
invalidInput: '{{ _("Invalid input") }}',
requiredField: '{{ _("This field is required") }}',
noActiveTimer: '{{ _("No active timer") }}',
timerStopped: '{{ _("Timer stopped") }}',
timerStopFailed: '{{ _("Failed to stop timer") }}',
errorStoppingTimer: '{{ _("Error stopping timer") }}',
noFormToSave: '{{ _("No form to save") }}',
noTimerFound: '{{ _("No timer found") }}',
timerStoppedInactivity: '{{ _("Timer stopped due to inactivity") }}',
selectProjectOrClient: '{{ _("Please select either a project or a client") }}',
endTimeAfterStartTime: '{{ _("End time must be after start time") }}'
}
};
</script>
{% block extra_css %}{% endblock %}
</head>
<body class="bg-background-light dark:bg-background-dark text-text-light dark:text-text-dark">
<script>
// Sidebar collapsed ASAP
(function(){ try { if (localStorage.getItem('sidebar-collapsed') === 'true') { document.documentElement.classList.add('sidebar-collapsed'); } } catch(_) {} })();
</script>
<a href="#mainContentAnchor" class="sr-only focus:not-sr-only focus-ring absolute left-2 top-2 z-[1000] px-3 py-2 bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark rounded">{{ _('Skip to content') }}</a>
<div id="appShell" class="flex min-h-screen">
<!-- Sidebar -->
<aside id="sidebar" class="w-64 bg-card-light dark:bg-card-dark text-text-light dark:text-text-dark p-4 flex-col hidden lg:flex transition-all duration-200 ease-in-out fixed top-0 left-0 h-screen overflow-y-auto z-10">
<div class="flex items-center mb-8">
<img src="{{ url_for('static', filename='images/timetracker-logo.svg') }}" alt="Logo" class="h-10 w-10 mr-2">
<h1 class="text-2xl font-bold text-primary sidebar-header-title"><a href="{{ url_for('main.dashboard') }}" class="no-underline">TimeTracker</a></h1>
</div>
<nav class="flex-1">
{% set ep = request.endpoint or '' %}
{% set work_open = ep.startswith('projects.') or ep.startswith('tasks.') or ep.startswith('timer.') or ep.startswith('kanban.') or ep.startswith('weekly_goals.') or ep.startswith('project_templates.') or ep.startswith('gantt.') %}
{% set calendar_open = ep.startswith('calendar.') %}
{% set finance_open = ep.startswith('reports.') or ep.startswith('invoices.') or ep.startswith('recurring_invoices.') or ep.startswith('payments.') or ep.startswith('expenses.') or ep.startswith('mileage.') or ep.startswith('per_diem.') or ep.startswith('budget_alerts.') or ep.startswith('invoice_approvals.') or ep.startswith('payment_gateways.') or ep.startswith('scheduled_reports.') or ep.startswith('custom_reports.') %}
{% set crm_open = ep.startswith('clients.') or ep.startswith('quotes.') %}
{% set inventory_open = ep.startswith('inventory.') %}
{% set analytics_open = ep.startswith('analytics.') %}
{% set tools_open = ep.startswith('import_export.') or ep.startswith('saved_filters.') or ep.startswith('integrations.') %}
{% set admin_open = ep.startswith('admin.') or ep.startswith('permissions.') or (ep.startswith('expense_categories.') and current_user.is_admin) or (ep.startswith('per_diem.list_rates') and current_user.is_admin) or ep.startswith('time_entry_templates.') or ep.startswith('audit_logs.') or ep.startswith('webhooks.') or ep.startswith('custom_field_definitions.') or ep.startswith('link_templates.') %}
{% set admin_user_mgmt_open = ep == 'admin.list_users' or ep.startswith('permissions.') %}
{% set admin_settings_open = ep == 'admin.settings' or ep == 'admin.email_support' or ep.startswith('admin.') and ('email_template' in ep or 'email-templates' in request.path) or ep == 'admin.pdf_layout' or ep == 'admin.quote_pdf_layout' or ep == 'admin.oidc_debug' %}
{% set admin_security_open = ep == 'admin.api_tokens' or ep.startswith('webhooks.') or ep.startswith('audit_logs.') %}
{% set admin_integrations_open = ep == 'admin.list_integrations_admin' %}
{% set admin_data_open = ep == 'expense_categories.list_categories' or ep == 'per_diem.list_rates' or ep.startswith('time_entry_templates.') or ep.startswith('custom_field_definitions.') or ep.startswith('link_templates.') %}
{% set admin_maintenance_open = ep == 'admin.system_info' or ep == 'admin.backups_management' or ep == 'admin.telemetry_dashboard' %}
{% set pdf_open = ep == 'admin.pdf_layout' or ep == 'admin.quote_pdf_layout' %}
<div class="flex items-center justify-between mb-4">
<h2 class="text-xs font-semibold text-text-muted-light dark:text-text-muted-dark uppercase tracking-wider sidebar-label">{{ _('Navigation') }}</h2>
<button id="sidebarCollapseBtn" class="p-1.5 rounded hover:bg-background-light dark:hover:bg-background-dark" aria-label="{{ _('Toggle sidebar') }}" title="{{ _('Toggle sidebar') }}">
<i id="sidebarCollapseIcon" class="fas fa-angles-left"></i>
</button>
</div>
<ul>
<li>
<a href="{{ url_for('main.dashboard') }}" class="flex items-center p-2 rounded-lg {% if ep == 'main.dashboard' %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-tachometer-alt w-6 text-center"></i>
<span class="ml-3 sidebar-label">{{ _('Dashboard') }}</span>
</a>
</li>
{% if (not settings or settings.ui_allow_calendar) and current_user.ui_show_calendar %}
<li class="mt-2">
<button onclick="toggleDropdown('calendarDropdown')" data-dropdown="calendarDropdown" class="w-full flex items-center p-2 rounded-lg {% if calendar_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-calendar-alt w-6 text-center"></i>
<span class="ml-3 sidebar-label">{{ _('Calendar') }}</span>
<i class="fas fa-chevron-down ml-auto sidebar-label"></i>
</button>
<ul id="calendarDropdown" class="{% if not calendar_open %}hidden {% endif %}mt-2 space-y-2 ml-6">
{% set nav_active_calendar = ep.startswith('calendar.view_calendar') %}
{% set nav_active_calendar_integrations = ep.startswith('calendar.list_integrations') or ep.startswith('calendar.connect') or ep.startswith('calendar.disconnect') %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_calendar %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('calendar.view_calendar') }}">
<i class="fas fa-calendar w-4 mr-2"></i>{{ _('Calendar View') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_calendar_integrations %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('calendar.list_integrations') }}">
<i class="fas fa-plug w-4 mr-2"></i>{{ _('Integrations') }}
</a>
</li>
</ul>
</li>
{% endif %}
<li class="mt-2">
<button onclick="toggleDropdown('workDropdown')" data-dropdown="workDropdown" class="w-full flex items-center p-2 rounded-lg {% if work_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-briefcase w-6 text-center"></i>
<span class="ml-3 sidebar-label">{{ _('Time Tracking') }}</span>
<i class="fas fa-chevron-down ml-auto sidebar-label"></i>
</button>
<ul id="workDropdown" class="{% if not work_open %}hidden {% endif %}mt-2 space-y-2 ml-6">
{% set nav_active_timer = ep.startswith('timer.manual') %}
{% set nav_active_time_entries = ep == 'timer.time_entries_overview' %}
{% set nav_active_projects = ep.startswith('projects.') %}
{% set nav_active_clients = ep.startswith('clients.') %}
{% set nav_active_quotes = ep.startswith('quotes.') %}
{% set nav_active_tasks = ep.startswith('tasks.') %}
{% set nav_active_kanban = ep.startswith('kanban.') %}
{% set nav_active_templates = ep.startswith('time_entry_templates.') %}
{% set nav_active_goals = ep.startswith('weekly_goals.') %}
{% set nav_active_project_templates = ep.startswith('project_templates.') %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_timer %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('timer.manual_entry') }}">
<i class="fas fa-clock w-4 mr-2"></i>{{ _('Log Time') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_time_entries %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('timer.time_entries_overview') }}">
<i class="fas fa-list-alt w-4 mr-2"></i>{{ _('Time Entries') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_projects %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('projects.list_projects') }}">
<i class="fas fa-folder w-4 mr-2"></i>{{ _('Projects') }}
</a>
</li>
{% if settings.ui_allow_project_templates and current_user.ui_show_project_templates %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_project_templates %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('project_templates.list_templates') }}">
<i class="fas fa-layer-group w-4 mr-2"></i>{{ _('Project Templates') }}
</a>
</li>
{% endif %}
{% if settings.ui_allow_gantt_chart and current_user.ui_show_gantt_chart %}
<li>
<a class="block px-2 py-1 rounded {% if ep.startswith('gantt.') %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('gantt.gantt_view') }}">
<i class="fas fa-project-diagram w-4 mr-2"></i>{{ _('Gantt Chart') }}
</a>
</li>
{% endif %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_tasks %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('tasks.list_tasks') }}">
<i class="fas fa-tasks w-4 mr-2"></i>{{ _('Tasks') }}
</a>
</li>
{% if settings.ui_allow_kanban_board and current_user.ui_show_kanban_board %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_kanban %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('kanban.board') }}">
<i class="fas fa-columns w-4 mr-2"></i>{{ _('Kanban Board') }}
</a>
</li>
{% endif %}
{% if settings.ui_allow_weekly_goals and current_user.ui_show_weekly_goals %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_goals %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('weekly_goals.index') }}">
<i class="fas fa-bullseye w-4 mr-2"></i>{{ _('Weekly Goals') }}
</a>
</li>
{% endif %}
</ul>
</li>
<li class="mt-2">
<button onclick="toggleDropdown('crmDropdown')" data-dropdown="crmDropdown" class="w-full flex items-center p-2 rounded-lg {% if crm_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-handshake w-6 text-center"></i>
<span class="ml-3 sidebar-label">{{ _('CRM') }}</span>
<i class="fas fa-chevron-down ml-auto sidebar-label"></i>
</button>
<ul id="crmDropdown" class="{% if not crm_open %}hidden {% endif %}mt-2 space-y-2 ml-6">
{% set nav_active_clients = ep.startswith('clients.') %}
{% set nav_active_quotes = ep.startswith('quotes.') %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_clients %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('clients.list_clients') }}">
<i class="fas fa-users w-4 mr-2"></i>{{ _('Clients') }}
</a>
</li>
{% if settings.ui_allow_quotes and current_user.ui_show_quotes %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_quotes %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('quotes.list_quotes') }}">
<i class="fas fa-file-contract w-4 mr-2"></i>{{ _('Quotes') }}
</a>
</li>
{% endif %}
</ul>
</li>
<li class="mt-2">
<button onclick="toggleDropdown('financeDropdown')" data-dropdown="financeDropdown" class="w-full flex items-center p-2 rounded-lg {% if finance_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-dollar-sign w-6 text-center"></i>
<span class="ml-3 sidebar-label">{{ _('Finance & Expenses') }}</span>
<i class="fas fa-chevron-down ml-auto sidebar-label"></i>
</button>
<ul id="financeDropdown" class="{% if not finance_open %}hidden {% endif %}mt-2 space-y-2 ml-6">
{% set nav_active_invoices = ep.startswith('invoices.') %}
{% set nav_active_invoice_approvals = ep.startswith('invoice_approvals.') %}
{% set nav_active_payment_gateways = ep.startswith('payment_gateways.') %}
{% set nav_active_recurring_invoices = ep.startswith('recurring_invoices.') %}
{% set nav_active_payments = ep.startswith('payments.') %}
{% set nav_active_expenses = ep.startswith('expenses.') %}
{% set nav_active_mileage = ep.startswith('mileage.') %}
{% set nav_active_perdiem = ep.startswith('per_diem.') and not ep.startswith('per_diem.list_rates') %}
{% set nav_active_budget = ep.startswith('budget_alerts.') %}
{% set reports_open = ep.startswith('reports.') or ep.startswith('scheduled_reports.') or ep.startswith('custom_reports.') %}
{% if settings.ui_allow_reports and current_user.ui_show_reports %}
<li>
<button onclick="toggleDropdown('reportsDropdown', event)" data-dropdown="reportsDropdown" class="w-full flex items-center px-2 py-1 rounded {% if reports_open %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-chart-bar w-4 mr-2"></i>
<span class="flex-1 text-left">{{ _('Reports') }}</span>
<i class="fas fa-chevron-down text-xs"></i>
</button>
<ul id="reportsDropdown" class="{% if not reports_open %}hidden {% endif %}mt-2 space-y-2 ml-6">
{% set nav_active_reports = ep.startswith('reports.') and not ep.startswith('scheduled_reports.') and not ep.startswith('custom_reports.') %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if nav_active_reports %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('reports.reports') }}">
<i class="fas fa-chart-line w-4 mr-2"></i>{{ _('All Reports') }}
</a>
</li>
{% if settings.ui_allow_report_builder and current_user.ui_show_report_builder %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'custom_reports.report_builder' or ep == 'custom_reports.view_custom_report' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('custom_reports.report_builder') }}">
<i class="fas fa-magic w-4 mr-2"></i>{{ _('Report Builder') }}
</a>
</li>
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'custom_reports.list_saved_views' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('custom_reports.list_saved_views') }}">
<i class="fas fa-save w-4 mr-2"></i>{{ _('Saved Views') }}
</a>
</li>
{% endif %}
{% if settings.ui_allow_scheduled_reports and current_user.ui_show_scheduled_reports %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep.startswith('scheduled_reports.') %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('scheduled_reports.list_scheduled') }}">
<i class="fas fa-clock w-4 mr-2"></i>{{ _('Scheduled Reports') }}
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_invoices %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('invoices.list_invoices') }}">
<i class="fas fa-file-invoice w-4 mr-2"></i>{{ _('Invoices') }}
</a>
</li>
{% if settings.ui_allow_invoice_approvals and current_user.ui_show_invoice_approvals %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_invoice_approvals %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('invoice_approvals.list_approvals') }}">
<i class="fas fa-check-circle w-4 mr-2"></i>{{ _('Invoice Approvals') }}
</a>
</li>
{% endif %}
{% if settings.ui_allow_payment_gateways and current_user.ui_show_payment_gateways %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_payment_gateways %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('payment_gateways.list_gateways') }}">
<i class="fas fa-credit-card w-4 mr-2"></i>{{ _('Payment Gateways') }}
</a>
</li>
{% endif %}
{% if settings.ui_allow_recurring_invoices and current_user.ui_show_recurring_invoices %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_recurring_invoices %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('recurring_invoices.list_recurring_invoices') }}">
<i class="fas fa-sync-alt w-4 mr-2"></i>{{ _('Recurring Invoices') }}
</a>
</li>
{% endif %}
{% if settings.ui_allow_payments and current_user.ui_show_payments %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_payments %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('payments.list_payments') }}">
<i class="fas fa-credit-card w-4 mr-2"></i>{{ _('Payments') }}
</a>
</li>
{% endif %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_expenses %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('expenses.list_expenses') }}">
<i class="fas fa-receipt w-4 mr-2"></i>{{ _('Expenses') }}
</a>
</li>
{% if settings.ui_allow_mileage and current_user.ui_show_mileage %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_mileage %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('mileage.list_mileage') }}">
<i class="fas fa-car w-4 mr-2"></i>{{ _('Mileage') }}
</a>
</li>
{% endif %}
{% if settings.ui_allow_per_diem and current_user.ui_show_per_diem %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_perdiem %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('per_diem.list_per_diem') }}">
<i class="fas fa-utensils w-4 mr-2"></i>{{ _('Per Diem') }}
</a>
</li>
{% endif %}
{% if settings.ui_allow_budget_alerts and current_user.ui_show_budget_alerts %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_budget %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('budget_alerts.budget_dashboard') }}">
<i class="fas fa-exclamation-triangle w-4 mr-2"></i>{{ _('Budget Alerts') }}
</a>
</li>
{% endif %}
</ul>
</li>
{% if settings.ui_allow_inventory and current_user.ui_show_inventory %}
<li class="mt-2">
<button onclick="toggleDropdown('inventoryDropdown')" data-dropdown="inventoryDropdown" class="w-full flex items-center p-2 rounded-lg {% if inventory_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-boxes w-6 text-center"></i>
<span class="ml-3 sidebar-label">{{ _('Inventory') }}</span>
<i class="fas fa-chevron-down ml-auto sidebar-label"></i>
</button>
<ul id="inventoryDropdown" class="{% if not inventory_open %}hidden {% endif %}mt-2 space-y-2 ml-6">
{% set nav_active_stock_items = ep.startswith('inventory.list_stock_items') or ep.startswith('inventory.view_stock_item') or ep.startswith('inventory.new_stock_item') or ep.startswith('inventory.edit_stock_item') %}
{% set nav_active_warehouses = ep.startswith('inventory.list_warehouses') or ep.startswith('inventory.view_warehouse') or ep.startswith('inventory.new_warehouse') or ep.startswith('inventory.edit_warehouse') %}
{% set nav_active_suppliers = ep.startswith('inventory.list_suppliers') or ep.startswith('inventory.view_supplier') or ep.startswith('inventory.new_supplier') or ep.startswith('inventory.edit_supplier') %}
{% set nav_active_stock_levels = ep.startswith('inventory.stock_levels') %}
{% set nav_active_movements = ep.startswith('inventory.list_movements') or ep.startswith('inventory.new_movement') %}
{% set nav_active_transfers = ep.startswith('inventory.list_transfers') or ep.startswith('inventory.new_transfer') %}
{% set nav_active_adjustments = ep.startswith('inventory.list_adjustments') or ep.startswith('inventory.new_adjustment') %}
{% set nav_active_reservations = ep.startswith('inventory.list_reservations') %}
{% set nav_active_low_stock = ep.startswith('inventory.low_stock_alerts') %}
{% set nav_active_reports = ep.startswith('inventory.reports') %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_stock_items %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('inventory.list_stock_items') }}">
<i class="fas fa-cubes w-4 mr-2"></i>{{ _('Stock Items') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_warehouses %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('inventory.list_warehouses') }}">
<i class="fas fa-warehouse w-4 mr-2"></i>{{ _('Warehouses') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_suppliers %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('inventory.list_suppliers') }}">
<i class="fas fa-truck w-4 mr-2"></i>{{ _('Suppliers') }}
</a>
</li>
{% set nav_active_purchase_orders = ep.startswith('inventory.list_purchase_orders') or ep.startswith('inventory.view_purchase_order') or ep.startswith('inventory.new_purchase_order') or ep.startswith('inventory.receive_purchase_order') %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_purchase_orders %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('inventory.list_purchase_orders') }}">
<i class="fas fa-shopping-cart w-4 mr-2"></i>{{ _('Purchase Orders') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_stock_levels %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('inventory.stock_levels') }}">
<i class="fas fa-list-ul w-4 mr-2"></i>{{ _('Stock Levels') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_movements %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('inventory.list_movements') }}">
<i class="fas fa-exchange-alt w-4 mr-2"></i>{{ _('Stock Movements') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_transfers %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('inventory.list_transfers') }}">
<i class="fas fa-truck w-4 mr-2"></i>{{ _('Transfers') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_adjustments %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('inventory.list_adjustments') }}">
<i class="fas fa-edit w-4 mr-2"></i>{{ _('Adjustments') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_reservations %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('inventory.list_reservations') }}">
<i class="fas fa-bookmark w-4 mr-2"></i>{{ _('Reservations') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_low_stock %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('inventory.low_stock_alerts') }}">
<i class="fas fa-exclamation-triangle w-4 mr-2"></i>{{ _('Low Stock Alerts') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_reports %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('inventory.reports_dashboard') }}">
<i class="fas fa-chart-pie w-4 mr-2"></i>{{ _('Reports') }}
</a>
</li>
</ul>
</li>
{% endif %}
{% if settings.ui_allow_analytics and current_user.ui_show_analytics %}
<li class="mt-2">
<a href="{{ url_for('analytics.analytics_dashboard') }}" class="flex items-center p-2 rounded-lg {% if analytics_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-chart-line w-6 text-center"></i>
<span class="ml-3 sidebar-label">{{ _('Analytics') }}</span>
</a>
</li>
{% endif %}
{% if settings.ui_allow_tools and current_user.ui_show_tools %}
<li class="mt-2">
<button onclick="toggleDropdown('toolsDropdown')" data-dropdown="toolsDropdown" class="w-full flex items-center p-2 rounded-lg {% if tools_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-tools w-6 text-center"></i>
<span class="ml-3 sidebar-label">{{ _('Tools & Data') }}</span>
<i class="fas fa-chevron-down ml-auto sidebar-label"></i>
</button>
<ul id="toolsDropdown" class="{% if not tools_open %}hidden {% endif %}mt-2 space-y-2 ml-6">
{% set nav_active_import_export = ep.startswith('import_export.') %}
{% set nav_active_filters = ep.startswith('saved_filters.') %}
{% set nav_active_integrations = ep.startswith('integrations.') %}
<li>
<a class="block px-2 py-1 rounded {% if nav_active_integrations %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('integrations.list_integrations') }}">
<i class="fas fa-plug w-4 mr-2"></i>{{ _('Integrations') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_import_export %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('import_export.import_export_page') }}">
<i class="fas fa-exchange-alt w-4 mr-2"></i>{{ _('Import / Export') }}
</a>
</li>
<li>
<a class="block px-2 py-1 rounded {% if nav_active_filters %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('saved_filters.list_filters') }}">
<i class="fas fa-filter w-4 mr-2"></i>{{ _('Saved Filters') }}
</a>
</li>
</ul>
</li>
{% endif %}
{% if current_user.is_admin or has_any_permission(['view_users', 'manage_settings', 'view_system_info', 'manage_backups']) %}
<li class="mt-2">
<button onclick="toggleDropdown('adminDropdown')" data-dropdown="adminDropdown" class="w-full flex items-center p-2 rounded-lg {% if admin_open %}bg-background-light dark:bg-background-dark text-primary font-semibold{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-cog w-6 text-center"></i>
<span class="ml-3 sidebar-label">{{ _('Admin') }}</span>
<i class="fas fa-chevron-down ml-auto sidebar-label"></i>
</button>
<ul id="adminDropdown" class="{% if not admin_open %}hidden {% endif %}mt-2 space-y-2 ml-6">
<!-- Dashboard -->
<li>
<a class="block px-2 py-1 rounded {% if ep == 'admin.admin_dashboard' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.admin_dashboard') }}">
<i class="fas fa-tachometer-alt w-4 mr-2"></i>{{ _('Admin Dashboard') }}
</a>
</li>
<!-- User Management Submenu -->
{% if current_user.is_admin or has_permission('view_users') %}
<li>
<button onclick="toggleDropdown('adminUserMgmtDropdown', event)" data-dropdown="adminUserMgmtDropdown" class="w-full flex items-center px-2 py-1 rounded {% if admin_user_mgmt_open %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-users w-4 mr-2"></i>
<span class="flex-1 text-left">{{ _('User Management') }}</span>
<i class="fas fa-chevron-down text-xs"></i>
</button>
<ul id="adminUserMgmtDropdown" class="{% if admin_user_mgmt_open %}{% else %}hidden {% endif %}mt-2 space-y-2 ml-6">
{% if current_user.is_admin or has_permission('view_users') %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.list_users' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.list_users') }}">
<i class="fas fa-users-cog w-4 mr-2"></i>{{ _('Manage Users') }}
</a>
</li>
{% endif %}
{% if current_user.is_admin %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep.startswith('permissions.') %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('permissions.list_roles') }}">
<i class="fas fa-shield-alt w-4 mr-2"></i>{{ _('Roles & Permissions') }}
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
<!-- System Settings Submenu -->
{% if current_user.is_admin or has_permission('manage_settings') or has_permission('manage_oidc') %}
<li>
<button onclick="toggleDropdown('adminSettingsDropdown', event)" data-dropdown="adminSettingsDropdown" class="w-full flex items-center px-2 py-1 rounded {% if admin_settings_open %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-cog w-4 mr-2"></i>
<span class="flex-1 text-left">{{ _('System Settings') }}</span>
<i class="fas fa-chevron-down text-xs"></i>
</button>
<ul id="adminSettingsDropdown" class="{% if admin_settings_open %}{% else %}hidden {% endif %}mt-2 space-y-2 ml-6">
{% if current_user.is_admin or has_permission('manage_settings') %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.settings' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.settings') }}">
<i class="fas fa-sliders-h w-4 mr-2"></i>{{ _('Settings') }}
</a>
</li>
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.email_support' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.email_support') }}">
<i class="fas fa-envelope w-4 mr-2"></i>{{ _('Email Configuration') }}
</a>
</li>
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep.startswith('admin.') and ('email_template' in ep or 'email-templates' in request.path) %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.list_email_templates') }}">
<i class="fas fa-envelope-open-text w-4 mr-2"></i>{{ _('Email Templates') }}
</a>
</li>
<li>
<button onclick="toggleDropdown('pdfDropdown', event)" data-dropdown="pdfDropdown" class="w-full flex items-center px-2 py-1 rounded {% if pdf_open %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-file-pdf w-4 mr-2"></i>
<span class="flex-1 text-left">{{ _('PDF Templates') }}</span>
<i class="fas fa-chevron-down text-xs"></i>
</button>
<ul id="pdfDropdown" class="{% if pdf_open %}{% else %}hidden {% endif %}mt-2 space-y-2 ml-6">
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.pdf_layout' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.pdf_layout') }}">
<i class="fas fa-file-invoice w-4 mr-2"></i>{{ _('Invoice PDF') }}
</a>
</li>
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.quote_pdf_layout' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.quote_pdf_layout') }}">
<i class="fas fa-file-contract w-4 mr-2"></i>{{ _('Quote PDF') }}
</a>
</li>
</ul>
</li>
{% endif %}
{% if current_user.is_admin or has_permission('manage_oidc') %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.oidc_debug' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.oidc_debug') }}">
<i class="fas fa-lock w-4 mr-2"></i>{{ _('OIDC Settings') }}
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
<!-- Security & Access Submenu -->
{% if current_user.is_admin or has_permission('view_audit_logs') %}
<li>
<button onclick="toggleDropdown('adminSecurityDropdown', event)" data-dropdown="adminSecurityDropdown" class="w-full flex items-center px-2 py-1 rounded {% if admin_security_open %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-shield-alt w-4 mr-2"></i>
<span class="flex-1 text-left">{{ _('Security & Access') }}</span>
<i class="fas fa-chevron-down text-xs"></i>
</button>
<ul id="adminSecurityDropdown" class="{% if admin_security_open %}{% else %}hidden {% endif %}mt-2 space-y-2 ml-6">
{% if current_user.is_admin %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.api_tokens' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.api_tokens') }}">
<i class="fas fa-key w-4 mr-2"></i>{{ _('API Tokens') }}
</a>
</li>
{% endif %}
{% if current_user.is_admin %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep.startswith('webhooks.') %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('webhooks.list_webhooks') }}">
<i class="fas fa-plug w-4 mr-2"></i>{{ _('Webhooks') }}
</a>
</li>
{% endif %}
{% if current_user.is_admin or has_permission('view_audit_logs') %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep.startswith('audit_logs.') %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('audit_logs.list_audit_logs') }}">
<i class="fas fa-history w-4 mr-2"></i>{{ _('Audit Logs') }}
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
<!-- Integrations Submenu -->
{% if current_user.is_admin %}
<li>
<button onclick="toggleDropdown('adminIntegrationsDropdown', event)" data-dropdown="adminIntegrationsDropdown" class="w-full flex items-center px-2 py-1 rounded {% if admin_integrations_open %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-plug w-4 mr-2"></i>
<span class="flex-1 text-left">{{ _('Integrations') }}</span>
<i class="fas fa-chevron-down text-xs"></i>
</button>
<ul id="adminIntegrationsDropdown" class="{% if admin_integrations_open %}{% else %}hidden {% endif %}mt-2 space-y-2 ml-6">
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.list_integrations_admin' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.list_integrations_admin') }}">
<i class="fas fa-plug w-4 mr-2"></i>{{ _('Integrations') }}
</a>
</li>
</ul>
</li>
{% endif %}
<!-- Data Management Submenu -->
{% if current_user.is_admin or has_permission('manage_settings') %}
<li>
<button onclick="toggleDropdown('adminDataDropdown', event)" data-dropdown="adminDataDropdown" class="w-full flex items-center px-2 py-1 rounded {% if admin_data_open %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-database w-4 mr-2"></i>
<span class="flex-1 text-left">{{ _('Data Management') }}</span>
<i class="fas fa-chevron-down text-xs"></i>
</button>
<ul id="adminDataDropdown" class="{% if admin_data_open %}{% else %}hidden {% endif %}mt-2 space-y-2 ml-6">
{% if current_user.is_admin %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'expense_categories.list_categories' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('expense_categories.list_categories') }}">
<i class="fas fa-tags w-4 mr-2"></i>{{ _('Expense Categories') }}
</a>
</li>
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'per_diem.list_rates' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('per_diem.list_rates') }}">
<i class="fas fa-list-ul w-4 mr-2"></i>{{ _('Per Diem Rates') }}
</a>
</li>
{% endif %}
{% if current_user.is_admin or has_permission('manage_settings') %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if nav_active_templates %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('time_entry_templates.list_templates') }}">
<i class="fas fa-file-lines w-4 mr-2"></i>{{ _('Time Entry Templates') }}
</a>
</li>
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep.startswith('custom_field_definitions.') %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('custom_field_definitions.list_custom_field_definitions') }}">
<i class="fas fa-tags w-4 mr-2"></i>{{ _('Custom Fields') }}
</a>
</li>
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep.startswith('link_templates.') %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('link_templates.list_link_templates') }}">
<i class="fas fa-link w-4 mr-2"></i>{{ _('Link Templates') }}
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
<!-- System Maintenance Submenu -->
{% if current_user.is_admin or has_permission('view_system_info') or has_permission('manage_backups') %}
<li>
<button onclick="toggleDropdown('adminMaintenanceDropdown', event)" data-dropdown="adminMaintenanceDropdown" class="w-full flex items-center px-2 py-1 rounded {% if admin_maintenance_open %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}">
<i class="fas fa-tools w-4 mr-2"></i>
<span class="flex-1 text-left">{{ _('System Maintenance') }}</span>
<i class="fas fa-chevron-down text-xs"></i>
</button>
<ul id="adminMaintenanceDropdown" class="{% if admin_maintenance_open %}{% else %}hidden {% endif %}mt-2 space-y-2 ml-6">
{% if current_user.is_admin or has_permission('view_system_info') %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.system_info' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.system_info') }}">
<i class="fas fa-info-circle w-4 mr-2"></i>{{ _('System Info') }}
</a>
</li>
{% endif %}
{% if current_user.is_admin or has_permission('manage_backups') %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.backups_management' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.backups_management') }}">
<i class="fas fa-database w-4 mr-2"></i>{{ _('Backups') }}
</a>
</li>
{% endif %}
{% if current_user.is_admin %}
<li>
<a onclick="event.stopPropagation();" class="block px-2 py-1 rounded {% if ep == 'admin.telemetry_dashboard' %}text-primary font-semibold bg-background-light dark:bg-background-dark{% else %}text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark{% endif %}" href="{{ url_for('admin.telemetry_dashboard') }}">
<i class="fas fa-chart-line w-4 mr-2"></i>{{ _('Telemetry') }}
</a>
</li>
{% endif %}
</ul>
</li>
{% endif %}
</ul>
</li>
{% endif %}
</ul>
</nav>
<div class="mt-auto pt-4 border-t border-border-light dark:border-border-dark">
<ul>
<li>
<a href="{{ url_for('main.about') }}" class="flex items-center p-2 rounded-lg text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
<i class="fas fa-circle-info w-6 text-center"></i>
<span class="ml-3 sidebar-label">{{ _('About') }}</span>
</a>
</li>
<li class="mt-2">
<a href="{{ url_for('main.help') }}" class="flex items-center p-2 rounded-lg text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
<i class="fas fa-life-ring w-6 text-center"></i>
<span class="ml-3 sidebar-label">{{ _('Help') }}</span>
</a>
</li>
<li class="mt-2">
<a href="https://buymeacoffee.com/DryTrix" target="_blank" rel="noopener noreferrer" class="flex items-center p-2 rounded-lg bg-gradient-to-r from-amber-500/10 to-orange-500/10 border border-amber-500/20 text-amber-600 dark:text-amber-400 hover:from-amber-500/20 hover:to-orange-500/20 hover:border-amber-500/30 transition-all duration-200 group">
<i class="fas fa-mug-saucer w-6 text-center group-hover:scale-110 transition-transform"></i>
<span class="ml-3 sidebar-label font-medium">{{ _('Buy me a coffee') }}</span>
<span class="ml-auto text-xs opacity-70"></span>
</a>
</li>
</ul>
<!-- App Version -->
<div class="mt-4 pt-3 border-t border-border-light dark:border-border-dark">
<div class="flex items-center justify-center px-2 py-1 text-xs text-text-muted-light dark:text-text-muted-dark">
<i class="fas fa-code-branch w-4 text-center"></i>
<span class="ml-2 sidebar-label">v{{ app_version }}</span>
</div>
</div>
</div>
</aside>
<!-- Main content -->
<div id="mainContent" class="flex-1 flex flex-col min-h-screen transition-all duration-200 ease-in-out lg:ml-64">
<!-- Header -->
<header class="bg-card-light dark:bg-card-dark p-4 border-b border-border-light dark:border-border-dark flex justify-between items-center">
<!-- Mobile menu button -->
<button id="mobileSidebarBtn" class="lg:hidden" aria-label="Open sidebar">
<i class="fas fa-bars"></i>
</button>
<!-- Search + Command button -->
<div class="hidden sm:flex items-center gap-2">
<div class="w-96">
<form class="navbar-search" role="search" action="{{ url_for('main.search') }}" method="get" autocomplete="off">
<label for="search" class="sr-only">{{ _('Search') }}</label>
<input id="search" name="q" type="search" placeholder="{{ _('Search') }}" aria-label="{{ _('Search') }}" data-enhanced-search='{"endpoint": "/api/search", "minChars": 2, "maxResults": 10}' class="w-full bg-background-light dark:bg-gray-700 border border-transparent dark:border-transparent rounded-lg py-2 pl-10 pr-14 text-text-light dark:text-text-dark placeholder-text-muted-light dark:placeholder-text-muted-dark focus:outline-none focus:ring-2 focus:ring-primary" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false">
</form>
</div>
<button type="button" class="text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5" aria-label="Open command palette" onclick="window.openCommandPalette && window.openCommandPalette();">
<i class="fa-solid fa-terminal"></i>
</button>
</div>
<!-- Right side controls -->
<div class="flex items-center space-x-4">
<!-- BuyMeACoffee Button -->
<a href="https://buymeacoffee.com/DryTrix"
target="_blank"
rel="noopener noreferrer"
class="hidden md:flex items-center gap-2 px-3 py-1.5 rounded-lg bg-gradient-to-r from-amber-500/10 to-orange-500/10 border border-amber-500/20 text-amber-600 dark:text-amber-400 hover:from-amber-500/20 hover:to-orange-500/20 hover:border-amber-500/30 transition-all duration-200 text-sm font-medium"
title="{{ _('Support TimeTracker development') }}">
<i class="fas fa-mug-saucer"></i>
<span class="hidden lg:inline">{{ _('Support') }}</span>
</a>
<button id="theme-toggle" type="button" class="text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5" aria-label="{{ _('Toggle dark mode') }}">
<i id="theme-toggle-dark-icon" class="hidden fa-solid fa-moon w-5 h-5"></i>
<i id="theme-toggle-light-icon" class="hidden fa-solid fa-sun w-5 h-5"></i>
</button>
<!-- Language Switcher -->
<div class="relative z-50">
<button onclick="toggleDropdown('langDropdown')" class="flex items-center text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5" aria-haspopup="true" aria-expanded="false" aria-controls="langDropdown" title="{{ _('Change language') }}">
<i class="fas fa-globe"></i>
<span class="ml-2 hidden lg:inline">{{ current_language_label }}</span>
</button>
<ul id="langDropdown" class="hidden absolute right-0 mt-2 w-56 bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark rounded-md shadow-lg max-h-96 overflow-y-auto">
<li class="px-4 py-2 text-xs font-semibold text-text-muted-light dark:text-text-muted-dark uppercase tracking-wider border-b border-border-light dark:border-border-dark">{{ _('Language') }}</li>
{% for code, label in available_languages.items() %}
<li>
<a class="block px-4 py-2 text-sm text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center justify-between" href="{{ url_for('user.set_language_direct', language=code) }}">
<span>{{ label }}</span>
{% if code == current_language_code %}
<i class="fas fa-check text-primary"></i>
{% endif %}
</a>
</li>
{% endfor %}
</ul>
</div>
<!-- User Profile -->
<div class="relative z-50">
<button onclick="toggleDropdown('userDropdown')" class="flex items-center space-x-2" aria-label="{{ _('User menu') }}" aria-haspopup="true" aria-expanded="false" aria-controls="userDropdown">
{% if current_user.is_authenticated %}
{% if current_user.get_avatar_url() %}
<img id="userAvatar" src="{{ current_user.get_avatar_url() }}" alt="{{ current_user.display_name }}" class="w-8 h-8 rounded-full object-cover border-2 border-primary" onerror="this.onerror=null; this.style.display='none'; document.getElementById('userAvatarFallback').style.display='flex';">
<div id="userAvatarFallback" class="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-semibold text-sm" style="display: none;">
{{ current_user.display_name[0:1].upper() }}
</div>
{% else %}
<div class="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-semibold text-sm border-2 border-primary">
{{ current_user.display_name[0:1].upper() }}
</div>
{% endif %}
<span class="hidden md:inline text-text-light dark:text-text-dark font-medium">{{ current_user.display_name }}</span>
{% else %}
<div class="w-8 h-8 rounded-full bg-gray-400 flex items-center justify-center">
<i class="fas fa-user text-white"></i>
</div>
<span class="hidden md:inline text-text-light dark:text-text-dark">{{ _('Guest') }}</span>
{% endif %}
<i class="fas fa-chevron-down text-text-muted-light dark:text-text-muted-dark hidden md:inline" aria-hidden="true"></i>
</button>
<ul id="userDropdown" class="hidden absolute right-0 mt-2 w-48 bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark rounded-md shadow-lg z-50">
<li class="px-4 py-3 border-b border-border-light dark:border-border-dark">
<div class="text-sm font-medium text-text-light dark:text-text-dark">{{ current_user.display_name if current_user.is_authenticated else _('Guest') }}</div>
<div class="text-xs text-text-muted-light dark:text-text-muted-dark">{{ current_user.email if current_user.is_authenticated else '' }}</div>
</li>
<li><a href="{{ url_for('auth.profile') }}" class="flex items-center gap-2 px-4 py-2 text-sm text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700"><i class="fas fa-user w-4"></i> {{ _('My Profile') }}</a></li>
<li><a href="{{ url_for('user.settings') }}" class="flex items-center gap-2 px-4 py-2 text-sm text-text-light dark:text-text-dark hover:bg-gray-100 dark:hover:bg-gray-700"><i class="fas fa-cog w-4"></i> {{ _('My Settings') }}</a></li>
<li class="border-t border-border-light dark:border-border-dark"><a href="{{ url_for('auth.logout') }}" class="flex items-center gap-2 px-4 py-2 text-sm text-rose-600 dark:text-rose-400 hover:bg-gray-100 dark:hover:bg-gray-700"><i class="fas fa-sign-out-alt w-4"></i> {{ _('Logout') }}</a></li>
</ul>
</div>
</div>
</header>
<!-- Flash Messages (hidden, converted to toasts by toast-notifications.js) -->
<div id="flash-messages-container" class="hidden">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert {% if category == 'success' %}alert-success{% elif category == 'error' %}alert-danger{% elif category == 'warning' %}alert-warning{% else %}alert-info{% endif %}" data-toast-message="{{ message }}" data-toast-type="{{ category }}"></div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<!-- Dismissible Support Banner -->
<div id="supportBanner" class="bg-gradient-to-r from-amber-50 to-orange-50 dark:from-amber-900/20 dark:to-orange-900/20 border-b border-amber-200 dark:border-amber-800 px-4 py-3 opacity-0 invisible max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<div class="max-w-7xl mx-auto flex items-center justify-between gap-4">
<div class="flex items-center gap-3 flex-1">
<i class="fas fa-mug-saucer text-amber-600 dark:text-amber-400 text-lg"></i>
<div class="flex-1">
<p class="text-sm font-medium text-amber-900 dark:text-amber-100">
{{ _('Enjoying TimeTracker?') }}
</p>
<p class="text-xs text-amber-700 dark:text-amber-300">
{{ _('Support continued development with a coffee') }} ☕
</p>
</div>
</div>
<div class="flex items-center gap-2">
<a href="https://buymeacoffee.com/DryTrix"
target="_blank"
rel="noopener noreferrer"
class="px-3 py-1.5 bg-amber-600 hover:bg-amber-700 text-white text-sm font-medium rounded-lg transition-colors">
{{ _('Support') }}
</a>
<button onclick="dismissSupportBanner()"
class="p-1.5 text-amber-600 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-900/30 rounded transition-colors"
aria-label="{{ _('Dismiss') }}">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
<!-- Main page content -->
<main id="mainContentAnchor" class="flex-1 p-6">
{% block content %}{% endblock %}
</main>
</div>
<!-- Mobile Bottom Navigation -->
<nav class="lg:hidden fixed bottom-0 inset-x-0 bg-card-light dark:bg-card-dark border-t border-border-light dark:border-border-dark flex justify-around">
<a href="{{ url_for('main.dashboard') }}" class="flex flex-col items-center p-2 text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
<i class="fas fa-tachometer-alt"></i>
<span class="text-xs">{{ _('Dashboard') }}</span>
</a>
<a href="{{ url_for('projects.list_projects') }}" class="flex flex-col items-center p-2 text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
<i class="fas fa-briefcase"></i>
<span class="text-xs">{{ _('Projects') }}</span>
</a>
<a href="{{ url_for('timer.manual_entry') }}" class="flex flex-col items-center p-2 text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
<i class="fas fa-plus-circle text-2xl"></i>
</a>
<a href="{{ url_for('reports.reports') }}" class="flex flex-col items-center p-2 text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
<i class="fas fa-chart-line"></i>
<span class="text-xs">{{ _('Reports') }}</span>
</a>
<a href="{{ url_for('auth.profile') }}" class="flex flex-col items-center p-2 text-text-light dark:text-text-dark hover:bg-background-light dark:hover:bg-background-dark">
<i class="fas fa-user-circle"></i>
<span class="text-xs">{{ _('Profile') }}</span>
</a>
</nav>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/js/all.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='toast-notifications.css') }}">
<!-- Enhanced UI scripts -->
<script src="{{ url_for('static', filename='enhanced-search.js') }}"></script>
<script src="{{ url_for('static', filename='form-validation.js') }}"></script>
<script src="{{ url_for('static', filename='toast-notifications.js') }}"></script>
<script src="{{ url_for('static', filename='enhanced-tables.js') }}"></script>
<script src="{{ url_for('static', filename='interactions.js') }}"></script>
<script src="{{ url_for('static', filename='offline-sync.js') }}"></script>
<script src="{{ url_for('static', filename='mentions.js') }}"></script>
<!-- Old command palette and keyboard navigation (restored) -->
<script src="{{ url_for('static', filename='commands.js') }}?v=2.0"></script>
<script>
// Minimal global shortcuts: Ctrl+/ (focus search), Ctrl+Shift+L (toggle theme), 't' (toggle timer)
// Note: Ctrl+K is handled by keyboard-shortcuts-advanced.js for command palette
(function(){
function isTyping(e){
const t = e.target;
const tag = (t && t.tagName || '').toLowerCase();
// Check standard inputs
if (tag === 'input' || tag === 'textarea' || tag === 'select' || (t && t.isContentEditable)) {
return true;
}
// Check for rich text editors (Toast UI Editor, etc.)
const editorSelectors = ['.toastui-editor', '.toastui-editor-contents', '.ProseMirror', '.CodeMirror', '.ql-editor', '.tox-edit-area', '.note-editable', '[contenteditable="true"]', '.toastui-editor-ww-container', '.toastui-editor-md-container'];
for (let i = 0; i < editorSelectors.length; i++) {
if (t.closest && t.closest(editorSelectors[i])) return true;
}
return false;
}
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + / -> focus search (removed Ctrl+K to avoid conflict with command palette)
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.key === '/') {
e.preventDefault();
const search = document.getElementById('search');
if (search) {
search.focus();
if (search.select) search.select();
}
}
});
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + Shift + L -> toggle theme
if ((e.ctrlKey || e.metaKey) && e.shiftKey && (e.key === 'l' || e.key === 'L')) {
e.preventDefault();
document.getElementById('theme-toggle')?.click();
}
});
async function toggleTimer(){
try {
const statusRes = await fetch("{{ url_for('timer.timer_status') }}", { credentials: 'same-origin' });
const status = await statusRes.json();
if (status && status.active) {
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
const stopRes = await fetch("{{ url_for('timer.stop_timer') }}", { method: 'POST', headers: { 'X-CSRF-Token': token }, credentials: 'same-origin' });
if (stopRes.ok) { window.toastManager && window.toastManager.info('Timer stopped'); }
window.location.href = "{{ url_for('main.dashboard') }}";
} else {
window.location.href = "{{ url_for('timer.manual_entry') }}";
}
} catch (_) { window.location.href = "{{ url_for('timer.manual_entry') }}"; }
}
document.addEventListener('keydown', (e) => {
if (isTyping(e)) return;
if (!e.ctrlKey && !e.metaKey && !e.altKey && (e.key === 't' || e.key === 'T')) {
e.preventDefault();
toggleTimer();
}
});
})();
</script>
<script>
// Sidebar collapse logic with persisted state
(function(){
const appShell = document.getElementById('appShell');
const sidebar = document.getElementById('sidebar');
const main = document.getElementById('mainContent');
const collapseBtn = document.getElementById('sidebarCollapseBtn');
const mobileBtn = document.getElementById('mobileSidebarBtn');
let flyout;
function applyCollapsed(isCollapsed){
// Toggle a class on the shell to let Tailwind width utilities react
const icon = document.getElementById('sidebarCollapseIcon');
if (isCollapsed){
appShell.classList.add('sidebar-collapsed');
sidebar.classList.add('w-16');
sidebar.classList.remove('w-64');
// Adjust main content margin for collapsed sidebar
main.classList.remove('lg:ml-64');
main.classList.add('lg:ml-16');
icon && icon.classList.remove('fa-angles-left');
icon && icon.classList.add('fa-angles-right');
} else {
appShell.classList.remove('sidebar-collapsed');
sidebar.classList.remove('w-16');
sidebar.classList.add('w-64');
// Adjust main content margin for expanded sidebar
main.classList.remove('lg:ml-16');
main.classList.add('lg:ml-64');
icon && icon.classList.remove('fa-angles-right');
icon && icon.classList.add('fa-angles-left');
}
}
// Initialize from storage
try {
const saved = localStorage.getItem('sidebar-collapsed');
applyCollapsed(saved === 'true');
} catch(_) {}
// Desktop toggle
collapseBtn && collapseBtn.addEventListener('click', function(){
const nowCollapsed = !appShell.classList.contains('sidebar-collapsed');
applyCollapsed(nowCollapsed);
try { localStorage.setItem('sidebar-collapsed', String(nowCollapsed)); } catch(_) {}
});
// Mobile toggle simply shows/hides sidebar via utility classes
mobileBtn && mobileBtn.addEventListener('click', function(){
// On small screens we can temporarily show sidebar as overlay
const showing = sidebar.classList.contains('hidden');
sidebar.classList.toggle('hidden', !showing);
// Sidebar is already fixed, just need to adjust z-index for overlay
if (showing) {
sidebar.style.zIndex = '50';
} else {
sidebar.style.zIndex = '10';
}
});
// Flyout submenu when collapsed
function hideFlyout(){
if (flyout){ flyout.classList.add('hidden'); flyout.innerHTML=''; }
}
function ensureFlyout(){
if (!flyout){
flyout = document.createElement('div');
flyout.id = 'sidebarFlyout';
flyout.className = 'hidden fixed z-50 bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark rounded-md shadow-lg p-2';
document.body.appendChild(flyout);
document.addEventListener('click', (e) => {
if (!flyout || flyout.classList.contains('hidden')) return;
const inside = flyout.contains(e.target);
const trigger = e.target.closest('[data-dropdown]');
if (!inside && !trigger) hideFlyout();
});
window.addEventListener('resize', hideFlyout);
window.addEventListener('scroll', hideFlyout, true);
}
}
function showFlyout(triggerBtn, listId){
ensureFlyout();
const list = document.getElementById(listId);
if (!list) return;
flyout.innerHTML = '';
const ul = document.createElement('ul');
ul.className = 'min-w-[12rem] space-y-1';
list.querySelectorAll('a').forEach((a) => {
const li = document.createElement('li');
const link = a.cloneNode(true);
link.classList.add('block','px-3','py-2','rounded','hover:bg-background-light','dark:hover:bg-background-dark');
li.appendChild(link);
ul.appendChild(li);
});
flyout.appendChild(ul);
const rect = triggerBtn.getBoundingClientRect();
const sbRect = sidebar.getBoundingClientRect();
const left = sbRect.right + 8 + window.scrollX;
const top = rect.top + window.scrollY;
flyout.style.left = left + 'px';
flyout.style.top = top + 'px';
flyout.classList.remove('hidden');
}
sidebar.addEventListener('click', function(e){
const btn = e.target.closest('[data-dropdown]');
if (!btn) return;
const listId = btn.getAttribute('data-dropdown');
const isCollapsed = appShell.classList.contains('sidebar-collapsed') || document.documentElement.classList.contains('sidebar-collapsed');
if (isCollapsed){
e.preventDefault();
e.stopPropagation();
showFlyout(btn, listId);
}
});
})();
</script>
<!-- Command Palette Modal (restored, Tailwind-styled, no Bootstrap required) -->
<!-- Keyboard Shortcuts Help Modal -->
{% include 'components/keyboard_shortcuts_help.html' %}
<div id="commandPaletteModal" class="fixed inset-0 z-50 hidden">
<div class="absolute inset-0 bg-black/50" onclick="document.getElementById('commandPaletteModal').classList.add('hidden')"></div>
<div class="relative max-w-2xl mx-auto mt-24 bg-card-light dark:bg-card-dark text-text-light dark:text-text-dark rounded-lg shadow-xl">
<div class="p-3 sm:p-4 border-b border-border-light dark:border-border-dark flex items-center justify-between">
<div id="commandPaletteHelp" class="text-xs text-text-muted-light dark:text-text-muted-dark">
Press <kbd class="px-1 py-0.5 bg-background-light dark:bg-background-dark border border-border-light dark:border-border-dark rounded text-xs">Esc</kbd> to close, <kbd class="px-1 py-0.5 bg-background-light dark:bg-background-dark border border-border-light dark:border-border-dark rounded text-xs">Shift+?</kbd> for all shortcuts
</div>
<button id="commandPaletteClose" class="px-2 py-1 text-sm hover:bg-background-light dark:hover:bg-background-dark rounded">Close</button>
</div>
<div class="p-3 sm:p-4">
<div class="flex items-center gap-2 mb-3">
<i class="fas fa-terminal text-text-muted-light dark:text-text-muted-dark"></i>
<input id="commandPaletteInput" type="text" class="w-full bg-background-light dark:bg-gray-700 border border-border-light dark:border-border-dark rounded-lg py-2 px-3 focus:outline-none focus:ring-2 focus:ring-primary" placeholder="{{ _('Type a command or search...') }}" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false">
</div>
<div id="commandPaletteList" class="flex flex-col max-h-96 overflow-y-auto divide-y divide-border-light dark:divide-border-dark"></div>
</div>
</div>
</div>
<script>
// Command Palette close handlers
(function() {
const modal = document.getElementById('commandPaletteModal');
const closeBtn = document.getElementById('commandPaletteClose');
const input = document.getElementById('commandPaletteInput');
if (closeBtn) {
closeBtn.addEventListener('click', () => modal.classList.add('hidden'));
}
// Close on Escape key
if (input) {
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
e.preventDefault();
modal.classList.add('hidden');
}
});
}
})();
</script>
<script>
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
// Change the icons inside the button based on previous settings
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
var themeToggleBtn = document.getElementById('theme-toggle');
themeToggleBtn.addEventListener('click', function() {
// toggle icons inside button
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
var newTheme;
// if set via local storage previously
if (localStorage.getItem('color-theme')) {
if (localStorage.getItem('color-theme') === 'light') {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
newTheme = 'dark';
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
newTheme = 'light';
}
// if NOT set via local storage previously
} else {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
newTheme = 'light';
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
newTheme = 'dark';
}
}
// Save to database if user is logged in
{% if current_user.is_authenticated %}
fetch('/api/theme', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ theme: newTheme })
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.error || 'Failed to save theme preference');
});
}
return response.json();
})
.then(data => {
if (data.success) {
console.log('Theme preference saved:', data.theme);
}
})
.catch(err => {
console.error('Failed to save theme preference:', err);
// Don't show error toast for theme changes - it's not critical
// The theme change already worked locally, just didn't persist
});
{% endif %}
});
function toggleDropdown(id, event) {
if (event) {
event.stopPropagation(); // Prevent event bubbling for nested dropdowns
}
const dropdown = document.getElementById(id);
const isCurrentlyHidden = dropdown.classList.contains('hidden');
// Define nested dropdowns and their parent dropdowns
// All admin submenus should keep the parent adminDropdown open
const nestedDropdowns = {
'pdfDropdown': 'adminSettingsDropdown',
'adminUserMgmtDropdown': 'adminDropdown',
'adminSettingsDropdown': 'adminDropdown',
'adminSecurityDropdown': 'adminDropdown',
'adminIntegrationsDropdown': 'adminDropdown',
'adminDataDropdown': 'adminDropdown',
'adminMaintenanceDropdown': 'adminDropdown',
'reportsDropdown': 'financeDropdown' // Reports is nested under Finance
};
// If this is a nested dropdown, don't close its parent
const parentDropdown = nestedDropdowns[id];
// Special handling for reportsDropdown: keep it open if already open and on a reports page
if (id === 'reportsDropdown' && !isCurrentlyHidden) {
// Check if we're on a reports page by checking the current URL
const currentPath = window.location.pathname;
const isReportsPage = currentPath.includes('/reports/') || currentPath.includes('/scheduled') || currentPath.includes('/builder');
if (isReportsPage) {
// Don't close if we're on a reports page
return;
}
}
// Close all other top-level dropdowns in the sidebar (accordion behavior)
// Note: Nested dropdowns (like pdfDropdown) are not in this list, so they work independently
const allSidebarDropdowns = ['workDropdown', 'financeDropdown', 'adminDropdown', 'crmDropdown', 'toolsDropdown'];
allSidebarDropdowns.forEach(dropdownId => {
// Don't close the parent if this is a nested dropdown
if (dropdownId === parentDropdown) {
return;
}
// Don't close adminDropdown if clicking on any of its submenus
if (dropdownId === 'adminDropdown' && parentDropdown === 'adminDropdown') {
return;
}
// Don't close financeDropdown if clicking on reportsDropdown
if (dropdownId === 'financeDropdown' && parentDropdown === 'financeDropdown') {
return;
}
if (dropdownId !== id) {
const otherDropdown = document.getElementById(dropdownId);
if (otherDropdown) {
otherDropdown.classList.add('hidden');
// Rotate chevron back for closed dropdowns
const otherBtn = document.querySelector(`[data-dropdown="${dropdownId}"]`);
if (otherBtn) {
const otherChevron = otherBtn.querySelector('.fa-chevron-down');
if (otherChevron) {
otherChevron.style.transform = 'rotate(0deg)';
}
}
}
}
});
// Toggle the clicked dropdown
if (isCurrentlyHidden) {
dropdown.classList.remove('hidden');
// If opening a submenu of adminDropdown, ensure adminDropdown is also open
if (parentDropdown === 'adminDropdown') {
const adminDropdown = document.getElementById('adminDropdown');
if (adminDropdown) {
adminDropdown.classList.remove('hidden');
}
}
// If opening reportsDropdown, ensure financeDropdown is also open
if (parentDropdown === 'financeDropdown') {
const financeDropdown = document.getElementById('financeDropdown');
if (financeDropdown) {
financeDropdown.classList.remove('hidden');
}
}
} else {
dropdown.classList.add('hidden');
}
// Rotate chevron icon for visual feedback
const btn = document.querySelector(`[data-dropdown="${id}"]`);
if (btn) {
const chevron = btn.querySelector('.fa-chevron-down');
if (chevron) {
chevron.style.transition = 'transform 0.2s ease';
chevron.style.transform = isCurrentlyHidden ? 'rotate(180deg)' : 'rotate(0deg)';
}
}
}
</script>
<!-- Enhanced UI Scripts -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="{{ url_for('static', filename='charts.js') }}"></script>
<script src="{{ url_for('static', filename='enhanced-ui.js') }}"></script>
<script src="{{ url_for('static', filename='onboarding.js') }}"></script>
<script src="{{ url_for('static', filename='onboarding-enhanced.js') }}"></script>
<script src="{{ url_for('static', filename='error-handling-enhanced.js') }}"></script>
<script src="{{ url_for('static', filename='ui-enhancements.js') }}"></script>
<!-- Advanced Features -->
<script src="{{ url_for('static', filename='keyboard-shortcuts-advanced.js') }}?v=2.2"></script>
<script src="{{ url_for('static', filename='keyboard-shortcuts-enhanced.js') }}?v=2.2"></script>
<script src="{{ url_for('static', filename='quick-actions.js') }}"></script>
<script src="{{ url_for('static', filename='smart-notifications.js') }}"></script>
<script src="{{ url_for('static', filename='dashboard-widgets.js') }}"></script>
<script src="{{ url_for('static', filename='dashboard-enhancements.js') }}"></script>
<script src="{{ url_for('static', filename='activity-feed.js') }}"></script>
<!-- PWA Registration -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/static/service-worker.js')
.then(registration => {
console.log('ServiceWorker registered:', registration);
// Check for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New version available
if (window.toastManager) {
const toast = window.toastManager.info('New version available!', 0);
const btn = document.createElement('button');
btn.textContent = 'Reload';
btn.className = 'ml-2 px-3 py-1 bg-primary text-white rounded hover:bg-primary/90';
btn.onclick = () => window.location.reload();
toast.appendChild(btn);
}
}
});
});
})
.catch(err => console.log('ServiceWorker registration failed:', err));
});
}
// Install prompt
let deferredPrompt;
// Check if user has previously dismissed the install prompt or app is already installed
const installPromptDismissed = localStorage.getItem('pwa-install-dismissed');
const isAppInstalled = window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true ||
document.referrer.includes('android-app://');
// Only register the event listener if prompt hasn't been dismissed and app isn't installed
if (installPromptDismissed !== 'true' && !isAppInstalled) {
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
// Double-check dismissal status when event fires
const stillDismissed = localStorage.getItem('pwa-install-dismissed');
if (stillDismissed === 'true') {
return; // Don't show the prompt if it was dismissed
}
deferredPrompt = e;
// Show install button in UI
if (window.toastManager) {
// Create a non-dismissible toast so we can add custom buttons
const toastId = window.toastManager.show({
message: 'Install TimeTracker as an app!',
type: 'info',
duration: 0,
dismissible: false
});
// Get the toast element after it's added to DOM
requestAnimationFrame(() => {
const toastElement = document.querySelector(`[data-toast-id="${toastId}"]`);
if (toastElement) {
const btn = document.createElement('button');
btn.textContent = 'Install';
btn.className = 'ml-2 px-3 py-1 bg-primary text-white rounded hover:bg-primary/90';
btn.onclick = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
window.toastManager.success('App installed successfully!');
}
// Always mark as dismissed after user interaction
localStorage.setItem('pwa-install-dismissed', 'true');
deferredPrompt = null;
window.toastManager.dismiss(toastId);
};
// Add a dismiss button
const dismissBtn = document.createElement('button');
dismissBtn.textContent = '×';
dismissBtn.className = 'ml-2 px-2 py-1 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded';
dismissBtn.title = 'Dismiss permanently';
dismissBtn.onclick = () => {
localStorage.setItem('pwa-install-dismissed', 'true');
deferredPrompt = null;
window.toastManager.dismiss(toastId);
};
// Insert buttons in the toast content area
const content = toastElement.querySelector('.toast-content');
if (content) {
content.appendChild(btn);
content.appendChild(dismissBtn);
}
}
});
}
});
// Also listen for app installation to mark as dismissed
window.addEventListener('appinstalled', () => {
localStorage.setItem('pwa-install-dismissed', 'true');
deferredPrompt = null;
});
}
</script>
<script>
// Global helpers for bulk menus and confirmation using custom modal (no native confirm)
window.showConfirm = function(message, opts){
try {
const options = Object.assign({
title: '',
confirmText: 'Confirm',
cancelText: 'Cancel',
variant: 'primary' // 'primary' | 'danger' | 'warning'
}, opts || {});
return new Promise((resolve) => {
// Build overlay
const overlay = document.createElement('div');
overlay.className = 'fixed inset-0 z-[2000] flex items-center justify-center';
overlay.innerHTML = `
<div class="absolute inset-0 bg-black/50" data-close></div>
<div class="relative bg-card-light dark:bg-card-dark text-text-light dark:text-text-dark rounded-lg shadow-xl w-full max-w-md mx-4">
<div class="p-6">
<div class="flex items-start gap-3">
<div class="w-12 h-12 rounded-full ${options.variant==='danger' ? 'bg-rose-100 dark:bg-rose-900/30' : (options.variant==='warning' ? 'bg-amber-100 dark:bg-amber-900/30' : 'bg-sky-100 dark:bg-sky-900/30')} flex items-center justify-center flex-shrink-0">
<i class="fas fa-exclamation-triangle ${options.variant==='danger' ? 'text-rose-600 dark:text-rose-400' : (options.variant==='warning' ? 'text-amber-600 dark:text-amber-400' : 'text-sky-600 dark:text-sky-400')}"></i>
</div>
<div class="flex-1">
${options.title ? `<h3 class="text-lg font-semibold mb-1">${options.title}</h3>` : ''}
<p class="text-sm">${message || ''}</p>
</div>
</div>
<div class="mt-6 flex justify-end gap-3">
<button type="button" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-lg" data-cancel>${options.cancelText}</button>
<button type="button" class="px-4 py-2 ${options.variant==='danger' ? 'bg-rose-600 hover:bg-rose-700' : (options.variant==='warning' ? 'bg-amber-500 hover:bg-amber-600' : 'bg-primary hover:bg-primary/90')} text-white rounded-lg" data-confirm>${options.confirmText}</button>
</div>
</div>
</div>`;
function cleanup(result){
try { document.body.removeChild(overlay); } catch(_) {}
resolve(result);
}
overlay.addEventListener('click', (e) => {
if (e.target.hasAttribute('data-close')) cleanup(false);
if (e.target.hasAttribute('data-cancel')) cleanup(false);
if (e.target.hasAttribute('data-confirm')) cleanup(true);
});
document.addEventListener('keydown', function onKey(e){
if (e.key === 'Escape'){ cleanup(false); document.removeEventListener('keydown', onKey); }
if (e.key === 'Enter'){ cleanup(true); document.removeEventListener('keydown', onKey); }
});
document.body.appendChild(overlay);
// Focus confirm button
setTimeout(() => { try { overlay.querySelector('[data-confirm]').focus(); } catch(_) {} }, 0);
});
} catch(_) {
// Absolute fallback if anything goes wrong
try { return Promise.resolve(window.confirm(message)); } catch(__) { return Promise.resolve(false); }
}
};
window.showAlert = window.showAlert || function(message){
try { window.alert(message); } catch(_) {}
};
function closeAllMenus(){
try { document.querySelectorAll('.bulk-menu').forEach(m => m.classList.add('hidden')); } catch(_) {}
}
function openMenu(triggerEl, menuId){
try{
const menu = document.getElementById(menuId);
if (!menu) return;
const willOpen = menu.classList.contains('hidden');
closeAllMenus();
if (!willOpen) return;
// Reset positioning
menu.style.top = '';
menu.style.bottom = '';
menu.style.maxHeight = '16rem';
// Temporarily show off-screen to measure accurately
const originalDisplay = menu.style.display;
menu.style.visibility = 'hidden';
menu.style.display = 'block';
const rect = triggerEl.getBoundingClientRect();
const menuHeight = menu.scrollHeight || menu.offsetHeight || 200;
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
// Restore visibility before final placement
menu.style.display = originalDisplay || '';
menu.style.visibility = '';
// Flip to dropup if not enough space below
const needsDropup = spaceBelow < Math.min(menuHeight, 256) + 16 && spaceAbove > spaceBelow;
if (needsDropup) { menu.style.bottom = 'calc(100% + 8px)'; } else { menu.style.top = 'calc(100% + 8px)'; }
menu.classList.remove('hidden');
} catch(_) {}
}
// Click outside to close any bulk menus
document.addEventListener('click', function(e){
const trigger = e.target.closest('#bulkActionsBtn');
const insideAnyMenu = e.target.closest('.bulk-menu');
if (!trigger && !insideAnyMenu){ closeAllMenus(); }
});
// Close on Escape
document.addEventListener('keydown', function(e){ if (e.key === 'Escape') closeAllMenus(); });
// Support Banner Logic
function dismissSupportBanner() {
const banner = document.getElementById('supportBanner');
if (banner) {
banner.classList.add('opacity-0', 'invisible', 'max-h-0', 'overflow-hidden');
banner.classList.remove('opacity-100', 'visible', 'max-h-[100px]');
// Store dismissal timestamp (show again after 7 days)
try {
localStorage.setItem('supportBannerDismissed', Date.now().toString());
} catch(e) {}
}
}
function shouldShowSupportBanner() {
try {
const dismissed = localStorage.getItem('supportBannerDismissed');
if (!dismissed) return true;
const dismissedTime = parseInt(dismissed);
const sevenDays = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
return (Date.now() - dismissedTime) > sevenDays;
} catch(e) {
return true; // Show by default if localStorage fails
}
}
// Show support banner if conditions are met
// Check immediately to reserve space and prevent layout shift
(function() {
const banner = document.getElementById('supportBanner');
if (!banner) return;
if (shouldShowSupportBanner()) {
// Reserve space immediately by removing height constraints
// This prevents layout shift when banner becomes visible
banner.classList.remove('max-h-0', 'overflow-hidden');
// Space is now reserved, content just invisible
// Show banner with a slight delay for better UX, but space is already reserved
setTimeout(() => {
banner.classList.remove('opacity-0', 'invisible');
banner.classList.add('opacity-100', 'visible');
}, 2000); // Show after 2 seconds
} else {
// Keep it hidden and collapsed - no space reserved
banner.classList.add('max-h-0', 'overflow-hidden');
}
})();
</script>
<script src="{{ url_for('static', filename='data-tables-enhanced.js') }}"></script>
{% block scripts_extra %}{% endblock %}
</body>
</html>