147 lines
8.4 KiB
HTML
147 lines
8.4 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ article.title }}{% endblock %}
|
|
{% block page_title %}Knowledge Base{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-8">
|
|
<div class="mb-3">
|
|
<a href="{{ url_for('tickets.knowledge_base') }}" style="font-size:13px;color:var(--accent3);">
|
|
← Back to Knowledge Base
|
|
</a>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header d-flex align-items-center justify-content-between">
|
|
<span><i class="bi bi-file-earmark-text me-2"></i>Article</span>
|
|
{% if current_user.is_it_staff %}
|
|
<a href="{{ url_for('admin.kb_edit', article_id=article.id) }}" class="btn btn-secondary btn-sm">
|
|
<i class="bi bi-pencil me-1"></i>Edit
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body">
|
|
<h1 style="font-size:22px;font-weight:700;margin-bottom:12px;">{{ article.title }}</h1>
|
|
<div style="font-size:12px;color:var(--muted);margin-bottom:24px;display:flex;gap:16px;">
|
|
{% if article.category %}<span><i class="bi bi-tag me-1"></i>{{ article.category.replace('_',' ').title() }}</span>{% endif %}
|
|
<span><i class="bi bi-person me-1"></i>{{ article.author.full_name if article.author else 'IT Team' }}</span>
|
|
<span><i class="bi bi-calendar3 me-1"></i>{{ article.updated_at.strftime('%B %d, %Y') }}</span>
|
|
<span><i class="bi bi-eye me-1"></i>{{ article.view_count }} views</span>
|
|
</div>
|
|
{% if article.tags %}
|
|
<div class="mb-3">
|
|
{% for tag in article.tags.split(',') %}
|
|
<span style="background:var(--surface2);border:1px solid var(--border);border-radius:5px;padding:2px 10px;font-size:11px;color:var(--muted);margin-right:4px;">{{ tag.strip() }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
<!-- Render rich HTML saved by TinyMCE -->
|
|
<style>
|
|
.kb-body { font-size:14px; line-height:1.8; color:var(--text); }
|
|
.kb-body h1,.kb-body h2,.kb-body h3,.kb-body h4 { font-weight:700; margin:1.4em 0 .5em; color:var(--text); }
|
|
.kb-body h1{font-size:1.55em;} .kb-body h2{font-size:1.3em;} .kb-body h3{font-size:1.12em;}
|
|
.kb-body p { margin:0 0 1em; }
|
|
.kb-body a { color:var(--accent); }
|
|
.kb-body code { background:var(--surface2); border:1px solid var(--border); border-radius:4px; padding:1px 5px; font-size:.88em; font-family:'Space Mono',monospace; }
|
|
.kb-body pre { background:#1e293b; color:#e2e8f0; padding:14px 18px; border-radius:8px; overflow-x:auto; margin:1em 0; }
|
|
.kb-body pre code { background:none; border:none; padding:0; color:inherit; }
|
|
.kb-body table { border-collapse:collapse; width:100%; margin:1em 0; }
|
|
.kb-body table th { background:var(--surface2); font-weight:600; }
|
|
.kb-body table th,.kb-body table td { border:1px solid var(--border); padding:8px 12px; font-size:13px; }
|
|
.kb-body table tr:nth-child(even) td { background:var(--bg); }
|
|
.kb-body img { max-width:100%; height:auto; border-radius:6px; margin:4px 0; }
|
|
.kb-body blockquote { border-left:4px solid var(--accent); margin:1em 0; padding:10px 16px; background:var(--info-bg); border-radius:0 6px 6px 0; color:var(--info); }
|
|
.kb-body ul,.kb-body ol { padding-left:24px; margin:0 0 1em; }
|
|
.kb-body li { margin-bottom:.3em; }
|
|
.kb-body hr { border:none; border-top:1px solid var(--border); margin:2em 0; }
|
|
.kb-body .callout-info { background:var(--info-bg); border:1px solid #bfdbfe; border-radius:8px; padding:14px 16px; margin:1em 0; }
|
|
.kb-body .callout-warning { background:var(--warning-bg); border:1px solid #fde68a; border-radius:8px; padding:14px 16px; margin:1em 0; }
|
|
.kb-body .callout-tip { background:var(--success-bg); border:1px solid #a7f3d0; border-radius:8px; padding:14px 16px; margin:1em 0; }
|
|
</style>
|
|
<div class="kb-body">{{ article.body | safe }}</div>
|
|
|
|
{% set existing_atts = article.attachments.all() %}
|
|
{% if existing_atts %}
|
|
<div style="margin-top:24px;padding-top:16px;border-top:1px solid var(--border);">
|
|
<div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--muted);margin-bottom:10px;">
|
|
<i class="bi bi-paperclip me-1"></i>Attachments
|
|
</div>
|
|
<div style="display:flex;flex-direction:column;gap:6px;">
|
|
{% for att in existing_atts %}
|
|
<a href="{{ url_for('admin.kb_serve_file', stored_name=att.stored_name) }}"
|
|
target="_blank"
|
|
style="display:flex;align-items:center;gap:10px;padding:9px 14px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;">
|
|
<i class="bi bi-{% if att.mime_type and 'image' in att.mime_type %}image{% elif att.mime_type and 'pdf' in att.mime_type %}file-earmark-pdf{% else %}file-earmark{% endif %}"
|
|
style="color:var(--accent);font-size:16px;flex-shrink:0;"></i>
|
|
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ att.filename }}</span>
|
|
<span style="font-size:11px;color:var(--muted);font-family:'Space Mono',monospace;">{{ (att.file_size / 1024)|int }} KB</span>
|
|
<i class="bi bi-download" style="color:var(--muted);font-size:13px;"></i>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="card mt-3">
|
|
<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 %} |