Updated UI strings and references from "coach" to "counselor"
Some checks failed
PHPUnit / test (push) Has been cancelled

This commit is contained in:
2026-06-12 13:41:42 +02:00
parent 63a560e803
commit 5b5cc52133
18 changed files with 92 additions and 90 deletions

View File

@ -129,7 +129,7 @@
"auth_footer_accounts": "Konten werden von Ihrer Organisation vergeben.", "auth_footer_accounts": "Konten werden von Ihrer Organisation vergeben.",
"auth_subtitle_change_password": "Bitte legen Sie jetzt ein eigenes Passwort fest.", "auth_subtitle_change_password": "Bitte legen Sie jetzt ein eigenes Passwort fest.",
"auth_subtitle_login": "Melden Sie sich mit Ihrem Coach-Konto an.", "auth_subtitle_login": "Melden Sie sich mit Ihrem Coach-Konto an.",
"auth_title": "BW Schützt", "auth_title": "BW Schützt - NAT-AS",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"category_green": "Grün", "category_green": "Grün",
"category_red": "Rot", "category_red": "Rot",

View File

@ -445,8 +445,8 @@ function qdb_build_server_export_zip(PDO $pdo, array $tokenRec, bool $allVersion
} }
$readme = $allVersions $readme = $allVersions
? "BW Schützt — all questionnaire response exports (every upload version).\nOne CSV per questionnaire.\n" ? "BW Schützt - NAT-AS — all questionnaire response exports (every upload version).\nOne CSV per questionnaire.\n"
: "BW Schützt — all questionnaire response exports (current data).\nOne CSV per questionnaire.\n"; : "BW Schützt - NAT-AS — all questionnaire response exports (current data).\nOne CSV per questionnaire.\n";
$zip->addFromString('README.txt', $readme); $zip->addFromString('README.txt', $readme);
$zip->close(); $zip->close();

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="dark"> <meta name="color-scheme" content="dark">
<meta name="theme-color" content="#0f1218"> <meta name="theme-color" content="#0f1218">
<title>BW Sch&uuml;tzt - Dashboard</title> <title>BW Sch&uuml;tzt - NAT-AS</title>
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
</head> </head>
<body> <body>
@ -13,7 +13,7 @@
<div class="layout"> <div class="layout">
<nav class="sidebar" id="mainNav" style="display:none"> <nav class="sidebar" id="mainNav" style="display:none">
<div class="sidebar-brand"> <div class="sidebar-brand">
BW Sch&uuml;tzt BW Sch&uuml;tzt - NAT-AS
<small>Questionnaire Management</small> <small>Questionnaire Management</small>
</div> </div>
<ul class="sidebar-nav"> <ul class="sidebar-nav">
@ -38,7 +38,7 @@
<li data-nav-roles="admin supervisor"> <li data-nav-roles="admin supervisor">
<a href="#/coaches"> <a href="#/coaches">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="8.5" cy="7" r="4"/><polyline points="17 11 19 13 23 9"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="8.5" cy="7" r="4"/><polyline points="17 11 19 13 23 9"/></svg>
Coach activity Counselor activity
</a> </a>
</li> </li>
<li data-nav-roles="admin supervisor"> <li data-nav-roles="admin supervisor">

View File

