Initial commit
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Activity Logs{% endblock %}
|
||||
{% block page_title %}Activity Logs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="bi bi-list-ul me-2"></i>System Activity Log</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Action</th>
|
||||
<th>User</th>
|
||||
<th>Entity</th>
|
||||
<th>Details</th>
|
||||
<th>IP Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs.items %}
|
||||
<tr>
|
||||
<td style="font-size:11px;color:var(--muted);font-family:'Space Mono',monospace;white-space:nowrap;">
|
||||
{{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="mono" style="font-size:11px;
|
||||
color:{% if 'create' in log.action %}var(--success){% elif 'delete' in log.action or 'deactivate' in log.action %}var(--danger){% elif 'update' in log.action or 'edit' in log.action or 'change' in log.action %}var(--warning){% elif 'login' in log.action %}var(--accent3){% else %}var(--muted){% endif %};">
|
||||
{{ log.action }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="font-size:13px;">
|
||||
{% if log.user %}{{ log.user.full_name }}<br><span style="font-size:11px;color:var(--muted);">{{ log.user.email }}</span>{% else %}<span style="color:var(--muted);">System</span>{% endif %}
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--muted);">
|
||||
{{ log.entity_type or '—' }}{% if log.entity_id %} <span class="mono">#{{ log.entity_id }}</span>{% endif %}
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--muted);max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
||||
{{ log.details or '—' }}
|
||||
</td>
|
||||
<td style="font-size:11px;color:var(--muted);font-family:'Space Mono',monospace;">
|
||||
{{ log.ip_address or '—' }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if logs.pages > 1 %}
|
||||
<div class="d-flex justify-content-center py-3">
|
||||
<nav><ul class="pagination mb-0">
|
||||
{% if logs.has_prev %}
|
||||
<li class="page-item"><a class="page-link" href="{{ url_for('admin.activity_logs', page=logs.prev_num) }}">‹ Prev</a></li>
|
||||
{% endif %}
|
||||
{% for p in logs.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if p %}<li class="page-item {% if p==logs.page %}active{% endif %}"><a class="page-link" href="{{ url_for('admin.activity_logs', page=p) }}">{{ p }}</a></li>
|
||||
{% else %}<li class="page-item disabled"><span class="page-link">…</span></li>{% endif %}
|
||||
{% endfor %}
|
||||
{% if logs.has_next %}
|
||||
<li class="page-item"><a class="page-link" href="{{ url_for('admin.activity_logs', page=logs.next_num) }}">Next ›</a></li>
|
||||
{% endif %}
|
||||
</ul></nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,215 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Create User{% endblock %}
|
||||
{% block page_title %}User Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-7">
|
||||
|
||||
<div class="mb-3">
|
||||
<a href="{{ url_for('admin.users') }}" style="font-size:13px;color:var(--accent);">
|
||||
<i class="bi bi-arrow-left me-1"></i>Back to Users
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-person-plus me-2"></i>Create New User
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" novalidate>
|
||||
|
||||
<!-- ── Identity ─────────────────────────────────────────────────── -->
|
||||
<div style="font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:var(--muted);margin-bottom:14px;">
|
||||
Identity
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Full Name *</label>
|
||||
<input type="text" class="form-control" name="full_name"
|
||||
value="{{ form.get('full_name', '') }}" required
|
||||
placeholder="Jane Smith"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Username *</label>
|
||||
<input type="text" class="form-control" name="username"
|
||||
value="{{ form.get('username', '') }}" required
|
||||
placeholder="jsmith"
|
||||
pattern="[a-zA-Z0-9_\-]+"
|
||||
title="Letters, numbers, underscores and hyphens only"/>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Work Email *</label>
|
||||
<input type="email" class="form-control" name="email"
|
||||
value="{{ form.get('email', '') }}" required
|
||||
placeholder="jane.smith@company.com"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Department</label>
|
||||
<input type="text" class="form-control" name="department"
|
||||
value="{{ form.get('department', '') }}"
|
||||
placeholder="Finance"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Phone</label>
|
||||
<input type="text" class="form-control" name="phone"
|
||||
value="{{ form.get('phone', '') }}"
|
||||
placeholder="+1 555 000 0000"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Role & Status ────────────────────────────────────────────── -->
|
||||
<div style="font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:var(--muted);margin-bottom:14px;padding-top:4px;border-top:1px solid var(--border);">
|
||||
Role & Status
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Role *</label>
|
||||
<select class="form-select" name="role">
|
||||
{% for r in roles %}
|
||||
<option value="{{ r }}"
|
||||
{% if form.get('role', 'employee') == r %}selected{% endif %}>
|
||||
{{ r.replace('_', ' ').title() }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:5px;">
|
||||
<strong>Employee</strong> — can submit & track own tickets.<br>
|
||||
<strong>IT Staff</strong> — can manage all tickets and KB.<br>
|
||||
<strong>Admin</strong> — full access including user management.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 d-flex align-items-start pt-4">
|
||||
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;margin-top:10px;">
|
||||
<input type="checkbox" name="is_active" value="1"
|
||||
style="accent-color:var(--accent);width:17px;height:17px;"
|
||||
{% if form.get('is_active', '1') != '0' %}checked{% endif %}/>
|
||||
<span style="font-size:14px;font-weight:500;">Account Active</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Password ─────────────────────────────────────────────────── -->
|
||||
<div style="font-size:11px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:var(--muted);margin-bottom:14px;padding-top:4px;border-top:1px solid var(--border);">
|
||||
Password
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Password *</label>
|
||||
<div style="position:relative;">
|
||||
<input type="password" class="form-control" name="password"
|
||||
id="pw-field" required minlength="8"
|
||||
placeholder="Min. 8 characters"
|
||||
style="padding-right:44px;"/>
|
||||
<button type="button" onclick="togglePw('pw-field','eye1')"
|
||||
style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--muted);cursor:pointer;padding:4px;">
|
||||
<i class="bi bi-eye" id="eye1"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Confirm Password *</label>
|
||||
<div style="position:relative;">
|
||||
<input type="password" class="form-control" name="confirm_password"
|
||||
id="pw-field2" required minlength="8"
|
||||
placeholder="Repeat password"
|
||||
style="padding-right:44px;"/>
|
||||
<button type="button" onclick="togglePw('pw-field2','eye2')"
|
||||
style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--muted);cursor:pointer;padding:4px;">
|
||||
<i class="bi bi-eye" id="eye2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<!-- Password strength bar -->
|
||||
<div style="height:4px;background:var(--border);border-radius:2px;margin-top:4px;">
|
||||
<div id="pw-strength-bar" style="height:100%;border-radius:2px;width:0%;transition:width .3s,background .3s;"></div>
|
||||
</div>
|
||||
<div id="pw-strength-label" style="font-size:11px;color:var(--muted);margin-top:4px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Actions ──────────────────────────────────────────────────── -->
|
||||
<div class="d-flex gap-2 pt-2" style="border-top:1px solid var(--border);">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-person-check me-2"></i>Create User
|
||||
</button>
|
||||
<a href="{{ url_for('admin.users') }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// ── Show/hide password ────────────────────────────────────────────────────────
|
||||
function togglePw(fieldId, iconId) {
|
||||
const field = document.getElementById(fieldId);
|
||||
const icon = document.getElementById(iconId);
|
||||
if (field.type === 'password') {
|
||||
field.type = 'text';
|
||||
icon.className = 'bi bi-eye-slash';
|
||||
} else {
|
||||
field.type = 'password';
|
||||
icon.className = 'bi bi-eye';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Password strength indicator ───────────────────────────────────────────────
|
||||
document.getElementById('pw-field').addEventListener('input', function () {
|
||||
const val = this.value;
|
||||
let score = 0;
|
||||
if (val.length >= 8) score++;
|
||||
if (val.length >= 12) score++;
|
||||
if (/[A-Z]/.test(val) && /[a-z]/.test(val))score++;
|
||||
if (/[0-9]/.test(val)) score++;
|
||||
if (/[^A-Za-z0-9]/.test(val)) score++;
|
||||
|
||||
const bar = document.getElementById('pw-strength-bar');
|
||||
const label = document.getElementById('pw-strength-label');
|
||||
const levels = [
|
||||
{ pct: '20%', bg: '#dc2626', text: 'Very weak' },
|
||||
{ pct: '40%', bg: '#d97706', text: 'Weak' },
|
||||
{ pct: '60%', bg: '#ca8a04', text: 'Fair' },
|
||||
{ pct: '80%', bg: '#16a34a', text: 'Strong' },
|
||||
{ pct: '100%', bg: '#059669', text: 'Very strong' },
|
||||
];
|
||||
if (val.length === 0) {
|
||||
bar.style.width = '0%';
|
||||
label.textContent = '';
|
||||
return;
|
||||
}
|
||||
const lvl = levels[Math.min(score - 1, 4)] || levels[0];
|
||||
bar.style.width = lvl.pct;
|
||||
bar.style.background = lvl.bg;
|
||||
label.textContent = lvl.text;
|
||||
label.style.color = lvl.bg;
|
||||
});
|
||||
|
||||
// ── Client-side password match check before submit ────────────────────────────
|
||||
document.querySelector('form').addEventListener('submit', function (e) {
|
||||
const pw = document.getElementById('pw-field').value;
|
||||
const pw2 = document.getElementById('pw-field2').value;
|
||||
if (pw !== pw2) {
|
||||
e.preventDefault();
|
||||
document.getElementById('pw-field2').style.borderColor = 'var(--danger)';
|
||||
const msg = document.createElement('div');
|
||||
msg.style.cssText = 'font-size:12px;color:var(--danger);margin-top:4px;';
|
||||
msg.textContent = 'Passwords do not match.';
|
||||
const existing = document.getElementById('pw-match-msg');
|
||||
if (existing) existing.remove();
|
||||
msg.id = 'pw-match-msg';
|
||||
document.getElementById('pw-field2').insertAdjacentElement('afterend', msg);
|
||||
}
|
||||
});
|
||||
document.getElementById('pw-field2').addEventListener('input', function () {
|
||||
this.style.borderColor = '';
|
||||
const msg = document.getElementById('pw-match-msg');
|
||||
if (msg) msg.remove();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,58 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Edit User{% endblock %}
|
||||
{% block page_title %}Edit User{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6">
|
||||
<div class="mb-3"><a href="{{ url_for('admin.users') }}" style="font-size:13px;color:var(--accent3);">← Back to Users</a></div>
|
||||
<div class="card">
|
||||
<div class="card-header"><i class="bi bi-person-gear me-2"></i>Edit: {{ user.full_name }}</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Full Name</label>
|
||||
<input type="text" class="form-control" name="full_name" value="{{ user.full_name }}" required/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Department</label>
|
||||
<input type="text" class="form-control" name="department" value="{{ user.department or '' }}"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Phone</label>
|
||||
<input type="text" class="form-control" name="phone" value="{{ user.phone or '' }}"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Role</label>
|
||||
<select class="form-select" name="role">
|
||||
{% for r in roles %}
|
||||
<option value="{{ r }}" {% if r == user.role %}selected{% endif %}>{{ r.replace('_',' ').title() }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;margin:0;">
|
||||
<input type="checkbox" name="is_active" {% if user.is_active %}checked{% endif %}
|
||||
style="accent-color:var(--success);width:16px;height:16px;"/>
|
||||
<span style="font-size:14px;">Account Active</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12"><hr style="border-color:var(--border);"/></div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">New Password <span style="color:var(--muted);font-weight:400;">(optional)</span></label>
|
||||
<input type="password" class="form-control" name="new_password" placeholder="Leave blank to keep current"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check2 me-2"></i>Save</button>
|
||||
<a href="{{ url_for('admin.users') }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,68 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}IT Overview{% endblock %}
|
||||
{% block page_title %}IT Operations Overview{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Stats grid -->
|
||||
<div class="row g-3 mb-4">
|
||||
{% for label, value, icon, color, bg in [
|
||||
('Total Tickets', stats.total_tickets, 'bi-collection', 'var(--text)', 'rgba(255,255,255,.05)'),
|
||||
('Open', stats.open, 'bi-circle', 'var(--info)', 'rgba(96,165,250,.1)'),
|
||||
('In Progress', stats.in_progress, 'bi-arrow-repeat', 'var(--accent3)', 'rgba(0,180,216,.1)'),
|
||||
('Resolved', stats.resolved, 'bi-check-circle', 'var(--success)', 'rgba(45,212,191,.1)'),
|
||||
('Closed', stats.closed, 'bi-archive', 'var(--muted)', 'rgba(112,112,160,.1)'),
|
||||
('Active Users', stats.total_users, 'bi-people', 'var(--accent2)', 'rgba(123,94,167,.1)'),
|
||||
] %}
|
||||
<div class="col-6 col-xl-2">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon" style="background:{{ bg }};color:{{ color }};"><i class="bi {{ icon }}"></i></div>
|
||||
<div>
|
||||
<div class="stat-value" style="color:{{ color }};">{{ value }}</div>
|
||||
<div class="stat-label">{{ label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Activity log -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-activity me-2"></i>Recent Activity</span>
|
||||
{% if current_user.is_admin %}
|
||||
<a href="{{ url_for('admin.activity_logs') }}" style="font-size:12px;color:var(--accent3);">Full log →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Action</th>
|
||||
<th>User</th>
|
||||
<th>Entity</th>
|
||||
<th>Details</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in recent_logs %}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="mono" style="font-size:11px;
|
||||
color:{% if 'create' in log.action %}var(--success){% elif 'delete' in log.action %}var(--danger){% elif 'update' in log.action or 'edit' in log.action %}var(--warning){% else %}var(--muted){% endif %};">
|
||||
{{ log.action }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="font-size:13px;">
|
||||
{% if log.user %}{{ log.user.full_name }}{% else %}<span style="color:var(--muted);">System</span>{% endif %}
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ log.entity_type or '—' }} {% if log.entity_id %}#{{ log.entity_id }}{% endif %}</td>
|
||||
<td style="font-size:12px;color:var(--muted);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ log.details or '—' }}</td>
|
||||
<td style="font-size:11px;color:var(--muted);font-family:'Space Mono',monospace;">{{ log.created_at.strftime('%b %d %H:%M') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{% if article %}Edit Article{% else %}New Article{% endif %}{% endblock %}
|
||||
{% block page_title %}{% if article %}Edit Article{% else %}New KB Article{% endif %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="mb-3"><a href="{{ url_for('admin.kb_list') }}" style="font-size:13px;color:var(--accent3);">← Back to Knowledge Base</a></div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-{% if article %}pencil{% else %}plus-circle{% endif %} me-2"></i>
|
||||
{% if article %}Edit: {{ article.title[:40] }}{% else %}Create New Article{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Title *</label>
|
||||
<input type="text" class="form-control" name="title" required
|
||||
value="{{ article.title if article else '' }}"
|
||||
placeholder="e.g. How to connect to the VPN"/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Category</label>
|
||||
<select class="form-select" name="category">
|
||||
<option value="">— Select Category —</option>
|
||||
{% for cat in ['hardware','software','network','access','email','printer','phone','security','other'] %}
|
||||
<option value="{{ cat }}" {% if article and article.category == cat %}selected{% endif %}>
|
||||
{{ cat.replace('_',' ').title() }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Tags <span style="color:var(--muted);font-weight:400;">(comma-separated)</span></label>
|
||||
<input type="text" class="form-control" name="tags"
|
||||
value="{{ article.tags if article else '' }}"
|
||||
placeholder="vpn, remote, access"/>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Content *</label>
|
||||
<textarea class="form-control" name="body" rows="16" required
|
||||
placeholder="Write the article content here. You can use plain text with line breaks.">{{ article.body if article else '' }}</textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;">
|
||||
<input type="checkbox" name="is_published" style="accent-color:var(--accent3);width:16px;height:16px;"
|
||||
{% if not article or article.is_published %}checked{% endif %}/>
|
||||
<span style="font-size:14px;">Publish immediately (visible to all employees)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check2 me-2"></i>{% if article %}Save Changes{% else %}Publish Article{% endif %}
|
||||
</button>
|
||||
<a href="{{ url_for('admin.kb_list') }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Manage Knowledge Base{% endblock %}
|
||||
{% block page_title %}Knowledge Base Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-journal-text me-2"></i>Articles <span style="font-size:12px;color:var(--muted);">({{ articles|length }})</span></span>
|
||||
<a href="{{ url_for('admin.kb_new') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg me-1"></i>New Article
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% 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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for art in articles %}
|
||||
<tr>
|
||||
<td style="font-size:14px;">{{ art.title[:60] }}</td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ art.category or '—' }}</td>
|
||||
<td style="font-size:13px;">{{ art.author.full_name if art.author else '—' }}</td>
|
||||
<td>
|
||||
{% if art.is_published %}
|
||||
<span style="color:var(--success);font-size:12px;"><i class="bi bi-check-circle-fill"></i> Yes</span>
|
||||
{% else %}
|
||||
<span style="color:var(--muted);font-size:12px;"><i class="bi bi-dash-circle"></i> Draft</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ art.view_count }}</td>
|
||||
<td style="font-size:12px;color:var(--muted);">{{ art.updated_at.strftime('%b %d, %Y') }}</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<a href="{{ url_for('tickets.kb_article', article_id=art.id) }}" class="btn btn-secondary btn-sm" title="Preview"><i class="bi bi-eye"></i></a>
|
||||
<a href="{{ url_for('admin.kb_edit', article_id=art.id) }}" class="btn btn-secondary btn-sm" title="Edit"><i class="bi bi-pencil"></i></a>
|
||||
<form method="POST" action="{{ url_for('admin.kb_delete', article_id=art.id) }}" onsubmit="return confirm('Delete this article?');">
|
||||
<button type="submit" class="btn btn-sm" style="background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.2);color:var(--danger);" title="Delete"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="p-5 text-center" style="color:var(--muted);">
|
||||
<i class="bi bi-journal-x" style="font-size:40px;display:block;margin-bottom:12px;"></i>
|
||||
No articles yet. <a href="{{ url_for('admin.kb_new') }}" style="color:var(--accent3);">Create your first article →</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,110 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}All Tickets{% endblock %}
|
||||
{% block page_title %}All Tickets{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Filters -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<form method="GET" class="row g-2 align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="">All Statuses</option>
|
||||
{% for s in ['open','in_progress','pending','resolved','closed'] %}
|
||||
<option value="{{ s }}" {% if s == status %}selected{% endif %}>{{ s.replace('_',' ').title() }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Priority</label>
|
||||
<select class="form-select" name="priority">
|
||||
<option value="">All Priorities</option>
|
||||
{% for p in ['low','medium','high','critical'] %}
|
||||
<option value="{{ p }}" {% if p == priority %}selected{% endif %}>{{ p.title() }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Assignment</label>
|
||||
<select class="form-select" name="assigned">
|
||||
<option value="">All</option>
|
||||
<option value="me" {% if assigned == 'me' %}selected{% endif %}>Assigned to Me</option>
|
||||
<option value="unassigned" {% if assigned == 'unassigned' %}selected{% endif %}>Unassigned</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary flex-fill"><i class="bi bi-search me-1"></i>Filter</button>
|
||||
<a href="{{ url_for('admin.all_tickets') }}" class="btn btn-secondary"><i class="bi bi-x-lg"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-collection me-2"></i>Tickets
|
||||
<span style="font-size:12px;color:var(--muted);font-family:'Space Mono',monospace;">({{ tickets.total }})</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if tickets.items %}
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ticket #</th>
|
||||
<th>Title</th>
|
||||
<th>Category</th>
|
||||
<th>Status</th>
|
||||
<th>Priority</th>
|
||||
<th>Submitted By</th>
|
||||
<th>Assigned To</th>
|
||||
<th>Date</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in tickets.items %}
|
||||
<tr>
|
||||
<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>
|
||||
<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: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><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 %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if tickets.pages > 1 %}
|
||||
<div class="d-flex justify-content-center py-3">
|
||||
<nav><ul class="pagination mb-0">
|
||||
{% if tickets.has_prev %}
|
||||
<li class="page-item"><a class="page-link" href="{{ url_for('admin.all_tickets', page=tickets.prev_num, status=status, priority=priority, assigned=assigned) }}">‹</a></li>
|
||||
{% endif %}
|
||||
{% for p in tickets.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
||||
{% if p %}
|
||||
<li class="page-item {% if p==tickets.page %}active{% endif %}">
|
||||
<a class="page-link" href="{{ url_for('admin.all_tickets', page=p, status=status, priority=priority, assigned=assigned) }}">{{ p }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if tickets.has_next %}
|
||||
<li class="page-item"><a class="page-link" href="{{ url_for('admin.all_tickets', page=tickets.next_num, status=status, priority=priority, assigned=assigned) }}">›</a></li>
|
||||
{% endif %}
|
||||
</ul></nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="p-5 text-center" style="color:var(--muted);">
|
||||
<i class="bi bi-inbox" style="font-size:40px;display:block;margin-bottom:12px;"></i>No tickets match the current filters.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,77 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}User Management{% endblock %}
|
||||
{% block page_title %}User Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-people me-2"></i>All Users <span style="font-size:12px;color:var(--muted);">({{ users|length }})</span></span>
|
||||
<a href="{{ url_for('admin.create_user') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-person-plus me-1"></i>Add User
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Department</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Last Login</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<div style="width:30px;height:30px;border-radius:50%;background:var(--accent2);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0;">
|
||||
{{ u.full_name[0].upper() }}
|
||||
</div>
|
||||
<span style="font-size:14px;">{{ u.full_name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-size:13px;color:var(--muted);">{{ u.email }}</td>
|
||||
<td style="font-size:13px;color:var(--muted);">{{ u.department or '—' }}</td>
|
||||
<td>
|
||||
<span style="font-size:11px;padding:2px 8px;border-radius:4px;
|
||||
background:{% if u.role == 'admin' %}rgba(233,69,96,.15){% elif u.role == 'it_staff' %}rgba(0,180,216,.15){% else %}rgba(112,112,160,.15){% endif %};
|
||||
color:{% if u.role == 'admin' %}var(--accent){% elif u.role == 'it_staff' %}var(--accent3){% else %}var(--muted){% endif %};">
|
||||
{{ u.role.replace('_',' ').upper() }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if u.is_active %}
|
||||
<span style="color:var(--success);font-size:12px;"><i class="bi bi-circle-fill" style="font-size:8px;"></i> Active</span>
|
||||
{% else %}
|
||||
<span style="color:var(--muted);font-size:12px;"><i class="bi bi-circle" style="font-size:8px;"></i> Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--muted);">
|
||||
{{ u.last_login.strftime('%b %d, %Y') if u.last_login else 'Never' }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-1">
|
||||
<a href="{{ url_for('admin.edit_user', user_id=u.id) }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% if u.id != current_user.id and u.is_active %}
|
||||
<form method="POST" action="{{ url_for('admin.delete_user', user_id=u.id) }}"
|
||||
onsubmit="return confirm('Deactivate {{ u.full_name }}?');">
|
||||
<button type="submit" class="btn btn-sm" style="background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.2);color:var(--danger);">
|
||||
<i class="bi bi-person-dash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user