added submission history, data insights, download all functionality
This commit is contained in:
198
website/js/pages/coaches.js
Normal file
198
website/js/pages/coaches.js
Normal file
@ -0,0 +1,198 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user