04/07 updated timezone, report via email, etc
This commit is contained in:
@@ -65,7 +65,7 @@
|
||||
{% if articles %}
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr><th>Title</th><th>Category</th><th>Author</th><th>Published</th><th>Views</th><th>Updated</th><th>Actions</th></tr>
|
||||
<tr><th>Title</th><th>Category</th><th>Author</th><th>Published</th><th>Views</th><th style="white-space:nowrap;">👍 / 👎</th><th>Updated</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for art in articles %}
|
||||
@@ -81,6 +81,13 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ art.view_count }}</td>
|
||||
<td style="font-size:12px;white-space:nowrap;">
|
||||
{% set hc = art.feedback.filter_by(is_helpful=True).count() %}
|
||||
{% set nc = art.feedback.filter_by(is_helpful=False).count() %}
|
||||
<span style="color:var(--success);">👍 {{ hc }}</span>
|
||||
<span style="color:var(--muted);margin:0 3px;">/</span>
|
||||
<span style="color:var(--danger);">👎 {{ nc }}</span>
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ art.updated_at.strftime('%b %d, %Y') }}</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1 align-items-center">
|
||||
|
||||
@@ -18,6 +18,45 @@
|
||||
{% 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 & 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);">
|
||||
@@ -113,6 +152,98 @@
|
||||
</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>
|
||||
|
||||
<button type="submit" class="btn btn-primary mt-3">
|
||||
<i class="bi bi-check2 me-2"></i>Save Email Settings
|
||||
</button>
|
||||
</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);">
|
||||
|
||||
@@ -65,6 +65,10 @@
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:36px;">
|
||||
<input type="checkbox" id="select-all" title="Select all"
|
||||
style="accent-color:var(--accent);width:15px;height:15px;cursor:pointer;"/>
|
||||
</th>
|
||||
<th>Ticket #</th>
|
||||
<th>Title</th>
|
||||
<th>Category</th>
|
||||
@@ -79,6 +83,10 @@
|
||||
<tbody>
|
||||
{% for t in tickets.items %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" class="ticket-cb" value="{{ t.id }}"
|
||||
style="accent-color:var(--accent);width:15px;height:15px;cursor:pointer;"/>
|
||||
</td>
|
||||
<td><a href="{{ url_for('tickets.ticket_detail', ticket_id=t.id) }}" class="mono" style="font-size:11px;color:var(--accent3);">{{ t.ticket_number }}</a></td>
|
||||
<td style="font-size:13px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ t.title }}</td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ t.category.replace('_',' ').title() }}</td>
|
||||
@@ -86,7 +94,7 @@
|
||||
<td><span class="badge badge-{{ t.priority }}">{{ t.priority.upper() }}</span></td>
|
||||
<td style="font-size:13px;">{{ t.creator.full_name }}</td>
|
||||
<td style="font-size:13px;color:var(--muted);">{{ t.assignee.full_name if t.assignee else '—' }}</td>
|
||||
<td style="font-size:11px;color:var(--muted);">{{ t.created_at.strftime('%b %d') }}</td>
|
||||
<td style="font-size:11px;color:var(--muted);">{{ t.created_at | localtime("%b %d") }}</td>
|
||||
<td><a href="{{ url_for('tickets.ticket_detail', ticket_id=t.id) }}" class="btn btn-secondary btn-sm"><i class="bi bi-eye"></i></a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -121,4 +129,101 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bulk action bar: fixed at bottom, hidden until checkboxes are ticked -->
|
||||
<!-- NOTE: display is set entirely via JS — no inline display property here -->
|
||||
<div id="bulk-action-bar"
|
||||
style="position:fixed;bottom:24px;left:50%;transform:translateX(-50%);
|
||||
background:#1e293b;color:#fff;border-radius:12px;padding:12px 20px;
|
||||
box-shadow:0 8px 32px rgba(0,0,0,.3);z-index:1000;
|
||||
align-items:center;gap:12px;min-width:420px;">
|
||||
<span id="bulk-count" style="font-size:13px;font-weight:600;white-space:nowrap;"></span>
|
||||
<div style="flex:1;"></div>
|
||||
<form method="POST" action="{{ url_for('admin.bulk_ticket_action') }}" id="bulk-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="status" value="{{ status }}"/>
|
||||
<input type="hidden" name="priority" value="{{ priority }}"/>
|
||||
<input type="hidden" name="assigned" value="{{ assigned }}"/>
|
||||
<input type="hidden" name="q" value="{{ search }}"/>
|
||||
<div id="bulk-hidden-ids"></div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" name="action" value="resolve"
|
||||
class="btn btn-sm" style="background:#059669;color:#fff;border:none;">
|
||||
<i class="bi bi-check2-circle me-1"></i>Resolve
|
||||
</button>
|
||||
<button type="submit" name="action" value="close"
|
||||
class="btn btn-sm" style="background:#6b7280;color:#fff;border:none;">
|
||||
<i class="bi bi-archive me-1"></i>Close
|
||||
</button>
|
||||
<button type="submit" name="action" value="assign_me"
|
||||
class="btn btn-sm" style="background:#2563eb;color:#fff;border:none;">
|
||||
<i class="bi bi-person-check me-1"></i>Assign to Me
|
||||
</button>
|
||||
<button type="submit" name="action" value="unassign"
|
||||
class="btn btn-sm" style="color:#fff;border:1px solid #6b7280;background:transparent;">
|
||||
<i class="bi bi-person-dash me-1"></i>Unassign
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<button type="button" onclick="clearSelection()"
|
||||
style="background:none;border:none;color:#9ca3af;font-size:20px;cursor:pointer;
|
||||
line-height:1;padding:0 0 0 4px;flex-shrink:0;" title="Clear selection">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Script is AFTER all DOM elements so querySelectorAll finds the checkboxes -->
|
||||
<script>
|
||||
(function () {
|
||||
const bar = document.getElementById('bulk-action-bar');
|
||||
const countEl = document.getElementById('bulk-count');
|
||||
const hiddenIds = document.getElementById('bulk-hidden-ids');
|
||||
const selectAll = document.getElementById('select-all');
|
||||
|
||||
// Start hidden — JS controls visibility entirely
|
||||
bar.style.display = 'none';
|
||||
|
||||
function getChecked() {
|
||||
return Array.from(document.querySelectorAll('.ticket-cb:checked'));
|
||||
}
|
||||
|
||||
function updateBar() {
|
||||
const checked = getChecked();
|
||||
if (checked.length === 0) {
|
||||
bar.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
bar.style.display = 'flex';
|
||||
countEl.textContent = `${checked.length} ticket${checked.length !== 1 ? 's' : ''} selected`;
|
||||
hiddenIds.innerHTML = checked.map(cb =>
|
||||
`<input type="hidden" name="ticket_ids" value="${cb.value}"/>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
window.clearSelection = function () {
|
||||
document.querySelectorAll('.ticket-cb').forEach(cb => cb.checked = false);
|
||||
if (selectAll) { selectAll.checked = false; selectAll.indeterminate = false; }
|
||||
updateBar();
|
||||
};
|
||||
|
||||
if (selectAll) {
|
||||
selectAll.addEventListener('change', () => {
|
||||
document.querySelectorAll('.ticket-cb').forEach(cb => {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
updateBar();
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.ticket-cb').forEach(cb => {
|
||||
cb.addEventListener('change', () => {
|
||||
const all = document.querySelectorAll('.ticket-cb');
|
||||
const done = document.querySelectorAll('.ticket-cb:checked');
|
||||
if (selectAll) {
|
||||
selectAll.indeterminate = done.length > 0 && done.length < all.length;
|
||||
selectAll.checked = done.length === all.length && all.length > 0;
|
||||
}
|
||||
updateBar();
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<td>{{ t.title[:45] }}{% if t.title|length > 45 %}…{% endif %}</td>
|
||||
<td><span class="badge badge-{{ t.status }}">{{ t.status.replace('_',' ').upper() }}</span></td>
|
||||
<td><span class="badge badge-{{ t.priority }}">{{ t.priority.upper() }}</span></td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ t.created_at.strftime('%b %d') }}</td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ t.created_at | localtime("%b %d") }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
<span><i class="bi bi-tag me-1"></i>{{ ticket.category.replace('_',' ').title() }}</span>
|
||||
<span><i class="bi bi-calendar3 me-1"></i>{{ ticket.created_at.strftime('%b %d, %Y %H:%M') }}</span>
|
||||
<span><i class="bi bi-calendar3 me-1"></i>{{ ticket.created_at | localtime("%b %d, %Y %H:%M") }}</span>
|
||||
{% if ticket.location %}<span><i class="bi bi-geo-alt me-1"></i>{{ ticket.location }}</span>{% endif %}
|
||||
{% if ticket.asset_tag %}<span><i class="bi bi-cpu me-1"></i>{{ ticket.asset_tag }}</span>{% endif %}
|
||||
{% if ticket.ai_generated %}<span style="color:var(--accent3);"><i class="bi bi-robot me-1"></i>AI-generated</span>{% endif %}
|
||||
@@ -113,7 +113,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="comment-time">{{ comment.created_at.strftime('%b %d, %Y %H:%M') }}</span>
|
||||
<span class="comment-time">{{ comment.created_at | localtime("%b %d, %Y %H:%M") }}</span>
|
||||
{% if current_user.is_it_staff or comment.author_id == current_user.id %}
|
||||
<form method="POST" action="{{ url_for('tickets.delete_comment', comment_id=comment.id) }}"
|
||||
onsubmit="return confirm('Delete this comment?');">
|
||||
@@ -744,10 +744,10 @@ function buildCommentEl(c) {
|
||||
{{ info_row('bi-person-badge', 'Filed by (IT)', '<span style="color:var(--accent);font-weight:600;">' ~ ticket.filed_by_staff.full_name ~ '</span>') }}
|
||||
{% endif %}
|
||||
{{ info_row('bi-person-check', 'Assigned to', ticket.assignee.full_name if ticket.assignee else '—') }}
|
||||
{{ info_row('bi-calendar3', 'Created', ticket.created_at.strftime('%b %d, %Y')) }}
|
||||
{{ info_row('bi-calendar-check', 'Updated', ticket.updated_at.strftime('%b %d, %Y')) }}
|
||||
{% if ticket.due_date %}{{ info_row('bi-alarm', 'Due Date', ticket.due_date.strftime('%b %d, %Y')) }}{% endif %}
|
||||
{% if ticket.resolved_at %}{{ info_row('bi-check-circle', 'Resolved', ticket.resolved_at.strftime('%b %d, %Y')) }}{% endif %}
|
||||
{{ info_row('bi-calendar3', 'Created', ticket.created_at | localtime("%b %d, %Y")) }}
|
||||
{{ info_row('bi-calendar-check', 'Updated', ticket.updated_at | localtime("%b %d, %Y")) }}
|
||||
{% if ticket.due_date %}{{ info_row('bi-alarm', 'Due Date', ticket.due_date | localtime("%b %d, %Y")) }}{% endif %}
|
||||
{% if ticket.resolved_at %}{{ info_row('bi-check-circle', 'Resolved', ticket.resolved_at | localtime("%b %d, %Y")) }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -778,7 +778,7 @@ function buildCommentEl(c) {
|
||||
{% endif %}
|
||||
changed from <em>{{ old }}</em> to <em>{{ new }}</em>
|
||||
<br>
|
||||
<span style="font-size:10px;">by {{ h.changer.full_name }} · {{ h.changed_at.strftime('%b %d %H:%M') }}</span>
|
||||
<span style="font-size:10px;">by {{ h.changer.full_name }} · {{ h.changed_at | localtime("%b %d %H:%M") }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -83,18 +83,65 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-body d-flex align-items-center justify-content-between">
|
||||
<span style="font-size:13px;color:var(--muted);">Did this article solve your issue?</span>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ url_for('tickets.knowledge_base') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
<a href="{{ url_for('tickets.create_ticket') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-ticket me-1"></i>Still need help
|
||||
</a>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<!-- Feedback widget -->
|
||||
<div style="font-size:13px;">
|
||||
<span style="color:var(--muted);margin-right:10px;">Was this article helpful?</span>
|
||||
<button type="button" id="fb-yes"
|
||||
onclick="submitFeedback(1)"
|
||||
class="btn btn-sm {% if user_feedback and user_feedback.is_helpful %}btn-success{% else %}btn-outline-secondary{% endif %}"
|
||||
style="margin-right:6px;">
|
||||
<i class="bi bi-hand-thumbs-up me-1"></i>
|
||||
Yes <span id="fb-yes-count">({{ helpful_count }})</span>
|
||||
</button>
|
||||
<button type="button" id="fb-no"
|
||||
onclick="submitFeedback(0)"
|
||||
class="btn btn-sm {% if user_feedback and not user_feedback.is_helpful %}btn-danger{% else %}btn-outline-secondary{% endif %}">
|
||||
<i class="bi bi-hand-thumbs-down me-1"></i>
|
||||
No <span id="fb-no-count">({{ not_helpful_count }})</span>
|
||||
</button>
|
||||
<span id="fb-msg" style="font-size:12px;color:var(--muted);margin-left:10px;"></span>
|
||||
</div>
|
||||
<!-- Navigation -->
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ url_for('tickets.knowledge_base') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
<a href="{{ url_for('tickets.create_ticket') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-ticket me-1"></i>Still need help
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const FEEDBACK_URL = '{{ url_for("tickets.kb_feedback", article_id=article.id) }}';
|
||||
const CSRF_TOKEN = document.querySelector('meta[name=csrf-token]').content;
|
||||
|
||||
async function submitFeedback(helpful) {
|
||||
const fd = new FormData();
|
||||
fd.append('helpful', helpful);
|
||||
fd.append('csrf_token', CSRF_TOKEN);
|
||||
try {
|
||||
const r = await fetch(FEEDBACK_URL, { method: 'POST', credentials: 'same-origin', body: fd });
|
||||
const data = await r.json();
|
||||
if (!data.ok) return;
|
||||
document.getElementById('fb-yes-count').textContent = `(${data.helpful_count})`;
|
||||
document.getElementById('fb-no-count').textContent = `(${data.not_helpful_count})`;
|
||||
const btnYes = document.getElementById('fb-yes');
|
||||
const btnNo = document.getElementById('fb-no');
|
||||
const msg = document.getElementById('fb-msg');
|
||||
btnYes.className = 'btn btn-sm ' + (data.user_vote === 'helpful' ? 'btn-success' : 'btn-outline-secondary');
|
||||
btnNo.className = 'btn btn-sm ' + (data.user_vote === 'not_helpful' ? 'btn-danger' : 'btn-outline-secondary');
|
||||
msg.textContent = data.user_vote ? 'Thanks for your feedback!' : 'Vote removed.';
|
||||
setTimeout(() => msg.textContent = '', 3000);
|
||||
} catch {
|
||||
// silent fail
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -93,7 +93,7 @@
|
||||
{% if current_user.is_it_staff %}
|
||||
<td style="font-size:13px;">{{ t.creator.full_name }}</td>
|
||||
{% endif %}
|
||||
<td style="font-size:12px;color:var(--muted);">{{ t.created_at.strftime('%b %d, %Y') }}</td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ t.created_at | localtime("%b %d, %Y") }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('tickets.ticket_detail', ticket_id=t.id) }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-eye"></i>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<div style="font-size:12px;color:var(--muted);margin-top:3px;">{{ n.message[:120] }}</div>
|
||||
{% endif %}
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:5px;font-family:'Space Mono',monospace;">
|
||||
{{ n.created_at.strftime('%b %d, %Y at %H:%M') }}
|
||||
{{ n.created_at | localtime("%b %d, %Y at %H:%M") }}
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex-shrink:0;align-self:center;">
|
||||
|
||||
Reference in New Issue
Block a user