RELATÓRIO DE ESTRUTURA E CÓDIGO - DUO VOICING Gerado em: Thu Jan 22 11:42:16 AM -03 2026 ========================================== ESTRUTURA DE DIRETÓRIOS: - - - - - v2 - - - - - - scan_admin_tabela.txt - - - - - - login.php - - - - - - reviews_cache.json - - - - - - scan_contrato.txt - - - - - - admin.php.bk_ui_modern - - - - - - assets - - - - - - - media - - - - - - - - logoduo_clean.png - - - - - - - - logoduo.svg - - - - - - - - Quarteto.mp4 - - - - - - - - Duo.mp4 - - - - - - - - Trio.mp4 - - - - - - - - duovoicing.jpg - - - - - - - fonts - - - - - - - - GreatVibes-Regular.ttf - - - - - - - js - - - - - - - - regras.js - - - - - - - - app.js - - - - - - - css - - - - - - - - style.css - - - - - - - - style_v1.css - - - - - - duo_v2.db - - - - - - api.php - - - - - - wizard_modal.php - - - - - - logout.php - - - - - - contrato.php - - - - - - templates - - - - - - admin_preparar.php - - - - - - index.php - - - - - - og.php - - - - - - relatorio_auditoria_v2.txt - - - - - - api_reviews.php - - - - - - admin.php.bk_clean - - - - - - config.php - - - - - - admin.php.bk_views - - - - - - proposta.php - - - - - - scan_completo_v2.txt - - - - - - admin.php - - - - - - src - - - - - - - Database.php - - - - - - - Proposta.php - - - - - - get_views.php - - - - - - scan_duovoicing_1769092936.txt ========================================== CONTEÚDO DOS ARQUIVOS: ========================================== -------------------------------------------------- ARQUIVO: login.php -------------------------------------------------- Login - Portal V2

PORTAL V2

