diff --git a/handlers/clients.php b/handlers/clients.php index 92fb4c3..a9f435b 100644 --- a/handlers/clients.php +++ b/handlers/clients.php @@ -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 diff --git a/tests/Integration/ClientsLifecycleTest.php b/tests/Integration/ClientsLifecycleTest.php index 5870053..ec7ddbe 100644 --- a/tests/Integration/ClientsLifecycleTest.php +++ b/tests/Integration/ClientsLifecycleTest.php @@ -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(); diff --git a/website/css/style.css b/website/css/style.css index a629287..f58806e 100644 --- a/website/css/style.css +++ b/website/css/style.css @@ -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; diff --git a/website/js/pages/clients.js b/website/js/pages/clients.js index e9638b0..cd6e068 100644 --- a/website/js/pages/clients.js +++ b/website/js/pages/clients.js @@ -75,7 +75,7 @@ function renderClients() {
${archiveFilterSelectHTML()} - +
${emptyMsg}
`; @@ -93,7 +93,7 @@ function renderClients() {
${archiveFilterSelectHTML()} - +
${profileLegend} @@ -106,6 +106,7 @@ function renderClients() { Client Code Current counselor + Note ${showProfileColumn ? 'Profile scores' : ''} Actions @@ -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 ? `${profileDotsHTML(c.scoringProfiles || [], c.clientCode)}` : ''; - 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 + ? `${esc(note)}` + : `—`; return ` @@ -746,6 +751,7 @@ function clientRowHTML(c) { ${expandControl}${esc(c.clientCode)}${clientArchivedLabel(c)} ${coach} + ${noteCell} ${profileCol} ${actions}