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

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="dark">
<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">
</head>
<body>
@ -13,7 +13,7 @@
<div class="layout">
<nav class="sidebar" id="mainNav" style="display:none">
<div class="sidebar-brand">
BW Sch&uuml;tzt
BW Sch&uuml;tzt - NAT-AS
<small>Questionnaire Management</small>
</div>
<ul class="sidebar-nav">
@ -38,7 +38,7 @@
<li data-nav-roles="admin supervisor">
<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>
Coach activity
Counselor activity
</a>
</li>
<li data-nav-roles="admin supervisor">

View File

@ -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 => {

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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 => {

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)">
<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'
);

View File

@ -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;

View File

@ -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) {

View File

@ -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>

View File

@ -27,9 +27,9 @@ export async function loginPage() {
app.innerHTML = `
<div class="login-wrapper">
<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="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;
}

View File

@ -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">

View File

@ -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' },

View File

@ -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',

View File

@ -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.';
}

View File

@ -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';