From 92651811540c1b74800d20128b4f909ad8435cbf Mon Sep 17 00:00:00 2001 From: Tom Hempel Date: Fri, 17 Jul 2026 17:37:32 +0200 Subject: [PATCH] cleaner note edit form --- website/css/style.css | 51 ++++++++++++++-- website/js/pages/clients.js | 117 ++++++++++++++++++++++++++---------- 2 files changed, 132 insertions(+), 36 deletions(-) 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 = ` + `; + 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) {