pif / app /templates /admin /users.html
pramodmisra's picture
Add user deletion + full superadmin control; fix migration DB path
73b7619
Raw
History Blame Contribute Delete
10.9 kB
{% extends "base.html" %}
{% block title %}User Management — Admin{% endblock %}
{% block content %}
<div class="space-y-4">
<a href="/admin/?token={{ token }}" class="text-sm text-gray-400 hover:text-gray-600">← Admin Panel</a>
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold sw-blue-text">User Management</h1>
<button onclick="document.getElementById('addUser').classList.toggle('hidden')"
class="bg-green-600 text-white px-4 py-2 rounded-md text-sm font-semibold hover:bg-green-700">+ New User</button>
</div>
<!-- Add user form -->
<div id="addUser" class="bg-blue-50 border border-blue-200 rounded-lg p-4 hidden">
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 items-end">
<div>
<label class="text-xs text-gray-500">Full Name</label>
<input id="new_name" type="text" class="w-full border rounded px-2 py-1.5 text-sm">
</div>
<div>
<label class="text-xs text-gray-500">Email</label>
<input id="new_email" type="email" class="w-full border rounded px-2 py-1.5 text-sm">
</div>
<div>
<label class="text-xs text-gray-500">Password</label>
<input id="new_pass" type="password" class="w-full border rounded px-2 py-1.5 text-sm" placeholder="Minimum 8 characters">
</div>
<div>
<label class="text-xs text-gray-500">Role</label>
<select id="new_role" class="w-full border rounded px-2 py-1.5 text-sm">
<option value="user">User</option>
<option value="admin">Admin</option>
{% if user.role == 'superadmin' %}<option value="superadmin">Super Admin</option>{% endif %}
</select>
</div>
<button onclick="addUser()" class="bg-green-600 text-white px-4 py-1.5 rounded text-sm">Create</button>
</div>
</div>
<!-- Show inactive toggle -->
<div class="flex items-center space-x-2">
<label class="flex items-center space-x-2 text-sm text-gray-600 cursor-pointer">
<input type="checkbox" id="show_inactive" onchange="toggleInactiveVisibility()">
<span>Show inactive users</span>
</label>
<span id="inactive_count" class="text-xs text-gray-400"></span>
</div>
<div class="bg-white rounded-lg shadow overflow-hidden">
<table class="w-full text-sm">
<thead><tr class="sw-blue text-white">
<th class="px-4 py-3 text-left">Name</th>
<th class="px-4 py-3 text-left">Email</th>
<th class="px-4 py-3 text-center">Role</th>
<th class="px-4 py-3 text-center">Status</th>
<th class="px-4 py-3 text-center">Actions</th>
</tr></thead>
<tbody>
{% for u in users %}
<tr class="{{ 'bg-gray-50' if loop.index % 2 == 0 }} {{ 'opacity-50' if not u.is_active }} user-row" data-active="{{ 'true' if u.is_active else 'false' }}"
{% if not u.is_active %}style="display: none;"{% endif %}>
<td class="px-4 py-3 font-medium">
<span class="view-mode" id="name_view_{{ u.id }}">{{ u.full_name }}</span>
<input type="text" class="edit-mode hidden w-full border rounded px-2 py-1 text-sm"
id="name_edit_{{ u.id }}" value="{{ u.full_name }}">
</td>
<td class="px-4 py-3 text-gray-600">
<span class="view-mode" id="email_view_{{ u.id }}">{{ u.email }}</span>
{% if user.role == 'superadmin' and u.id != user.id %}
<input type="email" class="edit-mode hidden w-full border rounded px-2 py-1 text-sm"
id="email_edit_{{ u.id }}" value="{{ u.email }}">
{% endif %}
</td>
<td class="px-4 py-3 text-center">
<span class="view-mode px-2 py-0.5 rounded text-xs font-semibold
{{ 'bg-purple-100 text-purple-700' if u.role == 'superadmin' else 'bg-blue-100 text-blue-700' if u.role == 'admin' else 'bg-gray-100 text-gray-600' }}"
id="role_view_{{ u.id }}">
{{ u.role }}
</span>
{% if user.role == 'superadmin' and u.id != user.id %}
<select class="edit-mode hidden border rounded px-1 py-0.5 text-xs" id="role_edit_{{ u.id }}">
<option value="user" {% if u.role == 'user' %}selected{% endif %}>user</option>
<option value="admin" {% if u.role == 'admin' %}selected{% endif %}>admin</option>
<option value="superadmin" {% if u.role == 'superadmin' %}selected{% endif %}>superadmin</option>
</select>
{% endif %}
</td>
<td class="px-4 py-3 text-center">
<span class="view-mode text-xs {{ 'text-green-600' if u.is_active else 'text-red-500' }}" id="active_view_{{ u.id }}">
{{ 'Active' if u.is_active else 'Inactive' }}
</span>
{% if user.role == 'superadmin' and u.id != user.id %}
<label class="edit-mode hidden text-xs inline-flex items-center space-x-1">
<input type="checkbox" id="active_edit_{{ u.id }}" {% if u.is_active %}checked{% endif %}>
<span>Active</span>
</label>
{% endif %}
</td>
<td class="px-4 py-3 text-center space-x-2">
{% if u.id != user.id %}
<span class="view-mode space-x-2">
{% if user.role == 'superadmin' %}
<button onclick="startEdit({{ u.id }})" class="text-green-700 hover:underline text-xs">Edit</button>
{% endif %}
<button onclick="toggleUser({{ u.id }})" class="text-blue-600 hover:underline text-xs">
{{ 'Deactivate' if u.is_active else 'Activate' }}
</button>
<button onclick="resetPassword({{ u.id }}, '{{ u.email }}')" class="text-amber-600 hover:underline text-xs">
Reset PW
</button>
{% if user.role == 'superadmin' and not u.is_active %}
<button onclick="deleteUser({{ u.id }}, '{{ u.email }}')" class="text-red-600 hover:underline text-xs">
Delete
</button>
{% endif %}
</span>
<span class="edit-mode hidden space-x-2">
<button onclick="saveEdit({{ u.id }})" class="text-green-700 hover:underline text-xs font-semibold">Save</button>
<button onclick="cancelEdit({{ u.id }})" class="text-gray-500 hover:underline text-xs">Cancel</button>
</span>
{% else %}
<span class="text-gray-300 text-xs">You</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<script>
const authToken = '{{ token }}';
const authHeaders = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + authToken};
// Hide inactive users by default
function toggleInactiveVisibility() {
const show = document.getElementById('show_inactive').checked;
document.querySelectorAll('.user-row[data-active="false"]').forEach(row => {
row.style.display = show ? '' : 'none';
});
}
// Show inactive count on load
(function() {
const inactiveCount = document.querySelectorAll('.user-row[data-active="false"]').length;
if (inactiveCount > 0) {
document.getElementById('inactive_count').textContent = `(${inactiveCount} inactive)`;
}
})();
async function addUser() {
const resp = await fetch('/admin/api/users/new', {
method: 'POST', headers: authHeaders,
body: JSON.stringify({
full_name: document.getElementById('new_name').value,
email: document.getElementById('new_email').value,
password: document.getElementById('new_pass').value,
role: document.getElementById('new_role').value,
}),
});
const data = await resp.json();
if (data.success) location.reload();
else alert(data.error || 'Failed');
}
async function toggleUser(id) {
await fetch('/admin/api/users/' + id + '/toggle', { method: 'POST', headers: {'Authorization': 'Bearer ' + authToken} });
location.reload();
}
function rowEls(id) {
return document.querySelector(`tr.user-row [id="name_view_${id}"]`).closest('tr');
}
function startEdit(id) {
const row = rowEls(id);
row.querySelectorAll('.view-mode').forEach(el => el.classList.add('hidden'));
row.querySelectorAll('.edit-mode').forEach(el => el.classList.remove('hidden'));
}
function cancelEdit(id) {
const row = rowEls(id);
row.querySelectorAll('.edit-mode').forEach(el => el.classList.add('hidden'));
row.querySelectorAll('.view-mode').forEach(el => el.classList.remove('hidden'));
}
async function saveEdit(id) {
const payload = {
full_name: document.getElementById('name_edit_' + id).value,
email: document.getElementById('email_edit_' + id).value,
role: document.getElementById('role_edit_' + id).value,
is_active: document.getElementById('active_edit_' + id).checked,
};
const resp = await fetch('/admin/api/users/' + id + '/update', {
method: 'POST', headers: authHeaders, body: JSON.stringify(payload),
});
const data = await resp.json();
if (data.success) location.reload();
else alert(data.error || 'Failed to save');
}
async function resetPassword(id, email) {
if (!confirm('Send password reset email to ' + email + '?')) return;
const resp = await fetch('/admin/api/users/' + id + '/reset-password', {
method: 'POST', headers: authHeaders,
});
const data = await resp.json();
if (data.success) alert('Reset link sent to ' + email);
else alert(data.error || 'Failed to send reset email');
}
async function deleteUser(id, email) {
if (!confirm('Permanently delete ' + email + '? This cannot be undone.')) return;
const resp = await fetch('/admin/api/users/' + id, {
method: 'DELETE', headers: {'Authorization': 'Bearer ' + authToken},
});
const data = await resp.json();
if (data.success) location.reload();
else alert(data.error || 'Failed to delete user');
}
</script>
{% endblock %}