added website overwrite for score decision
Some checks failed
PHPUnit / test (push) Has been cancelled
Some checks failed
PHPUnit / test (push) Has been cancelled
This commit is contained in:
@ -3034,6 +3034,20 @@ select:disabled {
|
||||
letter-spacing: .04em;
|
||||
color: #ca8a04;
|
||||
}
|
||||
.client-coach-band-select {
|
||||
min-width: 6.5rem;
|
||||
max-width: 100%;
|
||||
padding: 2px 6px;
|
||||
font-size: .75rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface, #fff);
|
||||
color: var(--text);
|
||||
}
|
||||
.client-coach-band-select:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: wait;
|
||||
}
|
||||
.client-profile-score-hint,
|
||||
.client-profile-score-meta {
|
||||
margin: 6px 0 0;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { apiGet, apiPost, apiDelete } from '../api.js';
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
||||
import { pageHeaderHTML, showToast } from '../app.js';
|
||||
import { isDevTestClientCode, testDataRowClassForClient } from '../test-data.js';
|
||||
|
||||
@ -167,6 +167,12 @@ function renderClientTableBody() {
|
||||
body.querySelectorAll('.delete-client-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => deleteClient(btn.dataset.code));
|
||||
});
|
||||
body.querySelectorAll('.client-coach-band-select').forEach(select => {
|
||||
select.addEventListener('focus', () => {
|
||||
select.dataset.prev = select.value;
|
||||
});
|
||||
select.addEventListener('change', () => onCoachBandChange(select));
|
||||
});
|
||||
}
|
||||
|
||||
const pag = document.getElementById('clientsPagination');
|
||||
@ -251,7 +257,7 @@ function clientDetailPanelHTML(clientCode) {
|
||||
Coach: ${esc(cl.coachUsername || '—')}
|
||||
${cl.supervisorUsername ? ` · Supervisor: ${esc(cl.supervisorUsername)}` : ''}
|
||||
</p>
|
||||
${renderClientScoringProfiles(detail.scoringProfiles || [])}
|
||||
${renderClientScoringProfiles(detail.scoringProfiles || [], clientCode)}
|
||||
${questionnaires.map(q => clientQnBlockHTML(clientCode, q)).join('')}`;
|
||||
}
|
||||
|
||||
@ -296,7 +302,39 @@ function bandBadgeHTML(band, { pending = false, incomplete = false, label = '' }
|
||||
return `<span class="band-badge band-${esc(band)}">${esc(text || band)}</span>`;
|
||||
}
|
||||
|
||||
function profileScoreBlockHTML(p) {
|
||||
const COACH_BANDS = ['green', 'yellow', 'red'];
|
||||
|
||||
function bandLabel(band) {
|
||||
if (!band) return '—';
|
||||
return band.charAt(0).toUpperCase() + band.slice(1);
|
||||
}
|
||||
|
||||
function coachBandSelectHTML(p, clientCode) {
|
||||
const calcBand = p.calculatedBand || p.band;
|
||||
if (!calcBand || !clientCode || !p.profileID) {
|
||||
return '';
|
||||
}
|
||||
const current = p.coachBand || '';
|
||||
const coachPending = p.pendingReview !== false && !current;
|
||||
const placeholder = coachPending
|
||||
? '<option value="" selected disabled>Awaiting review</option>'
|
||||
: '';
|
||||
const options = COACH_BANDS.map(b => {
|
||||
const selected = current === b ? ' selected' : '';
|
||||
return `<option value="${esc(b)}"${selected}>${esc(bandLabel(b))}</option>`;
|
||||
}).join('');
|
||||
return `
|
||||
<select class="client-coach-band-select"
|
||||
data-client="${esc(clientCode)}"
|
||||
data-profile="${esc(p.profileID)}"
|
||||
data-profile-name="${esc(p.name || p.profileID)}"
|
||||
data-calculated="${esc(calcBand)}"
|
||||
data-weighted="${esc(String(p.weightedTotal ?? ''))}"
|
||||
data-prev="${esc(current)}"
|
||||
title="Set coach category">${placeholder}${options}</select>`;
|
||||
}
|
||||
|
||||
function profileScoreBlockHTML(p, clientCode = '') {
|
||||
const calcBand = p.calculatedBand || p.band;
|
||||
if (!calcBand) {
|
||||
return `
|
||||
@ -307,8 +345,11 @@ function profileScoreBlockHTML(p) {
|
||||
}
|
||||
const coachPending = p.pendingReview !== false && !p.coachBand;
|
||||
const coachDiffers = p.coachBand && p.coachBand !== calcBand;
|
||||
const coachControl = clientCode
|
||||
? coachBandSelectHTML(p, clientCode)
|
||||
: (coachPending ? bandBadgeHTML(null, { pending: true }) : bandBadgeHTML(p.coachBand));
|
||||
return `
|
||||
<div class="client-profile-score-block${coachDiffers ? ' has-coach-override' : ''}">
|
||||
<div class="client-profile-score-block${coachDiffers ? ' has-coach-override' : ''}" data-profile-id="${esc(p.profileID || '')}">
|
||||
<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>
|
||||
@ -317,13 +358,13 @@ function profileScoreBlockHTML(p) {
|
||||
</div>
|
||||
<div class="client-profile-score-row">
|
||||
<span class="client-profile-score-label">Coach</span>
|
||||
${coachPending ? bandBadgeHTML(null, { pending: true }) : bandBadgeHTML(p.coachBand)}
|
||||
${coachControl}
|
||||
${coachDiffers ? '<span class="client-profile-override-hint">override</span>' : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderClientScoringProfiles(profiles) {
|
||||
function renderClientScoringProfiles(profiles, clientCode = '') {
|
||||
if (!profiles.length) return '';
|
||||
return `
|
||||
<div class="client-scoring-profiles-detail">
|
||||
@ -333,14 +374,14 @@ function renderClientScoringProfiles(profiles) {
|
||||
</p>
|
||||
<div class="client-profile-scores">${profiles.map(p => {
|
||||
const calc = p.calculatedBand || p.band;
|
||||
if (!calc) return profileScoreBlockHTML(p);
|
||||
if (!calc) return profileScoreBlockHTML(p, clientCode);
|
||||
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)}
|
||||
${profileScoreBlockHTML(p, clientCode)}
|
||||
${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>`;
|
||||
@ -413,7 +454,7 @@ function clientQnBlockHTML(clientCode, q) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function profileDotsHTML(profiles) {
|
||||
function profileDotsHTML(profiles, clientCode) {
|
||||
if (!profiles?.length) {
|
||||
return '<span class="client-profile-dots-empty">—</span>';
|
||||
}
|
||||
@ -421,7 +462,71 @@ function profileDotsHTML(profiles) {
|
||||
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>`;
|
||||
return `<div class="client-profile-scores">${profiles.map(p => profileScoreBlockHTML(p, clientCode)).join('')}</div>`;
|
||||
}
|
||||
|
||||
function patchClientScoringBand(clientCode, profileID, coachBand) {
|
||||
const client = clientsList.find(c => c.clientCode === clientCode);
|
||||
if (client?.scoringProfiles) {
|
||||
const profile = client.scoringProfiles.find(p => p.profileID === profileID);
|
||||
if (profile) {
|
||||
profile.coachBand = coachBand;
|
||||
profile.pendingReview = false;
|
||||
}
|
||||
}
|
||||
const detail = detailByClient[clientCode];
|
||||
if (detail?.scoringProfiles) {
|
||||
const profile = detail.scoringProfiles.find(p => p.profileID === profileID);
|
||||
if (profile) {
|
||||
profile.coachBand = coachBand;
|
||||
profile.pendingReview = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onCoachBandChange(select) {
|
||||
const clientCode = select.dataset.client;
|
||||
const profileID = select.dataset.profile;
|
||||
const profileName = select.dataset.profileName || profileID;
|
||||
const calculatedBand = select.dataset.calculated;
|
||||
const weightedRaw = select.dataset.weighted;
|
||||
const weightedTotal = weightedRaw !== '' ? Number(weightedRaw) : null;
|
||||
const prev = select.dataset.prev || '';
|
||||
const coachBand = select.value;
|
||||
|
||||
if (!coachBand) {
|
||||
select.value = prev;
|
||||
return;
|
||||
}
|
||||
if (coachBand === prev) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = `Set coach category to "${bandLabel(coachBand)}" for ${profileName} (${clientCode})?\n\nCalculated category: ${bandLabel(calculatedBand)}.`;
|
||||
if (!confirm(msg)) {
|
||||
select.value = prev || '';
|
||||
return;
|
||||
}
|
||||
|
||||
select.disabled = true;
|
||||
try {
|
||||
await apiPut('clients.php', {
|
||||
clientCode,
|
||||
profileID,
|
||||
coachBand,
|
||||
calculatedBand,
|
||||
weightedTotal,
|
||||
});
|
||||
patchClientScoringBand(clientCode, profileID, coachBand);
|
||||
select.dataset.prev = coachBand;
|
||||
showToast(`Coach category saved for ${profileName}`, 'success');
|
||||
renderClientTableBody();
|
||||
} catch (e) {
|
||||
select.value = prev || '';
|
||||
showToast(e.message, 'error');
|
||||
} finally {
|
||||
select.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clientHasResponseData(c) {
|
||||
@ -438,7 +543,7 @@ function clientRowHTML(c) {
|
||||
? `<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>`
|
||||
? `<td class="client-profile-dots-cell">${profileDotsHTML(c.scoringProfiles || [], c.clientCode)}</td>`
|
||||
: '';
|
||||
const colCount = scoringProfileCatalog.length > 0 ? 4 : 3;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user