updated terminology from "Coach" to "Counselor" across multiple files for consistency and clarity
Some checks failed
PHPUnit / test (push) Has been cancelled

This commit is contained in:
2026-06-15 16:04:44 +02:00
parent 5dd0ee28c0
commit 3d738bb14e
20 changed files with 51 additions and 46 deletions

View File

@ -291,7 +291,7 @@ function qdb_reject_coach_web_login(string $role): void {
if ($role === 'coach' && qdb_is_web_client_request()) { if ($role === 'coach' && qdb_is_web_client_request()) {
json_error( json_error(
'FORBIDDEN', 'FORBIDDEN',
'Coach accounts can only sign in through the mobile app.', 'Counselor accounts can only sign in through the mobile app.',
403 403
); );
} }

View File

@ -1,6 +1,6 @@
{ {
"version": 2, "version": 2,
"description": "Coach-visible app UI strings only (excludes dev settings \/ database tools).", "description": "Berater-visible app UI strings only (excludes dev settings / database tools).",
"keys": [ "keys": [
"all_clients", "all_clients",
"answer", "answer",
@ -128,7 +128,7 @@
"august": "August", "august": "August",
"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 Berater-Konto an.",
"auth_title": "BW Schützt - NAT-AS", "auth_title": "BW Schützt - NAT-AS",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"category_green": "Grün", "category_green": "Grün",
@ -139,7 +139,7 @@
"client": "Klient", "client": "Klient",
"client_code": "Klientencode", "client_code": "Klientencode",
"client_code_exists": "Dieser Client-Code existiert bereits.", "client_code_exists": "Dieser Client-Code existiert bereits.",
"coach_code": "Code Coach", "coach_code": "Berater-Code",
"confirm_password_hint": "Passwort bestätigen", "confirm_password_hint": "Passwort bestätigen",
"date_after": "Das Datum muss nach", "date_after": "Das Datum muss nach",
"date_before": "Das Datum muss vor", "date_before": "Das Datum muss vor",
@ -148,7 +148,7 @@
"done": "Erledigt", "done": "Erledigt",
"download": "Herunterladen", "download": "Herunterladen",
"download_failed_use_offline": "Download fehlgeschlagen arbeite offline mit vorhandener Datenbank", "download_failed_use_offline": "Download fehlgeschlagen arbeite offline mit vorhandener Datenbank",
"enter_coach_code": "Bitte geben Sie Ihren Coach Code ein, um fortzufahren!", "enter_coach_code": "Bitte geben Sie Ihren Berater-Code ein, um fortzufahren!",
"error": "Fehler", "error": "Fehler",
"error_invalid_data": "Ungültige Daten", "error_invalid_data": "Ungültige Daten",
"error_not_found": "Nicht gefunden", "error_not_found": "Nicht gefunden",
@ -157,7 +157,7 @@
"extreme_glass": "Extrem", "extreme_glass": "Extrem",
"february": "Februar", "february": "Februar",
"fill_all_fields": "Bitte alle Felder ausfüllen!", "fill_all_fields": "Bitte alle Felder ausfüllen!",
"fill_both_fields": "Bitte Klienten-Code und Coach-Code prüfen.", "fill_both_fields": "Bitte Klienten-Code und Berater-Code prüfen.",
"hours_short": "h", "hours_short": "h",
"january": "Januar", "january": "Januar",
"july": "Juli", "july": "Juli",
@ -210,7 +210,7 @@
"review_scores": "Punkte prüfen", "review_scores": "Punkte prüfen",
"review_scores_agree": "Berechnete Kategorie bestätigen", "review_scores_agree": "Berechnete Kategorie bestätigen",
"review_scores_calculated_category": "Berechnete Kategorie", "review_scores_calculated_category": "Berechnete Kategorie",
"review_scores_coach_category": "Coach-Kategorie", "review_scores_coach_category": "Berater-Kategorie",
"review_scores_empty": "Noch keine vollständigen Profilwerte zum Prüfen.", "review_scores_empty": "Noch keine vollständigen Profilwerte zum Prüfen.",
"review_scores_pending_upload": "Ausstehend — wird mit dem Upload gesendet", "review_scores_pending_upload": "Ausstehend — wird mit dem Upload gesendet",
"review_scores_save_failed": "Kategorie konnte nicht gespeichert werden.", "review_scores_save_failed": "Kategorie konnte nicht gespeichert werden.",
@ -233,10 +233,10 @@
"upload_item_scoring_review": "Punkteprüfung: {profile}", "upload_item_scoring_review": "Punkteprüfung: {profile}",
"upload_nothing_pending": "Es wurde nichts hochgeladen. Es liegen keine abgeschlossenen Fragebögen zum Upload vor. Bitte zuerst einen Fragebogen abschließen.", "upload_nothing_pending": "Es wurde nichts hochgeladen. Es liegen keine abgeschlossenen Fragebögen zum Upload vor. Bitte zuerst einen Fragebogen abschließen.",
"upload_prepare_failed": "Es wurde nichts hochgeladen. Die Daten konnten nicht für den Upload vorbereitet werden. Bitte erneut synchronisieren oder den Support kontaktieren.", "upload_prepare_failed": "Es wurde nichts hochgeladen. Die Daten konnten nicht für den Upload vorbereitet werden. Bitte erneut synchronisieren oder den Support kontaktieren.",
"upload_scoring_review_summary": "Coach: {band} · Summe {total}", "upload_scoring_review_summary": "Berater: {band} · Summe {total}",
"upload_summary": "{qn} Fragebögen · {scores} Punkteprüfungen · {clients} Klienten", "upload_summary": "{qn} Fragebögen · {scores} Punkteprüfungen · {clients} Klienten",
"upload_success_message": "Hochladen: {count} erledigt.", "upload_success_message": "Hochladen: {count} erledigt.",
"upload_success_questionnaires": "{count} Fragebogen\/Fragebögen", "upload_success_questionnaires": "{count} Fragebogen/Fragebögen",
"upload_success_scoring_reviews": "{count} Punkteprüfung(en)", "upload_success_scoring_reviews": "{count} Punkteprüfung(en)",
"username_hint": "Benutzername", "username_hint": "Benutzername",
"year": "Jahr" "year": "Jahr"

View File

@ -70,7 +70,7 @@ case 'POST':
} }
if (!$chk->fetch()) { if (!$chk->fetch()) {
qdb_discard($tmpDb, $lockFp); qdb_discard($tmpDb, $lockFp);
json_error('NOT_FOUND', 'Coach not found or not authorized', 404); json_error('NOT_FOUND', 'Counselor not found or not authorized', 404);
} }
[$rbacClause, $rbacParams] = rbac_client_filter($tokenRec); [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec);

View File

@ -83,7 +83,7 @@ case 'POST':
} }
if (!$chkCoach->fetch()) { if (!$chkCoach->fetch()) {
qdb_discard($tmpDb, $lockFp); qdb_discard($tmpDb, $lockFp);
json_error('NOT_FOUND', 'Coach not found or not authorized', 404); json_error('NOT_FOUND', 'Counselor not found or not authorized', 404);
} }
$chk = $pdo->prepare("SELECT clientCode FROM client WHERE clientCode = :cc"); $chk = $pdo->prepare("SELECT clientCode FROM client WHERE clientCode = :cc");

View File

@ -20,7 +20,7 @@ if (!$rec) {
$role = (string)($rec['role'] ?? ''); $role = (string)($rec['role'] ?? '');
if ($role === 'coach') { if ($role === 'coach') {
json_error('FORBIDDEN', 'Coach accounts can only sign in through the mobile app.', 403); json_error('FORBIDDEN', 'Counselor accounts can only sign in through the mobile app.', 403);
} }
$username = ''; $username = '';

View File

@ -207,7 +207,7 @@ case 'PATCH':
$chk->execute([':cid' => $target['entityID'], ':svid' => $callerEntityID]); $chk->execute([':cid' => $target['entityID'], ':svid' => $callerEntityID]);
if (!$chk->fetch()) { if (!$chk->fetch()) {
qdb_discard($tmpDb, $lockFp); qdb_discard($tmpDb, $lockFp);
json_error('FORBIDDEN', 'Coach is not under your supervision', 403); json_error('FORBIDDEN', 'Counselor is not under your supervision', 403);
} }
} else { } else {
qdb_discard($tmpDb, $lockFp); qdb_discard($tmpDb, $lockFp);
@ -274,7 +274,7 @@ case 'PATCH':
$chk->execute([':cid' => $target['entityID'], ':svid' => $callerEntityID]); $chk->execute([':cid' => $target['entityID'], ':svid' => $callerEntityID]);
if (!$chk->fetch()) { if (!$chk->fetch()) {
qdb_discard($tmpDb, $lockFp); qdb_discard($tmpDb, $lockFp);
json_error('FORBIDDEN', 'Coach is not under your supervision', 403); json_error('FORBIDDEN', 'Counselor is not under your supervision', 403);
} }
} else { } else {
qdb_discard($tmpDb, $lockFp); qdb_discard($tmpDb, $lockFp);
@ -396,7 +396,7 @@ case 'DELETE':
$chk->execute([':cid' => $target['entityID'], ':svid' => $callerEntityID]); $chk->execute([':cid' => $target['entityID'], ':svid' => $callerEntityID]);
if (!$chk->fetch()) { if (!$chk->fetch()) {
qdb_discard($tmpDb, $lockFp); qdb_discard($tmpDb, $lockFp);
json_error('FORBIDDEN', 'Coach is not under your supervision', 403); json_error('FORBIDDEN', 'Counselor is not under your supervision', 403);
} }
} }

