199 lines
6.6 KiB
JavaScript
199 lines
6.6 KiB
JavaScript
import { apiGet } from '../api.js';
|
|
import { showToast } from '../app.js';
|
|
|
|
let coachesList = [];
|
|
let expandedCoachId = null;
|
|
let recentByCoach = {};
|
|
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>
|
|
<div id="coachesContent"><div class="spinner"></div></div>
|
|
`;
|
|
|
|
try {
|
|
const data = await apiGet('coaches.php');
|
|
coachesList = data.coaches || [];
|
|
renderCoaches();
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
document.getElementById('coachesContent').innerHTML =
|
|
`<p class="error-text">${esc(e.message)}</p>`;
|
|
}
|
|
}
|
|
|
|
function coachSearchText(c) {
|
|
return [c.username, c.coachID, c.supervisorUsername].filter(Boolean).join(' ');
|
|
}
|
|
|
|
function coachHasUploads(c) {
|
|
return (c.totalSubmissions ?? 0) > 0;
|
|
}
|
|
|
|
function filteredCoaches() {
|
|
const q = filterSearch.trim().toLowerCase();
|
|
if (!q) return coachesList;
|
|
return coachesList.filter(c => coachSearchText(c).toLowerCase().includes(q));
|
|
}
|
|
|
|
function renderCoaches() {
|
|
const el = document.getElementById('coachesContent');
|
|
|
|
if (!coachesList.length) {
|
|
el.innerHTML = `<div class="card empty-state"><h3>No coaches</h3></div>`;
|
|
return;
|
|
}
|
|
|
|
el.innerHTML = `
|
|
<div class="card">
|
|
<p class="data-toolbar-hint" style="margin:0 0 12px">Track uploads and client load per coach. Expand a row for recent submissions.</p>
|
|
<div class="filter-bar">
|
|
<input type="search" id="coachListSearch" class="filter-search"
|
|
placeholder="Search coach or supervisor…" value="${esc(filterSearch)}">
|
|
<span class="data-toolbar-hint" id="coachListCount"></span>
|
|
</div>
|
|
<div class="table-wrapper">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Coach</th><th>Supervisor</th><th>Clients</th>
|
|
<th>Total uploads</th><th>7d</th><th>30d</th><th>Last upload</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="coachTableBody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('coachListSearch').addEventListener('input', (e) => {
|
|
filterSearch = e.target.value;
|
|
const filtered = filteredCoaches();
|
|
if (
|
|
expandedCoachId &&
|
|
!filtered.some(c => c.coachID === expandedCoachId && coachHasUploads(c))
|
|
) {
|
|
expandedCoachId = null;
|
|
}
|
|
renderCoachTableBody();
|
|
});
|
|
|
|
renderCoachTableBody();
|
|
}
|
|
|
|
function coachRowHTML(c) {
|
|
const canExpand = coachHasUploads(c);
|
|
const expanded = canExpand && expandedCoachId === c.coachID;
|
|
const recent = recentByCoach[c.coachID] || [];
|
|
const expandControl = canExpand
|
|
? `<button type="button" class="btn btn-sm btn-link coach-expand-btn" data-coach="${esc(c.coachID)}">${expanded ? '▼' : '▶'}</button> `
|
|
: '';
|
|
return `
|
|
<tr class="coach-row" data-coach="${esc(c.coachID)}">
|
|
<td>${expandControl}<strong>${esc(c.username)}</strong></td>
|
|
<td>${esc(c.supervisorUsername || '—')}</td>
|
|
<td>${c.clientCount ?? 0}</td>
|
|
<td>${c.totalSubmissions ?? 0}</td>
|
|
<td>${c.submissionsLast7d ?? 0}</td>
|
|
<td>${c.submissionsLast30d ?? 0}</td>
|
|
<td>${esc(c.lastSubmissionAt || '—')}</td>
|
|
</tr>
|
|
${expanded ? `
|
|
<tr class="coach-detail-row">
|
|
<td colspan="7">
|
|
<div class="coach-recent-panel">
|
|
${recent.length ? `
|
|
<table class="data-table data-table-compact">
|
|
<thead>
|
|
<tr><th>When</th><th>Client</th><th>Questionnaire</th><th>Ver.</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
${recent.map(s => `
|
|
<tr>
|
|
<td>${esc(s.submittedAt)}</td>
|
|
<td><code>${esc(s.clientCode)}</code></td>
|
|
<td>${esc(s.questionnaireName)}</td>
|
|
<td>${s.version}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
` : '<p class="data-toolbar-hint">Loading…</p>'}
|
|
</div>
|
|
</td>
|
|
</tr>` : ''}`;
|
|
}
|
|
|
|
function renderCoachTableBody() {
|
|
const body = document.getElementById('coachTableBody');
|
|
const countEl = document.getElementById('coachListCount');
|
|
if (!body) return;
|
|
|
|
const filtered = filteredCoaches();
|
|
|
|
if (!filtered.length) {
|
|
body.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" style="text-align:center;color:var(--text-secondary);padding:24px">
|
|
No coaches match
|
|
</td>
|
|
</tr>`;
|
|
if (countEl) {
|
|
countEl.textContent = filterSearch.trim()
|
|
? `0 of ${coachesList.length} coaches`
|
|
: '';
|
|
}
|
|
return;
|
|
}
|
|
|
|
body.innerHTML = filtered.map(c => coachRowHTML(c)).join('');
|
|
|
|
if (countEl) {
|
|
const q = filterSearch.trim();
|
|
countEl.textContent = q
|
|
? `${filtered.length} of ${coachesList.length} coaches`
|
|
: `${coachesList.length} coach${coachesList.length === 1 ? '' : 'es'}`;
|
|
}
|
|
|
|
body.querySelectorAll('.coach-expand-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => toggleCoach(btn.dataset.coach));
|
|
});
|
|
}
|
|
|
|
async function toggleCoach(coachID) {
|
|
const coach = coachesList.find(c => c.coachID === coachID);
|
|
if (!coach || !coachHasUploads(coach)) {
|
|
return;
|
|
}
|
|
if (expandedCoachId === coachID) {
|
|
expandedCoachId = null;
|
|
renderCoachTableBody();
|
|
return;
|
|
}
|
|
expandedCoachId = coachID;
|
|
renderCoachTableBody();
|
|
if (!recentByCoach[coachID]) {
|
|
try {
|
|
const data = await apiGet(
|
|
`coaches.php?coachID=${encodeURIComponent(coachID)}&recent=1`
|
|
);
|
|
recentByCoach[coachID] = data.submissions || [];
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
recentByCoach[coachID] = [];
|
|
}
|
|
renderCoachTableBody();
|
|
}
|
|
}
|
|
|
|
function esc(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s ?? '';
|
|
return d.innerHTML;
|
|
}
|