Files
TimeTracker/templates/admin/settings.html
T
Dries Peeters 77aec94b86 feat: Add project costs tracking and remove license server integration
Major Features:
- Add project costs feature with full CRUD operations
- Implement toast notification system for better user feedback
- Enhance analytics dashboard with improved visualizations
- Add OIDC authentication improvements and debug tools

Improvements:
- Enhance reports with new filtering and export capabilities
- Update command palette with additional shortcuts
- Improve mobile responsiveness across all pages
- Refactor UI components for consistency

Removals:
- Remove license server integration and related dependencies
- Clean up unused license-related templates and utilities

Technical Changes:
- Add new migration 018 for project_costs table
- Update models: Project, Settings, User with new relationships
- Refactor routes: admin, analytics, auth, invoices, projects, reports
- Update static assets: CSS improvements, new JS modules
- Enhance templates: analytics, admin, projects, reports

Documentation:
- Add comprehensive documentation for project costs feature
- Document toast notification system with visual guides
- Update README with new feature descriptions
- Add migration instructions and quick start guides
- Document OIDC improvements and Kanban enhancements

Files Changed:
- Modified: 56 files (core app, models, routes, templates, static assets)
- Deleted: 6 files (license server integration)
- Added: 28 files (new features, documentation, migrations)
2025-10-09 11:50:26 +02:00

