new system prototype
This commit is contained in:
326
website/js/pages/users.js
Normal file
326
website/js/pages/users.js
Normal file
@ -0,0 +1,326 @@
|
||||
import { apiGet, apiPost, apiDelete } from '../api.js';
|
||||
import { getRole, getUser, showToast } from '../app.js';
|
||||
|
||||
// Roles an admin can create; supervisors can only create coaches
|
||||
const CREATEABLE_ROLES = {
|
||||
admin: ['admin', 'supervisor', 'coach'],
|
||||
supervisor: ['coach'],
|
||||
};
|
||||
|
||||
let usersList = [];
|
||||
let supervisorsList = [];
|
||||
let callerRole = '';
|
||||
|
||||
export async function usersPage() {
|
||||
callerRole = getRole();
|
||||
const app = document.getElementById('app');
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Users</h1>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" id="showCreateFormBtn">+ Create User</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="createUserWrapper" style="display:none"></div>
|
||||
<div id="usersContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
document.getElementById('showCreateFormBtn').addEventListener('click', () => {
|
||||
const wrapper = document.getElementById('createUserWrapper');
|
||||
if (wrapper.style.display === 'none') {
|
||||
showCreateForm();
|
||||
} else {
|
||||
hideCreateForm();
|
||||
}
|
||||
});
|
||||
|
||||
await loadUsers();
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const data = await apiGet('users.php');
|
||||
usersList = data.users || [];
|
||||
supervisorsList = data.supervisors || [];
|
||||
callerRole = data.callerRole || callerRole;
|
||||
renderUsers();
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
document.getElementById('usersContent').innerHTML =
|
||||
`<p class="error-text" style="padding:16px">${esc(e.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── User table ────────────────────────────────────────────────────────────
|
||||
|
||||
function renderUsers() {
|
||||
const container = document.getElementById('usersContent');
|
||||
const myUsername = getUser();
|
||||
|
||||
if (!usersList.length) {
|
||||
container.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="empty-state">
|
||||
<h3>No users found</h3>
|
||||
<p>${callerRole === 'supervisor' ? 'You have no coaches yet.' : 'Create the first user above.'}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by role for a cleaner display
|
||||
const byRole = { admin: [], supervisor: [], coach: [] };
|
||||
usersList.forEach(u => (byRole[u.role] || (byRole.other = byRole.other || [])).push(u));
|
||||
|
||||
// Which role groups to show
|
||||
const groups = callerRole === 'supervisor'
|
||||
? [{ label: 'Coaches', key: 'coach' }]
|
||||
: [
|
||||
{ label: 'Admins', key: 'admin' },
|
||||
{ label: 'Supervisors', key: 'supervisor' },
|
||||
{ label: 'Coaches', key: 'coach' },
|
||||
];
|
||||
|
||||
container.innerHTML = groups.map(group => {
|
||||
const users = byRole[group.key] || [];
|
||||
if (!users.length) return '';
|
||||
return `
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<h3 style="margin-bottom:12px">${group.label} (${users.length})</h3>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
${group.key !== 'coach' ? '<th>Location</th>' : '<th>Supervisor</th>'}
|
||||
<th>Password Change Required</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${users.map(u => userRowHTML(u, myUsername)).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
container.querySelectorAll('.delete-user-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => deleteUser(btn.dataset.id, btn.dataset.name));
|
||||
});
|
||||
}
|
||||
|
||||
function userRowHTML(u, myUsername) {
|
||||
const isSelf = u.username === myUsername;
|
||||
const detail = u.role === 'coach'
|
||||
? esc(u.supervisorUsername || '—')
|
||||
: esc(u.location || '—');
|
||||
const created = u.createdAt
|
||||
? new Date(parseInt(u.createdAt, 10) * 1000).toLocaleDateString()
|
||||
: '—';
|
||||
const pwBadge = u.mustChangePassword == 1
|
||||
? '<span class="badge badge-pending">Pending</span>'
|
||||
: '<span style="color:var(--text-secondary);font-size:.8rem">No</span>';
|
||||
|
||||
const canDelete = !isSelf && (
|
||||
callerRole === 'admin' ||
|
||||
(callerRole === 'supervisor' && u.role === 'coach')
|
||||
);
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
<strong>${esc(u.username)}</strong>
|
||||
${isSelf ? '<span class="badge badge-you">You</span>' : ''}
|
||||
</td>
|
||||
<td>${detail}</td>
|
||||
<td>${pwBadge}</td>
|
||||
<td>${created}</td>
|
||||
<td>
|
||||
${canDelete
|
||||
? `<button class="btn btn-sm btn-danger delete-user-btn" data-id="${u.userID}" data-name="${esc(u.username)}">Delete</button>`
|
||||
: ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
async function deleteUser(userID, username) {
|
||||
if (!confirm(`Delete user "${username}"? This cannot be undone.`)) return;
|
||||
try {
|
||||
const data = await apiDelete('users.php', { userID });
|
||||
if (data.success) {
|
||||
usersList = usersList.filter(u => u.userID !== userID);
|
||||
showToast(`User "${username}" deleted`, 'success');
|
||||
renderUsers();
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Create user form ──────────────────────────────────────────────────────
|
||||
|
||||
function showCreateForm() {
|
||||
const wrapper = document.getElementById('createUserWrapper');
|
||||
const allowedRoles = CREATEABLE_ROLES[callerRole] || ['coach'];
|
||||
const isSupervisor = callerRole === 'supervisor';
|
||||
|
||||
wrapper.style.display = '';
|
||||
wrapper.innerHTML = `
|
||||
<div class="inline-form-card" style="margin-bottom:20px">
|
||||
<h4>Create New User</h4>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="flex:2">
|
||||
<label>Username</label>
|
||||
<input type="text" id="cu_username" autocomplete="off" placeholder="Enter username">
|
||||
</div>
|
||||
<div class="form-group" style="flex:2">
|
||||
<label>Password</label>
|
||||
<input type="password" id="cu_password" autocomplete="new-password" placeholder="Min 6 characters">
|
||||
</div>
|
||||
<div class="form-group" style="flex:1;min-width:160px">
|
||||
<label>Role</label>
|
||||
${isSupervisor
|
||||
? `<input type="text" value="Coach" disabled style="background:#f8fafc">`
|
||||
: `<select id="cu_role">
|
||||
${allowedRoles.map(r =>
|
||||
`<option value="${r}">${r.charAt(0).toUpperCase() + r.slice(1)}</option>`
|
||||
).join('')}
|
||||
</select>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cu_location_row" class="form-group" ${isSupervisor ? 'style="display:none"' : ''}>
|
||||
<label>Location <span style="font-weight:400;color:var(--text-secondary)">(for admin/supervisor)</span></label>
|
||||
<input type="text" id="cu_location" placeholder="e.g. Stuttgart">
|
||||
</div>
|
||||
|
||||
<div id="cu_supervisor_row" class="form-group" style="${isSupervisor ? '' : 'display:none'}">
|
||||
<label>Supervisor</label>
|
||||
${isSupervisor
|
||||
? `<input type="text" value="You (auto-assigned)" disabled style="background:#f8fafc">`
|
||||
: `<select id="cu_supervisor_select">
|
||||
<option value="">— select supervisor —</option>
|
||||
${supervisorsList.map(s =>
|
||||
`<option value="${s.supervisorID}">${esc(s.username)}${s.location ? ` (${esc(s.location)})` : ''}</option>`
|
||||
).join('')}
|
||||
</select>`
|
||||
}
|
||||
</div>
|
||||
|
||||
<label class="checkbox-label" style="margin-bottom:14px;display:inline-flex">
|
||||
<input type="checkbox" id="cu_mustchange" checked> Require password change on first login
|
||||
</label>
|
||||
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn btn-primary btn-sm" id="cu_submit">Create User</button>
|
||||
<button class="btn btn-sm" id="cu_cancel">Cancel</button>
|
||||
</div>
|
||||
<p id="cu_error" class="error-text" style="display:none;margin-top:8px"></p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('cu_username').focus();
|
||||
|
||||
// Show/hide location vs supervisor field based on role selection (admin only)
|
||||
if (!isSupervisor) {
|
||||
document.getElementById('cu_role').addEventListener('change', (e) => {
|
||||
const r = e.target.value;
|
||||
document.getElementById('cu_location_row').style.display = r !== 'coach' ? '' : 'none';
|
||||
document.getElementById('cu_supervisor_row').style.display = r === 'coach' ? '' : 'none';
|
||||
});
|
||||
// Set initial state based on default selection
|
||||
const initRole = document.getElementById('cu_role').value;
|
||||
document.getElementById('cu_location_row').style.display = initRole !== 'coach' ? '' : 'none';
|
||||
document.getElementById('cu_supervisor_row').style.display = initRole === 'coach' ? '' : 'none';
|
||||
}
|
||||
|
||||
document.getElementById('cu_submit').addEventListener('click', submitCreateUser);
|
||||
document.getElementById('cu_cancel').addEventListener('click', hideCreateForm);
|
||||
|
||||
// Enter key on last visible field submits
|
||||
wrapper.querySelectorAll('input').forEach(inp => {
|
||||
inp.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') submitCreateUser();
|
||||
if (e.key === 'Escape') hideCreateForm();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function hideCreateForm() {
|
||||
const wrapper = document.getElementById('createUserWrapper');
|
||||
if (wrapper) { wrapper.style.display = 'none'; wrapper.innerHTML = ''; }
|
||||
}
|
||||
|
||||
async function submitCreateUser() {
|
||||
const errEl = document.getElementById('cu_error');
|
||||
errEl.style.display = 'none';
|
||||
|
||||
const isSupervisor = callerRole === 'supervisor';
|
||||
const username = document.getElementById('cu_username').value.trim();
|
||||
const password = document.getElementById('cu_password').value;
|
||||
const role = isSupervisor ? 'coach' : (document.getElementById('cu_role').value || 'coach');
|
||||
const mustChange = document.getElementById('cu_mustchange').checked ? 1 : 0;
|
||||
|
||||
let location = '';
|
||||
let supervisorID = '';
|
||||
if (!isSupervisor) {
|
||||
if (role !== 'coach') {
|
||||
location = document.getElementById('cu_location').value.trim();
|
||||
} else {
|
||||
supervisorID = document.getElementById('cu_supervisor_select').value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!username) { showError(errEl, 'Username is required'); return; }
|
||||
if (password.length < 6) { showError(errEl, 'Password must be at least 6 characters'); return; }
|
||||
if (role === 'coach' && !isSupervisor && !supervisorID) {
|
||||
showError(errEl, 'Please select a supervisor for this coach');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('cu_submit');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Creating...';
|
||||
|
||||
try {
|
||||
const data = await apiPost('users.php', {
|
||||
username, password, role, location, supervisorID, mustChangePassword: mustChange,
|
||||
});
|
||||
if (!data.success) throw new Error(data.error || 'Failed to create user');
|
||||
|
||||
// Add to local list and re-render
|
||||
usersList.push({
|
||||
userID: data.userID,
|
||||
username: data.username,
|
||||
role: data.role,
|
||||
entityID: data.entityID,
|
||||
location: data.location || '',
|
||||
supervisorID: data.supervisorID || null,
|
||||
supervisorUsername: data.supervisorUsername || null,
|
||||
mustChangePassword: data.mustChangePassword,
|
||||
createdAt: data.createdAt,
|
||||
});
|
||||
showToast(`User "${data.username}" created`, 'success');
|
||||
hideCreateForm();
|
||||
renderUsers();
|
||||
} catch (e) {
|
||||
showError(errEl, e.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Create User';
|
||||
}
|
||||
}
|
||||
|
||||
function showError(el, msg) {
|
||||
el.textContent = msg;
|
||||
el.style.display = '';
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s ?? '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
Reference in New Issue
Block a user