Files
TimeTracker/app/templates/base.html
T
Dries Peeters a4797b25ac fix: Fix email template editor initialization and JavaScript errors
- Fix script block name from extra_js to scripts_extra to match base.html
- Replace inline onclick handlers with event listeners to fix scope issues
- Fix ReferenceError for toggleViewMode and insertVariable functions
- Improve editor initialization flow with proper script loading detection
- Add error handling and fallback to textarea if Toast UI Editor fails to load
- Add debug logging for troubleshooting initialization issues
- Ensure default templates are editable (no restrictions in backend)
- Add email templates link to admin menu in base.html
- Remove ENV file configuration details from email support page

The editor now properly initializes and all interactive features work correctly.
2025-11-14 13:40:00 +01:00

1219 lines
82 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">
<!--
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') }}">
<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%; }
</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") }}'
}
};
</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/drytrix-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('clients.') or ep.startswith('tasks.') or ep.startswith('timer.') or ep.startswith('kanban.') or ep.startswith('weekly_goals.') %}
{% 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('budget_alerts.') or ep.startswith('mileage.') or (ep.startswith('per_diem.') and not ep.startswith('per_diem.list_rates')) %}
{% set analytics_open = ep.startswith('analytics.') %}
{% set tools_open = ep.startswith('import_export.') or ep.startswith('saved_filters.') %}
{% 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.') %}
<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>
<li class="mt-2">
<a href="{{ url_for('calendar.view_calendar') }}" class="flex items-center p-2 rounded-lg {% if ep.startswith('calendar.') %}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>
</a>
</li>
<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.') %}
{% set nav_active_projects = ep.startswith('projects.') %}
{% set nav_active_clients = ep.startswith('clients.') %}
{% 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.') %}
<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_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>
<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>
<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>
<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>
<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>
</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_reports = ep.startswith('reports.') %}
{% set nav_active_invoices = ep.startswith('invoices.') %}
{% 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.') %}
<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('reports.reports') }}">
<i class="fas fa-chart-bar w-4 mr-2"></i>{{ _('Reports') }}
</a>
</li>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</ul>
</li>
<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>
<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.') %}
<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>
{% 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 -->
{% if current_user.is_admin or has_permission('view_users') %}
<li>
<a 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>{{ _('Users') }}
</a>
</li>
{% endif %}
{% if current_user.is_admin %}
<li>
<a 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 %}
<!-- Security & Monitoring -->
{% if current_user.is_admin or has_permission('view_audit_logs') %}
<li>
<a 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 %}
{% if current_user.is_admin %}
<li>
<a 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 %}
<!-- System Configuration -->
{% if current_user.is_admin or has_permission('manage_settings') %}
<li>
<a 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>{{ _('System Settings') }}
</a>
</li>
<li>
<a 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 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>
<a 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-pdf w-4 mr-2"></i>{{ _('PDF Layout') }}
</a>
</li>
{% endif %}
<!-- Data Management -->
{% if current_user.is_admin %}
<li>
<a 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 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 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>
{% endif %}
<!-- System Info & Maintenance -->
{% if current_user.is_admin or has_permission('view_system_info') %}
<li>
<a 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 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 or has_permission('manage_oidc') %}
<li>
<a 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 %}
</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="hidden 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">
<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>
<!-- 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/preferences', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ theme: newTheme })
}).catch(err => console.error('Failed to save theme preference:', err));
{% endif %}
});
function toggleDropdown(id) {
const dropdown = document.getElementById(id);
const isCurrentlyHidden = dropdown.classList.contains('hidden');
// Close all other dropdowns in the sidebar (accordion behavior)
const allSidebarDropdowns = ['workDropdown', 'financeDropdown', 'adminDropdown'];
allSidebarDropdowns.forEach(dropdownId => {
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');
} 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>
<!-- 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;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
// Check if user has previously dismissed the install prompt
const installPromptDismissed = localStorage.getItem('pwa-install-dismissed');
if (installPromptDismissed === 'true') {
return; // Don't show the prompt if it was dismissed before
}
// Show install button in UI
if (window.toastManager) {
const toast = window.toastManager.info('Install TimeTracker as an app!', 0);
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 () => {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
window.toastManager.success('App installed successfully!');
localStorage.setItem('pwa-install-dismissed', 'true');
} else {
// User declined, remember their choice
localStorage.setItem('pwa-install-dismissed', 'true');
}
deferredPrompt = null;
toast.remove();
};
// Add a dismiss button
const dismissBtn = document.createElement('button');
dismissBtn.textContent = '×';
dismissBtn.className = 'ml-2 px-2 py-1 text-white hover:bg-white/20 rounded';
dismissBtn.title = 'Dismiss permanently';
dismissBtn.onclick = () => {
localStorage.setItem('pwa-install-dismissed', 'true');
toast.remove();
};
toast.appendChild(btn);
toast.appendChild(dismissBtn);
}
});
</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('hidden');
// 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
document.addEventListener('DOMContentLoaded', function() {
const banner = document.getElementById('supportBanner');
if (banner && shouldShowSupportBanner()) {
// Show banner with a slight delay for better UX
setTimeout(() => {
banner.classList.remove('hidden');
}, 2000); // Show after 2 seconds
}
});
</script>
<script src="{{ url_for('static', filename='data-tables-enhanced.js') }}"></script>
{% block scripts_extra %}{% endblock %}
</body>
</html>