diff --git a/website/css/style.css b/website/css/style.css
index a470467..cd654ea 100644
--- a/website/css/style.css
+++ b/website/css/style.css
@@ -3498,20 +3498,63 @@ body.logged-in .insights-chart-card.card:hover {
}
.client-note-cell {
- min-width: 200px;
max-width: 280px;
vertical-align: middle;
}
-.client-note-cell .client-note-input {
- width: 100%;
- margin-bottom: 8px;
+.client-note-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+}
+
+.client-note-text {
+ flex: 1;
+ min-width: 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-size: 0.9rem;
+ line-height: 1.35;
+ color: var(--text-primary);
}
.client-note-empty {
+ flex: 1;
color: var(--text-secondary);
}
+.client-note-row .edit-client-note-btn {
+ flex-shrink: 0;
+ padding: 0 4px;
+ min-height: 0;
+ line-height: 1.35;
+}
+
+.client-note-modal-dialog {
+ max-width: 420px;
+}
+
+.client-note-modal-label {
+ display: block;
+ margin: 0 0 6px;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+}
+
+.client-note-modal-input {
+ width: 100%;
+ min-height: 96px;
+ resize: vertical;
+ margin-bottom: 4px;
+}
+
+.client-note-modal-count {
+ margin: 0 0 12px;
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ text-align: right;
+}
+
.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 7d43e9e..2aeb04a 100644
--- a/website/js/pages/clients.js
+++ b/website/js/pages/clients.js
@@ -204,12 +204,8 @@ function renderClientTableBody() {
body.querySelectorAll('.restore-client-btn').forEach(btn => {
btn.addEventListener('click', () => setClientArchived(btn.dataset.code, false));
});
- body.querySelectorAll('.save-client-note-btn').forEach(btn => {
- btn.addEventListener('click', () => {
- const tr = btn.closest('tr');
- const ta = tr?.querySelector('.client-note-input');
- saveClientNote(btn.dataset.client, ta);
- });
+ body.querySelectorAll('.edit-client-note-btn').forEach(btn => {
+ btn.addEventListener('click', () => openClientNoteEditor(btn.dataset.client));
});
bindCoachBandPickerEvents(body);
}
@@ -747,20 +743,20 @@ function clientRowHTML(c) {
archived,
showDelete: true,
});
- const note = String(c.note || '');
+ const note = String(c.note || '').trim();
const noteCell = `
-
-
-
-
+
+
+ ${note
+ ? `${esc(note)}`
+ : `—`}
+
| `;
return `
-
+
|
${expandControl}${esc(c.clientCode)}${clientArchivedLabel(c)}
|
@@ -820,23 +816,80 @@ function toggleVersion(clientCode, questionnaireID, submissionID) {
renderClientTableBody();
}
-async function saveClientNote(clientCode, ta) {
- if (!ta || !clientCode) return;
- const note = ta.value.trim();
- if (note.length > 200) {
- showToast('Note must be at most 200 characters', 'error');
- return;
- }
- try {
- const res = await apiPut('analytics.php', { clientCode, note });
- const saved = res?.note ?? note;
- ta.value = saved;
- const row = clientsList.find(c => c.clientCode === clientCode);
- if (row) row.note = saved;
- showToast('Note saved', 'success');
- } catch (e) {
- showToast(e.message, 'error');
- }
+async function openClientNoteEditor(clientCode) {
+ const row = clientsList.find(c => c.clientCode === clientCode);
+ if (!row) return;
+
+ const overlay = document.createElement('div');
+ overlay.className = 'modal-overlay client-note-modal';
+ overlay.setAttribute('role', 'dialog');
+ overlay.setAttribute('aria-modal', 'true');
+ overlay.innerHTML = `
+
+ Edit note
+ ${esc(clientCode)}
+
+
+
+
+
+
+
+ `;
+ document.body.appendChild(overlay);
+ document.body.classList.add('modal-open');
+
+ const ta = overlay.querySelector('#clientNoteModalInput');
+ const countEl = overlay.querySelector('#clientNoteModalCount');
+ const saveBtn = overlay.querySelector('[data-note-save]');
+
+ const updateCount = () => {
+ const len = ta.value.length;
+ countEl.textContent = `${len} / 200`;
+ };
+ updateCount();
+ ta.addEventListener('input', updateCount);
+ ta.focus();
+ ta.setSelectionRange(ta.value.length, ta.value.length);
+
+ const close = () => {
+ overlay.remove();
+ document.body.classList.remove('modal-open');
+ document.removeEventListener('keydown', onKey);
+ };
+ const onKey = (e) => {
+ if (e.key === 'Escape') close();
+ };
+ document.addEventListener('keydown', onKey);
+
+ overlay.querySelector('[data-note-cancel]').addEventListener('click', close);
+ overlay.addEventListener('click', (e) => {
+ if (e.target === overlay) close();
+ });
+ overlay.querySelector('.modal-dialog').addEventListener('click', (e) => e.stopPropagation());
+
+ saveBtn.addEventListener('click', async () => {
+ const note = ta.value.trim();
+ if (note.length > 200) {
+ showToast('Note must be at most 200 characters', 'error');
+ return;
+ }
+ saveBtn.disabled = true;
+ saveBtn.textContent = 'Saving…';
+ try {
+ const res = await apiPut('analytics.php', { clientCode, note });
+ const saved = res?.note ?? note;
+ row.note = saved;
+ showToast('Note saved', 'success');
+ close();
+ renderClientTableBody();
+ } catch (e) {
+ showToast(e.message, 'error');
+ saveBtn.disabled = false;
+ saveBtn.textContent = 'Save';
+ }
+ });
}
async function setClientArchived(clientCode, archived) {
|