ligbox-ops-platform/projects/ops-desk/frontend/assets/billing-ui.js
Ligbox Spec Hub c815a32bbc feat(desk): Spec 043 Fases 2–6 — activar conta FOSS/OpenPanel/Odoo
Orquestra create_client/order FOSS, bridge OpenPanel com metadata,
upsert res.partner Odoo, mail-bundle wizard e webhook order-activated.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 15:19:54 +00:00

176 lines
8.7 KiB
JavaScript

/**
* 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`)
.join('');
return `
<section class="billing-activation-col">
<h4>${esc(title)}</h4>
<dl class="kv kv-compact">${rows || '<dt>—</dt><dd>sem dados</dd>'}</dl>
</section>`;
}
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 = `
<div class="billing-modal billing-modal-wide" role="dialog">
<h3 style="margin-top:0">Activar conta — ${esc(domain)}</h3>
<p class="ticket-meta">Estado: <strong>${esc(preview.billing_state)}</strong> · Plano: ${esc(preview.plan_code)}</p>
<div class="billing-activation-grid">
${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 })}
</div>
<p class="ticket-meta" style="margin-top:0.75rem">${esc(preview.notes || '')}</p>
<div style="display:flex;gap:0.5rem;margin-top:1rem;flex-wrap:wrap">
${canGo ? `<button type="button" class="btn btn-primary btn-sm" data-billing-activate-confirm>Confirmar activação</button>` : ''}
<button type="button" class="btn btn-sm" data-billing-close>Fechar</button>
</div>
<p class="ticket-meta" data-billing-activate-status style="min-height:1.2em;margin-top:0.5rem"></p>
</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-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 += `<br><span class="ticket-meta">${esc(data.message)}</span>`;
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) => `<option value="${esc(p.code)}" ${acc.plan_code === p.code ? 'selected' : ''}>${esc(p.label)}</option>`
).join('');
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>
${canActivate() ? `
<label class="ticket-meta" style="display:block;margin-top:0.5rem">Plano comercial
<select class="input-sm" data-billing-plan style="display:block;margin-top:0.25rem;max-width:100%">${planOptions}</select>
</label>` : ''}
<p class="ticket-meta">
<a href="${esc(acc.links?.fossbilling || '#')}" target="_blank" rel="noreferrer">FOSSBilling 💳</a>
· <a href="${esc(acc.links?.odoo || '#')}" target="_blank" rel="noreferrer">Odoo</a>
${acc.links?.odoo_partner ? ` · <a href="${esc(acc.links.odoo_partner)}" target="_blank" rel="noreferrer">Parceiro Odoo</a>` : ''}
</p>
<div style="display:flex;gap:0.5rem;margin-top:1rem;flex-wrap:wrap">
${canActivate() && !acc.recurrence_active ? `<button type="button" class="btn btn-primary btn-sm" data-billing-activate="${acc.id}" data-domain="${esc(domain)}">Activar conta</button>` : ''}
${canManageVm112Domains?.() ? `<button type="button" class="btn 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-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;