Files
IT_Ticket_System/app/templates/admin/settings.html
T
2026-05-22 11:53:41 -04:00

477 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}System Settings — {{ branding.app_name }}{% endblock %}
{% block content %}
<div class="page-header" style="margin-bottom:28px;">
<div>
<h1 class="page-title">System Settings</h1>
<p class="page-subtitle">Manage application-wide configuration and branding.</p>
</div>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show" role="alert">
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endwith %}
<!-- Timezone -->
<div class="card mb-4" style="max-width:680px;">
<div class="card-header" style="padding:16px 20px;border-bottom:1px solid var(--border);">
<h5 class="mb-0" style="font-size:15px;font-weight:600;">
<i class="bi bi-clock me-2"></i>Date &amp; Time
</h5>
</div>
<div class="card-body" style="padding:20px;">
<p style="font-size:13px;color:var(--muted);margin-bottom:20px;">
All timestamps are stored in UTC internally. This setting controls how they are
displayed throughout the application — tickets, comments, history, and logs.
</p>
<form method="POST" action="{{ url_for('admin.settings') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="form_type" value="timezone">
<div class="mb-3">
<label class="form-label">Display Timezone</label>
<select class="form-select" name="app_timezone" style="max-width:400px;">
{% for tz_value, tz_label in common_timezones %}
<option value="{{ tz_value }}" {% if tz_value == current_tz %}selected{% endif %}>
{{ tz_label }}
</option>
{% endfor %}
{% if current_tz not in common_timezones|map(attribute=0)|list %}
<option value="{{ current_tz }}" selected>{{ current_tz }}</option>
{% endif %}
</select>
<div class="form-text">
Current setting: <strong>{{ current_tz }}</strong>
local time is <strong>{{ now_local.strftime('%b %d, %Y %H:%M %Z') }}</strong>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check2 me-2"></i>Save Timezone
</button>
</form>
</div>
</div>
<!-- Branding -->
<div class="card mb-4" style="max-width:680px;">
<div class="card-header" style="padding:16px 20px;border-bottom:1px solid var(--border);">
<h5 class="mb-0" style="font-size:15px;font-weight:600;">
<i class="bi bi-palette me-2"></i>Branding &amp; Appearance
</h5>
</div>
<div class="card-body" style="padding:20px;">
<form method="POST" action="{{ url_for('admin.settings') }}" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="form_type" value="branding">
<input type="hidden" name="remove_logo" id="remove-logo-input" value="0">
<!-- Logo preview + upload -->
<div class="mb-4 d-flex align-items-center gap-4">
<div id="logo-preview-wrap"
style="width:64px;height:64px;border-radius:12px;background:{{ branding_settings.primary_color }};display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.12);">
{% if branding_settings.logo_stored_name %}
<img id="logo-preview-img"
src="{{ url_for('auth.serve_logo', filename=branding_settings.logo_stored_name) }}"
alt="Logo" style="width:64px;height:64px;object-fit:contain;padding:4px;"/>
{% else %}
<span id="logo-preview-initials"
style="font-family:'Space Mono',monospace;font-weight:700;font-size:18px;color:#fff;">
{{ branding_settings.logo_initials[:2] }}
</span>
{% endif %}
</div>
<div style="flex:1;">
<label class="form-label" style="font-size:12px;font-weight:600;">
Company Logo
<span style="color:var(--muted);font-weight:400;">(PNG, JPG, GIF, WebP — max 2 MB)</span>
</label>
<input type="file" class="form-control form-control-sm" name="logo_file"
id="logo-file-input" accept=".png,.jpg,.jpeg,.gif,.webp"/>
{% if branding_settings.logo_stored_name %}
<div class="mt-2" id="remove-logo-row">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeLogo()">
<i class="bi bi-trash me-1"></i>Remove current logo
</button>
</div>
{% endif %}
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Application Name</label>
<input type="text" class="form-control" name="app_name"
value="{{ branding_settings.app_name }}" maxlength="60" required/>
<div class="form-text">Shown in the sidebar and browser tab.</div>
</div>
<div class="col-md-6">
<label class="form-label">App Subtitle</label>
<input type="text" class="form-control" name="app_subtitle"
value="{{ branding_settings.app_subtitle }}" maxlength="80"/>
<div class="form-text">Shown below the app name in the sidebar.</div>
</div>
<div class="col-md-6">
<label class="form-label">Company Name</label>
<input type="text" class="form-control" name="company_name"
value="{{ branding_settings.company_name }}" maxlength="100"/>
<div class="form-text">Shown on the login and register pages.</div>
</div>
<div class="col-md-3">
<label class="form-label">Logo Initials</label>
<input type="text" class="form-control" name="logo_initials" id="logo-initials"
value="{{ branding_settings.logo_initials }}" maxlength="3"
oninput="updateInitialsPreview(this.value)"/>
<div class="form-text">Fallback when no logo is uploaded.</div>
</div>
<div class="col-md-3">
<label class="form-label">Primary Colour</label>
<div class="d-flex gap-2 align-items-center">
<input type="color" class="form-control form-control-color" name="primary_color"
id="primary-color-input"
value="{{ branding_settings.primary_color }}"
oninput="updateColorPreview(this.value)"
style="width:48px;height:38px;padding:2px;cursor:pointer;"/>
<input type="text" class="form-control form-control-sm" id="primary-color-text"
value="{{ branding_settings.primary_color }}" maxlength="7"
style="font-family:'Space Mono',monospace;font-size:12px;"
oninput="syncColorPicker(this.value)"/>
</div>
<div class="form-text">Accent colour across the interface.</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check2 me-2"></i>Save Branding
</button>
</form>
</div>
</div>
<!-- Email Ingestion -->
<div class="card mb-4" style="max-width:680px;">
<div class="card-header" style="padding:16px 20px;border-bottom:1px solid var(--border);">
<h5 class="mb-0" style="font-size:15px;font-weight:600;">
<i class="bi bi-envelope-arrow-down me-2"></i>Email-to-Ticket Ingestion
</h5>
</div>
<div class="card-body" style="padding:20px;">
<p style="font-size:13px;color:var(--muted);margin-bottom:20px;">
When enabled, TechDesk polls a dedicated IMAP mailbox and automatically converts
inbound emails into tickets. Use a dedicated support inbox with an app-specific password.
</p>
<form method="POST" action="{{ url_for('admin.settings') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="form_type" value="email_ingestion">
<!-- Enable toggle -->
<div class="d-flex align-items-center gap-3 mb-4 p-3"
style="border:1px solid var(--border);border-radius:10px;background:var(--surface);">
<div style="flex:1;">
<div style="font-weight:600;font-size:14px;">
<i class="bi bi-power me-2"></i>Enable Email Ingestion
</div>
<div style="font-size:12px;color:var(--muted);margin-top:3px;">
Start polling the mailbox below for new tickets.
</div>
</div>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" role="switch"
id="email-ingestion-toggle" name="email_ingestion_enabled" value="1"
{% if email_settings.enabled %}checked{% endif %}
style="width:40px;height:22px;cursor:pointer;"/>
</div>
</div>
<div class="row g-3">
<div class="col-md-8">
<label class="form-label">IMAP Host</label>
<input type="text" class="form-control" name="email_ingestion_host"
value="{{ email_settings.host }}" placeholder="e.g. imap.gmail.com"/>
</div>
<div class="col-md-4">
<label class="form-label">Port</label>
<input type="number" class="form-control" name="email_ingestion_port"
value="{{ email_settings.port }}" placeholder="993"/>
</div>
<div class="col-md-6">
<label class="form-label">Mailbox Username / Email</label>
<input type="text" class="form-control" name="email_ingestion_user"
value="{{ email_settings.user }}" placeholder="support@yourcompany.com"/>
</div>
<div class="col-md-6">
<label class="form-label">Password
<span style="font-size:11px;color:var(--muted);font-weight:400;">
— use an app-specific password
</span>
</label>
<input type="password" class="form-control" name="email_ingestion_password"
value="{{ email_settings.password }}" placeholder="Leave blank to keep current"/>
</div>
<div class="col-md-4">
<label class="form-label">Watch Folder</label>
<input type="text" class="form-control" name="email_ingestion_folder"
value="{{ email_settings.folder }}" placeholder="INBOX"/>
</div>
<div class="col-md-4">
<label class="form-label">Processed Folder</label>
<input type="text" class="form-control" name="email_ingestion_move_to"
value="{{ email_settings.move_to }}" placeholder="Processed"/>
</div>
<div class="col-md-4">
<label class="form-label">Poll Interval (minutes)</label>
<input type="number" class="form-control" name="email_ingestion_interval"
value="{{ email_settings.interval }}" min="1" max="60" placeholder="5"/>
</div>
</div>
<div class="alert alert-info mt-3 mb-0" style="font-size:12px;">
<i class="bi bi-info-circle me-2"></i>
<strong>Note:</strong> For Gmail, enable IMAP in Settings → Forwarding and POP/IMAP,
and use an App Password (Google Account → Security → 2-Step Verification → App passwords).
For other providers, ensure IMAP is enabled and check their specific settings.
</div>
<div class="d-flex align-items-center gap-2 flex-wrap mt-3">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check2 me-2"></i>Save Email Settings
</button>
<button type="button" class="btn btn-outline-secondary" id="btn-test-ingest"
onclick="emailIngestTest()">
<i class="bi bi-plug me-2"></i>Test Connection
</button>
<button type="button" class="btn btn-outline-primary" id="btn-run-ingest"
onclick="emailIngestRunNow()">
<i class="bi bi-play-circle me-2"></i>Run Now
</button>
</div>
<div id="ingest-result" class="mt-3" style="display:none;"></div>
</form>
</div>
</div>
<script>
(function () {
var csrfToken = document.querySelector('meta[name="csrf-token"]').content;
function setResult(ok, message) {
var el = document.getElementById('ingest-result');
el.style.display = '';
el.className = ok
? 'alert alert-success py-2 px-3'
: 'alert alert-danger py-2 px-3';
el.style.fontSize = '13px';
el.innerHTML = '<i class="bi bi-' + (ok ? 'check-circle' : 'x-circle') + ' me-2"></i>' +
message.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function collectFields() {
return {
host : document.querySelector('[name="email_ingestion_host"]').value.trim(),
port : document.querySelector('[name="email_ingestion_port"]').value.trim(),
user : document.querySelector('[name="email_ingestion_user"]').value.trim(),
password: document.querySelector('[name="email_ingestion_password"]').value.trim(),
folder : document.querySelector('[name="email_ingestion_folder"]').value.trim(),
};
}
window.emailIngestTest = function () {
var btn = document.getElementById('btn-test-ingest');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Testing…';
var fields = collectFields();
var body = new URLSearchParams(fields);
body.append('csrf_token', csrfToken);
fetch('{{ url_for("admin.email_ingestion_test") }}', {
method : 'POST',
headers: { 'X-CSRFToken': csrfToken, 'Content-Type': 'application/x-www-form-urlencoded' },
body : body.toString(),
})
.then(function (r) { return r.json(); })
.then(function (data) {
setResult(data.ok, data.message);
})
.catch(function (err) {
setResult(false, 'Request failed: ' + err);
})
.finally(function () {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-plug me-2"></i>Test Connection';
});
};
window.emailIngestRunNow = function () {
var btn = document.getElementById('btn-run-ingest');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Running…';
var body = new URLSearchParams({ csrf_token: csrfToken });
fetch('{{ url_for("admin.email_ingestion_run_now") }}', {
method : 'POST',
headers: { 'X-CSRFToken': csrfToken, 'Content-Type': 'application/x-www-form-urlencoded' },
body : body.toString(),
})
.then(function (r) { return r.json(); })
.then(function (data) {
setResult(data.ok, data.message);
})
.catch(function (err) {
setResult(false, 'Request failed: ' + err);
})
.finally(function () {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-play-circle me-2"></i>Run Now';
});
};
})();
</script>
<!-- Satisfaction Survey -->
<div class="card mb-4" style="max-width:680px;">
<div class="card-header" style="padding:16px 20px;border-bottom:1px solid var(--border);">
<h5 class="mb-0" style="font-size:15px;font-weight:600;">
<i class="bi bi-star-half me-2"></i>Satisfaction Survey
</h5>
</div>
<div class="card-body" style="padding:20px;">
<p style="font-size:13px;color:var(--muted);margin-bottom:20px;">
When enabled, employees automatically receive a one-click satisfaction survey email
whenever their ticket is marked as Resolved. Responses appear in the
<a href="{{ url_for('admin.satisfaction_report') }}" style="color:var(--accent3);">Satisfaction Report</a>.
</p>
<form method="POST" action="{{ url_for('admin.settings') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="form_type" value="survey">
<div class="d-flex align-items-center justify-content-between p-3"
style="border:1px solid var(--border);border-radius:10px;background:var(--surface);">
<div>
<div style="font-weight:600;font-size:14px;">
<i class="bi bi-envelope-heart me-2"></i>Send Survey on Resolve
</div>
<div style="font-size:12px;color:var(--muted);margin-top:3px;">
Automatically email employees a 15 star rating prompt when their ticket is resolved.
</div>
</div>
<div class="d-flex align-items-center gap-3" style="flex-shrink:0;margin-left:16px;">
<span class="badge {% if survey_enabled %}bg-success{% else %}bg-secondary{% endif %}"
style="font-size:11px;padding:5px 10px;">
{{ 'Enabled' if survey_enabled else 'Disabled' }}
</span>
{% if survey_enabled %}
<button type="submit" name="survey_enabled" value="0"
class="btn btn-sm btn-outline-danger"
data-confirm="Disable satisfaction surveys? No survey emails will be sent when tickets are resolved."
data-confirm-title="Disable Satisfaction Surveys" data-confirm-ok="Disable">
<i class="bi bi-toggle-on me-1"></i>Disable
</button>
{% else %}
<button type="submit" name="survey_enabled" value="1"
class="btn btn-sm btn-outline-success">
<i class="bi bi-toggle-off me-1"></i>Enable
</button>
{% endif %}
</div>
</div>
</form>
</div>
</div>
<!-- Registration -->
<div class="card" style="max-width:680px;">
<div class="card-header" style="padding:16px 20px;border-bottom:1px solid var(--border);">
<h5 class="mb-0" style="font-size:15px;font-weight:600;">
<i class="bi bi-person-gear me-2"></i>User Registration
</h5>
</div>
<div class="card-body" style="padding:20px;">
<p style="font-size:13px;color:var(--muted);margin-bottom:20px;">
When disabled, the <code>/auth/register</code> endpoint redirects all visitors to the login
page. Existing accounts and admin-created accounts are unaffected.
</p>
<form method="POST" action="{{ url_for('admin.settings') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="form_type" value="registration">
<div class="d-flex align-items-center justify-content-between p-3"
style="border:1px solid var(--border);border-radius:10px;background:var(--surface);">
<div>
<div style="font-weight:600;font-size:14px;">
<i class="bi bi-person-plus me-2"></i>Allow Self-Registration
</div>
<div style="font-size:12px;color:var(--muted);margin-top:3px;">
Permits new employees to create their own accounts via the Register page.
</div>
</div>
<div class="d-flex align-items-center gap-3" style="flex-shrink:0;margin-left:16px;">
<span class="badge {% if registration_enabled %}bg-success{% else %}bg-secondary{% endif %}"
style="font-size:11px;padding:5px 10px;">
{{ 'Enabled' if registration_enabled else 'Disabled' }}
</span>
{% if registration_enabled %}
<button type="submit" name="registration_enabled" value="0"
class="btn btn-sm btn-outline-danger"
data-confirm="Disable self-registration? New users will not be able to sign up independently."
data-confirm-title="Disable Self-Registration" data-confirm-ok="Disable">
<i class="bi bi-toggle-on me-1"></i>Disable
</button>
{% else %}
<button type="submit" name="registration_enabled" value="1"
class="btn btn-sm btn-outline-success">
<i class="bi bi-toggle-off me-1"></i>Enable
</button>
{% endif %}
</div>
</div>
</form>
</div>
</div>
<script>
document.getElementById('logo-file-input').addEventListener('change', function() {
const file = this.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
document.getElementById('logo-preview-wrap').innerHTML =
`<img src="${e.target.result}" style="width:64px;height:64px;object-fit:contain;padding:4px;"/>`;
};
reader.readAsDataURL(file);
});
function updateInitialsPreview(val) {
const el = document.getElementById('logo-preview-initials');
if (el) el.textContent = val.substring(0, 2).toUpperCase();
}
function updateColorPreview(hex) {
document.getElementById('primary-color-text').value = hex;
document.getElementById('logo-preview-wrap').style.background = hex;
}
function syncColorPicker(val) {
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
document.getElementById('primary-color-input').value = val;
document.getElementById('logo-preview-wrap').style.background = val;
}
}
function removeLogo() {
confirmModal('Remove Logo', 'Remove the current logo? The initials fallback will be displayed instead.', 'Remove', 'btn-danger', () => {
document.getElementById('remove-logo-input').value = '1';
const initials = document.getElementById('logo-initials').value.substring(0, 2).toUpperCase();
document.getElementById('logo-preview-wrap').innerHTML =
`<span id="logo-preview-initials" style="font-family:'Space Mono',monospace;font-weight:700;font-size:18px;color:#fff;">${initials}</span>`;
const row = document.getElementById('remove-logo-row');
if (row) row.style.display = 'none';
});
}
</script>
{% endblock %}