View File

@ -346,7 +346,7 @@ function qdb_coach_recent_submissions(
$chk->execute([':id' => $coachID]); $chk->execute([':id' => $coachID]);
$coach = $chk->fetch(PDO::FETCH_ASSOC); $coach = $chk->fetch(PDO::FETCH_ASSOC);
if (!$coach) { if (!$coach) {
json_error('NOT_FOUND', 'Coach not found', 404); json_error('NOT_FOUND', 'Counselor not found', 404);
} }
if ($role === 'supervisor' && ($coach['supervisorID'] ?? '') !== $entityID) { if ($role === 'supervisor' && ($coach['supervisorID'] ?? '') !== $entityID) {
json_error('FORBIDDEN', 'Not authorized for this coach', 403); json_error('FORBIDDEN', 'Not authorized for this coach', 403);

View File

@ -93,13 +93,13 @@ function qdb_import_dev_fixture(PDO $pdo, array $fixture): array
$username = qdb_dev_norm_username($row, $prefix); $username = qdb_dev_norm_username($row, $prefix);
$svUser = trim($row['supervisor'] ?? $row['supervisorUsername'] ?? ''); $svUser = trim($row['supervisor'] ?? $row['supervisorUsername'] ?? '');
if ($svUser === '' || !qdb_dev_has_prefix($svUser, $prefix)) { if ($svUser === '' || !qdb_dev_has_prefix($svUser, $prefix)) {
$errors[] = "Coach $username: missing or invalid supervisor"; $errors[] = "Counselor $username: missing or invalid supervisor";
continue; continue;
} }
$supervisorID = $supervisorIdByUsername[$svUser] $supervisorID = $supervisorIdByUsername[$svUser]
?? qdb_dev_lookup_entity_id($pdo, $svUser); ?? qdb_dev_lookup_entity_id($pdo, $svUser);
if ($supervisorID === '') { if ($supervisorID === '') {
$errors[] = "Coach $username: supervisor $svUser not found"; $errors[] = "Counselor $username: supervisor $svUser not found";
continue; continue;
} }
if (qdb_dev_user_exists($pdo, $username)) { if (qdb_dev_user_exists($pdo, $username)) {

View File

@ -35,6 +35,13 @@ export function canEdit() {
return canAccessWebsite(); return canAccessWebsite();
} }
/** User-facing label for the coach role (internal key remains `coach`). */
export function formatRoleLabel(role) {
if (role === 'coach') return 'Counselor';
if (!role) return '';
return role.charAt(0).toUpperCase() + role.slice(1);
}
function clearSession() { function clearSession() {
invalidateSessionCheck(); invalidateSessionCheck();
localStorage.removeItem('qdb_token'); localStorage.removeItem('qdb_token');
@ -109,7 +116,7 @@ function updateNav() {
if (isLoggedIn()) { if (isLoggedIn()) {
nav.style.display = ''; nav.style.display = '';
const role = getRole(); const role = getRole();
const roleLabel = role === 'coach' ? 'counselor' : role; const roleLabel = formatRoleLabel(role);
userInfo.textContent = `${getUser()} (${roleLabel})`; userInfo.textContent = `${getUser()} (${roleLabel})`;
// Show/hide role-gated nav items // Show/hide role-gated nav items

View File

@ -108,7 +108,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 Counselor</th> <th>Current counselor</th>
</tr> </tr>
</thead> </thead>
<tbody id="clientTableBody"></tbody> <tbody id="clientTableBody"></tbody>

View File

@ -105,7 +105,7 @@ function renderClients() {
<thead> <thead>
<tr> <tr>
<th>Client Code</th> <th>Client Code</th>
<th>Current Counselor</th> <th>Current counselor</th>
${showProfileColumn ? '<th>Profile scores</th>' : ''} ${showProfileColumn ? '<th>Profile scores</th>' : ''}
<th>Actions</th> <th>Actions</th>
</tr> </tr>
@ -305,7 +305,7 @@ 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 counselor review).</dd> <dd>Automatic category from answers and profile rules (shown before Counselor review).</dd>
</div> </div>
<div> <div>
<dt>Counselor</dt> <dt>Counselor</dt>
@ -371,7 +371,7 @@ 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 counselor 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>
@ -423,7 +423,7 @@ 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 counselor 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;
@ -595,7 +595,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 counselor 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>
@ -610,7 +610,7 @@ function openCoachBandConfirmModal({ clientCode, profileName, calculatedBand, co
${bandBadgeHTML(coachBand, { label: bandLabel(coachBand) })} ${bandBadgeHTML(coachBand, { label: bandLabel(coachBand) })}
</div> </div>
</div> </div>
<p class="modal-hint">This overrides the counselor 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>
@ -859,7 +859,7 @@ 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 Counselor</label> <label>Assign to counselor</label>
<select id="cc_coach"> <select id="cc_coach">
<option value="">— select counselor —</option> <option value="">— select counselor —</option>
${coachesList.map(c => ${coachesList.map(c =>

View File

@ -325,7 +325,7 @@ function renderCoachTableBody() {
const q = filterSearch.trim(); const q = filterSearch.trim();
countEl.textContent = q countEl.textContent = q
? `${filtered.length} of ${coachesList.length} counselors` ? `${filtered.length} of ${coachesList.length} counselors`
: `${coachesList.length} counselor${coachesList.length === 1 ? '' : 's'}`; : `${coachesList.length} counselors${coachesList.length === 1 ? '' : ''}`;
} }
body.querySelectorAll('.coach-expand-btn').forEach(btn => { body.querySelectorAll('.coach-expand-btn').forEach(btn => {

View File

@ -84,7 +84,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, counselor, and supervisor <strong>Destructive.</strong> Deletes all clients, completions, counselors, and supervisors
(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>

View File

@ -28,7 +28,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 / Counselor Code' }, { value: 'client_coach_code_question', label: 'Client / Counselor code' },
{ value: 'last_page', label: 'Last Page' }, { value: 'last_page', label: 'Last Page' },
]; ];
@ -659,7 +659,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="counselor_code"> <input type="text" id="${prefix}_hint2" value="${esc(config.hint2 || '')}" placeholder="berater_code">
</div> </div>
</div>`; </div>`;
break; break;
@ -824,7 +824,7 @@ function renderEditor() {
<div id="retiredQuestionsSection" style="display:none;margin-top:20px"> <div id="retiredQuestionsSection" style="display:none;margin-top:20px">
<details class="retired-questions-panel"> <details class="retired-questions-panel">
<summary>Retired questions (<span id="retiredCount">0</span>)</summary> <summary>Retired questions (<span id="retiredCount">0</span>)</summary>
<p class="field-hint">Retired questions stay in upload history; counselors on older forms can still submit.</p> <p class="field-hint">Retired questions stay in upload history; Counselors on older forms can still submit.</p>
<ul class="question-list retired-question-list" id="retiredQuestionList"></ul> <ul class="question-list retired-question-list" id="retiredQuestionList"></ul>
</details> </details>
</div> </div>

View File

@ -102,7 +102,7 @@ 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 counselor 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}
@ -141,7 +141,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 counselor 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">

View File

@ -575,7 +575,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 counselors 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

@ -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, counselor, status…" value="${esc(filterSearch)}"> <input type="search" id="filterSearch" class="filter-search" placeholder="Search client, counselors, 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>
@ -423,7 +423,7 @@ function exportCSV() {
}); });
}); });
const headers = ['clientCode', 'coach', 'supervisor', 'status', 'sumPoints', 'completedAt', const headers = ['clientCode', 'Counselor', 'supervisor', 'status', 'sumPoints', 'completedAt',
...resultColumns.map(col => col.label)]; ...resultColumns.map(col => col.label)];
const rows = clients.map(c => { const rows = clients.map(c => {

View File

@ -17,11 +17,11 @@ let filterSearchByRole = { admin: '', supervisor: '', coach: '' };
function roleGroupsForCaller() { function roleGroupsForCaller() {
return callerRole === 'supervisor' return callerRole === 'supervisor'
? [{ label: 'Counselors', key: 'coach' }] ? [{ label: 'Counselor', key: 'coach' }]
: [ : [
{ label: 'Admins', key: 'admin' }, { label: 'Admins', key: 'admin' },
{ label: 'Supervisors', key: 'supervisor' }, { label: 'Supervisors', key: 'supervisor' },
{ label: 'Counselors', key: 'coach' }, { label: 'Counselor', key: 'coach' },
]; ];
} }
@ -541,7 +541,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 === 'coach' ? 'counselor' : 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

@ -1,5 +1,5 @@
import { apiPatch, apiPost, invalidateSessionCheck } from './api.js'; import { apiPatch, apiPost, invalidateSessionCheck } from './api.js';
import { getRole, getUser, showToast } from './app.js'; import { getRole, getUser, showToast, formatRoleLabel } from './app.js';
/** @typedef {'admin_reset' | 'self'} PasswordModalMode */ /** @typedef {'admin_reset' | 'self'} PasswordModalMode */
@ -13,9 +13,7 @@ function esc(s) {
} }
function roleLabel(role) { function roleLabel(role) {
if (!role) return ''; return formatRoleLabel(role);
if (role === 'coach') return 'Counselor';
return role.charAt(0).toUpperCase() + role.slice(1);
} }
function signInHintForRole(role) { function signInHintForRole(role) {

View File

@ -399,7 +399,7 @@ 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 counselor codes are loaded automatically — counselors 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 — Counselor 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"
@ -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 / counselor 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';