Initial commit: Queensway Europe Telco Portal (Flask + SQLite, Docker deploy)
This commit is contained in:
Executable
+183
@@ -0,0 +1,183 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Users - Queensway Europe Telco Portal{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4 gap-2">
|
||||
<h2 class="mb-0 d-flex align-items-center gap-2">
|
||||
{% include "icons/people.svg" %} Users
|
||||
</h2>
|
||||
<button type="button" class="btn btn-success d-inline-flex align-items-center gap-2" onclick="openUserModal(null)">
|
||||
{% include "icons/plus-circle.svg" %} Add User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-icon-total">{% include "icons/collection.svg" %}</div>
|
||||
<div>
|
||||
<div class="stat-value">{{ users|length }}</div>
|
||||
<div class="stat-label">Total Users</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-icon-verified">{% include "icons/shield-lock-fill.svg" %}</div>
|
||||
<div>
|
||||
<div class="stat-value">{{ admin_count }}</div>
|
||||
<div class="stat-label">Admins</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-icon-headoffice">{% include "icons/people.svg" %}</div>
|
||||
<div>
|
||||
<div class="stat-value">{{ viewer_count }}</div>
|
||||
<div class="stat-label">Viewers</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modern-card mb-0">
|
||||
<div class="table-toolbar d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<div class="text-muted small">Admins can add, edit and remove phones, internet connections and other user accounts. Viewers can only view.</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0 modern-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td class="fw-semibold">{{ u.username }}{% if u.id == session.user_id %}<span class="text-muted fw-normal"> (you)</span>{% endif %}</td>
|
||||
<td>
|
||||
{% if u.role == "admin" %}
|
||||
<span class="badge rounded-pill bg-success-subtle text-success-emphasis">Admin</span>
|
||||
{% else %}
|
||||
<span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis">Viewer</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="d-inline-flex gap-1">
|
||||
<button type="button" class="btn btn-icon btn-icon-primary" title="Edit"
|
||||
onclick='openUserModal({{ u.to_dict() | tojson }})'>{% include "icons/pencil-square.svg" %}</button>
|
||||
<button type="button" class="btn btn-icon btn-icon-danger" title="Delete"
|
||||
onclick="deleteUser({{ u.id }}, {{ u.username | tojson }})" {% if u.id == session.user_id %}disabled{% endif %}>{% include "icons/trash.svg" %}</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3" class="text-center text-muted py-5">No users yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add / Edit modal -->
|
||||
<div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form id="userForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="userModalTitle">Edit User</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="userId">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="userUsername" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="userPassword" autocomplete="new-password">
|
||||
<div class="form-text" id="userPasswordHelp" hidden>Leave blank to keep the current password.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Role</label>
|
||||
<select class="form-select" id="userRole">
|
||||
{% for option in role_options %}
|
||||
<option value="{{ option }}">{{ option|capitalize }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div id="userError" class="text-danger small"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const userModalEl = document.getElementById('userModal');
|
||||
const userModal = new bootstrap.Modal(userModalEl);
|
||||
|
||||
function openUserModal(user) {
|
||||
document.getElementById('userForm').reset();
|
||||
document.getElementById('userError').textContent = '';
|
||||
const isEdit = !!user;
|
||||
document.getElementById('userPasswordHelp').hidden = !isEdit;
|
||||
document.getElementById('userPassword').required = !isEdit;
|
||||
|
||||
if (isEdit) {
|
||||
document.getElementById('userModalTitle').textContent = 'Edit User';
|
||||
document.getElementById('userId').value = user.id;
|
||||
document.getElementById('userUsername').value = user.username || '';
|
||||
document.getElementById('userRole').value = user.role || 'viewer';
|
||||
} else {
|
||||
document.getElementById('userModalTitle').textContent = 'Add User';
|
||||
document.getElementById('userId').value = '';
|
||||
document.getElementById('userRole').value = 'viewer';
|
||||
}
|
||||
userModal.show();
|
||||
}
|
||||
|
||||
document.getElementById('userForm').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('userId').value;
|
||||
const payload = {
|
||||
username: document.getElementById('userUsername').value,
|
||||
password: document.getElementById('userPassword').value,
|
||||
role: document.getElementById('userRole').value,
|
||||
};
|
||||
const url = id ? `/api/users/${id}` : '/api/users';
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
const resp = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (resp.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
document.getElementById('userError').textContent = data.error || 'Failed to save. Please check the fields.';
|
||||
}
|
||||
});
|
||||
|
||||
async function deleteUser(id, username) {
|
||||
if (!confirm(`Delete user "${username}"? This cannot be undone.`)) return;
|
||||
const resp = await fetch(`/api/users/${id}`, { method: 'DELETE' });
|
||||
if (resp.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
alert(data.error || 'Failed to delete.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user