bmc_hub/static/js/vendor-resources.js

120 lines
12 KiB
JavaScript
Raw Permalink Normal View History

(() => {
const base = `/api/v1/vendors/${vendorId}`;
const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const date = value => value ? new Date(value).toLocaleDateString('da-DK') : '—';
async function api(url, options) {
const response = await fetch(url, options);
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new Error(body.detail || 'Kunne ikke hente data');
}
return response.json();
}
const post = (url, payload) => api(url, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)});
const message = text => { document.getElementById('vendorResourcesStatus').textContent = text; };
let mailOffset = 0;
async function loadFiles() {
const rows = await api(`${base}/files`);
document.getElementById('vendorFilesList').innerHTML = rows.length ? rows.map(f => `<div class="list-group-item d-flex flex-wrap justify-content-between gap-2"><div><a href="${base}/files/${f.source}/${Number(f.id)}?inline=true" target="_blank" rel="noopener">${esc(f.filename)}</a><div class="small text-muted">${f.source === 'email' ? 'Mailvedhæftning' : 'Uploadet'} · ${date(f.created_at)} · ${Math.ceil((f.size_bytes || 0)/1024)} KB</div></div><div class="d-flex flex-wrap gap-2 align-items-center"><a class="btn btn-sm btn-outline-primary" href="${base}/files/${f.source}/${Number(f.id)}?inline=true" target="_blank" rel="noopener">Åbn</a><a class="btn btn-sm btn-outline-secondary" href="${base}/files/${f.source}/${Number(f.id)}">Download</a>${f.email_id ? `<button class="btn btn-sm btn-outline-secondary" data-mail="${Number(f.email_id)}">Vis mail</button>` : ''}</div></div>`).join('') : '<p class="text-muted">Ingen filer endnu.</p>';
document.querySelectorAll('#vendorFilesList [data-mail]').forEach(b => b.onclick = () => openMail(Number(b.dataset.mail)));
}
async function openMail(id) {
try {
const mail = await api(`${base}/emails/${id}`);
document.querySelector('[data-tab="email"]').click();
const host = document.getElementById('vendorMailPreview');
host.innerHTML = `<h6>${esc(mail.subject || '(Uden emne)')}</h6><div class="small text-muted mb-3">Fra: ${esc(mail.sender_email)}<br>Til: ${esc(mail.recipient_email)}${mail.cc ? `<br>Cc: ${esc(mail.cc)}` : ''}</div><pre class="mb-0" style="white-space:pre-wrap;font-family:inherit;overflow-wrap:anywhere">${esc(mail.body_text || '')}</pre>`;
if (!mail.body_text && mail.body_html) {
const frame = document.createElement('iframe');
frame.setAttribute('sandbox','');
frame.title = 'Mailindhold';
frame.style.cssText = 'width:100%;height:420px;border:0';
frame.srcdoc = '<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; style-src \'unsafe-inline\'; img-src data:">' + mail.body_html;
host.append(frame);
}
host.scrollIntoView({block:'nearest'});
} catch(e) { message(e.message); }
}
async function loadEmails() {
const rows = await api(`${base}/emails?limit=50&offset=${mailOffset}`);
const groups = new Map();
rows.forEach(m => {
const key = m.thread_key || `mail:${m.id}`;
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(m);
});
document.getElementById('vendorEmailsList').innerHTML = rows.length ? [...groups.values()].map(items => `<div class="border rounded mb-3 p-3"><h6>${esc(items[0].subject || '(Uden emne)')}</h6>${items.map(m => `<button class="list-group-item list-group-item-action border-0" data-mail="${Number(m.id)}"><span class="${m.is_read ? '' : 'fw-bold'}">${esc(m.subject || '(Uden emne)')}</span><div class="small text-muted">${esc(m.sender_email)} → ${esc(m.recipient_email)} · ${date(m.received_date)}${m.is_read ? '' : ' · Ulæst'}${m.has_attachments ? ' · Vedhæftninger' : ''}</div></button>${m.linked_case_id ? `<a class="small" href="/sag/${Number(m.linked_case_id)}/v5">Sag #${Number(m.linked_case_id)}</a>` : ''}`).join('')}</div>`).join('') : '<p class="text-muted">Ingen mails på denne side.</p>';
document.querySelectorAll('#vendorEmailsList [data-mail]').forEach(b => b.onclick = () => openMail(Number(b.dataset.mail)));
document.getElementById('vendorMailPrevious').disabled = mailOffset === 0;
document.getElementById('vendorMailNext').disabled = rows.length < 50;
document.getElementById('vendorMailPage').textContent = `Side ${mailOffset / 50 + 1}`;
}
async function loadCases() {
const rows = await api(`${base}/cases`);
document.getElementById('vendorCasesList').innerHTML = rows.length ? rows.map(s => `<a class="list-group-item list-group-item-action" href="/sag/${Number(s.id)}/v5"><strong>#${Number(s.id)} · ${esc(s.titel)}</strong><div class="small text-muted">${esc(s.customer_name)} · ${esc(s.status)} · ${s.explicit_link ? 'Direkte kobling' : 'Via mail'} · ${date(s.updated_at)}</div></a>`).join('') : '<p class="text-muted">Ingen tilknyttede sager.</p>';
}
window.showAddVendorContact = () => { location.href=`/contacts?vendor_id=${vendorId}&create=1&return_to=${encodeURIComponent(`/vendors/${vendorId}#kontakter`)}`; };
let contactsData = [], onlyCallable = false;
const contactName = c => [c.first_name,c.last_name].filter(Boolean).join(' ') || '—';
const phoneValue = value => { const clean=String(value || '').trim(); return /[0-9]/.test(clean) && !['-', '—', 'null', 'none', 'n/a'].includes(clean.toLowerCase()) ? clean : ''; };
function renderContacts() {
const query = document.getElementById('contactsSearchInput').value.trim().toLowerCase();
const helpers={name:contactName,phone:c=>phoneValue(c.phone),mobile:c=>phoneValue(c.mobile)};
const rows = CompanyContacts.filter(contactsData,query,onlyCallable,helpers);
document.getElementById('contactsResultCount').textContent = `${rows.length} / ${contactsData.length}`;
const actions = (number,name,id) => `<div class="d-flex align-items-center gap-2 flex-wrap"><button type="button" class="btn btn-sm btn-outline-success btn-voip" data-call="${esc(number)}"><i class="bi bi-telephone-outbound me-1"></i>VOIP</button><button type="button" class="btn btn-sm btn-outline-primary" data-sms="${esc(number)}" data-name="${esc(name)}" data-contact="${id}">SMS</button></div>`;
const host = document.getElementById('contactsContainer');
host.innerHTML = rows.length ? CompanyContacts.table(CompanyContacts.rows(rows,{...helpers,actions})) : `<div class="text-center py-5 text-muted">${contactsData.length ? 'Ingen kontakter matcher søgningen' : 'Ingen kontakter endnu'}</div>`;
const unlinkHost=document.getElementById('vendorContactUnlinkList');
unlinkHost.innerHTML=contactsData.map(c=>`<div class="list-group-item d-flex justify-content-between gap-2"><span>${esc(contactName(c))}</span><button class="btn btn-sm btn-outline-secondary" data-unlink="${Number(c.id)}">Fjern kobling</button></div>`).join('');
host.querySelectorAll('[data-sms]').forEach(b=>b.onclick=()=>openSmsPrompt(b.dataset.sms,b.dataset.name,Number(b.dataset.contact)));
host.querySelectorAll('[data-call]').forEach(b=>b.onclick=async()=>{
b.disabled=true;
try { const me=await api('/api/v1/auth/me'); await post('/api/v1/telefoni/click-to-call',{number:b.dataset.call,user_id:Number(me.id)||null}); message('Ringer ud via Yealink…'); } catch(e){message(e.message);} finally{b.disabled=false;}
});
unlinkHost.querySelectorAll('[data-unlink]').forEach(b => b.onclick = async () => {
try { await api(`${base}/contacts/${b.dataset.unlink}`, {method:'DELETE'}); await loadContacts(); message('Kontaktkobling fjernet. Kontaktpersonen er bevaret.'); } catch(e) { message(e.message); }
});
}
async function loadContacts() { contactsData = await api(`${base}/contacts`); renderContacts(); }
function search(inputId, resultsId, endpoint, render, select) {
let timer, revision = 0;
document.getElementById(inputId).oninput = event => {
const q = event.target.value.trim(), current = ++revision;
clearTimeout(timer);
const host = document.getElementById(resultsId);
host.replaceChildren();
if (q.length < 2) return;
timer = setTimeout(async () => {
try {
const result = await api(endpoint + encodeURIComponent(q));
if (current !== revision) return;
const rows = Array.isArray(result) ? result : result.contacts || result.items || [];
host.innerHTML = rows.length ? rows.slice(0,20).map((row,i) => `<button type="button" class="list-group-item list-group-item-action" data-index="${i}">${esc(render(row))}</button>`).join('') : '<div class="small text-muted">Ingen resultater.</div>';
host.querySelectorAll('button').forEach(b => b.onclick = async () => {
try { await select(rows[Number(b.dataset.index)]); host.replaceChildren(); message('Koblingen er gemt.'); } catch(e) { message(e.message); }
});
} catch(e) { message(e.message); }
},250);
};
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('contactsSearchInput').oninput=renderContacts;
document.getElementById('contactsOnlyCallableToggle').onclick=event=>{ onlyCallable=!onlyCallable; event.target.classList.toggle('btn-primary',onlyCallable); event.target.classList.toggle('btn-outline-secondary',!onlyCallable); renderContacts(); };
document.getElementById('contactsClearFilters').onclick=()=>{ document.getElementById('contactsSearchInput').value=''; onlyCallable=false; const button=document.getElementById('contactsOnlyCallableToggle'); button.classList.remove('btn-primary'); button.classList.add('btn-outline-secondary'); renderContacts(); };
document.getElementById('vendorFileUpload').onsubmit = async event => {
event.preventDefault();
const button = event.target.querySelector('button'); button.disabled = true;
try { await api(`${base}/files`, {method:'POST',body:new FormData(event.target)}); event.target.reset(); await loadFiles(); message('Filen er uploadet.'); } catch(e) { message(e.message); } finally { button.disabled=false; }
};
document.getElementById('vendorMailPrevious').onclick = () => { mailOffset=Math.max(0,mailOffset-50); loadEmails().catch(e=>message(e.message)); };
document.getElementById('vendorMailNext').onclick = () => { mailOffset+=50; loadEmails().catch(e=>message(e.message)); };
search('vendorCaseSearch','vendorCaseResults','/api/v1/emails/search-sager?q=', s=>`#${s.id} · ${s.titel || s.title}`, async s=>{ await post(`${base}/cases`, {sag_id:s.id}); await loadCases(); });
search('vendorContactSearch','vendorContactResults','/api/v1/contacts?limit=20&search=', c=>`${c.first_name} ${c.last_name} · ${c.email || ''}`, async c=>{ await post(`${base}/contacts`, {contact_id:c.id,role:document.getElementById('vendorContactRole').value,is_primary:document.getElementById('vendorContactPrimary').checked}); await loadContacts(); });
Promise.allSettled([loadFiles(),loadEmails(),loadCases(),loadContacts()]).then(results => {
const errors=results.filter(r=>r.status==='rejected').map(r=>r.reason.message);
if(errors.length) message(errors.join(' · '));
});
});
})();