-------------------------------------------------- ARQUIVO: reviews_cache.json -------------------------------------------------- [{"name":"Bianca Mayra","label":"Avalia\u00e7\u00e3o no Google","text":"Minha experi\u00eancia em 05.12 no dia do meu casamento foi incr\u00edvel.. Um sonho!\nEncontrei a for\u00e7a, a do\u00e7ura, leveza e amor na voz dessas duas pessoas maravilhosas, que eternizaram o meu grande dia!\nDesde o primeiro contato, houve muita clareza e acolhimento. Totalmente flex\u00edveis, pontuais e profissionais em todos os aspectos! Indico de olhos fechados!!!!! Obrigada por tudo!!!\nQue Deus aben\u00e7oe!!!","img":"https:\/\/lh3.googleusercontent.com\/a\/ACg8ocJNVSe2uDf4HpsN7SqU7Yxe5U10v0GHucPBh-TMlCu8oIQSuA=s128-c0x00000000-cc-rp-mo","link":"https:\/\/www.google.com\/maps\/contrib\/107937261206976556441\/reviews"},{"name":"caroline pereira","label":"Avalia\u00e7\u00e3o no Google","text":"No dia 25\/10\/2025 foi realizado meu casamento e com certeza foi um dia maravilhoso na voz da equipe duo Voicing! Todos os convidados ficaram emocionados e elogiaram cada m\u00fasica cantada.. voc\u00eas foram impec\u00e1veis! Obrigado, desejo muito sucesso. \u2665\ufe0f","img":"https:\/\/lh3.googleusercontent.com\/a-\/ALV-UjU3A0enq8g5N0bWEW8Ln0UbQULUr1WQF7BllLTh3dQ4A_eGx1Wj=s128-c0x00000000-cc-rp-mo","link":"https:\/\/www.google.com\/maps\/contrib\/106705057112035970315\/reviews"},{"name":"Adriana Bernnardo","label":"Avalia\u00e7\u00e3o no Google","text":"Pesquisei muitas empresas para meu casamento e o atendimento da Duo Voicing foi um grande diferencial! Agrade\u00e7o pela agilidade e aten\u00e7\u00e3o que tiveram comigo! A Lari tem uma voz muito linda, que faz com que a can\u00e7\u00e3o seja ouvida de modo muito leve e natural!! Desejo muito sucesso \ud83e\udd0d","img":"https:\/\/lh3.googleusercontent.com\/a-\/ALV-UjUr9uFCcli9iwsDzYpWdli0WqtX9qGnnGwbXvyqLnqGYZiIdXvW=s128-c0x00000000-cc-rp-mo","link":"https:\/\/www.google.com\/maps\/contrib\/117423802761720140871\/reviews"},{"name":"Nathalia Borges","label":"Avalia\u00e7\u00e3o no Google","text":"A Duo Voicing tornou a nossa cerim\u00f4nia um momento simplesmente inesquec\u00edvel. Cada m\u00fasica foi executada com tanta emo\u00e7\u00e3o e delicadeza que tocou o cora\u00e7\u00e3o de todos os presentes. A harmonia das vozes e a sensibilidade do repert\u00f3rio deixaram tudo ainda mais especial. Somos imensamente gratos por fazerem parte desse dia t\u00e3o importante e por transformarem cada instante em pura magia. \ud83d\udc96\ud83c\udfb6","img":"https:\/\/lh3.googleusercontent.com\/a\/ACg8ocKRETVi-OzODf3y_Dw6ubAQA0H9-pldvwsOR63IRqctW_2rEw=s128-c0x00000000-cc-rp-mo","link":"https:\/\/www.google.com\/maps\/contrib\/108148015230815213563\/reviews"},{"name":"williany braga","label":"Avalia\u00e7\u00e3o no Google","text":"Foi a melhor escolha que fizemos!! Quer\u00edamos um casamento cat\u00f3lico e tivemos , eles tiveram total colabora\u00e7\u00e3o nisso! Houve atraso no nosso casamento e eles prontamente fizeram uma sala Linda para os convidados, todos elogiaram muito! Estava tudo perfeito! Que voz \u2665\ufe0f","img":"https:\/\/lh3.googleusercontent.com\/a-\/ALV-UjXv2uYRgPAhwgnjGgvI5vtJ76PDbnHzHVKxvsOosn7T88qvqmPD2Q=s128-c0x00000000-cc-rp-mo","link":"https:\/\/www.google.com\/maps\/contrib\/112040310264352930419\/reviews"}] -------------------------------------------------- ARQUIVO: assets/js/regras.js -------------------------------------------------- /** * DUO VOICING - REGRAS DE NEGÓCIO (V2) * Centraliza toda a lógica de preços, logística e validações do sistema. */ const Regras = { // --- CONFIGURAÇÕES GERAIS --- Config: { origemCep: '05527-000', descontoPix: 0.20, // 20% multiplicadorCoq: 1.8, // +80% no valor total descontoSom: 300, // Desconto R$ 300 (Som Local) arredondamento: 50, // Arredonda para cima (múltiplos de 50) // Custo por Instrumento Adicional (Baseado em média, mas o valor real vem do banco) baseInstrumentoPadrao: 650, // Tabela de Frete (Ida e Volta inclusas no cálculo) frete: [ { max: 60, taxa: 0, fixo: 0 }, // Até 60min: Grátis { max: 180, taxa: 5, fixo: 0 }, // 61-180min: R$ 5/km { max: 300, taxa: 10, fixo: 0 }, // 181-300min: R$ 10/km { max: 9999, taxa: 15, fixo: 400 } // >300min: R$ 15/km + R$ 400 ] }, // --- MÓDULO DE CÁLCULO --- Calculo: { /** * Calcula o valor do serviço musical (Cachê) */ servico: function(baseVal, baseInst, formacao, temCoquetel, somLocal) { let v = parseFloat(baseVal) || 0; let inst = parseFloat(baseInst) || Regras.Config.baseInstrumentoPadrao; // 1. Formação (Soma instrumentos extras à base) if (formacao === 'Trio') v += inst; if (formacao === 'Quarteto') v += (inst * 2); // 2. Coquetel (Inflaciona o contrato em 80%) if (temCoquetel) v = v * Regras.Config.multiplicadorCoq; // 3. Sonorização (Desconto se não usar a nossa) if (somLocal === 'local') v -= Regras.Config.descontoSom; return v; }, /** * Calcula o Frete baseado em KM e Tempo (Google Maps) */ frete: function(km, minutos) { if (!km || !minutos) return 0; if (minutos <= 60) return 0; // Isenção zona local const faixa = Regras.Config.frete.find(f => minutos <= f.max) || Regras.Config.frete[3]; // Fórmula: (KM * Taxa * 2 [Ida+Volta]) + TaxaFixa return (km * faixa.taxa * 2) + faixa.fixo; }, /** * Arredonda valores para ficar "bonito" (Ex: 2123 -> 2150) */ arredondar: function(valor) { const mult = Regras.Config.arredondamento; return Math.ceil(valor / mult) * mult; }, /** * Processa o Total Geral */ processar: function(p) { // A. Serviço let valServico = this.servico(p.base, p.base_inst, p.formacao, p.coquetel, p.som); // B. Cupom (Aplica sobre o serviço) if (p.cupom > 0) valServico = valServico * (1 - p.cupom); valServico = this.arredondar(valServico); // C. Frete let valFrete = this.frete(p.km, p.minutos); // D. Totais let totalParcelado = valServico + valFrete; // Regra Pix: Desconto apenas no serviço, frete é cheio let servicoVista = this.arredondar(valServico * (1 - Regras.Config.descontoPix)); let totalVista = servicoVista + valFrete; // Seleção final baseada no método de pagamento let totalFinal = (p.pagamento === 'vista') ? totalVista : totalParcelado; return { total: totalFinal, totalSemDesconto: totalParcelado, // Para mostrar "De X por Y" totalVista: totalVista, frete: valFrete, fmt: totalFinal.toLocaleString('pt-br', {style: 'currency', currency: 'BRL'}), fmtVista: totalVista.toLocaleString('pt-br', {style: 'currency', currency: 'BRL'}) }; } }, // --- MÓDULO DE VALIDAÇÕES --- Validacao: { cpf: function(cpf) { cpf = cpf.replace(/[^\d]+/g,''); if(cpf == '') return false; if (cpf.length != 11 || /^(\d)\1{10}$/.test(cpf)) return false; let add = 0; for (let i=0; i < 9; i ++) add += parseInt(cpf.charAt(i)) * (10 - i); let rev = 11 - (add % 11); if (rev == 10 || rev == 11) rev = 0; if (rev != parseInt(cpf.charAt(9))) return false; add = 0; for (let i = 0; i < 10; i ++) add += parseInt(cpf.charAt(i)) * (11 - i); rev = 11 - (add % 11); if (rev == 10 || rev == 11) rev = 0; if (rev != parseInt(cpf.charAt(10))) return false; return true; }, maioridade: function(dataNasc) { if(!dataNasc || dataNasc.length < 10) return false; const hoje = new Date(); const partes = dataNasc.split('/'); // espera dd/mm/aaaa const nasc = new Date(partes[2], partes[1] - 1, partes[0]); let idade = hoje.getFullYear() - nasc.getFullYear(); const m = hoje.getMonth() - nasc.getMonth(); if (m < 0 || (m === 0 && hoje.getDate() < nasc.getDate())) idade--; return idade >= 18; } } }; -------------------------------------------------- ARQUIVO: assets/js/app.js -------------------------------------------------- /** * 1. CONFIGURAÇÕES */ const Config = { logistica: { origem: "05527-000", isento_ate_minutos: 60, faixas: [ { min: 61, max: 180, taxa: 5.00 }, { min: 181, max: 300, taxa: 10.00 }, { min: 301, max: 9999, taxa: 15.00, extra: 400.00 } ] } }; /** * 2. CALCULADORA */ const Calculadora = { moedaParaFloat(valor) { if (!valor) return 0; return parseFloat(valor.replace('R$', '').replace(/\./g, '').replace(',', '.')) || 0; }, floatParaMoeda(valor) { return 'R$ ' + valor.toLocaleString('pt-BR', { minimumFractionDigits: 2 }); }, calcularTotal(cache, inst, log, temCupom, percCupom) { let subtotal = cache + log; // Inst não soma visualmente let desconto = 0; if (temCupom) { desconto = (subtotal * percCupom) / 100; } return subtotal - desconto; } }; /** * 3. CONTROLLER */ const App = { st: { step: 1, tot: 4, id: null }, init() { this.ev(); if(typeof google !== 'undefined' && google.maps && google.maps.places) this.ac(); else window.initMapCallback = () => this.ac(); }, ev() { document.getElementById('btnNext').onclick = () => this.go(1); document.getElementById('btnPrev').onclick = () => this.go(-1); document.getElementById('in-data').onchange = (e) => this.chkDt(e.target.value); ['base-cache', 'base-inst', 'base-logistica'].forEach(id => { const el = document.getElementById(id); el.onblur = () => this.atualizarValores(); el.onfocus = () => el.select(); }); document.getElementById('tg-cupom').onchange = (e) => { document.getElementById('box-cupom').style.display = e.target.checked ? 'block' : 'none'; this.atualizarCupomLabel(); this.atualizarValores(); }; document.getElementById('in-cupom-perc').oninput = (e) => { let v = e.target.value.replace(/\D/g, ''); if(v>100) v=100; e.target.value = v; this.atualizarCupomLabel(); this.atualizarValores(); }; const btns = document.querySelectorAll('.ft-item'); if(btns.length > 0) btns.forEach(b => b.onclick = () => { btns.forEach(x => x.classList.remove('active')); b.classList.add('active'); this.atualizarCupomLabel(); }); }, atualizarValores() { const cache = Calculadora.moedaParaFloat(document.getElementById('base-cache').value); const inst = Calculadora.moedaParaFloat(document.getElementById('base-inst').value); const log = Calculadora.moedaParaFloat(document.getElementById('base-logistica').value); const temCupom = document.getElementById('tg-cupom').checked; const percCupom = parseFloat(document.getElementById('in-cupom-perc').value) || 0; const total = Calculadora.calcularTotal(cache, inst, log, temCupom, percCupom); document.getElementById('in-valor-display').innerText = Calculadora.floatParaMoeda(total); document.getElementById('in-valor').value = Calculadora.floatParaMoeda(total); }, atualizarCupomLabel() { const perc = document.getElementById('in-cupom-perc').value; const activeBtn = document.querySelector('.ft-item.active'); const prefix = activeBtn ? activeBtn.innerText.toUpperCase() : 'DUO'; document.getElementById('lbl-cupom').innerText = perc ? `${prefix}${perc}%OFF` : '...'; }, ac() { const input = document.getElementById('in-local'); if (!input || !google || !google.maps || !google.maps.places) return; const ac = new google.maps.places.Autocomplete(input, {componentRestrictions: {country: "br"}, fields: ["formatted_address", "geometry"]}); ac.addListener('place_changed', () => { const p = ac.getPlace(); if (p.geometry) this.calcRoute(p.formatted_address); }); }, calcRoute(dest) { const mb = document.getElementById('meta-info'); mb.innerHTML = 'Calculando...'; new google.maps.DirectionsService().route({ origin: Config.logistica.origem, destination: dest, travelMode: 'DRIVING' }, (res, status) => { if (status === 'OK') { const leg = res.routes[0].legs[0]; const km = Math.round(leg.distance.value / 1000); const mins = Math.round(leg.duration.value / 60); mb.innerHTML = `${km} km • ${leg.duration.text}`; let custo = 0; if (mins > Config.logistica.isento_ate_minutos) { const r = Config.logistica.faixas.find(x => mins >= x.min && mins <= x.max); custo = r ? (km * r.taxa * 2) + (r.extra || 0) : (km * 15.00 * 2) + 400.00; } document.getElementById('base-logistica').value = Calculadora.floatParaMoeda(custo); this.atualizarValores(); } else { mb.innerText = 'Rota n/d'; document.getElementById('base-logistica').value = 'R$ 0,00'; } }); }, // --- SEGURANÇA: CHECAR DATA --- async chkDt(d){ if(!d)return; try{ const id = document.getElementById('formId').value; let r=await fetch('api.php?action=check_date&date='+d+'&id='+id); let j=await r.json(); let box=document.getElementById('conflict-alert'); let list=document.getElementById('conflict-list'); if(j.items.length>0){ box.style.display='block'; // Exibe Nome, Local e Valor list.innerHTML = j.items.map(i => `
${i.titulo}
${i.local}
${i.valor}
`).join(''); } else { box.style.display='none'; } }catch(e){console.error(e)} }, async load(id){ try{ let r=await fetch("api.php?action=get_info&id="+id); let d=await r.json(); document.getElementById("in-nome").value=d.nome_casal; document.getElementById("in-local").value=d.local_evento; document.getElementById("in-data").value=d.data_evento; this.chkDt(d.data_evento); document.getElementById("in-valor-display").innerText=d.valor_display; document.getElementById("base-cache").value=d.base_cache; document.getElementById("base-inst").value=d.base_inst; document.getElementById("base-logistica").value=d.base_log; let tgCupom = document.getElementById("tg-cupom"); if(d.tem_cupom == 1) { tgCupom.checked = true; document.getElementById("box-cupom").style.display="block"; document.getElementById("in-cupom-perc").value=d.cupom_perc; this.atualizarCupomLabel(); } else { tgCupom.checked = false; document.getElementById("box-cupom").style.display="none"; } let tgSemEntrada = document.querySelector('input[name="sem_entrada"]'); if(tgSemEntrada) tgSemEntrada.checked = (d.sem_entrada == 1); this.atualizarValores(); }catch(e){console.error(e)} }, open(mode, id=null) { this.st.id = id; this.st.step = 1; document.getElementById('formAction').value = mode === 'edit' ? 'update' : 'create'; document.getElementById('formId').value = id || ''; document.getElementById('wizardModal').style.display = 'flex'; if (mode === 'create') { document.getElementById('wizardForm').reset(); document.getElementById('base-cache').value = 'R$ 2.600,00'; document.getElementById('base-inst').value = 'R$ 650,00'; document.getElementById('base-logistica').value = 'R$ 0,00'; document.getElementById('in-valor-display').innerText = 'R$ 0,00'; document.getElementById('box-cupom').style.display = 'none'; document.getElementById('lbl-cupom').innerText = '...'; document.getElementById('meta-info').innerText = '--'; this.show(1); this.atualizarValores(); } else this.load(id); }, close() { document.getElementById('wizardModal').style.display = 'none'; }, go(n) { let x=this.st.step+n; if(x>this.st.tot){document.getElementById('wizardForm').submit();return} if(x<1)return; this.show(x); }, show(n) { document.querySelectorAll('.step-content').forEach(e=>e.classList.remove('active')); document.getElementById('step-'+n).classList.add('active'); this.st.step=n; for(let i=1;i<=4;i++){ let el=document.getElementById('icon-'+i); el.className='step-icon'; if(i===n)el.classList.add('active'); if(i App.init(); -------------------------------------------------- ARQUIVO: assets/css/style.css -------------------------------------------------- /* Importando a fonte original */ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap'); body { font-family: 'DM Sans', sans-serif; background: #f2f0eb; color: #2c2825; margin: 0; padding-bottom: 120px; -webkit-tap-highlight-color: transparent; } .container { max-width: 1000px; margin: 0 auto; padding: 15px 20px; } /* Estilo dos Cards (Idêntico ao V1) */ .list-item { display: flex; justify-content: space-between; align-items: center; padding: 15px 10px; margin-bottom: 5px; background: #fff; border-bottom: 1px solid #f3f4f6; cursor: pointer; transition: background 0.2s; border-radius: 0; } .list-item:hover { background: #fdfcf8; } .li-info { flex: 1; padding-right: 10px; display:flex; flex-direction:column; gap:3px; } .li-top-row { display: flex; align-items: center; gap: 8px; } .li-name { font-size: 15px; font-weight: 700; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 90%; } .li-actions { display: flex; align-items: center; gap: 10px; } .li-meta { text-align: right; display: flex; flex-direction: column; justify-content: center; } .li-date { font-size: 11px; font-weight: 600; color: #9ca3af; text-transform: uppercase; } .li-price { font-size: 13px; font-weight: 700; color: #b68b35; } .li-local { font-size: 12px; color: #6b7280; } /* Badges */ .mini-tag { font-size: 9px; padding: 2px 6px; border-radius: 4px; font-weight: 700; margin-left: 5px; text-transform: uppercase; display: inline-block; vertical-align: middle; } .tag-blue { background: #e0f2fe; color: #0369a1; } .tag-pink { background: #fce7f3; color: #be185d; } /* Wizard e Modais (Mantendo o estilo limpo, mas com a fonte certa) */ .modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 2000; align-items: center; justify-content: center; } .wizard-card { background: #fff; width: 90%; max-width: 500px; border-radius: 24px; padding: 30px 25px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); position: relative; margin: auto; } .step-content { display: none; text-align: center; } .step-content.active { display: block; } .big-input { width: 100%; padding: 18px; border: 2px solid #e5e7eb; border-radius: 16px; font-size: 18px; font-weight: 700; text-align: center; outline: none; margin-top: 10px; font-family: 'DM Sans', sans-serif; } .step-title { font-size: 18px; font-weight: 800; color: #b68b35; margin-bottom: 5px; text-transform: uppercase; } /* Botões */ .btn-dark { background: #222; color: #fff; border: none; border-radius: 12px; padding: 10px 20px; font-weight: 700; } .btn-light { background: #f3f4f6; color: #666; border: none; border-radius: 12px; font-weight: 700; } .text-warning { color: #b68b35 !important; } /* Input de busca estilo V1 */ .search-box { position: relative; display: flex; align-items: center; margin-bottom: 20px; } .search-input { width: 100%; height: 44px; padding: 0 15px; border-radius: 12px; border: 1px solid #e5e7eb; background: #fff; font-size: 14px; outline: none; font-family: 'DM Sans', sans-serif; } .fab { position: fixed; bottom: 90px; right: 25px; background: #222; color: #fff; width: 50px; height: 50px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; box-shadow: 0 5px 20px rgba(0,0,0,0.2); z-index: 900; cursor: pointer; text-decoration: none;} /* --- PATCH DE ALINHAMENTO (V2) --- */ .big-input { height: 60px !important; /* Força Data e Nome a terem a mesma altura */ min-height: 60px !important; /* Garante que não encolha no mobile */ line-height: normal !important;/* Centraliza o cursor verticalmente */ box-sizing: border-box; /* A borda conta no tamanho total (evita quebra) */ padding: 10px 15px !important; /* Espaço interno seguro */ margin-top: 15px !important; /* Espaço padrão do título */ width: 100% !important; /* Ocupa a largura total */ } /* Remove aparência nativa feia do iOS na data */ input[type="date"] { -webkit-appearance: none; appearance: none; background-color: #fff; } /* --- WIZARD STEPPER (Ícones de Etapas) --- */ .stepper { display: flex; justify-content: center; gap: 20px; margin-bottom: 25px; position: relative; } .step-icon { width: 35px; height: 35px; border-radius: 50%; background: #fff; border: 2px solid #e5e7eb; display: flex; align-items: center; justify-content: center; color: #ccc; font-weight: 700; transition: 0.3s; z-index: 2; } .step-icon.active { border-color: #b68b35; background: #b68b35; color: #fff; transform: scale(1.1); } .step-icon.done { background: #b68b35; border-color: #b68b35; color: #fff; } /* Linha conectora */ .stepper::before { content: ''; position: absolute; top: 50%; left: 20px; right: 20px; height: 2px; background: #e5e7eb; z-index: 1; transform: translateY(-50%); } /* --- INPUTS COM ÍCONE (Lápis) --- */ .input-edit-group { position: relative; display: flex; align-items: center; } .input-edit-group input { padding-right: 35px; text-align: center; font-weight: 700; color: #555; } .edit-icon { position: absolute; right: 10px; color: #9ca3af; cursor: pointer; width: 16px; height: 16px; } .edit-icon:hover { color: #b68b35; } /* --- TOGGLES (Chaves Sim/Não) --- */ .toggle-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px dashed #eee; font-size: 13px; font-weight: 600; color: #555; } .switch { position: relative; display: inline-block; width: 46px; height: 24px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px; } .slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .slider { background-color: #b68b35; } input:checked + .slider:before { transform: translateX(22px); } /* --- CUPOM AREA --- */ .cupom-area { background: #fdf8ef; border: 1px solid #efe6d5; padding: 10px; border-radius: 8px; margin-top: 10px; display: none; } .cupom-label { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: #b68b35; font-weight: 800; text-align: center; margin-top: 5px; display: block; } .formation-toggles { display: flex; gap: 5px; justify-content: center; margin-top: 8px; } .ft-item { font-size: 10px; padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; color: #999; } .ft-item.active { background: #b68b35; color: #fff; border-color: #b68b35; } /* --- WIZARD STEPPER (Ícones de Etapas) --- */ .stepper { display: flex; justify-content: center; gap: 20px; margin-bottom: 25px; position: relative; } .step-icon { width: 35px; height: 35px; border-radius: 50%; background: #fff; border: 2px solid #e5e7eb; display: flex; align-items: center; justify-content: center; color: #ccc; font-weight: 700; transition: 0.3s; z-index: 2; } .step-icon.active { border-color: #b68b35; background: #b68b35; color: #fff; transform: scale(1.1); } .step-icon.done { background: #b68b35; border-color: #b68b35; color: #fff; } /* Linha conectora */ .stepper::before { content: ''; position: absolute; top: 50%; left: 20px; right: 20px; height: 2px; background: #e5e7eb; z-index: 1; transform: translateY(-50%); } /* --- INPUTS COM ÍCONE (Lápis) --- */ .input-edit-group { position: relative; display: flex; align-items: center; } .input-edit-group input { padding-right: 35px; text-align: center; font-weight: 700; color: #555; } .edit-icon { position: absolute; right: 10px; color: #9ca3af; cursor: pointer; width: 16px; height: 16px; } .edit-icon:hover { color: #b68b35; } /* --- TOGGLES (Chaves Sim/Não) --- */ .toggle-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px dashed #eee; font-size: 13px; font-weight: 600; color: #555; } .switch { position: relative; display: inline-block; width: 46px; height: 24px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px; } .slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .slider { background-color: #b68b35; } input:checked + .slider:before { transform: translateX(22px); } /* --- CUPOM AREA --- */ .cupom-area { background: #fdf8ef; border: 1px solid #efe6d5; padding: 10px; border-radius: 8px; margin-top: 10px; display: none; } .cupom-label { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: #b68b35; font-weight: 800; text-align: center; margin-top: 5px; display: block; } .formation-toggles { display: flex; gap: 5px; justify-content: center; margin-top: 8px; } .ft-item { font-size: 10px; padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; color: #999; } .ft-item.active { background: #b68b35; color: #fff; border-color: #b68b35; } /* --- FIX: Placeholder para Input Date --- */ input[type="date"] { position: relative; } input[type="date"]::-webkit-calendar-picker-indicator { background: transparent; bottom: 0; color: transparent; cursor: pointer; height: auto; left: 0; position: absolute; right: 0; top: 0; width: auto; } input[type="date"]:invalid::-webkit-datetime-edit { color: transparent; } input[type="date"]:invalid::before { content: attr(placeholder); color: #9ca3af; position: absolute; left: 16px; pointer-events: none; } input[type="date"]:focus:invalid::before { display: none; } /* --- REFINAMENTO STEP 4 (Sair do modo Ogro) --- */ .closure-card { background: #fdfcf8; border: 1px solid #efe6d5; border-radius: 16px; padding: 20px; text-align: center; margin-bottom: 20px; } .closure-total-label { font-size: 11px; font-weight: 700; color: #999; text-transform: uppercase; letter-spacing: 1px; } .closure-total-value { font-size: 36px; font-weight: 800; color: #b68b35; margin: 5px 0; letter-spacing: -1px; } .detail-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px dashed #eee; font-size: 14px; color: #555; } .detail-row:last-child { border-bottom: none; } .detail-label { font-weight: 600; color: #666; display: flex; align-items: center; gap: 6px; } .detail-input-wrapper { position: relative; width: 100px; } .detail-input { width: 100%; border: 1px solid transparent; background: transparent; text-align: right; font-weight: 700; color: #333; padding-right: 20px; transition: 0.2s; border-radius: 6px; } .detail-input:hover, .detail-input:focus { background: #fff; border-color: #ddd; outline: none; } .detail-edit-icon { position: absolute; right: 0; top: 50%; transform: translateY(-50%); width: 12px; height: 12px; color: #ccc; pointer-events: none; } /* Ícones do Stepper (V1 Style) */ .step-icon svg { width: 16px; height: 16px; stroke-width: 2.5; stroke: currentColor; fill: none; } /* --- FIX GOOGLE MAPS AUTOCOMPLETE --- */ .pac-container { z-index: 100000 !important; /* Fica acima do modal */ border-radius: 0 0 12px 12px; border: none; box-shadow: 0 15px 30px rgba(0,0,0,0.2); font-family: 'DM Sans', sans-serif; margin-top: 2px; } .pac-item { padding: 10px 15px; border-top: 1px solid #f0f0f0; cursor: pointer; } .pac-item:hover { background-color: #fdf8ef; } .pac-item-query { font-size: 14px; color: #333; font-weight: 700; } -------------------------------------------------- ARQUIVO: assets/css/style_v1.css -------------------------------------------------- :root{ --bg:#f6f2ec; --surface:#ffffff; --ink:#22201c; --muted:#6b645a; --border:#e2d6c6; --accent:#b68b35; --accent-bright:#fef9c3; --radius-lg:22px; --whatsapp:#25d366; } *{box-sizing:border-box; -webkit-tap-highlight-color: transparent;} body{ margin:0; font-family:"DM Sans", sans-serif; background: radial-gradient(circle at top left,#fbf7f1 0,#f6f2ec 40%,#eee4d5 100%); color:var(--ink); line-height: 1.5; padding-bottom: 120px; } .page{ max-width:800px; margin:0 auto; padding: 0 20px; } header{ display:flex; justify-content:space-between; align-items:center; padding:16px 0; position:sticky; top:0; background:rgba(246,242,236,0.85); backdrop-filter:blur(12px); z-index:100; border-bottom: 1px solid rgba(0,0,0,0.05); } .brand{ display:flex; align-items:center; gap:12px; } .brand-logo{ height:38px; width:auto; } .brand-text{ display:flex; flex-direction:column; } .brand-wordmark{ font-size:16px; font-weight:700; letter-spacing:0.1em; text-transform:uppercase; } .brand-sub{ font-size:10px; letter-spacing:0.15em; color:var(--muted); text-transform:uppercase; } .hero-wrap{ margin: 20px 0; border-radius: var(--radius-lg); overflow: hidden; height: 280px; box-shadow: 0 12px 30px rgba(0,0,0,0.08); position:relative; } /* CAMINHO RELATIVO CORRETO: Sobe uma pasta (../) e entra em media */ .hero-photo-band{ width:100%; height:100%; background: #666 linear-gradient(to top, rgba(0,0,0,0.4), transparent), url("/portal/v2/assets/media/duovoicing.jpg") center/cover no-repeat; } .event-top{ background: var(--surface); border-radius: 20px; padding: 24px; border: 1px solid var(--border); margin-bottom: 32px; box-shadow: 0 12px 30px rgba(0,0,0,0.05); } .event-header-flex{ display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; border-bottom: 1px solid var(--border); padding-bottom: 15px; } .event-title-main { font-size: 22px; font-weight: 700; color: var(--ink); } .status-badge { background: #e3f9eb; color: #166534; padding: 6px 12px; border-radius: 99px; font-size: 12px; font-weight: 600; display: flex; align-items: center; gap: 6px; } .status-dot { width: 8px; height: 8px; background: #22c55e; border-radius: 50%; } .event-details-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } .detail-item label { display: block; font-size: 11px; text-transform: uppercase; color: var(--muted); letter-spacing: 1px; margin-bottom: 4px; } .detail-item span { font-weight: 600; font-size: 15px; } .section-label { font-size: 12px; font-weight: 700; color: var(--accent); letter-spacing: 2px; text-transform: uppercase; margin: 40px 0 12px; } .check-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; } .check-item { display: flex; gap: 10px; font-size: 14px; align-items: center; background: rgba(255,255,255,0.5); padding: 10px; border-radius: 12px; border: 1px solid transparent; } .check-icon { color: #16a34a; font-weight: bold; } .toggle-wrapper { display: flex; justify-content: center; margin: 30px 0 20px; } .toggle-container { background: #e2d6c6; padding: 4px; border-radius: 99px; display: inline-flex; position: relative; box-shadow: inset 0 2px 6px rgba(0,0,0,0.1); } .toggle-btn { padding: 10px 24px; border-radius: 99px; font-size: 13px; font-weight: 700; color: #6b645a; cursor: pointer; position: relative; z-index: 2; transition: 0.3s; user-select: none; } .toggle-btn.active { color: var(--accent); } .toggle-bg { position: absolute; top: 4px; left: 4px; height: calc(100% - 8px); width: 50%; background: #fff; border-radius: 99px; transition: 0.3s cubic-bezier(0.4, 0.0, 0.2, 1); box-shadow: 0 4px 10px rgba(0,0,0,0.1); z-index: 1; } .toggle-container[data-state="cocktail"] .toggle-bg { transform: translateX(96%); } .package-grid { display: flex; flex-direction: column; gap: 24px; margin-top: 10px; } .package-card { background: #1a1a1a; color: #fff; border-radius: 24px; overflow: hidden; display: flex; flex-direction: column; border: 1px solid #333; box-shadow: 0 20px 40px rgba(0,0,0,0.2); transition: transform 0.3s; } .package-content { padding: 30px; } .package-tag { font-size: 10px; letter-spacing: 2px; color: var(--accent-bright); text-transform: uppercase; margin-bottom: 8px; display: block; } .package-name { font-size: 24px; font-weight: 700; margin-bottom: 12px; color: #fff; } .package-desc { font-size: 14px; color: #ccc; margin-bottom: 20px; line-height: 1.6; } .price-block { border-top: 1px solid #333; padding-top: 20px; } .price-label { font-size: 11px; color: #999; text-transform: uppercase; display: flex; justify-content: space-between; } .price-tag-cocktail { color: var(--accent-bright); font-weight: bold; display: none; } .price-value { font-size: 32px; font-weight: 700; color: var(--accent-bright); margin: 6px 0 2px 0; transition: color 0.3s; } .price-sub { font-size: 12px; color: #4ade80; font-weight: 600; text-transform: uppercase; margin-bottom: 12px; letter-spacing: 0.5px; } .price-installments { font-size: 13px; color: #aaa; background: rgba(255,255,255,0.05); padding: 12px; border-radius: 8px; line-height: 1.5; border: 1px solid #333; } .price-installments strong { color: #fff; } /* AJUSTE DO VÍDEO DO CARD (Autoplay Style) */ .package-video-trigger { position: relative; height: 200px; cursor: pointer; border-bottom: 1px solid #333; overflow: hidden; background: #000; } .package-video-trigger video { width: 100%; height: 100%; object-fit: cover; object-position: center; position: absolute; top:0; left:0; opacity: 0.8; transition: opacity 0.3s; } .package-video-trigger:hover video { opacity: 1; } .video-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; transition: background 0.3s; z-index: 2; } .video-overlay:hover { background: rgba(0,0,0,0.1); } .play-btn { width: 56px; height: 56px; background: rgba(255,255,255,0.95); border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 20px rgba(0,0,0,0.4); } .play-icon { width: 0; height: 0; border-left: 18px solid var(--accent); border-top: 11px solid transparent; border-bottom: 11px solid transparent; margin-left: 4px; } .payment-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; margin-top: 16px; } .payment-card { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 18px; } .payment-chip { display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; color: var(--accent); margin-bottom: 8px; background: #fff8e6; padding: 4px 8px; border-radius: 4px; } .payment-text { font-size: 13px; color: var(--muted); line-height: 1.5; } .steps-section { margin-top: 50px; background: #fff; border-radius: 24px; padding: 30px; border: 1px solid var(--border); box-shadow: 0 10px 40px rgba(0,0,0,0.05); position:relative; overflow:hidden; } .steps-section::before { content:''; position:absolute; top:0; left:0; width:100%; height:6px; background: linear-gradient(90deg, #d4af37, #f6f2ec); } .timeline { position: relative; margin-top: 20px; padding-left: 20px; border-left: 2px solid #f0e6d8; } .timeline-item { position: relative; margin-bottom: 30px; padding-left: 25px; } .timeline-marker { position: absolute; left: -29px; top: 0; width: 16px; height: 16px; border-radius: 50%; background: #fff; border: 2px solid var(--accent); box-shadow: 0 0 0 4px #fff; z-index: 2; } .timeline-marker.active { background: var(--accent); box-shadow: 0 0 0 4px #fffbf0; } .t-title { font-weight: 700; font-size: 15px; color: var(--ink); margin-bottom: 4px; } .t-desc { font-size: 13px; color: var(--muted); line-height: 1.5; } .final-cta-box { text-align: center; margin-top: 30px; padding-top: 20px; border-top: 1px dashed #e2d6c6; } .secure-lock { font-size: 11px; color: #166534; display: flex; align-items: center; justify-content: center; gap: 6px; margin-bottom: 12px; background: #e3f9eb; display: inline-flex; padding: 4px 10px; border-radius: 99px; } .btn-primary { background: linear-gradient(135deg, #d4af37, #b68b35); color: #fff; border: none; padding: 16px 32px; border-radius: 99px; font-weight: 700; font-size: 15px; cursor: pointer; width: 100%; max-width: 400px; box-shadow: 0 8px 20px rgba(182, 139, 53, 0.35); text-transform: uppercase; letter-spacing: 0.5px; transition: transform 0.2s; } .btn-primary:active { transform: scale(0.98); } .wa-float { position: fixed; right: 20px; bottom: 20px; background: var(--whatsapp); color: #fff; width: 52px; height: 52px; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 5px 15px rgba(37,211,102,0.4); z-index: 999; text-decoration: none; } .modal { position:fixed; inset:0; background:rgba(0,0,0,0.9); display:none; align-items:center; justify-content:center; z-index:2000; padding:20px; } .modal-content { width:100%; max-width:800px; position:relative; } .modal-close { position:absolute; top:-40px; right:0; color:#fff; cursor:pointer; font-size:14px; } video { width:100%; border-radius:12px; outline:none; background:#000; } .dv-testimonials{margin-top:26px;} .dv-testimonials-card{border-radius:var(--radius-lg);border:1px solid var(--border);background:var(--surface);padding:18px 18px 24px;box-shadow:0 12px 30px rgba(0,0,0,0.05);} .dv-testimonials-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;} .dv-testimonials-top-line{height:2px;width:60px;background:var(--accent);} .dv-chip{display:inline-block;font-size:11px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--accent);background:#fff8e6;padding:4px 10px;border-radius:999px;border:1px solid rgba(0,0,0,0.06);} .dv-testimonials-content{display:flex;align-items:center;gap:10px;} .dv-test-nav-btn{width:28px;height:28px;border-radius:999px;border:1px solid var(--border);background:#fff;font-size:16px;cursor:pointer;color:var(--muted);} .dv-test-main{flex:1;} .dv-test-text{font-size:14px;line-height:1.7;margin:0;} .dv-test-meta{margin-top:10px;display:flex;justify-content:flex-start;align-items:center;gap:8px;font-size:12px;color:var(--muted);} .dv-test-meta-left{display:flex;align-items:center;gap:10px;} .dv-test-avatar{width:42px;height:42px;border-radius:50%;background-position:center;background-size:cover;background-repeat:no-repeat;background-color:#e5ded4;border:2px solid #fff;box-shadow:0 4px 12px rgba(0,0,0,0.18);flex-shrink:0;} .dv-test-meta-text{display:flex;flex-direction:column;gap:2px;} .dv-test-name{font-weight:700;color:var(--ink);font-size:13px;} .dv-test-label{font-size:11px;color:#9b9286;} .dv-test-dots{margin-top:10px;display:flex;justify-content:center;gap:4px;} .dv-test-dot{width:18px;height:2px;border-radius:999px;background:#e7dbcc;} .dv-test-dot.active{background:var(--accent);} .dv-btn-google-wrap { text-align: center; margin-top: 25px; } .dv-btn-google { display: inline-block; text-decoration: none; color: var(--accent); font-weight: 600; font-size: 13px; border: 1px solid var(--accent); padding: 8px 24px; border-radius: 50px; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.3s ease; font-family: "DM Sans", sans-serif; } .dv-btn-google:hover { background: var(--accent); color: #fff; } #agendaModal { position: fixed; inset: 0; z-index: 9999; display: none; align-items: center; justify-content: center; padding: 16px; background: rgba(0,0,0,0.55); } #agendaModal.dv-open { display: flex; } #agendaModal .agenda-content { background: #fff; border-radius: 16px; max-width: 900px; width: 100%; max-height: 90vh; box-shadow: 0 18px 40px rgba(0,0,0,0.3); display: flex; flex-direction: column; overflow: hidden; } #agendaModal .agenda-close { align-self: flex-end; padding: 10px 16px; font-size: 12px; letter-spacing: .08em; font-weight: 600; color: #666; cursor: pointer; } #agendaFrame { border: 0; width: 100%; height: 80vh; } /* --- CORREÇÕES DEFINITIVAS (iPhone/Mobile) --- */ .hero-photo-band { width: 100%; height: 100%; background-color: #666; /* Caminho Absoluto para garantir carregamento */ background-image: linear-gradient(to top, rgba(0,0,0,0.4), transparent), url("/portal/v2/assets/media/duovoicing.jpg"); background-position: center; background-size: cover; background-repeat: no-repeat; } .package-video-trigger { position: relative; height: 200px; cursor: pointer; border-bottom: 1px solid #333; overflow: hidden; background: #000; } .package-video-trigger video { width: 100%; height: 100%; object-fit: cover; object-position: center; position: absolute; top: 0; left: 0; opacity: 1; } -------------------------------------------------- ARQUIVO: api.php -------------------------------------------------- true, 'items'=>$service->checarData($_GET['date']??'', $_GET['id']??0)]); } elseif ($action === 'get_info') { echo json_encode($service->buscarPorId($_GET['id']??0) ?: []); } else { echo json_encode(['error'=>'Action invalida']); } } catch (Exception $e) { http_response_code(500); echo json_encode(['error'=>$e->getMessage()]); } -------------------------------------------------- ARQUIVO: wizard_modal.php -------------------------------------------------- -------------------------------------------------- ARQUIVO: logout.php -------------------------------------------------- prepare("SELECT * FROM propostas WHERE id = :id"); $stmt->bindValue(':id', $id, SQLITE3_INTEGER); $data = $stmt->execute()->fetchArray(SQLITE3_ASSOC); } catch (Exception $e) { die("Erro de conexão."); } if (!$data) die("Proposta não encontrada."); // --- LÓGICA DE "VALOR FECHADO" (IGUAL V1) --- // Se tiver valor_final (negociado), usa ele. Se não, usa o valor de tabela. $valorBase = !empty($data['valor_final']) ? $data['valor_final'] : $data['valor']; // Dados do Evento (Somente Leitura) $formacao = $data['formacao'] ?? 'Duo'; $temCoquetel = !empty($data['coquetel']); $dataEvento = $data['data_evento'] ?? date('Y-m-d'); // Formatação $meses = [1=>"Janeiro", 2=>"Fevereiro", 3=>"Março", 4=>"Abril", 5=>"Maio", 6=>"Junho", 7=>"Julho", 8=>"Agosto", 9=>"Setembro", 10=>"Outubro", 11=>"Novembro", 12=>"Dezembro"]; $ts = strtotime($dataEvento); $dataExtenso = date("d", $ts) . " de " . $meses[date("n", $ts)] . " de " . date("Y", $ts); ?> Contrato · Duo Voicing
Contrato de Serviços
Confirme seus dados para formalizar.
1
Seus Dados
Editar
Nome Completo
CPF
Nascimento
WhatsApp
Endereço (CEP)
Número
Complemento
2
Detalhes do Evento
Expandir
Data
Local
Formação
Coquetel
Responsabilidade da Sonorização
Duo Voicing (Incluso)
Levamos equipamento completo
Som do Local (-R$ 300)
Usamos o equipamento do evento
3
Pagamento
Expandir
Forma de Pagamento
Parcelamento no Cartão
R$ 0,00
Valor Final do Contrato
Status: Aguardando aceite
-------------------------------------------------- ARQUIVO: admin_preparar.php -------------------------------------------------- prepare("SELECT * FROM propostas WHERE id = :id"); $stmt->bindValue(':id', $id, SQLITE3_INTEGER); $data = $stmt->execute()->fetchArray(SQLITE3_ASSOC); } catch (Exception $e) { die("Erro de conexão."); } if (!$data) die("Proposta não encontrada."); // Helpers $valorRaw = $data['valor'] ?? '0'; $valorBase = (float)str_replace(',', '.', str_replace('.', '', preg_replace('/[^0-9,.]/', '', $valorRaw))); $valorFinal = !empty($data['valor_final']) ? $data['valor_final'] : $valorBase; $dataEvento = $data['data_evento'] ?? date('Y-m-d'); // --- PROCESSAR SALVAMENTO --- if ($_SERVER['REQUEST_METHOD'] === 'POST') { header('Content-Type: application/json'); try { $valFinal = (float)str_replace(',', '.', str_replace('.', '', str_replace('R$ ', '', $_POST['valor_final']))); $upd = [ 'nome_casal' => $_POST['nome_resp'], 'cpf' => $_POST['cpf'], 'nascimento' => $_POST['nascimento'], 'whatsapp' => $_POST['telefone'], 'cep' => $_POST['cep'], 'endereco' => $_POST['endereco'], 'numero' => $_POST['numero'], 'complemento' => $_POST['complemento'], 'data_evento' => $_POST['data_evento'], 'hora_inicio' => $_POST['hora_inicio'], 'hora_fim' => $_POST['hora_fim'], 'local_nome' => $_POST['local_nome'], 'local_endereco' => $_POST['local_endereco'], 'formacao' => $_POST['formacao'], 'resp_som' => $_POST['resp_som'], 'coquetel' => isset($_POST['coquetel']) ? 1 : 0, 'hora_coquetel' => $_POST['hora_coquetel'], 'valor_final' => $valFinal, 'forma_pagamento' => $_POST['pagamento'], 'qtd_parcelas' => $_POST['qtd_parcelas'] ?? 1, 'dia_vencimento' => $_POST['dia_vencimento'] ?? 10 ]; $cols = []; foreach($upd as $k => $v) $cols[] = "$k = :$k"; $sql = "UPDATE propostas SET ".implode(', ', $cols)." WHERE id = :id"; $stmtUpdate = $db->prepare($sql); $stmtUpdate->bindValue(':id', $id, SQLITE3_INTEGER); foreach($upd as $k => $v) $stmtUpdate->bindValue(":$k", $v); $stmtUpdate->execute(); echo json_encode(['ok' => true]); } catch (Exception $e) { echo json_encode(['ok' => false, 'message' => $e->getMessage()]); } exit; } ?> Preparar Proposta
Duo Voicing
Painel Administrativo
1
O Evento
Expandir
Data do Evento
Horário da Cerimônia
Término previsto:
(+30min de tolerância)
Local da Cerimônia
Formação Musical
Duo
Voz+Violão
Trio
+Violino
Quarteto
+Cello
Sonorização
Duo Voicing
Nós levamos
Local
Do evento
Incluir Coquetel
Adiciona 1h de música
Horário do Coquetel
Término previsto: --:--
(1h de duração)
2
Financeiro
Expandir
Valor Final (Clique para editar)
Forma de Pagamento
Boleto/Pix
Cartão
À Vista
Qtd. Parcelas
Dia Venc.
3
Contratante
Editar
Nome Completo
CPF
WhatsApp
Endereço (CEP)
Número
Complemento
Admin:
Confirme os dados
-------------------------------------------------- ARQUIVO: index.php -------------------------------------------------- 20) ? 100 : 130; escrever($im, $fontSize, 400, $brown, $fontScript, $nome, $w); // 4. DATA if (!empty($dataRaw)) { $meses = ['01'=>'Janeiro','02'=>'Fevereiro','03'=>'Março','04'=>'Abril','05'=>'Maio','06'=>'Junho','07'=>'Julho','08'=>'Agosto','09'=>'Setembro','10'=>'Outubro','11'=>'Novembro','12'=>'Dezembro']; if(strpos($dataRaw, '-') !== false) { $parts = explode('-', $dataRaw); if(count($parts) == 3) { $d = $parts[2]; $m = $meses[$parts[1]] ?? ''; $a = $parts[0]; $txtData = "$d de $m de $a"; } else { $txtData = $dataRaw; } } elseif(count(explode('/', $dataRaw)) == 3) { $parts = explode('/', $dataRaw); $d = $parts[0]; $m = $meses[$parts[1]] ?? ''; $a = $parts[2]; $txtData = "$d de $m de $a"; } else { $txtData = $dataRaw; } escrever($im, 22, 530, $black, $fontSerif, $txtData, $w); } imagejpeg($im, null, 90); imagedestroy($im); ?> -------------------------------------------------- ARQUIVO: api_reviews.php -------------------------------------------------- $r['authorAttribution']['displayName'] ?? 'Cliente', 'label' => 'Avaliação no Google', 'text' => $text, 'img' => $r['authorAttribution']['photoUri'] ?? '', 'link' => $r['authorAttribution']['uri'] ?? '' // <--- O PULO DO GATO AQUI ]; } } // Fallback de cache antigo se a API falhar if (empty($output) && file_exists($cacheFile)) { echo file_get_contents($cacheFile); exit; } $jsonFinal = json_encode($output); file_put_contents($cacheFile, $jsonFinal); echo $jsonFinal; ?> -------------------------------------------------- ARQUIVO: config.php -------------------------------------------------- __DIR__ . '/duo_v2.db', // 2. Chave do Google Maps 'google_maps_key' => 'AIzaSyB3Kv7rZa-wha_WdiOMPKVfd5NS4MjJqXw', // 3. Login do Admin 'admin_user' => 'admin', 'admin_pass' => '123456' ]; -------------------------------------------------- ARQUIVO: proposta.php -------------------------------------------------- prepararVisualizacao($id); if (!$data) die("

Proposta não encontrada.

"); // --- RASTREADOR DE VISITAS --- try { $dbLog = new SQLite3(__DIR__ . "/duo_v2.db"); $dbLog->busyTimeout(2000); $ua = $_SERVER["HTTP_USER_AGENT"] ?? ""; $ip = $_SERVER["REMOTE_ADDR"] ?? ""; // Grava o acesso $stmt = $dbLog->prepare("INSERT INTO acessos (proposta_id, ip, user_agent) VALUES (:pid, :ip, :ua)"); $stmt->bindValue(":pid", $id, SQLITE3_INTEGER); $stmt->bindValue(":ip", $ip, SQLITE3_TEXT); $stmt->bindValue(":ua", $ua, SQLITE3_TEXT); $stmt->execute(); $dbLog->close(); } catch(Exception $e) { /* Falha silenciosa para não travar o site */ } // ----------------------------- // --- 1. CONFIGURAÇÃO --- $pacotes = [ 'Duo' => [ 'tag' => 'Essencial', 'titulo' => 'Duo: Voz e Violão', 'video' => 'Duo.mp4', 'desc' => 'Ideal para casamentos intimistas onde a conexão e a letra da música são os protagonistas.' ], 'Trio' => [ 'tag' => 'Clássico', 'titulo' => 'Trio: Com Violino', 'video' => 'Trio.mp4', 'desc' => 'A emoção clássica do violino somada à base acolhedora do violão.' ], 'Quarteto' => [ 'tag' => 'Premium', 'titulo' => 'Quarteto: Com Cello', 'video' => 'Quarteto.mp4', 'desc' => 'Voz, violão, violino e violoncelo. Camadas profundas de som com timbres clássicos.' ] ]; // --- 2. HELPERS --- function h($s) { return htmlspecialchars($s ?? '', ENT_QUOTES, 'UTF-8'); } function money($v) { return 'R$ ' . number_format($v, 2, ',', '.'); } function fmtDate($d) { return date('d/m/Y', strtotime($d)); } // --- 3. LÓGICA VISUAL --- $semEntrada = ($data['sem_entrada'] == 1); $validade = date('d/m/Y', strtotime($data['created_at'] . ' + 15 days')); $mediaUrl = "/portal/v2/assets/media"; $txtParcelado = $semEntrada ? "Sem entrada + saldo parcelado (até 15 dias antes)." : "30% de entrada + saldo parcelado (até 15 dias antes)."; $v = time(); // --- 4. GERAÇÃO DA IMAGEM E TEXTOS (OPEN GRAPH) --- // Imagem gerada dinamicamente $nomeLimpo = trim($data['nome_casal']); // Remove espaços extras do final $nomeUrl = urlencode($nomeLimpo); $dataUrl = urlencode(fmtDate($data['data_evento'])); $ogImage = "https://duovoicing.com.br/portal/v2/og.php?nome=$nomeUrl&data=$dataUrl&v=$v"; // Textos Personalizados para o WhatsApp (Com trim para colar a vírgula) $ogTitle = "Proposta Especial (20% OFF)"; $ogDesc = h($nomeLimpo) . ", esse é o início de algo muito especial. Ficamos felizes em considerar nossa proposta para contemplar esse momento inesquecível"; ?> Proposta <?= h($nomeLimpo) ?>
Duo Voicing
Música para Casamentos
Proposta personalizada
Aberto
Até
Google Avaliações ★★★★★

Carregando...

Só Cerimônia
+ Coquetel (1h)
$pkg): $val = $data['ofertas'][$key]; ?>

InvestimentoCOM COQUETEL
À VISTA NO PIX (20% DE DESCONTO)
...
PIX À VISTA
20% de desconto na quitação imediata.
PARCELADO
CARTÃO
Em até 12x (com taxas aplicadas).
Ambiente seguro e processo transparente

Prontos para dar o próximo passo?

-------------------------------------------------- ARQUIVO: admin.php -------------------------------------------------- criar($_POST); $_SESSION['msg']=['tipo'=>'success','texto'=>'Proposta criada!']; } elseif ($action === 'update' && $id) { $app->atualizar($id, $_POST); $_SESSION['msg']=['tipo'=>'success','texto'=>'Atualizado!']; } elseif ($action === 'delete' && $id) { $app->excluir($id); $_SESSION['msg']=['tipo'=>'success','texto'=>'Removido.']; } elseif ($action === 'mass_delete' && !empty($ids)) { $count = 0; foreach(explode(',', $ids) as $massId) { if(is_numeric($massId)) { $app->excluir($massId); $count++; } } $_SESSION['msg']=['tipo'=>'success','texto'=>"$count propostas removidas."]; } } catch (Exception $e) { $_SESSION['msg'] = ['tipo'=>'danger', 'texto'=>'Erro: '.$e->getMessage()]; } header("Location: admin.php"); exit; } // --- DADOS --- $list = $app->listar(); $dbView = new SQLite3(__DIR__ . "/duo_v2.db"); $vRes = $dbView->query("SELECT proposta_id, COUNT(*) as c FROM acessos GROUP BY proposta_id"); $viewsCount = []; while($r = $vRes->fetchArray()) { $viewsCount[$r["proposta_id"]] = $r["c"]; } $dbView->close(); // --- SORTING --- $sort = $_GET['sort'] ?? 'recent'; usort($list, function($a, $b) use ($sort, $viewsCount) { if ($sort === 'views') { $va = $viewsCount[$a['id']] ?? 0; $vb = $viewsCount[$b['id']] ?? 0; return $vb <=> $va; } elseif ($sort === 'value') { return cleanVal($b['valor']) <=> cleanVal($a['valor']); } else { return $b['id'] <=> $a['id']; } }); // --- ABAS --- $tabAberto = []; $tabAvancadas = []; $tabFinalizadas = []; $hoje = date('Y-m-d'); foreach($list as $p) { if ($p['status'] === 'assinado') $tabFinalizadas[] = $p; elseif ($p['status'] === 'contrato') $tabAvancadas[] = $p; elseif ($p['data_evento'] < $hoje) $tabFinalizadas[] = $p; else $tabAberto[] = $p; } $msg = $_SESSION['msg'] ?? null; unset($_SESSION['msg']); // --- RENDER CARD --- function renderCard($p, $viewsCount) { $views = $viewsCount[$p['id']] ?? 0; ?>
SEM ENTRADA CUPOM EXPIRADO FECHADO
V2 Admin
Duo Voicing
Música para Casamentos
Nenhuma proposta em aberto.
Nenhuma em contrato.
Nenhuma finalizada.
+
0 selecionados
Cancelar Excluir
-------------------------------------------------- ARQUIVO: src/Database.php -------------------------------------------------- enableExceptions(true); self::$instance->busyTimeout(5000); } catch (Exception $e) { die("❌ ERRO DE CONEXÃO: Não foi possível abrir o banco V2 em: $dbPath
Detalhe: " . $e->getMessage()); } } return self::$instance; } } -------------------------------------------------- ARQUIVO: src/Proposta.php -------------------------------------------------- db = Database::connect(); } // --- MÉTODOS CRUD (MANTIDOS) --- public function listar() { $res = $this->db->query("SELECT * FROM propostas ORDER BY created_at DESC"); $lista = []; while ($row = $res->fetchArray(SQLITE3_ASSOC)) { $row['valor'] = 'R$ ' . number_format($row['valor_total_final'], 2, ',', '.'); $row['tem_cupom'] = $row['cupom_ativo']; $lista[] = $row; } return $lista; } public function criar($d) { $dados = $this->processarRegras($d); $sql = "INSERT INTO propostas (data_evento, nome_casal, local_evento, sem_entrada, cupom_ativo, cupom_porcentagem, valor_cache_duo, valor_instrumento_adicional, valor_logistica, valor_total_final, valor_entrada, valor_final, data_pagamento_final, numero_parcelas, created_at) VALUES (:dt, :nm, :loc, :se, :cup, :perc, :vduo, :vinst, :vlog, :vtot, :ventrada, :vfinal, :dpag, :num_parc, :criado)"; $stmt = $this->db->prepare($sql); $this->bind($stmt, $dados); $stmt->bindValue(':criado', date('Y-m-d H:i:s')); return $stmt->execute(); } public function atualizar($id, $d) { $dados = $this->processarRegras($d); $sql = "UPDATE propostas SET data_evento=:dt, nome_casal=:nm, local_evento=:loc, sem_entrada=:se, cupom_ativo=:cup, cupom_porcentagem=:perc, valor_cache_duo=:vduo, valor_instrumento_adicional=:vinst, valor_logistica=:vlog, valor_total_final=:vtot, valor_entrada=:ventrada, valor_final=:vfinal, data_pagamento_final=:dpag, numero_parcelas=:num_parc WHERE id=:id"; $stmt = $this->db->prepare($sql); $this->bind($stmt, $dados); $stmt->bindValue(':id', $id, SQLITE3_INTEGER); return $stmt->execute(); } public function excluir($id) { $stmt = $this->db->prepare("DELETE FROM propostas WHERE id = :id"); $stmt->bindValue(':id', $id, SQLITE3_INTEGER); return $stmt->execute(); } public function buscarPorId($id) { $stmt = $this->db->prepare("SELECT * FROM propostas WHERE id = :id"); $stmt->bindValue(':id', $id, SQLITE3_INTEGER); $res = $stmt->execute(); $row = $res->fetchArray(SQLITE3_ASSOC); if ($row) { $fmt = fn($v) => 'R$ ' . number_format($v, 2, ',', '.'); $row['valor_display'] = $fmt($row['valor_total_final']); $row['base_cache'] = $fmt($row['valor_cache_duo']); $row['base_inst'] = $fmt($row['valor_instrumento_adicional']); $row['base_log'] = $fmt($row['valor_logistica']); $row['tem_cupom'] = $row['cupom_ativo']; $row['cupom_perc'] = $row['cupom_porcentagem']; } return $row; } // --- NOVA FUNÇÃO: PREPARAR DADOS PARA O SITE (VISUAL) --- // Centraliza a matemática financeira para exibição public function prepararVisualizacao($id) { $data = $this->buscarPorId($id); if (!$data) return null; // 1. Cálculos Base $baseDuo = $data['valor_cache_duo'] + $data['valor_logistica']; $vInst = $data['valor_instrumento_adicional']; $pkgs = [ 'Duo' => $baseDuo, 'Trio' => $baseDuo + $vInst, 'Quarteto' => $baseDuo + ($vInst * 2) ]; // 2. Processa Totais e Parcelas $semEntrada = ($data['sem_entrada'] == 1); $dataEventoStr = $data['data_evento']; // Simula parcelamento (Lógica V1) $maxParcelas = 10; if (!empty($dataEventoStr)) { try { $hoje = new DateTime('today'); $limite = new DateTime($dataEventoStr); $limite->modify('-15 days'); $meses = ($hoje->diff($limite)->y * 12) + $hoje->diff($limite)->m; if ($limite <= $hoje) $meses = 1; $maxParcelas = max(1, min(12, $meses)); } catch (Exception $e) { $maxParcelas = 6; } } $ofertas = []; foreach($pkgs as $nome => $totalBruto) { $total = round($totalBruto / 100) * 100; $vista = round($total * 0.80 / 100) * 100; // 20% OFF if ($semEntrada) { $entrada = 0; $saldo = $total; } else { $entrada = round($total * 0.30 / 100) * 100; $saldo = max(0, $total - $entrada); } $valorParcela = ($maxParcelas > 0) ? ($saldo / $maxParcelas) : $saldo; $ofertas[$nome] = [ 'total' => $total, 'vista' => $vista, 'entrada' => $entrada, 'parcela' => $valorParcela, 'num_parcelas' => $maxParcelas ]; } $data['ofertas'] = $ofertas; return $data; } // --- MÉTODOS PRIVADOS --- private function processarRegras($post) { $money = fn($v) => (float)str_replace(['R$', '.', ','], ['', '', '.'], $post[$v] ?? '0'); $dataEvento = $post['data']; $valorTotal = $money('valor'); $semEntrada = isset($post['sem_entrada']); $dataPagamento = $this->calcDiasUteisReverso($dataEvento, 15); $numParcelas = 1; if ($semEntrada) { $hoje = new DateTime(); $limite = new DateTime($dataPagamento); $intervalo = $hoje->diff($limite); $mesesDisponiveis = ($intervalo->y * 12) + $intervalo->m; if ($limite <= $hoje) $mesesDisponiveis = 1; $numParcelas = ($mesesDisponiveis >= 12) ? 12 : max(1, $mesesDisponiveis); $valorEntrada = 0; $valorFinal = $valorTotal; } else { $valorEntrada = $valorTotal * 0.30; $valorFinal = $valorTotal - $valorEntrada; } return [ 'dt' => $dataEvento, 'nm' => $post['nome'], 'loc' => $post['local'], 'se' => $semEntrada?1:0, 'cup' => isset($post['tem_cupom'])?1:0, 'perc' => (int)($post['cupom_perc']??0), 'vduo' => $money('base_cache'), 'vinst' => $money('valor_instrumento'), 'vlog' => $money('valor_logistica'), 'vtot' => $valorTotal, 'ventrada' => $valorEntrada, 'vfinal' => $valorFinal, 'dpag' => $dataPagamento, 'num_parc' => $numParcelas ]; } public function checarData($dt, $ign=0) { $i=[]; $s=$this->db->prepare("SELECT nome_casal, local_evento, valor_total_final, status FROM propostas WHERE data_evento=:d AND id!=:id"); $s->bindValue(':d',$dt); $s->bindValue(':id',$ign); $r=$s->execute(); while($w=$r->fetchArray(SQLITE3_ASSOC)) { $i[]=['titulo'=>$w['nome_casal'], 'local'=>$w['local_evento'], 'valor'=>'R$ '.number_format($w['valor_total_final'],2,',','.'), 'status'=>$w['status']]; } return $i; } private function bind($stmt, $d) { $stmt->bindValue(':dt',$d['dt']); $stmt->bindValue(':nm',$d['nm']); $stmt->bindValue(':loc',$d['loc']); $stmt->bindValue(':se',$d['se']); $stmt->bindValue(':cup',$d['cup']); $stmt->bindValue(':perc',$d['perc']); $stmt->bindValue(':vduo',$d['vduo']); $stmt->bindValue(':vinst',$d['vinst']); $stmt->bindValue(':vlog',$d['vlog']); $stmt->bindValue(':vtot',$d['vtot']); $stmt->bindValue(':ventrada',$d['ventrada']); $stmt->bindValue(':vfinal',$d['vfinal']); $stmt->bindValue(':dpag',$d['dpag']); $stmt->bindValue(':num_parc',$d['num_parc']); } private function calcDiasUteisReverso($dataStr, $dias) { if(!$dataStr) return null; try { $d = new DateTime($dataStr); $c=0; while($c < $dias){ $d->modify('-1 day'); if($d->format('N') < 6) $c++; } return $d->format('Y-m-d'); } catch (Exception $e) { return null; } } } -------------------------------------------------- ARQUIVO: get_views.php -------------------------------------------------- 'Acesso negado'])); } $id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT); if(!$id) exit(json_encode([])); $db = new SQLite3(__DIR__ . '/duo_v2.db'); $db->busyTimeout(5000); // Busca os acessos mais recentes primeiro $res = $db->query("SELECT * FROM acessos WHERE proposta_id = $id ORDER BY id DESC"); $log = []; while ($row = $res->fetchArray(SQLITE3_ASSOC)) { // Detecta Dispositivo (Lógica herdada da V1) $ua = $row['user_agent']; $device = 'PC/Outro'; if (stripos($ua, 'iPhone') !== false) $device = 'iPhone'; elseif (stripos($ua, 'Android') !== false) $device = 'Android'; elseif (stripos($ua, 'Windows') !== false) $device = 'Windows PC'; elseif (stripos($ua, 'Macintosh') !== false) $device = 'Mac'; elseif (stripos($ua, 'Linux') !== false) $device = 'Linux'; elseif (stripos($ua, 'Instagram') !== false) $device = 'Instagram App'; $row['device'] = $device; // Formata data (d/m H:i) $row['data_fmt'] = date('d/m H:i', strtotime($row['created_at'])); $log[] = $row; } header('Content-Type: application/json'); echo json_encode($log); ?>