enhanced navigation and added coach reassignment
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import { addRoute, startRouter, navigate } from './router.js';
|
||||
import { loginPage } from './pages/login.js';
|
||||
import { dashboardPage } from './pages/dashboard.js';
|
||||
import { homeDashboardPage } from './pages/home.js';
|
||||
import { questionnairesPage } from './pages/questionnaires.js';
|
||||
import { editorPage } from './pages/editor.js';
|
||||
import { resultsPage } from './pages/results.js';
|
||||
import { exportPage } from './pages/export.js';
|
||||
@ -44,6 +45,39 @@ function rejectCoachSession() {
|
||||
navigate('#/login');
|
||||
}
|
||||
|
||||
/** Link to home dashboard (#/). */
|
||||
export const HOME_HREF = '#/';
|
||||
|
||||
const HOME_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1h-5v-7H9v7H4a1 1 0 01-1-1V9.5z"/></svg>`;
|
||||
|
||||
/** House icon button — returns to dashboard. */
|
||||
export function homeNavButton(title = 'Dashboard') {
|
||||
return `<a href="${HOME_HREF}" class="btn btn-home" title="${title}" aria-label="${title}">${HOME_ICON_SVG}</a>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard page header with home button.
|
||||
* @param {string} title HTML-safe title text
|
||||
* @param {string} [actionsHTML] optional buttons inside .actions
|
||||
* @param {{ subtitle?: string }} [opts]
|
||||
*/
|
||||
export function pageHeaderHTML(title, actionsHTML = '', opts = {}) {
|
||||
const subtitle = opts.subtitle
|
||||
? `<p class="page-header-sub">${opts.subtitle}</p>`
|
||||
: '';
|
||||
return `
|
||||
<div class="page-header">
|
||||
<div class="page-header-start">
|
||||
${homeNavButton()}
|
||||
<div class="page-header-titles">
|
||||
<h1>${title}</h1>
|
||||
${subtitle}
|
||||
</div>
|
||||
</div>
|
||||
${actionsHTML ? `<div class="actions">${actionsHTML}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export function showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toastContainer');
|
||||
const toast = document.createElement('div');
|
||||
@ -82,8 +116,15 @@ function updateNav() {
|
||||
// Highlight active nav link
|
||||
const hash = window.location.hash.slice(1) || '/';
|
||||
document.querySelectorAll('.sidebar-nav a').forEach(a => {
|
||||
const href = a.getAttribute('href').slice(1); // strip leading #
|
||||
a.classList.toggle('active', hash === href || (href !== '/' && hash.startsWith(href)));
|
||||
const href = a.getAttribute('href').slice(1);
|
||||
let active = hash === href;
|
||||
if (!active && href === '/questionnaires' && hash.startsWith('/questionnaire')) {
|
||||
active = true;
|
||||
}
|
||||
if (!active && href !== '/' && hash.startsWith(href)) {
|
||||
active = true;
|
||||
}
|
||||
a.classList.toggle('active', active);
|
||||
});
|
||||
} else {
|
||||
nav.style.display = 'none';
|
||||
@ -101,8 +142,8 @@ function roleGuard(roles, handler) {
|
||||
|
||||
// Routes
|
||||
addRoute('/login', loginPage);
|
||||
addRoute('/', authGuard(dashboardPage));
|
||||
addRoute('/dashboard', authGuard(dashboardPage));
|
||||
addRoute('/', roleGuard(['admin', 'supervisor'], homeDashboardPage));
|
||||
addRoute('/questionnaires', authGuard(questionnairesPage));
|
||||
addRoute('/questionnaire/new', authGuard(editorPage));
|
||||
addRoute('/questionnaire/:id/results', authGuard(resultsPage));
|
||||
addRoute('/questionnaire/:id', authGuard(editorPage));
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { apiGet, apiPost } from '../api.js';
|
||||
import { showToast } from '../app.js';
|
||||
import { pageHeaderHTML, showToast } from '../app.js';
|
||||
import { isDevTestClientCode, isDevTestUsername, testDataRowClassForClient } from '../test-data.js';
|
||||
|
||||
const CLIENT_PAGE_SIZE = 50;
|
||||
@ -22,10 +22,7 @@ export async function assignmentsPage() {
|
||||
const app = document.getElementById('app');
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Assign Clients</h1>
|
||||
<div class="actions"><a href="#/" class="btn">← Dashboard</a></div>
|
||||
</div>
|
||||
${pageHeaderHTML('Assign Clients')}
|
||||
<div id="assignContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { apiGet, apiPost, apiDelete } from '../api.js';
|
||||
import { showToast } from '../app.js';
|
||||
import { pageHeaderHTML, showToast } from '../app.js';
|
||||
import { isDevTestClientCode, testDataRowClassForClient } from '../test-data.js';
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
@ -18,12 +18,7 @@ export async function clientsPage() {
|
||||
const app = document.getElementById('app');
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Clients</h1>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" id="showCreateFormBtn">+ Create Client</button>
|
||||
</div>
|
||||
</div>
|
||||
${pageHeaderHTML('Clients', '<button class="btn btn-primary" id="showCreateFormBtn">+ Create Client</button>')}
|
||||
<div id="createClientWrapper" style="display:none"></div>
|
||||
<div id="clientsContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { apiGet } from '../api.js';
|
||||
import { showToast } from '../app.js';
|
||||
import { pageHeaderHTML, showToast } from '../app.js';
|
||||
|
||||
const FETCH_LIMIT = 100;
|
||||
const CLIENT_GROUP_PAGE = 10;
|
||||
@ -16,10 +16,7 @@ let filterSearch = '';
|
||||
export async function coachesPage() {
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Coach activity</h1>
|
||||
<div class="actions"><a href="#/" class="btn">← Dashboard</a></div>
|
||||
</div>
|
||||
${pageHeaderHTML('Coach activity')}
|
||||
<div id="coachesContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { apiPost } from '../api.js';
|
||||
import { getRole, showToast } from '../app.js';
|
||||
import { getRole, pageHeaderHTML, showToast } from '../app.js';
|
||||
import { navigate } from '../router.js';
|
||||
|
||||
export function devPage() {
|
||||
@ -10,9 +10,7 @@ export function devPage() {
|
||||
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Admin Settings</h1>
|
||||
</div>
|
||||
${pageHeaderHTML('Admin Settings')}
|
||||
<div class="card" style="max-width:720px;margin-bottom:16px">
|
||||
<h2 style="margin:0 0 8px;font-size:1.1rem">Export all response data (ZIP)</h2>
|
||||
<p class="field-hint" style="margin:0 0 14px">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { apiGet, apiPost, apiPut, apiPatch, apiDelete } from '../api.js';
|
||||
import { canEdit, showToast } from '../app.js';
|
||||
import { canEdit, homeNavButton, showToast } from '../app.js';
|
||||
import { navigate } from '../router.js';
|
||||
import {
|
||||
SOURCE_LANG,
|
||||
@ -50,9 +50,14 @@ export async function editorPage(params) {
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1 id="editorTitle">${isNew ? 'New Questionnaire' : 'Loading...'}</h1>
|
||||
<div class="page-header-start">
|
||||
${homeNavButton()}
|
||||
<div class="page-header-titles">
|
||||
<h1 id="editorTitle">${isNew ? 'New Questionnaire' : 'Loading...'}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="#/" class="btn">← Back</a>
|
||||
<a href="#/questionnaires" class="btn">Questionnaires</a>
|
||||
${!isNew ? `<a href="#/questionnaire/${params.id}/results" class="btn">View Results</a>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
@ -77,7 +82,7 @@ export async function editorPage(params) {
|
||||
questionnaire = allQuestionnaires.find(q => q.questionnaireID === params.id);
|
||||
if (!questionnaire) {
|
||||
showToast('Questionnaire not found', 'error');
|
||||
navigate('#/');
|
||||
navigate('#/questionnaires');
|
||||
return;
|
||||
}
|
||||
questions = quData.questions || [];
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
import { apiGet } from '../api.js';
|
||||
import { canEdit, showToast } from '../app.js';
|
||||
import { canEdit, pageHeaderHTML, showToast } from '../app.js';
|
||||
let questionnairesList = [];
|
||||
let filterSearch = '';
|
||||
|
||||
export async function exportPage() {
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Export Data</h1>
|
||||
<div class="actions"><a href="#/" class="btn">← Dashboard</a></div>
|
||||
</div>
|
||||
${pageHeaderHTML('Export Data')}
|
||||
<div class="card" id="exportContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
|
||||
108
website/js/pages/home.js
Normal file
108
website/js/pages/home.js
Normal file
@ -0,0 +1,108 @@
|
||||
import { apiGet } from '../api.js';
|
||||
import { getRole, pageHeaderHTML, showToast } from '../app.js';
|
||||
|
||||
export async function homeDashboardPage() {
|
||||
const app = document.getElementById('app');
|
||||
const role = getRole();
|
||||
const isAdmin = role === 'admin';
|
||||
|
||||
app.innerHTML = `
|
||||
${pageHeaderHTML('Dashboard', '', {
|
||||
subtitle: `Overview for your ${role === 'admin' ? 'organization' : 'supervised coaches'}`,
|
||||
})}
|
||||
<div id="homeDashboardContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const ov = await apiGet('analytics.php?overview=1');
|
||||
renderHomeDashboard(ov, isAdmin);
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
document.getElementById('homeDashboardContent').innerHTML =
|
||||
`<p class="error-text">${esc(e.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderHomeDashboard(ov, isAdmin) {
|
||||
const el = document.getElementById('homeDashboardContent');
|
||||
|
||||
const qPreview = (ov.questionnaires || []).slice(0, 6).map(q => `
|
||||
<tr>
|
||||
<td>${esc(q.name)}</td>
|
||||
<td>${q.completedCount ?? 0} / ${q.eligibleCount ?? 0}</td>
|
||||
<td>${q.completionRatio ?? 0}%</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
const links = [
|
||||
{ href: '#/questionnaires', label: 'Questionnaires', desc: 'Edit forms, order, and categories' },
|
||||
{ href: '#/clients', label: 'Clients', desc: 'Client codes, coaches, response history' },
|
||||
{ href: '#/coaches', label: 'Coach activity', desc: 'Uploads and workload per coach' },
|
||||
{ href: '#/insights', label: 'Insights', desc: 'Charts, completion rates, follow-up' },
|
||||
{ href: '#/export', label: 'Export data', desc: 'Download CSV and bundles' },
|
||||
{ href: '#/assignments', label: 'Assign clients', desc: 'Move clients between coaches' },
|
||||
{ href: '#/users', label: 'Users', desc: 'Admins, supervisors, and coaches' },
|
||||
{ href: '#/translations', label: 'Translations', desc: 'German source and other languages' },
|
||||
];
|
||||
if (isAdmin) {
|
||||
links.push({ href: '#/dev', label: 'Admin settings', desc: 'Dev import, exports, database tools' });
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="insights-kpi-grid">
|
||||
<a href="#/clients" class="insights-kpi card home-kpi-link">
|
||||
<div class="insights-kpi-value">${ov.clientCount ?? 0}</div>
|
||||
<div class="insights-kpi-label">Clients</div>
|
||||
</a>
|
||||
<a href="#/insights" class="insights-kpi card home-kpi-link">
|
||||
<div class="insights-kpi-value">${ov.clientsWithCompletions ?? 0}</div>
|
||||
<div class="insights-kpi-label">With completions</div>
|
||||
</a>
|
||||
<a href="#/coaches" class="insights-kpi card home-kpi-link">
|
||||
<div class="insights-kpi-value">${ov.submissionsLast7d ?? 0}</div>
|
||||
<div class="insights-kpi-label">Uploads (7 days)</div>
|
||||
</a>
|
||||
<a href="#/coaches" class="insights-kpi card home-kpi-link">
|
||||
<div class="insights-kpi-value">${ov.submissionsLast30d ?? 0}</div>
|
||||
<div class="insights-kpi-label">Uploads (30 days)</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:20px">
|
||||
<h3 style="margin:0 0 12px">Quick links</h3>
|
||||
<div class="home-link-grid">
|
||||
${links.map(l => `
|
||||
<a href="${l.href}" class="home-link-card">
|
||||
<span class="home-link-title">${esc(l.label)}</span>
|
||||
<span class="home-link-desc">${esc(l.desc)}</span>
|
||||
</a>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:20px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<h3 style="margin:0">Questionnaire completion</h3>
|
||||
<a href="#/insights" class="btn btn-sm">Full report</a>
|
||||
</div>
|
||||
${(ov.questionnaires || []).length ? `
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table data-table-compact">
|
||||
<thead>
|
||||
<tr><th>Questionnaire</th><th>Completed</th><th>Rate</th></tr>
|
||||
</thead>
|
||||
<tbody>${qPreview}${(ov.questionnaires.length > 6)
|
||||
? `<tr><td colspan="3" class="data-toolbar-hint" style="text-align:center">+ ${ov.questionnaires.length - 6} more — see Insights</td></tr>`
|
||||
: ''}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
` : '<p class="data-toolbar-hint">No active questionnaires.</p>'}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s ?? '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { apiGet, apiPut } from '../api.js';
|
||||
import { showToast } from '../app.js';
|
||||
import { pageHeaderHTML, showToast } from '../app.js';
|
||||
|
||||
let overview = null;
|
||||
let staleClients = [];
|
||||
@ -8,10 +8,7 @@ let followupSearch = '';
|
||||
export async function insightsPage() {
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Insights</h1>
|
||||
<div class="actions"><a href="#/" class="btn">← Dashboard</a></div>
|
||||
</div>
|
||||
${pageHeaderHTML('Insights')}
|
||||
<div id="insightsContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
||||
import { canEdit, showToast, getRole } from '../app.js';
|
||||
import { canEdit, pageHeaderHTML, showToast, getRole } from '../app.js';
|
||||
import { navigate } from '../router.js';
|
||||
|
||||
export async function dashboardPage() {
|
||||
export async function questionnairesPage() {
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Questionnaires</h1>
|
||||
<div class="actions" id="headerActions"></div>
|
||||
</div>
|
||||
${pageHeaderHTML('Questionnaires', '<span id="headerActions"></span>')}
|
||||
<div id="qGrid" class="q-dashboard-root"><div class="spinner"></div></div>
|
||||
<div id="categoryKeysPanel"></div>
|
||||
`;
|
||||
@ -120,7 +117,7 @@ function renderCategoryKeysPanel(categoryKeys) {
|
||||
: `Deleted "${key}"`,
|
||||
'success'
|
||||
);
|
||||
await dashboardPage();
|
||||
await questionnairesPage();
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
btn.disabled = false;
|
||||
@ -222,7 +219,14 @@ function groupQuestionnairesByCategory(questionnaires, categoryKeys) {
|
||||
sortKey: Math.min(...items.map(q => q.orderIndex ?? 0)),
|
||||
}));
|
||||
|
||||
sections.sort((a, b) => a.sortKey - b.sortKey);
|
||||
sections.sort((a, b) => {
|
||||
const aUncat = a.categoryKey === '';
|
||||
const bUncat = b.categoryKey === '';
|
||||
if (aUncat !== bUncat) {
|
||||
return aUncat ? 1 : -1;
|
||||
}
|
||||
return a.sortKey - b.sortKey;
|
||||
});
|
||||
return sections;
|
||||
}
|
||||
|
||||
@ -418,7 +422,7 @@ function bindImportBundle() {
|
||||
});
|
||||
const n = result.imported ?? count;
|
||||
showToast(`Imported ${n} questionnaire(s)`, 'success');
|
||||
await dashboardPage();
|
||||
await questionnairesPage();
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
} finally {
|
||||
@ -1,5 +1,5 @@
|
||||
import { apiGet } from '../api.js';
|
||||
import { showToast } from '../app.js';
|
||||
import { homeNavButton, showToast } from '../app.js';
|
||||
import {
|
||||
buildResultColumns,
|
||||
isDevTestClientCode,
|
||||
@ -41,10 +41,15 @@ export async function resultsPage(params) {
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1 id="resultsTitle">Results</h1>
|
||||
<div class="page-header-start">
|
||||
${homeNavButton()}
|
||||
<div class="page-header-titles">
|
||||
<h1 id="resultsTitle">Results</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="#/questionnaire/${params.id}" class="btn">← Back to Editor</a>
|
||||
<a href="#/" class="btn">Dashboard</a>
|
||||
<a href="#/questionnaire/${params.id}" class="btn">Editor</a>
|
||||
<a href="#/questionnaires" class="btn">Questionnaires</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="resultsContent"><div class="spinner"></div></div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
||||
import { canEdit, showToast } from '../app.js';
|
||||
import { canEdit, pageHeaderHTML, showToast } from '../app.js';
|
||||
import {
|
||||
SOURCE_LANG,
|
||||
normalizeEntry,
|
||||
@ -21,10 +21,7 @@ let saveTimers = {};
|
||||
export async function translationsPage() {
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Translations</h1>
|
||||
<div class="actions" id="transHeaderActions"></div>
|
||||
</div>
|
||||
${pageHeaderHTML('Translations', '<span id="transHeaderActions"></span>')}
|
||||
<div id="transPageRoot"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { apiGet, apiPost, apiDelete } from '../api.js';
|
||||
import { getRole, getUser, showToast } from '../app.js';
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
||||
import { getRole, getUser, pageHeaderHTML, showToast } from '../app.js';
|
||||
import { isDevTestUsername, testDataRowClassForUser } from '../test-data.js';
|
||||
|
||||
// Roles an admin can create; supervisors can only create coaches
|
||||
@ -45,13 +45,10 @@ export async function usersPage() {
|
||||
const app = document.getElementById('app');
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1>Users</h1>
|
||||
<div class="actions">
|
||||
${pageHeaderHTML('Users', `
|
||||
<button class="btn" id="downloadUsersBtn" ${usersList.length ? '' : 'disabled'}>Download CSV</button>
|
||||
<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>
|
||||
`;
|
||||
@ -216,13 +213,37 @@ function updateRoleGroupTable(roleKey, myUsername) {
|
||||
tbody.querySelectorAll('.delete-user-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => deleteUser(btn.dataset.id, btn.dataset.name));
|
||||
});
|
||||
tbody.querySelectorAll('.coach-supervisor-select').forEach(sel => {
|
||||
sel.addEventListener('change', () => reassignCoachSupervisor(sel));
|
||||
});
|
||||
}
|
||||
|
||||
function supervisorSelectHTML(u) {
|
||||
if (!supervisorsList.length) {
|
||||
return '<span class="data-toolbar-hint">No supervisors</span>';
|
||||
}
|
||||
const cur = u.supervisorID || '';
|
||||
const opts = supervisorsList.map(s => {
|
||||
const label = s.location ? `${s.username} (${s.location})` : s.username;
|
||||
const selected = s.supervisorID === cur ? ' selected' : '';
|
||||
return `<option value="${esc(s.supervisorID)}"${selected}>${esc(label)}</option>`;
|
||||
}).join('');
|
||||
return `<select class="coach-supervisor-select" data-user-id="${esc(u.userID)}"
|
||||
data-username="${esc(u.username)}" data-prev="${esc(cur)}" aria-label="Supervisor for ${esc(u.username)}">
|
||||
${opts}
|
||||
</select>`;
|
||||
}
|
||||
|
||||
function userRowHTML(u, myUsername) {
|
||||
const isSelf = u.username === myUsername;
|
||||
const detail = u.role === 'coach'
|
||||
? esc(u.supervisorUsername || '—')
|
||||
: esc(u.location || '—');
|
||||
let detail;
|
||||
if (u.role === 'coach') {
|
||||
detail = callerRole === 'admin'
|
||||
? supervisorSelectHTML(u)
|
||||
: esc(u.supervisorUsername || '—');
|
||||
} else {
|
||||
detail = esc(u.location || '—');
|
||||
}
|
||||
const created = u.createdAt
|
||||
? new Date(parseInt(u.createdAt, 10) * 1000).toLocaleDateString()
|
||||
: '—';
|
||||
@ -252,6 +273,34 @@ function userRowHTML(u, myUsername) {
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
async function reassignCoachSupervisor(selectEl) {
|
||||
const userID = selectEl.dataset.userId;
|
||||
const username = selectEl.dataset.username || '';
|
||||
const prev = selectEl.dataset.prev || '';
|
||||
const supervisorID = selectEl.value;
|
||||
|
||||
if (supervisorID === prev) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectEl.disabled = true;
|
||||
try {
|
||||
const data = await apiPut('users.php', { userID, supervisorID });
|
||||
const u = usersList.find(x => x.userID === userID);
|
||||
if (u) {
|
||||
u.supervisorID = data.supervisorID ?? supervisorID;
|
||||
u.supervisorUsername = data.supervisorUsername ?? '';
|
||||
}
|
||||
selectEl.dataset.prev = supervisorID;
|
||||
showToast(`Coach "${username}" assigned to ${data.supervisorUsername || 'supervisor'}`, 'success');
|
||||
} catch (e) {
|
||||
selectEl.value = prev;
|
||||
showToast(e.message, 'error');
|
||||
} finally {
|
||||
selectEl.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser(userID, username) {
|
||||
if (!confirm(`Delete user "${username}"? This cannot be undone.`)) return;
|
||||
try {
|
||||
|
||||
@ -46,7 +46,7 @@ async function resolve() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// fallback: dashboard
|
||||
// fallback: home dashboard
|
||||
navigate('#/');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user