`} function quoteOpenPrint(q,items){const w=window.open('','_blank');if(!w)return alert('Permita pop-ups para gerar/imprimir a cotação.');w.document.open();w.document.write(quotePrintHtml(q,items));w.document.close()} async function quoteDetail(id){ const settled=await Promise.allSettled([api(withCid(`/api/quotes/${id}`)),api(withCid('/api/travel/product-capabilities')),api(withCid('/api/email/accounts'))]);if(settled[0].status!=='fulfilled')throw settled[0].reason;const d=settled[0].value,cap=settled[1].status==='fulfilled'?settled[1].value:{products:[]},mail=settled[2].status==='fulfilled'?settled[2].value:{accounts:[]},q=d.quote,items=d.items||[],events=d.events||[],profit=Number(q.total||0)-Number(q.cost_total||0),margin=Number(q.total||0)>0?profit/Number(q.total)*100:0; const legacyTypes=[...new Set(items.map(x=>x.item_type).filter(Boolean))],types=quoteAllowedTypes(cap,legacyTypes),firstType=types[0]?.[0]||'other',typeOpts=(sel='')=>types.map(([v,l])=>``).join(''); QUOTE_ITEM_META_CACHE.clear();items.forEach(x=>QUOTE_ITEM_META_CACHE.set(Number(x.id),x.metadata||{})); const itemRows=items.map(x=>`
${esc(quoteMetaSummary(x.item_type,x.metadata||{}))}
${quoteMilesEligible(x.item_type)?`
Cálculo de milhas · interno${quoteMilesInternalSummary(x.metadata,q.currency)?' · '+esc(quoteMilesInternalSummary(x.metadata,q.currency)):''}${quoteMilesEditorHtml('qim'+x.id,x.metadata||{},q.currency,x.unit_price)}
`:''}${money(x.total_price,q.currency)}
`).join('')||'Adicione os serviços que compõem a proposta.'; const history=events.map(e=>`
${esc(e.summary||e.event_type)}
${esc(e.user_name||'Sistema')}
${fmt(e.created_at)}
`).join('')||'
Sem eventos registrados.
'; const emailAccounts=(mail.accounts||[]).filter(a=>a.status==='active'),emailUi=q.contact_email&&emailAccounts.length?``:''; $('#content').innerHTML=`
${quoteStatusBadge(q.status)}${emailUi}

${esc(q.quote_number)} — ${esc(q.title)}

Viajante: ${esc(q.contact_name||q.contact_phone||'—')}${q.lead_title?' · Lead: '+esc(q.lead_title):''}
Custo${money(q.cost_total,q.currency)}
Venda${money(q.total,q.currency)}
Resultado${money(profit,q.currency)}
Margem${margin.toFixed(1)}%

Itens da cotação

${itemRows}
TipoProduto/serviçoFornecedor internoQtd.Custo un.Venda un.TotalAções

Adicionar outro produto / serviço

Somente produtos e fornecedores liberados para esta empresa aparecem nas opções.
${quoteItemFormHtml('qa',cap,firstType)}

Histórico da cotação

${history}
`; $('#quoteBack').onclick=quotes;$('#quotePrint').onclick=()=>quoteOpenPrint(q,items);await quoteBindItemBuilder('qa',cap); items.forEach(x=>{if(!quoteMilesEligible(x.item_type))return;quoteMilesBind('qim'+x.id,q.currency,async(calc,meta)=>{try{const current=QUOTE_ITEM_META_CACHE.get(Number(x.id))||{};await api('/api/quote-items/'+x.id,{method:'PATCH',body:JSON.stringify({company_id:cid(),unit_cost:calc.calculated_cost,metadata:{...current,internal_miles:meta}})});await quoteDetail(id)}catch(err){alert(err.message)}})}); const fillProvider=(typeSel,providerSel,selected=0,legacy='')=>{providerSel.innerHTML=quoteProviderOptions(cap,typeSel.value,selected,legacy)};document.querySelectorAll('tr[data-qi]').forEach(tr=>{const t=tr.querySelector('.qiType'),pr=tr.querySelector('.qiProvider');t.onchange=()=>fillProvider(t,pr)}); $('#qeStatus').onchange=async()=>{await api('/api/quotes/'+id,{method:'PATCH',body:JSON.stringify({company_id:cid(),status:$('#qeStatus').value})});await quoteDetail(id)}; $('#quoteEditForm').onsubmit=async e=>{e.preventDefault();try{await api('/api/quotes/'+id,{method:'PATCH',body:JSON.stringify({company_id:cid(),title:$('#qeTitle').value,destination:$('#qeDestination').value,start_date:$('#qeStart').value||null,end_date:$('#qeEnd').value||null,adults:Number($('#qeAdults').value||1),children:Number($('#qeChildren').value||0),valid_until:$('#qeValid').value?$('#qeValid').value.replace('T',' ')+':00':null,discount:Number($('#qeDiscount').value||0),fees:Number($('#qeFees').value||0),currency:$('#qeCurrency').value||'BRL',customer_notes:$('#qeCustomerNotes').value,internal_notes:$('#qeInternalNotes').value,status:$('#qeStatus').value})});await quoteDetail(id)}catch(err){alert(err.message)}}; $('#qaAdd').onclick=async()=>{try{const item=quoteReadItemBuilder('qa');await api(`/api/quotes/${id}/items`,{method:'POST',body:JSON.stringify({company_id:cid(),...item})});await quoteDetail(id)}catch(err){alert(err.message)}}; document.querySelectorAll('.qiSave').forEach(b=>b.onclick=async()=>{const tr=b.closest('tr'),provider=quoteProviderPayload(tr.querySelector('.qiProvider'));try{await api('/api/quote-items/'+b.dataset.id,{method:'PATCH',body:JSON.stringify({company_id:cid(),item_type:tr.querySelector('.qiType').value,title:tr.querySelector('.qiTitle').value,...provider,quantity:Number(tr.querySelector('.qiQty').value||1),unit_cost:Number(tr.querySelector('.qiCost').value||0),unit_price:Number(tr.querySelector('.qiPrice').value||0)})});await quoteDetail(id)}catch(err){alert(err.message)}}); document.querySelectorAll('.qiDup').forEach(b=>b.onclick=async()=>{const x=items.find(i=>Number(i.id)===Number(b.dataset.id));if(!x)return;try{await api(`/api/quotes/${id}/items`,{method:'POST',body:JSON.stringify({company_id:cid(),item_type:x.item_type,title:x.title,platform_provider_id:x.platform_provider_id,provider_key:x.provider_key,supplier_name:x.supplier_name,product_ref:x.product_ref,quantity:x.quantity,unit_cost:x.unit_cost,unit_price:x.unit_price,description:x.description,metadata:x.metadata||{}})});await quoteDetail(id)}catch(err){alert(err.message)}}); document.querySelectorAll('.qiDelete').forEach(b=>b.onclick=async()=>{if(!confirm('Remover este item da cotação?'))return;try{await api(withCid('/api/quote-items/'+b.dataset.id),{method:'DELETE'});await quoteDetail(id)}catch(err){alert(err.message)}}); $('#quoteSend').onclick=async()=>{if(!q.conversation_id)return;if(!items.length)return alert('Adicione pelo menos um item antes de enviar.');if(!confirm(`Enviar a ${q.contact_name||'este contato'} o resumo desta cotação pelo WhatsApp?`))return;try{await api(`/api/conversations/${q.conversation_id}/send`,{method:'POST',body:JSON.stringify({company_id:cid(),text:quoteWhatsappText(q,items)})});await api('/api/quotes/'+id,{method:'PATCH',body:JSON.stringify({company_id:cid(),status:'sent'})});alert('Cotação enviada pelo WhatsApp.');await quoteDetail(id)}catch(err){alert(err.message)}}; if($('#quoteEmail'))$('#quoteEmail').onclick=async()=>{if(!q.contact_email)return alert('Viajante sem e-mail cadastrado.');const accountId=Number($('#qeMailAccount').value);if(!accountId)return alert('Selecione uma conta de e-mail.');try{await api('/api/email/send',{method:'POST',body:JSON.stringify({company_id:cid(),account_id:accountId,to:q.contact_email,subject:`${q.quote_number} — ${q.title}`,text_body:quoteWhatsappText(q,items).replace(/\*/g,''),html_body:quotePrintHtml(q,items)})});await api('/api/quotes/'+id,{method:'PATCH',body:JSON.stringify({company_id:cid(),status:'sent'})});alert('Cotação enviada por e-mail.');await quoteDetail(id)}catch(err){alert(err.message)}}; $('#quoteConvert').onclick=async()=>{if(q.status==='converted'||!items.length)return;if(!confirm('Converter esta cotação aprovada em solicitação e pedido/reserva?'))return;try{const r=await api(`/api/quotes/${id}/convert`,{method:'POST',body:JSON.stringify({company_id:cid()})});alert(`Conversão concluída: ${r.order_number||('Pedido #'+r.order_id)}`);await quoteDetail(id)}catch(err){alert(err.message)}}; } async function tasks(){const d=await api(withCid('/api/tasks')),canManage=['admin','superadmin'].includes(S.user?.role);$('#content').innerHTML=`

Nova tarefa

${d.tasks.map(t=>``).join('')}
TarefaPrazoStatusAção
${esc(t.title)}
${esc(t.description||'')}
${fmt(t.due_at)}${esc(t.status)}
${t.status==='pending'?``:''}${canManage?``:''}
`;$('#taskForm').onsubmit=async e=>{e.preventDefault();await api('/api/tasks',{method:'POST',body:JSON.stringify({company_id:cid(),title:$('#taskTitle').value,description:$('#taskDesc').value,due_at:$('#taskDue').value?$('#taskDue').value.replace('T',' ')+':00':null})});await tasks()};document.querySelectorAll('.taskDone').forEach(b=>b.onclick=async()=>{await api('/api/tasks/'+b.dataset.id,{method:'PATCH',body:JSON.stringify({company_id:cid(),status:'done'})});await tasks()});document.querySelectorAll('.taskDelete').forEach(b=>b.onclick=async()=>{const title=b.dataset.title||('Tarefa #'+b.dataset.id);if(!confirm(`Apagar a tarefa \"${title}\"?\n\nEsta ação remove somente a tarefa.`))return;try{await api('/api/tasks/'+b.dataset.id+'?company_id='+encodeURIComponent(cid()),{method:'DELETE'});await tasks()}catch(e){alert(e.message)}})} const PROVIDER_CATALOG=[ ['Wooba / Travellink','consolidator',['flights']],['RexturAdvance','consolidator',['flights']],['Flytour Consolidadora','consolidator',['flights']],['Mystifly','b2b_air',['flights']],['Duffel','ndc',['flights']],['PKFARE','b2b_air',['flights']],['TBO','b2b_air',['flights','hotels']],['Sabre','gds',['flights','hotels','cars']],['Travelport','gds',['flights','hotels','cars']],['Amadeus','gds',['flights','hotels','cars']], ['TRAVLR','other',['flights','hotels','activities']],['DIGITRIPS','other',['flights','hotels']],['Agoda','bedbank',['hotels']],['Booking.com','other',['hotels']],['Viator','activities',['activities']],['Musement','activities',['activities']],['TourRadar','tour_operator',['activities']],['DiscoverCars','cars',['cars']],['eSIMStory','esim',['esim']],['Fever','activities',['activities']],['Civitatis','activities',['activities']],['12Go','rail_bus_ferry',['rail','bus','ferry']],['Travelpayouts / Aviasales','other',['flights','hotels']],['Airalo','esim',['esim']],['CruiseDirect','cruise',['cruises']],['Priority Pass','other',[]],['Bounce','other',[]],['OpenTable','other',['restaurants']],['Expedia White Label','other',['hotels','flights']],['Omio White Label','rail_bus_ferry',['rail','bus']],['GetYourGuide','activities',['activities']],['Klook','activities',['activities']],['Tiqets','activities',['activities']],['Trip.com','other',['flights','hotels','rail']],['Welcome Pickups','transfer',['transfers']],['Mozio','transfer',['transfers']],['i’way','transfer',['transfers']],['Skyscanner','other',['flights']],['KAYAK Affiliate Network','other',['flights','hotels','cars']],['Momondo','other',['flights']],['Cheapflights','other',['flights']],['HotelsCombined','other',['hotels']],['Rentcars','cars',['cars']],['EconomyBookings','cars',['cars']],['Vio','other',['hotels']],['HotelPlanner','other',['hotels']],['Nuitée / LiteAPI','bedbank',['hotels']],['Priceline','other',['hotels','flights','cars']],['HTS / Hopper','other',['flights','hotels']],['Headout','activities',['activities']] ]; const PROVIDER_TYPES=[['airline','Companhia aérea'],['consolidator','Consolidadora aérea'],['gds','GDS'],['ndc','Agregador NDC'],['b2b_air','B2B passagens'],['miles','Fornecedor de milhas'],['bedbank','Bedbank / Hotel B2B'],['tour_operator','Operadora'],['activities','Passeios / Atividades'],['transfer','Transfer'],['cars','Carros'],['rail_bus_ferry','Trem / Ônibus / Ferry'],['cruise','Cruzeiros'],['insurance','Seguro viagem'],['esim','eSIM'],['other','Outro']]; const PROVIDER_PRODUCTS=[['flights','Voos'],['hotels','Hotéis'],['cars','Carros'],['transfers','Transfers'],['activities','Passeios'],['restaurants','Restaurantes'],['miles','Milhas'],['cruises','Cruzeiros'],['rail','Trem'],['bus','Ônibus'],['ferry','Ferry'],['insurance','Seguro'],['esim','eSIM']]; const providerTypeLabel=v=>PROVIDER_TYPES.find(x=>x[0]===v)?.[1]||v||'—'; const providerProductLabel=v=>PROVIDER_PRODUCTS.find(x=>x[0]===v)?.[1]||v; const providerTags=a=>(a||[]).map(v=>`${esc(providerProductLabel(v))}`).join(''); const providerCheck=(arr,prefix,selected=[])=>arr.map(([v,l])=>``).join(''); const providerBoolCheck=(arr,prefix,obj={})=>arr.map(([v,l])=>``).join(''); // AELFLOW_1_6_0_TRANSACTION_OPERATIONS_UI let TRAVEL_TX_CACHE=[]; const txProductLabel=k=>({flight:'Passagem',hotel:'Estadia',car:'Carro'}[k]||k||'—'); const txStatusLabel=s=>({checked:'Checking concluído',awaiting_payment:'Aguardando pagamento',booking:'Reservando',confirmed:'Confirmada',indeterminate:'Indeterminada',failed:'Falhou'}[s]||s||'—'); function txBadge(s){return `${esc(txStatusLabel(s))}`} function txSplitName(v){const a=String(v||'').trim().split(/\s+/).filter(Boolean);return {given_name:a.shift()||'',family_name:a.join(' ')||''}} async function transactions(){ const d=await api(withCid('/api/travel/transactions'));TRAVEL_TX_CACHE=d.transactions||[];const all=TRAVEL_TX_CACHE,ready=all.filter(x=>x.ready_for_booking),recovery=all.filter(x=>x.needs_reconciliation),confirmed=all.filter(x=>x.status==='confirmed'); $('#content').innerHTML=`
Transações${all.length}
Pagas · prontas para reservar${ready.length}
Exigem reconciliação${recovery.length}
Confirmadas${confirmed.length}
Reserva externa nunca é repetida automaticamente.
IDProdutoFornecedorPedido / clientePagamentoStatusReferênciaAção
`; const render=()=>{const sf=$('#txStatusFilter').value,pf=$('#txProductFilter').value;let rows=all.filter(x=>!pf||x.product_type===pf);if(sf==='ready')rows=rows.filter(x=>x.ready_for_booking);else if(sf==='recovery')rows=rows.filter(x=>x.needs_reconciliation);else if(sf)rows=rows.filter(x=>x.status===sf);$('#txRows').innerHTML=rows.map(x=>`#${x.id}
${esc(x.created_at||'')}
${esc(txProductLabel(x.product_type))}${esc(x.provider_key)}
${esc(x.provider_source||'')}
${esc(x.order_number||'Sem pedido')}
${esc(x.contact_name||'')}
${esc(x.payment_status||'—')}${txBadge(x.status)}${x.last_error?`
${esc(x.last_error)}
`:''}${esc(x.external_reference||x.external_booking_id||'—')}`).join('')||'Nenhuma transação neste filtro.';document.querySelectorAll('.txOperate').forEach(b=>b.onclick=()=>travelTxOperate(Number(b.dataset.id)))};$('#txStatusFilter').onchange=render;$('#txProductFilter').onchange=render;$('#txRefresh').onclick=transactions;render(); } function travelTxOperate(id){const tx=TRAVEL_TX_CACHE.find(x=>Number(x.id)===Number(id));if(!tx)return;const overlay=document.createElement('div');overlay.className='tx-operation-overlay';const parts=txSplitName(tx.contact_name),canBook=tx.ready_for_booking===true,needsRecovery=tx.needs_reconciliation===true;let operation=''; if(canBook){if(tx.provider_key==='duffel'&&tx.product_type==='hotel')operation=`

