feat: sidebar nav (default), group member management, per-customer ticketing identity mapping
- Navigation: new per-user setting (default "side") renders the page links as a left sidebar while keeping theme/bell/avatar/logout in the top-right; falls back to a top bar under 760px or when set to "top". Profile selector added. - Group conversations: record a creatorId; the creator gets a "Manage members" button to add/remove participants (new GET/POST/DELETE members endpoints, creator-only). Existing groups backfilled. - Customer ticketing: admin can map each assigned consultant to their username in that customer's ticketing system. Per-customer mapping takes precedence over the consultant's global ticketingIdentities during sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,8 @@ async function openConversation(c) {
|
||||
current = c;
|
||||
oldestCursor = '';
|
||||
document.getElementById('chat-title').textContent = c.title;
|
||||
const mbtn = document.getElementById('chat-members');
|
||||
mbtn.hidden = !(c.kind !== 'dm' && c.creatorId && c.creatorId === meId);
|
||||
form.hidden = false;
|
||||
msgHost.replaceChildren();
|
||||
renderConvList();
|
||||
@@ -297,6 +299,73 @@ document.getElementById('newconv-form').addEventListener('submit', async (e) =>
|
||||
} catch (err) { toast(err.message, 'err'); }
|
||||
});
|
||||
|
||||
// ---- manage members (group creator only) ----
|
||||
const membersDlg = document.getElementById('members-dialog');
|
||||
let mmMemberIds = new Set();
|
||||
|
||||
async function renderMembers() {
|
||||
const res = await api('GET', `/api/v1/conversations/${current.id}/members`);
|
||||
mmMemberIds = new Set(res.members.map((u) => u.id));
|
||||
const host = document.getElementById('mm-list');
|
||||
host.replaceChildren(...res.members.map((u) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'conv-item';
|
||||
li.style.justifyContent = 'space-between';
|
||||
li.innerHTML = `<span>${esc(u.name)} ${u.isCreator ? '<span class="badge accent">creator</span>' : ''}
|
||||
<br><span class="muted">${esc(u.email)}</span></span>`;
|
||||
if (res.canManage && !u.isCreator) {
|
||||
const rm = document.createElement('button');
|
||||
rm.className = 'btn small';
|
||||
rm.textContent = 'Remove';
|
||||
rm.addEventListener('click', async () => {
|
||||
try {
|
||||
await api('DELETE', `/api/v1/conversations/${current.id}/members/${u.id}`);
|
||||
await renderMembers();
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
});
|
||||
li.appendChild(rm);
|
||||
}
|
||||
return li;
|
||||
}));
|
||||
document.getElementById('mm-add-wrap').hidden = !res.canManage;
|
||||
}
|
||||
|
||||
let mmTimer = null;
|
||||
async function mmSearch() {
|
||||
const q = document.getElementById('mm-search').value.trim();
|
||||
const res = await api('GET', `/api/v1/users?q=${encodeURIComponent(q)}`);
|
||||
const host = document.getElementById('mm-results');
|
||||
const rows = res.users.filter((u) => !mmMemberIds.has(u.id)).map((u) => {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'nc-row';
|
||||
b.textContent = `${u.name} — ${u.email}`;
|
||||
b.addEventListener('click', async () => {
|
||||
try {
|
||||
await api('POST', `/api/v1/conversations/${current.id}/members`, { userId: u.id });
|
||||
document.getElementById('mm-search').value = '';
|
||||
host.replaceChildren();
|
||||
await renderMembers();
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
});
|
||||
return b;
|
||||
});
|
||||
host.replaceChildren(...rows);
|
||||
}
|
||||
|
||||
document.getElementById('chat-members').addEventListener('click', async () => {
|
||||
if (!current) return;
|
||||
document.getElementById('mm-search').value = '';
|
||||
document.getElementById('mm-results').replaceChildren();
|
||||
try { await renderMembers(); membersDlg.showModal(); }
|
||||
catch (e) { toast(e.message, 'err'); }
|
||||
});
|
||||
document.getElementById('mm-close').addEventListener('click', () => membersDlg.close());
|
||||
document.getElementById('mm-search').addEventListener('input', () => {
|
||||
clearTimeout(mmTimer);
|
||||
mmTimer = setTimeout(mmSearch, 250);
|
||||
});
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const me = await api('GET', '/api/v1/auth/me');
|
||||
|
||||
Reference in New Issue
Block a user