This commit is contained in:
@ -6,6 +6,7 @@ const PAGE_SIZE = 50;
|
||||
|
||||
let clientsList = [];
|
||||
let coachesList = [];
|
||||
let scoringProfileCatalog = [];
|
||||
let filterSearch = '';
|
||||
let filterTestData = '';
|
||||
let page = 0;
|
||||
@ -42,6 +43,7 @@ async function loadClients() {
|
||||
apiGet('assignments.php'),
|
||||
]);
|
||||
clientsList = clientsData.clients || [];
|
||||
scoringProfileCatalog = clientsData.scoringProfiles || [];
|
||||
coachesList = assignData.coaches || [];
|
||||
renderClients();
|
||||
} catch (e) {
|
||||
@ -65,6 +67,9 @@ function renderClients() {
|
||||
return;
|
||||
}
|
||||
|
||||
const showProfileColumn = scoringProfileCatalog.length > 0;
|
||||
const profileLegend = showProfileColumn ? scoringLegendHTML() : '';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="filter-bar">
|
||||
@ -76,6 +81,7 @@ function renderClients() {
|
||||
</select>
|
||||
<span class="data-toolbar-hint" id="clientListCount"></span>
|
||||
</div>
|
||||
${profileLegend}
|
||||
<p class="data-toolbar-hint" style="margin:0 0 12px">
|
||||
Expand a row to view questionnaire responses and upload version history.
|
||||
</p>
|
||||
@ -85,6 +91,7 @@ function renderClients() {
|
||||
<tr>
|
||||
<th>Client Code</th>
|
||||
<th>Current Coach</th>
|
||||
${showProfileColumn ? '<th>Profile scores</th>' : ''}
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -132,6 +139,7 @@ function renderClientTableBody() {
|
||||
const countEl = document.getElementById('clientListCount');
|
||||
const filtered = getFilteredClientsList();
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
||||
const colCount = scoringProfileCatalog.length > 0 ? 4 : 3;
|
||||
if (page >= totalPages) page = totalPages - 1;
|
||||
const slice = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
||||
|
||||
@ -140,7 +148,7 @@ function renderClientTableBody() {
|
||||
}
|
||||
|
||||
if (!slice.length) {
|
||||
body.innerHTML = `<tr><td colspan="3" style="text-align:center;color:var(--text-secondary);padding:24px">No clients match</td></tr>`;
|
||||
body.innerHTML = `<tr><td colspan="${colCount}" style="text-align:center;color:var(--text-secondary);padding:24px">No clients match</td></tr>`;
|
||||
} else {
|
||||
body.innerHTML = slice.map(c => clientRowHTML(c)).join('');
|
||||
body.querySelectorAll('.client-expand-btn').forEach(btn => {
|
||||
@ -243,9 +251,103 @@ function clientDetailPanelHTML(clientCode) {
|
||||
Coach: ${esc(cl.coachUsername || '—')}
|
||||
${cl.supervisorUsername ? ` · Supervisor: ${esc(cl.supervisorUsername)}` : ''}
|
||||
</p>
|
||||
${renderClientScoringProfiles(detail.scoringProfiles || [])}
|
||||
${questionnaires.map(q => clientQnBlockHTML(clientCode, q)).join('')}`;
|
||||
}
|
||||
|
||||
function scoringLegendHTML() {
|
||||
return `
|
||||
<div class="client-scoring-legend">
|
||||
<p class="client-scoring-legend-title"><strong>How profile scores work</strong></p>
|
||||
<p class="data-toolbar-hint client-scoring-legend-body">
|
||||
Each profile adds up questionnaire points (× weight) into a <strong>weighted total</strong>,
|
||||
then maps that total to <span class="band-badge band-green">green</span>
|
||||
<span class="band-badge band-yellow">yellow</span>
|
||||
<span class="band-badge band-red">red</span> using the thresholds on the Questionnaires page.
|
||||
</p>
|
||||
<dl class="client-scoring-legend-dl">
|
||||
<div>
|
||||
<dt>Calculated</dt>
|
||||
<dd>Automatic category from answers and profile rules (shown before coach review).</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Coach</dt>
|
||||
<dd>Category confirmed or changed by the coach in the app (“Review scores”).</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Incomplete</dt>
|
||||
<dd>Not all questionnaires in that profile are completed yet.</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function bandBadgeHTML(band, { pending = false, incomplete = false, label = '' } = {}) {
|
||||
const text = label || band || '';
|
||||
if (incomplete) {
|
||||
return '<span class="band-badge band-none">incomplete</span>';
|
||||
}
|
||||
if (pending) {
|
||||
return '<span class="band-badge band-none">awaiting review</span>';
|
||||
}
|
||||
if (!band) {
|
||||
return '<span class="band-badge band-none">—</span>';
|
||||
}
|
||||
return `<span class="band-badge band-${esc(band)}">${esc(text || band)}</span>`;
|
||||
}
|
||||
|
||||
function profileScoreBlockHTML(p) {
|
||||
const calcBand = p.calculatedBand || p.band;
|
||||
if (!calcBand) {
|
||||
return `
|
||||
<div class="client-profile-score-block is-incomplete">
|
||||
<div class="client-profile-score-block-title">${esc(p.name)}</div>
|
||||
<p class="client-profile-score-hint">Profile incomplete — not all member questionnaires are done.</p>
|
||||
</div>`;
|
||||
}
|
||||
const coachPending = p.pendingReview !== false && !p.coachBand;
|
||||
const coachDiffers = p.coachBand && p.coachBand !== calcBand;
|
||||
return `
|
||||
<div class="client-profile-score-block${coachDiffers ? ' has-coach-override' : ''}">
|
||||
<div class="client-profile-score-block-title">${esc(p.name)}</div>
|
||||
<div class="client-profile-score-row">
|
||||
<span class="client-profile-score-label">Calculated</span>
|
||||
${bandBadgeHTML(calcBand)}
|
||||
<span class="client-profile-score-total" title="Weighted total">${esc(String(p.weightedTotal))}</span>
|
||||
</div>
|
||||
<div class="client-profile-score-row">
|
||||
<span class="client-profile-score-label">Coach</span>
|
||||
${coachPending ? bandBadgeHTML(null, { pending: true }) : bandBadgeHTML(p.coachBand)}
|
||||
${coachDiffers ? '<span class="client-profile-override-hint">override</span>' : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderClientScoringProfiles(profiles) {
|
||||
if (!profiles.length) return '';
|
||||
return `
|
||||
<div class="client-scoring-profiles-detail">
|
||||
<strong>Scoring profiles</strong>
|
||||
<p class="data-toolbar-hint" style="margin:4px 0 10px">
|
||||
Weighted totals and calculated vs coach categories for this client.
|
||||
</p>
|
||||
<div class="client-profile-scores">${profiles.map(p => {
|
||||
const calc = p.calculatedBand || p.band;
|
||||
if (!calc) return profileScoreBlockHTML(p);
|
||||
const coachPending = p.pendingReview !== false && !p.coachBand;
|
||||
const computedAt = p.computedAt ? `Last computed ${esc(p.computedAt)}` : '';
|
||||
const reviewedAt = p.coachReviewedAt ? `Coach reviewed ${esc(p.coachReviewedAt)}` : '';
|
||||
const meta = [computedAt, reviewedAt].filter(Boolean).join(' · ');
|
||||
return `
|
||||
<div class="scoring-profile-detail-card">
|
||||
${profileScoreBlockHTML(p)}
|
||||
${meta ? `<p class="client-profile-score-meta">${meta}</p>` : ''}
|
||||
${coachPending ? '<p class="client-profile-score-hint">Coach has not reviewed this profile in the app yet.</p>' : ''}
|
||||
</div>`;
|
||||
}).join('')}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function clientQnBlockHTML(clientCode, q) {
|
||||
const key = qnKey(clientCode, q.questionnaireID);
|
||||
const qnExpanded = expandedQnKey === key;
|
||||
@ -311,6 +413,17 @@ function clientQnBlockHTML(clientCode, q) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function profileDotsHTML(profiles) {
|
||||
if (!profiles?.length) {
|
||||
return '<span class="client-profile-dots-empty">—</span>';
|
||||
}
|
||||
const hasAny = profiles.some(p => p.calculatedBand || p.band);
|
||||
if (!hasAny) {
|
||||
return `<span class="client-profile-dots-empty" title="No complete scoring profiles yet">—</span>`;
|
||||
}
|
||||
return `<div class="client-profile-scores">${profiles.map(p => profileScoreBlockHTML(p)).join('')}</div>`;
|
||||
}
|
||||
|
||||
function clientHasResponseData(c) {
|
||||
return Number(c.hasResponseData) === 1;
|
||||
}
|
||||
@ -324,6 +437,10 @@ function clientRowHTML(c) {
|
||||
const expandControl = canExpand
|
||||
? `<button type="button" class="btn btn-sm btn-link client-expand-btn" data-code="${esc(c.clientCode)}">${expanded ? '▼' : '▶'}</button> `
|
||||
: '';
|
||||
const profileCol = scoringProfileCatalog.length > 0
|
||||
? `<td class="client-profile-dots-cell">${profileDotsHTML(c.scoringProfiles || [])}</td>`
|
||||
: '';
|
||||
const colCount = scoringProfileCatalog.length > 0 ? 4 : 3;
|
||||
|
||||
return `
|
||||
<tr class="${testDataRowClassForClient(c.clientCode).trim()}">
|
||||
@ -331,13 +448,14 @@ function clientRowHTML(c) {
|
||||
${expandControl}<strong>${esc(c.clientCode)}</strong>
|
||||
</td>
|
||||
<td>${coach}</td>
|
||||
${profileCol}
|
||||
<td>
|
||||
<button class="btn btn-sm btn-danger delete-client-btn" data-code="${esc(c.clientCode)}">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
${expanded ? `
|
||||
<tr class="client-detail-row">
|
||||
<td colspan="3">
|
||||
<td colspan="${colCount}">
|
||||
<div class="client-detail-panel">${clientDetailPanelHTML(c.clientCode)}</div>
|
||||
</td>
|
||||
</tr>` : ''}`;
|
||||
|
||||
Reference in New Issue
Block a user