Spaces:
Running
Running
| {% 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 %} | |