/**
* Billing UI — Spec 023 + Spec 043 (conta cliente + activação preview)
*/
const DeskBilling = (() => {
const API = '/api';
const PLAN_OPTIONS = [
{ code: 'ligbox-mail-starter', label: 'Mail Starter (10)' },
{ code: 'ligbox-mail-business', label: 'Mail Business (25)' },
{ code: 'ligbox-mail-enterprise', label: 'Mail Enterprise (50)' },
{ code: 'ligbox-mail-custom', label: 'Mail Custom' },
];
function esc(s) {
return String(s ?? '').replace(/&/g, '&').replace(//g, '>');
}
function canActivate() {
return typeof canManageBilling === 'function' && canManageBilling();
}
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 renderMappedBlock(title, obj) {
const rows = Object.entries(obj || {})
.filter(([, v]) => v != null && v !== '' && typeof v !== 'object')
.map(([k, v]) => `
${esc(k)}${esc(v)}`)
.join('');
return `
${esc(title)}
${rows || '- —
- sem dados
'}
`;
}
async function openActivationPreview(accountId, domain, planCode) {
const plan = planCode || document.querySelector('[data-billing-plan]')?.value || '';
const qs = plan ? `?plan_code=${encodeURIComponent(plan)}` : '';
const preview = await api(`/v1/billing/accounts/${accountId}/activation-preview${qs}`);
const m = preview.mapped || {};
const foss = m.foss_client || {};
const op = m.openpanel || {};
const odoo = m.odoo_partner || {};
const meta = op.metadata || {};
const canGo = preview.can_activate !== false;
const backdrop = document.createElement('div');
backdrop.className = 'billing-modal-backdrop';
backdrop.innerHTML = `
Activar conta — ${esc(domain)}
Estado: ${esc(preview.billing_state)} · Plano: ${esc(preview.plan_code)}
${renderMappedBlock('FOSSBilling — cliente', foss)}
${renderMappedBlock('OpenPanel — hub', { username: op.username, email: op.email, plan_name: op.plan_name, domain: op.domain, seats: meta.max_seats })}
${renderMappedBlock('Odoo — empresa', { name: odoo.name, vat: odoo.vat, email: odoo.email, ref: odoo.ref })}
${esc(preview.notes || '')}
${canGo ? `` : ''}
`;
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-activate-confirm]')?.addEventListener('click', async (e) => {
const btn = e.currentTarget;
const statusEl = backdrop.querySelector('[data-billing-activate-status]');
btn.disabled = true;
btn.textContent = 'A activar…';
try {
const res = await fetch(`${API}/v1/billing/accounts/${accountId}/activate`, {
method: 'POST',
headers: { ...authHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({
plan_code: preview.plan_code,
config: preview.config,
provision_mail: true,
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok && res.status !== 207) {
throw new Error(data.detail || data.message || res.statusText);
}
const fossOk = data.steps?.foss?.client_id ? '✅' : '⚠️';
const opOk = data.steps?.openpanel?.success || data.steps?.openpanel?.reused ? '✅' : '⚠️';
const odooOk = data.steps?.odoo?.partner_id ? '✅' : '⚠️';
statusEl.innerHTML = `${data.ok ? '✅ Conta activada' : '⚠️ Parcial'} — FOSS ${fossOk} · OP ${opOk} · Odoo ${odooOk}`;
if (data.message) statusEl.innerHTML += `
${esc(data.message)}`;
btn.textContent = data.ok ? 'Concluído' : 'Concluído com avisos';
if (typeof state !== 'undefined' && state.view === 'overview-home' && typeof renderOverviewHome === 'function') {
await renderOverviewHome();
}
} catch (err) {
statusEl.textContent = err.message || 'Erro na activação';
btn.disabled = false;
btn.textContent = 'Tentar novamente';
}
});
}
async function openAccountModal(domain) {
closeModal();
const acc = await api(`/v1/billing/accounts/by-domain/${encodeURIComponent(domain)}`);
const planOptions = PLAN_OPTIONS.map(
(p) => ``
).join('');
const backdrop = document.createElement('div');
backdrop.className = 'billing-modal-backdrop';
backdrop.innerHTML = `
Conta do cliente — ${esc(domain)}
- Estado
- ${esc(acc.billing_state)}
- Razão social
- ${esc(acc.legal_name || acc.trade_name || '—')}
- Email cobrança
- ${esc(acc.email_billing || '—')}
- CNPJ/CPF
- ${esc(acc.tax_id || '—')}
- Recorrência
- ${acc.recurrence_active ? '✅ ativa' : '—'}
${canActivate() ? `
` : ''}
FOSSBilling 💳
· Odoo
${acc.links?.odoo_partner ? ` · Parceiro Odoo` : ''}
${canActivate() && !acc.recurrence_active ? `` : ''}
${canManageVm112Domains?.() ? `` : ''}
`;
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-activate]')?.addEventListener('click', async (e) => {
const btn = e.currentTarget;
const id = btn.getAttribute('data-billing-activate');
const dom = btn.getAttribute('data-domain') || domain;
const plan = backdrop.querySelector('[data-billing-plan]')?.value || '';
try {
await openActivationPreview(id, dom, plan);
} catch (err) {
alert(err.message || 'Erro ao carregar preview');
}
});
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 (typeof state !== 'undefined' && state.view === 'overview-home' && typeof renderOverviewHome === 'function') {
await renderOverviewHome();
}
});
}
return { openAccountModal, openActivationPreview, closeModal };
})();
window.DeskBilling = DeskBilling;