mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-12 23:39:17 -05:00
77aec94b86
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)
596 lines
46 KiB
HTML
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 %}
|
|
|
|
|