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