Specs stay at repo root (cross-VM). Move deploy and code into logical projects with README per domain, updated manifest.yaml, and symlinks at legacy paths for VM122 backward compatibility.
88 lines
3.8 KiB
JavaScript
88 lines
3.8 KiB
JavaScript
/**
|
|
* Billing UI — Spec 023 + VM123 deep-links (Spec 027 Fase 3)
|
|
*/
|
|
const DeskBilling = (() => {
|
|
const API = '/api';
|
|
|
|
function esc(s) {
|
|
return String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
async function api(path, options = {}) {
|
|
const res = await fetch(`${API}${path}`, {
|
|
...options,
|
|
headers: { ...authHeaders(), 'Content-Type': 'application/json', ...(options.headers || {}) },
|
|
});
|
|
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).detail || res.statusText);
|
|
return res.json();
|
|
}
|
|
|
|
function closeModal() {
|
|
document.querySelector('.billing-modal-backdrop')?.remove();
|
|
}
|
|
|
|
function vm123LinksHtml(vm123, acc) {
|
|
const links = vm123?.links || {};
|
|
const perms = vm123?.permissions || {};
|
|
const foss = links.foss || {};
|
|
const odoo = links.odoo || {};
|
|
const op = links.openpanel || {};
|
|
const fossHint = vm123?.foss?.client_id ? ` · cliente #${vm123.foss.client_id}` : '';
|
|
const odooHint = vm123?.odoo?.partner_name ? ` · ${vm123.odoo.partner_name}` : '';
|
|
const parts = [
|
|
`<a href="${esc(foss.url || acc.links?.fossbilling || '#')}" target="_blank" rel="noreferrer">FOSSBilling 💳${esc(fossHint)}</a>`,
|
|
`<a href="${esc(odoo.url || acc.links?.odoo || '#')}" target="_blank" rel="noreferrer">Odoo${esc(odooHint)}</a>`,
|
|
];
|
|
if (perms.can_foss_admin || perms.can_openpanel_autologin) {
|
|
parts.push(`<a href="${esc(op.url || '#')}" target="_blank" rel="noreferrer">OpenAdmin</a>`);
|
|
}
|
|
return parts.join(' · ');
|
|
}
|
|
|
|
async function openAccountModal(domain) {
|
|
closeModal();
|
|
const acc = await api(`/v1/billing/accounts/by-domain/${encodeURIComponent(domain)}`);
|
|
let vm123 = null;
|
|
try {
|
|
const q = new URLSearchParams({ domain });
|
|
if (acc.email_billing) q.set('email', acc.email_billing);
|
|
vm123 = await api(`/v1/vm123/links/client?${q}`);
|
|
} catch {
|
|
vm123 = null;
|
|
}
|
|
const canManage = typeof canManageBilling === 'function' ? canManageBilling() : canManageVm112Domains?.();
|
|
const backdrop = document.createElement('div');
|
|
backdrop.className = 'billing-modal-backdrop';
|
|
backdrop.innerHTML = `
|
|
<div class="billing-modal" role="dialog">
|
|
<h3 style="margin-top:0">Conta do cliente — ${esc(domain)}</h3>
|
|
<dl class="kv">
|
|
<dt>Estado</dt><dd>${esc(acc.billing_state)}</dd>
|
|
<dt>Razão social</dt><dd>${esc(acc.legal_name || acc.trade_name || '—')}</dd>
|
|
<dt>Email cobrança</dt><dd>${esc(acc.email_billing || '—')}</dd>
|
|
<dt>CNPJ/CPF</dt><dd>${esc(acc.tax_id || '—')}</dd>
|
|
<dt>Recorrência</dt><dd>${acc.recurrence_active ? '✅ ativa' : '—'}</dd>
|
|
</dl>
|
|
<p class="ticket-meta vm123-links">${vm123LinksHtml(vm123, acc)}</p>
|
|
<div style="display:flex;gap:0.5rem;margin-top:1rem;flex-wrap:wrap">
|
|
${canManage ? `<button type="button" class="btn btn-primary btn-sm" data-billing-ativate="${acc.id}">Activar recorrência</button>` : ''}
|
|
<button type="button" class="btn btn-sm" data-billing-close>Fechar</button>
|
|
</div>
|
|
</div>`;
|
|
document.body.appendChild(backdrop);
|
|
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) closeModal(); });
|
|
backdrop.querySelector('[data-billing-close]')?.addEventListener('click', closeModal);
|
|
backdrop.querySelector('[data-billing-ativate]')?.addEventListener('click', async () => {
|
|
await api(`/v1/billing/accounts/${acc.id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ recurrence_active: true, billing_state: 'billing_active' }),
|
|
});
|
|
closeModal();
|
|
if (state.view === 'overview-home') await renderOverviewHome();
|
|
});
|
|
}
|
|
|
|
return { openAccountModal, closeModal };
|
|
})();
|
|
|
|
window.DeskBilling = DeskBilling;
|