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>
176 lines
8.7 KiB
JavaScript
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, '&').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]) => `<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;
|