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