cleaner note edit form
Some checks failed
PHPUnit / test (push) Has been cancelled

This commit is contained in:
2026-07-17 17:37:32 +02:00
parent 497246cd57
commit 9265181154
2 changed files with 132 additions and 36 deletions

View File

@ -3498,20 +3498,63 @@ body.logged-in .insights-chart-card.card:hover {
} }
.client-note-cell { .client-note-cell {
min-width: 200px;
max-width: 280px; max-width: 280px;
vertical-align: middle; vertical-align: middle;
} }
.client-note-cell .client-note-input { .client-note-row {
width: 100%; display: flex;
margin-bottom: 8px; 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 { .client-note-empty {
flex: 1;
color: var(--text-secondary); 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 { .followup-note-cell .followup-note-input {
width: 100%; width: 100%;
margin-bottom: 8px; margin-bottom: 8px;

View File

@ -204,12 +204,8 @@ function renderClientTableBody() {
body.querySelectorAll('.restore-client-btn').forEach(btn => { body.querySelectorAll('.restore-client-btn').forEach(btn => {
btn.addEventListener('click', () => setClientArchived(btn.dataset.code, false)); btn.addEventListener('click', () => setClientArchived(btn.dataset.code, false));
}); });
body.querySelectorAll('.save-client-note-btn').forEach(btn => { body.querySelectorAll('.edit-client-note-btn').forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => openClientNoteEditor(btn.dataset.client));
const tr = btn.closest('tr');
const ta = tr?.querySelector('.client-note-input');
saveClientNote(btn.dataset.client, ta);
});
}); });
bindCoachBandPickerEvents(body); bindCoachBandPickerEvents(body);
} }
@ -747,20 +743,20 @@ function clientRowHTML(c) {
archived, archived,
showDelete: true, showDelete: true,
}); });
const note = String(c.note || ''); const note = String(c.note || '').trim();
const noteCell = ` const noteCell = `
<td class="followup-note-cell client-note-cell"> <td class="client-note-cell">
<textarea class="followup-note-input client-note-input" rows="2" <div class="client-note-row">
data-client="${esc(c.clientCode)}" maxlength="200" ${note
placeholder="Client note">${esc(note)}</textarea> ? `<span class="client-note-text">${esc(note)}</span>`
<div class="followup-note-actions"> : `<span class="client-note-empty">—</span>`}
<button type="button" class="btn btn-sm btn-primary save-client-note-btn" <button type="button" class="btn btn-sm btn-link edit-client-note-btn"
data-client="${esc(c.clientCode)}">Save note</button> data-client="${esc(c.clientCode)}" title="Edit note">Edit</button>
</div> </div>
</td>`; </td>`;
return ` return `
<tr class="${archived ? 'client-row-archived' : ''}" data-client="${esc(c.clientCode)}"> <tr class="${archived ? 'client-row-archived' : ''}">
<td> <td>
${expandControl}<strong>${esc(c.clientCode)}</strong>${clientArchivedLabel(c)} ${expandControl}<strong>${esc(c.clientCode)}</strong>${clientArchivedLabel(c)}
</td> </td>
@ -820,23 +816,80 @@ function toggleVersion(clientCode, questionnaireID, submissionID) {
renderClientTableBody(); renderClientTableBody();
} }
async function saveClientNote(clientCode, ta) { async function openClientNoteEditor(clientCode) {
if (!ta || !clientCode) return; const row = clientsList.find(c => c.clientCode === clientCode);
const note = ta.value.trim(); if (!row) return;
if (note.length > 200) {
showToast('Note must be at most 200 characters', 'error'); const overlay = document.createElement('div');
return; overlay.className = 'modal-overlay client-note-modal';
} overlay.setAttribute('role', 'dialog');
try { overlay.setAttribute('aria-modal', 'true');
const res = await apiPut('analytics.php', { clientCode, note }); overlay.innerHTML = `
const saved = res?.note ?? note; <div class="modal-dialog client-note-modal-dialog">
ta.value = saved; <h2>Edit note</h2>
const row = clientsList.find(c => c.clientCode === clientCode); <p class="modal-subtitle"><strong>${esc(clientCode)}</strong></p>
if (row) row.note = saved; <label class="client-note-modal-label" for="clientNoteModalInput">Note (max 200 characters)</label>
showToast('Note saved', 'success'); <textarea id="clientNoteModalInput" class="client-note-modal-input" rows="4"
} catch (e) { maxlength="200" placeholder="Client note…">${esc(row.note || '')}</textarea>
showToast(e.message, 'error'); <p class="client-note-modal-count" id="clientNoteModalCount"></p>
} <div class="modal-actions">
<button type="button" class="btn" data-note-cancel>Cancel</button>
<button type="button" class="btn btn-primary" data-note-save>Save</button>
</div>
</div>`;
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) { async function setClientArchived(clientCode, archived) {