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()) {
json_error(
'FORBIDDEN',
'Coach accounts can only sign in through the mobile app.',
'Counselor accounts can only sign in through the mobile app.',
403
);
}

View File

@ -1,6 +1,6 @@
{
"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": [
"all_clients",
"answer",
@ -128,7 +128,7 @@
"august": "August",
"auth_footer_accounts": "Konten werden von Ihrer Organisation vergeben.",
"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",
"cancel": "Abbrechen",
"category_green": "Grün",
@ -139,7 +139,7 @@
"client": "Klient",
"client_code": "Klientencode",
"client_code_exists": "Dieser Client-Code existiert bereits.",
"coach_code": "Code Coach",
"coach_code": "Berater-Code",
"confirm_password_hint": "Passwort bestätigen",
"date_after": "Das Datum muss nach",
"date_before": "Das Datum muss vor",
@ -148,7 +148,7 @@
"done": "Erledigt",
"download": "Herunterladen",
"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_invalid_data": "Ungültige Daten",
"error_not_found": "Nicht gefunden",
@ -157,7 +157,7 @@
"extreme_glass": "Extrem",
"february": "Februar",
"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",
"january": "Januar",
"july": "Juli",
@ -210,7 +210,7 @@
"review_scores": "Punkte prüfen",
"review_scores_agree": "Berechnete Kategorie bestätigen",
"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_pending_upload": "Ausstehend — wird mit dem Upload gesendet",
"review_scores_save_failed": "Kategorie konnte nicht gespeichert werden.",
@ -233,10 +233,10 @@
"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_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_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)",
"username_hint": "Benutzername",
"year": "Jahr"

View File

@ -70,7 +70,7 @@ case 'POST':
}
if (!$chk->fetch()) {
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);

View File

@ -83,7 +83,7 @@ case 'POST':
}
if (!$chkCoach->fetch()) {
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");

View File

@ -20,7 +20,7 @@ if (!$rec) {
$role = (string)($rec['role'] ?? '');
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 = '';

View File

@ -207,7 +207,7 @@ case 'PATCH':
$chk->execute([':cid' => $target['entityID'], ':svid' => $callerEntityID]);
if (!$chk->fetch()) {
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 {
qdb_discard($tmpDb, $lockFp);
@ -274,7 +274,7 @@ case 'PATCH':
$chk->execute([':cid' => $target['entityID'], ':svid' => $callerEntityID]);
if (!$chk->fetch()) {
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 {
qdb_discard($tmpDb, $lockFp);
@ -396,7 +396,7 @@ case 'DELETE':
$chk->execute([':cid' => $target['entityID'], ':svid' => $callerEntityID]);
if (!$chk->fetch()) {
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]);
$coach = $chk->fetch(PDO::FETCH_ASSOC);
if (!$coach) {
json_error('NOT_FOUND', 'Coach not found', 404);
json_error('NOT_FOUND', 'Counselor not found', 404);
}
if ($role === 'supervisor' && ($coach['supervisorID'] ?? '') !== $entityID) {
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);
$svUser = trim($row['supervisor'] ?? $row['supervisorUsername'] ?? '');
if ($svUser === '' || !qdb_dev_has_prefix($svUser, $prefix)) {
$errors[] = "Coach $username: missing or invalid supervisor";
$errors[] = "Counselor $username: missing or invalid supervisor";
continue;
}
$supervisorID = $supervisorIdByUsername[$svUser]
?? qdb_dev_lookup_entity_id($pdo, $svUser);
if ($supervisorID === '') {
$errors[] = "Coach $username: supervisor $svUser not found";
$errors[] = "Counselor $username: supervisor $svUser not found";
continue;
}
if (qdb_dev_user_exists($pdo, $username)) {

View File

@ -35,6 +35,13 @@ export function canEdit() {
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() {
invalidateSessionCheck();
localStorage.removeItem('qdb_token');
@ -109,7 +116,7 @@ function updateNav() {
if (isLoggedIn()) {
nav.style.display = '';
const role = getRole();
const roleLabel = role === 'coach' ? 'counselor' : role;
const roleLabel = formatRoleLabel(role);
userInfo.textContent = `${getUser()} (${roleLabel})`;
// 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">
</th>
<th>Client Code</th>
<th>Current Counselor</th>
<th>Current counselor</th>
</tr>
</thead>
<tbody id="clientTableBody"></tbody>

View File

@ -105,7 +105,7 @@ function renderClients() {
<thead>
<tr>
<th>Client Code</th>
<th>Current Counselor</th>
<th>Current counselor</th>
${showProfileColumn ? '<th>Profile scores</th>' : ''}
<th>Actions</th>
</tr>
@ -305,7 +305,7 @@ function scoringLegendHTML() {
<dl class="client-scoring-legend-dl">
<div>
<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>
<dt>Counselor</dt>
@ -371,7 +371,7 @@ 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 counselor category">
title="Change Counselor category">
<span class="coach-band-trigger-badge">${triggerBadge}</span>
<span class="coach-band-trigger-caret" aria-hidden="true">▾</span>
</button>
@ -423,7 +423,7 @@ 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 counselor 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;
@ -595,7 +595,7 @@ function openCoachBandConfirmModal({ clientCode, profileName, calculatedBand, co
overlay.setAttribute('aria-modal', 'true');
overlay.innerHTML = `
<div class="modal-dialog">
<h2>Set counselor category</h2>
<h2>Set Counselor category</h2>
<p class="modal-subtitle">
<strong>${esc(profileName)}</strong> · ${esc(clientCode)}
</p>
@ -610,7 +610,7 @@ function openCoachBandConfirmModal({ clientCode, profileName, calculatedBand, co
${bandBadgeHTML(coachBand, { label: bandLabel(coachBand) })}
</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">
<button type="button" class="btn" data-coach-cancel>Cancel</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">
</div>
<div class="form-group" style="flex:2">
<label>Assign to Counselor</label>
<label>Assign to counselor</label>
<select id="cc_coach">
<option value="">— select counselor —</option>
${coachesList.map(c =>

View File

@ -325,7 +325,7 @@ function renderCoachTableBody() {
const q = filterSearch.trim();
countEl.textContent = q
? `${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 => {

View File

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

View File

@ -28,7 +28,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 / Counselor Code' },
{ value: 'client_coach_code_question', label: 'Client / Counselor code' },
{ value: 'last_page', label: 'Last Page' },
];
@ -659,7 +659,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="counselor_code">
<input type="text" id="${prefix}_hint2" value="${esc(config.hint2 || '')}" placeholder="berater_code">
</div>
</div>`;
break;
@ -824,7 +824,7 @@ function renderEditor() {
<div id="retiredQuestionsSection" style="display:none;margin-top:20px">
<details class="retired-questions-panel">
<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>
</details>
</div>

View File

@ -102,7 +102,7 @@ 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 counselor 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}
@ -141,7 +141,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 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>` : ''}
<div class="card" style="margin-top:20px">

View File

@ -575,7 +575,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 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 class="form-group scoring-profile-active-row">
<label class="checkbox-inline">

View File

@ -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, 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">
<datalist id="questionKeyList"></datalist>
<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)];
const rows = clients.map(c => {

View File

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

View File

@ -1,5 +1,5 @@
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 */
@ -13,9 +13,7 @@ function esc(s) {
}
function roleLabel(role) {
if (!role) return '';
if (role === 'coach') return 'Counselor';
return role.charAt(0).toUpperCase() + role.slice(1);
return formatRoleLabel(role);
}
function signInHintForRole(role) {

View File

@ -399,7 +399,7 @@ function renderQuestionBody(q, sectionIdx) {
case 'client_coach_code_question':
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">
<label>Client code
<input type="text" class="qp-readonly-field" data-field="client"
@ -434,7 +434,7 @@ function layoutLabel(type) {
date_spinner: 'Date',
string_spinner: 'String spinner',
free_text: 'Free text',
client_coach_code_question: 'Client / counselor code',
client_coach_code_question: 'Client / Counselor code',
last_page: 'Last page',
};
return labels[type] || type || 'Unknown';