@ -107,7 +107,8 @@ function updateNav() {
if (isLoggedIn()) { if (isLoggedIn()) {
nav.style.display = ''; nav.style.display = '';
const role = getRole(); const role = getRole();
userInfo.textContent = `${getUser()} (${role})`; const roleLabel = role === 'coach' ? 'counselor' : role;
userInfo.textContent = `${getUser()} (${roleLabel})`;
// Show/hide role-gated nav items // Show/hide role-gated nav items
document.querySelectorAll('[data-nav-roles]').forEach(li => { document.querySelectorAll('[data-nav-roles]').forEach(li => {

View File

@ -393,7 +393,7 @@ export function renderConditionEditorHTML(form, {
<div class="condition-editor" id="conditionEditor"> <div class="condition-editor" id="conditionEditor">
<h4 style="margin:0 0 12px;font-size:.95rem">Availability</h4> <h4 style="margin:0 0 12px;font-size:.95rem">Availability</h4>
<div class="form-group"> <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> <select id="conditionMode"${dis}>${modeOptions}</select>
</div> </div>

View File

@ -53,8 +53,8 @@ function renderAssignments() {
if (!coaches.length) { if (!coaches.length) {
container.innerHTML = ` container.innerHTML = `
<div class="card empty-state"> <div class="card empty-state">
<h3>No coaches found</h3> <h3>No counselors found</h3>
<p>Create coaches first before assigning clients.</p> <p>Create counselors first before assigning clients.</p>
</div>`; </div>`;
return; return;
} }
@ -65,16 +65,16 @@ function renderAssignments() {
<div class="assign-page"> <div class="assign-page">
<div class="assign-layout"> <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"> <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"> <div class="filter-bar">
<input type="search" id="coachSearch" class="filter-search" <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> </div>
<div class="assign-panel-body"> <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>
<div class="assign-panel-footer assign-panel-footer--align" aria-hidden="true"></div> <div class="assign-panel-footer assign-panel-footer--align" aria-hidden="true"></div>
</aside> </aside>
@ -89,8 +89,8 @@ function renderAssignments() {
</div> </div>
</div> </div>
<div class="assign-clients-filters filter-bar"> <div class="assign-clients-filters filter-bar">
<select id="filterByCoach" aria-label="Filter by coach"> <select id="filterByCoach" aria-label="Filter by counselor">
<option value="">All coaches (${clients.length})</option> <option value="">All counselors (${clients.length})</option>
<option value="__unassigned__">Unassigned (${unassignedCount})</option> <option value="__unassigned__">Unassigned (${unassignedCount})</option>
${coaches.map(c => { ${coaches.map(c => {
const n = (clientsByCoach[c.coachID] || []).length; const n = (clientsByCoach[c.coachID] || []).length;
@ -98,7 +98,7 @@ function renderAssignments() {
}).join('')} }).join('')}
</select> </select>
<input type="search" id="clientSearch" class="filter-search" <input type="search" id="clientSearch" class="filter-search"
placeholder="Search client or coach…"> placeholder="Search client or counselor…">
</div> </div>
</div> </div>
<div class="assign-table-panel"> <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"> <input type="checkbox" id="selectAllCb" title="Toggle all visible rows" aria-label="Select all visible">
</th> </th>
<th>Client Code</th> <th>Client Code</th>
<th>Current Coach</th> <th>Current Counselor</th>
</tr> </tr>
</thead> </thead>
<tbody id="clientTableBody"></tbody> <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"> <section class="card assign-action-card" id="assignActionCard" style="display:none" aria-label="Assign selection">
<h4>Assign selected clients to</h4> <h4>Assign selected clients to</h4>
<div class="assign-action-row"> <div class="assign-action-row">
<select id="targetCoachSelect" aria-label="Target coach"> <select id="targetCoachSelect" aria-label="Target counselor">
<option value="">— select coach —</option> <option value="">— select counselor —</option>
${coaches.map(c => ${coaches.map(c =>
`<option value="${c.coachID}">${esc(c.username)}${c.supervisorUsername ? ` (${esc(c.supervisorUsername)})` : ''}</option>` `<option value="${c.coachID}">${esc(c.username)}${c.supervisorUsername ? ` (${esc(c.supervisorUsername)})` : ''}</option>`
).join('')} ).join('')}
@ -290,7 +290,7 @@ function renderCoachList() {
const q = filterCoachSearch.trim(); const q = filterCoachSearch.trim();
if (!filtered.length) { 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; return;
} }
@ -496,7 +496,7 @@ async function doAssign() {
const coachID = document.getElementById('targetCoachSelect').value; const coachID = document.getElementById('targetCoachSelect').value;
if (!selected.length) { showToast('Select at least one client', 'error'); return; } 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; const coachName = coaches.find(c => c.coachID === coachID)?.username || coachID;
if (!confirm(`Assign ${selected.length} client(s) to ${coachName}?`)) return; if (!confirm(`Assign ${selected.length} client(s) to ${coachName}?`)) return;

View File

@ -71,7 +71,7 @@ function renderClients() {
container.innerHTML = ` container.innerHTML = `
<div class="card"> <div class="card">
<div class="filter-bar"> <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> <span class="data-toolbar-hint" id="clientListCount"></span>
</div> </div>
${profileLegend} ${profileLegend}
@ -83,7 +83,7 @@ function renderClients() {
<thead> <thead>
<tr> <tr>
<th>Client Code</th> <th>Client Code</th>
<th>Current Coach</th> <th>Current Counselor</th>
${showProfileColumn ? '<th>Profile scores</th>' : ''} ${showProfileColumn ? '<th>Profile scores</th>' : ''}
<th>Actions</th> <th>Actions</th>
</tr> </tr>
@ -223,7 +223,7 @@ function clientDetailPanelHTML(clientCode) {
if (!questionnaires.length) { if (!questionnaires.length) {
return ` return `
<p class="client-detail-meta"> <p class="client-detail-meta">
Coach: ${esc(cl.coachUsername || '—')} Counselor: ${esc(cl.coachUsername || '—')}
${cl.supervisorUsername ? ` · Supervisor: ${esc(cl.supervisorUsername)}` : ''} ${cl.supervisorUsername ? ` · Supervisor: ${esc(cl.supervisorUsername)}` : ''}
</p> </p>
<p class="data-toolbar-hint">No questionnaire data for this client yet.</p>`; <p class="data-toolbar-hint">No questionnaire data for this client yet.</p>`;
@ -231,7 +231,7 @@ function clientDetailPanelHTML(clientCode) {
return ` return `
<p class="client-detail-meta"> <p class="client-detail-meta">
Coach: ${esc(cl.coachUsername || '—')} Counselor: ${esc(cl.coachUsername || '—')}
${cl.supervisorUsername ? ` · Supervisor: ${esc(cl.supervisorUsername)}` : ''} ${cl.supervisorUsername ? ` · Supervisor: ${esc(cl.supervisorUsername)}` : ''}
</p> </p>
${renderClientScoringProfiles(detail.scoringProfiles || [], clientCode)} ${renderClientScoringProfiles(detail.scoringProfiles || [], clientCode)}
@ -251,11 +251,11 @@ function scoringLegendHTML() {
<dl class="client-scoring-legend-dl"> <dl class="client-scoring-legend-dl">
<div> <div>
<dt>Calculated</dt> <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>
<div> <div>
<dt>Coach</dt> <dt>Counselor</dt>
<dd>Category confirmed or changed by the coach in the app (“Review scores”).</dd> <dd>Category confirmed or changed by the counselor in the app (“Review scores”).</dd>
</div> </div>
<div> <div>
<dt>Incomplete</dt> <dt>Incomplete</dt>
@ -317,12 +317,12 @@ function coachBandPickerHTML(p, clientCode) {
data-weighted="${esc(String(p.weightedTotal ?? ''))}" data-weighted="${esc(String(p.weightedTotal ?? ''))}"
data-coach="${esc(current)}"> data-coach="${esc(current)}">
<button type="button" class="coach-band-trigger" aria-haspopup="listbox" aria-expanded="false" <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-badge">${triggerBadge}</span>
<span class="coach-band-trigger-caret" aria-hidden="true">▾</span> <span class="coach-band-trigger-caret" aria-hidden="true">▾</span>
</button> </button>
<div class="coach-band-menu" role="listbox" hidden> <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> <div class="coach-band-menu-options">${options}</div>
${agreeBtn} ${agreeBtn}
</div> </div>
@ -356,7 +356,7 @@ function profileScoreBlockHTML(p, clientCode = '') {
<span class="client-profile-score-total" title="Weighted total">${esc(String(p.weightedTotal))}</span> <span class="client-profile-score-total" title="Weighted total">${esc(String(p.weightedTotal))}</span>
</div> </div>
<div class="client-profile-score-row"> <div class="client-profile-score-row">
<span class="client-profile-score-label">Coach</span> <span class="client-profile-score-label">Counselor</span>
${coachControl} ${coachControl}
${coachDiffers ? '<span class="client-profile-override-hint">override</span>' : ''} ${coachDiffers ? '<span class="client-profile-override-hint">override</span>' : ''}
</div> </div>
@ -369,20 +369,20 @@ function renderClientScoringProfiles(profiles, clientCode = '') {
<div class="client-scoring-profiles-detail"> <div class="client-scoring-profiles-detail">
<strong>Scoring profiles</strong> <strong>Scoring profiles</strong>
<p class="data-toolbar-hint" style="margin:4px 0 10px"> <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> </p>
<div class="client-profile-scores">${profiles.map(p => { <div class="client-profile-scores">${profiles.map(p => {
const calc = p.calculatedBand || p.band; const calc = p.calculatedBand || p.band;
if (!calc) return profileScoreBlockHTML(p, clientCode); if (!calc) return profileScoreBlockHTML(p, clientCode);
const coachPending = p.pendingReview !== false && !p.coachBand; const coachPending = p.pendingReview !== false && !p.coachBand;
const computedAt = p.computedAt ? `Last computed ${esc(p.computedAt)}` : ''; 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(' · '); const meta = [computedAt, reviewedAt].filter(Boolean).join(' · ');
return ` return `
<div class="scoring-profile-detail-card"> <div class="scoring-profile-detail-card">
${profileScoreBlockHTML(p, clientCode)} ${profileScoreBlockHTML(p, clientCode)}
${meta ? `<p class="client-profile-score-meta">${meta}</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>' : ''} ${coachPending ? '<p class="client-profile-score-hint">Counselor has not reviewed this profile in the app yet.</p>' : ''}
</div>`; </div>`;
}).join('')}</div> }).join('')}</div>
</div>`; </div>`;
@ -540,7 +540,7 @@ function openCoachBandConfirmModal({ clientCode, profileName, calculatedBand, co
overlay.setAttribute('aria-modal', 'true'); overlay.setAttribute('aria-modal', 'true');
overlay.innerHTML = ` overlay.innerHTML = `
<div class="modal-dialog"> <div class="modal-dialog">
<h2>Set coach category</h2> <h2>Set counselor category</h2>
<p class="modal-subtitle"> <p class="modal-subtitle">
<strong>${esc(profileName)}</strong> · ${esc(clientCode)} <strong>${esc(profileName)}</strong> · ${esc(clientCode)}
</p> </p>
@ -551,11 +551,11 @@ function openCoachBandConfirmModal({ clientCode, profileName, calculatedBand, co
</div> </div>
<div class="coach-band-confirm-arrow" aria-hidden="true">→</div> <div class="coach-band-confirm-arrow" aria-hidden="true">→</div>
<div class="coach-band-confirm-col"> <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) })} ${bandBadgeHTML(coachBand, { label: bandLabel(coachBand) })}
</div> </div>
</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"> <div class="modal-actions">
<button type="button" class="btn" data-coach-cancel>Cancel</button> <button type="button" class="btn" data-coach-cancel>Cancel</button>
<button type="button" class="btn btn-primary" data-coach-confirm>Save category</button> <button type="button" class="btn btn-primary" data-coach-confirm>Save category</button>
@ -612,7 +612,7 @@ async function promptCoachBandSave(picker, coachBand) {
weightedTotal, weightedTotal,
}); });
patchClientScoringBand(clientCode, profileID, coachBand); patchClientScoringBand(clientCode, profileID, coachBand);
showToast(`Coach category saved for ${profileName}`, 'success'); showToast(`Counselor category saved for ${profileName}`, 'success');
renderClientTableBody(); renderClientTableBody();
} catch (e) { } catch (e) {
showToast(e.message, 'error'); showToast(e.message, 'error');
@ -749,7 +749,7 @@ async function deleteClient(clientCode) {
function showCreateForm() { function showCreateForm() {
if (!coachesList.length) { if (!coachesList.length) {
showToast('No coaches available. Create a coach first.', 'error'); showToast('No counselors available. Create a counselor first.', 'error');
return; return;
} }
@ -764,9 +764,9 @@ function showCreateForm() {
<input type="text" id="cc_code" autocomplete="off" placeholder="Enter unique client code"> <input type="text" id="cc_code" autocomplete="off" placeholder="Enter unique client code">
</div> </div>
<div class="form-group" style="flex:2"> <div class="form-group" style="flex:2">
<label>Assign to Coach</label> <label>Assign to Counselor</label>
<select id="cc_coach"> <select id="cc_coach">
<option value="">— select coach —</option> <option value="">— select counselor —</option>
${coachesList.map(c => ${coachesList.map(c =>
`<option value="${c.coachID}">${esc(c.username)}${c.supervisorUsername ? ` (${esc(c.supervisorUsername)})` : ''}</option>` `<option value="${c.coachID}">${esc(c.username)}${c.supervisorUsername ? ` (${esc(c.supervisorUsername)})` : ''}</option>`
).join('')} ).join('')}
@ -807,7 +807,7 @@ async function submitCreateClient() {
const coachID = document.getElementById('cc_coach').value; const coachID = document.getElementById('cc_coach').value;
if (!clientCode) { showError(errEl, 'Client code is required'); return; } 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'); const btn = document.getElementById('cc_submit');
btn.disabled = true; btn.disabled = true;

View File

@ -16,7 +16,7 @@ let filterSearch = '';
export async function coachesPage() { export async function coachesPage() {
const app = document.getElementById('app'); const app = document.getElementById('app');
app.innerHTML = ` app.innerHTML = `
${pageHeaderHTML('Coach activity')} ${pageHeaderHTML('Counselor activity')}
<div id="coachesContent"><div class="spinner"></div></div> <div id="coachesContent"><div class="spinner"></div></div>
`; `;
@ -192,25 +192,25 @@ function renderCoaches() {
const el = document.getElementById('coachesContent'); const el = document.getElementById('coachesContent');
if (!coachesList.length) { 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; return;
} }
el.innerHTML = ` el.innerHTML = `
<div class="card"> <div class="card">
<p class="data-toolbar-hint" style="margin:0 0 12px"> <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> </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 counselor or supervisor…" value="${esc(filterSearch)}">
<span class="data-toolbar-hint" id="coachListCount"></span> <span class="data-toolbar-hint" id="coachListCount"></span>
</div> </div>
<div class="table-wrapper"> <div class="table-wrapper">
<table class="data-table"> <table class="data-table">
<thead> <thead>
<tr> <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> <th>Total uploads</th><th>7d</th><th>30d</th><th>Last upload</th>
</tr> </tr>
</thead> </thead>
@ -308,12 +308,12 @@ function renderCoachTableBody() {
body.innerHTML = ` body.innerHTML = `
<tr> <tr>
<td colspan="7" style="text-align:center;color:var(--text-secondary);padding:24px"> <td colspan="7" style="text-align:center;color:var(--text-secondary);padding:24px">
No coaches match No counselors match
</td> </td>
</tr>`; </tr>`;
if (countEl) { if (countEl) {
countEl.textContent = filterSearch.trim() countEl.textContent = filterSearch.trim()
? `0 of ${coachesList.length} coaches` ? `0 of ${coachesList.length} counselors`
: ''; : '';
} }
return; return;
@ -324,8 +324,8 @@ function renderCoachTableBody() {
if (countEl) { if (countEl) {
const q = filterSearch.trim(); const q = filterSearch.trim();
countEl.textContent = q countEl.textContent = q
? `${filtered.length} of ${coachesList.length} coaches` ? `${filtered.length} of ${coachesList.length} counselors`
: `${coachesList.length} coach${coachesList.length === 1 ? '' : 'es'}`; : `${coachesList.length} counselor${coachesList.length === 1 ? '' : 's'}`;
} }
body.querySelectorAll('.coach-expand-btn').forEach(btn => { body.querySelectorAll('.coach-expand-btn').forEach(btn => {

View File

@ -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)"> <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> <h3 style="margin:0 0 8px;font-size:1rem">Revoke all sessions</h3>
<p class="field-hint callout-danger" style="margin:0 0 12px"> <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. You will be logged out too and must sign in again. Use after a suspected compromise or policy change.
</p> </p>
<label for="revokeAllConfirm" style="font-size:.85rem;font-weight:500;display:block;margin-bottom:6px"> <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"> <div class="form-group">
<label for="devFixtureFile">Fixture JSON</label> <label for="devFixtureFile">Fixture JSON</label>
<input type="file" id="devFixtureFile" accept=".json,application/json"> <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>
<div id="devFixtureSummary" class="field-hint" style="margin:12px 0;display:none"></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"> <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"> <div class="card" style="max-width:720px;margin-top:16px">
<h2 style="margin:0 0 8px;font-size:1.1rem">Reset database</h2> <h2 style="margin:0 0 8px;font-size:1.1rem">Reset database</h2>
<p class="field-hint callout-danger"> <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. (including non-dev accounts). Admin logins and questionnaire definitions are kept.
</p> </p>
<button type="button" class="btn btn-danger" id="devWipeBtn">Remove all data (keep admins)</button> <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> <strong>Preview:</strong>
${parsedFixture.admins?.length ?? st.admins ?? 0} admins, ${parsedFixture.admins?.length ?? st.admins ?? 0} admins,
${parsedFixture.supervisors?.length ?? st.supervisors ?? 0} supervisors, ${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.clients?.length ?? st.clients ?? 0} clients,
${parsedFixture.completions?.length ?? st.completionRecords ?? st.completions ?? 0} completions, ${parsedFixture.completions?.length ?? st.completionRecords ?? st.completions ?? 0} completions,
${st.totalUploads ?? '—'} uploads (${st.multiVersionPairs ?? '—'} re-uploaded) ${st.totalUploads ?? '—'} uploads (${st.multiVersionPairs ?? '—'} re-uploaded)
@ -207,7 +207,7 @@ export function devPage() {
document.getElementById('devWipeBtn').addEventListener('click', async () => { document.getElementById('devWipeBtn').addEventListener('click', async () => {
if (!confirm( 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' + 'Admin accounts and questionnaires will NOT be deleted.\n'
+ 'This cannot be undone.' + 'This cannot be undone.'
)) { )) {
@ -266,7 +266,7 @@ export function devPage() {
const sk = res.skipped || {}; const sk = res.skipped || {};
showToast( showToast(
`Imported: ${imp.completions ?? 0} completions, ${imp.submissions ?? 0} uploads, ` `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)`, + `(skipped ${sk.completions ?? 0} existing)`,
'success' 'success'
); );

View File

@ -27,7 +27,7 @@ const LAYOUT_TYPES = [
{ value: 'date_spinner', label: 'Date Spinner' }, { value: 'date_spinner', label: 'Date Spinner' },
{ value: 'string_spinner', label: 'String Spinner' }, { value: 'string_spinner', label: 'String Spinner' },
{ value: 'free_text', label: 'Free Text' }, { 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' }, { value: 'last_page', label: 'Last Page' },
]; ];
@ -652,7 +652,7 @@ function configFormHTML(layout, config, prefix, opts = {}) {
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Hint 2 key</label> <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>
</div>`; </div>`;
break; break;

View File

@ -8,7 +8,7 @@ export async function homeDashboardPage() {
app.innerHTML = ` app.innerHTML = `
${pageHeaderHTML('Dashboard', '', { ${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> <div id="homeDashboardContent"><div class="spinner"></div></div>
`; `;
@ -36,12 +36,12 @@ function renderHomeDashboard(ov, isAdmin) {
const links = [ const links = [
{ href: '#/questionnaires', label: 'Questionnaires', desc: 'Edit forms, order, and categories' }, { href: '#/questionnaires', label: 'Questionnaires', desc: 'Edit forms, order, and categories' },
{ href: '#/clients', label: 'Clients', desc: 'Client codes, coaches, response history' }, { href: '#/clients', label: 'Clients', desc: 'Client codes, counselors, response history' },
{ href: '#/coaches', label: 'Coach activity', desc: 'Uploads and workload per coach' }, { href: '#/coaches', label: 'Counselor activity', desc: 'Uploads and workload per counselor' },
{ href: '#/insights', label: 'Insights', desc: 'Charts, completion rates, follow-up' }, { href: '#/insights', label: 'Insights', desc: 'Charts, completion rates, follow-up' },
{ href: '#/export', label: 'Export data', desc: 'Download CSV and bundles' }, { href: '#/export', label: 'Export data', desc: 'Download CSV and bundles' },
{ href: '#/assignments', label: 'Assign clients', desc: 'Move clients between coaches' }, { href: '#/assignments', label: 'Assign clients', desc: 'Move clients between counselors' },
{ href: '#/users', label: 'Users', desc: 'Admins, supervisors, and coaches' }, { href: '#/users', label: 'Users', desc: 'Admins, supervisors, and counselors' },
{ href: '#/translations', label: 'Translations', desc: 'German source and other languages' }, { href: '#/translations', label: 'Translations', desc: 'German source and other languages' },
]; ];
if (isAdmin) { if (isAdmin) {

View File

@ -101,11 +101,11 @@ function renderInsights() {
<p class="insights-chart-hint"> <p class="insights-chart-hint">
${sp.clientCount ?? 0} client(s) with complete profile ${sp.clientCount ?? 0} client(s) with complete profile
${sp.averageTotal != null ? ` · avg ${sp.averageTotal}` : ''} ${sp.averageTotal != null ? ` · avg ${sp.averageTotal}` : ''}
${sp.pendingReview ? ` · ${sp.pendingReview} pending coach review` : ''} ${sp.pendingReview ? ` · ${sp.pendingReview} pending counselor review` : ''}
</p> </p>
<p class="insights-chart-hint" style="margin:0 0 6px"><strong>Calculated bands</strong></p> <p class="insights-chart-hint" style="margin:0 0 6px"><strong>Calculated bands</strong></p>
${computedChart} ${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} ${coachChart}
</div>`; </div>`;
}).join(''); }).join('');
@ -140,7 +140,7 @@ function renderInsights() {
${scoringProfiles.length ? ` ${scoringProfiles.length ? `
<div class="card" style="margin-top:20px"> <div class="card" style="margin-top:20px">
<h3 style="margin:0 0 8px">Scoring profiles</h3> <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 class="insights-charts-grid">${scoringCharts}</div>
</div>` : ''} </div>` : ''}
<div class="card" style="margin-top:20px"> <div class="card" style="margin-top:20px">
@ -163,14 +163,14 @@ function renderInsights() {
</p> </p>
<div class="filter-bar"> <div class="filter-bar">
<input type="search" id="followupListSearch" class="filter-search" <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> <span class="data-toolbar-hint" id="followupListCount"></span>
</div> </div>
<div class="table-wrapper"> <div class="table-wrapper">
<table class="data-table"> <table class="data-table">
<thead> <thead>
<tr> <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> <th>Last activity</th><th>Days idle</th><th>Note</th>
</tr> </tr>
</thead> </thead>

View File

@ -27,9 +27,9 @@ export async function loginPage() {
app.innerHTML = ` app.innerHTML = `
<div class="login-wrapper"> <div class="login-wrapper">
<div class="login-card card"> <div class="login-card card">
<h1>BW Sch&uuml;tzt</h1> <h1>BW Sch&uuml;tzt - NAT-AS</h1>
<p class="subtitle">Questionnaire Management</p> <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"> <form id="loginForm">
<div class="form-group"> <div class="form-group">
<label for="username">Username</label> <label for="username">Username</label>
@ -97,7 +97,7 @@ export async function loginPage() {
} }
const d = json.data; const d = json.data;
if (d.role === 'coach') { 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 = ''; errEl.style.display = '';
return; return;
} }
@ -152,7 +152,7 @@ export async function loginPage() {
} }
const d = json.data; const d = json.data;
if (d.role === 'coach') { 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 = ''; errEl.style.display = '';
return; return;
} }

View File

@ -559,7 +559,7 @@ function openScoringProfileModal(profile, questionnaires, allProfiles) {
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="spDesc">Description</label> <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>
<div class="form-group scoring-profile-active-row"> <div class="form-group scoring-profile-active-row">
<label class="checkbox-inline"> <label class="checkbox-inline">

View File

@ -79,7 +79,7 @@ function renderResults() {
${supervisorOptions.map(name => `<option value="${esc(name)}" ${filterSupervisor === name ? 'selected' : ''}>${esc(name)}</option>`).join('')} ${supervisorOptions.map(name => `<option value="${esc(name)}" ${filterSupervisor === name ? 'selected' : ''}>${esc(name)}</option>`).join('')}
</select> </select>
<select id="filterCoach"> <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('')} ${coachOptions.map(([id, label]) => `<option value="${esc(id)}" ${filterCoach === id ? 'selected' : ''}>${esc(label)}</option>`).join('')}
</select> </select>
<select id="filterStatus"> <select id="filterStatus">
@ -94,7 +94,7 @@ function renderResults() {
to to
<input type="date" id="filterDateTo" value="${esc(filterDateTo)}"> <input type="date" id="filterDateTo" value="${esc(filterDateTo)}">
</label> </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"> <input type="search" id="filterQuestion" placeholder="Go to question key…" value="${esc(filterQuestion)}" style="min-width:160px" list="questionKeyList">
<datalist id="questionKeyList"></datalist> <datalist id="questionKeyList"></datalist>
<button type="button" class="btn btn-sm" id="gotoQuestionBtn">Go to column</button> <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 head = document.getElementById('resultsHead');
const fixedCols = [ const fixedCols = [
{ key: 'clientCode', label: 'Client' }, { key: 'clientCode', label: 'Client' },
{ key: 'coachUsername', label: 'Coach' }, { key: 'coachUsername', label: 'Counselor' },
{ key: 'supervisorUsername', label: 'Supervisor' }, { key: 'supervisorUsername', label: 'Supervisor' },
{ key: 'status', label: 'Status' }, { key: 'status', label: 'Status' },
{ key: 'sumPoints', label: 'Score' }, { key: 'sumPoints', label: 'Score' },

View File

@ -16,11 +16,11 @@ let filterSearchByRole = { admin: '', supervisor: '', coach: '' };
function roleGroupsForCaller() { function roleGroupsForCaller() {
return callerRole === 'supervisor' return callerRole === 'supervisor'
? [{ label: 'Coaches', key: 'coach' }] ? [{ label: 'Counselors', key: 'coach' }]
: [ : [
{ label: 'Admins', key: 'admin' }, { label: 'Admins', key: 'admin' },
{ label: 'Supervisors', key: 'supervisor' }, { label: 'Supervisors', key: 'supervisor' },
{ label: 'Coaches', key: 'coach' }, { label: 'Counselors', key: 'coach' },
]; ];
} }
@ -101,7 +101,7 @@ function renderUsers() {
<div class="card"> <div class="card">
<div class="empty-state"> <div class="empty-state">
<h3>No users found</h3> <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>
</div>`; </div>`;
return; return;
@ -309,7 +309,7 @@ async function reassignCoachSupervisor(selectEl) {
u.supervisorUsername = data.supervisorUsername ?? ''; u.supervisorUsername = data.supervisorUsername ?? '';
} }
selectEl.dataset.prev = supervisorID; selectEl.dataset.prev = supervisorID;
showToast(`Coach "${username}" assigned to ${data.supervisorUsername || 'supervisor'}`, 'success'); showToast(`Counselor "${username}" assigned to ${data.supervisorUsername || 'supervisor'}`, 'success');
} catch (e) { } catch (e) {
selectEl.value = prev; selectEl.value = prev;
showToast(e.message, 'error'); showToast(e.message, 'error');
@ -372,10 +372,10 @@ function showCreateForm() {
<div class="form-group" style="flex:1;min-width:160px"> <div class="form-group" style="flex:1;min-width:160px">
<label>Role</label> <label>Role</label>
${isSupervisor ${isSupervisor
? `<input type="text" value="Coach" disabled>` ? `<input type="text" value="Counselor" disabled>`
: `<select id="cu_role"> : `<select id="cu_role">
${allowedRoles.map(r => ${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('')} ).join('')}
</select>` </select>`
} }
@ -483,7 +483,7 @@ async function submitCreateUser() {
if (!username) { showError(errEl, 'Username is required'); return; } if (!username) { showError(errEl, 'Username is required'); return; }
if (password.length < 6) { showError(errEl, 'Password must be at least 6 characters'); return; } if (password.length < 6) { showError(errEl, 'Password must be at least 6 characters'); return; }
if (role === 'coach' && !isSupervisor && !supervisorID) { if (role === 'coach' && !isSupervisor && !supervisorID) {
showError(errEl, 'Please select a supervisor for this coach'); showError(errEl, 'Please select a supervisor for this counselor');
return; return;
} }
@ -533,7 +533,7 @@ function downloadUsersCSV() {
const headers = ['username', 'role', 'location', 'supervisor', 'mustChangePassword', 'createdAt', 'userID']; const headers = ['username', 'role', 'location', 'supervisor', 'mustChangePassword', 'createdAt', 'userID'];
const rows = usersList.map(u => [ const rows = usersList.map(u => [
u.username, u.username,
u.role, u.role === 'coach' ? 'counselor' : u.role,
u.role === 'coach' ? '' : (u.location || ''), u.role === 'coach' ? '' : (u.location || ''),
u.role === 'coach' ? (u.supervisorUsername || u.supervisorID || '') : '', u.role === 'coach' ? (u.supervisorUsername || u.supervisorID || '') : '',
u.mustChangePassword == 1 ? 'yes' : 'no', u.mustChangePassword == 1 ? 'yes' : 'no',

View File

@ -14,12 +14,13 @@ function esc(s) {
function roleLabel(role) { function roleLabel(role) {
if (!role) return ''; if (!role) return '';
if (role === 'coach') return 'Counselor';
return role.charAt(0).toUpperCase() + role.slice(1); return role.charAt(0).toUpperCase() + role.slice(1);
} }
function signInHintForRole(role) { function signInHintForRole(role) {
if (role === 'coach') { 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.'; return 'Supervisors sign in through this website and will be prompted to choose a new password at login.';
} }

View File

@ -399,13 +399,13 @@ function renderQuestionBody(q, sectionIdx) {
case 'client_coach_code_question': case 'client_coach_code_question':
inner += ` 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"> <div class="qp-form-stack qp-review-codes">
<label>Client code <label>Client code
<input type="text" class="qp-readonly-field" data-field="client" <input type="text" class="qp-readonly-field" data-field="client"
value="${esc(PREVIEW_CLIENT_CODE)}" readonly tabindex="-1" aria-readonly="true"> value="${esc(PREVIEW_CLIENT_CODE)}" readonly tabindex="-1" aria-readonly="true">
</label> </label>
<label>Coach code <label>Counselor code
<input type="text" class="qp-readonly-field" data-field="coach" <input type="text" class="qp-readonly-field" data-field="coach"
value="${esc(PREVIEW_COACH_CODE)}" readonly tabindex="-1" aria-readonly="true"> value="${esc(PREVIEW_COACH_CODE)}" readonly tabindex="-1" aria-readonly="true">
</label> </label>
@ -434,7 +434,7 @@ function layoutLabel(type) {
date_spinner: 'Date', date_spinner: 'Date',
string_spinner: 'String spinner', string_spinner: 'String spinner',
free_text: 'Free text', free_text: 'Free text',
client_coach_code_question: 'Client / coach code', client_coach_code_question: 'Client / counselor code',
last_page: 'Last page', last_page: 'Last page',
}; };
return labels[type] || type || 'Unknown'; return labels[type] || type || 'Unknown';