596 lines
46 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ _('Settings') }} - {{ app_name }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">
<i class="fas fa-sliders-h text-primary"></i> {{ _('System Settings') }}
</h1>
<a href="{{ url_for('admin.admin_dashboard') }}" class="btn-header btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>{{ _('Back to Dashboard') }}
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-cog me-1"></i> {{ _('Configuration') }}</h5>
<a href="{{ url_for('admin.pdf_layout') }}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-file-pdf me-2"></i>{{ _('Edit PDF Layout') }}
</a>
</div>
</div>
<div class="card-body">
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="timezone">{{ _('Timezone') }}</label>
<select class="form-select" id="timezone" name="timezone">
<optgroup label="UTC">
<option value="UTC" {% if settings and settings.timezone == 'UTC' %}selected{% endif %}>UTC (UTC+0)</option>
</optgroup>
<optgroup label="Europe">
<option value="Europe/London" {% if settings and settings.timezone == 'Europe/London' %}selected{% endif %}>Europe/London (UTC+0/+1)</option>
<option value="Europe/Paris" {% if settings and settings.timezone == 'Europe/Paris' %}selected{% endif %}>Europe/Paris (UTC+1/+2)</option>
<option value="Europe/Berlin" {% if settings and settings.timezone == 'Europe/Berlin' %}selected{% endif %}>Europe/Berlin (UTC+1/+2)</option>
<option value="Europe/Rome" {% if (settings and settings.timezone == 'Europe/Rome') or not settings %}selected{% endif %}>Europe/Rome (UTC+1/+2)</option>
<option value="Europe/Madrid" {% if settings and settings.timezone == 'Europe/Madrid' %}selected{% endif %}>Europe/Madrid (UTC+1/+2)</option>
<option value="Europe/Amsterdam" {% if settings and settings.timezone == 'Europe/Amsterdam' %}selected{% endif %}>Europe/Amsterdam (UTC+1/+2)</option>
<option value="Europe/Brussels" {% if settings and settings.timezone == 'Europe/Brussels' %}selected{% endif %}>Europe/Brussels (UTC+1/+2)</option>
<option value="Europe/Vienna" {% if settings and settings.timezone == 'Europe/Vienna' %}selected{% endif %}>Europe/Vienna (UTC+1/+2)</option>
<option value="Europe/Zurich" {% if settings and settings.timezone == 'Europe/Zurich' %}selected{% endif %}>Europe/Zurich (UTC+1/+2)</option>
<option value="Europe/Prague" {% if settings and settings.timezone == 'Europe/Prague' %}selected{% endif %}>Europe/Prague (UTC+1/+2)</option>
<option value="Europe/Warsaw" {% if settings and settings.timezone == 'Europe/Warsaw' %}selected{% endif %}>Europe/Warsaw (UTC+1/+2)</option>
<option value="Europe/Budapest" {% if settings and settings.timezone == 'Europe/Budapest' %}selected{% endif %}>Europe/Budapest (UTC+1/+2)</option>
<option value="Europe/Stockholm" {% if settings and settings.timezone == 'Europe/Stockholm' %}selected{% endif %}>Europe/Stockholm (UTC+1/+2)</option>
<option value="Europe/Oslo" {% if settings and settings.timezone == 'Europe/Oslo' %}selected{% endif %}>Europe/Oslo (UTC+1/+2)</option>
<option value="Europe/Copenhagen" {% if settings and settings.timezone == 'Europe/Copenhagen' %}selected{% endif %}>Europe/Copenhagen (UTC+1/+2)</option>
<option value="Europe/Helsinki" {% if settings and settings.timezone == 'Europe/Helsinki' %}selected{% endif %}>Europe/Helsinki (UTC+2/+3)</option>
<option value="Europe/Athens" {% if settings and settings.timezone == 'Europe/Athens' %}selected{% endif %}>Europe/Athens (UTC+2/+3)</option>
<option value="Europe/Istanbul" {% if settings and settings.timezone == 'Europe/Istanbul' %}selected{% endif %}>Europe/Istanbul (UTC+3)</option>
<option value="Europe/Moscow" {% if settings and settings.timezone == 'Europe/Moscow' %}selected{% endif %}>Europe/Moscow (UTC+3)</option>
<option value="Europe/Kiev" {% if settings and settings.timezone == 'Europe/Kiev' %}selected{% endif %}>Europe/Kiev (UTC+2/+3)</option>
</optgroup>
<optgroup label="North America">
<option value="America/New_York" {% if settings and settings.timezone == 'America/New_York' %}selected{% endif %}>America/New_York (UTC-5/-4)</option>
<option value="America/Chicago" {% if settings and settings.timezone == 'America/Chicago' %}selected{% endif %}>America/Chicago (UTC-6/-5)</option>
<option value="America/Denver" {% if settings and settings.timezone == 'America/Denver' %}selected{% endif %}>America/Denver (UTC-7/-6)</option>
<option value="America/Los_Angeles" {% if settings and settings.timezone == 'America/Los_Angeles' %}selected{% endif %}>America/Los_Angeles (UTC-8/-7)</option>
<option value="America/Toronto" {% if settings and settings.timezone == 'America/Toronto' %}selected{% endif %}>America/Toronto (UTC-5/-4)</option>
<option value="America/Vancouver" {% if settings and settings.timezone == 'America/Vancouver' %}selected{% endif %}>America/Vancouver (UTC-8/-7)</option>
<option value="America/Mexico_City" {% if settings and settings.timezone == 'America/Mexico_City' %}selected{% endif %}>America/Mexico_City (UTC-6/-5)</option>
<option value="America/Phoenix" {% if settings and settings.timezone == 'America/Phoenix' %}selected{% endif %}>America/Phoenix (UTC-7)</option>
<option value="America/Anchorage" {% if settings and settings.timezone == 'America/Anchorage' %}selected{% endif %}>America/Anchorage (UTC-9/-8)</option>
<option value="America/Honolulu" {% if settings and settings.timezone == 'America/Honolulu' %}selected{% endif %}>America/Honolulu (UTC-10)</option>
<option value="America/Sao_Paulo" {% if settings and settings.timezone == 'America/Sao_Paulo' %}selected{% endif %}>America/Sao_Paulo (UTC-3/-2)</option>
<option value="America/Buenos_Aires" {% if settings and settings.timezone == 'America/Buenos_Aires' %}selected{% endif %}>America/Buenos_Aires (UTC-3)</option>
<option value="America/Santiago" {% if settings and settings.timezone == 'America/Santiago' %}selected{% endif %}>America/Santiago (UTC-3/-4)</option>
<option value="America/Lima" {% if settings and settings.timezone == 'America/Lima' %}selected{% endif %}>America/Lima (UTC-5)</option>
<option value="America/Bogota" {% if settings and settings.timezone == 'America/Bogota' %}selected{% endif %}>America/Bogota (UTC-5)</option>
<option value="America/Caracas" {% if settings and settings.timezone == 'America/Caracas' %}selected{% endif %}>America/Caracas (UTC-4)</option>
</optgroup>
<optgroup label="Asia">
<option value="Asia/Tokyo" {% if settings and settings.timezone == 'Asia/Tokyo' %}selected{% endif %}>Asia/Tokyo (UTC+9)</option>
<option value="Asia/Shanghai" {% if settings and settings.timezone == 'Asia/Shanghai' %}selected{% endif %}>Asia/Shanghai (UTC+8)</option>
<option value="Asia/Seoul" {% if settings and settings.timezone == 'Asia/Seoul' %}selected{% endif %}>Asia/Seoul (UTC+9)</option>
<option value="Asia/Hong_Kong" {% if settings and settings.timezone == 'Asia/Hong_Kong' %}selected{% endif %}>Asia/Hong_Kong (UTC+8)</option>
<option value="Asia/Singapore" {% if settings and settings.timezone == 'Asia/Singapore' %}selected{% endif %}>Asia/Singapore (UTC+8)</option>
<option value="Asia/Bangkok" {% if settings and settings.timezone == 'Asia/Bangkok' %}selected{% endif %}>Asia/Bangkok (UTC+7)</option>
<option value="Asia/Ho_Chi_Minh" {% if settings and settings.timezone == 'Asia/Ho_Chi_Minh' %}selected{% endif %}>Asia/Ho_Chi_Minh (UTC+7)</option>
<option value="Asia/Jakarta" {% if settings and settings.timezone == 'Asia/Jakarta' %}selected{% endif %}>Asia/Jakarta (UTC+7)</option>
<option value="Asia/Manila" {% if settings and settings.timezone == 'Asia/Manila' %}selected{% endif %}>Asia/Manila (UTC+8)</option>
<option value="Asia/Kolkata" {% if settings and settings.timezone == 'Asia/Kolkata' %}selected{% endif %}>Asia/Kolkata (UTC+5:30)</option>
<option value="Asia/Dhaka" {% if settings and settings.timezone == 'Asia/Dhaka' %}selected{% endif %}>Asia/Dhaka (UTC+6)</option>
<option value="Asia/Kathmandu" {% if settings and settings.timezone == 'Asia/Kathmandu' %}selected{% endif %}>Asia/Kathmandu (UTC+5:45)</option>
<option value="Asia/Tashkent" {% if settings and settings.timezone == 'Asia/Tashkent' %}selected{% endif %}>Asia/Tashkent (UTC+5)</option>
<option value="Asia/Dubai" {% if settings and settings.timezone == 'Asia/Dubai' %}selected{% endif %}>Asia/Dubai (UTC+4)</option>
<option value="Asia/Tehran" {% if settings and settings.timezone == 'Asia/Tehran' %}selected{% endif %}>Asia/Tehran (UTC+3:30/+4:30)</option>
<option value="Asia/Jerusalem" {% if settings and settings.timezone == 'Asia/Jerusalem' %}selected{% endif %}>Asia/Jerusalem (UTC+2/+3)</option>
<option value="Asia/Riyadh" {% if settings and settings.timezone == 'Asia/Riyadh' %}selected{% endif %}>Asia/Riyadh (UTC+3)</option>
<option value="Asia/Baghdad" {% if settings and settings.timezone == 'Asia/Baghdad' %}selected{% endif %}>Asia/Baghdad (UTC+3)</option>
<option value="Asia/Kabul" {% if settings and settings.timezone == 'Asia/Kabul' %}selected{% endif %}>Asia/Kabul (UTC+4:30)</option>
<option value="Asia/Almaty" {% if settings and settings.timezone == 'Asia/Almaty' %}selected{% endif %}>Asia/Almaty (UTC+6)</option>
<option value="Asia/Novosibirsk" {% if settings and settings.timezone == 'Asia/Novosibirsk' %}selected{% endif %}>Asia/Novosibirsk (UTC+7)</option>
<option value="Asia/Vladivostok" {% if settings and settings.timezone == 'Asia/Vladivostok' %}selected{% endif %}>Asia/Vladivostok (UTC+10)</option>
</optgroup>
<optgroup label="Australia & Pacific">
<option value="Australia/Sydney" {% if settings and settings.timezone == 'Australia/Sydney' %}selected{% endif %}>Australia/Sydney (UTC+10/+11)</option>
<option value="Australia/Melbourne" {% if settings and settings.timezone == 'Australia/Melbourne' %}selected{% endif %}>Australia/Melbourne (UTC+10/+11)</option>
<option value="Australia/Brisbane" {% if settings and settings.timezone == 'Australia/Brisbane' %}selected{% endif %}>Australia/Brisbane (UTC+10)</option>
<option value="Australia/Perth" {% if settings and settings.timezone == 'Australia/Perth' %}selected{% endif %}>Australia/Perth (UTC+8)</option>
<option value="Australia/Adelaide" {% if settings and settings.timezone == 'Australia/Adelaide' %}selected{% endif %}>Australia/Adelaide (UTC+9:30/+10:30)</option>
<option value="Australia/Darwin" {% if settings and settings.timezone == 'Australia/Darwin' %}selected{% endif %}>Australia/Darwin (UTC+9:30)</option>
<option value="Pacific/Auckland" {% if settings and settings.timezone == 'Pacific/Auckland' %}selected{% endif %}>Pacific/Auckland (UTC+12/+13)</option>
<option value="Pacific/Fiji" {% if settings and settings.timezone == 'Pacific/Fiji' %}selected{% endif %}>Pacific/Fiji (UTC+12)</option>
<option value="Pacific/Guam" {% if settings and settings.timezone == 'Pacific/Guam' %}selected{% endif %}>Pacific/Guam (UTC+10)</option>
<option value="Pacific/Honolulu" {% if settings and settings.timezone == 'Pacific/Honolulu' %}selected{% endif %}>Pacific/Honolulu (UTC-10)</option>
<option value="Pacific/Tahiti" {% if settings and settings.timezone == 'Pacific/Tahiti' %}selected{% endif %}>Pacific/Tahiti (UTC-10)</option>
</optgroup>
<optgroup label="Africa">
<option value="Africa/Cairo" {% if settings and settings.timezone == 'Africa/Cairo' %}selected{% endif %}>Africa/Cairo (UTC+2)</option>
<option value="Africa/Johannesburg" {% if settings and settings.timezone == 'Africa/Johannesburg' %}selected{% endif %}>Africa/Johannesburg (UTC+2)</option>
<option value="Africa/Lagos" {% if settings and settings.timezone == 'Africa/Lagos' %}selected{% endif %}>Africa/Lagos (UTC+1)</option>
<option value="Africa/Nairobi" {% if settings and settings.timezone == 'Africa/Nairobi' %}selected{% endif %}>Africa/Nairobi (UTC+3)</option>
<option value="Africa/Casablanca" {% if settings and settings.timezone == 'Africa/Casablanca' %}selected{% endif %}>Africa/Casablanca (UTC+0/+1)</option>
<option value="Africa/Algiers" {% if settings and settings.timezone == 'Africa/Algiers' %}selected{% endif %}>Africa/Algiers (UTC+1)</option>
<option value="Africa/Tunis" {% if settings and settings.timezone == 'Africa/Tunis' %}selected{% endif %}>Africa/Tunis (UTC+1)</option>
<option value="Africa/Dar_es_Salaam" {% if settings and settings.timezone == 'Africa/Dar_es_Salaam' %}selected{% endif %}>Africa/Dar_es_Salaam (UTC+3)</option>
<option value="Africa/Addis_Ababa" {% if settings and settings.timezone == 'Africa/Addis_Ababa' %}selected{% endif %}>Africa/Addis_Ababa (UTC+3)</option>
<option value="Africa/Khartoum" {% if settings and settings.timezone == 'Africa/Khartoum' %}selected{% endif %}>Africa/Khartoum (UTC+2)</option>
<option value="Africa/Luanda" {% if settings and settings.timezone == 'Africa/Luanda' %}selected{% endif %}>Africa/Luanda (UTC+1)</option>
<option value="Africa/Kinshasa" {% if settings and settings.timezone == 'Africa/Kinshasa' %}selected{% endif %}>Africa/Kinshasa (UTC+1)</option>
<option value="Africa/Harare" {% if settings and settings.timezone == 'Africa/Harare' %}selected{% endif %}>Africa/Harare (UTC+2)</option>
</optgroup>
<optgroup label="Atlantic & Indian Ocean">
<option value="Atlantic/Reykjavik" {% if settings and settings.timezone == 'Atlantic/Reykjavik' %}selected{% endif %}>Atlantic/Reykjavik (UTC+0)</option>
<option value="Atlantic/Azores" {% if settings and settings.timezone == 'Atlantic/Azores' %}selected{% endif %}>Atlantic/Azores (UTC-1/+0)</option>
<option value="Atlantic/Canary" {% if settings and settings.timezone == 'Atlantic/Canary' %}selected{% endif %}>Atlantic/Canary (UTC+0/+1)</option>
<option value="Atlantic/Cape_Verde" {% if settings and settings.timezone == 'Atlantic/Cape_Verde' %}selected{% endif %}>Atlantic/Cape_Verde (UTC-1)</option>
<option value="Indian/Mauritius" {% if settings and settings.timezone == 'Indian/Mauritius' %}selected{% endif %}>Indian/Mauritius (UTC+4)</option>
<option value="Indian/Reunion" {% if settings and settings.timezone == 'Indian/Reunion' %}selected{% endif %}>Indian/Reunion (UTC+4)</option>
<option value="Indian/Maldives" {% if settings and settings.timezone == 'Indian/Maldives' %}selected{% endif %}>Indian/Maldives (UTC+5)</option>
<option value="Indian/Chagos" {% if settings and settings.timezone == 'Indian/Chagos' %}selected{% endif %}>Indian/Chagos (UTC+6)</option>
</optgroup>
<optgroup label="Arctic & Antarctic">
<option value="Arctic/Longyearbyen" {% if settings and settings.timezone == 'Arctic/Longyearbyen' %}selected{% endif %}>Arctic/Longyearbyen (UTC+1/+2)</option>
<option value="Antarctica/McMurdo" {% if settings and settings.timezone == 'Antarctica/McMurdo' %}selected{% endif %}>Antarctica/McMurdo (UTC+12/+13)</option>
<option value="Antarctica/Palmer" {% if settings and settings.timezone == 'Antarctica/Palmer' %}selected{% endif %}>Antarctica/Palmer (UTC-3)</option>
</optgroup>
</select>
<small class="form-text text-muted">{{ _('Select your local timezone for proper time display. Times shown include DST adjustments.') }}</small>
</div>
<div class="col-md-6">
<label class="form-label" for="currency">{{ _('Currency') }}</label>
<input type="text" class="form-control" id="currency" name="currency" value="{{ settings.currency if settings else 'EUR' }}" placeholder="e.g. EUR">
</div>
<div class="col-md-6">
<label class="form-label" for="rounding_minutes">{{ _('Rounding (minutes)') }}</label>
<input type="number" min="0" step="1" class="form-control" id="rounding_minutes" name="rounding_minutes" value="{{ settings.rounding_minutes if settings else 1 }}">
</div>
<div class="col-md-6">
<label class="form-label" for="idle_timeout_minutes">{{ _('Idle Timeout (minutes)') }}</label>
<input type="number" min="0" step="1" class="form-control" id="idle_timeout_minutes" name="idle_timeout_minutes" value="{{ settings.idle_timeout_minutes if settings else 30 }}">
</div>
<div class="col-md-6">
<label class="form-label" for="backup_retention_days">{{ _('Backup Retention (days)') }}</label>
<input type="number" min="0" step="1" class="form-control" id="backup_retention_days" name="backup_retention_days" value="{{ settings.backup_retention_days if settings else 30 }}">
</div>
<div class="col-md-6">
<label class="form-label" for="backup_time">{{ _('Backup Time (HH:MM)') }}</label>
<input type="time" class="form-control" id="backup_time" name="backup_time" value="{{ settings.backup_time if settings else '02:00' }}">
</div>
<div class="col-md-6">
<label class="form-label" for="export_delimiter">{{ _('Export Delimiter') }}</label>
<select class="form-select" id="export_delimiter" name="export_delimiter">
{% set delim = (settings.export_delimiter if settings else ',') %}
<option value="," {% if delim == ',' %}selected{% endif %}>{{ _(', (comma)') }}</option>
<option value=";" {% if delim == ';' %}selected{% endif %}>{{ _('; (semicolon)') }}</option>
<option value="\t" {% if delim == '\t' %}selected{% endif %}>{{ _('Tab') }}</option>
<option value="|" {% if delim == '|' %}selected{% endif %}>{{ _('| (pipe)') }}</option>
</select>
</div>
<div class="col-md-6 d-flex align-items-end">
<div class="form-check me-4">
<input class="form-check-input" type="checkbox" id="single_active_timer" name="single_active_timer" {% if settings and settings.single_active_timer %}checked{% endif %}>
<label class="form-check-label" for="single_active_timer">{{ _('Single Active Timer') }}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="allow_self_register" name="allow_self_register" {% if settings and settings.allow_self_register %}checked{% endif %}>
<label class="form-check-label" for="allow_self_register">{{ _('Allow Self Register') }}</label>
</div>
</div>
<div class="col-12 mt-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Save Settings
</button>
</div>
<!-- Company Branding Section -->
<div class="row g-3 mt-4">
<div class="col-12">
<h5 class="text-primary border-bottom pb-2">
<i class="fas fa-building me-2"></i>Company Branding (for Invoices)
</h5>
</div>
<div class="col-md-6">
<label class="form-label" for="company_name">{{ _('Company Name') }}</label>
<input type="text" class="form-control" id="company_name" name="company_name"
value="{{ settings.company_name if settings else 'Your Company Name' }}" required>
</div>
<div class="col-md-6">
<label class="form-label" for="company_email">{{ _('Company Email') }}</label>
<input type="email" class="form-control" id="company_email" name="company_email"
value="{{ settings.company_email if settings else 'info@yourcompany.com' }}" required>
</div>
<div class="col-md-6">
<label class="form-label" for="company_phone">{{ _('Company Phone') }}</label>
<input type="text" class="form-control" id="company_phone" name="company_phone"
value="{{ settings.company_phone if settings else '+1 (555) 123-4567' }}" required>
</div>
<div class="col-md-6">
<label class="form-label" for="company_website">{{ _('Company Website') }}</label>
<input type="url" class="form-control" id="company_website" name="company_website"
value="{{ settings.company_website if settings else 'www.yourcompany.com' }}" required>
</div>
<div class="col-12">
<label class="form-label" for="company_address">{{ _('Company Address') }}</label>
<textarea class="form-control" id="company_address" name="company_address" rows="3" required>{{ settings.company_address if settings else 'Your Company Address' }}</textarea>
</div>
<div class="col-md-6">
<label class="form-label" for="company_tax_id">{{ _('Tax ID / VAT Number') }}</label>
<input type="text" class="form-control" id="company_tax_id" name="company_tax_id"
value="{{ settings.company_tax_id if settings else '' }}" placeholder="Optional">
</div>
<div class="col-md-6">
<label class="form-label" for="company_logo">{{ _('Company Logo') }}</label>
<div class="logo-upload-section">
{% if settings and settings.has_logo() %}
<div class="current-logo mb-3">
<img src="{{ settings.get_logo_url() }}" alt="{{ _('Current Company Logo') }}"
class="img-thumbnail" style="max-width: 150px; max-height: 150px;">
<div class="mt-2">
<form method="POST" action="{{ url_for('admin.remove_logo') }}"
class="d-inline"
data-confirm="{{ _('Are you sure you want to remove the current logo?') }}"
onsubmit="return confirm(this.getAttribute('data-confirm'))">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="fas fa-trash me-1"></i> {{ _('Remove Logo') }}
</button>
</form>
</div>
</div>
{% endif %}
<div class="upload-controls">
<input type="file" class="form-control" id="logo_file"
accept=".png,.jpg,.jpeg,.gif,.webp"
onchange="previewLogo(this)">
<small class="form-text text-muted">
{{ _('Supported formats: PNG, JPG, JPEG, GIF, WEBP (Max size: 5MB)') }}
</small>
<div class="mt-3">
<button type="button" class="btn btn-primary" onclick="uploadLogo()">
<i class="fas fa-upload me-1"></i> {{ _('Upload Logo') }}
</button>
</div>
</div>
<div id="logo-preview" class="mt-3" style="display: none;">
<h6>{{ _('Logo Preview:') }}</h6>
<img id="preview-image" class="img-thumbnail" style="max-width: 150px; max-height: 150px;">
</div>
</div>
</div>
<div class="col-12">
<label class="form-label" for="company_bank_info">{{ _('Bank/Payment Information') }}</label>
<textarea class="form-control" id="company_bank_info" name="company_bank_info" rows="3" placeholder="Bank account details, payment instructions, etc.">{{ settings.company_bank_info if settings else '' }}</textarea>
<small class="form-text text-muted">{{ _('This will appear on invoices for payment instructions') }}</small>
</div>
</div>
<!-- Invoice Defaults Section -->
<div class="row g-3 mt-4">
<div class="col-12">
<h5 class="text-primary border-bottom pb-2">
<i class="fas fa-file-invoice-dollar me-2"></i>{{ _('Invoice Defaults') }}
</h5>
</div>
<div class="col-md-6">
<label class="form-label" for="invoice_prefix">{{ _('Invoice Number Prefix') }}</label>
<input type="text" class="form-control" id="invoice_prefix" name="invoice_prefix"
value="{{ settings.invoice_prefix if settings else 'INV' }}" required>
</div>
<div class="col-md-6">
<label class="form-label" for="invoice_start_number">{{ _('Starting Invoice Number') }}</label>
<input type="number" class="form-control" id="invoice_start_number" name="invoice_start_number"
value="{{ settings.invoice_start_number if settings else 1000 }}" min="1" required>
</div>
<div class="col-12">
<label class="form-label" for="invoice_terms">{{ _('Default Terms & Conditions') }}</label>
<textarea class="form-control" id="invoice_terms" name="invoice_terms" rows="3" required>{{ settings.invoice_terms if settings else 'Payment is due within 30 days of invoice date.' }}</textarea>
</div>
<div class="col-12">
<label class="form-label" for="invoice_notes">{{ _('Default Invoice Notes') }}</label>
<textarea class="form-control" id="invoice_notes" name="invoice_notes" rows="2" required>{{ settings.invoice_notes if settings else 'Thank you for your business!' }}</textarea>
</div>
</div>
<!-- Privacy & Analytics Section -->
<div class="row g-3 mt-4">
<div class="col-12">
<h5 class="text-primary border-bottom pb-2">
<i class="fas fa-shield-alt me-2"></i>{{ _('Privacy & Analytics') }}
</h5>
</div>
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="allow_analytics" name="allow_analytics"
{% if settings and settings.allow_analytics %}checked{% endif %}>
<label class="form-check-label" for="allow_analytics">
<strong>{{ _('Allow Analytics Information') }}</strong>
</label>
<div class="form-text">
<i class="fas fa-info-circle me-1"></i>
{{ _('When enabled, basic system information (OS, version, etc.) may be shared for analytics purposes.') }}
<strong>{{ _('Core functionality will continue to work regardless of this setting.') }}</strong>
<br><small class="text-muted">{{ _('This helps improve the application.') }}</small>
</div>
</div>
</div>
</div>
<div class="col-12 mt-3">
<div class="alert alert-info">
<div class="row align-items-center">
<div class="col-md-8">
<i class="fas fa-clock me-2"></i>
<strong>{{ _('Current Time:') }}</strong>
<span id="current-time-display" class="h5 mb-0 ms-2">{{ _('Loading...') }}</span>
</div>
<div class="col-md-4 text-md-end">
<small class="text-muted">
{{ _('in') }} <strong>{{ settings.timezone if settings else 'Europe/Rome' }}</strong> {{ _('timezone') }}
</small>
</div>
</div>
<div class="mt-2">
<div class="row">
<div class="col-md-6">
<small class="text-muted">
<i class="fas fa-info-circle me-1"></i>
{{ _('This time updates every second and shows the current time in your selected timezone') }}
</small>
</div>
<div class="col-md-6 text-md-end">
<small class="text-muted">
<i class="fas fa-globe me-1"></i>
{{ _('Current offset:') }} <span id="timezone-offset">--</span>
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-info-circle me-1"></i> {{ _('Help') }}</h5>
</div>
<div class="card-body">
<p class="text-muted">{{ _('Configure application-wide settings such as timezone, currency, timer behavior, data export options, and company branding for invoices.') }}</p>
<ul class="text-muted mb-0">
<li>{{ _('Rounding affects how durations are rounded when displayed.') }}</li>
<li>{{ _('Single Active Timer stops any running timer when a new one is started.') }}</li>
<li>{{ _('Self Register allows new usernames to be created on login.') }}</li>
<li>{{ _('Company branding settings are used for PDF invoice generation.') }}</li>
<li>{{ _('Company logos can be uploaded directly through the interface (PNG, JPG, JPEG, GIF, SVG, WEBP formats supported).') }}</li>
<li>{{ _('Analytics setting controls whether system information is shared for analytics purposes.') }}</li>
<li><strong>{{ _('Core functionality will continue to work regardless of the analytics setting.') }}</strong></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script type="application/json" id="i18n-json-admin-settings">
{
"file_size_less_than_5mb": {{ _('File size must be less than 5MB')|tojson }},
"invalid_file_type": {{ _('Invalid file type. Please select a valid image file.')|tojson }},
"select_logo_file": {{ _('Please select a logo file to upload')|tojson }},
"uploading": {{ _('Uploading...')|tojson }},
"logo_upload_failed": {{ _('Logo upload failed. Please try again.')|tojson }},
"no_timezone_selected": {{ _('No timezone selected')|tojson }},
"invalid_timezone": {{ _('Invalid timezone')|tojson }}
}
</script>
<script>
var i18nSettings = (function(){
try { var el = document.getElementById('i18n-json-admin-settings'); return el ? JSON.parse(el.textContent) : {}; }
catch(e) { return {}; }
})();
document.addEventListener('DOMContentLoaded', function() {
const timezoneSelect = document.getElementById('timezone');
const currentTimeDisplay = document.getElementById('current-time-display');
function updateCurrentTime() {
const now = new Date();
const timezone = timezoneSelect.value;
if (!timezone) {
currentTimeDisplay.textContent = (i18nSettings.no_timezone_selected || 'No timezone selected');
document.getElementById('timezone-offset').textContent = '--';
return;
}
try {
// Format time in the selected timezone
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
const localTime = formatter.format(now);
currentTimeDisplay.textContent = localTime;
// Calculate and display timezone offset
const utcTime = new Date(now.toLocaleString("en-US", {timeZone: "UTC"}));
const localTimeObj = new Date(now.toLocaleString("en-US", {timeZone: timezone}));
const offset = (localTimeObj - utcTime) / (1000 * 60 * 60);
const offsetText = offset >= 0 ? `UTC+${offset.toFixed(1)}` : `UTC${offset.toFixed(1)}`;
document.getElementById('timezone-offset').textContent = offsetText;
} catch (e) {
currentTimeDisplay.textContent = (i18nSettings.invalid_timezone || 'Invalid timezone');
document.getElementById('timezone-offset').textContent = '--';
}
}
// Update time when timezone changes
timezoneSelect.addEventListener('change', updateCurrentTime);
// Update time every second
setInterval(updateCurrentTime, 1000);
// Initial update with a small delay to ensure DOM is ready
setTimeout(updateCurrentTime, 100);
});
// Logo upload and preview functions
function previewLogo(input) {
const preview = document.getElementById('logo-preview');
const previewImage = document.getElementById('preview-image');
if (input.files && input.files[0]) {
const file = input.files[0];
// Validate file size (5MB limit)
if (file.size > 5 * 1024 * 1024) {
alert(i18nSettings.file_size_less_than_5mb || 'File size must be less than 5MB');
input.value = '';
return;
}
// Validate file type
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
alert(i18nSettings.invalid_file_type || 'Invalid file type. Please select a valid image file.');
input.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
previewImage.src = e.target.result;
preview.style.display = 'block';
};
reader.readAsDataURL(file);
} else {
preview.style.display = 'none';
}
}
function uploadLogo() {
const fileInput = document.getElementById('logo_file');
const file = fileInput.files[0];
if (!file) {
alert(i18nSettings.select_logo_file || 'Please select a logo file to upload');
return;
}
// Validate file size (5MB limit)
if (file.size > 5 * 1024 * 1024) {
alert(i18nSettings.file_size_less_than_5mb || 'File size must be less than 5MB');
return;
}
// Validate file type
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
alert(i18nSettings.invalid_file_type || 'Invalid file type. Please select a valid image file.');
return;
}
// Create FormData and submit
const formData = new FormData();
formData.append('logo', file);
// Show loading state
const uploadBtn = document.querySelector('button[onclick="uploadLogo()"]');
const originalText = uploadBtn.innerHTML;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> ' + (i18nSettings.uploading || 'Uploading...');
uploadBtn.disabled = true;
const csrfMeta = document.querySelector('meta[name="csrf-token"]');
const csrf = csrfMeta ? csrfMeta.getAttribute('content') : '';
fetch('{{ url_for("admin.upload_logo") }}', {
method: 'POST',
headers: csrf ? { 'X-CSRFToken': csrf } : {},
body: formData
})
.then(response => {
if (response.ok) {
// Reload page to show new logo
window.location.reload();
} else {
throw new Error('Upload failed');
}
})
.catch(error => {
console.error('Upload error:', error);
alert(i18nSettings.logo_upload_failed || 'Logo upload failed. Please try again.');
// Reset button state
uploadBtn.innerHTML = originalText;
uploadBtn.disabled = false;
});
}
// File input change handler
document.addEventListener('DOMContentLoaded', function() {
const fileInput = document.getElementById('logo_file');
if (fileInput) {
fileInput.addEventListener('change', function() {
previewLogo(this);
});
}
});
</script>
{% endblock %}