Files

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 %}