display client notes in client list
Some checks failed
PHPUnit / test (push) Has been cancelled

This commit is contained in:
2026-07-17 17:33:48 +02:00
parent f90d5db40a
commit fc23f7a619
4 changed files with 71 additions and 5 deletions

View File

@ -24,9 +24,16 @@ case 'GET':
$archiveFilter = 'active';
}
[$clause, $params] = rbac_client_filter($tokenRec, 'cl', $archiveFilter);
$noteSelect = qdb_table_exists($pdo, 'client_followup_note')
? 'n.note AS note'
: "'' AS note";
$noteJoin = qdb_table_exists($pdo, 'client_followup_note')
? 'LEFT JOIN client_followup_note n ON n.clientCode = cl.clientCode'
: '';
$stmt = $pdo->prepare(
"SELECT cl.clientCode, cl.coachID, cl.archived, cl.archivedAt,
co.username AS coachUsername,
$noteSelect,
CASE WHEN EXISTS (
SELECT 1 FROM completed_questionnaire cq WHERE cq.clientCode = cl.clientCode
) OR EXISTS (
@ -34,6 +41,7 @@ case 'GET':
) THEN 1 ELSE 0 END AS hasResponseData
FROM client cl
LEFT JOIN coach co ON co.coachID = cl.coachID
$noteJoin
WHERE $clause
ORDER BY cl.archived ASC, hasResponseData DESC, cl.clientCode ASC"
);
@ -44,6 +52,7 @@ case 'GET':
require_once __DIR__ . '/../lib/submissions.php';
$scoringSummary = qdb_clients_scoring_summary_for_list($pdo, $tokenRec);
foreach ($clients as &$client) {
$client['note'] = (string)($client['note'] ?? '');
$client['scoringProfiles'] = qdb_client_scoring_dots_for_client(
$client['clientCode'],
$scoringSummary

View File

@ -41,6 +41,36 @@ final class ClientsLifecycleTest extends QdbTestCase
$this->assertSame($code, $res['data']['clientCode']);
}
public function testClientListIncludesFollowupNote(): void
{
$token = $this->api()->loginWebAndGetToken(
DatabaseSeeder::ADMIN_USERNAME,
DatabaseSeeder::PASSWORD
)['token'];
$code = $this->fixture()->clientCode;
$note = 'Clients list note check';
$save = $this->api()->withToken($token, 'PUT', 'analytics', [
'clientCode' => $code,
'note' => $note,
]);
$this->assertApiOk($save);
$list = $this->api()->withToken($token, 'GET', 'clients', null, [
'archiveFilter' => 'all',
]);
$this->assertApiOk($list);
$match = null;
foreach ($list['data']['clients'] ?? [] as $row) {
if (($row['clientCode'] ?? '') === $code) {
$match = $row;
break;
}
}
$this->assertNotNull($match);
$this->assertSame($note, $match['note'] ?? null);
}
public function testArchiveHidesClientFromListsAndFollowUp(): void
{
$this->submitFixtureQuestionnaire();

View File

@ -3497,6 +3497,27 @@ body.logged-in .insights-chart-card.card:hover {
min-width: 200px;
}
.client-note-cell {
max-width: 220px;
vertical-align: middle;
}
.client-note-text {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.9rem;
line-height: 1.35;
color: var(--text-primary);
}
.client-note-empty {
color: var(--text-secondary);
}
.followup-note-cell .followup-note-input {
width: 100%;
margin-bottom: 8px;

View File

@ -75,7 +75,7 @@ function renderClients() {
<div class="card">
<div class="filter-bar clients-list-filters">
${archiveFilterSelectHTML()}
<input type="search" id="clientListSearch" class="filter-search" placeholder="Search client or counselor…" value="${esc(filterSearch)}">
<input type="search" id="clientListSearch" class="filter-search" placeholder="Search client, counselor, or note…" value="${esc(filterSearch)}">
</div>
<div class="empty-state">${emptyMsg}</div>
</div>`;
@ -93,7 +93,7 @@ function renderClients() {
<div class="card">
<div class="filter-bar clients-list-filters">
${archiveFilterSelectHTML()}
<input type="search" id="clientListSearch" class="filter-search" placeholder="Search client or counselor…" value="${esc(filterSearch)}">
<input type="search" id="clientListSearch" class="filter-search" placeholder="Search client, counselor, or note…" value="${esc(filterSearch)}">
<span class="data-toolbar-hint" id="clientListCount"></span>
</div>
${profileLegend}
@ -106,6 +106,7 @@ function renderClients() {
<tr>
<th>Client Code</th>
<th>Current counselor</th>
<th>Note</th>
${showProfileColumn ? '<th>Profile scores</th>' : ''}
<th>Actions</th>
</tr>
@ -157,7 +158,7 @@ function getFilteredClientsList() {
if (filterSearch) {
const q = filterSearch.toLowerCase();
list = list.filter(c => {
const hay = `${c.clientCode || ''} ${c.coachUsername || ''}`.toLowerCase();
const hay = `${c.clientCode || ''} ${c.coachUsername || ''} ${c.note || ''}`.toLowerCase();
return hay.includes(q);
});
}
@ -169,7 +170,7 @@ function renderClientTableBody() {
const countEl = document.getElementById('clientListCount');
const filtered = getFilteredClientsList();
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
const colCount = scoringProfileCatalog.length > 0 ? 4 : 3;
const colCount = scoringProfileCatalog.length > 0 ? 5 : 4;
if (page >= totalPages) page = totalPages - 1;
const slice = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
@ -733,12 +734,16 @@ function clientRowHTML(c) {
const profileCol = scoringProfileCatalog.length > 0
? `<td class="client-profile-dots-cell">${profileDotsHTML(c.scoringProfiles || [], c.clientCode)}</td>`
: '';
const colCount = scoringProfileCatalog.length > 0 ? 4 : 3;
const colCount = scoringProfileCatalog.length > 0 ? 5 : 4;
const actions = clientTableActionsHTML({
clientCode: esc(c.clientCode),
archived,
showDelete: true,
});
const note = String(c.note || '').trim();
const noteCell = note
? `<td class="client-note-cell" title="${esc(note)}"><span class="client-note-text">${esc(note)}</span></td>`
: `<td class="client-note-cell client-note-empty">—</td>`;
return `
<tr class="${archived ? 'client-row-archived' : ''}">
@ -746,6 +751,7 @@ function clientRowHTML(c) {
${expandControl}<strong>${esc(c.clientCode)}</strong>${clientArchivedLabel(c)}
</td>
<td>${coach}</td>
${noteCell}
${profileCol}
<td class="client-actions-cell">${actions}</td>
</tr>