279 lines
11 KiB
HTML
279 lines
11 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}User Management - Raccoon Timekeeper{% endblock %}
|
||
{% block tagline %}Manage users{% endblock %}
|
||
|
||
{% block content %}
|
||
<section class="card">
|
||
<div class="card-header">
|
||
<h2>User Management</h2>
|
||
<button class="btn btn-primary" onclick="openAddUserModal()">
|
||
<span class="btn-icon">+</span>
|
||
Add User
|
||
</button>
|
||
</div>
|
||
|
||
<div id="usersList" class="users-list">
|
||
<div class="loading">Loading users...</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Add User Modal -->
|
||
<div id="addUserModal" class="modal">
|
||
<div class="modal-content modal-large">
|
||
<div class="modal-header">
|
||
<h3>Add New User</h3>
|
||
<button class="modal-close" onclick="closeModal('addUserModal')">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-grid">
|
||
<div class="form-group">
|
||
<label for="newUsername">Username *</label>
|
||
<input type="text" id="newUsername" placeholder="Username (min 3 chars)" minlength="3">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="newEmail">Email *</label>
|
||
<input type="email" id="newEmail" placeholder="user@example.com">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="newDisplayName">Display Name</label>
|
||
<input type="text" id="newDisplayName" placeholder="Display name (optional)">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="newPassword">Password *</label>
|
||
<input type="password" id="newPassword" placeholder="Min 8 characters" minlength="8">
|
||
</div>
|
||
<div class="form-group checkbox-group">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" id="newIsAdmin">
|
||
<span>Administrator</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" onclick="closeModal('addUserModal')">Cancel</button>
|
||
<button class="btn btn-primary" onclick="saveNewUser()">Create User</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Edit User Modal -->
|
||
<div id="editUserModal" class="modal">
|
||
<div class="modal-content modal-large">
|
||
<div class="modal-header">
|
||
<h3>Edit User</h3>
|
||
<button class="modal-close" onclick="closeModal('editUserModal')">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<input type="hidden" id="editUserId">
|
||
<div class="form-grid">
|
||
<div class="form-group">
|
||
<label for="editUsername">Username</label>
|
||
<input type="text" id="editUsername" disabled>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="editEmail">Email</label>
|
||
<input type="email" id="editEmail" placeholder="user@example.com">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="editDisplayName">Display Name</label>
|
||
<input type="text" id="editDisplayName" placeholder="Display name">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="editPassword">New Password</label>
|
||
<input type="password" id="editPassword" placeholder="Leave blank to keep current">
|
||
</div>
|
||
<div class="form-group checkbox-group">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" id="editIsAdmin">
|
||
<span>Administrator</span>
|
||
</label>
|
||
</div>
|
||
<div class="form-group checkbox-group">
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" id="editIsActive">
|
||
<span>Active</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-danger" onclick="deleteUser()">Delete User</button>
|
||
<button class="btn btn-secondary" onclick="closeModal('editUserModal')">Cancel</button>
|
||
<button class="btn btn-primary" onclick="saveEditUser()">Save Changes</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
const currentUserId = {{ current_user.id }};
|
||
|
||
document.addEventListener('DOMContentLoaded', loadUsers);
|
||
|
||
async function loadUsers() {
|
||
try {
|
||
const response = await fetch('/api/admin/users');
|
||
const users = await response.json();
|
||
renderUsers(users);
|
||
} catch (error) {
|
||
showToast('Error loading users', 'error');
|
||
}
|
||
}
|
||
|
||
function renderUsers(users) {
|
||
const container = document.getElementById('usersList');
|
||
|
||
let html = '<table class="data-table"><thead><tr>' +
|
||
'<th>User</th><th>Email</th><th>Role</th><th>Status</th><th>Last Login</th><th></th>' +
|
||
'</tr></thead><tbody>';
|
||
|
||
users.forEach(user => {
|
||
const lastLogin = user.last_login ? new Date(user.last_login).toLocaleDateString() : 'Never';
|
||
const isCurrentUser = user.id === currentUserId;
|
||
|
||
html += `<tr class="${!user.is_active ? 'inactive' : ''}">
|
||
<td>
|
||
<div class="user-cell">
|
||
<span class="user-avatar">${(user.display_name || user.username)[0].toUpperCase()}</span>
|
||
<div>
|
||
<strong>${user.display_name || user.username}</strong>
|
||
<small>@${user.username}</small>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td>${user.email}</td>
|
||
<td>${user.is_admin ? '<span class="badge admin-badge">Admin</span>' : '<span class="badge">User</span>'}</td>
|
||
<td>${user.is_active ? '<span class="status-active">Active</span>' : '<span class="status-inactive">Inactive</span>'}</td>
|
||
<td>${lastLogin}</td>
|
||
<td>
|
||
<button class="btn btn-icon" onclick='openEditUserModal(${JSON.stringify(user)})'>✏️</button>
|
||
</td>
|
||
</tr>`;
|
||
});
|
||
|
||
html += '</tbody></table>';
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function openAddUserModal() {
|
||
document.getElementById('addUserModal').classList.add('active');
|
||
document.getElementById('newUsername').value = '';
|
||
document.getElementById('newEmail').value = '';
|
||
document.getElementById('newDisplayName').value = '';
|
||
document.getElementById('newPassword').value = '';
|
||
document.getElementById('newIsAdmin').checked = false;
|
||
document.getElementById('newUsername').focus();
|
||
}
|
||
|
||
function openEditUserModal(user) {
|
||
document.getElementById('editUserId').value = user.id;
|
||
document.getElementById('editUsername').value = user.username;
|
||
document.getElementById('editEmail').value = user.email;
|
||
document.getElementById('editDisplayName').value = user.display_name || '';
|
||
document.getElementById('editPassword').value = '';
|
||
document.getElementById('editIsAdmin').checked = user.is_admin;
|
||
document.getElementById('editIsActive').checked = user.is_active;
|
||
document.getElementById('editUserModal').classList.add('active');
|
||
}
|
||
|
||
function closeModal(modalId) {
|
||
document.getElementById(modalId).classList.remove('active');
|
||
}
|
||
|
||
async function saveNewUser() {
|
||
const data = {
|
||
username: document.getElementById('newUsername').value.trim(),
|
||
email: document.getElementById('newEmail').value.trim(),
|
||
display_name: document.getElementById('newDisplayName').value.trim(),
|
||
password: document.getElementById('newPassword').value,
|
||
is_admin: document.getElementById('newIsAdmin').checked
|
||
};
|
||
|
||
if (!data.username || !data.email || !data.password) {
|
||
showToast('Fill in all required fields', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/admin/users', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showToast('User created!', 'success');
|
||
closeModal('addUserModal');
|
||
loadUsers();
|
||
} else {
|
||
showToast(result.message, 'error');
|
||
}
|
||
} catch (error) {
|
||
showToast('Error creating user', 'error');
|
||
}
|
||
}
|
||
|
||
async function saveEditUser() {
|
||
const id = document.getElementById('editUserId').value;
|
||
const data = {
|
||
email: document.getElementById('editEmail').value.trim(),
|
||
display_name: document.getElementById('editDisplayName').value.trim(),
|
||
is_admin: document.getElementById('editIsAdmin').checked,
|
||
is_active: document.getElementById('editIsActive').checked
|
||
};
|
||
|
||
const password = document.getElementById('editPassword').value;
|
||
if (password) data.password = password;
|
||
|
||
try {
|
||
const response = await fetch(`/api/admin/users/${id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showToast('User updated!', 'success');
|
||
closeModal('editUserModal');
|
||
loadUsers();
|
||
} else {
|
||
showToast(result.message, 'error');
|
||
}
|
||
} catch (error) {
|
||
showToast('Error updating user', 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteUser() {
|
||
const id = document.getElementById('editUserId').value;
|
||
|
||
if (parseInt(id) === currentUserId) {
|
||
showToast('Cannot delete your own account', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!confirm('Delete this user? All their data will be permanently removed.')) return;
|
||
|
||
try {
|
||
const response = await fetch(`/api/admin/users/${id}`, { method: 'DELETE' });
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showToast('User deleted', 'success');
|
||
closeModal('editUserModal');
|
||
loadUsers();
|
||
} else {
|
||
showToast(result.message, 'error');
|
||
}
|
||
} catch (error) {
|
||
showToast('Error deleting user', 'error');
|
||
}
|
||
}
|
||
</script>
|
||
{% endblock %}
|