Files
BountyBoard/web/static/js/profile.js
etalon e8beb7d4f3 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>
2026-06-13 11:05:33 +02:00

104 lines
3.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { api, toast } from '/static/js/api.js';
const form = document.getElementById('profile-form');
const errorBox = document.getElementById('error');
const okBox = document.getElementById('ok');
const extraRows = document.getElementById('extra-rows');
let version = parseInt(form.dataset.version, 10);
function addExtraRow(key, value) {
const row = document.createElement('div');
row.className = 'kv-row';
const k = document.createElement('input');
k.type = 'text'; k.placeholder = 'Field name'; k.value = key || '';
k.setAttribute('aria-label', 'Field name');
const v = document.createElement('input');
v.type = 'text'; v.placeholder = 'Value'; v.value = value || '';
v.setAttribute('aria-label', 'Field value');
const rm = document.createElement('button');
rm.type = 'button'; rm.className = 'btn small'; rm.textContent = '×';
rm.setAttribute('aria-label', 'Remove field');
rm.addEventListener('click', () => row.remove());
row.append(k, v, rm);
extraRows.appendChild(row);
}
async function loadExtra() {
const me = await api('GET', '/api/v1/auth/me');
version = me.user.version;
const extra = me.user.extra || {};
Object.keys(extra).forEach((k) => addExtraRow(k, String(extra[k])));
}
loadExtra().catch((e) => { errorBox.textContent = e.message; });
document.getElementById('extra-add').addEventListener('click', () => addExtraRow('', ''));
form.addEventListener('submit', async (e) => {
e.preventDefault();
errorBox.textContent = '';
okBox.textContent = '';
const extra = {};
extraRows.querySelectorAll('.kv-row').forEach((row) => {
const [k, v] = row.querySelectorAll('input');
if (k.value.trim()) extra[k.value.trim()] = v.value;
});
const links = document.getElementById('p-links').value
.split('\n').map((s) => s.trim()).filter(Boolean);
try {
const res = await api('PATCH', '/api/v1/profile', {
version,
name: document.getElementById('p-name').value.trim(),
bio: document.getElementById('p-bio').value,
contact: {
phone: document.getElementById('p-phone').value.trim(),
location: document.getElementById('p-location').value.trim(),
links,
},
extra,
settings: {
theme: document.getElementById('p-theme').value,
navLayout: document.getElementById('p-nav').value,
},
});
version = res.user.version;
const theme = res.user.settings.theme;
document.documentElement.dataset.theme = theme;
try { localStorage.setItem('theme', theme); } catch (err) { /* ignore */ }
document.body.dataset.nav = res.user.settings.navLayout || 'side';
okBox.textContent = 'Profile saved.';
} catch (err) {
if (err.status === 409) {
errorBox.textContent = 'This profile changed elsewhere — reload the page and try again.';
} else {
errorBox.textContent = err.message;
}
}
});
document.getElementById('avatar-file').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const fd = new FormData();
fd.append('file', file);
try {
const res = await api('POST', '/api/v1/profile/avatar', fd);
const img = document.createElement('img');
img.className = 'avatar large';
img.alt = 'Current avatar';
img.src = '/files/' + res.fileId + '?v=' + Date.now();
const preview = document.getElementById('avatar-preview');
preview.replaceChildren(img);
toast('Avatar updated.', 'ok');
} catch (err) {
toast('Avatar upload failed: ' + err.message, 'err');
}
});
document.getElementById('logout-all').addEventListener('click', async () => {
try {
await api('POST', '/api/v1/auth/logout-all', {});
} catch (e) { /* session is gone either way */ }
window.location.href = '/login';
});