Reserva de estadia · pagamento confirmado

`;else if(tx.provider_key==='duffel'&&tx.product_type==='car')operation=`

Reserva de carro · pagamento confirmado

`;else operation=`

Reserva transacional · pagamento confirmado

`} else if(tx.internal_order_id&&tx.payment_status!=='paid')operation=`
A reserva externa está bloqueada até o pedido ficar com pagamento paid.
`; if(needsRecovery)operation+=`

Reconciliação obrigatória

Antes de qualquer nova tentativa, confira no painel/API do fornecedor se a reserva existe.

`; overlay.innerHTML=`

Transação #${tx.id}

${esc(txProductLabel(tx.product_type))} · ${esc(tx.provider_key)} · ${esc(tx.order_number||'sem pedido')}
Status
${txBadge(tx.status)}
Pagamento
${esc(tx.payment_status||'—')}
Custo fornecedor
${tx.currency?Number(tx.supplier_amount||0).toLocaleString('pt-BR',{style:'currency',currency:tx.currency}):esc(tx.supplier_amount||'—')}
Reserva externa
${esc(tx.external_reference||tx.external_booking_id||'—')}
${operation||'
Nenhuma ação operacional disponível para este estado.
'}
`;document.body.appendChild(overlay);$('#txClose').onclick=()=>overlay.remove();overlay.onclick=e=>{if(e.target===overlay)overlay.remove()};if($('#txGoPayments'))$('#txGoPayments').onclick=async()=>{overlay.remove();await go('payments')}; if($('#txBookNow'))$('#txBookNow').onclick=async()=>{if(!confirm('Confirmar reserva externa agora? Esta ação pode gerar cobrança no fornecedor e não será repetida automaticamente.'))return;let booking_data={};try{if(tx.provider_key==='duffel'&&tx.product_type==='hotel'){const guests=$('#txHotelGuests').value.split(/\n+/).map(v=>v.trim()).filter(Boolean).map(v=>{const [name,born]=v.split('|').map(x=>x.trim()),n=txSplitName(name);return {...n,...(born?{born_on:born}:{})}});booking_data={email:$('#txHotelEmail').value.trim(),phone_number:$('#txHotelPhone').value.trim(),guests,accommodation_special_requests:$('#txHotelSpecial').value.trim()||undefined}}else if(tx.provider_key==='duffel'&&tx.product_type==='car'){booking_data={driver:{given_name:$('#txDriverGiven').value.trim(),family_name:$('#txDriverFamily').value.trim(),date_of_birth:$('#txDriverDob').value||undefined,email:$('#txDriverEmail').value.trim(),phone_number:$('#txDriverPhone').value.trim()},inbound_flight_number:$('#txInboundFlight').value.trim()||undefined}}else booking_data=JSON.parse($('#txGenericBookingData').value||'{}');const out=await api(`/api/travel/transactions/${tx.id}/book`,{method:'POST',headers:{'Idempotency-Key':globalThis.crypto?.randomUUID?.()||`${Date.now()}-${Math.random()}`},body:JSON.stringify({company_id:cid(),confirm:true,booking_data})});alert(`Reserva confirmada${out.external_reference?' · '+out.external_reference:''}`);overlay.remove();await transactions()}catch(e){alert(e.message)}}; const reconcile=async outcome=>{const found=outcome==='confirmed';if(!confirm(found?'Confirmar que a reserva foi encontrada no fornecedor?':'Confirmar que você verificou o fornecedor e NÃO existe reserva? Somente então uma nova tentativa será liberada.'))return;try{await api(`/api/travel/transactions/${tx.id}/reconcile`,{method:'POST',body:JSON.stringify({company_id:cid(),confirm:true,outcome,external_booking_id:$('#txReconBookingId')?.value.trim()||'',external_reference:$('#txReconReference')?.value.trim()||'',note:$('#txReconNote')?.value.trim()||''})});overlay.remove();await transactions()}catch(e){alert(e.message)}};if($('#txReconConfirmed'))$('#txReconConfirmed').onclick=()=>reconcile('confirmed');if($('#txReconNotFound'))$('#txReconNotFound').onclick=()=>reconcile('not_found'); } const PAYMENT_GATEWAYS=[['mercado_pago','Mercado Pago'],['stripe','Stripe'],['paypal','PayPal'],['rede','Rede'],['cielo','Cielo'],['zello','Zello'],['custom','Outro / Personalizado']]; const paymentGatewayLabel=k=>PAYMENT_GATEWAYS.find(x=>x[0]===k)?.[1]||k; const paymentGatewayOptions=(selected='')=>``+PAYMENT_GATEWAYS.map(([v,l])=>``).join(''); async function payments(){ const [pd,gd]=await Promise.all([api(withCid('/api/payment-settings')),api(withCid('/api/payment-gateways'))]); const ps=pd.settings||{},by=Object.fromEntries((gd.gateways||[]).map(x=>[x.gateway_key,x])); const gatewayCards=PAYMENT_GATEWAYS.map(([key,label])=>{ const g=by[key]||{},pub=g.public_config||{}; return `

${esc(label)}

${g.has_secret?'Credencial protegida cadastrada':'Credencial pendente'}
${g.status==='active'?'Ativo':'Inativo'}
`; }).join(''); $('#content').innerHTML=`

PIX

Configuração separada dos gateways da agência.
${ps.pix_enabled?'Ativo':'Inativo'}
${gatewayCards}
Os gateways são configurados por empresa. No fornecedor, o gateway é apenas associado à oferta. A forma de pagamento não elimina ofertas durante a busca.
`; $('#pixForm').onsubmit=async e=>{e.preventDefault();await api('/api/payment-settings',{method:'PUT',body:JSON.stringify({company_id:cid(),pix_enabled:$('#pixEnabled').checked,pix_holder:$('#pixHolder').value,pix_key:$('#pixKey').value.trim(),clear_pix_key:$('#pixClear').checked})});alert('Configuração PIX salva');await payments()}; document.querySelectorAll('[data-gateway-form]').forEach(form=>form.onsubmit=async e=>{e.preventDefault();const key=form.dataset.gatewayForm;await api('/api/payment-gateways/'+key,{method:'PUT',body:JSON.stringify({company_id:cid(),status:form.querySelector('[data-field="status"]').value,environment:form.querySelector('[data-field="environment"]').value,public_config:{public_id:form.querySelector('[data-field="public_id"]').value.trim()},secret:form.querySelector('[data-field="secret"]').value.trim(),clear_secret:form.querySelector('[data-field="clear"]').checked})});alert(paymentGatewayLabel(key)+' salvo');await payments()}); } // CLIENTES_V2_7 - cadastro de clientes e botoes de acesso aos links de fornecedor. // FORNECEDORES_CONSOLIDADOS - catálogo-base + gestão exclusiva do Super Admin. // FORNECEDORES_MULTIEMPRESA - catálogo mestre + permissões por cliente; credencial da plataforma por padrão, com override próprio opcional. function openProviderUrlField(inputId){let v=document.getElementById(inputId)?.value.trim()||'';if(!v)return alert('Informe o link primeiro.');if(!/^https?:\/\//i.test(v))v='https://'+v;try{const u=new URL(v);if(!['http:','https:'].includes(u.protocol))throw new Error();window.open(u.href,'_blank','noopener,noreferrer')}catch{return alert('Link inválido. Use um endereço http:// ou https://')}} function providerUrlInput(id,value,placeholder,buttonLabel='Acessar'){return `
`} function bindProviderUrlButtons(){document.querySelectorAll('.providerUrlOpen').forEach(b=>b.onclick=()=>openProviderUrlField(b.dataset.input))} function providerLinks(x){const page=x.website?`Abrir página`:'';const reg=(x.registration_url||x.publication?.registration_url)?`Abrir cadastro`:'';return page||reg?``:'Links não informados'} function providerOwnCreateHtml(canManage=true){return `

Cadastrar meu fornecedor

Fornecedor privado da empresa. Não será compartilhado com outros clientes.
${canManage?`
${providerUrlInput('opWebsite','','https://...','Acessar site')}
${providerUrlInput('opRegistration','','https://...','Abrir cadastro')}
Produtos
${providerCheck(PROVIDER_PRODUCTS,'ownproduct')}
`:'
Somente administradores podem cadastrar ou editar fornecedores próprios.
'}
`} function platformMasterCreateHtml(){return `
+Cadastrar consolidadora / fornecedor— somente Super Admin
${providerUrlInput('mpWebsite','','https://...','Acessar site')}
${providerUrlInput('mpRegistration','','https://...','Abrir cadastro')}
Produtos
${providerCheck(PROVIDER_PRODUCTS,'masterproduct')}
O Super Admin cadastra a consolidadora, configura a credencial da plataforma uma única vez e libera seu uso. Empresas liberadas herdam essa credencial automaticamente; uma empresa pode usar credencial própria como override.
A credencial mestre é protegida e nunca é exibida aos clientes. Credenciais próprias continuam isoladas por empresa.
`} function ownProviderRows(rows,canManage){return rows.map(x=>`${esc(x.name)}
PRÓPRIO ${esc(x.slug||'')}
${esc(providerTypeLabel(x.provider_type))}
${providerTags(x.products)}
${providerLinks(x)}${esc(x.status)}
${canManage?``:''}
`).join('')||'Nenhum fornecedor próprio cadastrado.'} function platformClientRows(rows,canOperate=false){return rows.map(x=>{const ci=x.company_integration||{},configured=!!ci.has_secret,tested=ci.last_test_status==='ok',active=ci.status==='active'&&tested&&configured,source=ci.credential_source==='own'?'own':'platform',sourceLabel=source==='own'?'CREDENCIAL PRÓPRIA':'CREDENCIAL DA PLATAFORMA',productButton=providerNonFlightProducts(x.products).length?``:'';return `${esc(x.name)}
LIBERADO PELO SUPER ADMIN
${esc(providerTypeLabel(x.provider_type))}
${providerTags(x.products)}
${providerLinks(x)}${active?'API ATIVA':configured?`${tested?'CONFIGURADA':'AGUARDA TESTE'}`:'API NÃO CONFIGURADA'}
${esc(sourceLabel)} · ${esc((ci.environment||'test').toUpperCase())}
${canOperate&&x.integration_type!=='manual'?``:''}${canOperate?productButton:''}${canOperate&&x.slug==='duffel'&&active?``:''}
`}).join('')||'Nenhuma consolidadora foi liberada para esta empresa.'} function providerExternalDirectoryHtml(catalog=[]){ const rows=(Array.isArray(catalog)?catalog:[]).filter(x=>x&&x.name); return `

Empresas e APIs para busca, reserva e emissão

Diretório derivado do catálogo mestre atual. Links, produtos e tipo de integração vêm da mesma fonte usada para operar fornecedores.
${rows.map(x=>`
${esc(x.name)}
${esc(providerTypeLabel(x.provider_type))} · ${esc(x.integration_type||'manual')}
${providerTags(x.products)}
${providerLinks(x)}
`).join('')||'
Nenhum fornecedor cadastrado no catálogo mestre.
'}
`; } const PROVIDER_POLICY_LABELS={flight:'Passagens',hotel:'Estadias',car:'Carros',tour:'Passeios',transfer:'Traslados',restaurant:'Restaurantes'}; function providerPolicyCardHtml(policies=[]){const by=new Map((policies||[]).map(x=>[x.product_key,x]));return `

Política de escolha de fornecedor

Defina por produto quem escolhe o fornecedor. O AELFLOW continua consultando todos os fornecedores habilitados; Duffel é apenas um deles.
${Object.entries(PROVIDER_POLICY_LABELS).map(([k,label])=>{const p=by.get(k)||{};return `
${label}
Fornecedor: visível somente no painel interno
`}).join('')}
`} async function loadProviderPolicies(){try{return (await api(withCid('/api/travel/provider-policies'))).policies||[]}catch{return []}} function bindProviderPolicyActions(){document.querySelectorAll('[data-policy-save]').forEach(b=>b.onclick=async()=>{const k=b.dataset.policySave;try{await api('/api/travel/provider-policies',{method:'PUT',body:JSON.stringify({company_id:cid(),product_key:k,selection_mode:document.querySelector(`[data-policy-mode="${k}"]`)?.value,sort_strategy:document.querySelector(`[data-policy-sort="${k}"]`)?.value,show_provider_public:false,manual_fallback:!!document.querySelector(`[data-policy-manual="${k}"]`)?.checked})});b.textContent='Salvo ✓';setTimeout(()=>b.textContent=`Salvar ${PROVIDER_POLICY_LABELS[k]}`,1200)}catch(err){alert(err.message)}})} async function providers(){ const superView=S.user.role==='superadmin',canOperate=['superadmin','admin'].includes(S.user.role),company=S.companies.find(x=>Number(x.id)===Number(cid())),providerPolicies=await loadProviderPolicies(); if(superView){ const [catData,packData]=await Promise.all([api(withCid('/api/travel/provider-catalog')),api(withCid('/api/travel/provider-packages'))]); const catalog=catData.providers||[],packs=packData.packages||[],granted=catalog.filter(x=>x.granted).length; const catalogRows=catalog.map(x=>{const globalConfigured=!!x.has_secret,globalHealthy=x.integration_last_test_status==='ok',apiBadge=x.integration_type==='manual'?'MANUAL':globalConfigured?`API PLATAFORMA ${globalHealthy?'ATIVA':'CONFIGURADA'}
${esc((x.integration_environment||'test').toUpperCase())}
`:'API PLATAFORMA PENDENTE';return `${esc(x.name)}
${esc(x.slug)}
${esc(providerTypeLabel(x.provider_type))}
${providerTags(x.products)}
${providerLinks(x)}${apiBadge}${x.granted?'LIBERADO':'NÃO LIBERADO'}${x.package_grants?` PACOTE ${Number(x.package_grants)}`:''}
`}).join('')||'Catálogo mestre vazio.'; const packageRows=packs.map(x=>`
${esc(x.name)}
${x.providers.length} fornecedor(es)${x.description?' · '+esc(x.description):''}
${x.assigned?'APLICADO':'NÃO APLICADO'}
${x.providers.map(p=>`${esc(p.name)}`).join('')}
`).join('')||'
Nenhum pacote criado.
'; $('#content').innerHTML=`${providerPolicyCardHtml(providerPolicies)}${providerExternalDirectoryHtml(catalog)}${platformMasterCreateHtml()}

Consolidadoras / fornecedores da plataforma

Cadastro, credencial da plataforma e liberação são exclusivos do Super Admin; clientes liberados herdam a API automaticamente e podem usar override próprio. Cliente selecionado: ${esc(company?.name||'Empresa')} · ${catalog.length} cadastrado(s) · ${granted} liberado(s).
${catalogRows}
FornecedorTipoProdutosLinksIntegraçãoAcessoAções

Novo pacote de fornecedores

Crie pacotes do catálogo mestre e aplique-os aos clientes.
${catalog.filter(x=>x.status==='active').map(x=>``).join('')||'Cadastre fornecedores no catálogo mestre primeiro.'}

Pacotes cadastrados

${packageRows}
`; bindProviderPolicyActions();bindProviderUrlButtons(); const selected=kind=>[...document.querySelectorAll(`[data-${kind}]:checked`)].map(x=>Number(x.getAttribute(`data-${kind}`))); $('#masterProviderForm').onsubmit=async e=>{e.preventDefault();try{await api('/api/travel/provider-catalog',{method:'POST',body:JSON.stringify({name:$('#mpName').value,slug:$('#mpSlug').value,provider_type:$('#mpType').value,integration_type:$('#mpIntegration').value,website:$('#mpWebsite').value,registration_url:$('#mpRegistration').value,country_code:$('#mpCountry').value,currencies:$('#mpCurrencies').value.split(',').map(v=>v.trim().toUpperCase()).filter(Boolean),products:selected('masterproduct'),priority:Number($('#mpPriority').value||100),notes:$('#mpNotes').value})});await providers()}catch(err){alert(err.message)}}; document.querySelectorAll('.superProviderManage').forEach(b=>b.onclick=()=>superProviderManage(Number(b.dataset.id))); document.querySelectorAll('.masterProviderEdit').forEach(b=>b.onclick=()=>platformProviderEdit(Number(b.dataset.id))); document.querySelectorAll('.platformProviderConfigure').forEach(b=>b.onclick=()=>platformProviderIntegration(b.dataset.key));document.querySelectorAll('.providerProductsOpen').forEach(b=>b.onclick=()=>{let products=[];try{products=JSON.parse(b.dataset.products||'[]')}catch{}providerProductsManage(Number(b.dataset.id),b.dataset.name||'Fornecedor',products)}); document.querySelectorAll('.duffelOperationsOpen').forEach(b=>b.onclick=()=>duffelOperations()); document.querySelectorAll('.providerGrantToggle').forEach(b=>b.onclick=async()=>{try{await api('/api/travel/provider-access/'+b.dataset.id,{method:'PATCH',body:JSON.stringify({company_id:cid(),enabled:b.dataset.enabled!=='1'})});await providers()}catch(err){alert(err.message)}}); $('#grantAllProviders').onclick=async()=>{if(confirm('Liberar todos os fornecedores ativos do catálogo mestre para este cliente?')){await api('/api/travel/provider-access',{method:'PATCH',body:JSON.stringify({company_id:cid(),enabled:true})});await providers()}}; $('#removeManualGrants').onclick=async()=>{if(confirm('Remover somente as liberações manuais? Fornecedores de pacotes continuarão liberados.')){await api('/api/travel/provider-access',{method:'PATCH',body:JSON.stringify({company_id:cid(),enabled:false})});await providers()}}; $('#packageForm').onsubmit=async e=>{e.preventDefault();const ids=selected('pkg-provider');if(!ids.length)return alert('Selecione pelo menos um fornecedor.');try{await api('/api/travel/provider-packages',{method:'POST',body:JSON.stringify({name:$('#pkgName').value,description:$('#pkgDescription').value,provider_ids:ids})});await providers()}catch(err){alert(err.message)}}; document.querySelectorAll('.packageEdit').forEach(b=>b.onclick=()=>providerPackageEdit(Number(b.dataset.id))); document.querySelectorAll('.packageApply').forEach(b=>b.onclick=async()=>{const assigned=b.dataset.assigned==='1',id=Number(b.dataset.id);if(!confirm(assigned?'Remover este pacote do cliente? Liberações manuais serão preservadas.':'Aplicar este pacote ao cliente?'))return;try{await api(`/api/travel/provider-packages/${id}/${assigned?'remove':'apply'}`,{method:'POST',body:JSON.stringify({company_id:cid()})});await providers()}catch(err){alert(err.message)}}); return; } const d=await api(withCid('/api/travel/providers')),rows=d.providers||[]; const tableRows=rows.map(x=>`${esc(x.name)}${esc(providerTypeLabel(x.provider_type))}
${providerTags(x.products||[])}
LIBERADO`).join('')||'Nenhum fornecedor foi liberado pelo Super Admin para esta empresa.'; $('#content').innerHTML=`

Fornecedores da minha empresa

Lista definida pelo Super Admin. Tokens, APIs, credenciais, ambiente, cadastro, edição e configuração técnica não ficam disponíveis no painel do cliente.
${rows.length}
${tableRows}
FornecedorTipoProdutos liberadosStatus
`; } async function superProviderManage(providerId){ if(S.user.role!=='superadmin')return alert('Somente o Super Admin pode gerenciar fornecedores.'); const d=await api(withCid('/api/travel/provider-catalog')),x=(d.providers||[]).find(p=>Number(p.id)===Number(providerId)); if(!x){$('#content').innerHTML=notify('Fornecedor não encontrado.','error');return} const company=S.companies.find(c=>Number(c.id)===Number(cid())); const integration=x.integration_type==='manual'?'Manual':(x.has_secret?'API configurada':'API pendente'); $('#content').innerHTML=`

${esc(x.name)}

Fornecedor selecionado para ${esc(company?.name||'cliente selecionado')}. Esta página mostra somente este fornecedor.
${x.granted?'LIBERADO PARA O CLIENTE':'NÃO LIBERADO'}
Tipo${esc(providerTypeLabel(x.provider_type))}
Integração${esc(integration)}
Produtos
${providerTags(x.products||[])}
Configuração deste fornecedor
${x.integration_type!=='manual'?'':''}${x.slug==='duffel'&&x.granted?'':''}
IA, tokens, APIs e credenciais são configurados somente pelo Super Admin e permanecem vinculados ao cliente selecionado.
`; $('#spmBack').onclick=providers; $('#spmEdit').onclick=()=>platformProviderEdit(Number(x.id)); const master=$('#spmPlatformApi');if(master)master.onclick=()=>platformProviderIntegration(x.slug); const client=$('#spmClientConfig');if(client)client.onclick=()=>companyProviderIntegration(x.slug); $('#spmProducts').onclick=()=>providerProductsManage(Number(x.id),x.name,x.products||[]); $('#spmGrant').onclick=async()=>{try{await api('/api/travel/provider-access/'+x.id,{method:'PATCH',body:JSON.stringify({company_id:cid(),enabled:!x.manual_granted})});await superProviderManage(x.id)}catch(err){alert(err.message)}}; const op=$('#spmOperate');if(op)op.onclick=()=>duffelOperations(); } function bindOwnProviderActions(){document.querySelectorAll('.ownProviderEdit').forEach(b=>b.onclick=()=>providerEdit(Number(b.dataset.id),false));document.querySelectorAll('.ownProviderIntegration').forEach(b=>b.onclick=()=>providerEdit(Number(b.dataset.id),true));} const PROVIDER_PRODUCT_UI_LABELS={flights:'Voos',hotels:'Hotéis',cars:'Carros',activities:'Passeios / atividades',transfers:'Traslados',restaurants:'Restaurantes',miles:'Milhas',cruises:'Cruzeiros',rail:'Trem',bus:'Ônibus',ferry:'Ferry',insurance:'Seguro',esim:'eSIM'}; const providerProductUiLabel=k=>PROVIDER_PRODUCT_UI_LABELS[k]||k; function providerProductConfigHtml(products,config,prefix,includeFlights=false){const pcs=config?.product_configs||{};return (products||[]).filter(k=>includeFlights||k!=='flights').map(k=>{const x=pcs[k]||{},fm=x.field_map||{},pm=x.param_map||{},id=`${prefix}_${k}`;return `
${esc(providerProductUiLabel(k))}
Somente configurações deste produto. O fornecedor participa da busca de ${esc(providerProductUiLabel(k))} apenas se este produto estiver liberado.
Checking / Reserva transacional
Opcional. Ative somente quando este fornecedor possuir endpoints documentados de revalidação e reserva.
`}).join('')} function readProviderProductConfigs(products,prefix,includeFlights=false){const out={};for(const k of (products||[]).filter(v=>includeFlights||v!=='flights')){const id=`${prefix}_${k}`,get=s=>document.getElementById(id+s),json=()=>{try{return JSON.parse(get('_testquery')?.value||'{}')}catch{throw new Error(`JSON de teste inválido em ${providerProductUiLabel(k)}`)}};out[k]={enabled:!!get('_enabled')?.checked,mode:get('_mode')?.value||'manual',method:get('_method')?.value||'GET',auth_type:get('_auth')?.value||'bearer',auth_name:get('_authname')?.value.trim()||'',test_url:get('_testurl')?.value.trim()||'',live_url:get('_liveurl')?.value.trim()||'',health_url:get('_healthurl')?.value.trim()||'',result_path:get('_resultpath')?.value.trim()||'',field_map:{name:get('_map_name')?.value.trim()||'',price:get('_map_price')?.value.trim()||'',currency:get('_map_currency')?.value.trim()||'',image_url:get('_map_image')?.value.trim()||'',location:get('_map_location')?.value.trim()||'',deeplink_url:get('_map_link')?.value.trim()||''},param_map:{origin:get('_param_origin')?.value.trim()||'',destination:get('_param_destination')?.value.trim()||''},transactional_enabled:!!get('_txenabled')?.checked,check_url:get('_checkurl')?.value.trim()||'',check_method:get('_checkmethod')?.value||'POST',check_result_path:get('_checkresult')?.value.trim()||'',book_url:get('_bookurl')?.value.trim()||'',book_method:get('_bookmethod')?.value||'POST',book_result_path:get('_bookresult')?.value.trim()||'',transaction_map:{quote_id:get('_tx_quote')?.value.trim()||'',booking_id:get('_tx_booking')?.value.trim()||'',booking_reference:get('_tx_reference')?.value.trim()||'',total_amount:get('_tx_total')?.value.trim()||'',currency:get('_tx_currency')?.value.trim()||'',status:get('_tx_status')?.value.trim()||''},transaction_param_map:{rate_id:get('_tx_rateparam')?.value.trim()||'',quote_id:get('_tx_quoteparam')?.value.trim()||''},test_query:json(),timeout_ms:Number(get('_timeout')?.value||12000)}}return out} function providerNonFlightProducts(products){return (products||[]).filter(x=>x!=='flights')} async function providerProductsManage(providerId,providerName,products){ const pid=Number(providerId||0),allowed=providerNonFlightProducts(products);if(!pid||!allowed.length)return alert('Este fornecedor não possui produtos gerenciáveis nesta tela.'); try{ const d=await api(withCid(`/api/travel/provider-products?provider_id=${pid}`)),rows=d.products||[],isSuper=S.user.role==='superadmin'; const productOpts=allowed.map(k=>``).join(''); const cards=rows.map(x=>`
${esc(x.name)}
${esc(providerProductUiLabel(x.product_type))}${x.location?' · '+esc(x.location):''}${x.company_id===null?' · GLOBAL':' · EMPRESA'}
${esc(x.status)}
Venda: ${x.sale_price===null||x.sale_price===undefined?'—':money(x.sale_price,x.currency||'BRL')}${x.rating!==null&&x.rating!==undefined?' · Nota '+esc(String(x.rating)):''}
${x.deeplink_url?`Abrir produto`:''}
`).join('')||'
Nenhum produto manual cadastrado para este fornecedor. Produtos vindos por API não precisam ser gravados aqui.
'; $('#content').innerHTML=`

${esc(providerName)} · produtos / inventário

Cadastre produtos manuais quando o fornecedor não tiver API. O portal mostrará produtos, nunca o fornecedor como resultado.
${rows.length}
${isSuper?'':''}

Produtos cadastrados

${cards}
`; $('#providerProductsBack').onclick=providers; $('#providerProductForm').onsubmit=async e=>{ e.preventDefault(); try{ await api('/api/travel/provider-products',{method:'POST',body:JSON.stringify({company_id:cid(),global:isSuper&&!!$('#ppGlobal')?.checked,platform_provider_id:pid,product_type:$('#ppProductType').value,name:$('#ppName').value,location:$('#ppLocation').value,sale_price:$('#ppSale').value||null,supplier_price:$('#ppSupplier').value||null,currency:$('#ppCurrency').value||'BRL',rating:$('#ppRating').value||null,status:$('#ppStatus').value,image_url:$('#ppImage').value,deeplink_url:$('#ppLink').value,description:$('#ppDescription').value})}); await providerProductsManage(pid,providerName,products); }catch(err){alert(err.message)} }; document.querySelectorAll('.providerProductDelete').forEach(b=>b.onclick=async()=>{if(!confirm('Excluir este produto do inventário?'))return;try{await api(withCid('/api/travel/provider-products/'+b.dataset.id),{method:'DELETE'});await providerProductsManage(pid,providerName,products)}catch(err){alert(err.message)}}); }catch(err){alert(err.message);await providers()} } const duffelPct=v=>(Math.max(0,Number(v||0))*100).toFixed(2).replace(/\.00$/,''); const duffelMoneyFmt=(amount,currency)=>{const n=Number(amount);if(!Number.isFinite(n))return '—';try{return new Intl.NumberFormat('pt-BR',{style:'currency',currency:currency||'BRL'}).format(n)}catch{return `${currency||''} ${n.toFixed(2)}`}}; async function platformProviderIntegration(providerKey){ const key=String(providerKey||'').trim().toLowerCase();if(!key)return;if(S.user.role!=='superadmin')return alert('Somente o Super Admin configura a API da plataforma.'); try{ const d=await api('/api/travel/platform-provider-integrations/'+encodeURIComponent(key)),x=d.integration||{},c=x.config||{},products=Array.isArray(x.products)?x.products:[],isDuffel=key==='duffel',fm=c.fare_markup||{},am=c.ancillary_markup||{}; const lastTest=x.last_test_at?`${esc(String(x.last_test_at))} · ${esc(x.last_test_message||'')}`:'Ainda não testada',previousEnv=x.environment==='live'?'live':'test'; const testState=x.has_test_secret?'CONFIGURADA':'PENDENTE',liveState=x.has_live_secret?'CONFIGURADA':'PENDENTE'; $('#content').innerHTML=`

${esc(x.provider_name||key)} · API da plataforma

Configure a API por ambiente e somente para os produtos realmente oferecidos por este fornecedor. As chaves de TESTE e PRODUÇÃO ficam separadas e criptografadas.
${x.status==='active'&&x.last_test_status==='ok'?'API ATIVA':x.has_secret?'CONFIGURADA':'PENDENTE'}
Credencial de TESTE · ${testState}
Credencial de PRODUÇÃO · ${liveState}
A troca Teste ⇄ Produção não apaga a outra credencial. A chave nunca é devolvida ao navegador.
${isDuffel?`
Configuração de Voos
Markup de Voos
`:''} ${providerProductConfigHtml(products,c,'ppiProduct',!isDuffel)}
Último teste efetivo: ${lastTest}
`; $('#platformProviderBack').onclick=providers; const payload=()=>{const baseConfig={...c,product_configs:readProviderProductConfigs(products,'ppiProduct',!isDuffel)},base={status:$('#ppiStatus').value,environment:$('#ppiEnvironment').value,secret_test:$('#ppiSecretTest').value.trim(),secret_live:$('#ppiSecretLive').value.trim(),clear_test_secret:$('#ppiClearTest').checked,clear_live_secret:$('#ppiClearLive').checked};if(!isDuffel)return {...base,config:baseConfig};const pct=id=>Math.max(0,Number($(id).value||0))/100;return {...base,config:{...baseConfig,public_search_enabled:$('#ppiPublicSearch').checked,services:{bags:$('#ppiBags').checked,seats:$('#ppiSeats').checked},fare_markup:{rate:pct('#ppiFarePct'),amount:Number($('#ppiFareFixed').value||0)},ancillary_markup:{bags:{rate:pct('#ppiBagPct'),amount:Number($('#ppiBagFixed').value||0)},seats:{rate:pct('#ppiSeatPct'),amount:Number($('#ppiSeatFixed').value||0)}},auto_issue_after_paid:$('#ppiAutoIssue').checked,checkout_mode:$('#ppiCheckout').value,max_offers:Number($('#ppiMaxOffers').value||30),supplier_timeout_ms:Number($('#ppiTimeout').value||15)*1000,max_connections:Number($('#ppiConnections').value||0)}}}; const selectedHasSecret=p=>p.environment==='live'?(Boolean(p.secret_live)||(x.has_live_secret&&!p.clear_live_secret)):(Boolean(p.secret_test)||(x.has_test_secret&&!p.clear_test_secret)); const hasApiAdapter=p=>isDuffel||Object.values(p.config?.product_configs||{}).some(z=>z?.enabled&&z?.mode==='api'); const rollbackEnv=async()=>{if($('#ppiEnvironment').value===previousEnv)return;try{await api('/api/travel/platform-provider-integrations/'+encodeURIComponent(key),{method:'PUT',body:JSON.stringify({environment:previousEnv,status:x.status||'inactive',config:c,activate_verified:true})})}catch{}}; const saveAndTest=async(testNow)=>{const p=payload();await api('/api/travel/platform-provider-integrations/'+encodeURIComponent(key),{method:'PUT',body:JSON.stringify({...p,status:p.status==='active'?'testing':p.status})});if(testNow&&p.status!=='inactive'&&selectedHasSecret(p)&&hasApiAdapter(p)){try{return await api('/api/travel/platform-provider-integrations/'+encodeURIComponent(key)+'/test',{method:'POST',body:'{}'})}catch(e){await rollbackEnv();throw e}}return null}; $('#platformProviderForm').onsubmit=async e=>{e.preventDefault();try{const p=payload(),r=await saveAndTest(true);alert(r?.message||'Configuração salva. Produtos, ambiente e credenciais foram sincronizados.');await platformProviderIntegration(key)}catch(err){alert('Configuração não ativada: '+err.message);await platformProviderIntegration(key)}}; $('#ppiTest').onclick=async()=>{const btn=$('#ppiTest');btn.disabled=true;btn.textContent='Testando...';try{const p=payload();if(!selectedHasSecret(p))throw new Error('Cadastre a credencial do ambiente selecionado.');if(!hasApiAdapter(p))throw new Error('Nenhum produto está configurado no modo API.');const r=await saveAndTest(true);alert(r?.message||'Conexão confirmada.');await platformProviderIntegration(key)}catch(err){alert('Teste falhou: '+err.message);btn.disabled=false;btn.textContent='Testar ambiente selecionado'}}; }catch(err){$('#content').innerHTML=`${notify('Não foi possível carregar a API da plataforma: '+err.message,'error')}`;$('#platformProviderBack').onclick=providers} } async function companyProviderIntegration(providerKey){ const key=String(providerKey||'').trim().toLowerCase();if(!key)return;if(S.user.role!=='superadmin')return alert('Somente o Super Admin pode configurar fornecedores.'); try{ const d=await api(withCid('/api/travel/provider-integrations/'+encodeURIComponent(key))),x=d.integration||{},c=x.config||{},products=Array.isArray(x.products)?x.products:[],isDuffel=key==='duffel',fm=c.fare_markup||{},am=c.ancillary_markup||{},source=x.credential_source==='own'?'own':'platform',previousEnv=x.environment==='live'?'live':'test'; const lastTest=x.last_test_at?`${esc(String(x.last_test_at))} · ${esc(x.last_test_message||'')}`:'Ainda não testada',configured=!!x.has_secret,healthy=x.last_test_status==='ok',stateLabel=!configured?'API NÃO CONFIGURADA':healthy?'API ATIVA':'API CONFIGURADA · validação pendente'; $('#content').innerHTML=`

${esc(x.provider_name||key)} · integração da empresa

As opções abaixo aparecem somente para os produtos liberados neste fornecedor. O Super Admin configura este fornecedor exclusivamente para o cliente selecionado, incluindo produtos, credenciais e ambiente.
${esc(stateLabel)}
Plataforma: TESTE ${x.platform_has_test_secret?'OK':'PENDENTE'} · PRODUÇÃO ${x.platform_has_live_secret?'OK':'PENDENTE'}. Credencial própria: TESTE ${x.company_has_test_secret?'OK':'PENDENTE'} · PRODUÇÃO ${x.company_has_live_secret?'OK':'PENDENTE'}.
Credencial própria de TESTE
Credencial própria de PRODUÇÃO
${isDuffel?`
Busca e serviços de Voos desta empresa
Markup de Voos da empresa
`:''} ${providerProductConfigHtml(products,c,'cpiProduct',!isDuffel)}
Último teste efetivo: ${lastTest}
`; $('#providerIntegrationBack').onclick=providers; const syncSourceUi=()=>{const own=$('#cpiSource').value==='own';$('#cpiEnvironment').disabled=!own;['#cpiSecretTest','#cpiSecretLive','#cpiClearTest','#cpiClearLive'].forEach(id=>{const el=$(id);if(el)el.disabled=!own});$('#cpiOwnSecrets').style.opacity=own?'1':'.55'};$('#cpiSource').onchange=syncSourceUi;syncSourceUi(); const payload=()=>{const own=$('#cpiSource').value==='own',baseConfig={...c,product_configs:readProviderProductConfigs(products,'cpiProduct',!isDuffel)},base={company_id:cid(),credential_source:own?'own':'platform',environment:$('#cpiEnvironment').value,secret_test:own?$('#cpiSecretTest').value.trim():'',secret_live:own?$('#cpiSecretLive').value.trim():'',clear_test_secret:own&&$('#cpiClearTest').checked,clear_live_secret:own&&$('#cpiClearLive').checked};if(!isDuffel)return {...base,config:baseConfig};const pct=id=>Math.max(0,Number($(id).value||0))/100;return {...base,config:{...baseConfig,public_search_enabled:$('#cpiPublicSearch').checked,services:{bags:$('#cpiBags').checked,seats:$('#cpiSeats').checked},fare_markup:{rate:pct('#cpiFarePct'),amount:Number($('#cpiFareFixed').value||0)},ancillary_markup:am,auto_issue_after_paid:$('#cpiAutoIssue').checked,checkout_mode:$('#cpiCheckout').value,max_offers:Number($('#cpiMaxOffers').value||30),supplier_timeout_ms:Number($('#cpiTimeout').value||15)*1000,max_connections:Number(c.max_connections||1)}}}; const ownSelectedHasSecret=p=>p.environment==='live'?(Boolean(p.secret_live)||(x.company_has_live_secret&&!p.clear_live_secret)):(Boolean(p.secret_test)||(x.company_has_test_secret&&!p.clear_test_secret)); const hasEffectiveSecret=p=>p.credential_source==='platform'?(p.environment==='live'?x.platform_has_live_secret:x.platform_has_test_secret):ownSelectedHasSecret(p); const hasApiAdapter=p=>isDuffel||Object.values(p.config?.product_configs||{}).some(z=>z?.enabled&&z?.mode==='api'); const rollbackEnv=async()=>{if(source!=='own'||$('#cpiEnvironment').value===previousEnv)return;try{await api('/api/travel/provider-integrations/'+encodeURIComponent(key),{method:'PUT',body:JSON.stringify({company_id:cid(),credential_source:'own',environment:previousEnv,status:x.status||'inactive',config:c,activate_verified:true})})}catch{}}; const saveAndMaybeTest=async(testNow)=>{const p=payload();await api('/api/travel/provider-integrations/'+encodeURIComponent(key),{method:'PUT',body:JSON.stringify(p)});if(testNow&&hasEffectiveSecret(p)&&hasApiAdapter(p)){try{return await api('/api/travel/provider-integrations/'+encodeURIComponent(key)+'/test',{method:'POST',body:JSON.stringify({company_id:cid()})})}catch(e){await rollbackEnv();throw e}}return null}; $('#companyProviderForm').onsubmit=async e=>{e.preventDefault();try{const p=payload(),r=await saveAndMaybeTest(p.credential_source==='own');alert(r?.message||(p.credential_source==='platform'?'Configuração salva usando a credencial da plataforma.':'Configuração própria salva e validada quando aplicável.'));await companyProviderIntegration(key)}catch(err){alert('Configuração não ativada: '+err.message);await companyProviderIntegration(key)}}; $('#cpiTest').onclick=async()=>{const btn=$('#cpiTest');btn.disabled=true;btn.textContent='Testando...';try{const p=payload();if(!hasEffectiveSecret(p))throw new Error('Não há credencial para o ambiente selecionado.');if(!hasApiAdapter(p))throw new Error('Nenhum produto está configurado no modo API.');const r=await saveAndMaybeTest(true);alert(r?.message||'Acesso confirmado.');await companyProviderIntegration(key)}catch(err){alert('Teste falhou: '+err.message);btn.disabled=false;btn.textContent='Validar ambiente'}}; }catch(err){$('#content').innerHTML=`${notify('Não foi possível carregar a integração: '+err.message,'error')}`;$('#providerIntegrationBack').onclick=providers} } async function duffelOperations(){ try{ const od=await api(withCid('/api/travel/duffel/orders')),orders=od.orders||[]; const rows=orders.map(o=>`${esc(o.order_number||('#'+o.internal_order_id))}
#${o.internal_order_id}
${esc(o.payment_status||'—')}
${esc(o.internal_status||'')}
${duffelMoneyFmt(o.sell_total,o.sell_currency)}${o.external_order_id?`${esc(o.locator||'Sem localizador')}
${esc(o.external_order_id)}
`:`Ainda não emitida
${esc(o.quote_status||'')}
`}${o.supplier_amount!==null&&o.supplier_amount!==undefined?duffelMoneyFmt(o.supplier_amount,o.supplier_currency):'—'}
${o.payment_status==='paid'&&!o.external_order_id?``:''}${o.external_order_id?`${o.internal_status==='awaiting_review_supplier'&&o.provider_status!=='cancelled'?``:''}${o.provider_status!=='cancelled'?``:''}`:''}
`).join('')||'Ainda não existem pedidos vinculados à Duffel para esta empresa.'; $('#content').innerHTML=`

Duffel · operação da empresa

A consolidadora foi liberada pelo Super Admin. A operação usa a credencial efetiva desta empresa (plataforma por padrão ou credencial própria quando configurada). Aqui ficam emissão, sincronização, conferência e pós-venda.
${orders.length} pedido(s)
${rows}
PedidoPagamento / StatusVendaDuffel / PNRCusto fornecedorAções
`; $('#duffelBack').onclick=providers; document.querySelectorAll('.duffelIssue').forEach(b=>b.onclick=async()=>{if(!confirm('Emitir este pedido na Duffel agora? O pagamento deve estar confirmado.'))return;try{b.disabled=true;const r=await api(`/api/travel/duffel/orders/${b.dataset.id}/issue`,{method:'POST',body:JSON.stringify({company_id:cid()})});alert(`Emissão criada. ${r.locator?'Localizador: '+r.locator:'Aguardando conferência.'}`);await duffelOperations()}catch(err){alert(err.message);b.disabled=false}}); document.querySelectorAll('.duffelSync').forEach(b=>b.onclick=async()=>{try{b.disabled=true;await api(`/api/travel/duffel/orders/${b.dataset.id}/sync`,{method:'POST',body:JSON.stringify({company_id:cid()})});await duffelOperations()}catch(err){alert(err.message);b.disabled=false}}); document.querySelectorAll('.duffelApproveRelease').forEach(b=>b.onclick=async()=>{if(!confirm('Conferir a emissão e liberar localizador/e-tickets ao viajante?'))return;try{b.disabled=true;const r=await api(`/api/travel/duffel/orders/${b.dataset.id}/approve-release`,{method:'POST',body:JSON.stringify({company_id:cid()})});alert(`Emissão conferida e liberada.${r.locator?' Localizador: '+r.locator:''}`);await duffelOperations()}catch(err){alert(err.message);b.disabled=false}}); document.querySelectorAll('.duffelCancelQuote').forEach(b=>b.onclick=async()=>{if(!confirm('Solicitar cotação de cancelamento? Isto NÃO cancela ainda.'))return;try{b.disabled=true;const r=await api(`/api/travel/duffel/orders/${b.dataset.id}/cancellation-quote`,{method:'POST',body:JSON.stringify({company_id:cid()})}),q=r.cancellation||{};$('#duffelActionBox').innerHTML=`
Cotação recebida — ainda NÃO cancelada

Reembolso estimado: ${duffelMoneyFmt(q.refund_amount,q.refund_currency)}

`;$('#duffelConfirmCancel').onclick=async()=>{const typed=prompt('Digite CANCELAR para confirmar:');if(typed!=='CANCELAR')return;try{await api(`/api/travel/duffel/orders/${b.dataset.id}/cancellation-confirm`,{method:'POST',body:JSON.stringify({company_id:cid(),cancellation_id:q.id,confirm:true})});alert('Cancelamento confirmado.');await duffelOperations()}catch(err){alert(err.message)}}}catch(err){alert(err.message);b.disabled=false}}); }catch(err){$('#content').innerHTML=`${notify('Não foi possível carregar a operação Duffel: '+err.message,'error')}`;$('#duffelBack').onclick=providers} } async function platformProviderEdit(id){ if(S.user.role!=='superadmin')return alert('Somente o Super Admin pode editar consolidadoras.'); const d=await api(withCid('/api/travel/provider-catalog')),x=(d.providers||[]).find(v=>Number(v.id)===Number(id));if(!x){$('#content').innerHTML=notify('Fornecedor do catálogo não encontrado.','error');return} $('#content').innerHTML=`

Editar consolidadora / fornecedor: ${esc(x.name)}

Cadastro mestre da plataforma. O Super Admin define o fornecedor e sua API mestre; empresas liberadas herdam a credencial automaticamente e podem optar por override próprio.
${providerLinks(x)}
${providerUrlInput('meWebsite',x.website||'','https://...','Acessar site')}
${providerUrlInput('meRegistration',x.registration_url||'','https://...','Abrir cadastro')}
Produtos
${providerCheck(PROVIDER_PRODUCTS,'meproduct',x.products||[])}
`; bindProviderUrlButtons();$('#providerBack').onclick=providers;const selected=kind=>[...document.querySelectorAll(`[data-${kind}]:checked`)].map(v=>v.getAttribute(`data-${kind}`));$('#masterEditForm').onsubmit=async e=>{e.preventDefault();try{await api('/api/travel/provider-catalog/'+id,{method:'PATCH',body:JSON.stringify({name:$('#meName').value,slug:$('#meSlug').value,provider_type:$('#meType').value,status:$('#meStatus').value,website:$('#meWebsite').value,registration_url:$('#meRegistration').value,country_code:$('#meCountry').value,integration_type:$('#meIntegration').value,products:selected('meproduct'),currencies:$('#meCurrencies').value.split(',').map(v=>v.trim().toUpperCase()).filter(Boolean),priority:Number($('#mePriority').value||100),notes:$('#meNotes').value})});await providers()}catch(err){alert(err.message)}}; } async function providerPackageEdit(id){ const [catData,packData]=await Promise.all([api(withCid('/api/travel/provider-catalog')),api(withCid('/api/travel/provider-packages'))]),catalog=catData.providers||[],x=(packData.packages||[]).find(v=>Number(v.id)===Number(id));if(!x){$('#content').innerHTML=notify('Pacote não encontrado.','error');return}const chosen=new Set((x.providers||[]).map(v=>Number(v.id))); $('#content').innerHTML=`

Editar pacote: ${esc(x.name)}

${catalog.filter(v=>v.status==='active').map(v=>``).join('')}
`;$('#providerBack').onclick=providers;$('#packageEditForm').onsubmit=async e=>{e.preventDefault();const ids=[...document.querySelectorAll('[data-pke-provider]:checked')].map(v=>Number(v.dataset.pkeProvider));if(!ids.length)return alert('Selecione pelo menos um fornecedor.');try{await api('/api/travel/provider-packages/'+id,{method:'PATCH',body:JSON.stringify({name:$('#pkeName').value,description:$('#pkeDescription').value,provider_ids:ids,status:'active'})});await providers()}catch(err){alert(err.message)}}; } async function providerEdit(id,focusIntegration=false){ const d=await api(withCid('/api/travel/providers')),x=(d.own_providers||[]).find(v=>Number(v.id)===Number(id));if(!x){$('#content').innerHTML=notify('Fornecedor próprio não encontrado.','error');return} $('#content').innerHTML=`

Editar fornecedor próprio: ${esc(x.name)}

Cadastro privado desta empresa.
${providerLinks(x)}
${providerUrlInput('oeWebsite',x.website||'','https://...','Acessar site')}
${providerUrlInput('oeRegistration',x.publication?.registration_url||'','https://...','Abrir cadastro')}
Produtos
${providerCheck(PROVIDER_PRODUCTS,'oeproduct',x.products||[])}
Ambiente da integração
${providerProductConfigHtml(x.products||[],x.config||{},'oeProduct',true)}
`; bindProviderUrlButtons();$('#providerBack').onclick=providers;const selected=kind=>[...document.querySelectorAll(`[data-${kind}]:checked`)].map(v=>v.getAttribute(`data-${kind}`));$('#ownEditForm').onsubmit=async e=>{e.preventDefault();try{await api('/api/travel/providers/'+id,{method:'PATCH',body:JSON.stringify({company_id:cid(),name:$('#oeName').value,slug:$('#oeSlug').value,provider_type:$('#oeType').value,integration_type:$('#oeIntegration').value,website:$('#oeWebsite').value,country_code:$('#oeCountry').value,products:selected('oeproduct'),currencies:$('#oeCurrencies').value.split(',').map(v=>v.trim().toUpperCase()).filter(Boolean),config:{...(x.config||{}),environment:$('#oeEnvironment').value,public_search_enabled:true,product_configs:readProviderProductConfigs(x.products||[],'oeProduct',true)},publication:{registration_url:$('#oeRegistration').value},secret:$('#oeSecret').value.trim(),notes:$('#oeNotes').value})});await providers()}catch(err){alert(err.message)}};if(focusIntegration)setTimeout(()=>$('#oeSecret')?.focus(),30); } const AI_AGENT_DEFS=[ ['orquestrador','Orquestrador','Coordena os demais agentes e distribui tarefas.'], ['atendimento','Atendimento','Atendimento automático e assistido nas conversas.'], ['email','E-mail','Classificação e assistência nas mensagens de e-mail.'], ['leads','Leads','Qualificação, prioridade e follow-up de oportunidades.'], ['crm','Clientes / CRM','Organização de contatos, clientes e histórico.'], ['cotacoes','Cotações','Preparação e organização de propostas.'], ['fornecedores','Fornecedores','Apoio à comparação e seleção de fornecedores.'], ['milhas','Milhas / Telegram','Triagem inteligente do Radar e Telegram.'], ['vendas','Vendas','Negociação, objeções e acompanhamento do fechamento.'], ['financeiro','Financeiro','Análise e alertas; ações sensíveis permanecem protegidas.'], ['pos_venda','Pós-venda','Acompanhamento depois da venda.'], ['viagens','Viagens','Voos, hotéis, traslados, passeios e itinerários.'], ['marketing','Marketing','Campanhas, relacionamento e conteúdo.'], ['seo','SEO / AEO','Conteúdo para buscadores e mecanismos de IA.'], ['audio_video','Áudio / Vídeo','Transcrição, resumo e tarefas de chamadas.'], ['documentos','Documentos','Leitura de PDFs, vouchers e comprovantes.'], ['risco','Segurança / Risco','Sinaliza inconsistências, riscos e operações suspeitas.'], ['qualidade','Qualidade','Audita respostas e ações dos demais agentes.'] ]; function aiAgentsHtml(cfg){const agents=cfg.agents||{};return `

Agentes IA

Ligue os agentes desta empresa e defina o nível de autonomia.
${AI_AGENT_DEFS.map(([id,name,desc])=>{const a=agents[id]||{},mode=['human','hybrid','auto'].includes(a.mode)?a.mode:'hybrid';return `
${esc(name)}
${esc(desc)}
`}).join('')}
`} function aiModulatorHtml(cfg){const m=cfg.modulator||{},field=(id,label,val,max=100)=>`
`;return `

Agente Modulador

Percentuais comerciais por empresa. Não altera cotações existentes silenciosamente.
${field('aiModMin','Margem mínima %',m.min_margin_pct??8)}${field('aiModDefault','Margem padrão %',m.default_margin_pct??15)}${field('aiModMax','Margem máxima %',m.max_margin_pct??35)}${field('aiModMarkup','Markup %',m.markup_pct??15,300)}${field('aiModDiscount','Desconto máximo %',m.max_discount_pct??5)}${field('aiModCommission','Comissão %',m.commission_pct??0)}${field('aiModSafety','Reserva de segurança %',m.safety_reserve_pct??3)}${field('aiModFlight','Voos %',m.flight_pct??12)}${field('aiModHotel','Hotéis %',m.hotel_pct??15)}${field('aiModMiles','Milhas %',m.miles_pct??18)}${field('aiModPackage','Pacotes %',m.package_pct??15)}${field('aiModTransfer','Traslados %',m.transfer_pct??12)}${field('aiModTour','Passeios %',m.tour_pct??15)}
`} function aiReadAgents(){const out={};AI_AGENT_DEFS.forEach(([id])=>out[id]={enabled:Boolean(document.querySelector(`[data-ai-agent-enabled="${id}"]`)?.checked),mode:document.querySelector(`[data-ai-agent-mode="${id}"]`)?.value||'hybrid'});return out} function aiReadModulator(){const n=id=>Number($(id)?.value||0);return {enabled:Boolean($('#aiModEnabled')?.checked),min_margin_pct:n('#aiModMin'),default_margin_pct:n('#aiModDefault'),max_margin_pct:n('#aiModMax'),markup_pct:n('#aiModMarkup'),max_discount_pct:n('#aiModDiscount'),commission_pct:n('#aiModCommission'),safety_reserve_pct:n('#aiModSafety'),flight_pct:n('#aiModFlight'),hotel_pct:n('#aiModHotel'),miles_pct:n('#aiModMiles'),package_pct:n('#aiModPackage'),transfer_pct:n('#aiModTransfer'),tour_pct:n('#aiModTour')}} const AI_PROVIDER_UI=[ ['groq','GroqCloud','openai/gpt-oss-120b',10], ['mistral','Mistral AI','mistral-small-latest',20], ['openrouter','OpenRouter','openrouter/free',30], ['openai','OpenAI','gpt-6-luna',35], ['google','Google Gemini','gemini-3.6-flash',40], ['cloudflare','Cloudflare Workers AI','@cf/meta/llama-3.1-8b-instruct',50], ['minimax','MiniMax','MiniMax-M3',60] ]; function aiProvidersHtml(rows=[]){ const by=Object.fromEntries((Array.isArray(rows)?rows:[]).map(x=>[String(x.provider||''),x])); return `

Provedores de IA e fallback

Prioridade menor = tenta primeiro. Se um provedor falhar, o próximo assume automaticamente.
${AI_PROVIDER_UI.map(([key,label,defModel,defPriority])=>{const p=by[key]||{},configured=Boolean(p.has_api_key),enabled=p.enabled===undefined?configured:Boolean(p.enabled),cfg=p.config||{};return `
${esc(label)}
${configured?'API configurada':'API pendente'}${p.last_status?' · último status: '+esc(p.last_status):''}
${configured?'Configurado':'Pendente'}
${key==='cloudflare'? `
` : ''}
${p.last_error?`
${esc(String(p.last_error).slice(0,220))}
`:''}
`;}).join('')}
`; } function aiPlatformProvidersHtml(rows=[]){ const by=Object.fromEntries((Array.isArray(rows)?rows:[]).map(x=>[String(x.provider||''),x])); return `

Credenciais centrais da plataforma

Cadastre cada provedor uma única vez. As empresas recebem apenas permissão de uso; as chaves não são duplicadas nem exibidas.
${AI_PROVIDER_UI.map(([key,label,defModel,defPriority])=>{const p=by[key]||{},configured=Boolean(p.has_api_key),enabled=p.enabled===undefined?configured:Boolean(p.enabled),cfg=p.config||{};return `
${esc(label)}
${configured?'Credencial central configurada':'Credencial central pendente'}${p.last_status?' · último status: '+esc(p.last_status):''}
${configured?'Configurado':'Pendente'}
${key==='cloudflare'? `
` : ''}
${p.last_error?`
${esc(String(p.last_error).slice(0,220))}
`:''}
`;}).join('')}
`; } function aiProviderGrantsHtml(rows=[]){ const by=Object.fromEntries((Array.isArray(rows)?rows:[]).map(x=>[String(x.provider||''),x])); const company=S.companies?.find(x=>Number(x.id)===Number(S.companyId)); return `

Liberação de IA para ${esc(company?.name||'empresa selecionada')}

Ative somente os provedores que esta empresa pode usar. A chave permanece no cofre central da plataforma.
${AI_PROVIDER_UI.map(([key,label,defModel,defPriority])=>{const g=by[key]||{},available=Boolean(g.available),enabled=Boolean(g.enabled);return `
${esc(label)}
${available?'Disponível na plataforma':'Cadastre a credencial central primeiro'}
${enabled?'Liberado':(available?'Bloqueado':'Indisponível')}
`;}).join('')}
`; } async function ai(){ // AI_CLIENT_BASIC_UI_V3_14 const aiTechnical=S.user?.role==='superadmin'; const d=await api(withCid('/api/ai-settings')); const s=d.settings||{}; const cfg=s.model_config||{}; let aiProviderRows=Array.isArray(s.providers)?s.providers:[],aiPlatformRows=[],aiGrantRows=[]; if(aiTechnical){ try{const pd=await api(withCid('/api/ai-providers'));aiProviderRows=Array.isArray(pd.providers)?pd.providers:aiProviderRows}catch{} try{const pd=await api('/api/platform-ai-providers');aiPlatformRows=Array.isArray(pd.providers)?pd.providers:[]}catch{} try{const gd=await api(withCid('/api/ai-provider-grants'));aiGrantRows=Array.isArray(gd.grants)?gd.grants:[]}catch{} } // ======================================================== // ADMIN DA AGÊNCIA — SOMENTE CONFIGURAÇÃO BÁSICA // ======================================================== if(!aiTechnical){ $('#content').innerHTML=`
Defina como a assistente e os agentes atuarão nesta empresa. A configuração técnica da API continua administrada pela plataforma.
${aiAgentsHtml(cfg)} ${aiModulatorHtml(cfg)}
`; $('#aiClientForm').onsubmit=async e=>{ e.preventDefault(); try{ await api('/api/ai-settings',{ method:'PUT', body:JSON.stringify({ company_id:cid(), enabled:$('#aiClientEnabled').checked, mode:$('#aiClientMode').value, model:$('#aiClientModel').value, language:$('#aiClientLanguage').value, assistant_name:$('#aiClientName').value.trim(), agents:aiReadAgents(), modulator:aiReadModulator() }) }); alert('Configurações da IA salvas.'); await ai(); }catch(err){ alert(err.message); } }; return; } // ======================================================== // SUPER ADMIN — CONFIGURAÇÃO TÉCNICA COMPLETA // ======================================================== const ch=cfg.channels||{}; $('#content').innerHTML=`

Orquestrador de IA

Configuração geral e roteamento multi-provider desta empresa.
${s.has_api_key?'API configurada':'API pendente'}
As credenciais de IA são administradas centralmente pela plataforma. A empresa selecionada recebe somente as liberações definidas abaixo.
Canais geridos pela IA

Teste e status

Use “Testar roteamento” para validar o provedor ativo e o fallback.
Configuração atual
Modelo: ${esc(cfg.model||'')}
Nome da IA: ${esc(cfg.assistant_name||'Isaura')}
Contexto: últimas ${Number(cfg.history_messages||20)} mensagens
API armazenada criptografada.
${aiPlatformProvidersHtml(aiPlatformRows)} ${aiProviderGrantsHtml(aiGrantRows)} ${aiAgentsHtml(cfg)} ${aiModulatorHtml(cfg)} `; document.querySelectorAll('[data-ai-platform-save]').forEach(btn=>btn.onclick=async()=>{ const key=btn.dataset.aiPlatformSave; try{ const config=key==='cloudflare'?{account_id:$('#aiPlatform_cloudflare_account')?.value.trim()||''}:{}; const body={ provider:key, enabled:Boolean($('#aiPlatform_'+key+'_enabled')?.checked), priority:Number($('#aiPlatform_'+key+'_priority')?.value||100), model:$('#aiPlatform_'+key+'_model')?.value.trim()||'', api_key:$('#aiPlatform_'+key+'_key')?.value.trim()||'', config }; await api('/api/platform-ai-providers',{method:'PUT',body:JSON.stringify(body)}); if($('#aiPlatform_'+key+'_key'))$('#aiPlatform_'+key+'_key').value=''; alert('Credencial central salva.'); await ai(); }catch(err){alert(err.message)} }); document.querySelectorAll('[data-ai-grant-save]').forEach(btn=>btn.onclick=async()=>{ const key=btn.dataset.aiGrantSave; try{ await api('/api/ai-provider-grants',{method:'PUT',body:JSON.stringify({ company_id:cid(), provider:key, enabled:Boolean($('#aiGrant_'+key+'_enabled')?.checked), priority:Number($('#aiGrant_'+key+'_priority')?.value||100), model:$('#aiGrant_'+key+'_model')?.value.trim()||'' })}); alert('Liberação da empresa salva.'); await ai(); }catch(err){alert(err.message)} }); const technicalPayload=()=>({ company_id:cid(), enabled:$('#aiEnabled').checked, mode:$('#aiMode').value, model:$('#aiModel').value, history_messages:Number($('#aiHistory').value||20), language:$('#aiLanguage').value, assistant_name:$('#aiName').value.trim(), system_prompt:$('#aiPrompt').value, handoff_rules:$('#aiHandoff').value, api_key:'', channels:{ whatsapp:$('#chWhatsapp').checked, instagram:$('#chInstagram').checked, facebook:$('#chFacebook').checked, webchat:$('#chWebchat').checked }, agents:aiReadAgents(), modulator:aiReadModulator() }); $('#aiTechnicalForm').onsubmit=async e=>{ e.preventDefault(); try{ await api('/api/ai-settings',{ method:'PUT', body:JSON.stringify(technicalPayload()) }); alert('Configuração técnica da IA salva.'); await ai(); }catch(err){ alert(err.message); } }; $('#aiTestBtn').onclick=async()=>{ const box=$('#aiTestResult'); box.className='notice'; box.textContent='Testando IA...'; try{ const p=technicalPayload(); const r=await api('/api/ai-test',{ method:'POST', body:JSON.stringify({ company_id:p.company_id, api_key:p.api_key, message: 'Confirme em uma frase curta que você está conectado ao CRM e pronto para atender clientes.' }) }); box.className='notice success'; box.innerHTML=` ${esc(r.model)}
${esc(r.answer)} `; }catch(err){ box.className='notice error'; box.textContent=err.message; } }; } // PANEL_QUOTE_LIVE_SEARCH_20260910 const QUOTE_LIVE_RESULT_CACHE=new Map(); const QUOTE_LIVE_SELECTED=new Map(); const QUOTE_LIVE_SEARCH_TYPES=new Set(['flight','hotel','car','activity','transfer','restaurant']); function quoteLiveSearchLabel(type){return ({flight:'Buscar voos',hotel:'Buscar hotéis',car:'Buscar carros',activity:'Buscar passeios',transfer:'Buscar traslados',restaurant:'Buscar restaurantes'})[type]||'Buscar opções'} function quoteLiveTravelerCounts(){const a=document.getElementById('qAdults')||document.getElementById('qeAdults'),c=document.getElementById('qChildren')||document.getElementById('qeChildren');return {adults:Math.max(1,Number(a?.value||1)||1),children:Math.max(0,Number(c?.value||0)||0)}} function quoteLiveCurrency(){return String((document.getElementById('qCurrency')||document.getElementById('qeCurrency'))?.value||'BRL').trim().toUpperCase()||'BRL'} function quoteLiveChildAges(prefix,count){if(!count)return[];const raw=String(document.getElementById(prefix+'LiveChildAges')?.value||'').trim(),ages=raw.split(/[\s,;]+/).filter(Boolean).map(Number);if(ages.length!==count||ages.some(x=>!Number.isInteger(x)||x<0||x>17))throw new Error(`Informe a idade das ${count} criança(s), separadas por vírgula.`);return ages} function quoteLiveSelectedProviderKey(prefix){return quoteProviderPayload(document.getElementById(prefix+'Provider')).provider_key||''} function quoteLiveClear(prefix){QUOTE_LIVE_SELECTED.delete(prefix);for(const key of [...QUOTE_LIVE_RESULT_CACHE.keys()])if(key.startsWith(prefix+'|'))QUOTE_LIVE_RESULT_CACHE.delete(key);const r=document.getElementById(prefix+'LiveResults'),s=document.getElementById(prefix+'LiveStatus');if(r)r.innerHTML='';if(s)s.textContent=''} function quoteLiveExtraHtml(prefix,type){if(type==='flight')return `
`;if(type==='hotel')return `
`;if(type==='car')return `
`;return''} function quoteLiveSearchBoxHtml(prefix,type){const hidden=QUOTE_LIVE_SEARCH_TYPES.has(type)?'':' hidden';return `
Busca automática no fornecedor
Consulta o inventário/API da empresa selecionada e preenche o item escolhido.
${quoteLiveExtraHtml(prefix,type)}
`} function quoteLiveUpdateBox(prefix,type){const box=document.getElementById(prefix+'LiveBox'),btn=document.getElementById(prefix+'LiveSearchBtn'),extra=document.getElementById(prefix+'LiveExtra');if(!box)return;box.hidden=!QUOTE_LIVE_SEARCH_TYPES.has(type);if(btn)btn.textContent=quoteLiveSearchLabel(type);if(extra)extra.innerHTML=quoteLiveExtraHtml(prefix,type);quoteLiveClear(prefix)} async function quoteLiveResolveIata(value){const raw=String(value||'').trim();if(/^[A-Za-z]{3}$/.test(raw))return raw.toUpperCase();if(raw.length<2)throw new Error('Informe uma cidade ou aeroporto válido.');const d=await api(withCid('/api/public/flights/places?q='+encodeURIComponent(raw))),code=String(d.places?.find(x=>x.iata_code)?.iata_code||'').toUpperCase();if(!code)throw new Error(`Não encontrei aeroporto para “${raw}”. Informe o código IATA, por exemplo GRU.`);return code} function quoteLiveProviderMessage(data,key){const rows=Array.isArray(data?.providers)?data.providers:[];if(key){const p=rows.find(x=>String(x.key||'')===String(key));if(p?.message)return p.message;if(p?.status==='error')return 'O fornecedor selecionado não respondeu.'}return data?.message||''} function quoteLiveNormalizeFlight(offer,meta){const slices=Array.isArray(offer?.slices)?offer.slices:[],first=slices[0]||{},last=slices[slices.length-1]||{},segments=slices.flatMap(x=>Array.isArray(x?.segments)?x.segments:[]),airline=String(offer?.owner?.name||offer?.provider_name||''),numbers=segments.map(x=>[x?.marketing_carrier?.iata_code||x?.operating_carrier?.iata_code||'',x?.marketing_carrier_flight_number||x?.flight_number||''].filter(Boolean).join(' ')).filter(Boolean).join(', '),origin=String(meta.origin||first?.origin?.iata_code||''),destination=String(meta.destination||last?.destination?.iata_code||''),amount=Number(offer?.total_amount);return {item_type:'flight',product_ref:String(offer?.id||''),provider_key:String(offer?.provider_key||''),supplier_name:String(offer?.provider_name||airline||'Fornecedor'),title:[airline||'Voo',origin&&destination?`${origin} → ${destination}`:''].filter(Boolean).join(' · '),unit_price:Number.isFinite(amount)?amount:null,currency:String(offer?.currency||offer?.total_currency||'BRL'),description:[offer?.external_description,numbers?`Voo(s): ${numbers}`:''].filter(Boolean).join(' · '),metadata:{...meta,origin,destination,airline,flight_number:numbers,provider_search_id:offer?.provider_search_id||null,source:'live'}}} function quoteLiveNormalizeService(type,p,meta,query){const amount=Number(p?.price),common={item_type:type,product_ref:String(p?.id||''),provider_key:String(p?.provider_key||''),supplier_name:String(p?.provider_name||'Fornecedor'),title:String(p?.name||'Produto'),unit_price:Number.isFinite(amount)?amount:null,currency:String(p?.currency||'BRL'),description:[p?.subtitle,p?.description,p?.location].filter(Boolean).join(' · ')},m={...meta,source:'live'};if(type==='hotel'){m.city=meta.city||p?.location||'';m.hotel_name=p?.name||meta.hotel_name||'';m.room_type=p?.subtitle||meta.room_type||'';m.check_in=query.start_date||meta.check_in||'';m.check_out=query.end_date||meta.check_out||''}else if(type==='car'){m.vehicle=p?.name||meta.vehicle||'';m.category=p?.subtitle||meta.category||''}else if(type==='activity')m.activity_name=p?.name||meta.activity_name||'';else if(type==='restaurant')m.restaurant_name=p?.name||meta.restaurant_name||'';return {...common,metadata:m}} function quoteLiveApplyResult(prefix,key){const x=QUOTE_LIVE_RESULT_CACHE.get(String(key));if(!x)return;QUOTE_LIVE_SELECTED.set(prefix,x);const provider=document.getElementById(prefix+'Provider');if(provider&&x.provider_key){const opt=[...provider.options].find(o=>String(o.dataset.key||'')===String(x.provider_key));if(opt)provider.value=opt.value}const catalog=document.getElementById(prefix+'Catalog');if(catalog)catalog.value='';const title=document.getElementById(prefix+'Title'),price=document.getElementById(prefix+'Price'),cost=document.getElementById(prefix+'Cost'),qty=document.getElementById(prefix+'Qty'),desc=document.getElementById(prefix+'Description');if(title)title.value=x.title||'';if(price)price.value=x.unit_price===null||x.unit_price===undefined?'':String(x.unit_price);if(cost)cost.value='0';if(qty)qty.value='1';if(desc)desc.value=x.description||'';for(const [k,v] of Object.entries(x.metadata||{})){const el=document.getElementById(prefix+'Meta_'+k);if(el&&v!==null&&v!==undefined)el.value=String(v)}const cur=document.getElementById('qCurrency')||document.getElementById('qeCurrency');if(cur&&x.currency&&!QUOTE_DRAFT_ITEMS.length)cur.value=String(x.currency).toUpperCase();const status=document.getElementById(prefix+'LiveStatus');if(status)status.textContent=`Selecionado: ${x.title} · ${x.supplier_name}`} function quoteLiveRenderResults(prefix,type,items,message=''){const box=document.getElementById(prefix+'LiveResults');if(!box)return;for(const key of [...QUOTE_LIVE_RESULT_CACHE.keys()])if(key.startsWith(prefix+'|'))QUOTE_LIVE_RESULT_CACHE.delete(key);if(!items.length){box.innerHTML=`
Nenhuma opção automática encontrada.${message?' '+esc(message):''} Você ainda pode preencher o item manualmente.
`;return}box.innerHTML=items.slice(0,40).map((x,i)=>{const key=`${prefix}|${type}|${Date.now()}|${i}|${x.product_ref||''}`;QUOTE_LIVE_RESULT_CACHE.set(key,x);const price=x.unit_price===null||x.unit_price===undefined?'Consultar':money(x.unit_price,x.currency||quoteLiveCurrency());return `
${esc(x.title)}
${esc(x.supplier_name||'Fornecedor')}${x.description?' · '+esc(x.description):''}
${esc(price)}
`}).join('');box.querySelectorAll('[data-quote-live-pick]').forEach(b=>b.onclick=()=>quoteLiveApplyResult(prefix,b.dataset.quoteLivePick))} async function quoteLiveSearch(prefix){const type=document.getElementById(prefix+'Type')?.value||'other';if(!QUOTE_LIVE_SEARCH_TYPES.has(type))return;const btn=document.getElementById(prefix+'LiveSearchBtn'),status=document.getElementById(prefix+'LiveStatus'),providerKey=quoteLiveSelectedProviderKey(prefix),meta=quoteReadMeta(type,prefix),counts=quoteLiveTravelerCounts();try{QUOTE_LIVE_SELECTED.delete(prefix);if(btn){btn.disabled=true;btn.textContent='Buscando...'}if(status)status.textContent='Consultando os fornecedores liberados para esta empresa...';let data,items=[];if(type==='flight'){meta.origin=String(document.getElementById(prefix+'Meta_origin')?.value||meta.origin||'').trim();meta.destination=String(document.getElementById(prefix+'Meta_destination')?.value||meta.destination||'').trim();if(!meta.origin||!meta.destination||!meta.departure_date)throw new Error('Informe origem, destino e data de ida.');const [origin,destination]=await Promise.all([quoteLiveResolveIata(meta.origin),quoteLiveResolveIata(meta.destination)]);if(origin===destination)throw new Error('Origem e destino precisam ser diferentes.');if(meta.return_date&&meta.return_dateString(x.provider_key||'')===String(providerKey));items=raw.map(x=>quoteLiveNormalizeFlight(x,meta))}else{const serviceType={hotel:'hotel',car:'car',activity:'tour',transfer:'transfer',restaurant:'restaurant'}[type];let origin='',destination='',start_date=null,end_date=null,filters={};if(type==='hotel'){destination=String(meta.city||meta.hotel_name||'').trim();start_date=meta.check_in||null;end_date=meta.check_out||null;if(!destination||!start_date||!end_date)throw new Error('Informe destino, check-in e check-out.');if(end_date<=start_date)throw new Error('O check-out deve ser posterior ao check-in.');filters.rooms=Math.max(1,Number(document.getElementById(prefix+'LiveRooms')?.value||1)||1);filters.hotel_child_ages=quoteLiveChildAges(prefix,counts.children)}else if(type==='car'){origin=String(meta.pickup_location||'').trim();destination=String(meta.dropoff_location||origin).trim();if(!origin||!meta.pickup_at||!meta.dropoff_at)throw new Error('Informe retirada, devolução e datas/horários.');start_date=String(meta.pickup_at).slice(0,10);end_date=String(meta.dropoff_at).slice(0,10);filters.pickup_time=String(meta.pickup_at).slice(11,16)||'10:00';filters.return_time=String(meta.dropoff_at).slice(11,16)||'10:00';filters.driver_age=Math.max(18,Number(document.getElementById(prefix+'LiveDriverAge')?.value||30)||30);filters.residence_country=String(document.getElementById(prefix+'LiveResidence')?.value||'BR').trim().toUpperCase()}else if(type==='activity'){origin=String(meta.destination||'').trim();destination=String(meta.activity_name||meta.destination||'').trim();start_date=meta.date||null;if(!destination)throw new Error('Informe o destino ou passeio desejado.')}else if(type==='transfer'){origin=String(meta.origin||'').trim();destination=String(meta.destination||'').trim();start_date=meta.date||null;if(!origin||!destination||!start_date)throw new Error('Informe origem, destino e data do traslado.');filters.pickup_time=meta.time||null;filters.flight_number=meta.flight_reference||null}else if(type==='restaurant'){origin=String(meta.city||'').trim();destination=String(meta.restaurant_name||meta.cuisine||meta.city||'').trim();start_date=meta.date||null;if(!origin&&!destination)throw new Error('Informe cidade, restaurante ou tipo de cozinha.');filters.restaurant_time=meta.time||null;filters.cuisine=meta.cuisine||null}const payload={type:serviceType,origin,destination,start_date,end_date,adults:counts.adults,children:counts.children,filters};data=await api(withCid('/api/public/services/search'),{method:'POST',body:JSON.stringify(payload)});let raw=Array.isArray(data.products)?data.products:[];if(providerKey)raw=raw.filter(x=>String(x.provider_key||'')===String(providerKey));items=raw.map(x=>quoteLiveNormalizeService(type,x,meta,payload))}const message=quoteLiveProviderMessage(data,providerKey);quoteLiveRenderResults(prefix,type,items,message);if(status)status.textContent=items.length?`${items.length} opção(ões) encontrada(s). Escolha uma para preencher a cotação.`:(message||'Busca concluída sem opções automáticas.')}catch(err){quoteLiveRenderResults(prefix,type,[],err.message);if(status)status.textContent=err.message}finally{if(btn){btn.disabled=false;btn.textContent=quoteLiveSearchLabel(type)}}} quoteItemFormHtml=function(prefix,cap,type,values={}){const types=quoteAllowedTypes(cap,[type]).map(([v,l])=>``).join('');return `
${quoteMetaFieldsHtml(type,prefix,values.metadata||{})}
${quoteLiveSearchBoxHtml(prefix,type)}
`} quoteBindItemBuilder=async function(prefix,cap,onTypeChange){const t=document.getElementById(prefix+'Type'),p=document.getElementById(prefix+'Provider'),cat=document.getElementById(prefix+'Catalog'),btn=document.getElementById(prefix+'LiveSearchBtn');if(!t||!p)return;const refresh=async()=>{p.innerHTML=quoteProviderOptions(cap,t.value);const box=document.getElementById(prefix+'MetaBox');if(box)box.innerHTML=quoteMetaFieldsHtml(t.value,prefix,{});quoteLiveUpdateBox(prefix,t.value);await quoteLoadCatalogProducts(t.value,p.value,cat);if(onTypeChange)onTypeChange(t.value)};t.onchange=refresh;p.onchange=async()=>{quoteLiveClear(prefix);await quoteLoadCatalogProducts(t.value,p.value,cat)};if(cat)cat.onchange=()=>{QUOTE_LIVE_SELECTED.delete(prefix);quoteApplyCatalog(prefix,cat.value)};if(btn)btn.onclick=()=>quoteLiveSearch(prefix);quoteLiveUpdateBox(prefix,t.value);await quoteLoadCatalogProducts(t.value,p.value,cat)} quoteReadItemBuilder=function(prefix){const type=document.getElementById(prefix+'Type')?.value||'other',baseProvider=quoteProviderPayload(document.getElementById(prefix+'Provider')),cat=document.getElementById(prefix+'Catalog'),product=QUOTE_CATALOG_CACHE.get(String(cat?.value||'')),live=QUOTE_LIVE_SELECTED.get(prefix),provider=live&&!baseProvider.provider_key?{...baseProvider,provider_key:live.provider_key,supplier_name:live.supplier_name}:baseProvider,title=String(document.getElementById(prefix+'Title')?.value||'').trim();if(title.length<2)throw new Error('Informe o produto/serviço.');const item={item_type:type,title,...provider,product_ref:live?String(live.product_ref||''):(product?String(product.external_id||product.id):null),quantity:Number(document.getElementById(prefix+'Qty')?.value||1),unit_cost:Number(document.getElementById(prefix+'Cost')?.value||0),unit_price:Number(document.getElementById(prefix+'Price')?.value||0),description:String(document.getElementById(prefix+'Description')?.value||'').trim(),metadata:{...(live?.metadata||{}),...quoteReadMeta(type,prefix),catalog_product_id:product?Number(product.id):null,source:live?'live':(product?'catalog':'manual')}};QUOTE_LIVE_SELECTED.delete(prefix);return item} // PANEL_ADMIN_QUOTE_SEARCH_V2_20260910 if(!document.getElementById('quoteAdminSearchV2Style'))document.head.insertAdjacentHTML('beforeend',``); const QUOTE_ADMIN_TAB_TYPES=['flight','hotel','car','activity','transfer','restaurant']; const QUOTE_ADMIN_SEARCH_KEYS={flight:['origin','destination','departure_date','return_date','cabin_class'],hotel:['city','check_in','check_out'],car:['pickup_location','dropoff_location','pickup_at','dropoff_at'],activity:['destination','date','time','participants'],transfer:['origin','destination','date','time'],restaurant:['city','date','time','people','cuisine']}; function quoteAdminTypeLabel(type){return ({flight:'✈ Passagens',hotel:'▦ Hotéis',car:'🚗 Aluguel de carros',activity:'◉ Passeios',transfer:'▣ Traslados',restaurant:'🍴 Restaurantes'})[type]||quoteItemTypeLabel(type)} function quoteAdminFieldHtml(prefix,[key,label,inputType,ph],value=''){return `
`} // AELFLOW_QUOTE_ROUTE_SEARCH_FIX_V1_20260915 function quoteAdminAirportFieldHtml(prefix,key,label,value=''){const listId=`${prefix}Airport_${key}`;return `
`} function quoteAdminSearchMetaHtml(prefix,type,meta={}){if(type==='flight'){return [quoteAdminAirportFieldHtml(prefix,'origin','Origem',meta?.origin||''),quoteAdminAirportFieldHtml(prefix,'destination','Destino',meta?.destination||''),quoteAdminFieldHtml(prefix,['departure_date','Ida','date',''],meta?.departure_date||''),quoteAdminFieldHtml(prefix,['return_date','Volta','date',''],meta?.return_date||''),quoteAdminFieldHtml(prefix,['cabin_class','Classe','text','Econômica'],meta?.cabin_class||'')].join('')}const keys=new Set(QUOTE_ADMIN_SEARCH_KEYS[type]||[]);return (QUOTE_META_FIELDS[type]||[]).filter(x=>keys.has(x[0])).map(x=>quoteAdminFieldHtml(prefix,x,meta?.[x[0]])).join('')} const QUOTE_AIRPORT_SUGGEST_TIMERS=new Map(); function quoteBindAirportSuggestions(prefix){for(const key of ['origin','destination']){const input=document.getElementById(`${prefix}Meta_${key}`),list=document.getElementById(`${prefix}Airport_${key}`);if(!input||!list)continue;input.oninput=()=>{const raw=String(input.value||'').trim();const timerKey=`${prefix}:${key}`;if(QUOTE_AIRPORT_SUGGEST_TIMERS.has(timerKey))clearTimeout(QUOTE_AIRPORT_SUGGEST_TIMERS.get(timerKey));if(raw.length<2){list.innerHTML='';return}QUOTE_AIRPORT_SUGGEST_TIMERS.set(timerKey,setTimeout(async()=>{try{const d=await api(withCid('/api/public/flights/places?q='+encodeURIComponent(raw)));list.innerHTML=(d.places||[]).slice(0,12).map(p=>``).join('')}catch{list.innerHTML=''}},220))}}} function quoteAdminManualExtraMetaHtml(prefix,type,meta={}){const keys=new Set(QUOTE_ADMIN_SEARCH_KEYS[type]||[]);const rows=(QUOTE_META_FIELDS[type]||[]).filter(x=>!keys.has(x[0]));return rows.length?`
${rows.map(x=>quoteAdminFieldHtml(prefix,x,meta?.[x[0]])).join('')}
`:''} function quoteAdminProviders(cap,type){return (cap?.products||[]).find(x=>x.key===type)?.providers||[]} function quoteAdminProviderFilterOptions(cap,type){const rows=quoteAdminProviders(cap,type);return `${rows.map(x=>``).join('')}`} function quoteAdminTabsHtml(prefix,cap,type){const allowed=new Set(quoteAllowedTypes(cap).map(x=>x[0]));return `
${QUOTE_ADMIN_TAB_TYPES.filter(x=>allowed.has(x)).map(x=>``).join('')}
`} function quoteAdminSearchFieldsHtml(prefix,cap,type,values={}){return `
A busca usa os fornecedores liberados para a empresa selecionada.
${quoteAdminSearchMetaHtml(prefix,type,values.metadata||{})}
${quoteLiveExtraHtml(prefix,type)}
Preencha os campos e clique em buscar.
`} function quoteAdminManualFieldsHtml(prefix,cap,type,values={}){const add=prefix==='qn'?`
`:'';return `
+ Adicionar manualmente
${quoteAdminManualExtraMetaHtml(prefix,type,values.metadata||{})}
${add}
`} quoteLiveSelectedProviderKey=function(prefix){return String(document.getElementById(prefix+'SearchProvider')?.value||'').trim()} quoteItemFormHtml=function(prefix,cap,type,values={}){return `
Pesquisa de produtos para a cotação. Escolha o serviço, pesquise e adicione um resultado. O modo manual fica separado abaixo.
${quoteAdminTabsHtml(prefix,cap,type)}${quoteAdminSearchFieldsHtml(prefix,cap,type,values)}${quoteAdminManualFieldsHtml(prefix,cap,type,values)}
`} function quoteAdminResultItem(x){return {item_type:x.item_type,title:x.title,platform_provider_id:null,provider_key:x.provider_key||null,supplier_name:x.supplier_name||null,product_ref:x.product_ref||null,quantity:1,unit_cost:0,unit_price:Number(x.unit_price||0),description:x.description||'',metadata:{...(x.metadata||{}),source:'live'}}} function quoteAdminAddLiveResult(prefix,key){const x=QUOTE_LIVE_RESULT_CACHE.get(String(key));if(!x)return;if(prefix==='qn'){QUOTE_DRAFT_ITEMS.push(quoteAdminResultItem(x));quoteBindDraftList(document.getElementById('qCurrency')?.value||x.currency||'BRL');const s=document.getElementById(prefix+'LiveStatus');if(s)s.textContent=`Adicionado à cotação: ${x.title}`;return}quoteLiveApplyResult(prefix,key)} quoteLiveRenderResults=function(prefix,type,items,message=''){const box=document.getElementById(prefix+'LiveResults');if(!box)return;for(const key of [...QUOTE_LIVE_RESULT_CACHE.keys()])if(key.startsWith(prefix+'|'))QUOTE_LIVE_RESULT_CACHE.delete(key);if(!items.length){box.innerHTML=`
Nenhuma opção encontrada.${message?' '+esc(message):''} ${type==='hotel'?'Não há inventário hoteleiro operacional para esta pesquisa.':'Você pode tentar outro fornecedor ou usar o modo manual.'}
`;return}box.innerHTML=items.slice(0,40).map((x,i)=>{const key=`${prefix}|${type}|${Date.now()}|${i}|${x.product_ref||''}`;QUOTE_LIVE_RESULT_CACHE.set(key,x);const price=x.unit_price===null||x.unit_price===undefined?'Consultar':money(x.unit_price,x.currency||quoteLiveCurrency());return `
${esc(x.title)}
${esc(x.supplier_name||'Fornecedor')}${x.description?' · '+esc(x.description):''}
${esc(price)}
`}).join('');box.querySelectorAll('[data-quote-live-pick]').forEach(b=>b.onclick=()=>quoteAdminAddLiveResult(prefix,b.dataset.quoteLivePick))} quoteBindItemBuilder=async function(prefix,cap,onTypeChange){const root=document.getElementById(prefix+'Tabs')?.closest('.quote-item-builder');if(!root)return;if(prefix==='qn'){const legacy=document.getElementById('qAddDraftItem');if(legacy)legacy.style.display='none';const intro=document.getElementById('qItemBuilder')?.previousElementSibling;if(intro?.classList.contains('notice'))intro.innerHTML='Pesquise nos fornecedores liberados e adicione os resultados à proposta. O fornecedor permanece interno.'}const bind=async()=>{const type=document.getElementById(prefix+'Type')?.value||'flight',p=document.getElementById(prefix+'Provider'),cat=document.getElementById(prefix+'Catalog'),btn=document.getElementById(prefix+'LiveSearchBtn'),manual=document.getElementById(prefix+'ManualAdd');if(btn)btn.onclick=()=>quoteLiveSearch(prefix);if(type==='flight')quoteBindAirportSuggestions(prefix);if(p)p.onchange=()=>quoteLoadCatalogProducts(type,p.value,cat);if(cat)cat.onchange=()=>quoteApplyCatalog(prefix,cat.value);if(manual)manual.onclick=()=>{try{QUOTE_DRAFT_ITEMS.push(quoteReadItemBuilder(prefix));quoteBindDraftList(document.getElementById('qCurrency')?.value||'BRL');const d=document.getElementById(prefix+'ManualBox');if(d)d.open=false}catch(e){alert(e.message)}};if(p)await quoteLoadCatalogProducts(type,p.value,cat)};root.querySelectorAll('[data-quote-admin-tab]').forEach(b=>b.onclick=async()=>{const type=b.dataset.quoteAdminTab;root.innerHTML=`
Pesquisa de produtos para a cotação. Escolha o serviço, pesquise e adicione um resultado. O modo manual fica separado abaixo.
${quoteAdminTabsHtml(prefix,cap,type)}${quoteAdminSearchFieldsHtml(prefix,cap,type,{})}${quoteAdminManualFieldsHtml(prefix,cap,type,{})}`;await quoteBindItemBuilder(prefix,cap,onTypeChange);if(onTypeChange)onTypeChange(type)});await bind()} quoteReadItemBuilder=function(prefix){const type=document.getElementById(prefix+'Type')?.value||'other',provider=quoteProviderPayload(document.getElementById(prefix+'Provider')),cat=document.getElementById(prefix+'Catalog'),product=QUOTE_CATALOG_CACHE.get(String(cat?.value||'')),title=String(document.getElementById(prefix+'Title')?.value||'').trim();if(title.length<2)throw new Error('Informe o produto/serviço.');return {item_type:type,title,...provider,product_ref:product?String(product.external_id||product.id):null,quantity:Number(document.getElementById(prefix+'Qty')?.value||1),unit_cost:Number(document.getElementById(prefix+'Cost')?.value||0),unit_price:Number(document.getElementById(prefix+'Price')?.value||0),description:String(document.getElementById(prefix+'Description')?.value||'').trim(),metadata:{...quoteReadMeta(type,prefix),catalog_product_id:product?Number(product.id):null,source:product?'catalog':'manual'}}} // PANEL_ADMIN_QUOTE_PROD_V3_20260910 const AELFLOW_ADMIN_UI_BUILD='20260910-admin-quote-v3'; const quoteItemFormHtmlAdminV2=quoteItemFormHtml; quoteItemFormHtml=function(prefix,cap,type,values={}){ return `
Painel ADM · Busca integrada${AELFLOW_ADMIN_UI_BUILD}
`+quoteItemFormHtmlAdminV2(prefix,cap,type,values); }; let AELFLOW_ADMIN_UI_RELOADING=false; async function aelflowAdminUiBuildCheck(){ if(AELFLOW_ADMIN_UI_RELOADING)return; try{ const r=await fetch('/api/ui-build?ts='+Date.now(),{credentials:'same-origin',cache:'no-store'}); if(!r.ok)return; const d=await r.json().catch(()=>null); const remote=String(d?.build||''); if(remote&&remote!==AELFLOW_ADMIN_UI_BUILD){ AELFLOW_ADMIN_UI_RELOADING=true; location.reload(); } }catch{} } window.addEventListener('focus',aelflowAdminUiBuildCheck); document.addEventListener('visibilitychange',()=>{if(!document.hidden)aelflowAdminUiBuildCheck()}); setInterval(aelflowAdminUiBuildCheck,60000); boot().catch(e=>{document.body.innerHTML=`
${notify(e.message,'error')}
`})