Files
TimeTracker/app/templates/user/settings.html
T
Dries Peeters a42a84ecdc Fix layout inconsistencies and remove footer from all pages
- Replace bg-white dark:bg-gray-800 with bg-card-light dark:bg-card-dark across all templates for consistent theming
- Fix layout rendering issues on multiple pages:
  * Calendar - Calendar View
  * Time Tracking - Projects, Weekly Goals, Time Entry Templates
  * Tools & Data - Import/Export, Saved Filters
  * Admin - Security Access (API Tokens), System Maintenance (Backups)
  * User pages (Profile, Settings)
  * Invoice, Quote, and Expense pages
  * All component widgets and help pages
- Remove footer section from base.html to clean up page layout
- Remove unused footer script for setting current year
- Ensure consistent card styling across entire application for proper dark mode support

Affects 33 template files total.
2025-12-30 20:41:58 +01:00

418 lines
24 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ _('Settings') }}{% endblock %}
{% block content %}
<div class="container mx-auto px-4 py-8 max-w-4xl">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">{{ _('Settings') }}</h1>
<p class="text-gray-600 dark:text-gray-400 mt-2">{{ _('Manage your account settings and preferences') }}</p>
</div>
<form method="POST" class="space-y-8">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<!-- Profile Information -->
<div class="bg-card-light dark:bg-card-dark rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
<i class="fas fa-user mr-2"></i>{{ _('Profile Information') }}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ _('Username') }}
</label>
<input type="text" value="{{ user.username }}" disabled
class="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-100 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-300 cursor-not-allowed">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ _('Username cannot be changed') }}</p>
</div>
<div>
<label for="full_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ _('Full Name') }}
</label>
<input type="text" id="full_name" name="full_name" value="{{ user.full_name or '' }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
</div>
<div class="md:col-span-2">
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ _('Email Address') }}
</label>
<input type="email" id="email" name="email" value="{{ user.email or '' }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
placeholder="your.email@example.com">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ _('Required for email notifications') }}</p>
</div>
</div>
</div>
<!-- Notification Preferences -->
<div class="bg-card-light dark:bg-card-dark rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
<i class="fas fa-bell mr-2"></i>{{ _('Notification Preferences') }}
</h2>
<div class="space-y-4">
<div class="flex items-center">
<input type="checkbox" id="email_notifications" name="email_notifications"
{% if user.email_notifications %}checked{% endif %}
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="email_notifications" class="ml-3 block text-sm text-gray-700 dark:text-gray-300">
<span class="font-medium">{{ _('Enable Email Notifications') }}</span>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ _('Master switch for all email notifications') }}</p>
</label>
</div>
<div class="ml-8 space-y-3 border-l-2 border-gray-200 dark:border-gray-700 pl-4">
<div class="flex items-center">
<input type="checkbox" id="notification_overdue_invoices" name="notification_overdue_invoices"
{% if user.notification_overdue_invoices %}checked{% endif %}
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="notification_overdue_invoices" class="ml-3 block text-sm text-gray-700 dark:text-gray-300">
{{ _('Overdue Invoice Notifications') }}
</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="notification_task_assigned" name="notification_task_assigned"
{% if user.notification_task_assigned %}checked{% endif %}
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="notification_task_assigned" class="ml-3 block text-sm text-gray-700 dark:text-gray-300">
{{ _('Task Assignment Notifications') }}
</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="notification_task_comments" name="notification_task_comments"
{% if user.notification_task_comments %}checked{% endif %}
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="notification_task_comments" class="ml-3 block text-sm text-gray-700 dark:text-gray-300">
{{ _('Comment & Mention Notifications') }}
</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="notification_weekly_summary" name="notification_weekly_summary"
{% if user.notification_weekly_summary %}checked{% endif %}
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="notification_weekly_summary" class="ml-3 block text-sm text-gray-700 dark:text-gray-300">
{{ _('Weekly Time Summary Email') }}
</label>
</div>
</div>
</div>
</div>
<!-- Display Preferences -->
<div class="bg-card-light dark:bg-card-dark rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
<i class="fas fa-palette mr-2"></i>{{ _('Display Preferences') }}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="theme_preference" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ _('Theme') }}
</label>
<select id="theme_preference" name="theme_preference"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<option value="">{{ _('System Default') }}</option>
<option value="light" {% if user.theme_preference == 'light' %}selected{% endif %}>☀️ {{ _('Light') }}</option>
<option value="dark" {% if user.theme_preference == 'dark' %}selected{% endif %}>🌙 {{ _('Dark') }}</option>
</select>
</div>
<div>
<label for="preferred_language" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ _('Language') }}
</label>
<select id="preferred_language" name="preferred_language"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<option value="">{{ _('System Default') }}</option>
{% for code, name in languages.items() %}
<option value="{{ code }}" {% if current_language_code == code %}selected{% endif %}>{{ name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
<!-- Time Rounding Preferences -->
<div class="bg-card-light dark:bg-card-dark rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
<i class="fas fa-clock mr-2"></i>{{ _('Time Rounding Preferences') }}
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{{ _('Configure how your time entries are rounded. This affects how durations are calculated when you stop timers.') }}
</p>
<div class="space-y-4">
<div class="flex items-center">
<input type="checkbox" id="time_rounding_enabled" name="time_rounding_enabled"
{% if user.time_rounding_enabled %}checked{% endif %}
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
onchange="toggleRoundingOptions()">
<label for="time_rounding_enabled" class="ml-3 block text-sm text-gray-700 dark:text-gray-300">
<span class="font-medium">{{ _('Enable Time Rounding') }}</span>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ _('Round time entries to configured intervals') }}</p>
</label>
</div>
<div id="rounding-options" class="ml-8 space-y-4 border-l-2 border-gray-200 dark:border-gray-700 pl-4">
<div>
<label for="time_rounding_minutes" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ _('Rounding Interval') }}
</label>
<select id="time_rounding_minutes" name="time_rounding_minutes"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
{% for minutes, label in rounding_intervals %}
<option value="{{ minutes }}" {% if user.time_rounding_minutes == minutes %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ _('Time entries will be rounded to this interval') }}</p>
</div>
<div>
<label for="time_rounding_method" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ _('Rounding Method') }}
</label>
<select id="time_rounding_method" name="time_rounding_method"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
onchange="updateRoundingMethodDescription()">
{% for method, label, description in rounding_methods %}
<option value="{{ method }}" data-description="{{ description }}" {% if user.time_rounding_method == method %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<p id="rounding-method-description" class="text-xs text-gray-500 dark:text-gray-400 mt-1"></p>
</div>
<!-- Example visualization -->
<div class="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-md">
<p class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">
<i class="fas fa-info-circle mr-1"></i>{{ _('Example') }}
</p>
<div id="rounding-example" class="text-xs text-blue-800 dark:text-blue-200 space-y-1">
<!-- Will be populated by JavaScript -->
</div>
</div>
</div>
</div>
</div>
<!-- Overtime Settings -->
<div class="bg-card-light dark:bg-card-dark rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
<i class="fas fa-business-time mr-2"></i>{{ _('Overtime Settings') }}
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{{ _('Set your standard working hours per day. Any time worked beyond this will be counted as overtime.') }}
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="standard_hours_per_day" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ _('Standard Hours Per Day') }}
</label>
<div class="relative">
<input type="number" id="standard_hours_per_day" name="standard_hours_per_day"
value="{{ user.standard_hours_per_day }}"
min="0.5" max="24" step="0.5"
class="w-full px-3 py-2 pr-16 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none">
<span class="absolute right-3 top-2.5 text-sm text-gray-500 dark:text-gray-400 pointer-events-none">{{ _('hours') }}</span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ _('Typically 8 hours for a full-time job') }}</p>
</div>
<div class="flex items-center bg-blue-50 dark:bg-blue-900/20 p-4 rounded-md">
<div>
<p class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-1">
<i class="fas fa-info-circle mr-1"></i>{{ _('How it works') }}
</p>
<p class="text-xs text-blue-800 dark:text-blue-200">
{{ _('If you work more than your standard hours in a day, the extra time will be tracked as overtime in reports and analytics.') }}
</p>
</div>
</div>
</div>
</div>
<!-- Regional Settings -->
<div class="bg-card-light dark:bg-card-dark rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
<i class="fas fa-globe mr-2"></i>{{ _('Regional Settings') }}
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="timezone" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ _('Timezone') }}
</label>
<select id="timezone" name="timezone"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<option value="">{{ _('System Default') }}</option>
{% for tz in timezones %}
<option value="{{ tz }}" {% if user.timezone == tz %}selected{% endif %}>{{ tz }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="date_format" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ _('Date Format') }}
</label>
<select id="date_format" name="date_format"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<option value="YYYY-MM-DD" {% if user.date_format == 'YYYY-MM-DD' %}selected{% endif %}>YYYY-MM-DD (2025-01-22)</option>
<option value="MM/DD/YYYY" {% if user.date_format == 'MM/DD/YYYY' %}selected{% endif %}>MM/DD/YYYY (01/22/2025)</option>
<option value="DD/MM/YYYY" {% if user.date_format == 'DD/MM/YYYY' %}selected{% endif %}>DD/MM/YYYY (22/01/2025)</option>
<option value="DD.MM.YYYY" {% if user.date_format == 'DD.MM.YYYY' %}selected{% endif %}>DD.MM.YYYY (22.01.2025)</option>
</select>
</div>
<div>
<label for="time_format" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ _('Time Format') }}
</label>
<select id="time_format" name="time_format"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<option value="24h" {% if user.time_format == '24h' %}selected{% endif %}>24-hour (14:30)</option>
<option value="12h" {% if user.time_format == '12h' %}selected{% endif %}>12-hour (2:30 PM)</option>
</select>
</div>
<div>
<label for="week_start_day" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ _('Week Starts On') }}
</label>
<select id="week_start_day" name="week_start_day"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<option value="0" {% if user.week_start_day == 0 %}selected{% endif %}>{{ _('Sunday') }}</option>
<option value="1" {% if user.week_start_day == 1 %}selected{% endif %}>{{ _('Monday') }}</option>
<option value="6" {% if user.week_start_day == 6 %}selected{% endif %}>{{ _('Saturday') }}</option>
</select>
</div>
</div>
</div>
<!-- Save Button -->
<div class="flex justify-end space-x-4">
<a href="{{ url_for('main.dashboard') }}" class="px-6 py-2 border border-gray-300 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
{{ _('Cancel') }}
</a>
<button type="submit" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition">
<i class="fas fa-save mr-2"></i>{{ _('Save Settings') }}
</button>
</div>
</form>
</div>
<script>
// Live theme preview
document.getElementById('theme_preference').addEventListener('change', function() {
const theme = this.value;
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
// System default
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
});
// Show warning if email notifications are enabled but no email is provided
document.getElementById('email_notifications').addEventListener('change', function() {
const emailField = document.getElementById('email');
if (this.checked && !emailField.value) {
emailField.classList.add('border-yellow-500');
emailField.focus();
} else {
emailField.classList.remove('border-yellow-500');
}
});
// Toggle rounding options visibility
function toggleRoundingOptions() {
const enabled = document.getElementById('time_rounding_enabled').checked;
const options = document.getElementById('rounding-options');
if (enabled) {
options.style.opacity = '1';
options.querySelectorAll('select').forEach(select => select.disabled = false);
} else {
options.style.opacity = '0.5';
options.querySelectorAll('select').forEach(select => select.disabled = true);
}
updateRoundingExample();
}
// Update rounding method description
function updateRoundingMethodDescription() {
const select = document.getElementById('time_rounding_method');
const description = select.options[select.selectedIndex].getAttribute('data-description');
document.getElementById('rounding-method-description').textContent = description;
updateRoundingExample();
}
// Update rounding example visualization
function updateRoundingExample() {
const enabled = document.getElementById('time_rounding_enabled').checked;
const minutes = parseInt(document.getElementById('time_rounding_minutes').value);
const method = document.getElementById('time_rounding_method').value;
const exampleDiv = document.getElementById('rounding-example');
if (!enabled) {
exampleDiv.innerHTML = '<p>{{ _("Time rounding is disabled. All times will be recorded exactly as tracked.") }}</p>';
return;
}
if (minutes === 1) {
exampleDiv.innerHTML = '<p>{{ _("No rounding - times will be recorded exactly as tracked.") }}</p>';
return;
}
// Calculate examples
const testDuration = 62; // 62 minutes = 1h 2min
let rounded;
if (method === 'up') {
rounded = Math.ceil(testDuration / minutes) * minutes;
} else if (method === 'down') {
rounded = Math.floor(testDuration / minutes) * minutes;
} else {
rounded = Math.round(testDuration / minutes) * minutes;
}
const formatTime = (mins) => {
const hours = Math.floor(mins / 60);
const remainingMins = mins % 60;
if (hours > 0) {
return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;
}
return `${remainingMins}m`;
};
exampleDiv.innerHTML = `
<p><strong>{{ _("Actual time:") }}</strong> ${formatTime(testDuration)} → <strong>{{ _("Rounded:") }}</strong> ${formatTime(rounded)}</p>
<p class="text-xs opacity-75">{{ _("With ") }}${minutes}{{ _(" minute intervals") }}</p>
`;
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
toggleRoundingOptions();
updateRoundingMethodDescription();
// Update example when settings change
document.getElementById('time_rounding_enabled').addEventListener('change', updateRoundingExample);
document.getElementById('time_rounding_minutes').addEventListener('change', updateRoundingExample);
});
</script>
{% endblock %}