Updated UI strings and references from "coach" to "counselor"
Some checks failed
PHPUnit / test (push) Has been cancelled
Some checks failed
PHPUnit / test (push) Has been cancelled
This commit is contained in:
@ -107,7 +107,8 @@ function updateNav() {
|
||||
if (isLoggedIn()) {
|
||||
nav.style.display = '';
|
||||
const role = getRole();
|
||||
userInfo.textContent = `${getUser()} (${role})`;
|
||||
const roleLabel = role === 'coach' ? 'counselor' : role;
|
||||
userInfo.textContent = `${getUser()} (${roleLabel})`;
|
||||
|
||||
// Show/hide role-gated nav items
|
||||
document.querySelectorAll('[data-nav-roles]').forEach(li => {
|
||||
|
||||
@ -393,7 +393,7 @@ export function renderConditionEditorHTML(form, {
|
||||
<div class="condition-editor" id="conditionEditor">
|
||||
<h4 style="margin:0 0 12px;font-size:.95rem">Availability</h4>
|
||||
<div class="form-group">
|
||||
<label>When can coaches open this questionnaire?</label>
|
||||
<label>When can counselors open this questionnaire?</label>
|
||||
<select id="conditionMode"${dis}>${modeOptions}</select>
|
||||
</div>
|
||||
|
||||
|
||||
@ -53,8 +53,8 @@ function renderAssignments() {
|
||||
if (!coaches.length) {
|
||||
container.innerHTML = `
|
||||
<div class="card empty-state">
|
||||
<h3>No coaches found</h3>
|
||||
<p>Create coaches first before assigning clients.</p>
|
||||
<h3>No counselors found</h3>
|
||||
<p>Create counselors first before assigning clients.</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
@ -65,16 +65,16 @@ function renderAssignments() {
|
||||
<div class="assign-page">
|
||||
<div class="assign-layout">
|
||||
|
||||
<aside class="card assign-coaches" aria-label="Coaches">
|
||||
<aside class="card assign-coaches" aria-label="Counselors">
|
||||
<div class="assign-panel-header">
|
||||
<h3>Coaches <span class="data-toolbar-hint">(${coaches.length})</span></h3>
|
||||
<h3>Counselors <span class="data-toolbar-hint">(${coaches.length})</span></h3>
|
||||
<div class="filter-bar">
|
||||
<input type="search" id="coachSearch" class="filter-search"
|
||||
placeholder="Search coach or supervisor…" value="${esc(filterCoachSearch)}">
|
||||
placeholder="Search counselor or supervisor…" value="${esc(filterCoachSearch)}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="assign-panel-body">
|
||||
<ul class="coach-list" id="coachList" role="listbox" aria-label="Filter by coach"></ul>
|
||||
<ul class="coach-list" id="coachList" role="listbox" aria-label="Filter by counselor"></ul>
|
||||
</div>
|
||||
<div class="assign-panel-footer assign-panel-footer--align" aria-hidden="true"></div>
|
||||
</aside>
|
||||
@ -89,8 +89,8 @@ function renderAssignments() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="assign-clients-filters filter-bar">
|
||||
<select id="filterByCoach" aria-label="Filter by coach">
|
||||
<option value="">All coaches (${clients.length})</option>
|
||||
<select id="filterByCoach" aria-label="Filter by counselor">
|
||||
<option value="">All counselors (${clients.length})</option>
|
||||
<option value="__unassigned__">Unassigned (${unassignedCount})</option>
|
||||
${coaches.map(c => {
|
||||
const n = (clientsByCoach[c.coachID] || []).length;
|
||||
@ -98,7 +98,7 @@ function renderAssignments() {
|
||||
}).join('')}
|
||||
</select>
|
||||
<input type="search" id="clientSearch" class="filter-search"
|
||||
placeholder="Search client or coach…">
|
||||
placeholder="Search client or counselor…">
|
||||
</div>
|
||||
</div>
|
||||
<div class="assign-table-panel">
|
||||
@ -110,7 +110,7 @@ function renderAssignments() {
|
||||
<input type="checkbox" id="selectAllCb" title="Toggle all visible rows" aria-label="Select all visible">
|
||||
</th>
|
||||
<th>Client Code</th>
|
||||
<th>Current Coach</th>
|
||||
<th>Current Counselor</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="clientTableBody"></tbody>
|
||||
@ -127,8 +127,8 @@ function renderAssignments() {
|
||||
<section class="card assign-action-card" id="assignActionCard" style="display:none" aria-label="Assign selection">
|
||||
<h4>Assign selected clients to</h4>
|
||||
<div class="assign-action-row">
|
||||
<select id="targetCoachSelect" aria-label="Target coach">
|
||||
<option value="">— select coach —</option>
|
||||
<select id="targetCoachSelect" aria-label="Target counselor">
|
||||
<option value="">— select counselor —</option>
|
||||
${coaches.map(c =>
|
||||
`<option value="${c.coachID}">${esc(c.username)}${c.supervisorUsername ? ` (${esc(c.supervisorUsername)})` : ''}</option>`
|
||||
).join('')}
|
||||
@ -290,7 +290,7 @@ function renderCoachList() {
|
||||
const q = filterCoachSearch.trim();
|
||||
|
||||
if (!filtered.length) {
|
||||
list.innerHTML = `<li class="coach-list-empty">${q ? 'No coaches match' : 'No coaches'}</li>`;
|
||||
list.innerHTML = `<li class="coach-list-empty">${q ? 'No counselors match' : 'No counselors'}</li>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -496,7 +496,7 @@ async function doAssign() {
|
||||
const coachID = document.getElementById('targetCoachSelect').value;
|
||||
|
||||
if (!selected.length) { showToast('Select at least one client', 'error'); return; }
|
||||
if (!coachID) { showToast('Select a target coach', 'error'); return; }
|
||||
if (!coachID) { showToast('Select a target counselor', 'error'); return; }
|
||||
|
||||
const coachName = coaches.find(c => c.coachID === coachID)?.username || coachID;
|
||||
if (!confirm(`Assign ${selected.length} client(s) to ${coachName}?`)) return;
|
||||
|
||||
@ -71,7 +71,7 @@ function renderClients() {
|
||||
container.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="filter-bar">
|
||||
<input type="search" id="clientListSearch" class="filter-search" placeholder="Search client or coach…" value="${esc(filterSearch)}">
|
||||
<input type="search" id="clientListSearch" class="filter-search" placeholder="Search client or counselor…" value="${esc(filterSearch)}">
|
||||
<span class="data-toolbar-hint" id="clientListCount"></span>
|
||||
</div>
|
||||
${profileLegend}
|
||||
@ -83,7 +83,7 @@ function renderClients() {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client Code</th>
|
||||
<th>Current Coach</th>
|
||||
<th>Current Counselor</th>
|
||||
${showProfileColumn ? '<th>Profile scores</th>' : ''}
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
@ -223,7 +223,7 @@ function clientDetailPanelHTML(clientCode) {
|
||||
if (!questionnaires.length) {
|
||||
return `
|
||||
<p class="client-detail-meta">
|
||||
Coach: ${esc(cl.coachUsername || '—')}
|
||||
Counselor: ${esc(cl.coachUsername || '—')}
|
||||
${cl.supervisorUsername ? ` · Supervisor: ${esc(cl.supervisorUsername)}` : ''}
|
||||
</p>
|
||||
<p class="data-toolbar-hint">No questionnaire data for this client yet.</p>`;
|
||||
@ -231,7 +231,7 @@ function clientDetailPanelHTML(clientCode) {
|
||||
|
||||
return `
|
||||
<p class="client-detail-meta">
|
||||
Coach: ${esc(cl.coachUsername || '—')}
|
||||
Counselor: ${esc(cl.coachUsername || '—')}
|
||||
${cl.supervisorUsername ? ` · Supervisor: ${esc(cl.supervisorUsername)}` : ''}
|
||||
</p>
|
||||
${renderClientScoringProfiles(detail.scoringProfiles || [], clientCode)}
|
||||
@ -251,11 +251,11 @@ function scoringLegendHTML() {
|
||||
<dl class="client-scoring-legend-dl">
|
||||
<div>
|
||||
<dt>Calculated</dt>
|
||||
<dd>Automatic category from answers and profile rules (shown before coach review).</dd>
|
||||
<dd>Automatic category from answers and profile rules (shown before counselor review).</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Coach</dt>
|
||||
<dd>Category confirmed or changed by the coach in the app (“Review scores”).</dd>
|
||||
<dt>Counselor</dt>
|
||||
<dd>Category confirmed or changed by the counselor in the app (“Review scores”).</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Incomplete</dt>
|
||||
@ -317,12 +317,12 @@ function coachBandPickerHTML(p, clientCode) {
|
||||
data-weighted="${esc(String(p.weightedTotal ?? ''))}"
|
||||
data-coach="${esc(current)}">
|
||||
<button type="button" class="coach-band-trigger" aria-haspopup="listbox" aria-expanded="false"
|
||||
title="Change coach category">
|
||||
title="Change counselor category">
|
||||
<span class="coach-band-trigger-badge">${triggerBadge}</span>
|
||||
<span class="coach-band-trigger-caret" aria-hidden="true">▾</span>
|
||||
</button>
|
||||
<div class="coach-band-menu" role="listbox" hidden>
|
||||
<p class="coach-band-menu-label">Coach category</p>
|
||||
<p class="coach-band-menu-label">Counselor category</p>
|
||||
<div class="coach-band-menu-options">${options}</div>
|
||||
${agreeBtn}
|
||||
</div>
|
||||
@ -356,7 +356,7 @@ function profileScoreBlockHTML(p, clientCode = '') {
|
||||
<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>
|
||||
<span class="client-profile-score-label">Counselor</span>
|
||||
${coachControl}
|
||||
${coachDiffers ? '<span class="client-profile-override-hint">override</span>' : ''}
|
||||
</div>
|
||||
@ -369,20 +369,20 @@ function renderClientScoringProfiles(profiles, clientCode = '') {
|
||||
<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.
|
||||
Weighted totals and calculated vs counselor categories for this client.
|
||||
</p>
|
||||
<div class="client-profile-scores">${profiles.map(p => {
|
||||
const calc = p.calculatedBand || p.band;
|
||||
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 reviewedAt = p.coachReviewedAt ? `Counselor reviewed ${esc(p.coachReviewedAt)}` : '';
|
||||
const meta = [computedAt, reviewedAt].filter(Boolean).join(' · ');
|
||||
return `
|
||||
<div class="scoring-profile-detail-card">
|
||||
${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>' : ''}
|
||||
${coachPending ? '<p class="client-profile-score-hint">Counselor has not reviewed this profile in the app yet.</p>' : ''}
|
||||
</div>`;
|
||||
}).join('')}</div>
|
||||
</div>`;
|
||||
@ -540,7 +540,7 @@ function openCoachBandConfirmModal({ clientCode, profileName, calculatedBand, co
|
||||
overlay.setAttribute('aria-modal', 'true');
|
||||
overlay.innerHTML = `
|
||||
<div class="modal-dialog">
|
||||
<h2>Set coach category</h2>
|
||||
<h2>Set counselor category</h2>
|
||||
<p class="modal-subtitle">
|
||||
<strong>${esc(profileName)}</strong> · ${esc(clientCode)}
|
||||
</p>
|
||||
@ -551,11 +551,11 @@ function openCoachBandConfirmModal({ clientCode, profileName, calculatedBand, co
|
||||
</div>
|
||||
<div class="coach-band-confirm-arrow" aria-hidden="true">→</div>
|
||||
<div class="coach-band-confirm-col">
|
||||
<span class="coach-band-confirm-label">Coach</span>
|
||||
<span class="coach-band-confirm-label">Counselor</span>
|
||||
${bandBadgeHTML(coachBand, { label: bandLabel(coachBand) })}
|
||||
</div>
|
||||
</div>
|
||||
<p class="modal-hint">This overrides the coach category for this client and profile.</p>
|
||||
<p class="modal-hint">This overrides the counselor category for this client and profile.</p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn" data-coach-cancel>Cancel</button>
|
||||
<button type="button" class="btn btn-primary" data-coach-confirm>Save category</button>
|
||||
@ -612,7 +612,7 @@ async function promptCoachBandSave(picker, coachBand) {
|
||||
weightedTotal,
|
||||
});
|
||||
patchClientScoringBand(clientCode, profileID, coachBand);
|
||||
showToast(`Coach category saved for ${profileName}`, 'success');
|
||||
showToast(`Counselor category saved for ${profileName}`, 'success');
|
||||
renderClientTableBody();
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
@ -749,7 +749,7 @@ async function deleteClient(clientCode) {
|
||||
|
||||
function showCreateForm() {
|
||||
if (!coachesList.length) {
|
||||
showToast('No coaches available. Create a coach first.', 'error');
|
||||
showToast('No counselors available. Create a counselor first.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -764,9 +764,9 @@ function showCreateForm() {
|
||||
<input type="text" id="cc_code" autocomplete="off" placeholder="Enter unique client code">
|
||||
</div>
|
||||
<div class="form-group" style="flex:2">
|
||||
<label>Assign to Coach</label>
|
||||
<label>Assign to Counselor</label>
|
||||
<select id="cc_coach">
|
||||
<option value="">— select coach —</option>
|
||||
<option value="">— select counselor —</option>
|
||||
${coachesList.map(c =>
|
||||
`<option value="${c.coachID}">${esc(c.username)}${c.supervisorUsername ? ` (${esc(c.supervisorUsername)})` : ''}</option>`
|
||||
).join('')}
|
||||
@ -807,7 +807,7 @@ async function submitCreateClient() {
|
||||
const coachID = document.getElementById('cc_coach').value;
|
||||
|
||||
if (!clientCode) { showError(errEl, 'Client code is required'); return; }
|
||||
if (!coachID) { showError(errEl, 'Please select a coach'); return; }
|
||||
if (!coachID) { showError(errEl, 'Please select a counselor'); return; }
|
||||
|
||||
const btn = document.getElementById('cc_submit');
|
||||
btn.disabled = true;
|
||||
|
||||
@ -16,7 +16,7 @@ let filterSearch = '';
|
||||
export async function coachesPage() {
|
||||
const app = document.getElementById('app');
|
||||
app.innerHTML = `
|
||||
${pageHeaderHTML('Coach activity')}
|
||||
${pageHeaderHTML('Counselor activity')}
|
||||
<div id="coachesContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
|
||||
@ -192,25 +192,25 @@ function renderCoaches() {
|
||||
const el = document.getElementById('coachesContent');
|
||||
|
||||
if (!coachesList.length) {
|
||||
el.innerHTML = `<div class="card empty-state"><h3>No coaches</h3></div>`;
|
||||
el.innerHTML = `<div class="card empty-state"><h3>No counselors</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 to browse uploads by client.
|
||||
Track uploads and client load per counselor. Expand a row to browse uploads by client.
|
||||
</p>
|
||||
<div class="filter-bar">
|
||||
<input type="search" id="coachListSearch" class="filter-search"
|
||||
placeholder="Search coach or supervisor…" value="${esc(filterSearch)}">
|
||||
placeholder="Search counselor 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>Counselor</th><th>Supervisor</th><th>Clients</th>
|
||||
<th>Total uploads</th><th>7d</th><th>30d</th><th>Last upload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -308,12 +308,12 @@ function renderCoachTableBody() {
|
||||
body.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="7" style="text-align:center;color:var(--text-secondary);padding:24px">
|
||||
No coaches match
|
||||
No counselors match
|
||||
</td>
|
||||
</tr>`;
|
||||
if (countEl) {
|
||||
countEl.textContent = filterSearch.trim()
|
||||
? `0 of ${coachesList.length} coaches`
|
||||
? `0 of ${coachesList.length} counselors`
|
||||
: '';
|
||||
}
|
||||
return;
|
||||
@ -324,8 +324,8 @@ function renderCoachTableBody() {
|
||||
if (countEl) {
|
||||
const q = filterSearch.trim();
|
||||
countEl.textContent = q
|
||||
? `${filtered.length} of ${coachesList.length} coaches`
|
||||
: `${coachesList.length} coach${coachesList.length === 1 ? '' : 'es'}`;
|
||||
? `${filtered.length} of ${coachesList.length} counselors`
|
||||
: `${coachesList.length} counselor${coachesList.length === 1 ? '' : 's'}`;
|
||||
}
|
||||
|
||||
body.querySelectorAll('.coach-expand-btn').forEach(btn => {
|
||||
|
||||
@ -35,7 +35,7 @@ export function devPage() {
|
||||
<div class="security-revoke-all" style="margin-top:20px;padding-top:16px;border-top:1px solid var(--border)">
|
||||
<h3 style="margin:0 0 8px;font-size:1rem">Revoke all sessions</h3>
|
||||
<p class="field-hint callout-danger" style="margin:0 0 12px">
|
||||
<strong>Signs out everyone</strong> — all admins, supervisors, and coaches on every device.
|
||||
<strong>Signs out everyone</strong> — all admins, supervisors, and counselors on every device.
|
||||
You will be logged out too and must sign in again. Use after a suspected compromise or policy change.
|
||||
</p>
|
||||
<label for="revokeAllConfirm" style="font-size:.85rem;font-weight:500;display:block;margin-bottom:6px">
|
||||
@ -71,7 +71,7 @@ export function devPage() {
|
||||
<div class="form-group">
|
||||
<label for="devFixtureFile">Fixture JSON</label>
|
||||
<input type="file" id="devFixtureFile" accept=".json,application/json">
|
||||
<span class="field-hint">Use <code>dev-test-fixture.json</code> from the repo root (regenerate with <code>nat-as-server/scripts/generate_dev_fixture.py</code> after importing the latest <code>questionnaires_bundle_*.json</code>; uneven coach load, multi-version uploads, natural timestamps).</span>
|
||||
<span class="field-hint">Use <code>dev-test-fixture.json</code> from the repo root (regenerate with <code>nat-as-server/scripts/generate_dev_fixture.py</code> after importing the latest <code>questionnaires_bundle_*.json</code>; uneven counselor load, multi-version uploads, natural timestamps).</span>
|
||||
</div>
|
||||
<div id="devFixtureSummary" class="field-hint" style="margin:12px 0;display:none"></div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px">
|
||||
@ -83,7 +83,7 @@ export function devPage() {
|
||||
<div class="card" style="max-width:720px;margin-top:16px">
|
||||
<h2 style="margin:0 0 8px;font-size:1.1rem">Reset database</h2>
|
||||
<p class="field-hint callout-danger">
|
||||
<strong>Destructive.</strong> Deletes every client, completion, coach, and supervisor
|
||||
<strong>Destructive.</strong> Deletes every client, completion, counselor, and supervisor
|
||||
(including non-dev accounts). Admin logins and questionnaire definitions are kept.
|
||||
</p>
|
||||
<button type="button" class="btn btn-danger" id="devWipeBtn">Remove all data (keep admins)</button>
|
||||
@ -153,7 +153,7 @@ export function devPage() {
|
||||
<strong>Preview:</strong>
|
||||
${parsedFixture.admins?.length ?? st.admins ?? 0} admins,
|
||||
${parsedFixture.supervisors?.length ?? st.supervisors ?? 0} supervisors,
|
||||
${parsedFixture.coaches?.length ?? st.coaches ?? 0} coaches,
|
||||
${parsedFixture.coaches?.length ?? st.coaches ?? 0} counselors,
|
||||
${parsedFixture.clients?.length ?? st.clients ?? 0} clients,
|
||||
${parsedFixture.completions?.length ?? st.completionRecords ?? st.completions ?? 0} completions,
|
||||
${st.totalUploads ?? '—'} uploads (${st.multiVersionPairs ?? '—'} re-uploaded)
|
||||
@ -207,7 +207,7 @@ export function devPage() {
|
||||
|
||||
document.getElementById('devWipeBtn').addEventListener('click', async () => {
|
||||
if (!confirm(
|
||||
'Remove ALL clients, completions, coaches, and supervisors?\n\n'
|
||||
'Remove ALL clients, completions, counselors, and supervisors?\n\n'
|
||||
+ 'Admin accounts and questionnaires will NOT be deleted.\n'
|
||||
+ 'This cannot be undone.'
|
||||
)) {
|
||||
@ -266,7 +266,7 @@ export function devPage() {
|
||||
const sk = res.skipped || {};
|
||||
showToast(
|
||||
`Imported: ${imp.completions ?? 0} completions, ${imp.submissions ?? 0} uploads, `
|
||||
+ `${imp.clients ?? 0} clients, ${imp.coaches ?? 0} coaches `
|
||||
+ `${imp.clients ?? 0} clients, ${imp.coaches ?? 0} counselors `
|
||||
+ `(skipped ${sk.completions ?? 0} existing)`,
|
||||
'success'
|
||||
);
|
||||
|
||||
@ -27,7 +27,7 @@ const LAYOUT_TYPES = [
|
||||
{ value: 'date_spinner', label: 'Date Spinner' },
|
||||
{ value: 'string_spinner', label: 'String Spinner' },
|
||||
{ value: 'free_text', label: 'Free Text' },
|
||||
{ value: 'client_coach_code_question', label: 'Client / Coach Code' },
|
||||
{ value: 'client_coach_code_question', label: 'Client / Counselor Code' },
|
||||
{ value: 'last_page', label: 'Last Page' },
|
||||
];
|
||||
|
||||
@ -652,7 +652,7 @@ function configFormHTML(layout, config, prefix, opts = {}) {
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Hint 2 key</label>
|
||||
<input type="text" id="${prefix}_hint2" value="${esc(config.hint2 || '')}" placeholder="coach_code">
|
||||
<input type="text" id="${prefix}_hint2" value="${esc(config.hint2 || '')}" placeholder="counselor_code">
|
||||
</div>
|
||||
</div>`;
|
||||
break;
|
||||
|
||||
@ -8,7 +8,7 @@ export async function homeDashboardPage() {
|
||||
|
||||
app.innerHTML = `
|
||||
${pageHeaderHTML('Dashboard', '', {
|
||||
subtitle: `Overview for your ${role === 'admin' ? 'organization' : 'supervised coaches'}`,
|
||||
subtitle: `Overview for your ${role === 'admin' ? 'organization' : 'supervised counselors'}`,
|
||||
})}
|
||||
<div id="homeDashboardContent"><div class="spinner"></div></div>
|
||||
`;
|
||||
@ -36,12 +36,12 @@ function renderHomeDashboard(ov, isAdmin) {
|
||||
|
||||
const links = [
|
||||
{ href: '#/questionnaires', label: 'Questionnaires', desc: 'Edit forms, order, and categories' },
|
||||
{ href: '#/clients', label: 'Clients', desc: 'Client codes, coaches, response history' },
|
||||
{ href: '#/coaches', label: 'Coach activity', desc: 'Uploads and workload per coach' },
|
||||
{ href: '#/clients', label: 'Clients', desc: 'Client codes, counselors, response history' },
|
||||
{ href: '#/coaches', label: 'Counselor activity', desc: 'Uploads and workload per counselor' },
|
||||
{ href: '#/insights', label: 'Insights', desc: 'Charts, completion rates, follow-up' },
|
||||
{ href: '#/export', label: 'Export data', desc: 'Download CSV and bundles' },
|
||||
{ href: '#/assignments', label: 'Assign clients', desc: 'Move clients between coaches' },
|
||||
{ href: '#/users', label: 'Users', desc: 'Admins, supervisors, and coaches' },
|
||||
{ href: '#/assignments', label: 'Assign clients', desc: 'Move clients between counselors' },
|
||||
{ href: '#/users', label: 'Users', desc: 'Admins, supervisors, and counselors' },
|
||||
{ href: '#/translations', label: 'Translations', desc: 'German source and other languages' },
|
||||
];
|
||||
if (isAdmin) {
|
||||
|
||||
@ -101,11 +101,11 @@ function renderInsights() {
|
||||
<p class="insights-chart-hint">
|
||||
${sp.clientCount ?? 0} client(s) with complete profile
|
||||
${sp.averageTotal != null ? ` · avg ${sp.averageTotal}` : ''}
|
||||
${sp.pendingReview ? ` · ${sp.pendingReview} pending coach review` : ''}
|
||||
${sp.pendingReview ? ` · ${sp.pendingReview} pending counselor review` : ''}
|
||||
</p>
|
||||
<p class="insights-chart-hint" style="margin:0 0 6px"><strong>Calculated bands</strong></p>
|
||||
${computedChart}
|
||||
<p class="insights-chart-hint" style="margin:12px 0 6px"><strong>Coach decisions</strong></p>
|
||||
<p class="insights-chart-hint" style="margin:12px 0 6px"><strong>Counselor decisions</strong></p>
|
||||
${coachChart}
|
||||
</div>`;
|
||||
}).join('');
|
||||
@ -140,7 +140,7 @@ function renderInsights() {
|
||||
${scoringProfiles.length ? `
|
||||
<div class="card" style="margin-top:20px">
|
||||
<h3 style="margin:0 0 8px">Scoring profiles</h3>
|
||||
<p class="insights-chart-hint" style="margin:0 0 12px">Calculated vs coach band distribution among clients with a complete weighted score</p>
|
||||
<p class="insights-chart-hint" style="margin:0 0 12px">Calculated vs counselor band distribution among clients with a complete weighted score</p>
|
||||
<div class="insights-charts-grid">${scoringCharts}</div>
|
||||
</div>` : ''}
|
||||
<div class="card" style="margin-top:20px">
|
||||
@ -163,14 +163,14 @@ function renderInsights() {
|
||||
</p>
|
||||
<div class="filter-bar">
|
||||
<input type="search" id="followupListSearch" class="filter-search"
|
||||
placeholder="Search client, coach, questionnaire, note…" value="${esc(followupSearch)}">
|
||||
placeholder="Search client, counselor, questionnaire, note…" value="${esc(followupSearch)}">
|
||||
<span class="data-toolbar-hint" id="followupListCount"></span>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client</th><th>Coach</th><th>First completed</th><th>First at</th>
|
||||
<th>Client</th><th>Counselor</th><th>First completed</th><th>First at</th>
|
||||
<th>Last activity</th><th>Days idle</th><th>Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@ -27,9 +27,9 @@ export async function loginPage() {
|
||||
app.innerHTML = `
|
||||
<div class="login-wrapper">
|
||||
<div class="login-card card">
|
||||
<h1>BW Schützt</h1>
|
||||
<h1>BW Schützt - NAT-AS</h1>
|
||||
<p class="subtitle">Questionnaire Management</p>
|
||||
<p class="login-privacy-note">Coach app stores interview answers encrypted on device; data shown here is loaded from the server after upload.</p>
|
||||
<p class="login-privacy-note">Counselor app stores interview answers encrypted on device; data shown here is loaded from the server after upload.</p>
|
||||
<form id="loginForm">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
@ -97,7 +97,7 @@ export async function loginPage() {
|
||||
}
|
||||
const d = json.data;
|
||||
if (d.role === 'coach') {
|
||||
errEl.textContent = 'Coach accounts can only sign in through the mobile app.';
|
||||
errEl.textContent = 'Counselor accounts can only sign in through the mobile app.';
|
||||
errEl.style.display = '';
|
||||
return;
|
||||
}
|
||||
@ -152,7 +152,7 @@ export async function loginPage() {
|
||||
}
|
||||
const d = json.data;
|
||||
if (d.role === 'coach') {
|
||||
errEl.textContent = 'Coach accounts can only sign in through the mobile app.';
|
||||
errEl.textContent = 'Counselor accounts can only sign in through the mobile app.';
|
||||
errEl.style.display = '';
|
||||
return;
|
||||
}
|
||||
|
||||
@ -559,7 +559,7 @@ function openScoringProfileModal(profile, questionnaires, allProfiles) {
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="spDesc">Description</label>
|
||||
<textarea id="spDesc" rows="2" placeholder="Optional note for coaches and admins">${esc(profile?.description || '')}</textarea>
|
||||
<textarea id="spDesc" rows="2" placeholder="Optional note for counselors and admins">${esc(profile?.description || '')}</textarea>
|
||||
</div>
|
||||
<div class="form-group scoring-profile-active-row">
|
||||
<label class="checkbox-inline">
|
||||
|
||||
@ -79,7 +79,7 @@ function renderResults() {
|
||||
${supervisorOptions.map(name => `<option value="${esc(name)}" ${filterSupervisor === name ? 'selected' : ''}>${esc(name)}</option>`).join('')}
|
||||
</select>
|
||||
<select id="filterCoach">
|
||||
<option value="">All coaches</option>
|
||||
<option value="">All counselors</option>
|
||||
${coachOptions.map(([id, label]) => `<option value="${esc(id)}" ${filterCoach === id ? 'selected' : ''}>${esc(label)}</option>`).join('')}
|
||||
</select>
|
||||
<select id="filterStatus">
|
||||
@ -94,7 +94,7 @@ function renderResults() {
|
||||
to
|
||||
<input type="date" id="filterDateTo" value="${esc(filterDateTo)}">
|
||||
</label>
|
||||
<input type="search" id="filterSearch" class="filter-search" placeholder="Search client, coach, status…" value="${esc(filterSearch)}">
|
||||
<input type="search" id="filterSearch" class="filter-search" placeholder="Search client, counselor, status…" value="${esc(filterSearch)}">
|
||||
<input type="search" id="filterQuestion" placeholder="Go to question key…" value="${esc(filterQuestion)}" style="min-width:160px" list="questionKeyList">
|
||||
<datalist id="questionKeyList"></datalist>
|
||||
<button type="button" class="btn btn-sm" id="gotoQuestionBtn">Go to column</button>
|
||||
@ -185,7 +185,7 @@ function renderTableHead() {
|
||||
const head = document.getElementById('resultsHead');
|
||||
const fixedCols = [
|
||||
{ key: 'clientCode', label: 'Client' },
|
||||
{ key: 'coachUsername', label: 'Coach' },
|
||||
{ key: 'coachUsername', label: 'Counselor' },
|
||||
{ key: 'supervisorUsername', label: 'Supervisor' },
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'sumPoints', label: 'Score' },
|
||||
|
||||
@ -16,11 +16,11 @@ let filterSearchByRole = { admin: '', supervisor: '', coach: '' };
|
||||
|
||||
function roleGroupsForCaller() {
|
||||
return callerRole === 'supervisor'
|
||||
? [{ label: 'Coaches', key: 'coach' }]
|
||||
? [{ label: 'Counselors', key: 'coach' }]
|
||||
: [
|
||||
{ label: 'Admins', key: 'admin' },
|
||||
{ label: 'Supervisors', key: 'supervisor' },
|
||||
{ label: 'Coaches', key: 'coach' },
|
||||
{ label: 'Counselors', key: 'coach' },
|
||||
];
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ function renderUsers() {
|
||||
<div class="card">
|
||||
<div class="empty-state">
|
||||
<h3>No users found</h3>
|
||||
<p>${callerRole === 'supervisor' ? 'You have no coaches yet.' : 'Create the first user above.'}</p>
|
||||
<p>${callerRole === 'supervisor' ? 'You have no counselors yet.' : 'Create the first user above.'}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
return;
|
||||
@ -309,7 +309,7 @@ async function reassignCoachSupervisor(selectEl) {
|
||||
u.supervisorUsername = data.supervisorUsername ?? '';
|
||||
}
|
||||
selectEl.dataset.prev = supervisorID;
|
||||
showToast(`Coach "${username}" assigned to ${data.supervisorUsername || 'supervisor'}`, 'success');
|
||||
showToast(`Counselor "${username}" assigned to ${data.supervisorUsername || 'supervisor'}`, 'success');
|
||||
} catch (e) {
|
||||
selectEl.value = prev;
|
||||
showToast(e.message, 'error');
|
||||
@ -372,10 +372,10 @@ function showCreateForm() {
|
||||
<div class="form-group" style="flex:1;min-width:160px">
|
||||
<label>Role</label>
|
||||
${isSupervisor
|
||||
? `<input type="text" value="Coach" disabled>`
|
||||
? `<input type="text" value="Counselor" disabled>`
|
||||
: `<select id="cu_role">
|
||||
${allowedRoles.map(r =>
|
||||
`<option value="${r}">${r.charAt(0).toUpperCase() + r.slice(1)}</option>`
|
||||
`<option value="${r}">${r === 'coach' ? 'Counselor' : r.charAt(0).toUpperCase() + r.slice(1)}</option>`
|
||||
).join('')}
|
||||
</select>`
|
||||
}
|
||||
@ -483,7 +483,7 @@ async function submitCreateUser() {
|
||||
if (!username) { showError(errEl, 'Username is required'); return; }
|
||||
if (password.length < 6) { showError(errEl, 'Password must be at least 6 characters'); return; }
|
||||
if (role === 'coach' && !isSupervisor && !supervisorID) {
|
||||
showError(errEl, 'Please select a supervisor for this coach');
|
||||
showError(errEl, 'Please select a supervisor for this counselor');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -533,7 +533,7 @@ function downloadUsersCSV() {
|
||||
const headers = ['username', 'role', 'location', 'supervisor', 'mustChangePassword', 'createdAt', 'userID'];
|
||||
const rows = usersList.map(u => [
|
||||
u.username,
|
||||
u.role,
|
||||
u.role === 'coach' ? 'counselor' : u.role,
|
||||
u.role === 'coach' ? '' : (u.location || ''),
|
||||
u.role === 'coach' ? (u.supervisorUsername || u.supervisorID || '') : '',
|
||||
u.mustChangePassword == 1 ? 'yes' : 'no',
|
||||
|
||||
@ -14,12 +14,13 @@ function esc(s) {
|
||||
|
||||
function roleLabel(role) {
|
||||
if (!role) return '';
|
||||
if (role === 'coach') return 'Counselor';
|
||||
return role.charAt(0).toUpperCase() + role.slice(1);
|
||||
}
|
||||
|
||||
function signInHintForRole(role) {
|
||||
if (role === 'coach') {
|
||||
return 'Coaches sign in through the mobile app and will be prompted to choose a new password there.';
|
||||
return 'Counselors sign in through the mobile app and will be prompted to choose a new password there.';
|
||||
}
|
||||
return 'Supervisors sign in through this website and will be prompted to choose a new password at login.';
|
||||
}
|
||||
|
||||
@ -399,13 +399,13 @@ function renderQuestionBody(q, sectionIdx) {
|
||||
|
||||
case 'client_coach_code_question':
|
||||
inner += `
|
||||
<p class="qp-hint qp-review-note">In the app, client and coach codes are loaded automatically — coaches review them here, not type them in.</p>
|
||||
<p class="qp-hint qp-review-note">In the app, client and counselor codes are loaded automatically — counselors review them here, not type them in.</p>
|
||||
<div class="qp-form-stack qp-review-codes">
|
||||
<label>Client code
|
||||
<input type="text" class="qp-readonly-field" data-field="client"
|
||||
value="${esc(PREVIEW_CLIENT_CODE)}" readonly tabindex="-1" aria-readonly="true">
|
||||
</label>
|
||||
<label>Coach code
|
||||
<label>Counselor code
|
||||
<input type="text" class="qp-readonly-field" data-field="coach"
|
||||
value="${esc(PREVIEW_COACH_CODE)}" readonly tabindex="-1" aria-readonly="true">
|
||||
</label>
|
||||
@ -434,7 +434,7 @@ function layoutLabel(type) {
|
||||
date_spinner: 'Date',
|
||||
string_spinner: 'String spinner',
|
||||
free_text: 'Free text',
|
||||
client_coach_code_question: 'Client / coach code',
|
||||
client_coach_code_question: 'Client / counselor code',
|
||||
last_page: 'Last page',
|
||||
};
|
||||
return labels[type] || type || 'Unknown';
|
||||
|
||||
Reference in New Issue
Block a user