good client and coach data viewing
This commit is contained in:
@ -13,9 +13,11 @@ if ($method !== 'GET') {
|
|||||||
try {
|
try {
|
||||||
$coachID = trim((string)($_GET['coachID'] ?? ''));
|
$coachID = trim((string)($_GET['coachID'] ?? ''));
|
||||||
if ($coachID !== '' && !empty($_GET['recent'])) {
|
if ($coachID !== '' && !empty($_GET['recent'])) {
|
||||||
$recent = qdb_coach_recent_submissions($pdo, $tokenRec, $coachID);
|
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 100;
|
||||||
|
$offset = isset($_GET['offset']) ? (int)$_GET['offset'] : 0;
|
||||||
|
$recent = qdb_coach_recent_submissions($pdo, $tokenRec, $coachID, $limit, $offset);
|
||||||
qdb_discard($tmpDb, $lockFp);
|
qdb_discard($tmpDb, $lockFp);
|
||||||
json_success(['submissions' => $recent]);
|
json_success($recent);
|
||||||
}
|
}
|
||||||
|
|
||||||
$coaches = qdb_coach_activity_list($pdo, $tokenRec);
|
$coaches = qdb_coach_activity_list($pdo, $tokenRec);
|
||||||
|
|||||||
@ -251,7 +251,13 @@ function qdb_coach_activity_list(PDO $pdo, array $tokenRec): array {
|
|||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function qdb_coach_recent_submissions(PDO $pdo, array $tokenRec, string $coachID, int $limit = 25): array {
|
function qdb_coach_recent_submissions(
|
||||||
|
PDO $pdo,
|
||||||
|
array $tokenRec,
|
||||||
|
string $coachID,
|
||||||
|
int $limit = 100,
|
||||||
|
int $offset = 0
|
||||||
|
): array {
|
||||||
$coachID = trim($coachID);
|
$coachID = trim($coachID);
|
||||||
if ($coachID === '') {
|
if ($coachID === '') {
|
||||||
json_error('INVALID_FIELD', 'coachID is required', 400);
|
json_error('INVALID_FIELD', 'coachID is required', 400);
|
||||||
@ -271,7 +277,18 @@ function qdb_coach_recent_submissions(PDO $pdo, array $tokenRec, string $coachID
|
|||||||
}
|
}
|
||||||
|
|
||||||
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
|
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
|
||||||
$limit = max(1, min($limit, 100));
|
$limit = max(1, min($limit, 200));
|
||||||
|
$offset = max(0, $offset);
|
||||||
|
|
||||||
|
$countSql = "
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM questionnaire_submission qs
|
||||||
|
INNER JOIN client cl ON cl.clientCode = qs.clientCode
|
||||||
|
WHERE cl.coachID = :cid AND ($rbacClause)
|
||||||
|
";
|
||||||
|
$countStmt = $pdo->prepare($countSql);
|
||||||
|
$countStmt->execute(array_merge([':cid' => $coachID], $rbacParams));
|
||||||
|
$total = (int)$countStmt->fetchColumn();
|
||||||
|
|
||||||
$sql = "
|
$sql = "
|
||||||
SELECT qs.submissionID, qs.version, qs.submittedAt, qs.submittedByRole,
|
SELECT qs.submissionID, qs.version, qs.submittedAt, qs.submittedByRole,
|
||||||
@ -281,13 +298,13 @@ function qdb_coach_recent_submissions(PDO $pdo, array $tokenRec, string $coachID
|
|||||||
LEFT JOIN questionnaire qn ON qn.questionnaireID = qs.questionnaireID
|
LEFT JOIN questionnaire qn ON qn.questionnaireID = qs.questionnaireID
|
||||||
WHERE cl.coachID = :cid AND ($rbacClause)
|
WHERE cl.coachID = :cid AND ($rbacClause)
|
||||||
ORDER BY qs.submittedAt DESC
|
ORDER BY qs.submittedAt DESC
|
||||||
LIMIT $limit
|
LIMIT $limit OFFSET $offset
|
||||||
";
|
";
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
$stmt->execute(array_merge([':cid' => $coachID], $rbacParams));
|
$stmt->execute(array_merge([':cid' => $coachID], $rbacParams));
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
return array_map(static function ($r) {
|
$submissions = array_map(static function ($r) {
|
||||||
return [
|
return [
|
||||||
'submissionID' => $r['submissionID'],
|
'submissionID' => $r['submissionID'],
|
||||||
'version' => (int)$r['version'],
|
'version' => (int)$r['version'],
|
||||||
@ -298,4 +315,11 @@ function qdb_coach_recent_submissions(PDO $pdo, array $tokenRec, string $coachID
|
|||||||
'questionnaireName'=> $r['questionnaireName'] ?? $r['questionnaireID'],
|
'questionnaireName'=> $r['questionnaireName'] ?? $r['questionnaireID'],
|
||||||
];
|
];
|
||||||
}, $rows);
|
}, $rows);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'submissions' => $submissions,
|
||||||
|
'total' => $total,
|
||||||
|
'limit' => $limit,
|
||||||
|
'offset' => $offset,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1872,6 +1872,65 @@ table.data-table tbody tr.row-orphan-category td {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin: 4px 0 8px;
|
margin: 4px 0 8px;
|
||||||
}
|
}
|
||||||
|
.coach-recent-scroll {
|
||||||
|
max-height: min(420px, 55vh);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.coach-recent-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.coach-recent-toolbar .filter-search {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 180px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
.coach-client-groups {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.coach-client-group {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: var(--surface);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.coach-client-toggle {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px 10px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: 10px 12px !important;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.coach-client-meta {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.coach-client-uploads {
|
||||||
|
padding: 0 12px 12px 32px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.coach-recent-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 4px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
.data-table-compact td,
|
.data-table-compact td,
|
||||||
.data-table-compact th {
|
.data-table-compact th {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
|
|||||||
@ -1,9 +1,16 @@
|
|||||||
import { apiGet } from '../api.js';
|
import { apiGet } from '../api.js';
|
||||||
import { showToast } from '../app.js';
|
import { showToast } from '../app.js';
|
||||||
|
|
||||||
|
const FETCH_LIMIT = 100;
|
||||||
|
const CLIENT_GROUP_PAGE = 10;
|
||||||
|
const UPLOADS_VISIBLE = 5;
|
||||||
|
|
||||||
let coachesList = [];
|
let coachesList = [];
|
||||||
let expandedCoachId = null;
|
let expandedCoachId = null;
|
||||||
|
/** @type {Record<string, { submissions: object[], total: number }>} */
|
||||||
let recentByCoach = {};
|
let recentByCoach = {};
|
||||||
|
/** @type {Record<string, { clientLimit: number, expandedClients: Set<string>, showAllUploads: Set<string>, filter: string }>} */
|
||||||
|
let coachDetailUi = {};
|
||||||
let filterSearch = '';
|
let filterSearch = '';
|
||||||
|
|
||||||
export async function coachesPage() {
|
export async function coachesPage() {
|
||||||
@ -41,6 +48,149 @@ function filteredCoaches() {
|
|||||||
return coachesList.filter(c => coachSearchText(c).toLowerCase().includes(q));
|
return coachesList.filter(c => coachSearchText(c).toLowerCase().includes(q));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCoachUi(coachID) {
|
||||||
|
if (!coachDetailUi[coachID]) {
|
||||||
|
coachDetailUi[coachID] = {
|
||||||
|
clientLimit: CLIENT_GROUP_PAGE,
|
||||||
|
expandedClients: new Set(),
|
||||||
|
showAllUploads: new Set(),
|
||||||
|
filter: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return coachDetailUi[coachID];
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupSubmissionsByClient(submissions) {
|
||||||
|
const map = new Map();
|
||||||
|
for (const s of submissions) {
|
||||||
|
const code = s.clientCode || '';
|
||||||
|
if (!map.has(code)) {
|
||||||
|
map.set(code, []);
|
||||||
|
}
|
||||||
|
map.get(code).push(s);
|
||||||
|
}
|
||||||
|
const groups = [];
|
||||||
|
for (const [clientCode, uploads] of map) {
|
||||||
|
uploads.sort((a, b) => String(b.submittedAt).localeCompare(String(a.submittedAt)));
|
||||||
|
groups.push({
|
||||||
|
clientCode,
|
||||||
|
uploads,
|
||||||
|
count: uploads.length,
|
||||||
|
lastAt: uploads[0]?.submittedAt || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
groups.sort((a, b) => String(b.lastAt).localeCompare(String(a.lastAt)));
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterClientGroups(groups, q) {
|
||||||
|
if (!q) return groups;
|
||||||
|
const needle = q.toLowerCase();
|
||||||
|
return groups.filter(g => {
|
||||||
|
if (g.clientCode.toLowerCase().includes(needle)) return true;
|
||||||
|
return g.uploads.some(u =>
|
||||||
|
(u.questionnaireName || '').toLowerCase().includes(needle)
|
||||||
|
|| (u.questionnaireID || '').toLowerCase().includes(needle)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadsTableHTML(uploads) {
|
||||||
|
if (!uploads.length) {
|
||||||
|
return '<p class="data-toolbar-hint">No uploads</p>';
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<table class="data-table data-table-compact">
|
||||||
|
<thead>
|
||||||
|
<tr><th>When</th><th>Questionnaire</th><th>Ver.</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${uploads.map(s => `
|
||||||
|
<tr>
|
||||||
|
<td>${esc(s.submittedAt)}</td>
|
||||||
|
<td>${esc(s.questionnaireName)}</td>
|
||||||
|
<td>${s.version}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function coachRecentPanelHTML(coachID) {
|
||||||
|
const data = recentByCoach[coachID];
|
||||||
|
if (!data) {
|
||||||
|
return '<p class="data-toolbar-hint">Loading…</p>';
|
||||||
|
}
|
||||||
|
const ui = getCoachUi(coachID);
|
||||||
|
const allGroups = groupSubmissionsByClient(data.submissions || []);
|
||||||
|
const groups = filterClientGroups(allGroups, ui.filter.trim());
|
||||||
|
const visibleGroups = groups.slice(0, ui.clientLimit);
|
||||||
|
const hasMoreClients = groups.length > ui.clientLimit;
|
||||||
|
const hasMoreUploads = (data.submissions?.length ?? 0) < (data.total ?? 0);
|
||||||
|
|
||||||
|
const summary = data.total
|
||||||
|
? `${data.total} upload${data.total === 1 ? '' : 's'} · ${allGroups.length} client${allGroups.length === 1 ? '' : 's'}`
|
||||||
|
: 'No uploads';
|
||||||
|
|
||||||
|
let groupsHTML = '';
|
||||||
|
if (!groups.length) {
|
||||||
|
groupsHTML = `<p class="data-toolbar-hint">${ui.filter.trim() ? 'No matches' : 'No uploads'}</p>`;
|
||||||
|
} else {
|
||||||
|
groupsHTML = visibleGroups.map(g => {
|
||||||
|
const expanded = ui.expandedClients.has(g.clientCode);
|
||||||
|
const showAll = ui.showAllUploads.has(g.clientCode);
|
||||||
|
const visibleUploads = showAll ? g.uploads : g.uploads.slice(0, UPLOADS_VISIBLE);
|
||||||
|
const hiddenCount = g.uploads.length - UPLOADS_VISIBLE;
|
||||||
|
return `
|
||||||
|
<div class="coach-client-group" data-client="${esc(g.clientCode)}">
|
||||||
|
<button type="button" class="coach-client-toggle btn btn-sm btn-link"
|
||||||
|
data-coach="${esc(coachID)}" data-client="${esc(g.clientCode)}">
|
||||||
|
${expanded ? '▼' : '▶'}
|
||||||
|
<code>${esc(g.clientCode)}</code>
|
||||||
|
<span class="coach-client-meta">${g.count} upload${g.count === 1 ? '' : 's'} · last ${esc(g.lastAt || '—')}</span>
|
||||||
|
</button>
|
||||||
|
${expanded ? `
|
||||||
|
<div class="coach-client-uploads">
|
||||||
|
${uploadsTableHTML(visibleUploads)}
|
||||||
|
${!showAll && hiddenCount > 0 ? `
|
||||||
|
<button type="button" class="btn btn-sm coach-show-all-uploads"
|
||||||
|
data-coach="${esc(coachID)}" data-client="${esc(g.clientCode)}">
|
||||||
|
Show all ${g.count} uploads
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="coach-recent-panel coach-recent-scroll" data-coach-panel="${esc(coachID)}">
|
||||||
|
<div class="coach-recent-toolbar">
|
||||||
|
<input type="search" class="filter-search coach-detail-search"
|
||||||
|
placeholder="Filter client or questionnaire…"
|
||||||
|
value="${esc(ui.filter)}"
|
||||||
|
data-coach="${esc(coachID)}">
|
||||||
|
<span class="data-toolbar-hint">${esc(summary)}${ui.filter.trim() && groups.length !== allGroups.length
|
||||||
|
? ` · ${groups.length} shown`
|
||||||
|
: ''}</span>
|
||||||
|
</div>
|
||||||
|
<div class="coach-client-groups">${groupsHTML}</div>
|
||||||
|
<div class="coach-recent-actions">
|
||||||
|
${hasMoreClients ? `
|
||||||
|
<button type="button" class="btn btn-sm coach-more-clients" data-coach="${esc(coachID)}">
|
||||||
|
Show more clients (${groups.length - ui.clientLimit} remaining)
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
${hasMoreUploads ? `
|
||||||
|
<button type="button" class="btn btn-sm coach-more-uploads" data-coach="${esc(coachID)}">
|
||||||
|
Load more uploads (${(data.total ?? 0) - (data.submissions?.length ?? 0)} remaining)
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderCoaches() {
|
function renderCoaches() {
|
||||||
const el = document.getElementById('coachesContent');
|
const el = document.getElementById('coachesContent');
|
||||||
|
|
||||||
@ -51,7 +201,9 @@ function renderCoaches() {
|
|||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="card">
|
<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>
|
<p class="data-toolbar-hint" style="margin:0 0 12px">
|
||||||
|
Track uploads and client load per coach. Expand a row to browse uploads by client.
|
||||||
|
</p>
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
<input type="search" id="coachListSearch" class="filter-search"
|
<input type="search" id="coachListSearch" class="filter-search"
|
||||||
placeholder="Search coach or supervisor…" value="${esc(filterSearch)}">
|
placeholder="Search coach or supervisor…" value="${esc(filterSearch)}">
|
||||||
@ -89,7 +241,6 @@ function renderCoaches() {
|
|||||||
function coachRowHTML(c) {
|
function coachRowHTML(c) {
|
||||||
const canExpand = coachHasUploads(c);
|
const canExpand = coachHasUploads(c);
|
||||||
const expanded = canExpand && expandedCoachId === c.coachID;
|
const expanded = canExpand && expandedCoachId === c.coachID;
|
||||||
const recent = recentByCoach[c.coachID] || [];
|
|
||||||
const expandControl = canExpand
|
const expandControl = canExpand
|
||||||
? `<button type="button" class="btn btn-sm btn-link coach-expand-btn" data-coach="${esc(c.coachID)}">${expanded ? '▼' : '▶'}</button> `
|
? `<button type="button" class="btn btn-sm btn-link coach-expand-btn" data-coach="${esc(c.coachID)}">${expanded ? '▼' : '▶'}</button> `
|
||||||
: '';
|
: '';
|
||||||
@ -105,30 +256,50 @@ function coachRowHTML(c) {
|
|||||||
</tr>
|
</tr>
|
||||||
${expanded ? `
|
${expanded ? `
|
||||||
<tr class="coach-detail-row">
|
<tr class="coach-detail-row">
|
||||||
<td colspan="7">
|
<td colspan="7">${coachRecentPanelHTML(c.coachID)}</td>
|
||||||
<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>` : ''}`;
|
</tr>` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wireCoachDetailPanel(coachID) {
|
||||||
|
const panel = document.querySelector(`[data-coach-panel="${CSS.escape(coachID)}"]`);
|
||||||
|
if (!panel) return;
|
||||||
|
|
||||||
|
panel.querySelector('.coach-detail-search')?.addEventListener('input', (e) => {
|
||||||
|
getCoachUi(coachID).filter = e.target.value;
|
||||||
|
getCoachUi(coachID).clientLimit = CLIENT_GROUP_PAGE;
|
||||||
|
renderCoachTableBody();
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.querySelectorAll('.coach-client-toggle').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const ui = getCoachUi(coachID);
|
||||||
|
const client = btn.dataset.client;
|
||||||
|
if (ui.expandedClients.has(client)) {
|
||||||
|
ui.expandedClients.delete(client);
|
||||||
|
} else {
|
||||||
|
ui.expandedClients.add(client);
|
||||||
|
}
|
||||||
|
renderCoachTableBody();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.querySelectorAll('.coach-show-all-uploads').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
getCoachUi(coachID).showAllUploads.add(btn.dataset.client);
|
||||||
|
renderCoachTableBody();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.querySelector('.coach-more-clients')?.addEventListener('click', () => {
|
||||||
|
getCoachUi(coachID).clientLimit += CLIENT_GROUP_PAGE;
|
||||||
|
renderCoachTableBody();
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.querySelector('.coach-more-uploads')?.addEventListener('click', () => {
|
||||||
|
loadMoreCoachUploads(coachID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderCoachTableBody() {
|
function renderCoachTableBody() {
|
||||||
const body = document.getElementById('coachTableBody');
|
const body = document.getElementById('coachTableBody');
|
||||||
const countEl = document.getElementById('coachListCount');
|
const countEl = document.getElementById('coachListCount');
|
||||||
@ -163,6 +334,58 @@ function renderCoachTableBody() {
|
|||||||
body.querySelectorAll('.coach-expand-btn').forEach(btn => {
|
body.querySelectorAll('.coach-expand-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => toggleCoach(btn.dataset.coach));
|
btn.addEventListener('click', () => toggleCoach(btn.dataset.coach));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (expandedCoachId) {
|
||||||
|
wireCoachDetailPanel(expandedCoachId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCoachRecent(coachID, offset = 0, append = false) {
|
||||||
|
const data = await apiGet(
|
||||||
|
`coaches.php?coachID=${encodeURIComponent(coachID)}&recent=1`
|
||||||
|
+ `&limit=${FETCH_LIMIT}&offset=${offset}`
|
||||||
|
);
|
||||||
|
const submissions = data.submissions || [];
|
||||||
|
if (append && recentByCoach[coachID]) {
|
||||||
|
const existing = recentByCoach[coachID].submissions || [];
|
||||||
|
const seen = new Set(existing.map(s => s.submissionID));
|
||||||
|
for (const s of submissions) {
|
||||||
|
if (!seen.has(s.submissionID)) {
|
||||||
|
existing.push(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recentByCoach[coachID] = {
|
||||||
|
submissions: existing,
|
||||||
|
total: data.total ?? existing.length,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
recentByCoach[coachID] = {
|
||||||
|
submissions,
|
||||||
|
total: data.total ?? submissions.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMoreCoachUploads(coachID) {
|
||||||
|
const data = recentByCoach[coachID];
|
||||||
|
if (!data) return;
|
||||||
|
const btn = document.querySelector(
|
||||||
|
`[data-coach-panel="${CSS.escape(coachID)}"] .coach-more-uploads`
|
||||||
|
);
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Loading…';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await fetchCoachRecent(coachID, data.submissions.length, true);
|
||||||
|
renderCoachTableBody();
|
||||||
|
} catch (e) {
|
||||||
|
showToast(e.message, 'error');
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Load more uploads';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleCoach(coachID) {
|
async function toggleCoach(coachID) {
|
||||||
@ -176,16 +399,21 @@ async function toggleCoach(coachID) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
expandedCoachId = coachID;
|
expandedCoachId = coachID;
|
||||||
|
if (!coachDetailUi[coachID]) {
|
||||||
|
coachDetailUi[coachID] = {
|
||||||
|
clientLimit: CLIENT_GROUP_PAGE,
|
||||||
|
expandedClients: new Set(),
|
||||||
|
showAllUploads: new Set(),
|
||||||
|
filter: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
renderCoachTableBody();
|
renderCoachTableBody();
|
||||||
if (!recentByCoach[coachID]) {
|
if (!recentByCoach[coachID]) {
|
||||||
try {
|
try {
|
||||||
const data = await apiGet(
|
await fetchCoachRecent(coachID, 0, false);
|
||||||
`coaches.php?coachID=${encodeURIComponent(coachID)}&recent=1`
|
|
||||||
);
|
|
||||||
recentByCoach[coachID] = data.submissions || [];
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e.message, 'error');
|
showToast(e.message, 'error');
|
||||||
recentByCoach[coachID] = [];
|
recentByCoach[coachID] = { submissions: [], total: 0 };
|
||||||
}
|
}
|
||||||
renderCoachTableBody();
|
renderCoachTableBody();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user