This commit is contained in:
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user