ligbox-ops-platform/projects/ops-desk/frontend/assets/billing-ui.js
Ligbox Spec Hub f344931925 feat(desk): Spec 043 Fase 1 — activation preview API e UI
Adiciona mapper de campos, endpoint GET activation-preview, modal
FOSS/OpenPanel/Odoo e canManageBilling para staff activar conta.

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

139 lines
6.6 KiB
JavaScript
Raw 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.

/**
* 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) {
const planSel = document.querySelector('[data-billing-plan]');
const plan = planSel?.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 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">
<button type="button" class="btn btn-primary btn-sm" data-billing-activate-confirm disabled title="Fase 25 Spec 043">Activar conta (em breve)</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);
}
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>
</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;
try {
await openActivationPreview(id, dom);
} 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;