mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-05 11:59:42 -05:00
a42a84ecdc
- 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.
418 lines
24 KiB
HTML
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 %}
|
|
|