Initial commit: Queensway Europe Telco Portal (Flask + SQLite, Docker deploy)

This commit is contained in:
Martin Belan
2026-09-11 07:23:40 +00:00
commit 919df2220e
42 changed files with 2206 additions and 0 deletions
+183
View File
@@ -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 %}