Files
TimeTracker/app/templates/admin/settings.html
T
Dries Peeters b0dde80ba9 feat(web): high-visibility support modal, prompts, and supporter UX
Add a support modal with usage stats, tier and license links, share control, and offline-safe outbound CTAs. Surface support from the header, sidebar, user menu, dashboard card, and settings "Support & Community" section without hiding entry points when a supporter license is active.

Introduce UsageStatsService and a persisted users.support_stats_reports_generated counter incremented on key report exports and custom report views. Add SupportPromptService for session-scoped soft toasts (after export, dashboard milestones, long session via POST /donate/request-soft-prompt).

Wire consent-aware track_event names support.* and mirror funnel rows in DonationInteraction; fix has_recent_donation_click to treat link_clicked as a recent click. Document events and SUPPORT_* / migration notes in docs.

Tests: tests/test_support_services.py for prompt and usage stats behavior.
2026-04-15 10:55:37 +02:00

697 lines
50 KiB
HTML

{% extends "base.html" %}
{% from "components/ui.html" import page_header, breadcrumb_nav, button, filter_badge %}
{% block extra_css %}
<style>
.rotate-180 {
transform: rotate(180deg);
}
.category-chevron {
transition: transform 0.2s ease;
}
</style>
{% endblock %}
{% block content %}
{% set breadcrumbs = [
{'text': 'Admin', 'url': url_for('admin.admin_dashboard')},
{'text': 'System Settings'}
] %}
{{ page_header(
icon_class='fas fa-sliders-h',
title_text='System Settings',
subtitle_text='Configure system-wide application settings',
breadcrumbs=breadcrumbs,
actions_html=None
) }}
<!-- Mobile Section Navigation -->
<div class="mb-4 overflow-x-auto lg:hidden">
<nav class="flex gap-2 pb-2 min-w-max">
<a href="#section-general" class="px-3 py-1.5 text-xs font-medium rounded-full bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark whitespace-nowrap">General</a>
<a href="#section-timers" class="px-3 py-1.5 text-xs font-medium rounded-full bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark whitespace-nowrap">Timers</a>
<a href="#section-time-entry" class="px-3 py-1.5 text-xs font-medium rounded-full bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark whitespace-nowrap">Time Entry</a>
<a href="#section-branding" class="px-3 py-1.5 text-xs font-medium rounded-full bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark whitespace-nowrap">Branding</a>
<a href="#section-invoices" class="px-3 py-1.5 text-xs font-medium rounded-full bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark whitespace-nowrap">Invoices</a>
<a href="#section-peppol" class="px-3 py-1.5 text-xs font-medium rounded-full bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark whitespace-nowrap">Peppol</a>
<a href="#section-backup" class="px-3 py-1.5 text-xs font-medium rounded-full bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark whitespace-nowrap">Backup</a>
<a href="#section-kiosk" class="px-3 py-1.5 text-xs font-medium rounded-full bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark whitespace-nowrap">Kiosk</a>
<a href="#section-analytics" class="px-3 py-1.5 text-xs font-medium rounded-full bg-card-light dark:bg-card-dark border border-border-light dark:border-border-dark whitespace-nowrap">Analytics</a>
</nav>
</div>
<!-- Main Settings Form -->
<div class="bg-card-light dark:bg-card-dark p-6 rounded-xl border border-border-light dark:border-border-dark shadow-sm mb-6">
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="space-y-8">
<!-- General Settings -->
<div id="section-general" class="border-b border-border-light dark:border-border-dark pb-6">
<h2 class="text-lg font-semibold mb-4">General</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="timezone" class="form-label">Timezone</label>
<select name="timezone" id="timezone" class="form-input" required>
{% for tz in timezones %}
<option value="{{ tz }}" {% if settings.timezone == tz %}selected{% endif %}>{{ tz }}</option>
{% endfor %}
</select>
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
This becomes the default timezone for users who choose “System Default”.
</p>
</div>
<div>
<label for="currency" class="form-label">Currency</label>
<input type="text" name="currency" id="currency" value="{{ settings.currency }}" required class="form-input">
</div>
<div>
<label for="date_format" class="form-label">Date Format</label>
<select name="date_format" id="date_format" class="form-input">
<option value="YYYY-MM-DD" {% if settings.date_format == 'YYYY-MM-DD' %}selected{% endif %}>YYYY-MM-DD (2026-02-06)</option>
<option value="MM/DD/YYYY" {% if settings.date_format == 'MM/DD/YYYY' %}selected{% endif %}>MM/DD/YYYY (02/06/2026)</option>
<option value="DD/MM/YYYY" {% if settings.date_format == 'DD/MM/YYYY' %}selected{% endif %}>DD/MM/YYYY (06/02/2026)</option>
<option value="DD.MM.YYYY" {% if settings.date_format == 'DD.MM.YYYY' %}selected{% endif %}>DD.MM.YYYY (06.02.2026)</option>
</select>
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
System-wide default date format. Users can override this in their profile.
</p>
</div>
<div>
<label for="time_format" class="form-label">Time Format</label>
<select name="time_format" id="time_format" class="form-input">
<option value="24h" {% if settings.time_format == '24h' %}selected{% endif %}>24-hour (14:30)</option>
<option value="12h" {% if settings.time_format == '12h' %}selected{% endif %}>12-hour (2:30 PM)</option>
</select>
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
System-wide default time format. Users can override this in their profile.
</p>
</div>
</div>
</div>
<!-- Timer Settings -->
<div id="section-timers" class="border-b border-border-light dark:border-border-dark pb-6">
<h2 class="text-lg font-semibold mb-4">Timers</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="rounding_minutes" class="form-label">Rounding (Minutes)</label>
<input type="number" name="rounding_minutes" id="rounding_minutes" value="{{ settings.rounding_minutes }}" required class="form-input">
</div>
<div>
<label for="idle_timeout_minutes" class="form-label">Idle Timeout (Minutes)</label>
<input type="number" name="idle_timeout_minutes" id="idle_timeout_minutes" value="{{ settings.idle_timeout_minutes }}" required class="form-input">
</div>
<div class="md:col-span-2 flex items-center">
<input type="checkbox" name="single_active_timer" id="single_active_timer" {% if settings.single_active_timer %}checked{% endif %} class="h-4 w-4 rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<label for="single_active_timer" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">Allow only one active timer per user</label>
</div>
</div>
</div>
<!-- Time Entry Requirements -->
<div id="section-time-entry" class="border-b border-border-light dark:border-border-dark pb-6">
<h2 class="text-lg font-semibold mb-4">{{ _('Time Entry Requirements') }}</h2>
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mb-4">
{{ _('Optionally require users to provide task and description when logging worked time. Enforced across web, mobile, and desktop.') }}
</p>
<div class="space-y-4">
<div class="flex items-center">
<input type="checkbox" name="time_entry_require_task" id="time_entry_require_task" {% if getattr(settings, 'time_entry_require_task', false) %}checked{% endif %} class="h-4 w-4 rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<label for="time_entry_require_task" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">{{ _('Require task selection when logging time (project-based entries only)') }}</label>
</div>
<div class="flex items-center">
<input type="checkbox" name="time_entry_require_description" id="time_entry_require_description" {% if getattr(settings, 'time_entry_require_description', false) %}checked{% endif %} class="h-4 w-4 rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<label for="time_entry_require_description" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">{{ _('Require description when logging time') }}</label>
</div>
<div id="time_entry_min_length_row" class="ml-6 {% if not getattr(settings, 'time_entry_require_description', false) %}hidden{% endif %}">
<label for="time_entry_description_min_length" class="form-label">{{ _('Minimum description length (characters)') }}</label>
<input type="number" name="time_entry_description_min_length" id="time_entry_description_min_length" value="{{ getattr(settings, 'time_entry_description_min_length', 20) }}" min="1" max="500" class="form-input w-32 mt-1">
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">{{ _('Minimum number of characters required in the description field.') }}</p>
</div>
<div class="pt-4 border-t border-border-light dark:border-border-dark mt-4">
<label for="default_daily_working_hours" class="form-label">{{ _('Default daily working hours (for new users)') }}</label>
<input type="number" name="default_daily_working_hours" id="default_daily_working_hours" value="{{ getattr(settings, 'default_daily_working_hours', 8.0) }}" min="0.5" max="24" step="0.5" class="form-input w-32 mt-1">
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">{{ _('Used as the standard hours per day for overtime calculation when new users are created. Existing users keep their own setting.') }}</p>
</div>
</div>
</div>
<!-- User Management -->
<div class="border-b border-border-light dark:border-border-dark pb-6">
<h2 class="text-lg font-semibold mb-4">{{ _('User Management') }}</h2>
<div class="space-y-4">
<div class="flex items-center">
<input type="checkbox" name="allow_self_register" id="allow_self_register" {% if settings.allow_self_register %}checked{% endif %} class="h-4 w-4 rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<label for="allow_self_register" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">Allow self-registration (users can create accounts by entering any username on login page)</label>
</div>
<p class="text-xs text-text-muted-light dark:text-text-muted-dark ml-6">
Note: Admin users are configured via the ADMIN_USERNAMES environment variable, not in this UI.
</p>
</div>
</div>
<!-- Support / Donate visibility (system-wide) -->
<div class="border-b border-border-light dark:border-border-dark pb-6" id="donateVisibilitySection">
<h2 class="text-lg font-semibold mb-4"><i class="fas fa-eye-slash mr-2"></i>{{ _('Support visibility') }}</h2>
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mb-4">
{{ _('Remove donate and support prompts for all users with a one-time key (€25, one key per instance).') }}
</p>
<ol class="list-decimal list-inside space-y-2 mb-4 text-sm text-text-light dark:text-text-dark">
<li>{{ _('Copy your System ID below and use it when buying the key.') }}</li>
<li><a href="{{ support_purchase_url }}" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline">{{ _('Buy key') }} <i class="fas fa-external-link-alt text-xs"></i></a> {{ _('— key sent by email.') }}</li>
<li>{{ _('Paste the code below and click Verify.') }}</li>
</ol>
<p class="mb-4">
<a href="{{ support_purchase_url }}" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition">
<i class="fas fa-key mr-1"></i>{{ _('Get key') }}
<i class="fas fa-external-link-alt text-xs"></i>
</a>
</p>
{% if system_instance_id %}
<div class="mb-4">
<label class="form-label">{{ _('Step 1: System ID') }}</label>
<div class="flex items-center gap-2">
<input type="text" id="adminSystemInstanceId" value="{{ system_instance_id }}" readonly class="flex-1 form-input font-mono text-sm bg-gray-100 dark:bg-gray-700">
<button type="button" onclick="copyAdminSystemId(this)" class="px-3 py-2 bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 rounded-md text-sm font-medium transition">
<i class="fas fa-copy mr-1"></i>{{ _('Copy') }}
</button>
</div>
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">{{ _('Use this ID when purchasing your key.') }}</p>
</div>
{% endif %}
{% if settings.donate_ui_hidden %}
<div class="p-4 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800">
<p class="text-sm font-medium text-green-800 dark:text-green-200">
<i class="fas fa-check-circle mr-2"></i>{{ _('Supporter instance: prompts are minimized; support entry points remain available.') }}
</p>
</div>
{% else %}
<div class="flex flex-wrap items-end gap-2">
<div class="flex-1 min-w-[200px]">
<label for="adminDonateHideCode" class="form-label">{{ _('Step 3: Paste code') }}</label>
<input type="text" id="adminDonateHideCode" placeholder="{{ _('Enter code from email') }}" class="form-input w-full">
</div>
<button type="button" id="adminVerifyDonateHideCodeBtn" onclick="verifyAdminDonateHideCode()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium transition">
{{ _('Verify and hide for everyone') }}
</button>
</div>
<p id="adminDonateHideCodeMessage" class="mt-2 text-sm hidden"></p>
{% endif %}
</div>
<!-- Company Branding -->
<div id="section-branding" class="border-b border-border-light dark:border-border-dark pb-6">
<h2 class="text-lg font-semibold mb-4">{{ _('Company Branding') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="company_name" class="form-label">{{ _('Company Name') }}</label>
<input type="text" name="company_name" id="company_name" value="{{ settings.company_name }}" class="form-input">
</div>
<div>
<label for="company_email" class="form-label">Company Email</label>
<input type="email" name="company_email" id="company_email" value="{{ settings.company_email }}" class="form-input">
</div>
<div>
<label for="company_phone" class="form-label">Company Phone</label>
<input type="text" name="company_phone" id="company_phone" value="{{ settings.company_phone }}" class="form-input">
</div>
<div>
<label for="company_website" class="form-label">Company Website</label>
<input type="text" name="company_website" id="company_website" value="{{ settings.company_website }}" class="form-input">
</div>
<div class="md:col-span-2">
<label for="company_address" class="form-label">Company Address</label>
<textarea name="company_address" id="company_address" rows="3" class="form-input">{{ settings.company_address }}</textarea>
</div>
<div>
<label for="company_tax_id" class="form-label">Tax ID (optional)</label>
<input type="text" name="company_tax_id" id="company_tax_id" value="{{ settings.company_tax_id }}" class="form-input">
</div>
<div>
<label for="company_bank_info" class="form-label">Bank Information (optional)</label>
<textarea name="company_bank_info" id="company_bank_info" rows="3" class="form-input">{{ settings.company_bank_info }}</textarea>
</div>
</div>
</div>
<!-- Invoice Defaults -->
<div id="section-invoices" class="border-b border-border-light dark:border-border-dark pb-6">
<h2 class="text-lg font-semibold mb-4">{{ _('Invoice Defaults') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="invoice_prefix" class="form-label">Invoice Prefix</label>
<input type="text" name="invoice_prefix" id="invoice_prefix" value="{{ settings.invoice_prefix }}" class="form-input">
</div>
<div class="md:col-span-2">
<label for="invoice_number_pattern" class="form-label">Invoice Number Pattern</label>
<input type="text" name="invoice_number_pattern" id="invoice_number_pattern" value="{{ settings.invoice_number_pattern or '{PREFIX}-{YYYY}{MM}{DD}-{SEQ}' }}" class="form-input" placeholder="e.g. RE-{YYYY}-{SEQ}">
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
Available tokens: <code>{SEQ}</code>, <code>{YYYY}</code>, <code>{YY}</code>, <code>{MM}</code>, <code>{DD}</code>, <code>{PREFIX}</code>. Leave blank to generate sequence-only numbers.
</p>
</div>
<div>
<label for="invoice_start_number" class="form-label">Invoice Start Number</label>
<input type="number" name="invoice_start_number" id="invoice_start_number" value="{{ settings.invoice_start_number }}" class="form-input">
</div>
<div class="md:col-span-2">
<label for="invoice_terms" class="form-label">Default Payment Terms</label>
<textarea name="invoice_terms" id="invoice_terms" rows="3" class="form-input">{{ settings.invoice_terms }}</textarea>
</div>
<div class="md:col-span-2">
<label for="invoice_notes" class="form-label">Default Invoice Notes</label>
<textarea name="invoice_notes" id="invoice_notes" rows="3" class="form-input">{{ settings.invoice_notes }}</textarea>
</div>
</div>
</div>
<!-- Peppol e-Invoicing -->
<div id="section-peppol" class="border-b border-border-light dark:border-border-dark pb-6">
<h2 class="text-lg font-semibold mb-4">Peppol e-Invoicing</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="md:col-span-2">
<label for="peppol_enabled_mode" class="form-label">Enable Peppol sending</label>
<select name="peppol_enabled_mode" id="peppol_enabled_mode" class="form-input">
<option value="env" {% if settings.peppol_enabled is none %}selected{% endif %}>
Use environment variable (PEPPOL_ENABLED) — currently {% if peppol_env_enabled|default(false) %}enabled{% else %}disabled{% endif %}
</option>
<option value="true" {% if settings.peppol_enabled is true %}selected{% endif %}>Enabled</option>
<option value="false" {% if settings.peppol_enabled is false %}selected{% endif %}>Disabled</option>
</select>
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
If enabled, invoices can be sent via Peppol using your configured access point.
</p>
</div>
<div class="md:col-span-2 flex items-start">
<input type="checkbox" name="invoices_peppol_compliant" id="invoices_peppol_compliant" {% if settings.invoices_peppol_compliant %}checked{% endif %} class="h-4 w-4 rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 mt-1">
<div class="ml-2">
<label for="invoices_peppol_compliant" class="form-label">Make all invoices PEPPOL compliant</label>
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
When enabled, PDFs include PEPPOL/EN 16931 identifiers (seller/buyer endpoint and VAT), and warnings are shown when required data is missing. UBL for Peppol includes mandatory elements.
</p>
</div>
</div>
<div class="md:col-span-2 flex items-start">
<input type="checkbox" name="invoices_zugferd_pdf" id="invoices_zugferd_pdf" {% if settings.invoices_zugferd_pdf %}checked{% endif %} class="h-4 w-4 rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 mt-1">
<div class="ml-2">
<label for="invoices_zugferd_pdf" class="form-label">Embed Factur-X / ZUGFeRD CII XML in invoice PDFs (EN 16931)</label>
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
When enabled, exported invoice PDFs contain an embedded CII (Cross-Industry Invoice) XML file (<code>factur-x.xml</code>) conforming to the Factur-X / ZUGFeRD EN 16931 profile. The PDF is both human-readable and machine-readable.
</p>
</div>
</div>
<div class="md:col-span-2 flex items-start">
<input type="checkbox" name="invoices_pdfa3_compliant" id="invoices_pdfa3_compliant" {% if getattr(settings, 'invoices_pdfa3_compliant', false) %}checked{% endif %} class="h-4 w-4 rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 mt-1">
<div class="ml-2">
<label for="invoices_pdfa3_compliant" class="form-label">Normalize Factur-X PDFs to PDF/A-3b</label>
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
When enabled with Factur-X embedding, exported PDFs include PDF/A-3b identification, an embedded sRGB ICC profile, and output intent metadata for validator compliance (e.g. veraPDF).
</p>
</div>
</div>
<div class="md:col-span-2 flex items-start">
<input type="checkbox" name="invoices_validate_export" id="invoices_validate_export" {% if getattr(settings, 'invoices_validate_export', false) %}checked{% endif %} class="h-4 w-4 rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 mt-1">
<div class="ml-2">
<label for="invoices_validate_export" class="form-label">Run veraPDF after export (optional)</label>
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
When enabled, runs veraPDF on exported ZUGFeRD PDFs and shows a summary. Does not block download. Set path below.
</p>
</div>
</div>
<div class="md:col-span-2">
<label for="invoices_verapdf_path" class="form-label">veraPDF executable path</label>
<input type="text" name="invoices_verapdf_path" id="invoices_verapdf_path" value="{{ getattr(settings, 'invoices_verapdf_path', '') or '' }}" class="form-input" placeholder="e.g. /usr/bin/verapdf or C:\verapdf\verapdf.exe">
</div>
<div>
<label for="peppol_sender_endpoint_id" class="form-label">Sender Endpoint ID</label>
<input type="text" name="peppol_sender_endpoint_id" id="peppol_sender_endpoint_id" value="{{ settings.peppol_sender_endpoint_id or '' }}" class="form-input" placeholder="e.g. 9915:BE0123456789">
</div>
<div>
<label for="peppol_sender_scheme_id" class="form-label">Sender Scheme ID</label>
<input type="text" name="peppol_sender_scheme_id" id="peppol_sender_scheme_id" value="{{ settings.peppol_sender_scheme_id or '' }}" class="form-input" placeholder="e.g. 9915">
</div>
<div>
<label for="peppol_sender_country" class="form-label">Sender Country (optional)</label>
<input type="text" name="peppol_sender_country" id="peppol_sender_country" value="{{ settings.peppol_sender_country or '' }}" class="form-input" placeholder="e.g. BE">
</div>
<div>
<label for="peppol_provider" class="form-label">Provider label</label>
<input type="text" name="peppol_provider" id="peppol_provider" value="{{ settings.peppol_provider or 'generic' }}" class="form-input">
</div>
<div class="md:col-span-2">
<label for="peppol_transport_mode" class="form-label">Transport mode</label>
<select name="peppol_transport_mode" id="peppol_transport_mode" class="form-input">
<option value="generic" {% if (getattr(settings, 'peppol_transport_mode', '') or 'generic') == 'generic' %}selected{% endif %}>Generic (HTTP JSON access point)</option>
<option value="native" {% if getattr(settings, 'peppol_transport_mode', '') == 'native' %}selected{% endif %}>Native (SML/SMP + AS4) — experimental</option>
</select>
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
Generic: use your access point adapter URL. Native (experimental): discover recipient via SML/SMP and send AS4 — lacks WS-Security and receipt handling; use a Peppol AP provider for production.
</p>
</div>
<div id="peppol_native_fields" class="md:col-span-2 space-y-4 {% if getattr(settings, 'peppol_transport_mode', '') != 'native' %}hidden{% endif %}">
<div>
<label for="peppol_sml_url" class="form-label">SML URL (required for native)</label>
<input type="text" name="peppol_sml_url" id="peppol_sml_url" value="{{ getattr(settings, 'peppol_sml_url', '') or '' }}" class="form-input" placeholder="https://edelivery.tech.ec.europa.eu/edelivery-sml">
</div>
<div>
<label for="peppol_native_cert_path" class="form-label">Client certificate path (optional)</label>
<input type="text" name="peppol_native_cert_path" id="peppol_native_cert_path" value="{{ getattr(settings, 'peppol_native_cert_path', '') or '' }}" class="form-input" placeholder="/path/to/cert.pem">
</div>
<div>
<label for="peppol_native_key_path" class="form-label">Client key path (optional)</label>
<input type="text" name="peppol_native_key_path" id="peppol_native_key_path" value="{{ getattr(settings, 'peppol_native_key_path', '') or '' }}" class="form-input" placeholder="/path/to/key.pem">
</div>
</div>
<div class="md:col-span-2 peppol-generic-url">
<label for="peppol_access_point_url" class="form-label">Access Point URL (generic mode)</label>
<input type="text" name="peppol_access_point_url" id="peppol_access_point_url" value="{{ settings.peppol_access_point_url or '' }}" class="form-input" placeholder="https://your-access-point-adapter.example.com/send">
</div>
<div>
<label for="peppol_access_point_timeout" class="form-label">Access Point Timeout (seconds)</label>
<input type="number" name="peppol_access_point_timeout" id="peppol_access_point_timeout" value="{{ settings.peppol_access_point_timeout or 30 }}" class="form-input" min="1" max="300">
</div>
<div>
<label for="peppol_access_point_token" class="form-label">Access Point Token (optional)</label>
<input type="password" name="peppol_access_point_token" id="peppol_access_point_token" value="" class="form-input" placeholder="{% if settings.peppol_access_point_token %}Token is set (enter to replace){% else %}Enter token (optional){% endif %}">
<p class="mt-1 text-xs text-text-muted-light dark:text-text-muted-dark">
For security, the token is not shown. Leave blank to keep the current token.
</p>
</div>
</div>
</div>
<!-- Backup Settings -->
<div id="section-backup" class="border-b border-border-light dark:border-border-dark pb-6">
<h2 class="text-lg font-semibold mb-4">{{ _('Backup Settings') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="backup_retention_days" class="form-label">Backup Retention (Days)</label>
<input type="number" name="backup_retention_days" id="backup_retention_days" value="{{ settings.backup_retention_days }}" class="form-input">
</div>
<div>
<label for="backup_time" class="form-label">Backup Time (HH:MM)</label>
<input type="text" name="backup_time" id="backup_time" value="{{ settings.backup_time }}" placeholder="02:00" class="form-input">
</div>
</div>
</div>
<!-- Export Settings -->
<div class="border-b border-border-light dark:border-border-dark pb-6">
<h2 class="text-lg font-semibold mb-4">{{ _('Export Settings') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="export_delimiter" class="form-label">CSV Export Delimiter</label>
<select name="export_delimiter" id="export_delimiter" class="form-input">
<option value="," {% if settings.export_delimiter == ',' %}selected{% endif %}>Comma (,)</option>
<option value=";" {% if settings.export_delimiter == ';' %}selected{% endif %}>Semicolon (;)</option>
<option value="\t" {% if settings.export_delimiter == '\t' %}selected{% endif %}>Tab</option>
</select>
</div>
</div>
</div>
<!-- Kiosk Mode Settings -->
<div id="section-kiosk" class="border-b border-border-light dark:border-border-dark pb-6">
<h2 class="text-lg font-semibold mb-4">{{ _('Kiosk Mode') }}</h2>
<div class="space-y-4">
<div class="flex items-center">
<input type="checkbox" name="kiosk_mode_enabled" id="kiosk_mode_enabled" {% if kiosk_settings.kiosk_mode_enabled %}checked{% endif %} class="h-4 w-4 rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<label for="kiosk_mode_enabled" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">
{{ _('Enable Kiosk Mode') }}
</label>
</div>
<p class="ml-6 text-xs text-text-muted-light dark:text-text-muted-dark">
{{ _('Kiosk mode provides a simplified interface for warehouse operations with barcode scanning and time tracking. Access at') }} <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 py-0.5 rounded">/kiosk/login</code>
</p>
{% if kiosk_settings.kiosk_mode_enabled %}
<div class="ml-6 grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<label for="kiosk_auto_logout_minutes" class="form-label">{{ _('Auto-Logout Timeout (Minutes)') }}</label>
<input type="number" name="kiosk_auto_logout_minutes" id="kiosk_auto_logout_minutes" value="{{ kiosk_settings.kiosk_auto_logout_minutes }}" min="1" max="60" class="form-input">
</div>
<div>
<label for="kiosk_default_movement_type" class="form-label">{{ _('Default Movement Type') }}</label>
<select name="kiosk_default_movement_type" id="kiosk_default_movement_type" class="form-input">
<option value="adjustment" {% if kiosk_settings.kiosk_default_movement_type == 'adjustment' %}selected{% endif %}>{{ _('Adjustment') }}</option>
<option value="transfer" {% if kiosk_settings.kiosk_default_movement_type == 'transfer' %}selected{% endif %}>{{ _('Transfer') }}</option>
<option value="sale" {% if kiosk_settings.kiosk_default_movement_type == 'sale' %}selected{% endif %}>{{ _('Sale') }}</option>
<option value="rent" {% if kiosk_settings.kiosk_default_movement_type == 'rent' %}selected{% endif %}>{{ _('Rent') }}</option>
<option value="purchase" {% if kiosk_settings.kiosk_default_movement_type == 'purchase' %}selected{% endif %}>{{ _('Purchase') }}</option>
</select>
</div>
<div class="flex items-center">
<input type="checkbox" name="kiosk_allow_camera_scanning" id="kiosk_allow_camera_scanning" {% if kiosk_settings.kiosk_allow_camera_scanning %}checked{% endif %} class="h-4 w-4 rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<label for="kiosk_allow_camera_scanning" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">
{{ _('Allow Camera-Based Barcode Scanning') }}
</label>
</div>
<div class="flex items-center">
<input type="checkbox" name="kiosk_require_reason_for_adjustments" id="kiosk_require_reason_for_adjustments" {% if kiosk_settings.kiosk_require_reason_for_adjustments %}checked{% endif %} class="h-4 w-4 rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<label for="kiosk_require_reason_for_adjustments" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">
{{ _('Require Reason for Stock Adjustments') }}
</label>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Analytics Settings -->
<div id="section-analytics">
<h2 class="text-lg font-semibold mb-4">{{ _('Privacy & Analytics') }}</h2>
<div class="space-y-4">
<p class="text-sm text-text-muted-light dark:text-text-muted-dark">
<strong>Minimal install telemetry (always on):</strong> Version, platform, and last-seen heartbeat so we can understand install footprint and distribution. No personal data.
</p>
<div class="flex items-center">
<input type="checkbox" name="allow_analytics" id="allow_analytics" {% if settings.allow_analytics %}checked{% endif %} class="h-4 w-4 rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<label for="allow_analytics" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">Enable optional detailed analytics</label>
</div>
<div class="ml-6 text-xs text-text-muted-light dark:text-text-muted-dark space-y-1">
<p>When enabled, we also collect:</p>
<ul class="list-disc ml-4 space-y-0.5">
<li>Feature usage (e.g. timer started, project created)</li>
<li>Screens and pages visited</li>
<li>Errors linked to usage context (no PII)</li>
</ul>
<p class="mt-2"><strong>Privacy:</strong> No email, usernames, time entry content, or client data. You can turn this off anytime.</p>
<p>Same setting as the telemetry preference during initial setup.</p>
</div>
</div>
</div>
</div>
<div class="mt-8 pt-6 flex justify-end sticky bottom-0 bg-card-light dark:bg-card-dark py-4 z-10">
<button type="submit" class="bg-primary text-white px-4 py-2 rounded-lg">{{ _('Save Settings') }}</button>
</div>
</form>
</div>
<!-- Company Logo Upload - Separate Section -->
<div class="bg-card-light dark:bg-card-dark p-6 rounded-xl border border-border-light dark:border-border-dark shadow-sm">
<h2 class="text-lg font-semibold mb-4">{{ _('Company Logo') }}</h2>
<!-- Current Logo Display -->
{% if settings.has_logo() %}
<div class="mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border-2 border-green-500 dark:border-green-600">
<div class="flex items-start justify-between mb-3">
<div>
<h3 class="text-sm font-semibold text-green-700 dark:text-green-400 mb-1">✓ Current Company Logo</h3>
<p class="text-xs text-gray-600 dark:text-gray-400">This logo appears on invoices, PDFs, and other documents</p>
</div>
<form id="removeLogoForm" method="POST" action="{{ url_for('admin.remove_logo') }}" class="inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="button" class="text-sm text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 font-medium" onclick="confirmRemoveLogo()">
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
Remove Logo
</button>
</form>
</div>
<div class="flex items-center justify-center bg-white dark:bg-gray-900 p-6 rounded border border-gray-200 dark:border-gray-700">
<img src="{{ settings.get_logo_url() }}?v={{ range(1, 10000) | random }}" alt="{{ _('Company Logo') }}" class="max-h-32 max-w-full object-contain" onerror="this.onerror=null; this.parentElement.innerHTML='<p class=\'text-red-600 text-sm\'>Error loading logo</p>';">
</div>
</div>
{% else %}
<div class="mb-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600">
<div class="text-center py-4">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">No company logo uploaded yet</p>
<p class="text-xs text-gray-500 dark:text-gray-500 mt-1">Upload a logo to appear on invoices, PDFs, and documents</p>
</div>
</div>
{% endif %}
<!-- Upload Form -->
<div class="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<h3 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-3">{{ _('Upload New Logo') }}</h3>
<form method="POST" action="{{ url_for('admin.upload_logo') }}" enctype="multipart/form-data" id="logoUploadForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="space-y-3">
<div>
<input type="file" name="logo" id="logoFileInput" accept="image/png,image/jpeg,image/jpg,image/gif,image/svg+xml,image/webp" required
class="block w-full text-sm text-gray-900 dark:text-gray-300 bg-card-light dark:bg-card-dark border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-500"
onchange="previewLogoBeforeUpload(this)">
</div>
<!-- Preview of selected file before upload -->
<div id="logoPreview" class="hidden bg-white dark:bg-gray-900 p-4 rounded border border-gray-200 dark:border-gray-700">
<p class="text-xs text-gray-600 dark:text-gray-400 mb-2">Preview:</p>
<div class="flex items-center justify-center">
<img id="logoPreviewImage" src="" alt="{{ _('Logo Preview') }}" class="max-h-24 max-w-full object-contain">
</div>
</div>
<div class="flex items-center gap-3">
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium transition-colors">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
Upload Logo
</button>
<button type="button" onclick="document.getElementById('logoFileInput').value = ''; document.getElementById('logoPreview').classList.add('hidden');"
class="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 px-4 py-2 text-sm">
Clear
</button>
</div>
</div>
</form>
<div class="mt-3 text-xs text-gray-600 dark:text-gray-400 space-y-1">
<p><strong>Allowed formats:</strong> PNG, JPG, GIF, SVG, WEBP (max 5MB)</p>
<p><strong>Recommended:</strong> Square or landscape logo, at least 200x200 pixels</p>
<p><strong>Where it appears:</strong> PDF invoices, email templates, and exported documents</p>
</div>
</div>
</div>
<style>
</style>
<script>
function previewLogoBeforeUpload(input) {
const preview = document.getElementById('logoPreview');
const previewImage = document.getElementById('logoPreviewImage');
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
previewImage.src = e.target.result;
preview.classList.remove('hidden');
};
reader.readAsDataURL(input.files[0]);
} else {
preview.classList.add('hidden');
}
}
async function confirmRemoveLogo() {
const confirmed = await showConfirm(
'{{ _("Are you sure you want to remove the company logo?") }}',
{
title: '{{ _("Remove Logo") }}',
confirmText: '{{ _("Remove") }}',
cancelText: '{{ _("Cancel") }}',
variant: 'danger'
}
);
if (confirmed) {
document.getElementById('removeLogoForm').submit();
}
}
function copyAdminSystemId(btn) {
const input = document.getElementById('adminSystemInstanceId');
if (input) {
input.select();
document.execCommand('copy');
const orig = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check mr-1"></i>{{ _("Copied") }}';
setTimeout(function() { btn.innerHTML = orig; }, 2000);
}
}
async function verifyAdminDonateHideCode() {
const codeInput = document.getElementById('adminDonateHideCode');
const msgEl = document.getElementById('adminDonateHideCodeMessage');
const btn = document.getElementById('adminVerifyDonateHideCodeBtn');
if (!codeInput || !msgEl || !btn) return;
msgEl.classList.add('hidden');
msgEl.textContent = '';
const code = (codeInput.value || '').trim();
if (!code) {
msgEl.textContent = '{{ _("Please enter a code.") }}';
msgEl.classList.remove('hidden');
msgEl.classList.add('text-amber-600', 'dark:text-amber-400');
return;
}
btn.disabled = true;
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
try {
const res = await fetch('{{ url_for("admin.admin_verify_donate_hide_code") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({ code: code })
});
const data = await res.json().catch(function() { return {}; });
if (res.ok && data.success) {
msgEl.textContent = '{{ _("Success. Donate UI is now hidden for everyone. Reload the page to see the change.") }}';
msgEl.classList.remove('text-amber-600', 'dark:text-amber-400', 'text-red-600', 'dark:text-red-400');
msgEl.classList.add('text-green-600', 'dark:text-green-400');
msgEl.classList.remove('hidden');
setTimeout(function() { window.location.reload(); }, 1500);
} else {
msgEl.textContent = data.error || '{{ _("Invalid code.") }}';
msgEl.classList.remove('text-green-600', 'dark:text-green-400');
msgEl.classList.add('text-red-600', 'dark:text-red-400');
msgEl.classList.remove('hidden');
}
} catch (e) {
msgEl.textContent = '{{ _("Request failed.") }}';
msgEl.classList.remove('text-green-600', 'dark:text-green-400');
msgEl.classList.add('text-red-600', 'dark:text-red-400');
msgEl.classList.remove('hidden');
}
btn.disabled = false;
}
// Initialize: keep collapsed by default; toggle time entry min length visibility
document.addEventListener('DOMContentLoaded', function() {
const content = document.getElementById('integrationCredentialsContent');
const toggleIcon = document.getElementById('integrationCredentialsToggleIcon');
if (content && toggleIcon) {
// Ensure it starts collapsed
content.classList.add('integration-credentials-collapsed');
toggleIcon.classList.remove('fa-chevron-up');
toggleIcon.classList.add('fa-chevron-down');
}
const descCheck = document.getElementById('time_entry_require_description');
const minLenRow = document.getElementById('time_entry_min_length_row');
if (descCheck && minLenRow) {
descCheck.addEventListener('change', function() {
minLenRow.classList.toggle('hidden', !descCheck.checked);
});
}
const transportMode = document.getElementById('peppol_transport_mode');
const nativeFields = document.getElementById('peppol_native_fields');
if (transportMode && nativeFields) {
function togglePeppolTransport() {
const isNative = transportMode.value === 'native';
nativeFields.classList.toggle('hidden', !isNative);
}
transportMode.addEventListener('change', togglePeppolTransport);
togglePeppolTransport();
}
});
</script>
{% endblock %}